Bug 1308076 - Basic validation of EME initData. r=jwwang draft
authorChris Pearce <cpearce@mozilla.com>
Thu, 06 Oct 2016 17:54:36 +1300
changeset 423091 d023aec75d1ef1486116510efcc31e2b587e6e50
parent 423090 d72cf6ecebaf707c159f6697d9f5f6f6a6feb7e1
child 423092 fed2a26df89a5c2a600a6e6d1ab63ab86706fc45
push id31798
push userbmo:cpearce@mozilla.com
push dateMon, 10 Oct 2016 06:37:23 +0000
reviewersjwwang
bugs1308076
milestone52.0a1
Bug 1308076 - Basic validation of EME initData. r=jwwang Implement more of MediaKeySession.generateRequest() to validate initData before passing to CDM. Also throw TypeErrors when initData is not valid, as per the spec. Only trivial validation happens here, I'll add more comprehensive validation in subsequent patches. MozReview-Commit-ID: 3jTOsJNvRDo
dom/media/eme/MediaKeySession.cpp
dom/media/eme/MediaKeySystemAccess.cpp
dom/media/eme/MediaKeySystemAccess.h
--- a/dom/media/eme/MediaKeySession.cpp
+++ b/dom/media/eme/MediaKeySession.cpp
@@ -5,16 +5,17 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/dom/HTMLMediaElement.h"
 #include "mozilla/dom/MediaKeySession.h"
 #include "mozilla/dom/MediaKeyError.h"
 #include "mozilla/dom/MediaKeyMessageEvent.h"
 #include "mozilla/dom/MediaEncryptedEvent.h"
 #include "mozilla/dom/MediaKeyStatusMap.h"
+#include "mozilla/dom/MediaKeySystemAccess.h"
 #include "nsCycleCollectionParticipant.h"
 #include "mozilla/CDMProxy.h"
 #include "mozilla/AsyncEventDispatcher.h"
 #include "mozilla/Move.h"
 #include "nsContentUtils.h"
 #include "mozilla/EMEUtils.h"
 #include "GMPUtils.h"
 #include "nsPrintfCString.h"
@@ -34,16 +35,25 @@ NS_INTERFACE_MAP_END_INHERITING(DOMEvent
 
 NS_IMPL_ADDREF_INHERITED(MediaKeySession, DOMEventTargetHelper)
 NS_IMPL_RELEASE_INHERITED(MediaKeySession, DOMEventTargetHelper)
 
 // Count of number of instances. Used to give each instance a
 // unique token.
 static uint32_t sMediaKeySessionNum = 0;
 
+// Max length of keyId in EME "keyIds" or WebM init data format, as enforced
+// by web platform tests.
+static const uint32_t MAX_KEY_ID_LENGTH = 512;
+
+// Max length of CENC PSSH init data tolerated, as enforced by web
+// platform tests.
+static const uint32_t MAX_CENC_INIT_DATA_LENGTH = 64 * 1024;
+
+
 MediaKeySession::MediaKeySession(JSContext* aCx,
                                  nsPIDOMWindowInner* aParent,
                                  MediaKeys* aKeys,
                                  const nsAString& aKeySystem,
                                  const nsAString& aCDMVersion,
                                  MediaKeySessionType aSessionType,
                                  ErrorResult& aRv)
   : DOMEventTargetHelper(aParent)
@@ -155,55 +165,143 @@ MediaKeySession::UpdateKeyStatusMap()
 }
 
 MediaKeyStatusMap*
 MediaKeySession::KeyStatuses() const
 {
   return mKeyStatusMap;
 }
 
