Bug 1044742 - ClearKey CDM for testing EME - r=cpearce
authorEdwin Flores <eflores@mozilla.com>
Wed, 24 Sep 2014 10:04:49 +1200
changeset 222391 79cb9483483a9dda383ceaafcc0046f090cc8046
parent 222390 a57419135bb5b8efc54cc4e500e2766f824e6559
child 222392 bd0198b0803d3489d7649dc94a7f26852a77dc34
push id7107
push userraliiev@mozilla.com
push dateMon, 13 Oct 2014 17:43:31 +0000
treeherdermozilla-aurora@b4b34e0acc75 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerscpearce
bugs1044742
milestone35.0a1
Bug 1044742 - ClearKey CDM for testing EME - r=cpearce
media/gmp-clearkey/0.1/ClearKeyDecryptionManager.cpp
media/gmp-clearkey/0.1/ClearKeyDecryptionManager.h
media/gmp-clearkey/0.1/ClearKeySession.cpp
media/gmp-clearkey/0.1/ClearKeySession.h
media/gmp-clearkey/0.1/ClearKeyUtils.cpp
media/gmp-clearkey/0.1/ClearKeyUtils.h
media/gmp-clearkey/0.1/Makefile.in
media/gmp-clearkey/0.1/clearkey.info
media/gmp-clearkey/0.1/gmp-clearkey.cpp
media/gmp-clearkey/0.1/moz.build
toolkit/toolkit.mozbuild
new file mode 100644
--- /dev/null
+++ b/media/gmp-clearkey/0.1/ClearKeyDecryptionManager.cpp
@@ -0,0 +1,402 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <assert.h>
+#include <sstream>
+#include <stdint.h>
+#include <stdio.h>
+
+#include "ClearKeyDecryptionManager.h"
+#include "ClearKeyUtils.h"
+#include "mozilla/NullPtr.h"
+
+using namespace mozilla;
+using namespace std;
+
+
+class ClearKeyDecryptor
+{
+public:
+  ClearKeyDecryptor(GMPDecryptorCallback* aCallback, const Key& aKey);
+  ~ClearKeyDecryptor();
+
+  void InitKey();
+
+  void QueueDecrypt(GMPBuffer* aBuffer, GMPEncryptedBufferMetadata* aMetadata);
+
+  uint32_t AddRef();
+  uint32_t Release();
+
+private:
+  struct DecryptTask : public GMPTask
+  {
+    DecryptTask(ClearKeyDecryptor* aTarget, GMPBuffer* aBuffer,
+                GMPEncryptedBufferMetadata* aMetadata)
+      : mTarget(aTarget), mBuffer(aBuffer), mMetadata(aMetadata) { }
+
+    virtual void Run() MOZ_OVERRIDE
+    {
+      mTarget->Decrypt(mBuffer, mMetadata);
+    }
+
+    virtual void Destroy() MOZ_OVERRIDE {
+      delete this;
+    }
+
+    virtual ~DecryptTask() { }
+
+    ClearKeyDecryptor* mTarget;
+    GMPBuffer* mBuffer;
+    GMPEncryptedBufferMetadata* mMetadata;
+  };
+
+  struct DestroyTask : public GMPTask
+  {
+    DestroyTask(ClearKeyDecryptor* aTarget) : mTarget(aTarget) { }
+
+    virtual void Run() MOZ_OVERRIDE {
+      delete mTarget;
+    }
+
+    virtual void Destroy() MOZ_OVERRIDE {
+      delete this;
+    }
+
+    virtual ~DestroyTask() { }
+
+    ClearKeyDecryptor* mTarget;
+  };
+
+  void Decrypt(GMPBuffer* aBuffer, GMPEncryptedBufferMetadata* aMetadata);
+
+  uint32_t mRefCnt;
+
+  GMPDecryptorCallback* mCallback;
+  GMPThread* mThread;
+
+  Key mKey;
+};
+
+
+ClearKeyDecryptionManager::ClearKeyDecryptionManager(GMPDecryptorHost* aHost)
+  : mHost(aHost)
+{
+  CK_LOGD("ClearKeyDecryptionManager ctor");
+}
+
+ClearKeyDecryptionManager::~ClearKeyDecryptionManager()
+{
+  CK_LOGD("ClearKeyDecryptionManager dtor");
+}
+
+void
+ClearKeyDecryptionManager::Init(GMPDecryptorCallback* aCallback)
+{
+  CK_LOGD("ClearKeyDecryptionManager::Init");
+  mCallback = aCallback;
+  mCallback->SetCapabilities(GMP_EME_CAP_DECRYPT_AUDIO |
+                             GMP_EME_CAP_DECRYPT_VIDEO);
+}
+
+static string
+GetNewSessionId()
+{
+  static uint32_t sNextSessionId = 0;
+
+  string sessionId;
+  stringstream ss;
+  ss << ++sNextSessionId;
+  ss >> sessionId;
+
+  return sessionId;
+}
+
+void
+ClearKeyDecryptionManager::CreateSession(uint32_t aPromiseId,
+                                         const char* aInitDataType,
+                                         uint32_t aInitDataTypeSize,
+                                         const uint8_t* aInitData,
+                                         uint32_t aInitDataSize,
+                                         GMPSessionType aSessionType)
+{
+  CK_LOGD("ClearKeyDecryptionManager::CreateSession type:%s", aInitDataType);
+
+  // initDataType must be "cenc".
+  if (strcmp("cenc", aInitDataType)) {
+    mCallback->RejectPromise(aPromiseId, kGMPNotSupportedError,
+                             nullptr /* message */, 0 /* messageLen */);
+    return;
+  }
+
+  string sessionId = GetNewSessionId();
+  assert(mSessions.find(sessionId) == mSessions.end());
+
+  ClearKeySession* session = new ClearKeySession(sessionId, mHost, mCallback);
+  session->Init(aPromiseId, aInitData, aInitDataSize);
+  mSessions[sessionId] = session;
+
+  const vector<KeyId>& sessionKeys = session->GetKeyIds();
+  vector<KeyId> neededKeys;
+  for (auto it = sessionKeys.begin(); it != sessionKeys.end(); it++) {
+    if (mDecryptors.find(*it) == mDecryptors.end()) {
+      // Need to request this key ID from the client.
+      neededKeys.push_back(*it);
+      mDecryptors[*it] = nullptr;
+    } else {
+      // We already have a key for this key ID. Mark as usable.
+      mCallback->KeyIdUsable(sessionId.c_str(), sessionId.length(),
+                             &(*it)[0], it->size());
+    }
+  }
+
+  if (neededKeys.empty()) {
+    CK_LOGD("No keys needed from client.");
+    return;
+  }
+
+  // Send a request for needed key data.
+  string request;
+  ClearKeyUtils::MakeKeyRequest(neededKeys, request);
+  mCallback->SessionMessage(&sessionId[0], sessionId.length(),
+                            (uint8_t*)&request[0], request.length(),
+                            "" /* destination url */, 0);
+}
+
+void
+ClearKeyDecryptionManager::LoadSession(uint32_t aPromiseId,
+                                       const char* aSessionId,
+                                       uint32_t aSessionIdLength)
+{
+  // TODO implement "persistent" sessions.
+  mCallback->RejectPromise(aPromiseId, kGMPNotSupportedError,
+                           nullptr /* message */, 0 /* messageLen */);
+  CK_LOGD("ClearKeyDecryptionManager::LoadSession");
+}
+
+void
+ClearKeyDecryptionManager::UpdateSession(uint32_t aPromiseId,
+                                         const char* aSessionId,
+                                         uint32_t aSessionIdLength,
+                                         const uint8_t* aResponse,
+                                         uint32_t aResponseSize)
+{
+  CK_LOGD("ClearKeyDecryptionManager::UpdateSession");
+  string sessionId(aSessionId, aSessionId + aSessionIdLength);
+
+  if (mSessions.find(sessionId) == mSessions.end() || !mSessions[sessionId]) {
+    CK_LOGW("ClearKey CDM couldn't resolve session ID in UpdateSession.");
+    mCallback->RejectPromise(aPromiseId, kGMPNotFoundError, nullptr, 0);
+    return;
+  }
+
+  // Parse the response for any (key ID, key) pairs.
+  vector<KeyIdPair> keyPairs;
+  if (!ClearKeyUtils::ParseJWK(aResponse, aResponseSize, keyPairs)) {
+    CK_LOGW("ClearKey CDM failed to parse JSON Web Key.");
+    mCallback->RejectPromise(aPromiseId, kGMPAbortError, nullptr, 0);
+    return;
+  }
+  mCallback->ResolvePromise(aPromiseId);
+
+  for (auto it = keyPairs.begin(); it != keyPairs.end(); it++) {
+    KeyId& keyId = it->mKeyId;
+
+    if (mDecryptors.find(keyId) != mDecryptors.end()) {
+      mDecryptors[keyId] = new ClearKeyDecryptor(mCallback, it->mKey);
+      mCallback->KeyIdUsable(aSessionId, aSessionIdLength,
+                             &keyId[0], keyId.size());
+    }
+
+    mDecryptors[keyId]->AddRef();
+  }
+}
+
+void
+ClearKeyDecryptionManager::CloseSession(uint32_t aPromiseId,
+                                        const char* aSessionId,
+                                        uint32_t aSessionIdLength)
+{
+  CK_LOGD("ClearKeyDecryptionManager::CloseSession");
+
+  string sessionId(aSessionId, aSessionId + aSessionIdLength);
+  ClearKeySession* session = mSessions[sessionId];
+
+  assert(session);
+
+  const vector<KeyId>& keyIds = session->GetKeyIds();
+  for (auto it = keyIds.begin(); it != keyIds.end(); it++) {
+    assert(mDecryptors.find(*it) != mDecryptors.end());
+
+    if (!mDecryptors[*it]->Release()) {
+      mDecryptors.erase(*it);
+      mCallback->KeyIdNotUsable(aSessionId, aSessionIdLength,
+                                &(*it)[0], it->size());
+    }
+  }
+
+  mSessions.erase(sessionId);
+  delete session;
+
+  mCallback->ResolvePromise(aPromiseId);
+}
+
+void
+ClearKeyDecryptionManager::RemoveSession(uint32_t aPromiseId,
+                                         const char* aSessionId,
+                                         uint32_t aSessionIdLength)
+{
+  // TODO implement "persistent" sessions.
+  CK_LOGD("ClearKeyDecryptionManager::RemoveSession");
+  mCallback->RejectPromise(aPromiseId, kGMPInvalidAccessError,
+                           nullptr /* message */, 0 /* messageLen */);
+}
+
+void
+ClearKeyDecryptionManager::SetServerCertificate(uint32_t aPromiseId,
+                                                const uint8_t* aServerCert,
+                                                uint32_t aServerCertSize)
+{
+  // ClearKey CDM doesn't support this method by spec.
+  CK_LOGD("ClearKeyDecryptionManager::SetServerCertificate");
+  mCallback->RejectPromise(aPromiseId, kGMPNotSupportedError,
+                           nullptr /* message */, 0 /* messageLen */);
+}
+
+void
+ClearKeyDecryptionManager::Decrypt(GMPBuffer* aBuffer,
+                                   GMPEncryptedBufferMetadata* aMetadata)
+{
+  CK_LOGD("ClearKeyDecryptionManager::Decrypt");
+  KeyId keyId(aMetadata->KeyId(), aMetadata->KeyId() + aMetadata->KeyIdSize());
+
+  if (mDecryptors.find(keyId) == mDecryptors.end() || !mDecryptors[keyId]) {
+    mCallback->Decrypted(aBuffer, GMPNoKeyErr);
+  }
+
+  mDecryptors[keyId]->QueueDecrypt(aBuffer, aMetadata);
+}
+
+void
+ClearKeyDecryptionManager::DecryptingComplete()
+{
+  CK_LOGD("ClearKeyDecryptionManager::DecryptingComplete");
+
+  for (auto it = mSessions.begin(); it != mSessions.end(); it++) {
+    delete it->second;
+  }
+
+  for (auto it = mDecryptors.begin(); it != mDecryptors.end(); it++) {
+    delete it->second;
+  }
+
+  delete this;
+}
+
+void
+ClearKeyDecryptor::QueueDecrypt(GMPBuffer* aBuffer,
+                                GMPEncryptedBufferMetadata* aMetadata)
+{
+  CK_LOGD("ClearKeyDecryptor::QueueDecrypt");
+  mThread->Post(new DecryptTask(this, aBuffer, aMetadata));
+}
+
+void
+ClearKeyDecryptor::Decrypt(GMPBuffer* aBuffer,
+                           GMPEncryptedBufferMetadata* aMetadata)
+{
+  if (!mThread) {
+    mCallback->Decrypted(aBuffer, GMPGenericErr);
+  }
+
+  // If the sample is split up into multiple encrypted subsamples, we need to
+  // stitch them into one continuous buffer for decryption.
+  vector<uint8_t> tmp(aBuffer->Size());
+
+  if (aMetadata->NumSubsamples()) {
+    // Take all encrypted parts of subsamples and stitch them into one
+    // continuous encrypted buffer.
+    unsigned char* data = aBuffer->Data();
+    unsigned char* iter = &tmp[0];
+    for (size_t i = 0; i < aMetadata->NumSubsamples(); i++) {
+      data += aMetadata->ClearBytes()[i];
+      uint32_t cipherBytes = aMetadata->CipherBytes()[i];
+
+      memcpy(iter, data, cipherBytes);
+
+      data += cipherBytes;
+      iter += cipherBytes;
+    }
+
+    tmp.resize((size_t)(iter - &tmp[0]));
+  } else {
+    memcpy(&tmp[0], aBuffer->Data(), aBuffer->Size());
+  }
+
+  assert(aMetadata->IVSize() == 8 || aMetadata->IVSize() == 16);
+  vector<uint8_t> iv(aMetadata->IV(), aMetadata->IV() + aMetadata->IVSize());
+  iv.insert(iv.end(), CLEARKEY_KEY_LEN - aMetadata->IVSize(), 0);
+
+  ClearKeyUtils::DecryptAES(mKey, tmp, iv);
+
+  if (aMetadata->NumSubsamples()) {
+    // Take the decrypted buffer, split up into subsamples, and insert those
+    // subsamples back into their original position in the original buffer.
+    unsigned char* data = aBuffer->Data();
+    unsigned char* iter = &tmp[0];
+    for (size_t i = 0; i < aMetadata->NumSubsamples(); i++) {
+      data += aMetadata->ClearBytes()[i];
+      uint32_t cipherBytes = aMetadata->CipherBytes()[i];
+
+      memcpy(data, iter, cipherBytes);
+
+      data += cipherBytes;
+      iter += cipherBytes;
+    }
+  } else {
+    memcpy(aBuffer->Data(), &tmp[0], aBuffer->Size());
+  }
+
+  mCallback->Decrypted(aBuffer, GMPNoErr);
+}
+
+ClearKeyDecryptor::ClearKeyDecryptor(GMPDecryptorCallback* aCallback,
+                                     const Key& aKey)
+  : mRefCnt(0)
+  , mCallback(aCallback)
+  , mKey(aKey)
+{
+  if (GetPlatform()->createthread(&mThread) != GMPNoErr) {
+    CK_LOGD("failed to create thread in clearkey cdm");
+    mThread = nullptr;
+    return;
+  }
+}
+
+ClearKeyDecryptor::~ClearKeyDecryptor()
+{
+  CK_LOGD("ClearKeyDecryptor dtor; key ID = %08x...", *(uint32_t*)&mKey[0]);
+  if (mThread) {
+    mThread->Join();
+  }
+}
+
+uint32_t
+ClearKeyDecryptor::AddRef()
+{
+  return ++mRefCnt;
+}
+
+uint32_t
+ClearKeyDecryptor::Release()
+{
+  if (!--mRefCnt) {
+    if (mThread) {
+      mThread->Post(new DestroyTask(this));
+    } else {
+      delete this;
+    }
+  }
+
+  return mRefCnt;
+}
new file mode 100644
--- /dev/null
+++ b/media/gmp-clearkey/0.1/ClearKeyDecryptionManager.h
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __ClearKeyDecryptor_h__
+#define __ClearKeyDecryptor_h_
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include "ClearKeySession.h"
+#include "ClearKeyUtils.h"
+#include "gmp-api/gmp-decryption.h"
+#include "ScopedNSSTypes.h"
+
+class ClearKeyDecryptor;
+
+class ClearKeyDecryptionManager MOZ_FINAL : public GMPDecryptor
+{
+public:
+  ClearKeyDecryptionManager(GMPDecryptorHost* aHost);
+  ~ClearKeyDecryptionManager();
+
+  virtual void Init(GMPDecryptorCallback* aCallback) MOZ_OVERRIDE;
+
+  virtual void CreateSession(uint32_t aPromiseId,
+                             const char* aInitDataType,
+                             uint32_t aInitDataTypeSize,
+                             const uint8_t* aInitData,
+                             uint32_t aInitDataSize,
+                             GMPSessionType aSessionType) MOZ_OVERRIDE;
+
+  virtual void LoadSession(uint32_t aPromiseId,
+                           const char* aSessionId,
+                           uint32_t aSessionIdLength) MOZ_OVERRIDE;
+
+  virtual void UpdateSession(uint32_t aPromiseId,
+                             const char* aSessionId,
+                             uint32_t aSessionIdLength,
+                             const uint8_t* aResponse,
+                             uint32_t aResponseSize) MOZ_OVERRIDE;
+
+  virtual void CloseSession(uint32_t aPromiseId,
+                            const char* aSessionId,
+                            uint32_t aSessionIdLength) MOZ_OVERRIDE;
+
+  virtual void RemoveSession(uint32_t aPromiseId,
+                             const char* aSessionId,
+                             uint32_t aSessionIdLength) MOZ_OVERRIDE;
+
+  virtual void SetServerCertificate(uint32_t aPromiseId,
+                                    const uint8_t* aServerCert,
+                                    uint32_t aServerCertSize) MOZ_OVERRIDE;
+
+  virtual void Decrypt(GMPBuffer* aBuffer,
+                       GMPEncryptedBufferMetadata* aMetadata) MOZ_OVERRIDE;
+
+  virtual void DecryptingComplete() MOZ_OVERRIDE;
+
+private:
+  GMPDecryptorCallback* mCallback;
+  GMPDecryptorHost* mHost;
+
+  std::map<KeyId, ClearKeyDecryptor*> mDecryptors;
+  std::map<std::string, ClearKeySession*> mSessions;
+};
+
+#endif // __ClearKeyDecryptor_h__
new file mode 100644
--- /dev/null
+++ b/media/gmp-clearkey/0.1/ClearKeySession.cpp
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ClearKeySession.h"
+#include "ClearKeyUtils.h"
+
+#include "gmp-api/gmp-decryption.h"
+#include "mozilla/Endian.h"
+#include "pk11pub.h"
+
+using namespace mozilla;
+
+ClearKeySession::ClearKeySession(const std::string& aSessionId,
+                                 GMPDecryptorHost* aHost,
+                                 GMPDecryptorCallback* aCallback)
+  : mSessionId(aSessionId)
+  , mHost(aHost)
+  , mCallback(aCallback)
+{
+  CK_LOGD("ClearKeySession ctor %p", this);
+}
+
+ClearKeySession::~ClearKeySession()
+{
+  CK_LOGD("ClearKeySession dtor %p", this);
+}
+
+void
+ClearKeySession::Init(uint32_t aPromiseId,
+                      const uint8_t* aInitData, uint32_t aInitDataSize)
+{
+  CK_LOGD("ClearKeySession::Init");
+
+  ClearKeyUtils::ParseInitData(aInitData, aInitDataSize, mKeyIds);
+  if (!mKeyIds.size()) {
+    const char message[] = "Couldn't parse cenc key init data";
+    mCallback->RejectPromise(aPromiseId, kGMPAbortError, message, strlen(message));
+    return;
+  }
+
+  mCallback->ResolveNewSessionPromise(aPromiseId,
+                                      mSessionId.data(), mSessionId.length());
+}
new file mode 100644
--- /dev/null
+++ b/media/gmp-clearkey/0.1/ClearKeySession.h
@@ -0,0 +1,40 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __ClearKeySession_h__
+#define __ClearKeySession_h__
+
+#include "ClearKeyUtils.h"
+
+class GMPBuffer;
+class GMPDecryptorCallback;
+class GMPDecryptorHost;
+class GMPEncryptedBufferMetadata;
+
+/**
+ * Currently useless; will be fleshed out later with support for persistent
+ * key sessions.
+ */
+
+class ClearKeySession
+{
+public:
+  ClearKeySession(const std::string& aSessionId,
+                  GMPDecryptorHost* aHost, GMPDecryptorCallback *aCallback);
+
+  ~ClearKeySession();
+
+  const std::vector<KeyId>& GetKeyIds() { return mKeyIds; }
+
+  void Init(uint32_t aPromiseId,
+            const uint8_t* aInitData, uint32_t aInitDataSize);
+private:
+  std::string mSessionId;
+  std::vector<KeyId> mKeyIds;
+
+  GMPDecryptorCallback* mCallback;
+  GMPDecryptorHost* mHost;
+};
+
+#endif // __ClearKeySession_h__
new file mode 100644
--- /dev/null
+++ b/media/gmp-clearkey/0.1/ClearKeyUtils.cpp
@@ -0,0 +1,556 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <assert.h>
+#include <ctype.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <vector>
+
+#include "ClearKeyUtils.h"
+#include "mozilla/Endian.h"
+#include "mozilla/NullPtr.h"
+#include "openaes/oaes_lib.h"
+
+using namespace std;
+
+#define FOURCC(a,b,c,d) ((a << 24) + (b << 16) + (c << 8) + d)
+
+// System ID identifying the cenc v2 pssh box format; specified at:
+// https://dvcs.w3.org/hg/html-media/raw-file/tip/encrypted-media/cenc-format.html
+const uint8_t kSystemID[] = {
+  0x10, 0x77, 0xef, 0xec, 0xc0, 0xb2, 0x4d, 0x02,
+  0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb, 0x4b
+};
+
+void
+CK_Log(const char* aFmt, ...)
+{
+  va_list ap;
+
+  va_start(ap, aFmt);
+  vprintf(aFmt, ap);
+  va_end(ap);
+
+  printf("\n");
+}
+
+static void
+IncrementIV(vector<uint8_t>& aIV) {
+  using mozilla::BigEndian;
+
+  assert(aIV.size() == 16);
+  BigEndian::writeUint64(&aIV[8], BigEndian::readUint64(&aIV[8]) + 1);
+}
+
+/* static */ void
+ClearKeyUtils::DecryptAES(const vector<uint8_t>& aKey,
+                          vector<uint8_t>& aData, vector<uint8_t>& aIV)
+{
+  assert(aIV.size() == CLEARKEY_KEY_LEN);
+  assert(aKey.size() == CLEARKEY_KEY_LEN);
+
+  OAES_CTX* aes = oaes_alloc();
+  oaes_key_import_data(aes, &aKey[0], aKey.size());
+  oaes_set_option(aes, OAES_OPTION_ECB, nullptr);
+
+  for (size_t i = 0; i < aData.size(); i += CLEARKEY_KEY_LEN) {
+    size_t encLen;
+    oaes_encrypt(aes, &aIV[0], CLEARKEY_KEY_LEN, nullptr, &encLen);
+
+    vector<uint8_t> enc(encLen);
+    oaes_encrypt(aes, &aIV[0], CLEARKEY_KEY_LEN, &enc[0], &encLen);
+
+    for (size_t j = 0; j < CLEARKEY_KEY_LEN; j++) {
+      aData[i + j] ^= enc[2 * OAES_BLOCK_SIZE + j];
+    }
+    IncrementIV(aIV);
+  }
+
+  oaes_free(&aes);
+}
+
+/**
+ * ClearKey expects all Key IDs to be base64 encoded with non-standard alphabet
+ * and padding.
+ */
+static bool
+EncodeBase64Web(vector<uint8_t> aBinary, string& aEncoded)
+{
+  const char sAlphabet[] =
+    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
+  const uint8_t sMask = 0x3f;
+
+  aEncoded.resize((aBinary.size() * 8 + 5) / 6);
+
+  // Pad binary data in case there's rubbish past the last byte.
+  aBinary.push_back(0);
+
+  // Number of bytes not consumed in the previous character
+  uint32_t shift = 0;
+
+  auto out = aEncoded.begin();
+  auto data = aBinary.begin();
+  for (int i = 0; i < aEncoded.length(); i++) {
+    if (shift) {
+      out[i] = (*data << (6 - shift)) & sMask;
+      data++;
+    } else {
+      out[i] = 0;
+    }
+
+    out[i] += (*data >> (shift + 2)) & sMask;
+    shift = (shift + 2) % 8;
+
+    out[i] = sAlphabet[out[i]];
+  }
+
+  return true;
+}
+
+/* static */ void
+ClearKeyUtils::ParseInitData(const uint8_t* aInitData, uint32_t aInitDataSize,
+                             vector<KeyId>& aOutKeys)
+{
+  using mozilla::BigEndian;
+
+  uint32_t size = 0;
+  for (uint32_t offset = 0; offset + sizeof(uint32_t) < aInitDataSize; offset += size) {
+    const uint8_t* data = aInitData + offset;
+    size = BigEndian::readUint32(data); data += sizeof(uint32_t);
+
+    CK_LOGD("Looking for pssh at offset %u", offset);
+
+    if (size + offset > aInitDataSize) {
+      CK_LOGE("Box size %u overflows init data buffer", size);
+      return;
+    }
+
+    if (size < 36) {
+      // Too small to be a cenc2 pssh box
+      continue;
+    }
+
+    uint32_t box = BigEndian::readUint32(data); data += sizeof(uint32_t);
+    if (box != FOURCC('p','s','s','h')) {
+      CK_LOGE("ClearKey CDM passed non-pssh initData");
+      return;
+    }
+
+    uint32_t head = BigEndian::readUint32(data); data += sizeof(uint32_t);
+    CK_LOGD("Got version %u pssh box, length %u", head & 0xff, size);
+
+    if ((head >> 24) != 1) {
+      // Ignore pssh boxes with wrong version
+      CK_LOGD("Ignoring pssh box with wrong version");
+      continue;
+    }
+
+    if (memcmp(kSystemID, data, sizeof(kSystemID))) {
+      // Ignore pssh boxes with wrong system ID
+      continue;
+    }
+    data += sizeof(kSystemID);
+
+    uint32_t kidCount = BigEndian::readUint32(data); data += sizeof(uint32_t);
+    if (data + kidCount * CLEARKEY_KEY_LEN > aInitData + aInitDataSize) {
+      CK_LOGE("pssh key IDs overflow init data buffer");
+      return;
+    }
+
+    for (uint32_t i = 0; i < kidCount; i++) {
+      aOutKeys.push_back(KeyId(data, data + CLEARKEY_KEY_LEN));
+      data += CLEARKEY_KEY_LEN;
+    }
+  }
+}
+
+/* static */ void
+ClearKeyUtils::MakeKeyRequest(const vector<KeyId>& aKeyIDs,
+                              string& aOutRequest)
+{
+  MOZ_ASSERT(aKeyIDs.size() && aOutRequest.empty());
+
+  aOutRequest.append("{ \"kids\":[");
+  for (size_t i = 0; i < aKeyIDs.size(); i++) {
+    if (i) {
+      aOutRequest.append(",");
+    }
+    aOutRequest.append("\"");
+
+    string base64key;
+    EncodeBase64Web(aKeyIDs[i], base64key);
+    aOutRequest.append(base64key);
+
+    aOutRequest.append("\"");
+  }
+  aOutRequest.append("], \"type\":");
+  // TODO implement "persistent" session type
+  aOutRequest.append("\"temporary\"");
+  aOutRequest.append("}");
+}
+
+#define EXPECT_SYMBOL(CTX, X) do { \
+  if (GetNextSymbol(CTX) != (X)) { \
+    CK_LOGE("Unexpected symbol in JWK parser"); \
+    return false; \
+  } \
+} while (false)
+
+struct ParserContext {
+  const uint8_t* mIter;
+  const uint8_t* mEnd;
+};
+
+static uint8_t
+PeekSymbol(ParserContext& aCtx)
+{
+  for (; aCtx.mIter < aCtx.mEnd; (aCtx.mIter)++) {
+    if (!isspace(*aCtx.mIter)) {
+      return *aCtx.mIter;
+    }
+  }
+
+  return 0;
+}
+
+static uint8_t
+GetNextSymbol(ParserContext& aCtx)
+{
+  uint8_t sym = PeekSymbol(aCtx);
+  aCtx.mIter++;
+  return sym;
+}
+
+static bool SkipToken(ParserContext& aCtx);
+
+static bool
+SkipString(ParserContext& aCtx)
+{
+  EXPECT_SYMBOL(aCtx, '"');
+  for (uint8_t sym = GetNextSymbol(aCtx); sym; sym = GetNextSymbol(aCtx)) {
+    if (sym == '\\') {
+      sym = GetNextSymbol(aCtx);
+    } else if (sym == '"') {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+/**
+ * Skip whole object and values it contains.
+ */
+static bool
+SkipObject(ParserContext& aCtx)
+{
+  EXPECT_SYMBOL(aCtx, '{');
+
+  if (PeekSymbol(aCtx) == '}') {
+    GetNextSymbol(aCtx);
+    return true;
+  }
+
+  while (true) {
+    if (!SkipString(aCtx)) return false;
+    EXPECT_SYMBOL(aCtx, ':');
+    if (!SkipToken(aCtx)) return false;
+
+    if (PeekSymbol(aCtx) == '}') {
+      GetNextSymbol(aCtx);
+      return true;
+    }
+    EXPECT_SYMBOL(aCtx, ',');
+  }
+
+  return false;
+}
+
+/**
+ * Skip array value and the values it contains.
+ */
+static bool
+SkipArray(ParserContext& aCtx)
+{
+  EXPECT_SYMBOL(aCtx, '[');
+
+  if (PeekSymbol(aCtx) == ']') {
+    GetNextSymbol(aCtx);
+    return true;
+  }
+
+  while (SkipToken(aCtx)) {
+    if (PeekSymbol(aCtx) == ']') {
+      GetNextSymbol(aCtx);
+      return true;
+    }
+    EXPECT_SYMBOL(aCtx, ',');
+  }
+
+  return false;
+}
+
+/**
+ * Skip unquoted literals like numbers, |true|, and |null|.
+ * (XXX and anything else that matches /([:alnum:]|[+-.])+/)
+ */
+static bool
+SkipLiteral(ParserContext& aCtx)
+{
+  for (; aCtx.mIter < aCtx.mEnd; aCtx.mIter++) {
+    if (!isalnum(*aCtx.mIter) &&
+        *aCtx.mIter != '.' && *aCtx.mIter != '-' && *aCtx.mIter != '+') {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+static bool
+SkipToken(ParserContext& aCtx)
+{
+  uint8_t startSym = PeekSymbol(aCtx);
+  if (startSym == '"') {
+    CK_LOGD("JWK parser skipping string");
+    return SkipString(aCtx);
+  } else if (startSym == '{') {
+    CK_LOGD("JWK parser skipping object");
+    return SkipObject(aCtx);
+  } else if (startSym == '[') {
+    CK_LOGD("JWK parser skipping array");
+    return SkipArray(aCtx);
+  } else {
+    CK_LOGD("JWK parser skipping literal");
+    return SkipLiteral(aCtx);
+  }
+
+  return false;
+}
+
+static bool
+GetNextLabel(ParserContext& aCtx, string& aOutLabel)
+{
+  EXPECT_SYMBOL(aCtx, '"');
+
+  const uint8_t* start = aCtx.mIter;
+  for (uint8_t sym = GetNextSymbol(aCtx); sym; sym = GetNextSymbol(aCtx)) {
+    if (sym == '\\') {
+      GetNextSymbol(aCtx);
+      continue;
+    }
+
+    if (sym == '"') {
+      aOutLabel.assign(start, aCtx.mIter - 1);
+      return true;
+    }
+  }
+
+  return false;
+}
+
+/**
+ * Take a base64-encoded string, convert (in-place) each character to its
+ * corresponding value in the [0x00, 0x3f] range, and truncate any padding.
+ */
+static bool
+Decode6Bit(string& aStr)
+{
+  for (size_t i = 0; i < aStr.length(); i++) {
+    if (aStr[i] >= 'A' && aStr[i] <= 'Z') {
+      aStr[i] -= 'A';
+    } else if (aStr[i] >= 'a' && aStr[i] <= 'z') {
+      aStr[i] -= 'a' - 26;
+    } else if (aStr[i] >= '0' && aStr[i] <= '9') {
+      aStr[i] -= '0' - 52;
+    } else if (aStr[i] == '-' || aStr[i] == '+') {
+      aStr[i] = 62;
+    } else if (aStr[i] == '_' || aStr[i] == '/') {
+      aStr[i] = 63;
+    } else {
+      // Truncate '=' padding at the end of the aString.
+      if (aStr[i] != '=') {
+        return false;
+      }
+      aStr[i] = '\0';
+      aStr.resize(i);
+      break;
+    }
+  }
+
+  return true;
+}
+
+static bool
+DecodeBase64(string& aEncoded, vector<uint8_t>& aOutDecoded)
+{
+  if (!Decode6Bit(aEncoded)) {
+    return false;
+  }
+
+  // The number of bytes we haven't yet filled in the current byte, mod 8.
+  int shift = 0;
+
+  aOutDecoded.resize(aEncoded.length() * 6 / 8);
+  aOutDecoded.reserve(aEncoded.length() * 6 / 8 + 1);
+  auto out = aOutDecoded.begin();
+  for (size_t i = 0; i < aEncoded.length(); i++) {
+    if (!shift) {
+      *out = aEncoded[i] << 2;
+    } else {
+      *out |= aEncoded[i] >> (6 - shift);
+      *(++out) = aEncoded[i] << (shift + 2);
+    }
+    shift = (shift + 2) % 8;
+  }
+
+  return true;
+}
+
+static bool
+DecodeKey(string& aEncoded, Key& aOutDecoded)
+{
+  return DecodeBase64(aEncoded, aOutDecoded) &&
+    // Key should be 128 bits long.
+    aOutDecoded.size() == CLEARKEY_KEY_LEN;
+}
+
+static bool
+ParseKeyObject(ParserContext& aCtx, KeyIdPair& aOutKey, bool& aOutValid)
+{
+  aOutValid = false;
+
+  EXPECT_SYMBOL(aCtx, '{');
+
+  // Ignore empty objects
+  if (PeekSymbol(aCtx) == '}') {
+    GetNextSymbol(aCtx);
+    return true;
+  }
+
+  // By spec, type should be "oct".
+  bool isExpectedType = false;
+  // By spec, alg should be "A128KW".
+  bool isExpectedAlg = false;
+
+  string keyId;
+  string key;
+
+  while (true) {
+    string label;
+    string value;
+
+    if (!GetNextLabel(aCtx, label)) {
+      return false;
+    }
+
+    EXPECT_SYMBOL(aCtx, ':');
+    if (label == "kty") {
+      if (!GetNextLabel(aCtx, value)) return false;
+      isExpectedType = value == "oct";
+    } else if (label == "alg") {
+      if (!GetNextLabel(aCtx, value)) return false;
+      isExpectedAlg = value == "A128KW";
+    } else if (label == "k" && PeekSymbol(aCtx) == '"') {
+      // if this isn't a string we will fall through to the SkipToken() path.
+      if (!GetNextLabel(aCtx, key)) return false;
+    } else if (label == "kid" && PeekSymbol(aCtx) == '"') {
+      if (!GetNextLabel(aCtx, keyId)) return false;
+    } else {
+      if (!SkipToken(aCtx)) return false;
+    }
+
+    uint8_t sym = PeekSymbol(aCtx);
+    if (!sym || sym == '}') {
+      break;
+    }
+    EXPECT_SYMBOL(aCtx, ',');
+  }
+
+  if (isExpectedType && isExpectedAlg &&
+      !key.empty() && !keyId.empty() &&
+      DecodeBase64(keyId, aOutKey.mKeyId) &&
+      DecodeKey(key, aOutKey.mKey)) {
+    aOutValid = true;
+  }
+
+  return GetNextSymbol(aCtx) == '}';
+}
+
+static bool
+ParseKeys(ParserContext& aCtx, vector<KeyIdPair>& aOutKeys)
+{
+  // Consume start of array.
+  EXPECT_SYMBOL(aCtx, '[');
+
+  while (true) {
+    KeyIdPair key;
+    bool valid;
+    if (!ParseKeyObject(aCtx, key, valid)) {
+      CK_LOGE("Failed to parse key object");
+      return false;
+    }
+
+    if (valid) {
+      aOutKeys.push_back(key);
+    }
+
+    uint8_t sym = PeekSymbol(aCtx);
+    if (!sym || sym == ']') {
+      break;
+    }
+
+    EXPECT_SYMBOL(aCtx, ',');
+  }
+
+  return GetNextSymbol(aCtx) == ']';
+}
+
+/* static */ bool
+ClearKeyUtils::ParseJWK(const uint8_t* aKeyData, uint32_t aKeyDataSize,
+                        vector<KeyIdPair>& aOutKeys)
+{
+  ParserContext ctx;
+  ctx.mIter = aKeyData;
+  ctx.mEnd = aKeyData + aKeyDataSize;
+
+  // Consume '{' from start of object.
+  EXPECT_SYMBOL(ctx, '{');
+
+  while (true) {
+    string label;
+    // Consume member key.
+    if (!GetNextLabel(ctx, label)) return false;
+    EXPECT_SYMBOL(ctx, ':');
+
+    if (label == "keys") {
+      // Parse "keys" array.
+      if (!ParseKeys(ctx, aOutKeys)) return false;
+    } else if (label == "type") {
+      // Consume type string.
+      string type;
+      if (!GetNextLabel(ctx, type)) return false;
+      // XXX todo support "persistent" session type
+      if (type != "temporary") {
+        return false;
+      }
+    } else {
+      SkipToken(ctx);
+    }
+
+    // Check for end of object.
+    if (PeekSymbol(ctx) == '}') {
+      break;
+    }
+
+    // Consume ',' between object members.
+    EXPECT_SYMBOL(ctx, ',');
+  }
+
+  // Consume '}' from end of object.
+  EXPECT_SYMBOL(ctx, '}');
+
+  return true;
+}
new file mode 100644
--- /dev/null
+++ b/media/gmp-clearkey/0.1/ClearKeyUtils.h
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __ClearKeyUtils_h__
+#define __ClearKeyUtils_h__
+
+#include <stdint.h>
+#include <string>
+#include <vector>
+
+#define CLEARKEY_KEY_LEN 16
+
+#if 0
+void CK_Log(const char* aFmt, ...);
+#define CK_LOGE(...) CK_Log(__VA_ARGS__)
+#define CK_LOGD(...) CK_Log(__VA_ARGS__)
+#define CK_LOGW(...) CK_Log(__VA_ARGS__)
+#else
+#define CK_LOGE(...)
+#define CK_LOGD(...)
+#define CK_LOGW(...)
+#endif
+
+struct GMPPlatformAPI;
+extern GMPPlatformAPI* GetPlatform();
+
+typedef std::vector<uint8_t> KeyId;
+typedef std::vector<uint8_t> Key;
+
+struct KeyIdPair
+{
+  KeyId mKeyId;
+  Key mKey;
+};
+
+class ClearKeyUtils
+{
+public:
+  static void DecryptAES(const std::vector<uint8_t>& aKey,
+                         std::vector<uint8_t>& aData, std::vector<uint8_t>& aIV);
+
+  static void ParseInitData(const uint8_t* aInitData, uint32_t aInitDataSize,
+                            std::vector<Key>& aOutKeys);
+
+  static void MakeKeyRequest(const std::vector<KeyId>& aKeyIds,
+                             std::string& aOutRequest);
+
+  static bool ParseJWK(const uint8_t* aKeyData, uint32_t aKeyDataSize,
+                       std::vector<KeyIdPair>& aOutKeys);
+};
+
+#endif // __ClearKeyUtils_h__
new file mode 100644
--- /dev/null
+++ b/media/gmp-clearkey/0.1/Makefile.in
@@ -0,0 +1,8 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+include $(topsrcdir)/config/rules.mk
+
+libs::
+	cp $(srcdir)/clearkey.info .
new file mode 100644
--- /dev/null
+++ b/media/gmp-clearkey/0.1/clearkey.info
@@ -0,0 +1,4 @@
+Name: clearkey
+Description: ClearKey decrypt-only GMP plugin
+Version: 0.1
+APIs: eme-decrypt[org.w3.clearkey]
new file mode 100644
--- /dev/null
+++ b/media/gmp-clearkey/0.1/gmp-clearkey.cpp
@@ -0,0 +1,49 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <stdio.h>
+#include <string.h>
+
+#include "ClearKeyDecryptionManager.h"
+
+#include "gmp-api/gmp-decryption.h"
+#include "gmp-api/gmp-platform.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/NullPtr.h"
+
+static GMPPlatformAPI* sPlatform = nullptr;
+GMPPlatformAPI*
+GetPlatform()
+{
+  return sPlatform;
+}
+
+extern "C" {
+
+MOZ_EXPORT GMPErr
+GMPInit(GMPPlatformAPI* aPlatformAPI)
+{
+  sPlatform = aPlatformAPI;
+  return GMPNoErr;
+}
+
+MOZ_EXPORT GMPErr
+GMPGetAPI(const char* aApiName, void* aHostAPI, void** aPluginAPI)
+{
+  if (strcmp(aApiName, "eme-decrypt")) {
+    return GMPNotImplementedErr;
+  }
+
+  *aPluginAPI = new ClearKeyDecryptionManager(static_cast<GMPDecryptorHost*>(aHostAPI));
+
+  return GMPNoErr;
+}
+
+MOZ_EXPORT GMPErr
+GMPShutdown(void)
+{
+  return GMPNoErr;
+}
+
+}
new file mode 100644
--- /dev/null
+++ b/media/gmp-clearkey/0.1/moz.build
@@ -0,0 +1,23 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+SharedLibrary('clearkey')
+
+SOURCES += [
+    'ClearKeyDecryptionManager.cpp',
+    'ClearKeySession.cpp',
+    'ClearKeyUtils.cpp',
+    'gmp-clearkey.cpp',
+    'openaes/oaes_lib.c',
+    'openaes/rand.c',
+]
+
+LOCAL_INCLUDES += [
+    '/content/media/gmp',
+]
+
+USE_STATIC_LIBS = True
+USE_LIBS += [ 'mozalloc' ]
--- a/toolkit/toolkit.mozbuild
+++ b/toolkit/toolkit.mozbuild
@@ -167,16 +167,18 @@ if not CONFIG['MOZ_ENABLE_QTNETWORK'] an
 
 add_tier_dir('platform', 'addon-sdk')
 
 if CONFIG['ENABLE_MARIONETTE'] or CONFIG['MOZ_WIDGET_TOOLKIT'] not in ('gonk', 'android'):
     add_tier_dir('platform', 'testing/marionette')
 
 add_tier_dir('platform', 'tools/quitter')
 
+add_tier_dir('platform', 'media/gmp-clearkey/0.1')
+
 if CONFIG['ENABLE_TESTS']:
     add_tier_dir('platform', [
         'testing/mochitest',
         'testing/xpcshell',
         'testing/tools/screenshot',
         'testing/profiles',
         'testing/mozbase',
         'testing/modules',