Bug 1071482 - Make content encrypted via EME inaccessible from JS APIs. r=roc
authorChris Pearce <cpearce@mozilla.com>
Tue, 28 Oct 2014 13:21:12 +1300
changeset 213053 971beced9390898a5cad30a5b963d4565435f1f6
parent 213052 396575d789e430d815fd2ab1126c165db19d2629
child 213054 8839a3db64216f6ee1853649ee47d1c53a4d4a77
push id27738
push usercbook@mozilla.com
push dateThu, 30 Oct 2014 13:46:07 +0000
treeherdermozilla-central@1aa1b23d799e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersroc
bugs1071482
milestone36.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1071482 - Make content encrypted via EME inaccessible from JS APIs. r=roc
dom/html/HTMLMediaElement.cpp
dom/html/HTMLMediaElement.h
dom/media/test/eme.js
dom/media/test/mochitest.ini
dom/media/test/test_eme_canvas_blocked.html
dom/media/test/test_eme_stream_capture_blocked.html
dom/media/webaudio/AudioContext.cpp
layout/base/nsLayoutUtils.cpp
--- a/dom/html/HTMLMediaElement.cpp
+++ b/dom/html/HTMLMediaElement.cpp
@@ -1782,17 +1782,21 @@ NS_IMETHODIMP HTMLMediaElement::SetMuted
 
 already_AddRefed<DOMMediaStream>
 HTMLMediaElement::CaptureStreamInternal(bool aFinishWhenEnded)
 {
   nsIDOMWindow* window = OwnerDoc()->GetInnerWindow();
   if (!window) {
     return nullptr;
   }
-
+#ifdef MOZ_EME
+  if (ContainsRestrictedContent()) {
+    return nullptr;
+  }
+#endif
   OutputMediaStream* out = mOutputStreams.AppendElement();
 #ifdef DEBUG
   // Estimate hints based on the type of the media element
   // under the preference media.capturestream_hints for the
   // debug builds only. This allows WebRTC Peer Connection
   // to behave appropriately when media streams generated
   // via mozCaptureStream*() are added to the Peer Connection.
   // This functionality is planned to be used as part of Audio
@@ -3987,20 +3991,31 @@ NS_IMETHODIMP HTMLMediaElement::CanPlayC
 
 #ifdef MOZ_EME
 MediaKeys*
 HTMLMediaElement::GetMediaKeys() const
 {
   return mMediaKeys;
 }
 
+bool
+HTMLMediaElement::ContainsRestrictedContent()
+{
+  return GetMediaKeys() != nullptr;
+}
+
 already_AddRefed<Promise>
 HTMLMediaElement::SetMediaKeys(mozilla::dom::MediaKeys* aMediaKeys,
                                ErrorResult& aRv)
 {
+  if (MozAudioCaptured()) {
+    aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+    return nullptr;
+  }
+
   nsCOMPtr<nsIGlobalObject> global =
     do_QueryInterface(OwnerDoc()->GetInnerWindow());
   if (!global) {
     aRv.Throw(NS_ERROR_UNEXPECTED);
     return nullptr;
   }
   nsRefPtr<Promise> promise = Promise::Create(global, aRv);
   if (aRv.Failed()) {
@@ -4025,16 +4040,18 @@ HTMLMediaElement::SetMediaKeys(mozilla::
     if (NS_FAILED(mMediaKeys->Bind(this))) {
       promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
       mMediaKeys = nullptr;
       return promise.forget();
     }
     if (mDecoder) {
       mDecoder->SetCDMProxy(mMediaKeys->GetCDMProxy());
     }
+    // Update the same-origin status.
+    NotifyDecoderPrincipalChanged();
   }
   promise->MaybeResolve(JS::UndefinedHandleValue);
   return promise.forget();
 }
 
 MediaWaitingFor
 HTMLMediaElement::WaitingFor() const
 {
--- a/dom/html/HTMLMediaElement.h
+++ b/dom/html/HTMLMediaElement.h
@@ -545,16 +545,17 @@ public:
 
 
   bool IsEventAttributeName(nsIAtom* aName) MOZ_OVERRIDE;
 
   // Returns the principal of the "top level" document; the origin displayed
   // in the URL bar of the browser window.
   already_AddRefed<nsIPrincipal> GetTopLevelPrincipal();
 
+  bool ContainsRestrictedContent();
 #endif // MOZ_EME
 
   bool MozAutoplayEnabled() const
   {
     return mAutoplayEnabled;
   }
 
   already_AddRefed<DOMMediaStream> MozCaptureStream(ErrorResult& aRv);
--- a/dom/media/test/eme.js
+++ b/dom/media/test/eme.js
@@ -145,16 +145,20 @@ function LoadTest(test, elem)
     resolve();
   });
 }
 
 function SetupEME(test, token, params)
 {
   var v = document.createElement("video");
 
+  var onSetKeysFail = (params && params.onSetKeysFail)
+    ? params.onSetKeysFail
+    : bail(token + " Failed to set MediaKeys on <video> element");
+  
   v.addEventListener("encrypted", function(ev) {
     info(token + " got encrypted event");
     MediaKeys.create(KEYSYSTEM_TYPE).then(function(mediaKeys) {
       info(token + " created MediaKeys object ok");
       mediaKeys.sessions = [];
       return v.setMediaKeys(mediaKeys);
     }, bail("failed to create MediaKeys object")).then(function() {
       info(token + " set MediaKeys on <video> element ok");
@@ -162,13 +166,13 @@ function SetupEME(test, token, params)
       var session = v.mediaKeys.createSession(test.sessionType);
       if (params && params.onsessioncreated) {
         params.onsessioncreated(session);
       }
       session.addEventListener("message", UpdateSessionFunc(test));
       session.generateRequest(ev.initDataType, ev.initData).then(function() {
       }, bail(token + " Failed to initialise MediaKeySession"));
 
-    }, bail(token + " Failed to set MediaKeys on <video> element"));
+    }, onSetKeysFail);
   });
 
   return v;
 }