+// The user agent MUST thoroughly validate the Initialization Data before
+// passing it to the CDM. This includes verifying that the length and
+// values of fields are reasonable, verifying that values are within
+// reasonable limits, and stripping irrelevant, unsupported, or unknown
+// data or fields. It is RECOMMENDED that user agents pre-parse, sanitize,
+// and/or generate a fully sanitized version of the Initialization Data.
+// If the Initialization Data format specified by initDataType supports
+// multiple entries, the user agent SHOULD remove entries that are not
+// needed by the CDM. The user agent MUST NOT re-order entries within
+// the Initialization Data.
+static bool
+ValidateInitData(const nsTArray<uint8_t>& aInitData, const nsAString& aInitDataType)
+{
+  if (aInitDataType.LowerCaseEqualsLiteral("webm")) {
+    // WebM initData consists of a single keyId. Ensure it's of reasonable length.
+    return aInitData.Length() <= MAX_KEY_ID_LENGTH;
+  } else if (aInitDataType.LowerCaseEqualsLiteral("cenc")) {
+    // Limit initData to less than 64KB.
+    if (aInitData.Length() > MAX_CENC_INIT_DATA_LENGTH) {
+      return false;
+    }
+    // TODO: Validate PSSH in future patch...
+  } else if (aInitDataType.LowerCaseEqualsLiteral("keyids")) {
+    if (aInitData.Length() > MAX_KEY_ID_LENGTH) {
+      return false;
+    }
+    // TODO: Validate keyIds in future patch...
+  }
+  return true;
+}
+
+// Generates a license request based on the initData. A message of type
+// "license-request" or "individualization-request" will always be queued
+// if the algorithm succeeds and the promise is resolved.
 already_AddRefed<Promise>
 MediaKeySession::GenerateRequest(const nsAString& aInitDataType,
                                  const ArrayBufferViewOrArrayBuffer& aInitData,
                                  ErrorResult& aRv)
 {
   RefPtr<DetailedPromise> promise(MakePromise(aRv,
     NS_LITERAL_CSTRING("MediaKeySession.generateRequest")));
   if (aRv.Failed()) {
     return nullptr;
   }
 
+  // If this object is closed, return a promise rejected with an InvalidStateError.
+  if (IsClosed()) {
+    EME_LOG("MediaKeySession[%p,'%s'] GenerateRequest() failed, closed",
+            this, NS_ConvertUTF16toUTF8(mSessionId).get());
+    promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR,
+      NS_LITERAL_CSTRING("Session is closed in MediaKeySession.generateRequest()"));
+    return promise.forget();
+  }
+
+  // If this object's uninitialized value is false, return a promise rejected
+  // with an InvalidStateError.
   if (!mUninitialized) {
     EME_LOG("MediaKeySession[%p,'%s'] GenerateRequest() failed, uninitialized",
             this, NS_ConvertUTF16toUTF8(mSessionId).get());
-    promise->MaybeReject(NS_ERROR_DOM_INVALID_ACCESS_ERR,
+    promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR,
       NS_LITERAL_CSTRING("Session is already initialized in MediaKeySession.generateRequest()"));
     return promise.forget();
   }
 
+  // Let this object's uninitialized value be false.
   mUninitialized = false;
 
+  // If initDataType is the empty string, return a promise rejected
+  // with a newly created TypeError.
   if (aInitDataType.IsEmpty()) {
     promise->MaybeReject(NS_ERROR_DOM_TYPE_ERR,
       NS_LITERAL_CSTRING("Empty initDataType passed to MediaKeySession.generateRequest()"));
     EME_LOG("MediaKeySession[%p,'%s'] GenerateRequest() failed, empty initDataType",
       this, NS_ConvertUTF16toUTF8(mSessionId).get());
     return promise.forget();
   }
 
+  // If initData is an empty array, return a promise rejected with
+  // a newly created TypeError.
   nsTArray<uint8_t> data;
   CopyArrayBufferViewOrArrayBufferData(aInitData, data);
   if (data.IsEmpty()) {
     promise->MaybeReject(NS_ERROR_DOM_TYPE_ERR,
       NS_LITERAL_CSTRING("Empty initData passed to MediaKeySession.generateRequest()"));
     EME_LOG("MediaKeySession[%p,'%s'] GenerateRequest() failed, empty initData",
-      this, NS_ConvertUTF16toUTF8(mSessionId).get());
+            this, NS_ConvertUTF16toUTF8(mSessionId).get());
+    return promise.forget();
+  }
+
+  // If the Key System implementation represented by this object's
+  // cdm implementation value does not support initDataType as an
+  // Initialization Data Type, return a promise rejected with a
+  // NotSupportedError. String comparison is case-sensitive.
+  if (!MediaKeySystemAccess::KeySystemSupportsInitDataType(mKeySystem, aInitDataType)) {
+    promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR,
+      NS_LITERAL_CSTRING("Unsupported initDataType passed to MediaKeySession.generateRequest()"));
+    EME_LOG("MediaKeySession[%p,'%s'] GenerateRequest() failed, unsupported initDataType",
+            this, NS_ConvertUTF16toUTF8(mSessionId).get());
     return promise.forget();
   }
 
