diff options
Diffstat (limited to 'dom/media/eme/clearkey')
20 files changed, 3018 insertions, 0 deletions
diff --git a/dom/media/eme/clearkey/ArrayUtils.h b/dom/media/eme/clearkey/ArrayUtils.h new file mode 100644 index 0000000000..ae5f33b68e --- /dev/null +++ b/dom/media/eme/clearkey/ArrayUtils.h @@ -0,0 +1,22 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __ArrayUtils_h__ +#define __ArrayUtils_h__ + +#define MOZ_ARRAY_LENGTH(array_) (sizeof(array_) / sizeof(array_[0])) + +#endif diff --git a/dom/media/eme/clearkey/BigEndian.h b/dom/media/eme/clearkey/BigEndian.h new file mode 100644 index 0000000000..5ea1b6042f --- /dev/null +++ b/dom/media/eme/clearkey/BigEndian.h @@ -0,0 +1,59 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __BigEndian_h__ +#define __BigEndian_h__ + +#include <stdint.h> + +namespace mozilla { + +class BigEndian { + public: + static uint32_t readUint32(const void* aPtr) { + const uint8_t* p = reinterpret_cast<const uint8_t*>(aPtr); + return uint32_t(p[0]) << 24 | uint32_t(p[1]) << 16 | uint32_t(p[2]) << 8 | + uint32_t(p[3]); + } + + static uint16_t readUint16(const void* aPtr) { + const uint8_t* p = reinterpret_cast<const uint8_t*>(aPtr); + return uint32_t(p[0]) << 8 | uint32_t(p[1]); + } + + static uint64_t readUint64(const void* aPtr) { + const uint8_t* p = reinterpret_cast<const uint8_t*>(aPtr); + return uint64_t(p[0]) << 56 | uint64_t(p[1]) << 48 | uint64_t(p[2]) << 40 | + uint64_t(p[3]) << 32 | uint64_t(p[4]) << 24 | uint64_t(p[5]) << 16 | + uint64_t(p[6]) << 8 | uint64_t(p[7]); + } + + static void writeUint64(void* aPtr, uint64_t aValue) { + uint8_t* p = reinterpret_cast<uint8_t*>(aPtr); + p[0] = uint8_t(aValue >> 56) & 0xff; + p[1] = uint8_t(aValue >> 48) & 0xff; + p[2] = uint8_t(aValue >> 40) & 0xff; + p[3] = uint8_t(aValue >> 32) & 0xff; + p[4] = uint8_t(aValue >> 24) & 0xff; + p[5] = uint8_t(aValue >> 16) & 0xff; + p[6] = uint8_t(aValue >> 8) & 0xff; + p[7] = uint8_t(aValue) & 0xff; + } +}; + +} // namespace mozilla + +#endif // __BigEndian_h__ diff --git a/dom/media/eme/clearkey/ClearKeyBase64.cpp b/dom/media/eme/clearkey/ClearKeyBase64.cpp new file mode 100644 index 0000000000..bb610afd1b --- /dev/null +++ b/dom/media/eme/clearkey/ClearKeyBase64.cpp @@ -0,0 +1,90 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ClearKeyBase64.h" + +#include <algorithm> + +using std::string; +using std::vector; + +/** + * 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] != '=') { + aStr.erase(i, string::npos); + return false; + } + aStr[i] = '\0'; + aStr.resize(i); + break; + } + } + + return true; +} + +bool DecodeBase64(const string& aEncoded, vector<uint8_t>& aOutDecoded) { + if (aEncoded.empty()) { + aOutDecoded.clear(); + return true; + } + if (aEncoded.size() == 1) { + // Invalid Base64 encoding. + return false; + } + string encoded = aEncoded; + if (!Decode6Bit(encoded)) { + return false; + } + + // The number of bytes we haven't yet filled in the current byte, mod 8. + int shift = 0; + + aOutDecoded.resize((encoded.size() * 3) / 4); + vector<uint8_t>::iterator out = aOutDecoded.begin(); + for (size_t i = 0; i < encoded.length(); i++) { + if (!shift) { + *out = encoded[i] << 2; + } else { + *out |= encoded[i] >> (6 - shift); + out++; + if (out == aOutDecoded.end()) { + // Hit last 6bit octed in encoded, which is padding and can be ignored. + break; + } + *out = encoded[i] << (shift + 2); + } + shift = (shift + 2) % 8; + } + + return true; +} diff --git a/dom/media/eme/clearkey/ClearKeyBase64.h b/dom/media/eme/clearkey/ClearKeyBase64.h new file mode 100644 index 0000000000..39309c031e --- /dev/null +++ b/dom/media/eme/clearkey/ClearKeyBase64.h @@ -0,0 +1,29 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __ClearKeyBase64_h__ +#define __ClearKeyBase64_h__ + +#include <stdint.h> + +#include <string> +#include <vector> + +// Decodes a base64 encoded string. Returns true on success. +bool DecodeBase64(const std::string& aEncoded, + std::vector<uint8_t>& aOutDecoded); + +#endif diff --git a/dom/media/eme/clearkey/ClearKeyDecryptionManager.cpp b/dom/media/eme/clearkey/ClearKeyDecryptionManager.cpp new file mode 100644 index 0000000000..7eeca88985 --- /dev/null +++ b/dom/media/eme/clearkey/ClearKeyDecryptionManager.cpp @@ -0,0 +1,289 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ClearKeyDecryptionManager.h" + +#include <assert.h> +#include <string.h> + +#include <vector> +#include <algorithm> + +#include "mozilla/CheckedInt.h" +#include "mozilla/Span.h" +#include "psshparser/PsshParser.h" + +using namespace cdm; + +bool AllZero(const std::vector<uint32_t>& aBytes) { + return all_of(aBytes.begin(), aBytes.end(), + [](uint32_t b) { return b == 0; }); +} + +class ClearKeyDecryptor : public RefCounted { + public: + ClearKeyDecryptor(); + + void InitKey(const Key& aKey); + bool HasKey() const { return !mKey.empty(); } + + Status Decrypt(uint8_t* aBuffer, uint32_t aBufferSize, + const CryptoMetaData& aMetadata); + + const Key& DecryptionKey() const { return mKey; } + + private: + ~ClearKeyDecryptor(); + + Key mKey; +}; + +/* static */ +ClearKeyDecryptionManager* ClearKeyDecryptionManager::sInstance = nullptr; + +/* static */ +ClearKeyDecryptionManager* ClearKeyDecryptionManager::Get() { + if (!sInstance) { + sInstance = new ClearKeyDecryptionManager(); + } + return sInstance; +} + +ClearKeyDecryptionManager::ClearKeyDecryptionManager() { + CK_LOGD("ClearKeyDecryptionManager::ClearKeyDecryptionManager"); +} + +ClearKeyDecryptionManager::~ClearKeyDecryptionManager() { + CK_LOGD("ClearKeyDecryptionManager::~ClearKeyDecryptionManager"); + + sInstance = nullptr; + + for (auto it = mDecryptors.begin(); it != mDecryptors.end(); it++) { + it->second->Release(); + } + mDecryptors.clear(); +} + +bool ClearKeyDecryptionManager::HasSeenKeyId(const KeyId& aKeyId) const { + CK_LOGD("ClearKeyDecryptionManager::SeenKeyId %s", + mDecryptors.find(aKeyId) != mDecryptors.end() ? "t" : "f"); + return mDecryptors.find(aKeyId) != mDecryptors.end(); +} + +bool ClearKeyDecryptionManager::IsExpectingKeyForKeyId( + const KeyId& aKeyId) const { + CK_LOGARRAY("ClearKeyDecryptionManager::IsExpectingKeyForId ", aKeyId.data(), + aKeyId.size()); + const auto& decryptor = mDecryptors.find(aKeyId); + return decryptor != mDecryptors.end() && !decryptor->second->HasKey(); +} + +bool ClearKeyDecryptionManager::HasKeyForKeyId(const KeyId& aKeyId) const { + CK_LOGD("ClearKeyDecryptionManager::HasKeyForKeyId"); + const auto& decryptor = mDecryptors.find(aKeyId); + return decryptor != mDecryptors.end() && decryptor->second->HasKey(); +} + +const Key& ClearKeyDecryptionManager::GetDecryptionKey(const KeyId& aKeyId) { + assert(HasKeyForKeyId(aKeyId)); + return mDecryptors[aKeyId]->DecryptionKey(); +} + +void ClearKeyDecryptionManager::InitKey(KeyId aKeyId, Key aKey) { + CK_LOGD("ClearKeyDecryptionManager::InitKey ", aKeyId.data(), aKeyId.size()); + if (IsExpectingKeyForKeyId(aKeyId)) { + CK_LOGARRAY("Initialized Key ", aKeyId.data(), aKeyId.size()); + mDecryptors[aKeyId]->InitKey(aKey); + } else { + CK_LOGARRAY("Failed to initialize key ", aKeyId.data(), aKeyId.size()); + } +} + +void ClearKeyDecryptionManager::ExpectKeyId(KeyId aKeyId) { + CK_LOGD("ClearKeyDecryptionManager::ExpectKeyId ", aKeyId.data(), + aKeyId.size()); + if (!HasSeenKeyId(aKeyId)) { + mDecryptors[aKeyId] = new ClearKeyDecryptor(); + } + mDecryptors[aKeyId]->AddRef(); +} + +void ClearKeyDecryptionManager::ReleaseKeyId(KeyId aKeyId) { + CK_LOGD("ClearKeyDecryptionManager::ReleaseKeyId"); + assert(HasSeenKeyId(aKeyId)); + + ClearKeyDecryptor* decryptor = mDecryptors[aKeyId]; + if (!decryptor->Release()) { + mDecryptors.erase(aKeyId); + } +} + +Status ClearKeyDecryptionManager::Decrypt(std::vector<uint8_t>& aBuffer, + const CryptoMetaData& aMetadata) { + return Decrypt(&aBuffer[0], aBuffer.size(), aMetadata); +} + +Status ClearKeyDecryptionManager::Decrypt(uint8_t* aBuffer, + uint32_t aBufferSize, + const CryptoMetaData& aMetadata) { + CK_LOGD("ClearKeyDecryptionManager::Decrypt"); + if (!HasKeyForKeyId(aMetadata.mKeyId)) { + CK_LOGARRAY("Unable to find decryptor for keyId: ", aMetadata.mKeyId.data(), + aMetadata.mKeyId.size()); + return Status::kNoKey; + } + + CK_LOGARRAY("Found decryptor for keyId: ", aMetadata.mKeyId.data(), + aMetadata.mKeyId.size()); + return mDecryptors[aMetadata.mKeyId]->Decrypt(aBuffer, aBufferSize, + aMetadata); +} + +ClearKeyDecryptor::ClearKeyDecryptor() { CK_LOGD("ClearKeyDecryptor ctor"); } + +ClearKeyDecryptor::~ClearKeyDecryptor() { + if (HasKey()) { + CK_LOGARRAY("ClearKeyDecryptor dtor; key = ", mKey.data(), mKey.size()); + } else { + CK_LOGD("ClearKeyDecryptor dtor"); + } +} + +void ClearKeyDecryptor::InitKey(const Key& aKey) { mKey = aKey; } + +Status ClearKeyDecryptor::Decrypt(uint8_t* aBuffer, uint32_t aBufferSize, + const CryptoMetaData& aMetadata) { + CK_LOGD("ClearKeyDecryptor::Decrypt"); + // If the sample is split up into multiple encrypted subsamples, we need to + // stitch them into one continuous buffer for decryption. + std::vector<uint8_t> tmp(aBufferSize); + static_assert(sizeof(uintptr_t) == sizeof(uint8_t*), + "We need uintptr_t to be exactly the same size as a pointer"); + + // Decrypt CBCS case: + if (aMetadata.mEncryptionScheme == EncryptionScheme::kCbcs) { + mozilla::CheckedInt<uintptr_t> data = reinterpret_cast<uintptr_t>(aBuffer); + if (!data.isValid()) { + return Status::kDecryptError; + } + const uintptr_t endBuffer = + reinterpret_cast<uintptr_t>(aBuffer + aBufferSize); + + if (aMetadata.NumSubsamples() == 0) { + if (data.value() > endBuffer) { + return Status::kDecryptError; + } + mozilla::Span<uint8_t> encryptedSpan = + mozilla::Span(reinterpret_cast<uint8_t*>(data.value()), aBufferSize); + if (!ClearKeyUtils::DecryptCbcs(mKey, aMetadata.mIV, encryptedSpan, + aMetadata.mCryptByteBlock, + aMetadata.mSkipByteBlock)) { + return Status::kDecryptError; + } + return Status::kSuccess; + } + + for (size_t i = 0; i < aMetadata.NumSubsamples(); i++) { + data += aMetadata.mClearBytes[i]; + if (!data.isValid() || data.value() > endBuffer) { + return Status::kDecryptError; + } + mozilla::CheckedInt<uintptr_t> dataAfterCipher = + data + aMetadata.mCipherBytes[i]; + if (!dataAfterCipher.isValid() || dataAfterCipher.value() > endBuffer) { + // Trying to read past the end of the buffer! + return Status::kDecryptError; + } + mozilla::Span<uint8_t> encryptedSpan = mozilla::Span( + reinterpret_cast<uint8_t*>(data.value()), aMetadata.mCipherBytes[i]); + if (!ClearKeyUtils::DecryptCbcs(mKey, aMetadata.mIV, encryptedSpan, + aMetadata.mCryptByteBlock, + aMetadata.mSkipByteBlock)) { + return Status::kDecryptError; + } + data += aMetadata.mCipherBytes[i]; + if (!data.isValid()) { + return Status::kDecryptError; + } + } + return Status::kSuccess; + } + + // Decrypt CENC case: + if (aMetadata.NumSubsamples()) { + // Take all encrypted parts of subsamples and stitch them into one + // continuous encrypted buffer. + mozilla::CheckedInt<uintptr_t> data = reinterpret_cast<uintptr_t>(aBuffer); + const uintptr_t endBuffer = + reinterpret_cast<uintptr_t>(aBuffer + aBufferSize); + uint8_t* iter = &tmp[0]; + for (size_t i = 0; i < aMetadata.NumSubsamples(); i++) { + data += aMetadata.mClearBytes[i]; + if (!data.isValid() || data.value() > endBuffer) { + // Trying to read past the end of the buffer! + return Status::kDecryptError; + } + const uint32_t& cipherBytes = aMetadata.mCipherBytes[i]; + mozilla::CheckedInt<uintptr_t> dataAfterCipher = data + cipherBytes; + if (!dataAfterCipher.isValid() || dataAfterCipher.value() > endBuffer) { + // Trying to read past the end of the buffer! + return Status::kDecryptError; + } + + memcpy(iter, reinterpret_cast<uint8_t*>(data.value()), cipherBytes); + + data = dataAfterCipher; + iter += cipherBytes; + } + + tmp.resize((size_t)(iter - &tmp[0])); + } else { + memcpy(&tmp[0], aBuffer, aBufferSize); + } + + // It is possible that we could be passed an unencrypted sample, if all + // encrypted sample lengths are zero, and in this case, a zero length + // IV is allowed. + assert(aMetadata.mIV.size() == 8 || aMetadata.mIV.size() == 16 || + (aMetadata.mIV.empty() && AllZero(aMetadata.mCipherBytes))); + + std::vector<uint8_t> iv(aMetadata.mIV); + iv.insert(iv.end(), CENC_KEY_LEN - aMetadata.mIV.size(), 0); + + if (!ClearKeyUtils::DecryptAES(mKey, tmp, iv)) { + return Status::kDecryptError; + } + + if (aMetadata.NumSubsamples()) { + // Take the decrypted buffer, split up into subsamples, and insert those + // subsamples back into their original position in the original buffer. + uint8_t* data = aBuffer; + uint8_t* iter = &tmp[0]; + for (size_t i = 0; i < aMetadata.NumSubsamples(); i++) { + data += aMetadata.mClearBytes[i]; + uint32_t cipherBytes = aMetadata.mCipherBytes[i]; + + memcpy(data, iter, cipherBytes); + + data += cipherBytes; + iter += cipherBytes; + } + } else { + memcpy(aBuffer, &tmp[0], aBufferSize); + } + + return Status::kSuccess; +} diff --git a/dom/media/eme/clearkey/ClearKeyDecryptionManager.h b/dom/media/eme/clearkey/ClearKeyDecryptionManager.h new file mode 100644 index 0000000000..c21ff3119a --- /dev/null +++ b/dom/media/eme/clearkey/ClearKeyDecryptionManager.h @@ -0,0 +1,111 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __ClearKeyDecryptionManager_h__ +#define __ClearKeyDecryptionManager_h__ + +// This include is required in order for content_decryption_module to work +// on Unix systems. +#include <stddef.h> + +#include <map> + +#include "content_decryption_module.h" + +#include "ClearKeyUtils.h" +#include "RefCounted.h" + +class ClearKeyDecryptor; + +class CryptoMetaData { + public: + CryptoMetaData() = default; + + explicit CryptoMetaData(const cdm::InputBuffer_2* aInputBuffer) { + Init(aInputBuffer); + } + + void Init(const cdm::InputBuffer_2* aInputBuffer) { + if (!aInputBuffer) { + assert(!IsValid()); + return; + } + + mEncryptionScheme = aInputBuffer->encryption_scheme; + Assign(mKeyId, aInputBuffer->key_id, aInputBuffer->key_id_size); + Assign(mIV, aInputBuffer->iv, aInputBuffer->iv_size); + mCryptByteBlock = aInputBuffer->pattern.crypt_byte_block; + mSkipByteBlock = aInputBuffer->pattern.skip_byte_block; + + for (uint32_t i = 0; i < aInputBuffer->num_subsamples; ++i) { + const cdm::SubsampleEntry& subsample = aInputBuffer->subsamples[i]; + mClearBytes.push_back(subsample.clear_bytes); + mCipherBytes.push_back(subsample.cipher_bytes); + } + } + + bool IsValid() const { + return !mKeyId.empty() && !mIV.empty() && !mCipherBytes.empty() && + !mClearBytes.empty(); + } + + size_t NumSubsamples() const { + assert(mClearBytes.size() == mCipherBytes.size()); + return mClearBytes.size(); + } + + cdm::EncryptionScheme mEncryptionScheme; + std::vector<uint8_t> mKeyId; + std::vector<uint8_t> mIV; + uint32_t mCryptByteBlock; + uint32_t mSkipByteBlock; + std::vector<uint32_t> mClearBytes; + std::vector<uint32_t> mCipherBytes; +}; + +class ClearKeyDecryptionManager : public RefCounted { + private: + ClearKeyDecryptionManager(); + ~ClearKeyDecryptionManager(); + + static ClearKeyDecryptionManager* sInstance; + + public: + static ClearKeyDecryptionManager* Get(); + + bool HasSeenKeyId(const KeyId& aKeyId) const; + bool HasKeyForKeyId(const KeyId& aKeyId) const; + + const Key& GetDecryptionKey(const KeyId& aKeyId); + + // Create a decryptor for the given KeyId if one does not already exist. + void InitKey(KeyId aKeyId, Key aKey); + void ExpectKeyId(KeyId aKeyId); + void ReleaseKeyId(KeyId aKeyId); + + // Decrypts buffer *in place*. + cdm::Status Decrypt(uint8_t* aBuffer, uint32_t aBufferSize, + const CryptoMetaData& aMetadata); + cdm::Status Decrypt(std::vector<uint8_t>& aBuffer, + const CryptoMetaData& aMetadata); + + private: + bool IsExpectingKeyForKeyId(const KeyId& aKeyId) const; + + std::map<KeyId, ClearKeyDecryptor*> mDecryptors; +}; + +#endif // __ClearKeyDecryptionManager_h__ diff --git a/dom/media/eme/clearkey/ClearKeyPersistence.cpp b/dom/media/eme/clearkey/ClearKeyPersistence.cpp new file mode 100644 index 0000000000..0b81f81399 --- /dev/null +++ b/dom/media/eme/clearkey/ClearKeyPersistence.cpp @@ -0,0 +1,153 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ClearKeyPersistence.h" + +#include <assert.h> +#include <stdint.h> +#include <string.h> + +#include <sstream> + +#include "ClearKeySessionManager.h" +#include "ClearKeyStorage.h" +#include "ClearKeyUtils.h" +#include "RefCounted.h" + +using namespace cdm; + +using std::function; +using std::string; +using std::stringstream; +using std::vector; + +void ClearKeyPersistence::ReadAllRecordsFromIndex( + function<void()>&& aOnComplete) { + // Clear what we think the index file contains, we're about to read it again. + mPersistentSessionIds.clear(); + + // Hold a reference to the persistence manager, so it isn't released before + // we try and use it. + RefPtr<ClearKeyPersistence> self(this); + function<void(const uint8_t*, uint32_t)> onIndexSuccess = + [self, aOnComplete](const uint8_t* data, uint32_t size) { + CK_LOGD("ClearKeyPersistence: Loaded index file!"); + const char* charData = (const char*)data; + + stringstream ss(string(charData, charData + size)); + string name; + while (getline(ss, name)) { + if (ClearKeyUtils::IsValidSessionId(name.data(), name.size())) { + self->mPersistentSessionIds.insert(atoi(name.c_str())); + } + } + + self->mPersistentKeyState = PersistentKeyState::LOADED; + aOnComplete(); + }; + + function<void()> onIndexFailed = [self, aOnComplete]() { + CK_LOGD( + "ClearKeyPersistence: Failed to load index file (it might not exist"); + self->mPersistentKeyState = PersistentKeyState::LOADED; + aOnComplete(); + }; + + string filename = "index"; + ReadData(mHost, filename, std::move(onIndexSuccess), + std::move(onIndexFailed)); +} + +void ClearKeyPersistence::WriteIndex() { + function<void()> onIndexSuccess = []() { + CK_LOGD("ClearKeyPersistence: Wrote index file"); + }; + + function<void()> onIndexFail = []() { + CK_LOGD("ClearKeyPersistence: Failed to write index file (this is bad)"); + }; + + stringstream ss; + + for (const uint32_t& sessionId : mPersistentSessionIds) { + ss << sessionId; + ss << '\n'; + } + + string dataString = ss.str(); + uint8_t* dataArray = (uint8_t*)dataString.data(); + vector<uint8_t> data(dataArray, dataArray + dataString.size()); + + string filename = "index"; + WriteData(mHost, filename, data, std::move(onIndexSuccess), + std::move(onIndexFail)); +} + +ClearKeyPersistence::ClearKeyPersistence(Host_10* aHost) { + this->mHost = aHost; +} + +void ClearKeyPersistence::EnsureInitialized(bool aPersistentStateAllowed, + function<void()>&& aOnInitialized) { + if (aPersistentStateAllowed && + mPersistentKeyState == PersistentKeyState::UNINITIALIZED) { + mPersistentKeyState = LOADING; + ReadAllRecordsFromIndex(std::move(aOnInitialized)); + } else { + mPersistentKeyState = PersistentKeyState::LOADED; + aOnInitialized(); + } +} + +bool ClearKeyPersistence::IsLoaded() const { + return mPersistentKeyState == PersistentKeyState::LOADED; +} + +string ClearKeyPersistence::GetNewSessionId(SessionType aSessionType) { + static uint32_t sNextSessionId = 1; + + // Ensure we don't re-use a session id that was persisted. + while (Contains(mPersistentSessionIds, sNextSessionId)) { + sNextSessionId++; + } + + string sessionId; + stringstream ss; + ss << sNextSessionId; + ss >> sessionId; + + if (aSessionType == SessionType::kPersistentLicense) { + mPersistentSessionIds.insert(sNextSessionId); + + // Save the updated index file. + WriteIndex(); + } + + sNextSessionId++; + + return sessionId; +} + +bool ClearKeyPersistence::IsPersistentSessionId(const string& aSessionId) { + return Contains(mPersistentSessionIds, atoi(aSessionId.c_str())); +} + +void ClearKeyPersistence::PersistentSessionRemoved(string& aSessionId) { + mPersistentSessionIds.erase(atoi(aSessionId.c_str())); + + // Update the index file. + WriteIndex(); +} diff --git a/dom/media/eme/clearkey/ClearKeyPersistence.h b/dom/media/eme/clearkey/ClearKeyPersistence.h new file mode 100644 index 0000000000..b832db8c11 --- /dev/null +++ b/dom/media/eme/clearkey/ClearKeyPersistence.h @@ -0,0 +1,64 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __ClearKeyPersistence_h__ +#define __ClearKeyPersistence_h__ + +// This include is required in order for content_decryption_module to work +// on Unix systems. +#include <stddef.h> + +#include <functional> +#include <set> +#include <string> +#include <vector> + +#include "content_decryption_module.h" + +#include "RefCounted.h" + +class ClearKeySessionManager; + +// Whether we've loaded the persistent session ids yet. +enum PersistentKeyState { UNINITIALIZED, LOADING, LOADED }; + +class ClearKeyPersistence : public RefCounted { + public: + explicit ClearKeyPersistence(cdm::Host_10* aHost); + + void EnsureInitialized(bool aPersistentStateAllowed, + std::function<void()>&& aOnInitialized); + + bool IsLoaded() const; + + std::string GetNewSessionId(cdm::SessionType aSessionType); + + bool IsPersistentSessionId(const std::string& aSid); + + void PersistentSessionRemoved(std::string& aSid); + + private: + cdm::Host_10* mHost = nullptr; + + PersistentKeyState mPersistentKeyState = PersistentKeyState::UNINITIALIZED; + + std::set<uint32_t> mPersistentSessionIds; + + void ReadAllRecordsFromIndex(std::function<void()>&& aOnComplete); + void WriteIndex(); +}; + +#endif // __ClearKeyPersistence_h__ diff --git a/dom/media/eme/clearkey/ClearKeySession.cpp b/dom/media/eme/clearkey/ClearKeySession.cpp new file mode 100644 index 0000000000..59e104fc54 --- /dev/null +++ b/dom/media/eme/clearkey/ClearKeySession.cpp @@ -0,0 +1,72 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <assert.h> +#include <string.h> + +#include "BigEndian.h" +#include "ClearKeyDecryptionManager.h" +#include "ClearKeySession.h" +#include "ClearKeyStorage.h" +#include "ClearKeyUtils.h" +#include "psshparser/PsshParser.h" + +using namespace mozilla; +using namespace cdm; + +ClearKeySession::ClearKeySession(const std::string& aSessionId, + SessionType aSessionType) + : mSessionId(aSessionId), mSessionType(aSessionType) { + CK_LOGD("ClearKeySession ctor %p", this); +} + +ClearKeySession::~ClearKeySession() { + CK_LOGD("ClearKeySession dtor %p", this); +} + +bool ClearKeySession::Init(InitDataType aInitDataType, const uint8_t* aInitData, + uint32_t aInitDataSize) { + CK_LOGD("ClearKeySession::Init"); + + if (aInitDataType == InitDataType::kCenc) { + ParseCENCInitData(aInitData, aInitDataSize, mKeyIds); + } else if (aInitDataType == InitDataType::kKeyIds) { + ClearKeyUtils::ParseKeyIdsInitData(aInitData, aInitDataSize, mKeyIds); + } else if (aInitDataType == InitDataType::kWebM && + aInitDataSize <= kMaxWebmInitDataSize) { + // "webm" initData format is simply the raw bytes of the keyId. + std::vector<uint8_t> keyId; + keyId.assign(aInitData, aInitData + aInitDataSize); + mKeyIds.push_back(keyId); + } + + if (mKeyIds.empty()) { + CK_LOGD("ClearKeySession::Init, failed to get keyId"); + return false; + } +#ifdef WMF_CLEARKEY_DEBUG + for (const auto& keyId : mKeyIds) { + CK_LOGARRAY("ClearKeySession::Init, KeyId : ", keyId.data(), keyId.size()); + } +#endif + return true; +} + +SessionType ClearKeySession::Type() const { return mSessionType; } + +void ClearKeySession::AddKeyId(const KeyId& aKeyId) { + mKeyIds.push_back(aKeyId); +} diff --git a/dom/media/eme/clearkey/ClearKeySession.h b/dom/media/eme/clearkey/ClearKeySession.h new file mode 100644 index 0000000000..3a130aa45a --- /dev/null +++ b/dom/media/eme/clearkey/ClearKeySession.h @@ -0,0 +1,56 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __ClearKeySession_h__ +#define __ClearKeySession_h__ + +// This include is required in order for content_decryption_module to work +// on Unix systems. +#include <stddef.h> + +#include <string> +#include <vector> + +#include "content_decryption_module.h" + +#include "ClearKeyUtils.h" + +class ClearKeySession { + public: + explicit ClearKeySession(const std::string& aSessionId, + cdm::SessionType aSessionType); + + ~ClearKeySession(); + + const std::vector<KeyId>& GetKeyIds() const { return mKeyIds; } + + bool Init(cdm::InitDataType aInitDataType, const uint8_t* aInitData, + uint32_t aInitDataSize); + + cdm::SessionType Type() const; + + void AddKeyId(const KeyId& aKeyId); + + const std::string& Id() const { return mSessionId; } + + private: + const std::string mSessionId; + std::vector<KeyId> mKeyIds; + + const cdm::SessionType mSessionType; +}; + +#endif // __ClearKeySession_h__ diff --git a/dom/media/eme/clearkey/ClearKeySessionManager.cpp b/dom/media/eme/clearkey/ClearKeySessionManager.cpp new file mode 100644 index 0000000000..54a15f4fcf --- /dev/null +++ b/dom/media/eme/clearkey/ClearKeySessionManager.cpp @@ -0,0 +1,713 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ClearKeySessionManager.h" + +#include <assert.h> +#include <stdint.h> +#include <stdio.h> +#include <string.h> + +#include "content_decryption_module.h" + +#include "ClearKeyDecryptionManager.h" +#include "ClearKeyPersistence.h" +#include "ClearKeyStorage.h" +#include "ClearKeyUtils.h" +#include "psshparser/PsshParser.h" + +using namespace cdm; + +using std::function; +using std::string; +using std::vector; + +ClearKeySessionManager::ClearKeySessionManager(Host_10* aHost) + : mDecryptionManager(ClearKeyDecryptionManager::Get()) { + CK_LOGD("ClearKeySessionManager ctor %p", this); + AddRef(); + + mHost = aHost; + mPersistence = new ClearKeyPersistence(mHost); +} + +ClearKeySessionManager::~ClearKeySessionManager() { + CK_LOGD("ClearKeySessionManager dtor %p", this); +} + +void ClearKeySessionManager::Init(bool aDistinctiveIdentifierAllowed, + bool aPersistentStateAllowed) { + CK_LOGD("ClearKeySessionManager::Init"); + + RefPtr<ClearKeySessionManager> self(this); + function<void()> onPersistentStateLoaded = [self]() { + while (!self->mDeferredInitialize.empty()) { + function<void()> func = self->mDeferredInitialize.front(); + self->mDeferredInitialize.pop(); + + func(); + } + if (self->mHost) { + // The session manager should be the last thing the ClearKey CDM is + // waiting on to be initialized. + self->mHost->OnInitialized(true); + } + }; + + mPersistence->EnsureInitialized(aPersistentStateAllowed, + std::move(onPersistentStateLoaded)); +} + +void ClearKeySessionManager::CreateSession(uint32_t aPromiseId, + InitDataType aInitDataType, + const uint8_t* aInitData, + uint32_t aInitDataSize, + SessionType aSessionType) { + CK_LOGD("ClearKeySessionManager::CreateSession type:%u", aInitDataType); + + // Copy the init data so it is correctly captured by the lambda + vector<uint8_t> initData(aInitData, aInitData + aInitDataSize); + + RefPtr<ClearKeySessionManager> self(this); + function<void()> deferrer = [self, aPromiseId, aInitDataType, initData, + aSessionType]() { + self->CreateSession(aPromiseId, aInitDataType, initData.data(), + initData.size(), aSessionType); + }; + + // If we haven't loaded, don't do this yet + if (MaybeDeferTillInitialized(std::move(deferrer))) { + CK_LOGD("Deferring CreateSession"); + return; + } + + CK_LOGARRAY("ClearKeySessionManager::CreateSession initdata: ", aInitData, + aInitDataSize); + + // If 'DecryptingComplete' has been called mHost will be null so we can't + // won't be able to resolve our promise + if (!mHost) { + CK_LOGD("ClearKeySessionManager::CreateSession: mHost is nullptr"); + return; + } + + // initDataType must be "cenc", "keyids", or "webm". + if (aInitDataType != InitDataType::kCenc && + aInitDataType != InitDataType::kKeyIds && + aInitDataType != InitDataType::kWebM) { + string message = "initDataType is not supported by ClearKey"; + mHost->OnRejectPromise(aPromiseId, Exception::kExceptionNotSupportedError, + 0, message.c_str(), message.size()); + + return; + } + + string sessionId = mPersistence->GetNewSessionId(aSessionType); + assert(mSessions.find(sessionId) == mSessions.end()); + + ClearKeySession* session = new ClearKeySession(sessionId, aSessionType); + + if (!session->Init(aInitDataType, aInitData, aInitDataSize)) { + CK_LOGD("Failed to initialize session: %s", sessionId.c_str()); + + const static char* message = "Failed to initialize session"; + mHost->OnRejectPromise(aPromiseId, Exception::kExceptionInvalidStateError, + 0, message, strlen(message)); + delete session; + + return; + } + + mSessions[sessionId] = session; + mLastSessionId = sessionId; + + const vector<KeyId>& sessionKeys = session->GetKeyIds(); + vector<KeyId> neededKeys; + + for (auto it = sessionKeys.begin(); it != sessionKeys.end(); it++) { + // Need to request this key ID from the client. We always send a key + // request, whether or not another session has sent a request with the same + // key ID. Otherwise a script can end up waiting for another script to + // respond to the request (which may not necessarily happen). + neededKeys.push_back(*it); + mDecryptionManager->ExpectKeyId(*it); + } + + if (neededKeys.empty()) { + CK_LOGD("No keys needed from client."); + return; + } + + // Send a request for needed key data. + string request; + ClearKeyUtils::MakeKeyRequest(neededKeys, request, aSessionType); + + // Resolve the promise with the new session information. + mHost->OnResolveNewSessionPromise(aPromiseId, sessionId.c_str(), + sessionId.size()); + + mHost->OnSessionMessage(sessionId.c_str(), sessionId.size(), + MessageType::kLicenseRequest, request.c_str(), + request.size()); +} + +void ClearKeySessionManager::LoadSession(uint32_t aPromiseId, + const char* aSessionId, + uint32_t aSessionIdLength) { + CK_LOGD("ClearKeySessionManager::LoadSession"); + + // Copy the sessionId into a string so the lambda captures it properly. + string sessionId(aSessionId, aSessionId + aSessionIdLength); + + // Hold a reference to the SessionManager so that it isn't released before + // we try to use it. + RefPtr<ClearKeySessionManager> self(this); + function<void()> deferrer = [self, aPromiseId, sessionId]() { + self->LoadSession(aPromiseId, sessionId.data(), sessionId.size()); + }; + + if (MaybeDeferTillInitialized(std::move(deferrer))) { + CK_LOGD("Deferring LoadSession"); + return; + } + + // If the SessionManager has been shutdown mHost will be null and we won't + // be able to resolve the promise. + if (!mHost) { + return; + } + + if (!ClearKeyUtils::IsValidSessionId(aSessionId, aSessionIdLength)) { + mHost->OnResolveNewSessionPromise(aPromiseId, nullptr, 0); + return; + } + + if (!mPersistence->IsPersistentSessionId(sessionId)) { + mHost->OnResolveNewSessionPromise(aPromiseId, nullptr, 0); + return; + } + + function<void(const uint8_t*, uint32_t)> success = + [self, sessionId, aPromiseId](const uint8_t* data, uint32_t size) { + self->PersistentSessionDataLoaded(aPromiseId, sessionId, data, size); + }; + + function<void()> failure = [self, aPromiseId] { + if (!self->mHost) { + return; + } + // As per the API described in ContentDecryptionModule_8 + self->mHost->OnResolveNewSessionPromise(aPromiseId, nullptr, 0); + }; + + ReadData(mHost, sessionId, std::move(success), std::move(failure)); +} + +void ClearKeySessionManager::PersistentSessionDataLoaded( + uint32_t aPromiseId, const string& aSessionId, const uint8_t* aKeyData, + uint32_t aKeyDataSize) { + CK_LOGD("ClearKeySessionManager::PersistentSessionDataLoaded"); + + // Check that the SessionManager has not been shut down before we try and + // resolve any promises. + if (!mHost) { + return; + } + + if (Contains(mSessions, aSessionId) || + (aKeyDataSize % (2 * CENC_KEY_LEN)) != 0) { + // As per the instructions in ContentDecryptionModule_8 + mHost->OnResolveNewSessionPromise(aPromiseId, nullptr, 0); + return; + } + + ClearKeySession* session = + new ClearKeySession(aSessionId, SessionType::kPersistentLicense); + + mSessions[aSessionId] = session; + mLastSessionId = aSessionId; + + uint32_t numKeys = aKeyDataSize / (2 * CENC_KEY_LEN); + + vector<KeyInformation> keyInfos; + vector<KeyIdPair> keyPairs; + for (uint32_t i = 0; i < numKeys; i++) { + const uint8_t* base = aKeyData + 2 * CENC_KEY_LEN * i; + + KeyIdPair keyPair; + + keyPair.mKeyId = KeyId(base, base + CENC_KEY_LEN); + assert(keyPair.mKeyId.size() == CENC_KEY_LEN); + + keyPair.mKey = Key(base + CENC_KEY_LEN, base + 2 * CENC_KEY_LEN); + assert(keyPair.mKey.size() == CENC_KEY_LEN); + + session->AddKeyId(keyPair.mKeyId); + + mDecryptionManager->ExpectKeyId(keyPair.mKeyId); + mDecryptionManager->InitKey(keyPair.mKeyId, keyPair.mKey); + mKeyIds.insert(keyPair.mKey); + keyPairs.push_back(keyPair); + + KeyInformation keyInfo = {}; + keyInfo.key_id = &keyPairs.back().mKeyId[0]; + keyInfo.key_id_size = keyPair.mKeyId.size(); + keyInfo.status = KeyStatus::kUsable; + + keyInfos.push_back(keyInfo); + } + + mHost->OnSessionKeysChange(&aSessionId[0], aSessionId.size(), true, + keyInfos.data(), keyInfos.size()); + + mHost->OnResolveNewSessionPromise(aPromiseId, aSessionId.c_str(), + aSessionId.size()); +} + +void ClearKeySessionManager::UpdateSession(uint32_t aPromiseId, + const char* aSessionId, + uint32_t aSessionIdLength, + const uint8_t* aResponse, + uint32_t aResponseSize) { + CK_LOGD("ClearKeySessionManager::UpdateSession"); + + // Copy the method arguments so we can capture them in the lambda + string sessionId(aSessionId, aSessionId + aSessionIdLength); + vector<uint8_t> response(aResponse, aResponse + aResponseSize); + + // Hold a reference to the SessionManager so it isn't released before we + // callback. + RefPtr<ClearKeySessionManager> self(this); + function<void()> deferrer = [self, aPromiseId, sessionId, response]() { + self->UpdateSession(aPromiseId, sessionId.data(), sessionId.size(), + response.data(), response.size()); + }; + + // If we haven't fully loaded, defer calling this method + if (MaybeDeferTillInitialized(std::move(deferrer))) { + CK_LOGD("Deferring LoadSession"); + return; + } + + // Make sure the SessionManager has not been shutdown before we try and + // resolve any promises. + if (!mHost) { + return; + } + + CK_LOGD("Updating session: %s", sessionId.c_str()); + + auto itr = mSessions.find(sessionId); + if (itr == mSessions.end() || !(itr->second)) { + CK_LOGW("ClearKey CDM couldn't resolve session ID in UpdateSession."); + CK_LOGD("Unable to find session: %s", sessionId.c_str()); + mHost->OnRejectPromise(aPromiseId, Exception::kExceptionTypeError, 0, + nullptr, 0); + + return; + } + ClearKeySession* session = itr->second; + + // Verify the size of session response. + if (aResponseSize >= kMaxSessionResponseLength) { + CK_LOGW("Session response size is not within a reasonable size."); + CK_LOGD("Failed to parse response for session %s", sessionId.c_str()); + + mHost->OnRejectPromise(aPromiseId, Exception::kExceptionTypeError, 0, + nullptr, 0); + + return; + } + + // Parse the response for any (key ID, key) pairs. + vector<KeyIdPair> keyPairs; + if (!ClearKeyUtils::ParseJWK(aResponse, aResponseSize, keyPairs, + session->Type())) { + CK_LOGW("ClearKey CDM failed to parse JSON Web Key."); + + mHost->OnRejectPromise(aPromiseId, Exception::kExceptionTypeError, 0, + nullptr, 0); + + return; + } + + vector<KeyInformation> keyInfos; + for (size_t i = 0; i < keyPairs.size(); i++) { + KeyIdPair& keyPair = keyPairs[i]; + mDecryptionManager->InitKey(keyPair.mKeyId, keyPair.mKey); + mKeyIds.insert(keyPair.mKeyId); + + KeyInformation keyInfo = {}; + keyInfo.key_id = &keyPair.mKeyId[0]; + keyInfo.key_id_size = keyPair.mKeyId.size(); + keyInfo.status = KeyStatus::kUsable; + + keyInfos.push_back(keyInfo); + } + + mHost->OnSessionKeysChange(aSessionId, aSessionIdLength, true, + keyInfos.data(), keyInfos.size()); + + if (session->Type() != SessionType::kPersistentLicense) { + mHost->OnResolvePromise(aPromiseId); + return; + } + + // Store the keys on disk. We store a record whose name is the sessionId, + // and simply append each keyId followed by its key. + vector<uint8_t> keydata; + Serialize(session, keydata); + + function<void()> resolve = [self, aPromiseId]() { + if (!self->mHost) { + return; + } + self->mHost->OnResolvePromise(aPromiseId); + }; + + function<void()> reject = [self, aPromiseId]() { + if (!self->mHost) { + return; + } + + static const char* message = "Couldn't store cenc key init data"; + self->mHost->OnRejectPromise(aPromiseId, + Exception::kExceptionInvalidStateError, 0, + message, strlen(message)); + }; + + WriteData(mHost, sessionId, keydata, std::move(resolve), std::move(reject)); +} + +void ClearKeySessionManager::Serialize(const ClearKeySession* aSession, + std::vector<uint8_t>& aOutKeyData) { + const std::vector<KeyId>& keyIds = aSession->GetKeyIds(); + for (size_t i = 0; i < keyIds.size(); i++) { + const KeyId& keyId = keyIds[i]; + if (!mDecryptionManager->HasKeyForKeyId(keyId)) { + continue; + } + assert(keyId.size() == CENC_KEY_LEN); + aOutKeyData.insert(aOutKeyData.end(), keyId.begin(), keyId.end()); + const Key& key = mDecryptionManager->GetDecryptionKey(keyId); + assert(key.size() == CENC_KEY_LEN); + aOutKeyData.insert(aOutKeyData.end(), key.begin(), key.end()); + } +} + +void ClearKeySessionManager::CloseSession(uint32_t aPromiseId, + const char* aSessionId, + uint32_t aSessionIdLength) { + CK_LOGD("ClearKeySessionManager::CloseSession"); + + // Copy the sessionId into a string so we capture it properly. + string sessionId(aSessionId, aSessionId + aSessionIdLength); + // Hold a reference to the session manager, so it doesn't get deleted + // before we need to use it. + RefPtr<ClearKeySessionManager> self(this); + function<void()> deferrer = [self, aPromiseId, sessionId]() { + self->CloseSession(aPromiseId, sessionId.data(), sessionId.size()); + }; + + // If we haven't loaded, call this method later. + if (MaybeDeferTillInitialized(std::move(deferrer))) { + CK_LOGD("Deferring CloseSession"); + return; + } + + // If DecryptingComplete has been called mHost will be null and we won't + // be able to resolve our promise. + if (!mHost) { + return; + } + + auto itr = mSessions.find(sessionId); + if (itr == mSessions.end()) { + CK_LOGW("ClearKey CDM couldn't close non-existent session."); + mHost->OnRejectPromise(aPromiseId, Exception::kExceptionTypeError, 0, + nullptr, 0); + + return; + } + + ClearKeySession* session = itr->second; + assert(session); + + ClearInMemorySessionData(session); + + mHost->OnSessionClosed(aSessionId, aSessionIdLength); + mHost->OnResolvePromise(aPromiseId); +} + +void ClearKeySessionManager::ClearInMemorySessionData( + ClearKeySession* aSession) { + mSessions.erase(aSession->Id()); + delete aSession; +} + +void ClearKeySessionManager::RemoveSession(uint32_t aPromiseId, + const char* aSessionId, + uint32_t aSessionIdLength) { + CK_LOGD("ClearKeySessionManager::RemoveSession"); + + // Copy the sessionId into a string so it can be captured for the lambda. + string sessionId(aSessionId, aSessionId + aSessionIdLength); + + // Hold a reference to the SessionManager, so it isn't released before we + // try and use it. + RefPtr<ClearKeySessionManager> self(this); + function<void()> deferrer = [self, aPromiseId, sessionId]() { + self->RemoveSession(aPromiseId, sessionId.data(), sessionId.size()); + }; + + // If we haven't fully loaded, defer calling this method. + if (MaybeDeferTillInitialized(std::move(deferrer))) { + CK_LOGD("Deferring RemoveSession"); + return; + } + + // Check that the SessionManager has not been shutdown before we try and + // resolve any promises. + if (!mHost) { + return; + } + + auto itr = mSessions.find(sessionId); + if (itr == mSessions.end()) { + CK_LOGW("ClearKey CDM couldn't remove non-existent session."); + + mHost->OnRejectPromise(aPromiseId, Exception::kExceptionTypeError, 0, + nullptr, 0); + + return; + } + + ClearKeySession* session = itr->second; + assert(session); + string sid = session->Id(); + bool isPersistent = session->Type() == SessionType::kPersistentLicense; + ClearInMemorySessionData(session); + + if (!isPersistent) { + mHost->OnResolvePromise(aPromiseId); + return; + } + + mPersistence->PersistentSessionRemoved(sid); + + vector<uint8_t> emptyKeydata; + + function<void()> resolve = [self, aPromiseId]() { + if (!self->mHost) { + return; + } + self->mHost->OnResolvePromise(aPromiseId); + }; + + function<void()> reject = [self, aPromiseId]() { + if (!self->mHost) { + return; + } + static const char* message = "Could not remove session"; + self->mHost->OnRejectPromise(aPromiseId, Exception::kExceptionTypeError, 0, + message, strlen(message)); + }; + + WriteData(mHost, sessionId, emptyKeydata, std::move(resolve), + std::move(reject)); +} + +void ClearKeySessionManager::SetServerCertificate(uint32_t aPromiseId, + const uint8_t* aServerCert, + uint32_t aServerCertSize) { + // ClearKey CDM doesn't support this method by spec. + CK_LOGD("ClearKeySessionManager::SetServerCertificate"); + mHost->OnRejectPromise(aPromiseId, Exception::kExceptionNotSupportedError, 0, + nullptr /* message */, 0 /* messageLen */); +} + +Status ClearKeySessionManager::Decrypt(const InputBuffer_2& aBuffer, + DecryptedBlock* aDecryptedBlock) { + CK_LOGD("ClearKeySessionManager::Decrypt"); + + CK_LOGARRAY("Key: ", aBuffer.key_id, aBuffer.key_id_size); + + Buffer* buffer = mHost->Allocate(aBuffer.data_size); + assert(buffer != nullptr); + assert(buffer->Data() != nullptr); + assert(buffer->Capacity() >= aBuffer.data_size); + + memcpy(buffer->Data(), aBuffer.data, aBuffer.data_size); + + Status status = Status::kSuccess; + // According to the comment `If |iv_size| = 0, the data is unencrypted.` + // Use iv_size to determine if the sample is encrypted. + if (aBuffer.iv_size != 0) { + status = mDecryptionManager->Decrypt(buffer->Data(), buffer->Size(), + CryptoMetaData(&aBuffer)); + } + + aDecryptedBlock->SetDecryptedBuffer(buffer); + aDecryptedBlock->SetTimestamp(aBuffer.timestamp); + + return status; +} + +void ClearKeySessionManager::DecryptingComplete() { + CK_LOGD("ClearKeySessionManager::DecryptingComplete %p", this); + + for (auto it = mSessions.begin(); it != mSessions.end(); it++) { + delete it->second; + } + mSessions.clear(); + mLastSessionId = std::nullopt; + + mDecryptionManager = nullptr; + mHost = nullptr; + + Release(); +} + +bool ClearKeySessionManager::MaybeDeferTillInitialized( + function<void()>&& aMaybeDefer) { + if (mPersistence->IsLoaded()) { + return false; + } + + mDeferredInitialize.emplace(std::move(aMaybeDefer)); + return true; +} + +void ClearKeySessionManager::OnQueryOutputProtectionStatus( + QueryResult aResult, uint32_t aLinkMask, uint32_t aOutputProtectionMask) { + MOZ_ASSERT(mHasOutstandingOutputProtectionQuery, + "Should only be called if a query is outstanding"); + CK_LOGD("ClearKeySessionManager::OnQueryOutputProtectionStatus"); + mHasOutstandingOutputProtectionQuery = false; + + if (aResult == QueryResult::kQueryFailed) { + // Indicate the query failed. This can happen if we're in shutdown. + NotifyOutputProtectionStatus(KeyStatus::kInternalError); + return; + } + + if (aLinkMask & OutputLinkTypes::kLinkTypeNetwork) { + NotifyOutputProtectionStatus(KeyStatus::kOutputRestricted); + return; + } + + NotifyOutputProtectionStatus(KeyStatus::kUsable); +} + +void ClearKeySessionManager::QueryOutputProtectionStatusIfNeeded() { + MOZ_ASSERT( + mHost, + "Should not query protection status if we're shutdown (mHost == null)!"); + CK_LOGD( + "ClearKeySessionManager::UpdateOutputProtectionStatusAndQueryIfNeeded"); + if (mLastOutputProtectionQueryTime.IsNull()) { + // We haven't perfomed a check yet, get a query going. + MOZ_ASSERT( + !mHasOutstandingOutputProtectionQuery, + "Shouldn't have an outstanding query if we haven't recorded a time"); + QueryOutputProtectionStatusFromHost(); + return; + } + + MOZ_ASSERT(!mLastOutputProtectionQueryTime.IsNull(), + "Should have already handled the case where we don't yet have a " + "previous check time"); + const mozilla::TimeStamp now = mozilla::TimeStamp::NowLoRes(); + const mozilla::TimeDuration timeSinceQuery = + now - mLastOutputProtectionQueryTime; + + // The time between output protection checks to the host. I.e. if this amount + // of time has passed since the last check with the host, another should be + // performed (provided the first check has been handled). + static const mozilla::TimeDuration kOutputProtectionQueryInterval = + mozilla::TimeDuration::FromSeconds(0.2); + // The number of kOutputProtectionQueryInterval intervals we can miss before + // we decide a check has failed. I.e. if this value is 2, if we have not + // received a reply to a check after kOutputProtectionQueryInterval * 2 + // time, we consider the check failed. + constexpr uint32_t kMissedIntervalsBeforeFailure = 2; + // The length of time after which we will restrict output until we get a + // query response. + static const mozilla::TimeDuration kTimeToWaitBeforeFailure = + kOutputProtectionQueryInterval * kMissedIntervalsBeforeFailure; + + if ((timeSinceQuery > kOutputProtectionQueryInterval) && + !mHasOutstandingOutputProtectionQuery) { + // We don't have an outstanding query and enough time has passed we should + // query again. + QueryOutputProtectionStatusFromHost(); + return; + } + + if ((timeSinceQuery > kTimeToWaitBeforeFailure) && + mHasOutstandingOutputProtectionQuery) { + // A reponse was not received fast enough, notify. + NotifyOutputProtectionStatus(KeyStatus::kInternalError); + } +} + +void ClearKeySessionManager::QueryOutputProtectionStatusFromHost() { + MOZ_ASSERT( + mHost, + "Should not query protection status if we're shutdown (mHost == null)!"); + CK_LOGD("ClearKeySessionManager::QueryOutputProtectionStatusFromHost"); + if (mHost) { + mLastOutputProtectionQueryTime = mozilla::TimeStamp::NowLoRes(); + mHost->QueryOutputProtectionStatus(); + mHasOutstandingOutputProtectionQuery = true; + } +} + +void ClearKeySessionManager::NotifyOutputProtectionStatus(KeyStatus aStatus) { + MOZ_ASSERT(aStatus == KeyStatus::kUsable || + aStatus == KeyStatus::kOutputRestricted || + aStatus == KeyStatus::kInternalError, + "aStatus should have an expected value"); + CK_LOGD("ClearKeySessionManager::NotifyOutputProtectionStatus"); + if (!mLastSessionId.has_value()) { + // If we don't have a session id, either because we're too early, or are + // shutting down, don't notify. + return; + } + + string& lastSessionId = mLastSessionId.value(); + + // Use 'output-protection' as the key ID. This helps tests disambiguate key + // status updates related to this. + const uint8_t kKeyId[] = {'o', 'u', 't', 'p', 'u', 't', '-', 'p', 'r', + 'o', 't', 'e', 'c', 't', 'i', 'o', 'n'}; + KeyInformation keyInfo = {}; + keyInfo.key_id = kKeyId; + keyInfo.key_id_size = std::size(kKeyId); + keyInfo.status = aStatus; + + vector<KeyInformation> keyInfos; + keyInfos.push_back(keyInfo); + + // At time of writing, Gecko's higher level handling doesn't use this arg. + // However, we set it to false to mimic Chromium's similar case. Since + // Clearkey is used to test the Chromium CDM path, it doesn't hurt to try + // and mimic their behaviour. + bool hasAdditionalUseableKey = false; + mHost->OnSessionKeysChange(lastSessionId.c_str(), lastSessionId.size(), + hasAdditionalUseableKey, keyInfos.data(), + keyInfos.size()); +} diff --git a/dom/media/eme/clearkey/ClearKeySessionManager.h b/dom/media/eme/clearkey/ClearKeySessionManager.h new file mode 100644 index 0000000000..3a930f666d --- /dev/null +++ b/dom/media/eme/clearkey/ClearKeySessionManager.h @@ -0,0 +1,138 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __ClearKeyDecryptor_h__ +#define __ClearKeyDecryptor_h__ + +// This include is required in order for content_decryption_module to work +// on Unix systems. +#include <stddef.h> + +#include <functional> +#include <map> +#include <optional> +#include <queue> +#include <set> +#include <string> + +#include "content_decryption_module.h" + +#include "ClearKeyDecryptionManager.h" +#include "ClearKeyPersistence.h" +#include "ClearKeySession.h" +#include "ClearKeyUtils.h" +#include "RefCounted.h" +#include "mozilla/TimeStamp.h" + +class ClearKeySessionManager final : public RefCounted { + public: + explicit ClearKeySessionManager(cdm::Host_10* aHost); + + void Init(bool aDistinctiveIdentifierAllowed, bool aPersistentStateAllowed); + + void CreateSession(uint32_t aPromiseId, cdm::InitDataType aInitDataType, + const uint8_t* aInitData, uint32_t aInitDataSize, + cdm::SessionType aSessionType); + + void LoadSession(uint32_t aPromiseId, const char* aSessionId, + uint32_t aSessionIdLength); + + void UpdateSession(uint32_t aPromiseId, const char* aSessionId, + uint32_t aSessionIdLength, const uint8_t* aResponse, + uint32_t aResponseSize); + + void CloseSession(uint32_t aPromiseId, const char* aSessionId, + uint32_t aSessionIdLength); + + void RemoveSession(uint32_t aPromiseId, const char* aSessionId, + uint32_t aSessionIdLength); + + void SetServerCertificate(uint32_t aPromiseId, const uint8_t* aServerCert, + uint32_t aServerCertSize); + + cdm::Status Decrypt(const cdm::InputBuffer_2& aBuffer, + cdm::DecryptedBlock* aDecryptedBlock); + + void DecryptingComplete(); + + void PersistentSessionDataLoaded(uint32_t aPromiseId, + const std::string& aSessionId, + const uint8_t* aKeyData, + uint32_t aKeyDataSize); + + // Receives the result of an output protection query from the user agent. + // This may trigger a key status change. + // @param aResult indicates if the query succeeded or not. If a query did + // not succeed then that other arguments are ignored. + // @param aLinkMask is used to indicate if output could be captured by the + // user agent. It should be set to `kLinkTypeNetwork` if capture is possible, + // otherwise it should be zero. + // @param aOutputProtectionMask this argument is unused. + void OnQueryOutputProtectionStatus(cdm::QueryResult aResult, + uint32_t aLinkMask, + uint32_t aOutputProtectionMask); + + // Prompts the session manager to query the output protection status if we + // haven't yet, or if enough time has passed since the last check. Will also + // notify if a check has not been responded to on time. + void QueryOutputProtectionStatusIfNeeded(); + + private: + ~ClearKeySessionManager(); + + void ClearInMemorySessionData(ClearKeySession* aSession); + bool MaybeDeferTillInitialized(std::function<void()>&& aMaybeDefer); + void Serialize(const ClearKeySession* aSession, + std::vector<uint8_t>& aOutKeyData); + + // Signals the host to perform an output protection check. + void QueryOutputProtectionStatusFromHost(); + + // Called to notify the result of an output protection status call. The + // following arguments are expected, along with their intended use: + // - KeyStatus::kUsable indicates that the query was responded to and the + // response showed output is protected. + // - KeyStatus::kOutputRestricted indicates that the query was responded to + // and the response showed output is not protected. + // - KeyStatus::kInternalError indicates a query was not repsonded to on + // time, or that a query was responded to with a failed cdm::QueryResult. + // The status passed to this function will be used to update the status of + // the keyId "output-protection", which tests an observe. + void NotifyOutputProtectionStatus(cdm::KeyStatus aStatus); + + RefPtr<ClearKeyDecryptionManager> mDecryptionManager; + RefPtr<ClearKeyPersistence> mPersistence; + + cdm::Host_10* mHost = nullptr; + + std::set<KeyId> mKeyIds; + std::map<std::string, ClearKeySession*> mSessions; + + // The session id of the last session created or loaded from persistent + // storage. Used to fire test messages at that session. + std::optional<std::string> mLastSessionId; + + std::queue<std::function<void()>> mDeferredInitialize; + + // If there is an inflight query to the host to check the output protection + // status. Multiple in flight queries should not be allowed, avoid firing + // more if this is true. + bool mHasOutstandingOutputProtectionQuery = false; + // The last time the manager called QueryOutputProtectionStatus on the host. + mozilla::TimeStamp mLastOutputProtectionQueryTime; +}; + +#endif // __ClearKeyDecryptor_h__ diff --git a/dom/media/eme/clearkey/ClearKeyStorage.cpp b/dom/media/eme/clearkey/ClearKeyStorage.cpp new file mode 100644 index 0000000000..1375d33c57 --- /dev/null +++ b/dom/media/eme/clearkey/ClearKeyStorage.cpp @@ -0,0 +1,194 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ClearKeyStorage.h" + +#include <assert.h> +// This include is required in order for content_decryption_module to work +// on Unix systems. +#include <stddef.h> + +#include <vector> + +#include "content_decryption_module.h" + +#include "ArrayUtils.h" +#include "ClearKeyUtils.h" + +using namespace cdm; + +using std::function; +using std::string; +using std::vector; + +class WriteRecordClient : public FileIOClient { + public: + /* + * This function will take the memory ownership of the parameters and + * delete them when done. + */ + static void Write(Host_10* aHost, string& aRecordName, + const vector<uint8_t>& aData, function<void()>&& aOnSuccess, + function<void()>&& aOnFailure) { + WriteRecordClient* client = new WriteRecordClient( + aData, std::move(aOnSuccess), std::move(aOnFailure)); + client->Do(aRecordName, aHost); + } + + void OnOpenComplete(Status aStatus) override { + // If we hit an error, fail. + if (aStatus != Status::kSuccess) { + Done(aStatus); + } else if (mFileIO) { // Otherwise, write our data to the file. + mFileIO->Write(&mData[0], mData.size()); + } + } + + void OnReadComplete(Status aStatus, const uint8_t* aData, + uint32_t aDataSize) override { + // This function should never be called, we only ever write data with this + // client. + assert(false); + } + + void OnWriteComplete(Status aStatus) override { Done(aStatus); } + + private: + explicit WriteRecordClient(const vector<uint8_t>& aData, + function<void()>&& aOnSuccess, + function<void()>&& aOnFailure) + : mFileIO(nullptr), + mOnSuccess(std::move(aOnSuccess)), + mOnFailure(std::move(aOnFailure)), + mData(aData) {} + + void Do(const string& aName, Host_10* aHost) { + // Initialize the FileIO. + mFileIO = aHost->CreateFileIO(this); + mFileIO->Open(aName.c_str(), aName.size()); + } + + void Done(cdm::FileIOClient::Status aStatus) { + // Note: Call Close() before running continuation, in case the + // continuation tries to open the same record; if we call Close() + // after running the continuation, the Close() call will arrive + // just after the Open() call succeeds, immediately closing the + // record we just opened. + if (mFileIO) { + mFileIO->Close(); + } + + if (IO_SUCCEEDED(aStatus)) { + mOnSuccess(); + } else { + mOnFailure(); + } + + delete this; + } + + FileIO* mFileIO = nullptr; + + function<void()> mOnSuccess; + function<void()> mOnFailure; + + const vector<uint8_t> mData; +}; + +void WriteData(Host_10* aHost, string& aRecordName, + const vector<uint8_t>& aData, function<void()>&& aOnSuccess, + function<void()>&& aOnFailure) { + WriteRecordClient::Write(aHost, aRecordName, aData, std::move(aOnSuccess), + std::move(aOnFailure)); +} + +class ReadRecordClient : public FileIOClient { + public: + /* + * This function will take the memory ownership of the parameters and + * delete them when done. + */ + static void Read(Host_10* aHost, string& aRecordName, + function<void(const uint8_t*, uint32_t)>&& aOnSuccess, + function<void()>&& aOnFailure) { + (new ReadRecordClient(std::move(aOnSuccess), std::move(aOnFailure))) + ->Do(aRecordName, aHost); + } + + void OnOpenComplete(Status aStatus) override { + auto err = aStatus; + if (aStatus != Status::kSuccess) { + Done(err, nullptr, 0); + } else { + mFileIO->Read(); + } + } + + void OnReadComplete(Status aStatus, const uint8_t* aData, + uint32_t aDataSize) override { + Done(aStatus, aData, aDataSize); + } + + void OnWriteComplete(Status aStatus) override { + // We should never reach here, this client only ever reads data. + assert(false); + } + + private: + explicit ReadRecordClient( + function<void(const uint8_t*, uint32_t)>&& aOnSuccess, + function<void()>&& aOnFailure) + : mFileIO(nullptr), + mOnSuccess(std::move(aOnSuccess)), + mOnFailure(std::move(aOnFailure)) {} + + void Do(const string& aName, Host_10* aHost) { + mFileIO = aHost->CreateFileIO(this); + mFileIO->Open(aName.c_str(), aName.size()); + } + + void Done(cdm::FileIOClient::Status aStatus, const uint8_t* aData, + uint32_t aDataSize) { + // Note: Call Close() before running continuation, in case the + // continuation tries to open the same record; if we call Close() + // after running the continuation, the Close() call will arrive + // just after the Open() call succeeds, immediately closing the + // record we just opened. + if (mFileIO) { + mFileIO->Close(); + } + + if (IO_SUCCEEDED(aStatus)) { + mOnSuccess(aData, aDataSize); + } else { + mOnFailure(); + } + + delete this; + } + + FileIO* mFileIO = nullptr; + + function<void(const uint8_t*, uint32_t)> mOnSuccess; + function<void()> mOnFailure; +}; + +void ReadData(Host_10* aHost, string& aRecordName, + function<void(const uint8_t*, uint32_t)>&& aOnSuccess, + function<void()>&& aOnFailure) { + ReadRecordClient::Read(aHost, aRecordName, std::move(aOnSuccess), + std::move(aOnFailure)); +} diff --git a/dom/media/eme/clearkey/ClearKeyStorage.h b/dom/media/eme/clearkey/ClearKeyStorage.h new file mode 100644 index 0000000000..bd8e99b901 --- /dev/null +++ b/dom/media/eme/clearkey/ClearKeyStorage.h @@ -0,0 +1,42 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __ClearKeyStorage_h__ +#define __ClearKeyStorage_h__ + +#include <stdint.h> + +#include <functional> +#include <string> +#include <vector> + +#include "ClearKeySessionManager.h" + +#define IO_SUCCEEDED(x) ((x) == cdm::FileIOClient::Status::kSuccess) +#define IO_FAILED(x) ((x) != cdm::FileIOClient::Status::kSuccess) + +// Writes data to a file and fires the appropriate callback when complete. +void WriteData(cdm::Host_10* aHost, std::string& aRecordName, + const std::vector<uint8_t>& aData, + std::function<void()>&& aOnSuccess, + std::function<void()>&& aOnFailure); + +// Reads data from a file and fires the appropriate callback when complete. +void ReadData(cdm::Host_10* aHost, std::string& aRecordName, + std::function<void(const uint8_t*, uint32_t)>&& aOnSuccess, + std::function<void()>&& aOnFailure); + +#endif // __ClearKeyStorage_h__ diff --git a/dom/media/eme/clearkey/ClearKeyUtils.cpp b/dom/media/eme/clearkey/ClearKeyUtils.cpp new file mode 100644 index 0000000000..3f9588bdf9 --- /dev/null +++ b/dom/media/eme/clearkey/ClearKeyUtils.cpp @@ -0,0 +1,661 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ClearKeyUtils.h" + +#include <assert.h> +#include <ctype.h> +#include <memory.h> +#include <stdarg.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> + +#include <algorithm> +#include <cctype> +#include <memory> +#include <sstream> +#include <vector> + +#include "pk11pub.h" +#include "prerror.h" +#include "secmodt.h" + +#include "ArrayUtils.h" +#include "BigEndian.h" +#include "ClearKeyBase64.h" +#include "mozilla/Sprintf.h" +#include "psshparser/PsshParser.h" + +using namespace cdm; +using std::string; +using std::stringstream; +using std::vector; + +struct DeleteHelper { + void operator()(PK11Context* value) { PK11_DestroyContext(value, true); } + void operator()(PK11SlotInfo* value) { PK11_FreeSlot(value); } + void operator()(PK11SymKey* value) { PK11_FreeSymKey(value); } +}; + +template <class T> +struct MaybeDeleteHelper { + void operator()(T* ptr) { + if (ptr) { + DeleteHelper del; + del(ptr); + } + } +}; + +void CK_Log(const char* aFmt, ...) { + FILE* out = stdout; + + if (getenv("CLEARKEY_LOG_FILE")) { + out = fopen(getenv("CLEARKEY_LOG_FILE"), "a"); + } + + va_list ap; + + va_start(ap, aFmt); + const size_t len = 1024; + char buf[len]; + VsprintfLiteral(buf, aFmt, ap); + va_end(ap); + + fprintf(out, "%s\n", buf); + fflush(out); + + if (out != stdout) { + fclose(out); + } +} + +static bool PrintableAsString(const uint8_t* aBytes, uint32_t aLength) { + return std::all_of(aBytes, aBytes + aLength, + [](uint8_t c) { return isprint(c) == 1; }); +} + +void CK_LogArray(const char* prepend, const uint8_t* aData, + const uint32_t aDataSize) { + // If the data is valid ascii, use that. Otherwise print the hex + string data = PrintableAsString(aData, aDataSize) + ? string(aData, aData + aDataSize) + : ClearKeyUtils::ToHexString(aData, aDataSize); + + CK_LOGD("%s%s", prepend, data.c_str()); +} + +/* static */ +bool ClearKeyUtils::DecryptCbcs(const vector<uint8_t>& aKey, + const vector<uint8_t>& aIV, + mozilla::Span<uint8_t> aSubsample, + uint32_t aCryptByteBlock, + uint32_t aSkipByteBlock) { + if (aKey.size() != CENC_KEY_LEN || aIV.size() != CENC_KEY_LEN) { + CK_LOGE("Key and IV size should be 16!"); + return false; + } + + std::unique_ptr<PK11SlotInfo, MaybeDeleteHelper<PK11SlotInfo>> slot( + PK11_GetInternalKeySlot()); + + if (!slot.get()) { + CK_LOGE("Failed to get internal PK11 slot"); + return false; + } + + SECItem keyItem = {siBuffer, (unsigned char*)&aKey[0], CENC_KEY_LEN}; + SECItem ivItem = {siBuffer, (unsigned char*)&aIV[0], CENC_KEY_LEN}; + + std::unique_ptr<PK11SymKey, MaybeDeleteHelper<PK11SymKey>> key( + PK11_ImportSymKey(slot.get(), CKM_AES_CBC, PK11_OriginUnwrap, CKA_DECRYPT, + &keyItem, nullptr)); + + if (!key.get()) { + CK_LOGE("Failed to import sym key"); + return false; + } + + std::unique_ptr<PK11Context, MaybeDeleteHelper<PK11Context>> ctx( + PK11_CreateContextBySymKey(CKM_AES_CBC, CKA_DECRYPT, key.get(), &ivItem)); + + if (!ctx) { + CK_LOGE("Failed to get PK11Context!"); + return false; + } + + assert(aCryptByteBlock <= 0xFF); + assert(aSkipByteBlock <= 0xFF); + + uint8_t* encryptedSubsample = &aSubsample[0]; + const uint32_t BLOCK_SIZE = 16; + const uint32_t skipBytes = aSkipByteBlock * BLOCK_SIZE; + const uint32_t totalBlocks = aSubsample.Length() / BLOCK_SIZE; + uint32_t blocksProcessed = 0; + + if (aSkipByteBlock == 0) { + // ISO/IEC 23001 - 7 Section 9.6.1 + // 'When the fields default_crypt_byte_block and default_skip_byte_block in + // a version 1 Track Encryption Box('tenc') are non - zero numbers, pattern + // encryption SHALL be applied.' + // So if both are 0, then everything is encrypted. Similarly, if skip is 0 + // and crypt is non-0, everything is encrypted. + // In this case we can just decrypt all the blocks in one call. This is the + // same outcome as decrypting them using smaller steps, as either way the + // CBC result should be the same. + MOZ_ASSERT(skipBytes == 0); + aCryptByteBlock = totalBlocks; + } + + while (blocksProcessed < totalBlocks) { + uint32_t blocksToDecrypt = aCryptByteBlock <= totalBlocks - blocksProcessed + ? aCryptByteBlock + : totalBlocks - blocksProcessed; + uint32_t bytesToDecrypt = blocksToDecrypt * BLOCK_SIZE; + int outLen; + SECStatus rv; + rv = PK11_CipherOp(ctx.get(), encryptedSubsample, &outLen, bytesToDecrypt, + encryptedSubsample, bytesToDecrypt); + if (rv != SECSuccess) { + CK_LOGE("PK11_CipherOp() failed"); + return false; + } + + encryptedSubsample += skipBytes + bytesToDecrypt; + blocksProcessed += aSkipByteBlock + blocksToDecrypt; + } + + return true; +} + +/* static */ +bool ClearKeyUtils::DecryptAES(const vector<uint8_t>& aKey, + vector<uint8_t>& aData, vector<uint8_t>& aIV) { + assert(aIV.size() == CENC_KEY_LEN); + assert(aKey.size() == CENC_KEY_LEN); + + PK11SlotInfo* slot = PK11_GetInternalKeySlot(); + if (!slot) { + CK_LOGE("Failed to get internal PK11 slot"); + return false; + } + + SECItem keyItem = {siBuffer, (unsigned char*)&aKey[0], CENC_KEY_LEN}; + PK11SymKey* key = PK11_ImportSymKey(slot, CKM_AES_CTR, PK11_OriginUnwrap, + CKA_ENCRYPT, &keyItem, nullptr); + PK11_FreeSlot(slot); + if (!key) { + CK_LOGE("Failed to import sym key"); + return false; + } + + CK_AES_CTR_PARAMS params; + params.ulCounterBits = 32; + memcpy(¶ms.cb, &aIV[0], CENC_KEY_LEN); + SECItem paramItem = {siBuffer, (unsigned char*)¶ms, + sizeof(CK_AES_CTR_PARAMS)}; + + unsigned int outLen = 0; + auto rv = PK11_Decrypt(key, CKM_AES_CTR, ¶mItem, &aData[0], &outLen, + aData.size(), &aData[0], aData.size()); + + aData.resize(outLen); + PK11_FreeSymKey(key); + + if (rv != SECSuccess) { + CK_LOGE("PK11_Decrypt() failed"); + return false; + } + + return true; +} + +/** + * 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 (string::size_type 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; + + // Cast idx to size_t before using it as an array-index, + // to pacify clang 'Wchar-subscripts' warning: + size_t idx = static_cast<size_t>(out[i]); + + // out of bounds index for 'sAlphabet' + assert(idx < MOZ_ARRAY_LENGTH(sAlphabet)); + out[i] = sAlphabet[idx]; + } + + return true; +} + +/* static */ +void ClearKeyUtils::MakeKeyRequest(const vector<KeyId>& aKeyIDs, + string& aOutRequest, + SessionType aSessionType) { + assert(!aKeyIDs.empty() && 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\":"); + + aOutRequest.append("\""); + aOutRequest.append(SessionTypeToString(aSessionType)); + 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); + if (!sym) { + return false; + } + } 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, ','); + } +} + +/** + * 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); + } +} + +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; +} + +static bool ParseKeyObject(ParserContext& aCtx, KeyIdPair& aOutKey) { + EXPECT_SYMBOL(aCtx, '{'); + + // Reject empty objects as invalid licenses. + if (PeekSymbol(aCtx) == '}') { + GetNextSymbol(aCtx); + return 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; + // By spec, type must be "oct". + if (value != "oct") return false; + } 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, ','); + } + + return !key.empty() && !keyId.empty() && + DecodeBase64(keyId, aOutKey.mKeyId) && + DecodeBase64(key, aOutKey.mKey) && GetNextSymbol(aCtx) == '}'; +} + +static bool ParseKeys(ParserContext& aCtx, vector<KeyIdPair>& aOutKeys) { + // Consume start of array. + EXPECT_SYMBOL(aCtx, '['); + + while (true) { + KeyIdPair key; + if (!ParseKeyObject(aCtx, key)) { + CK_LOGE("Failed to parse key object"); + return false; + } + + assert(!key.mKey.empty() && !key.mKeyId.empty()); + 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, + SessionType aSessionType) { + 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; + if (type != SessionTypeToString(aSessionType)) { + 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; +} + +static bool ParseKeyIds(ParserContext& aCtx, vector<KeyId>& aOutKeyIds) { + // Consume start of array. + EXPECT_SYMBOL(aCtx, '['); + + while (true) { + string label; + vector<uint8_t> keyId; + if (!GetNextLabel(aCtx, label) || !DecodeBase64(label, keyId)) { + return false; + } + if (!keyId.empty() && keyId.size() <= kMaxKeyIdsLength) { + aOutKeyIds.push_back(keyId); + } + + uint8_t sym = PeekSymbol(aCtx); + if (!sym || sym == ']') { + break; + } + + EXPECT_SYMBOL(aCtx, ','); + } + + return GetNextSymbol(aCtx) == ']'; +} + +/* static */ +bool ClearKeyUtils::ParseKeyIdsInitData(const uint8_t* aInitData, + uint32_t aInitDataSize, + vector<KeyId>& aOutKeyIds) { + ParserContext ctx; + ctx.mIter = aInitData; + ctx.mEnd = aInitData + aInitDataSize; + + // Consume '{' from start of object. + EXPECT_SYMBOL(ctx, '{'); + + while (true) { + string label; + // Consume member kids. + if (!GetNextLabel(ctx, label)) return false; + EXPECT_SYMBOL(ctx, ':'); + + if (label == "kids") { + // Parse "kids" array. + if (!ParseKeyIds(ctx, aOutKeyIds) || aOutKeyIds.empty()) { + 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; +} + +/* static */ const char* ClearKeyUtils::SessionTypeToString( + SessionType aSessionType) { + switch (aSessionType) { + case SessionType::kTemporary: + return "temporary"; + case SessionType::kPersistentLicense: + return "persistent-license"; + default: { + // We don't support any other license types. + assert(false); + return "invalid"; + } + } +} + +/* static */ +bool ClearKeyUtils::IsValidSessionId(const char* aBuff, uint32_t aLength) { + if (aLength > 10) { + // 10 is the max number of characters in UINT32_MAX when + // represented as a string; ClearKey session ids are integers. + return false; + } + for (uint32_t i = 0; i < aLength; i++) { + if (!isdigit(aBuff[i])) { + return false; + } + } + return true; +} + +string ClearKeyUtils::ToHexString(const uint8_t* aBytes, uint32_t aLength) { + stringstream ss; + ss << std::showbase << std::uppercase << std::hex; + for (uint32_t i = 0; i < aLength; ++i) { + ss << std::hex << static_cast<uint32_t>(aBytes[i]); + ss << " "; + } + + return ss.str(); +} diff --git a/dom/media/eme/clearkey/ClearKeyUtils.h b/dom/media/eme/clearkey/ClearKeyUtils.h new file mode 100644 index 0000000000..04c227c71d --- /dev/null +++ b/dom/media/eme/clearkey/ClearKeyUtils.h @@ -0,0 +1,107 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __ClearKeyUtils_h__ +#define __ClearKeyUtils_h__ + +#include <assert.h> +// stdef.h is required for content_decryption_module to work on Unix systems. +#include <stddef.h> +#include <stdint.h> + +#include <string> +#include <vector> + +#include "content_decryption_module.h" +#include "pk11pub.h" + +#include "mozilla/Span.h" + +#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__) +# define CK_LOGARRAY(APREPEND, ADATA, ADATA_SIZE) \ + CK_LogArray(APREPEND, ADATA, ADATA_SIZE) +#else +// Note: Enabling logging slows things down a LOT, especially when logging to +// a file. +# define CK_LOGE(...) +# define CK_LOGD(...) +# define CK_LOGW(...) +# define CK_LOGARRAY(APREPEND, ADATA, ADATA_SIZE) +#endif + +typedef std::vector<uint8_t> KeyId; +typedef std::vector<uint8_t> Key; + +// The session response size should be within a reasonable limit. +// The size 64 KB is referenced from web-platform-test. +static const uint32_t kMaxSessionResponseLength = 65536; + +// Provide limitation for KeyIds length and webm initData size. +static const uint32_t kMaxWebmInitDataSize = 65536; +static const uint32_t kMaxKeyIdsLength = 512; + +void CK_LogArray(const char* aPrepend, const uint8_t* aData, + const uint32_t aDataSize); + +struct KeyIdPair { + KeyId mKeyId; + Key mKey; +}; + +class ClearKeyUtils { + public: + static bool DecryptCbcs(const std::vector<uint8_t>& aKey, + const std::vector<uint8_t>& aIV, + mozilla::Span<uint8_t> aSubsampleEncryptedRange, + uint32_t aCryptByteBlocks, uint32_t aSkipByteBlocks); + + static bool DecryptAES(const std::vector<uint8_t>& aKey, + std::vector<uint8_t>& aData, + std::vector<uint8_t>& aIV); + + static bool ParseKeyIdsInitData(const uint8_t* aInitData, + uint32_t aInitDataSize, + std::vector<KeyId>& aOutKeyIds); + + static void MakeKeyRequest(const std::vector<KeyId>& aKeyIds, + std::string& aOutRequest, + cdm::SessionType aSessionType); + + static bool ParseJWK(const uint8_t* aKeyData, uint32_t aKeyDataSize, + std::vector<KeyIdPair>& aOutKeys, + cdm::SessionType aSessionType); + static const char* SessionTypeToString(cdm::SessionType aSessionType); + + static bool IsValidSessionId(const char* aBuff, uint32_t aLength); + + static std::string ToHexString(const uint8_t* aBytes, uint32_t aLength); +}; + +template <class Container, class Element> +inline bool Contains(const Container& aContainer, const Element& aElement) { + return aContainer.find(aElement) != aContainer.end(); +} + +template <typename T> +inline void Assign(std::vector<T>& aVec, const T* aData, size_t aLength) { + aVec.assign(aData, aData + aLength); +} + +#endif // __ClearKeyUtils_h__ diff --git a/dom/media/eme/clearkey/RefCounted.h b/dom/media/eme/clearkey/RefCounted.h new file mode 100644 index 0000000000..25f308164c --- /dev/null +++ b/dom/media/eme/clearkey/RefCounted.h @@ -0,0 +1,85 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __RefCount_h__ +#define __RefCount_h__ + +#include <assert.h> +#include <stdint.h> + +#include <atomic> + +#include "ClearKeyUtils.h" + +// Note: Thread safe. +class RefCounted { + public: + void AddRef() { ++mRefCount; } + + uint32_t Release() { + uint32_t newCount = --mRefCount; + if (!newCount) { + delete this; + } + return newCount; + } + + protected: + RefCounted() : mRefCount(0) {} + virtual ~RefCounted() { assert(!mRefCount); } + std::atomic<uint32_t> mRefCount; +}; + +template <class T> +class RefPtr { + public: + RefPtr(const RefPtr& src) { Set(src.mPtr); } + + explicit RefPtr(T* aPtr) { Set(aPtr); } + RefPtr() { Set(nullptr); } + + ~RefPtr() { Set(nullptr); } + T* operator->() const { return mPtr; } + T** operator&() { return &mPtr; } + T* operator->() { return mPtr; } + operator T*() { return mPtr; } + + T* Get() const { return mPtr; } + + RefPtr& operator=(T* aVal) { + Set(aVal); + return *this; + } + + private: + T* Set(T* aPtr) { + if (mPtr == aPtr) { + return aPtr; + } + if (mPtr) { + mPtr->Release(); + } + mPtr = aPtr; + if (mPtr) { + aPtr->AddRef(); + } + return mPtr; + } + + T* mPtr = nullptr; +}; + +#endif // __RefCount_h__ diff --git a/dom/media/eme/clearkey/gtest/TestClearKeyUtils.cpp b/dom/media/eme/clearkey/gtest/TestClearKeyUtils.cpp new file mode 100644 index 0000000000..bff6d0f356 --- /dev/null +++ b/dom/media/eme/clearkey/gtest/TestClearKeyUtils.cpp @@ -0,0 +1,81 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "gtest/gtest.h" +#include <algorithm> +#include <stdint.h> +#include <vector> + +#include "../ClearKeyBase64.cpp" + +using namespace std; + +struct B64Test { + string b64; + vector<uint8_t> raw; + bool shouldPass; +}; + +const B64Test tests[] = { + {"AAAAADk4AU4AAAAAAAAAAA", + {0x0, 0x0, 0x0, 0x0, 0x39, 0x38, 0x1, 0x4e, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0}, + true}, + {"h2mqp1zAJjDIC34YXEXBxA==", + {0x87, 0x69, 0xaa, 0xa7, 0x5c, 0xc0, 0x26, 0x30, 0xc8, 0xb, 0x7e, 0x18, + 0x5c, 0x45, 0xc1, 0xc4}, + true}, + {"flcdA35XHQN-Vx0DflcdAw", + {0x7e, 0x57, 0x1d, 0x3, 0x7e, 0x57, 0x1d, 0x3, 0x7e, 0x57, 0x1d, 0x3, 0x7e, + 0x57, 0x1d, 0x3}, + true}, + { + "flczM35XMzN-VzMzflczMw", + {0x7e, 0x57, 0x33, 0x33, 0x7e, 0x57, 0x33, 0x33, 0x7e, 0x57, 0x33, 0x33, + 0x7e, 0x57, 0x33, 0x33}, + true, + }, + {"flcdBH5XHQR-Vx0EflcdBA", + {0x7e, 0x57, 0x1d, 0x4, 0x7e, 0x57, 0x1d, 0x4, 0x7e, 0x57, 0x1d, 0x4, 0x7e, + 0x57, 0x1d, 0x4}, + true}, + {"fldERH5XRER-V0REfldERA", + {0x7e, 0x57, 0x44, 0x44, 0x7e, 0x57, 0x44, 0x44, 0x7e, 0x57, 0x44, 0x44, + 0x7e, 0x57, 0x44, 0x44}, + true}, + {"fuzzbiz=", {0x7e, 0xec, 0xf3, 0x6e, 0x2c}, true}, + {"fuzzbizfuzzbizfuzzbizfuzzbizfuzzbizfuzzbizfuzzbizfuzzbiz", + {0x7e, 0xec, 0xf3, 0x6e, 0x2c, 0xdf, 0xbb, 0x3c, 0xdb, 0x8b, 0x37, + 0xee, 0xcf, 0x36, 0xe2, 0xcd, 0xfb, 0xb3, 0xcd, 0xb8, 0xb3, 0x7e, + 0xec, 0xf3, 0x6e, 0x2c, 0xdf, 0xbb, 0x3c, 0xdb, 0x8b, 0x37, 0xee, + 0xcf, 0x36, 0xe2, 0xcd, 0xfb, 0xb3, 0xcd, 0xb8, 0xb3}, + true}, + {"", {}, true}, + {"00", {0xd3}, true}, + {"000", {0xd3, 0x4d}, true}, + + {"invalid", {0x8a, 0x7b, 0xda, 0x96, 0x27}, true}, + {"invalic", {0x8a, 0x7b, 0xda, 0x96, 0x27}, true}, + + // Failure tests + {"A", {}, false}, // 1 character is too few. + {"_", {}, false}, // 1 character is too few. +}; + +TEST(ClearKey, DecodeBase64) +{ + for (const B64Test& test : tests) { + vector<uint8_t> v; + bool rv = DecodeBase64(string(test.b64), v); + EXPECT_EQ(test.shouldPass, rv); + if (test.shouldPass) { + EXPECT_EQ(test.raw.size(), v.size()); + for (size_t k = 0; k < test.raw.size(); k++) { + EXPECT_EQ(test.raw[k], v[k]); + } + } + } +} diff --git a/dom/media/eme/clearkey/gtest/moz.build b/dom/media/eme/clearkey/gtest/moz.build new file mode 100644 index 0000000000..5771c4724c --- /dev/null +++ b/dom/media/eme/clearkey/gtest/moz.build @@ -0,0 +1,15 @@ +# -*- Mode: python; 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/. + +UNIFIED_SOURCES += [ + "TestClearKeyUtils.cpp", +] + +FINAL_LIBRARY = "xul-gtest" + +LOCAL_INCLUDES += [ + "..", +] diff --git a/dom/media/eme/clearkey/moz.build b/dom/media/eme/clearkey/moz.build new file mode 100644 index 0000000000..57160624ea --- /dev/null +++ b/dom/media/eme/clearkey/moz.build @@ -0,0 +1,37 @@ +# 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/. + +EXPORTS += [ + "ArrayUtils.h", + "BigEndian.h", + "ClearKeyBase64.h", + "ClearKeyDecryptionManager.h", + "ClearKeyPersistence.h", + "ClearKeySession.h", + "ClearKeySessionManager.h", + "ClearKeyStorage.h", + "ClearKeyUtils.h", + "RefCounted.h", +] + +UNIFIED_SOURCES += [ + "ClearKeyBase64.cpp", + "ClearKeyDecryptionManager.cpp", + "ClearKeyPersistence.cpp", + "ClearKeySession.cpp", + "ClearKeySessionManager.cpp", + "ClearKeyStorage.cpp", + "ClearKeyUtils.cpp", +] + +TEST_DIRS += [ + "gtest", +] + +USE_LIBS += [ + "nss", + "psshparser", +] + +Library("gecko-clearkey") |