--- a/dom/media/test/mochitest.ini
+++ b/dom/media/test/mochitest.ini
@@ -356,18 +356,22 @@ skip-if = (toolkit == 'android' && proce
 [test_contentDuration7.html]
 [test_controls.html]
 [test_currentTime.html]
 [test_decode_error.html]
 [test_decoder_disable.html]
 [test_defaultMuted.html]
 [test_delay_load.html]
 skip-if = buildapp == 'b2g' && toolkit != 'gonk' # bug 1082984
+[test_eme_canvas_blocked.html]
+skip-if = buildapp == 'b2g' || toolkit == 'android' || e10s # bug 1043403, bug 1057908
 [test_eme_playback.html]
 skip-if = buildapp == 'b2g' || toolkit == 'android' || e10s # bug 1043403, bug 1057908
+[test_eme_stream_capture_blocked.html]
+skip-if = buildapp == 'b2g' || toolkit == 'android' || e10s # bug 1043403, bug 1057908
 [test_error_in_video_document.html]
 skip-if = toolkit == 'android' || (os == 'win' && !debug) || (os == 'mac' && !debug) # bug 608634
 [test_error_on_404.html]
 [test_fastSeek.html]
 skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_fastSeek-forwards.html]
 [test_imagecapture.html]
 [test_info_leak.html]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/test_eme_canvas_blocked.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test Encrypted Media Extensions</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+  <script type="text/javascript" src="manifest.js"></script>
