diff --git a/media/gmp-clearkey/0.1/ClearKeyDecryptionManager.cpp b/media/gmp-clearkey/0.1/ClearKeyDecryptionManager.cpp new file mode 100644 index 000000000000..635c5db59cc1 --- /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 +#include +#include +#include + +#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& sessionKeys = session->GetKeyIds(); + vector 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 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& 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 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 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; +} diff --git a/media/gmp-clearkey/0.1/ClearKeyDecryptionManager.h b/media/gmp-clearkey/0.1/ClearKeyDecryptionManager.h new file mode 100644 index 000000000000..01b12307ceff --- /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 +#include +#include + +#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 mDecryptors; + std::map mSessions; +}; + +#endif // __ClearKeyDecryptor_h__ diff --git a/media/gmp-clearkey/0.1/ClearKeySession.cpp b/media/gmp-clearkey/0.1/ClearKeySession.cpp new file mode 100644 index 000000000000..8ba769054fbd --- /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()); +} diff --git a/media/gmp-clearkey/0.1/ClearKeySession.h b/media/gmp-clearkey/0.1/ClearKeySession.h new file mode 100644 index 000000000000..b798db96a31a --- /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& GetKeyIds() { return mKeyIds; } + + void Init(uint32_t aPromiseId, + const uint8_t* aInitData, uint32_t aInitDataSize); +private: + std::string mSessionId; + std::vector mKeyIds; + + GMPDecryptorCallback* mCallback; + GMPDecryptorHost* mHost; +}; + +#endif // __ClearKeySession_h__ diff --git a/media/gmp-clearkey/0.1/ClearKeyUtils.cpp b/media/gmp-clearkey/0.1/ClearKeyUtils.cpp new file mode 100644 index 000000000000..3dffa2a9eec7 --- /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 +#include +#include +#include +#include + +#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& aIV) { + using mozilla::BigEndian; + + assert(aIV.size() == 16); + BigEndian::writeUint64(&aIV[8], BigEndian::readUint64(&aIV[8]) + 1); +} + +/* static */ void +ClearKeyUtils::DecryptAES(const vector& aKey, + vector& aData, vector& 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 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 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& 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& 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& 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& 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& 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; +} diff --git a/media/gmp-clearkey/0.1/ClearKeyUtils.h b/media/gmp-clearkey/0.1/ClearKeyUtils.h new file mode 100644 index 000000000000..9313f12701cc --- /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 +#include +#include + +#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 KeyId; +typedef std::vector Key; + +struct KeyIdPair +{ + KeyId mKeyId; + Key mKey; +}; + +class ClearKeyUtils +{ +public: + static void DecryptAES(const std::vector& aKey, + std::vector& aData, std::vector& aIV); + + static void ParseInitData(const uint8_t* aInitData, uint32_t aInitDataSize, + std::vector& aOutKeys); + + static void MakeKeyRequest(const std::vector& aKeyIds, + std::string& aOutRequest); + + static bool ParseJWK(const uint8_t* aKeyData, uint32_t aKeyDataSize, + std::vector& aOutKeys); +}; + +#endif // __ClearKeyUtils_h__ diff --git a/media/gmp-clearkey/0.1/Makefile.in b/media/gmp-clearkey/0.1/Makefile.in new file mode 100644 index 000000000000..b7bbbaea921b --- /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 . diff --git a/media/gmp-clearkey/0.1/clearkey.info b/media/gmp-clearkey/0.1/clearkey.info new file mode 100644 index 000000000000..ff77af8f7139 --- /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] diff --git a/media/gmp-clearkey/0.1/gmp-clearkey.cpp b/media/gmp-clearkey/0.1/gmp-clearkey.cpp new file mode 100644 index 000000000000..53a005c30755 --- /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 +#include + +#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(aHostAPI)); + + return GMPNoErr; +} + +MOZ_EXPORT GMPErr +GMPShutdown(void) +{ + return GMPNoErr; +} + +} diff --git a/media/gmp-clearkey/0.1/moz.build b/media/gmp-clearkey/0.1/moz.build new file mode 100644 index 000000000000..2f00fce949fc --- /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' ] diff --git a/toolkit/toolkit.mozbuild b/toolkit/toolkit.mozbuild index 0d7b6a4b453f..41d287f24762 100644 --- a/toolkit/toolkit.mozbuild +++ b/toolkit/toolkit.mozbuild @@ -172,6 +172,8 @@ if CONFIG['ENABLE_MARIONETTE'] or CONFIG['MOZ_WIDGET_TOOLKIT'] not in ('gonk', ' 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',