+  // Let init data be a copy of the contents of the initData parameter.
+  // Note: Handled by the CopyArrayBufferViewOrArrayBufferData call above.
+
+  // Let session type be this object's session type.
+
+  // Let promise be a new promise.
+
+  // Run the following steps in parallel:
+
+  // If the init data is not valid for initDataType, reject promise with
+  // a newly created TypeError.
+  if (!ValidateInitData(data, aInitDataType)) {
+    // If the preceding step failed, reject promise with a newly created TypeError.
+    promise->MaybeReject(NS_ERROR_DOM_TYPE_ERR,
+      NS_LITERAL_CSTRING("initData sanitization failed in MediaKeySession.generateRequest()"));
+    EME_LOG("MediaKeySession[%p,'%s'] GenerateRequest() initData sanitization failed",
+            this, NS_ConvertUTF16toUTF8(mSessionId).get());
+    return promise.forget();
+  }
+
+  // Let sanitized init data be a validated and sanitized version of init data.
+
+  // If sanitized init data is empty, reject promise with a NotSupportedError.
+
+  // Note: Remaining steps of generateRequest method continue in CDM.
+
   Telemetry::Accumulate(Telemetry::VIDEO_CDM_GENERATE_REQUEST_CALLED,
                         ToCDMTypeTelemetryEnum(mKeySystem));
 
   // Convert initData to base64 for easier logging.
   // Note: CreateSession() Move()s the data out of the array, so we have
   // to copy it here.
   nsAutoCString base64InitData(ToBase64(data));
   PromiseId pid = mKeys->StorePromise(promise);
--- a/dom/media/eme/MediaKeySystemAccess.cpp
+++ b/dom/media/eme/MediaKeySystemAccess.cpp
@@ -507,16 +507,26 @@ GetKeySystemConfig(const nsAString& aKey
   for (const KeySystemConfig& config : GetSupportedKeySystems()) {
     if (config.mKeySystem.Equals(aKeySystem)) {
       return &config;
     }
   }
   return nullptr;
 }
 
+/* static */
+bool
+MediaKeySystemAccess::KeySystemSupportsInitDataType(const nsAString& aKeySystem,
+                                                    const nsAString& aInitDataType)
+{
+  const KeySystemConfig* implementation = GetKeySystemConfig(aKeySystem);
+  return implementation &&
+         implementation->mInitDataTypes.Contains(aInitDataType);
+}
+
 enum CodecType
 {
   Audio,
   Video,
   Invalid
 };
 
 static bool
--- a/dom/media/eme/MediaKeySystemAccess.h
+++ b/dom/media/eme/MediaKeySystemAccess.h
@@ -70,16 +70,19 @@ public:
                                  const nsACString& aVersion,
                                  nsACString& aOutMessage);
 
   static bool GetSupportedConfig(const nsAString& aKeySystem,
                                  const Sequence<MediaKeySystemConfiguration>& aConfigs,
                                  MediaKeySystemConfiguration& aOutConfig,
                                  DecoderDoctorDiagnostics* aDiagnostics);
 
+  static bool KeySystemSupportsInitDataType(const nsAString& aKeySystem,
+                                            const nsAString& aInitDataType);
+
 private:
   nsCOMPtr<nsPIDOMWindowInner> mParent;
   const nsString mKeySystem;
   const nsString mCDMVersion;
   const MediaKeySystemConfiguration mConfig;
 };
 
 } // namespace dom