+  <script type="text/javascript" src="eme.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var manager = new MediaTestManager;
+
+function startTest(test, token)
+{
+  manager.started(token);
+
+  var sessions = [];
+
+  var v = SetupEME(test, token);
+  v.preload = "auto"; // Required due to "canplay" not firing for MSE unless we do this.
+
+  v.addEventListener("canplay", function(ev) {
+    var video = ev.target;
+    var canvas = document.createElement("canvas");
+    canvas.width = video.videoWidth;
+    canvas.height = video.videoHeight;
+    document.body.appendChild(canvas);
+    var ctx = canvas.getContext("2d");
+    var threwError = false;
+    try {
+      ctx.drawImage(video, 0, 0);
+    } catch (ex) {
+      threwError = true;
+    }
+    ok(threwError, token + " - Should throw an error when trying to draw EME video to canvas.");
+    manager.finished(token);
+  });
+
+  v.addEventListener("error", bail(token + " got error event"));
+
+  LoadTest(test, v);
+}
+
+function beginTest() {
+  manager.runTests(gEMETests, startTest);
+}
+
+var prefs = [
+  [ "media.mediasource.enabled", true ],
+  [ "media.mediasource.ignore_codecs", true ],
+];
+
+if (/Linux/.test(navigator.userAgent) ||
+    !document.createElement('video').canPlayType("video/mp4")) {
+  // XXX remove once we have mp4 PlatformDecoderModules on all platforms.
+  prefs.push([ "media.fragmented-mp4.exposed", true ]);
+  prefs.push([ "media.fragmented-mp4.use-blank-decoder", true ]);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({ "set" : prefs }, beginTest);
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/media/test/test_eme_stream_capture_blocked.html
@@ -0,0 +1,102 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test Encrypted Media Extensions</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+  <script type="text/javascript" src="manifest.js"></script>
+  <script type="text/javascript" src="eme.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var manager = new MediaTestManager;
+
+function startTest(test, token)
+{
+  // Three cases:
+  // 1. setting MediaKeys on an element captured by MediaElementSource should fail, and
+  // 2. creating a MediaElementSource on a media element with a MediaKeys should fail, and
+  // 3. capturing a media element with mozCaptureStream that has a MediaKeys should fail.
+
+  // Case 1. setting MediaKeys on an element captured by MediaElementSource should fail.
+  var case1token = token + "_case1";
+  var setKeysFailed = function() {
+    ok(true, case1token + " setMediaKeys failed as expected.");
+    manager.finished(case1token);
+  };
+  var v1 = SetupEME(test, case1token,  { onSetKeysFail: setKeysFailed });
+  var context = new AudioContext();
+  var node = context.createMediaElementSource(v1);
+  v1.preload = "auto"; // Required due to "canplay" not firing for MSE unless we do this.
+  v1.addEventListener("error", bail(case1token + " got error event"));
+  v1.addEventListener("canplay", function(ev) {
+    ok(false, case1token + " should never reach canplay, as setMediaKeys should fail");
+  });
+  manager.started(case1token);
+  LoadTest(test, v1);
+
+
+  // Case 2. creating a MediaElementSource on a media element with a MediaKeys should fail.
+  var case2token = token + "_case2";
+  var v2 = SetupEME(test, case2token);
+  v2.preload = "auto"; // Required due to "canplay" not firing for MSE unless we do this.
+  v2.addEventListener("error", bail(case2token + " got error event"));
+  v2.addEventListener("canplay", function(ev) {
+    ok(true, case2token + " should reach canplay");
+    var threw = false;
+    try {
+      var context = new AudioContext();
+      var node = context.createMediaElementSource(v2);
+    } catch (e) {
+      threw = true;
+    }
+    ok(threw, "Should throw an error creating a MediaElementSource on an EME video.");
+    manager.finished(case2token);
+  });
+  manager.started(case2token);
+  LoadTest(test, v2);
+
+
+  // Case 3. capturing a media element with mozCaptureStream that has a MediaKeys should fail.
+  var case3token = token + "_case3";
+  var v3 = SetupEME(test, case3token);
+  v3.preload = "auto"; // Required due to "canplay" not firing for MSE unless we do this.
+  v3.addEventListener("error", bail(case3token + " got error event"));
+  v3.addEventListener("canplay", function(ev) {
+    ok(true, case3token + " should reach canplay");
+    var threw = false;
+    try {
+      var stream = v3.mozCaptureStreamUntilEnded();
+    } catch (e) {
+      threw = true;
+    }
+    ok(threw, "Should throw an error calling mozCaptureStreamUntilEnded an EME video.");
+    manager.finished(case3token);
+  });
+  manager.started(case3token);
+  LoadTest(test, v3);
+}
+
+function beginTest() {
+  manager.runTests(gEMETests, startTest);
+}
+
+var prefs = [
+  [ "media.mediasource.enabled", true ],
+  [ "media.mediasource.ignore_codecs", true ],
+];
+
+if (/Linux/.test(navigator.userAgent) ||
+    !document.createElement('video').canPlayType("video/mp4")) {
+  // XXX remove once we have mp4 PlatformDecoderModules on all platforms.
+  prefs.push([ "media.fragmented-mp4.exposed", true ]);
+  prefs.push([ "media.fragmented-mp4.use-blank-decoder", true ]);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({ "set" : prefs }, beginTest);
+</script>
+</pre>
+</body>
+</html>
--- a/dom/media/webaudio/AudioContext.cpp
+++ b/dom/media/webaudio/AudioContext.cpp
@@ -280,16 +280,22 @@ AudioContext::CreateAnalyser()
 already_AddRefed<MediaElementAudioSourceNode>
 AudioContext::CreateMediaElementSource(HTMLMediaElement& aMediaElement,
                                        ErrorResult& aRv)
 {
   if (mIsOffline) {
     aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
     return nullptr;
   }
+#ifdef MOZ_EME
+  if (aMediaElement.ContainsRestrictedContent()) {
+    aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+    return nullptr;
+  }
+#endif
   nsRefPtr<DOMMediaStream> stream = aMediaElement.MozCaptureStream(aRv);
   if (aRv.Failed()) {
     return nullptr;
   }
   nsRefPtr<MediaElementAudioSourceNode> mediaElementAudioSourceNode =
     new MediaElementAudioSourceNode(this, stream);
   return mediaElementAudioSourceNode.forget();
 }
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -6102,16 +6102,22 @@ nsLayoutUtils::SurfaceFromElementResult
 nsLayoutUtils::SurfaceFromElement(HTMLVideoElement* aElement,
                                   uint32_t aSurfaceFlags,
                                   DrawTarget* aTarget)
 {
   SurfaceFromElementResult result;
 
   NS_WARN_IF_FALSE((aSurfaceFlags & SFE_PREFER_NO_PREMULTIPLY_ALPHA) == 0, "We can't support non-premultiplied alpha for video!");
 
+#ifdef MOZ_EME
+  if (aElement->ContainsRestrictedContent()) {
+    return result;
+  }
+#endif
+
   uint16_t readyState;
   if (NS_SUCCEEDED(aElement->GetReadyState(&readyState)) &&
       (readyState == nsIDOMHTMLMediaElement::HAVE_NOTHING ||
        readyState == nsIDOMHTMLMediaElement::HAVE_METADATA)) {
     result.mIsStillLoading = true;
     return result;
   }