tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

test_eme_protection_query.html (8504B)


      1 <!DOCTYPE html>
      2 <html>
      3 <head>
      4  <title>Test Encrypted Media Extensions - Protection Query</title>
      5  <script src="/tests/SimpleTest/SimpleTest.js"></script>
      6  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
      7  <script type="text/javascript" src="manifest.js"></script>
      8  <script type="text/javascript" src="eme.js"></script>
      9 </head>
     10 <body>
     11 <pre id="test">
     12 <script class="testbody" type="text/javascript">
     13 // Tests in this file check that output protection queries are performed and
     14 // handled correctly. This is done by using a special clear key key system that
     15 // emits key status to track protection status.
     16 
     17 // Special key system used for these tests.
     18 const kClearKeyWithProtectionQuery =
     19  "org.mozilla.clearkey_with_protection_query";
     20 const kTestFile = "bipbop-cenc-video-10s.mp4";
     21 const kTestMimeType = 'video/mp4; codecs="avc1.4d4015"';
     22 const kTestKeyId = "7e571d037e571d037e571d037e571d11"; // Hex representation
     23 const kTestKey = "7e5733337e5733337e5733337e573311";
     24 
     25 // This is the special key-id used by the mozilla clearkey CDM to signal
     26 // protection query status. As hex it is "6f75747075742d70726f74656374696f6e",
     27 // the hex translates to ascii "output-protection".
     28 const kProtectionQueryKeyIdString = "output-protection";
     29 
     30 // Options for requestMediaKeySystemAccess
     31 const kKeySystemOptions = [
     32  {
     33    initDataTypes: ["cenc"],
     34    videoCapabilities: [{ contentType: kTestMimeType }],
     35  },
     36 ];
     37 
     38 // Helper to setup EME on `video`.
     39 // @param video the HTMLMediaElement to configure EME on.
     40 // @returns a media key session for the video. Callers can use this to
     41 // configure the `onkeystatuseschange` event handler for each test. Callers
     42 // *should not* configure other aspects of the session as this helper already
     43 // does so.
     44 async function setupEme(video) {
     45  // Start setting up EME.
     46  let access = await navigator.requestMediaKeySystemAccess(
     47    kClearKeyWithProtectionQuery,
     48    kKeySystemOptions
     49  );
     50  let mediaKeys = await access.createMediaKeys();
     51  await video.setMediaKeys(mediaKeys);
     52 
     53  let session = video.mediaKeys.createSession();
     54 
     55  video.onencrypted = async encryptedEvent => {
     56    session.onmessage = () => {
     57      // Handle license messages. Hard code the license because we always test
     58      // with the same file and we know what the license should be.
     59      const license = {
     60        keys: [
     61          {
     62            kty: "oct",
     63            kid: HexToBase64(kTestKeyId),
     64            k: HexToBase64(kTestKey),
     65          },
     66        ],
     67        type: "temporary",
     68      };
     69 
     70      const encodedLicense = new TextEncoder().encode(JSON.stringify(license));
     71 
     72      session.update(encodedLicense);
     73    };
     74 
     75    session.generateRequest(
     76      encryptedEvent.initDataType,
     77      encryptedEvent.initData
     78    );
     79  };
     80 
     81  return session;
     82 }
     83 
     84 // Helper to setup MSE media on `video`.
     85 // @param video the HTMLMediaElement to configure MSE on.
     86 async function setupMse(video) {
     87  const mediaSource = new MediaSource();
     88  video.src = URL.createObjectURL(mediaSource);
     89  await once(mediaSource, "sourceopen");
     90  const sourceBuffer = mediaSource.addSourceBuffer("video/mp4");
     91  let fetchResponse = await fetch(kTestFile);
     92  sourceBuffer.appendBuffer(await fetchResponse.arrayBuffer());
     93  await once(sourceBuffer, "updateend");
     94  mediaSource.endOfStream();
     95  await once(mediaSource, "sourceended");
     96 }
     97 
     98 // Helper to create a video element and append it to the page.
     99 function createAndAppendVideo() {
    100  const video = document.createElement("video");
    101  video.id = "video";
    102  // Loop in case tests run slowly, we want video to keep playing until we
    103  // get expected events.
    104  video.loop = true;
    105  document.body.appendChild(video);
    106  return video;
    107 }
    108 
    109 // Helper to remove a video from the page.
    110 function removeVideo() {
    111  let video = document.getElementById("video");
    112  CleanUpMedia(video);
    113 }
    114 
    115 // Helper to get the status for the kProtectionQueryKeyIdString key id. A
    116 // session can (and will) have other keys with their own status, but we want
    117 // to check this special key to find the protection query status.
    118 function getKeyStatusForProtectionKeyId(session) {
    119  for (let [keyId, status] of session.keyStatuses) {
    120    if (ArrayBufferToString(keyId) == kProtectionQueryKeyIdString) {
    121      return status;
    122    }
    123  }
    124  return null;
    125 }
    126 
    127 async function getDisplayMedia() {
    128  SpecialPowers.wrap(document).notifyUserGestureActivation();
    129  return navigator.mediaDevices.getDisplayMedia();
    130 }
    131 
    132 // Tests playing encrypted media, starting a screen capture during playback,
    133 // then stopping the capture while playback continues.
    134 async function testProtectionQueryWithCaptureDuringVideo() {
    135  let video = createAndAppendVideo();
    136 
    137  // Configure the video and start it playing. KeyId should be usable (not restricted).
    138  let session = await setupEme(video);
    139  let keyStatusChangedPromise1 = new Promise(
    140    resolve =>
    141      (session.onkeystatuseschange = () => {
    142        // We may get status changes prior to kProtectionQueryKeyIdString changing,
    143        // ensure we wait for the first kProtectionQueryKeyIdString change.
    144        if (getKeyStatusForProtectionKeyId(session)) {
    145          resolve();
    146        }
    147      })
    148  );
    149  await setupMse(video);
    150  await Promise.all([video.play(), keyStatusChangedPromise1]);
    151  is(
    152    getKeyStatusForProtectionKeyId(session),
    153    "usable",
    154    "Should be usable as capture hasn't started"
    155  );
    156 
    157  let keyStatusChangedPromise2 = new Promise(
    158    resolve => (session.onkeystatuseschange = resolve)
    159  );
    160  let [displayMediaStream] = await Promise.all([
    161    // Start a screen capture, this should restrict output.
    162    getDisplayMedia(),
    163    keyStatusChangedPromise2,
    164  ]);
    165  is(
    166    getKeyStatusForProtectionKeyId(session),
    167    "output-restricted",
    168    "Should be output-restricted as capture is happening"
    169  );
    170 
    171  // Stop the screen capture, output should be usable again.
    172  let keyStatusChangedPromise3 = new Promise(
    173    resolve => (session.onkeystatuseschange = resolve)
    174  );
    175  displayMediaStream.getTracks().forEach(track => track.stop());
    176  displayMediaStream = null;
    177  await keyStatusChangedPromise3;
    178  is(
    179    getKeyStatusForProtectionKeyId(session),
    180    "usable",
    181    "Should be usable as capture has stopped"
    182  );
    183 
    184  removeVideo();
    185 }
    186 
    187 // Tests starting a screen capture, then starting encrypted playback, then
    188 // stopping the screen capture while encrypted playback continues.
    189 async function testProtectionQueryWithCaptureStartingBeforeVideo() {
    190  // Start capture before setting up video.
    191  let displayMediaStream = await getDisplayMedia();
    192 
    193  let video = createAndAppendVideo();
    194 
    195  // Configure the video and start it playing. KeyId should be restricted already.
    196  let session = await setupEme(video);
    197  let keyStatusChangedPromise1 = new Promise(
    198    resolve =>
    199      (session.onkeystatuseschange = () => {
    200        // We may get status changes prior to kProtectionQueryKeyIdString changing,
    201        // ensure we wait for the first kProtectionQueryKeyIdString change. In
    202        // rare cases the first protection status can be "usable" due to racing
    203        // between playback and the machinery that detects WebRTC capture. To
    204        // avoid this, wait for the first 'output-restricted' notification,
    205        // which will either be the first event, or will quickly follow 'usable'.
    206        if (getKeyStatusForProtectionKeyId(session) == "output-restricted") {
    207          resolve();
    208        }
    209      })
    210  );
    211  await setupMse(video);
    212  await Promise.all([video.play(), keyStatusChangedPromise1]);
    213  is(
    214    getKeyStatusForProtectionKeyId(session),
    215    "output-restricted",
    216    "Should be restricted as capture is happening"
    217  );
    218 
    219  // Stop the screen capture, output should be usable again.
    220  let keyStatusChangedPromise2 = new Promise(
    221    resolve => (session.onkeystatuseschange = resolve)
    222  );
    223  displayMediaStream.getTracks().forEach(track => track.stop());
    224  displayMediaStream = null;
    225  await keyStatusChangedPromise2;
    226  is(
    227    getKeyStatusForProtectionKeyId(session),
    228    "usable",
    229    "Should be usable as capture has stopped"
    230  );
    231 
    232  removeVideo();
    233 }
    234 
    235 add_task(async function setupEnvironment() {
    236  await SpecialPowers.pushPrefEnv({
    237    set: [
    238      // Need test key systems for test key system.
    239      ["media.clearkey.test-key-systems.enabled", true],
    240      // Need relaxed navigator permissions for getDisplayMedia.
    241      ["media.navigator.permission.disabled", true],
    242    ],
    243  });
    244 });
    245 add_task(testProtectionQueryWithCaptureDuringVideo);
    246 add_task(testProtectionQueryWithCaptureStartingBeforeVideo);
    247 </script>
    248 </pre>
    249 </body>
    250 </html>