diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /media/gmp-clearkey | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream.tar.xz firefox-esr-upstream.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'media/gmp-clearkey')
31 files changed, 4811 insertions, 0 deletions
diff --git a/media/gmp-clearkey/0.1/ArrayUtils.h b/media/gmp-clearkey/0.1/ArrayUtils.h new file mode 100644 index 0000000000..ae5f33b68e --- /dev/null +++ b/media/gmp-clearkey/0.1/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/media/gmp-clearkey/0.1/BigEndian.h b/media/gmp-clearkey/0.1/BigEndian.h new file mode 100644 index 0000000000..5ea1b6042f --- /dev/null +++ b/media/gmp-clearkey/0.1/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/media/gmp-clearkey/0.1/ClearKeyBase64.cpp b/media/gmp-clearkey/0.1/ClearKeyBase64.cpp new file mode 100644 index 0000000000..bb610afd1b --- /dev/null +++ b/media/gmp-clearkey/0.1/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/media/gmp-clearkey/0.1/ClearKeyBase64.h b/media/gmp-clearkey/0.1/ClearKeyBase64.h new file mode 100644 index 0000000000..39309c031e --- /dev/null +++ b/media/gmp-clearkey/0.1/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/media/gmp-clearkey/0.1/ClearKeyCDM.cpp b/media/gmp-clearkey/0.1/ClearKeyCDM.cpp new file mode 100644 index 0000000000..8f1f3655ec --- /dev/null +++ b/media/gmp-clearkey/0.1/ClearKeyCDM.cpp @@ -0,0 +1,188 @@ +/* -*- 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 "ClearKeyCDM.h" + +#include "ClearKeyUtils.h" + +using namespace cdm; + +ClearKeyCDM::ClearKeyCDM(Host_10* aHost) { + mHost = aHost; + mSessionManager = new ClearKeySessionManager(mHost); +} + +void ClearKeyCDM::Initialize(bool aAllowDistinctiveIdentifier, + bool aAllowPersistentState, + bool aUseHardwareSecureCodecs) { + mSessionManager->Init(aAllowDistinctiveIdentifier, aAllowPersistentState); + // We call mHost->OnInitialized() in the session manager once it has + // initialized. +} + +void ClearKeyCDM::GetStatusForPolicy(uint32_t aPromiseId, + const Policy& aPolicy) { + // MediaKeys::GetStatusForPolicy checks the keysystem and + // reject the promise with NS_ERROR_DOM_NOT_SUPPORTED_ERR without calling CDM. + // This function should never be called and is not supported. + assert(false); +} +void ClearKeyCDM::SetServerCertificate(uint32_t aPromiseId, + const uint8_t* aServerCertificateData, + uint32_t aServerCertificateDataSize) { + mSessionManager->SetServerCertificate(aPromiseId, aServerCertificateData, + aServerCertificateDataSize); +} + +void ClearKeyCDM::CreateSessionAndGenerateRequest(uint32_t aPromiseId, + SessionType aSessionType, + InitDataType aInitDataType, + const uint8_t* aInitData, + uint32_t aInitDataSize) { + mSessionManager->CreateSession(aPromiseId, aInitDataType, aInitData, + aInitDataSize, aSessionType); +} + +void ClearKeyCDM::LoadSession(uint32_t aPromiseId, SessionType aSessionType, + const char* aSessionId, uint32_t aSessionIdSize) { + mSessionManager->LoadSession(aPromiseId, aSessionId, aSessionIdSize); +} + +void ClearKeyCDM::UpdateSession(uint32_t aPromiseId, const char* aSessionId, + uint32_t aSessionIdSize, + const uint8_t* aResponse, + uint32_t aResponseSize) { + mSessionManager->UpdateSession(aPromiseId, aSessionId, aSessionIdSize, + aResponse, aResponseSize); +} + +void ClearKeyCDM::CloseSession(uint32_t aPromiseId, const char* aSessionId, + uint32_t aSessionIdSize) { + mSessionManager->CloseSession(aPromiseId, aSessionId, aSessionIdSize); +} + +void ClearKeyCDM::RemoveSession(uint32_t aPromiseId, const char* aSessionId, + uint32_t aSessionIdSize) { + mSessionManager->RemoveSession(aPromiseId, aSessionId, aSessionIdSize); +} + +void ClearKeyCDM::TimerExpired(void* aContext) { + // Clearkey is not interested in timers, so this method has not been + // implemented. + assert(false); +} + +Status ClearKeyCDM::Decrypt(const InputBuffer_2& aEncryptedBuffer, + DecryptedBlock* aDecryptedBuffer) { + if (mIsProtectionQueryEnabled) { + // Piggyback this check onto Decrypt calls. If Clearkey implements a timer + // based approach for firing events, we could instead trigger the check + // using that mechanism. + mSessionManager->QueryOutputProtectionStatusIfNeeded(); + } + return mSessionManager->Decrypt(aEncryptedBuffer, aDecryptedBuffer); +} + +Status ClearKeyCDM::InitializeAudioDecoder( + const AudioDecoderConfig_2& aAudioDecoderConfig) { + // Audio decoding is not supported by Clearkey because Widevine doesn't + // support it and Clearkey's raison d'etre is to provide test coverage + // for paths that Widevine will exercise in the wild. + return Status::kDecodeError; +} + +Status ClearKeyCDM::InitializeVideoDecoder( + const VideoDecoderConfig_2& aVideoDecoderConfig) { +#ifdef ENABLE_WMF + mVideoDecoder = new VideoDecoder(mHost); + return mVideoDecoder->InitDecode(aVideoDecoderConfig); +#else + return Status::kDecodeError; +#endif +} + +void ClearKeyCDM::DeinitializeDecoder(StreamType aDecoderType) { +#ifdef ENABLE_WMF + if (aDecoderType == StreamType::kStreamTypeVideo) { + mVideoDecoder->DecodingComplete(); + mVideoDecoder = nullptr; + } +#endif +} + +void ClearKeyCDM::ResetDecoder(StreamType aDecoderType) { +#ifdef ENABLE_WMF + if (aDecoderType == StreamType::kStreamTypeVideo) { + mVideoDecoder->Reset(); + } +#endif +} + +Status ClearKeyCDM::DecryptAndDecodeFrame(const InputBuffer_2& aEncryptedBuffer, + VideoFrame* aVideoFrame) { +#ifdef ENABLE_WMF + if (mIsProtectionQueryEnabled) { + // Piggyback this check onto Decrypt + Decode. If Clearkey implements a + // timer based approach for firing events, we could instead trigger the + // check using that mechanism. + mSessionManager->QueryOutputProtectionStatusIfNeeded(); + } + return mVideoDecoder->Decode(aEncryptedBuffer, aVideoFrame); +#else + return Status::kDecodeError; +#endif +} + +Status ClearKeyCDM::DecryptAndDecodeSamples( + const InputBuffer_2& aEncryptedBuffer, AudioFrames* aAudioFrame) { + // Audio decoding is not supported by Clearkey because Widevine doesn't + // support it and Clearkey's raison d'etre is to provide test coverage + // for paths that Widevine will exercise in the wild. + return Status::kDecodeError; +} + +void ClearKeyCDM::OnPlatformChallengeResponse( + const PlatformChallengeResponse& aResponse) { + // This function should never be called and is not supported. + assert(false); +} + +void ClearKeyCDM::OnQueryOutputProtectionStatus( + QueryResult aResult, uint32_t aLinkMask, uint32_t aOutputProtectionMask) { + // The higher level GMP machinery should not forward us query information + // unless we've requested it (even if mutiple CDMs exist at once and some + // others are reqeusting this info). If this assert fires we're violating + // that. + MOZ_ASSERT(mIsProtectionQueryEnabled, + "Should only receive a protection status " + "mIsProtectionQueryEnabled is true"); + // The session manager handles the guts of this for ClearKey. + mSessionManager->OnQueryOutputProtectionStatus(aResult, aLinkMask, + aOutputProtectionMask); +} + +void ClearKeyCDM::OnStorageId(uint32_t aVersion, const uint8_t* aStorageId, + uint32_t aStorageIdSize) { + // This function should never be called and is not supported. + assert(false); +} + +void ClearKeyCDM::Destroy() { + mSessionManager->DecryptingComplete(); +#ifdef ENABLE_WMF + // If we have called 'DeinitializeDecoder' mVideoDecoder will be null. + if (mVideoDecoder) { + mVideoDecoder->DecodingComplete(); + } +#endif + delete this; +} + +void ClearKeyCDM::EnableProtectionQuery() { + MOZ_ASSERT(!mIsProtectionQueryEnabled, + "Should not be called more than once per CDM"); + mIsProtectionQueryEnabled = true; +} diff --git a/media/gmp-clearkey/0.1/ClearKeyCDM.h b/media/gmp-clearkey/0.1/ClearKeyCDM.h new file mode 100644 index 0000000000..6977b0e787 --- /dev/null +++ b/media/gmp-clearkey/0.1/ClearKeyCDM.h @@ -0,0 +1,103 @@ +/* -*- 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/. */ + +#ifndef ClearKeyCDM_h_ +#define ClearKeyCDM_h_ + +// This include is required in order for content_decryption_module to work +// on Unix systems. +#include <stddef.h> + +#include "content_decryption_module.h" + +#include "ClearKeySessionManager.h" + +#ifdef ENABLE_WMF +# include "VideoDecoder.h" +# include "WMFUtils.h" +#endif + +class ClearKeyCDM : public cdm::ContentDecryptionModule_10 { + private: + RefPtr<ClearKeySessionManager> mSessionManager; +#ifdef ENABLE_WMF + RefPtr<VideoDecoder> mVideoDecoder; +#endif + bool mIsProtectionQueryEnabled = false; + + protected: + cdm::Host_10* mHost; + + public: + explicit ClearKeyCDM(cdm::Host_10* aHost); + + void Initialize(bool aAllowDistinctiveIdentifier, bool aAllowPersistentState, + bool aUseHardwareSecureCodecs) override; + + void GetStatusForPolicy(uint32_t aPromiseId, + const cdm::Policy& aPolicy) override; + + void SetServerCertificate(uint32_t aPromiseId, + const uint8_t* aServerCertificateData, + uint32_t aServerCertificateDataSize) override; + + void CreateSessionAndGenerateRequest(uint32_t aPromiseId, + cdm::SessionType aSessionType, + cdm::InitDataType aInitDataType, + const uint8_t* aInitData, + uint32_t aInitDataSize) override; + + void LoadSession(uint32_t aPromiseId, cdm::SessionType aSessionType, + const char* aSessionId, uint32_t aSessionIdSize) override; + + void UpdateSession(uint32_t aPromiseId, const char* aSessionId, + uint32_t aSessionIdSize, const uint8_t* aResponse, + uint32_t aResponseSize) override; + + void CloseSession(uint32_t aPromiseId, const char* aSessionId, + uint32_t aSessionIdSize) override; + + void RemoveSession(uint32_t aPromiseId, const char* aSessionId, + uint32_t aSessionIdSize) override; + + void TimerExpired(void* aContext) override; + + cdm::Status Decrypt(const cdm::InputBuffer_2& aEncryptedBuffer, + cdm::DecryptedBlock* aDecryptedBuffer) override; + + cdm::Status InitializeAudioDecoder( + const cdm::AudioDecoderConfig_2& aAudioDecoderConfig) override; + + cdm::Status InitializeVideoDecoder( + const cdm::VideoDecoderConfig_2& aVideoDecoderConfig) override; + + void DeinitializeDecoder(cdm::StreamType aDecoderType) override; + + void ResetDecoder(cdm::StreamType aDecoderType) override; + + cdm::Status DecryptAndDecodeFrame(const cdm::InputBuffer_2& aEncryptedBuffer, + cdm::VideoFrame* aVideoFrame) override; + + cdm::Status DecryptAndDecodeSamples( + const cdm::InputBuffer_2& aEncryptedBuffer, + cdm::AudioFrames* aAudioFrame) override; + + void OnPlatformChallengeResponse( + const cdm::PlatformChallengeResponse& aResponse) override; + + void OnQueryOutputProtectionStatus(cdm::QueryResult aResult, + uint32_t aLinkMask, + uint32_t aOutputProtectionMask) override; + + void OnStorageId(uint32_t aVersion, const uint8_t* aStorageId, + uint32_t aStorageIdSize) override; + + void Destroy() override; + + void EnableProtectionQuery(); +}; + +#endif diff --git a/media/gmp-clearkey/0.1/ClearKeyDecryptionManager.cpp b/media/gmp-clearkey/0.1/ClearKeyDecryptionManager.cpp new file mode 100644 index 0000000000..7eeca88985 --- /dev/null +++ b/media/gmp-clearkey/0.1/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/media/gmp-clearkey/0.1/ClearKeyDecryptionManager.h b/media/gmp-clearkey/0.1/ClearKeyDecryptionManager.h new file mode 100644 index 0000000000..c21ff3119a --- /dev/null +++ b/media/gmp-clearkey/0.1/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/media/gmp-clearkey/0.1/ClearKeyPersistence.cpp b/media/gmp-clearkey/0.1/ClearKeyPersistence.cpp new file mode 100644 index 0000000000..0b81f81399 --- /dev/null +++ b/media/gmp-clearkey/0.1/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/media/gmp-clearkey/0.1/ClearKeyPersistence.h b/media/gmp-clearkey/0.1/ClearKeyPersistence.h new file mode 100644 index 0000000000..b832db8c11 --- /dev/null +++ b/media/gmp-clearkey/0.1/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/media/gmp-clearkey/0.1/ClearKeySession.cpp b/media/gmp-clearkey/0.1/ClearKeySession.cpp new file mode 100644 index 0000000000..b5454d1d0e --- /dev/null +++ b/media/gmp-clearkey/0.1/ClearKeySession.cpp @@ -0,0 +1,67 @@ +/* + * 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()) { + return false; + } + + return true; +} + +SessionType ClearKeySession::Type() const { return mSessionType; } + +void ClearKeySession::AddKeyId(const KeyId& aKeyId) { + mKeyIds.push_back(aKeyId); +} diff --git a/media/gmp-clearkey/0.1/ClearKeySession.h b/media/gmp-clearkey/0.1/ClearKeySession.h new file mode 100644 index 0000000000..3a130aa45a --- /dev/null +++ b/media/gmp-clearkey/0.1/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/media/gmp-clearkey/0.1/ClearKeySessionManager.cpp b/media/gmp-clearkey/0.1/ClearKeySessionManager.cpp new file mode 100644 index 0000000000..54a15f4fcf --- /dev/null +++ b/media/gmp-clearkey/0.1/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/media/gmp-clearkey/0.1/ClearKeySessionManager.h b/media/gmp-clearkey/0.1/ClearKeySessionManager.h new file mode 100644 index 0000000000..3a930f666d --- /dev/null +++ b/media/gmp-clearkey/0.1/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/media/gmp-clearkey/0.1/ClearKeyStorage.cpp b/media/gmp-clearkey/0.1/ClearKeyStorage.cpp new file mode 100644 index 0000000000..1375d33c57 --- /dev/null +++ b/media/gmp-clearkey/0.1/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/media/gmp-clearkey/0.1/ClearKeyStorage.h b/media/gmp-clearkey/0.1/ClearKeyStorage.h new file mode 100644 index 0000000000..bd8e99b901 --- /dev/null +++ b/media/gmp-clearkey/0.1/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/media/gmp-clearkey/0.1/ClearKeyUtils.cpp b/media/gmp-clearkey/0.1/ClearKeyUtils.cpp new file mode 100644 index 0000000000..1b1a7b4200 --- /dev/null +++ b/media/gmp-clearkey/0.1/ClearKeyUtils.cpp @@ -0,0 +1,653 @@ +/* + * 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) { + assert(aKey.size() == CENC_KEY_LEN); + assert(aIV.size() == CENC_KEY_LEN); + assert(aCryptByteBlock <= 0xFF); + assert(aSkipByteBlock <= 0xFF); + + 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)); + + 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/media/gmp-clearkey/0.1/ClearKeyUtils.h b/media/gmp-clearkey/0.1/ClearKeyUtils.h new file mode 100644 index 0000000000..04c227c71d --- /dev/null +++ b/media/gmp-clearkey/0.1/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/media/gmp-clearkey/0.1/RefCounted.h b/media/gmp-clearkey/0.1/RefCounted.h new file mode 100644 index 0000000000..25f308164c --- /dev/null +++ b/media/gmp-clearkey/0.1/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/media/gmp-clearkey/0.1/VideoDecoder.cpp b/media/gmp-clearkey/0.1/VideoDecoder.cpp new file mode 100644 index 0000000000..6417defdaf --- /dev/null +++ b/media/gmp-clearkey/0.1/VideoDecoder.cpp @@ -0,0 +1,325 @@ +/* + * Copyright 2013, 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 <algorithm> +#include <cstdint> +#include <limits> + +#include "BigEndian.h" +#include "ClearKeyUtils.h" +#include "ClearKeyDecryptionManager.h" +#include "VideoDecoder.h" +#include "content_decryption_module.h" +#include "mozilla/CheckedInt.h" + +using namespace wmf; + +VideoDecoder::VideoDecoder(cdm::Host_10* aHost) + : mHost(aHost), mHasShutdown(false) { + CK_LOGD("VideoDecoder created"); + + // We drop the ref in DecodingComplete(). + AddRef(); + + mDecoder = new WMFH264Decoder(); + + uint32_t cores = std::max(1u, std::thread::hardware_concurrency()); + + HRESULT hr = mDecoder->Init(cores); + if (FAILED(hr)) { + CK_LOGE("Failed to initialize mDecoder!"); + } +} + +VideoDecoder::~VideoDecoder() { CK_LOGD("VideoDecoder destroyed"); } + +cdm::Status VideoDecoder::InitDecode(const cdm::VideoDecoderConfig_2& aConfig) { + CK_LOGD("VideoDecoder::InitDecode"); + + if (!mDecoder) { + CK_LOGD("VideoDecoder::InitDecode failed to init WMFH264Decoder"); + + return cdm::Status::kDecodeError; + } + + return cdm::Status::kSuccess; +} + +cdm::Status VideoDecoder::Decode(const cdm::InputBuffer_2& aInputBuffer, + cdm::VideoFrame* aVideoFrame) { + CK_LOGD("VideoDecoder::Decode"); + // If the input buffer we have been passed has a null buffer, it means we + // should drain. + if (!aInputBuffer.data) { + // This will drain the decoder until there are no frames left to drain, + // whereupon it will return 'NeedsMoreData'. + CK_LOGD("VideoDecoder::Decode Input buffer null: Draining"); + return Drain(aVideoFrame); + } + + DecodeData* data = new DecodeData(); + Assign(data->mBuffer, aInputBuffer.data, aInputBuffer.data_size); + data->mTimestamp = aInputBuffer.timestamp; + data->mCrypto = CryptoMetaData(&aInputBuffer); + + AutoPtr<DecodeData> d(data); + HRESULT hr; + + if (!data || !mDecoder) { + CK_LOGE("Decode job not set up correctly!"); + return cdm::Status::kDecodeError; + } + + std::vector<uint8_t>& buffer = data->mBuffer; + + if (data->mCrypto.IsValid()) { + cdm::Status rv = + ClearKeyDecryptionManager::Get()->Decrypt(buffer, data->mCrypto); + + if (STATUS_FAILED(rv)) { + CK_LOGARRAY("Failed to decrypt video using key ", aInputBuffer.key_id, + aInputBuffer.key_id_size); + return rv; + } + } + + hr = mDecoder->Input(buffer.data(), buffer.size(), data->mTimestamp); + + CK_LOGD("VideoDecoder::Decode() Input ret hr=0x%x", hr); + + if (FAILED(hr)) { + assert(hr != MF_E_TRANSFORM_NEED_MORE_INPUT); + + CK_LOGE("VideoDecoder::Decode() decode failed ret=0x%x%s", hr, + ((hr == MF_E_NOTACCEPTING) ? " (MF_E_NOTACCEPTING)" : "")); + CK_LOGD("Decode failed. The decoder is not accepting input"); + return cdm::Status::kDecodeError; + } + + return OutputFrame(aVideoFrame); +} + +cdm::Status VideoDecoder::OutputFrame(cdm::VideoFrame* aVideoFrame) { + CK_LOGD("VideoDecoder::OutputFrame"); + + HRESULT hr = S_OK; + + // Read all the output from the decoder. Ideally, this would be a while loop + // where we read the output and check the result as the condition. However, + // this produces a memory leak connected to assigning a new CComPtr to the + // address of the old one, which avoids the CComPtr cleaning up. + while (true) { + CComPtr<IMFSample> output; + hr = mDecoder->Output(&output); + + if (hr != S_OK) { + break; + } + + CK_LOGD("VideoDecoder::OutputFrame Decoder output ret=0x%x", hr); + + mOutputQueue.push(output); + CK_LOGD("VideoDecoder::OutputFrame: Queue size: %u", mOutputQueue.size()); + } + + // If we don't have any inputs, we need more data. + if (mOutputQueue.empty()) { + CK_LOGD("Decode failed. Not enought data; Requesting more input"); + return cdm::Status::kNeedMoreData; + } + + // We will get a MF_E_TRANSFORM_NEED_MORE_INPUT every time, as we always + // consume everything in the buffer. + if (hr != MF_E_TRANSFORM_NEED_MORE_INPUT && FAILED(hr)) { + CK_LOGD("Decode failed output ret=0x%x", hr); + return cdm::Status::kDecodeError; + } + + CComPtr<IMFSample> result = mOutputQueue.front(); + mOutputQueue.pop(); + + // The Chromium CDM API doesn't have support for negative strides, though + // they are theoretically possible in real world data. + if (mDecoder->GetStride() <= 0) { + CK_LOGD("VideoDecoder::OutputFrame Failed! (negative stride)"); + return cdm::Status::kDecodeError; + } + + const IntRect& picture = mDecoder->GetPictureRegion(); + hr = SampleToVideoFrame(result, picture.width, picture.height, + mDecoder->GetStride(), mDecoder->GetFrameHeight(), + aVideoFrame); + if (FAILED(hr)) { + CK_LOGD("VideoDecoder::OutputFrame Failed!"); + return cdm::Status::kDecodeError; + } + + CK_LOGD("VideoDecoder::OutputFrame Succeeded."); + return cdm::Status::kSuccess; +} + +HRESULT +VideoDecoder::SampleToVideoFrame(IMFSample* aSample, int32_t aPictureWidth, + int32_t aPictureHeight, int32_t aStride, + int32_t aFrameHeight, + cdm::VideoFrame* aVideoFrame) { + CK_LOGD("[%p] VideoDecoder::SampleToVideoFrame()", this); + + ENSURE(aSample != nullptr, E_POINTER); + ENSURE(aVideoFrame != nullptr, E_POINTER); + + HRESULT hr; + CComPtr<IMFMediaBuffer> mediaBuffer; + + aVideoFrame->SetFormat(cdm::kI420); + + // Must convert to contiguous mediaBuffer to use IMD2DBuffer interface. + hr = aSample->ConvertToContiguousBuffer(&mediaBuffer); + ENSURE(SUCCEEDED(hr), hr); + + // Try and use the IMF2DBuffer interface if available, otherwise fallback + // to the IMFMediaBuffer interface. Apparently IMF2DBuffer is more efficient, + // but only some systems (Windows 8?) support it. + BYTE* data = nullptr; + LONG stride = 0; + CComPtr<IMF2DBuffer> twoDBuffer; + hr = mediaBuffer->QueryInterface(static_cast<IMF2DBuffer**>(&twoDBuffer)); + if (SUCCEEDED(hr)) { + hr = twoDBuffer->Lock2D(&data, &stride); + ENSURE(SUCCEEDED(hr), hr); + } else { + hr = mediaBuffer->Lock(&data, nullptr, nullptr); + ENSURE(SUCCEEDED(hr), hr); + stride = aStride; + } + + // WMF stores the U and V planes 16-row-aligned, so we need to add padding + // to the row heights to ensure the source offsets of the Y'CbCr planes are + // referenced properly. + // YV12, planar format: [YYYY....][UUUU....][VVVV....] + // i.e., Y, then U, then V. + uint32_t padding = 0; + if (aFrameHeight % 16 != 0) { + padding = 16 - (aFrameHeight % 16); + } + uint32_t srcYSize = stride * (aFrameHeight + padding); + uint32_t srcUVSize = stride * (aFrameHeight + padding) / 4; + uint32_t halfStride = (stride + 1) / 2; + + aVideoFrame->SetStride(cdm::VideoPlane::kYPlane, stride); + aVideoFrame->SetStride(cdm::VideoPlane::kUPlane, halfStride); + aVideoFrame->SetStride(cdm::VideoPlane::kVPlane, halfStride); + + aVideoFrame->SetSize(cdm::Size{aPictureWidth, aPictureHeight}); + + // Note: We allocate the minimal sized buffer required to send the + // frame back over to the parent process. This is so that we request the + // same sized frame as the buffer allocator expects. + using mozilla::CheckedUint32; + CheckedUint32 bufferSize = CheckedUint32(stride) * aPictureHeight + + ((CheckedUint32(stride) * aPictureHeight) / 4) * 2; + + // If the buffer is bigger than the max for a 32 bit, fail to avoid buffer + // overflows. + if (!bufferSize.isValid()) { + CK_LOGD("VideoDecoder::SampleToFrame Buffersize bigger than UINT32_MAX"); + return E_FAIL; + } + + // Get the buffer from the host. + cdm::Buffer* buffer = mHost->Allocate(bufferSize.value()); + aVideoFrame->SetFrameBuffer(buffer); + + // Make sure the buffer is non-null (allocate guarantees it will be of + // sufficient size). + if (!buffer) { + CK_LOGD("VideoDecoder::SampleToFrame Out of memory"); + return E_OUTOFMEMORY; + } + + uint8_t* outBuffer = buffer->Data(); + + aVideoFrame->SetPlaneOffset(cdm::VideoPlane::kYPlane, 0); + + // Offset of U plane is the size of the Y plane, excluding the padding that + // WMF adds. + uint32_t dstUOffset = stride * aPictureHeight; + aVideoFrame->SetPlaneOffset(cdm::VideoPlane::kUPlane, dstUOffset); + + // Offset of the V plane is the size of the Y plane + the size of the U plane, + // excluding any padding WMF adds. + uint32_t dstVOffset = stride * aPictureHeight + (stride * aPictureHeight) / 4; + aVideoFrame->SetPlaneOffset(cdm::VideoPlane::kVPlane, dstVOffset); + + // Copy the pixel data, excluding WMF's padding. + memcpy(outBuffer, data, stride * aPictureHeight); + memcpy(outBuffer + dstUOffset, data + srcYSize, + (stride * aPictureHeight) / 4); + memcpy(outBuffer + dstVOffset, data + srcYSize + srcUVSize, + (stride * aPictureHeight) / 4); + + if (twoDBuffer) { + twoDBuffer->Unlock2D(); + } else { + mediaBuffer->Unlock(); + } + + LONGLONG hns = 0; + hr = aSample->GetSampleTime(&hns); + ENSURE(SUCCEEDED(hr), hr); + + aVideoFrame->SetTimestamp(HNsToUsecs(hns)); + + return S_OK; +} + +void VideoDecoder::Reset() { + CK_LOGD("VideoDecoder::Reset"); + + if (mDecoder) { + mDecoder->Reset(); + } + + // Remove all the frames from the output queue. + while (!mOutputQueue.empty()) { + mOutputQueue.pop(); + } +} + +cdm::Status VideoDecoder::Drain(cdm::VideoFrame* aVideoFrame) { + CK_LOGD("VideoDecoder::Drain()"); + + if (!mDecoder) { + CK_LOGD("Drain failed! Decoder was not initialized"); + return cdm::Status::kDecodeError; + } + + mDecoder->Drain(); + + // Return any pending output. + return OutputFrame(aVideoFrame); +} + +void VideoDecoder::DecodingComplete() { + CK_LOGD("VideoDecoder::DecodingComplete()"); + + mHasShutdown = true; + + // Release the reference we added in the constructor. There may be + // WrapRefCounted tasks that also hold references to us, and keep + // us alive a little longer. + Release(); +} diff --git a/media/gmp-clearkey/0.1/VideoDecoder.h b/media/gmp-clearkey/0.1/VideoDecoder.h new file mode 100644 index 0000000000..6a1544ce65 --- /dev/null +++ b/media/gmp-clearkey/0.1/VideoDecoder.h @@ -0,0 +1,72 @@ +/* + * Copyright 2013, 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 __VideoDecoder_h__ +#define __VideoDecoder_h__ + +// This include is required in order for content_decryption_module to work +// on Unix systems. +#include <stddef.h> + +#include <atomic> +#include <queue> +#include <thread> + +#include "content_decryption_module.h" +#include "WMFH264Decoder.h" + +class VideoDecoder : public RefCounted { + public: + explicit VideoDecoder(cdm::Host_10* aHost); + + cdm::Status InitDecode(const cdm::VideoDecoderConfig_2& aConfig); + + cdm::Status Decode(const cdm::InputBuffer_2& aEncryptedBuffer, + cdm::VideoFrame* aVideoFrame); + + void Reset(); + + void DecodingComplete(); + + bool HasShutdown() { return mHasShutdown; } + + private: + virtual ~VideoDecoder(); + + cdm::Status Drain(cdm::VideoFrame* aVideoFrame); + + struct DecodeData { + std::vector<uint8_t> mBuffer; + uint64_t mTimestamp = 0; + CryptoMetaData mCrypto; + }; + + cdm::Status OutputFrame(cdm::VideoFrame* aVideoFrame); + + HRESULT SampleToVideoFrame(IMFSample* aSample, int32_t aPictureWidth, + int32_t aPictureHeight, int32_t aStride, + int32_t aFrameHeight, + cdm::VideoFrame* aVideoFrame); + + cdm::Host_10* mHost; + wmf::AutoPtr<wmf::WMFH264Decoder> mDecoder; + + std::queue<wmf::CComPtr<IMFSample>> mOutputQueue; + + bool mHasShutdown; +}; + +#endif // __VideoDecoder_h__ diff --git a/media/gmp-clearkey/0.1/WMFH264Decoder.cpp b/media/gmp-clearkey/0.1/WMFH264Decoder.cpp new file mode 100644 index 0000000000..8054b38c64 --- /dev/null +++ b/media/gmp-clearkey/0.1/WMFH264Decoder.cpp @@ -0,0 +1,331 @@ +/* + * Copyright 2013, 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 "WMFH264Decoder.h" + +#include <codecapi.h> + +#include <algorithm> + +namespace wmf { + +WMFH264Decoder::WMFH264Decoder() : mDecoder(nullptr) { + memset(&mInputStreamInfo, 0, sizeof(MFT_INPUT_STREAM_INFO)); + memset(&mOutputStreamInfo, 0, sizeof(MFT_OUTPUT_STREAM_INFO)); +} + +WMFH264Decoder::~WMFH264Decoder() {} + +HRESULT +WMFH264Decoder::Init(int32_t aCoreCount) { + HRESULT hr; + + hr = CreateMFT(__uuidof(CMSH264DecoderMFT), WMFDecoderDllName(), mDecoder); + if (FAILED(hr)) { + // Windows 7 Enterprise Server N (which is what Mozilla's mochitests run + // on) need a different CLSID to instantiate the H.264 decoder. + hr = CreateMFT(CLSID_CMSH264DecMFT, WMFDecoderDllName(), mDecoder); + } + ENSURE(SUCCEEDED(hr), hr); + + CComPtr<IMFAttributes> attr; + hr = mDecoder->GetAttributes(&attr); + ENSURE(SUCCEEDED(hr), hr); + hr = attr->SetUINT32(CODECAPI_AVDecNumWorkerThreads, + GetNumThreads(aCoreCount)); + ENSURE(SUCCEEDED(hr), hr); + + hr = SetDecoderInputType(); + ENSURE(SUCCEEDED(hr), hr); + + hr = SetDecoderOutputType(); + ENSURE(SUCCEEDED(hr), hr); + + hr = SendMFTMessage(MFT_MESSAGE_NOTIFY_BEGIN_STREAMING, 0); + ENSURE(SUCCEEDED(hr), hr); + + hr = SendMFTMessage(MFT_MESSAGE_NOTIFY_START_OF_STREAM, 0); + ENSURE(SUCCEEDED(hr), hr); + + hr = mDecoder->GetInputStreamInfo(0, &mInputStreamInfo); + ENSURE(SUCCEEDED(hr), hr); + + hr = mDecoder->GetOutputStreamInfo(0, &mOutputStreamInfo); + ENSURE(SUCCEEDED(hr), hr); + + return S_OK; +} + +HRESULT +WMFH264Decoder::ConfigureVideoFrameGeometry(IMFMediaType* aMediaType) { + ENSURE(aMediaType != nullptr, E_POINTER); + HRESULT hr; + + IntRect pictureRegion; + hr = wmf::GetPictureRegion(aMediaType, pictureRegion); + ENSURE(SUCCEEDED(hr), hr); + + UINT32 width = 0, height = 0; + hr = MFGetAttributeSize(aMediaType, MF_MT_FRAME_SIZE, &width, &height); + ENSURE(SUCCEEDED(hr), hr); + ENSURE(width <= mozilla::MAX_VIDEO_WIDTH, E_FAIL); + ENSURE(height <= mozilla::MAX_VIDEO_HEIGHT, E_FAIL); + + UINT32 stride = 0; + hr = GetDefaultStride(aMediaType, &stride); + ENSURE(SUCCEEDED(hr), hr); + ENSURE(stride <= mozilla::MAX_VIDEO_WIDTH, E_FAIL); + + // Success! Save state. + mStride = stride; + mVideoWidth = width; + mVideoHeight = height; + mPictureRegion = pictureRegion; + + LOG("WMFH264Decoder frame geometry frame=(%u,%u) stride=%u picture=(%d, %d, " + "%d, %d)\n", + width, height, mStride, mPictureRegion.x, mPictureRegion.y, + mPictureRegion.width, mPictureRegion.height); + + return S_OK; +} + +int32_t WMFH264Decoder::GetFrameHeight() const { return mVideoHeight; } + +const IntRect& WMFH264Decoder::GetPictureRegion() const { + return mPictureRegion; +} + +int32_t WMFH264Decoder::GetStride() const { return mStride; } + +HRESULT +WMFH264Decoder::SetDecoderInputType() { + HRESULT hr; + + CComPtr<IMFMediaType> type; + hr = MFCreateMediaType(&type); + ENSURE(SUCCEEDED(hr), hr); + + hr = type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video); + ENSURE(SUCCEEDED(hr), hr); + + hr = type->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_H264); + ENSURE(SUCCEEDED(hr), hr); + + hr = type->SetUINT32(MF_MT_INTERLACE_MODE, + MFVideoInterlace_MixedInterlaceOrProgressive); + ENSURE(SUCCEEDED(hr), hr); + + hr = mDecoder->SetInputType(0, type, 0); + ENSURE(SUCCEEDED(hr), hr); + + return S_OK; +} + +HRESULT +WMFH264Decoder::SetDecoderOutputType() { + HRESULT hr; + + CComPtr<IMFMediaType> type; + + UINT32 typeIndex = 0; + while (static_cast<void>(type = nullptr), + SUCCEEDED(mDecoder->GetOutputAvailableType(0, typeIndex++, &type))) { + GUID subtype; + hr = type->GetGUID(MF_MT_SUBTYPE, &subtype); + if (FAILED(hr)) { + continue; + } + if (subtype == MFVideoFormat_I420 || subtype == MFVideoFormat_IYUV) { + // On Windows 7 Enterprise N the MFT reports it reports IYUV instead + // of I420. Other Windows' report I420. The formats are the same, so + // support both. + hr = mDecoder->SetOutputType(0, type, 0); + ENSURE(SUCCEEDED(hr), hr); + + hr = ConfigureVideoFrameGeometry(type); + ENSURE(SUCCEEDED(hr), hr); + + return S_OK; + } + } + + return E_FAIL; +} + +HRESULT +WMFH264Decoder::SendMFTMessage(MFT_MESSAGE_TYPE aMsg, UINT32 aData) { + ENSURE(mDecoder != nullptr, E_POINTER); + HRESULT hr = mDecoder->ProcessMessage(aMsg, aData); + ENSURE(SUCCEEDED(hr), hr); + return S_OK; +} + +HRESULT +WMFH264Decoder::CreateInputSample(const uint8_t* aData, uint32_t aDataSize, + Microseconds aTimestamp, + IMFSample** aOutSample) { + HRESULT hr; + CComPtr<IMFSample> sample; + hr = MFCreateSample(&sample); + ENSURE(SUCCEEDED(hr), hr); + + CComPtr<IMFMediaBuffer> buffer; + int32_t bufferSize = + std::max<uint32_t>(uint32_t(mInputStreamInfo.cbSize), aDataSize); + UINT32 alignment = + (mInputStreamInfo.cbAlignment > 1) ? mInputStreamInfo.cbAlignment - 1 : 0; + hr = MFCreateAlignedMemoryBuffer(bufferSize, alignment, &buffer); + ENSURE(SUCCEEDED(hr), hr); + + DWORD maxLength = 0; + DWORD currentLength = 0; + BYTE* dst = nullptr; + hr = buffer->Lock(&dst, &maxLength, ¤tLength); + ENSURE(SUCCEEDED(hr), hr); + + // Copy data into sample's buffer. + memcpy(dst, aData, aDataSize); + + hr = buffer->Unlock(); + ENSURE(SUCCEEDED(hr), hr); + + hr = buffer->SetCurrentLength(aDataSize); + ENSURE(SUCCEEDED(hr), hr); + + hr = sample->AddBuffer(buffer); + ENSURE(SUCCEEDED(hr), hr); + + hr = sample->SetSampleTime(UsecsToHNs(aTimestamp)); + ENSURE(SUCCEEDED(hr), hr); + + *aOutSample = sample.Detach(); + + return S_OK; +} + +HRESULT +WMFH264Decoder::CreateOutputSample(IMFSample** aOutSample) { + HRESULT hr; + CComPtr<IMFSample> sample; + hr = MFCreateSample(&sample); + ENSURE(SUCCEEDED(hr), hr); + + CComPtr<IMFMediaBuffer> buffer; + int32_t bufferSize = mOutputStreamInfo.cbSize; + UINT32 alignment = (mOutputStreamInfo.cbAlignment > 1) + ? mOutputStreamInfo.cbAlignment - 1 + : 0; + hr = MFCreateAlignedMemoryBuffer(bufferSize, alignment, &buffer); + ENSURE(SUCCEEDED(hr), hr); + + hr = sample->AddBuffer(buffer); + ENSURE(SUCCEEDED(hr), hr); + + *aOutSample = sample.Detach(); + + return S_OK; +} + +HRESULT +WMFH264Decoder::GetOutputSample(IMFSample** aOutSample) { + HRESULT hr; + // We allocate samples for MFT output. + MFT_OUTPUT_DATA_BUFFER output = {0}; + + CComPtr<IMFSample> sample; + hr = CreateOutputSample(&sample); + ENSURE(SUCCEEDED(hr), hr); + + output.pSample = sample; + + DWORD status = 0; + hr = mDecoder->ProcessOutput(0, 1, &output, &status); + // LOG(L"WMFH264Decoder::GetOutputSample() ProcessOutput returned 0x%x\n", + // hr); + CComPtr<IMFCollection> events = output.pEvents; // Ensure this is released. + + if (hr == MF_E_TRANSFORM_STREAM_CHANGE) { + // Type change. Probably geometric apperature change. + hr = SetDecoderOutputType(); + ENSURE(SUCCEEDED(hr), hr); + + return GetOutputSample(aOutSample); + } else if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT) { + return MF_E_TRANSFORM_NEED_MORE_INPUT; + } + // Treat other errors as fatal. + ENSURE(SUCCEEDED(hr), hr); + + assert(sample); + + // output.pSample + *aOutSample = sample.Detach(); // AddRefs + return S_OK; +} + +HRESULT +WMFH264Decoder::Input(const uint8_t* aData, uint32_t aDataSize, + Microseconds aTimestamp) { + HRESULT hr; + CComPtr<IMFSample> input = nullptr; + hr = CreateInputSample(aData, aDataSize, aTimestamp, &input); + ENSURE(SUCCEEDED(hr) && input != nullptr, hr); + + hr = mDecoder->ProcessInput(0, input, 0); + if (hr == MF_E_NOTACCEPTING) { + // MFT *already* has enough data to produce a sample. Retrieve it. + LOG("ProcessInput returned MF_E_NOTACCEPTING\n"); + return MF_E_NOTACCEPTING; + } + ENSURE(SUCCEEDED(hr), hr); + + return S_OK; +} + +HRESULT +WMFH264Decoder::Output(IMFSample** aOutput) { + HRESULT hr; + CComPtr<IMFSample> outputSample; + hr = GetOutputSample(&outputSample); + if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT) { + return MF_E_TRANSFORM_NEED_MORE_INPUT; + } + // Treat other errors as fatal. + ENSURE(SUCCEEDED(hr) && outputSample, hr); + + *aOutput = outputSample.Detach(); + + return S_OK; +} + +HRESULT +WMFH264Decoder::Reset() { + HRESULT hr = SendMFTMessage(MFT_MESSAGE_COMMAND_FLUSH, 0); + ENSURE(SUCCEEDED(hr), hr); + + return S_OK; +} + +HRESULT +WMFH264Decoder::Drain() { + HRESULT hr = SendMFTMessage(MFT_MESSAGE_COMMAND_DRAIN, 0); + ENSURE(SUCCEEDED(hr), hr); + + return S_OK; +} + +} // namespace wmf diff --git a/media/gmp-clearkey/0.1/WMFH264Decoder.h b/media/gmp-clearkey/0.1/WMFH264Decoder.h new file mode 100644 index 0000000000..3f74735e23 --- /dev/null +++ b/media/gmp-clearkey/0.1/WMFH264Decoder.h @@ -0,0 +1,70 @@ +/* + * Copyright 2013, 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. + */ + +#if !defined(WMFH264Decoder_h_) +# define WMFH264Decoder_h_ + +# include "WMFUtils.h" + +namespace wmf { + +class WMFH264Decoder { + public: + WMFH264Decoder(); + ~WMFH264Decoder(); + + HRESULT Init(int32_t aCoreCount); + + HRESULT Input(const uint8_t* aData, uint32_t aDataSize, + Microseconds aTimestamp); + + HRESULT Output(IMFSample** aOutput); + + HRESULT Reset(); + + int32_t GetFrameHeight() const; + const IntRect& GetPictureRegion() const; + int32_t GetStride() const; + + HRESULT Drain(); + + private: + HRESULT SetDecoderInputType(); + HRESULT SetDecoderOutputType(); + HRESULT SendMFTMessage(MFT_MESSAGE_TYPE aMsg, UINT32 aData); + + HRESULT CreateInputSample(const uint8_t* aData, uint32_t aDataSize, + Microseconds aTimestamp, IMFSample** aOutSample); + + HRESULT CreateOutputSample(IMFSample** aOutSample); + + HRESULT GetOutputSample(IMFSample** aOutSample); + HRESULT ConfigureVideoFrameGeometry(IMFMediaType* aMediaType); + + MFT_INPUT_STREAM_INFO mInputStreamInfo; + MFT_OUTPUT_STREAM_INFO mOutputStreamInfo; + + CComPtr<IMFTransform> mDecoder; + + int32_t mVideoWidth; + int32_t mVideoHeight; + IntRect mPictureRegion; + int32_t mStride; +}; + +} // namespace wmf + +#endif diff --git a/media/gmp-clearkey/0.1/WMFSymbols.h b/media/gmp-clearkey/0.1/WMFSymbols.h new file mode 100644 index 0000000000..60a0a6854b --- /dev/null +++ b/media/gmp-clearkey/0.1/WMFSymbols.h @@ -0,0 +1,20 @@ +/* + * 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. + */ + +MFPLAT_FUNC(MFCreateSample, "mfplat.dll"); +MFPLAT_FUNC(MFCreateAlignedMemoryBuffer, "mfplat.dll"); +MFPLAT_FUNC(MFGetStrideForBitmapInfoHeader, "evr.dll"); +MFPLAT_FUNC(MFCreateMediaType, "mfplat.dll"); diff --git a/media/gmp-clearkey/0.1/WMFUtils.cpp b/media/gmp-clearkey/0.1/WMFUtils.cpp new file mode 100644 index 0000000000..5aa628f619 --- /dev/null +++ b/media/gmp-clearkey/0.1/WMFUtils.cpp @@ -0,0 +1,223 @@ +/* + * Copyright 2013, 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 "WMFUtils.h" + +#define INITGUID +#include <guiddef.h> +#include <stdio.h> +#include <versionhelpers.h> + +#include <algorithm> + +#include "ClearKeyUtils.h" + +#ifndef __MINGW32__ +# pragma comment(lib, "mfuuid.lib") +# pragma comment(lib, "wmcodecdspuuid") +#endif + +void LOG(const char* format, ...) { +#ifdef WMF_DECODER_LOG + va_list args; + va_start(args, format); + vprintf(format, args); +#endif +} + +DEFINE_GUID(CLSID_CMSH264DecMFT, 0x62CE7E72, 0x4C71, 0x4d20, 0xB1, 0x5D, 0x45, + 0x28, 0x31, 0xA8, 0x7D, 0x9D); + +namespace wmf { + +#define MFPLAT_FUNC(_func, _dllname) decltype(::_func)* _func; +#include "WMFSymbols.h" +#undef MFPLAT_FUNC + +static bool LinkMfplat() { + static bool sInitDone = false; + static bool sInitOk = false; + if (!sInitDone) { + sInitDone = true; + HMODULE handle; + +#define MFPLAT_FUNC(_func, _dllname) \ + handle = GetModuleHandleA(_dllname); \ + if (!(_func = (decltype(_func))(GetProcAddress(handle, #_func)))) { \ + return false; \ + } + +#include "WMFSymbols.h" +#undef MFPLAT_FUNC + sInitOk = true; + } + return sInitOk; +} + +bool EnsureLibs() { + static bool sInitDone = false; + static bool sInitOk = false; + if (!sInitDone) { + sInitOk = LinkMfplat() && !!GetModuleHandleA(WMFDecoderDllName()); + sInitDone = true; + } + return sInitOk; +} + +int32_t MFOffsetToInt32(const MFOffset& aOffset) { + return int32_t(aOffset.value + (aOffset.fract / 65536.0f)); +} + +// Gets the sub-region of the video frame that should be displayed. +// See: +// http://msdn.microsoft.com/en-us/library/windows/desktop/bb530115(v=vs.85).aspx +HRESULT +GetPictureRegion(IMFMediaType* aMediaType, IntRect& aOutPictureRegion) { + // Determine if "pan and scan" is enabled for this media. If it is, we + // only display a region of the video frame, not the entire frame. + BOOL panScan = + MFGetAttributeUINT32(aMediaType, MF_MT_PAN_SCAN_ENABLED, FALSE); + + // If pan and scan mode is enabled. Try to get the display region. + HRESULT hr = E_FAIL; + MFVideoArea videoArea; + memset(&videoArea, 0, sizeof(MFVideoArea)); + if (panScan) { + hr = aMediaType->GetBlob(MF_MT_PAN_SCAN_APERTURE, (UINT8*)&videoArea, + sizeof(MFVideoArea), NULL); + } + + // If we're not in pan-and-scan mode, or the pan-and-scan region is not set, + // check for a minimimum display aperture. + if (!panScan || hr == MF_E_ATTRIBUTENOTFOUND) { + hr = aMediaType->GetBlob(MF_MT_MINIMUM_DISPLAY_APERTURE, (UINT8*)&videoArea, + sizeof(MFVideoArea), NULL); + } + + if (hr == MF_E_ATTRIBUTENOTFOUND) { + // Minimum display aperture is not set, for "backward compatibility with + // some components", check for a geometric aperture. + hr = aMediaType->GetBlob(MF_MT_GEOMETRIC_APERTURE, (UINT8*)&videoArea, + sizeof(MFVideoArea), NULL); + } + + if (SUCCEEDED(hr)) { + // The media specified a picture region, return it. + IntRect picture = IntRect(MFOffsetToInt32(videoArea.OffsetX), + MFOffsetToInt32(videoArea.OffsetY), + videoArea.Area.cx, videoArea.Area.cy); + ENSURE(picture.width <= mozilla::MAX_VIDEO_WIDTH, E_FAIL); + ENSURE(picture.height <= mozilla::MAX_VIDEO_HEIGHT, E_FAIL); + aOutPictureRegion = picture; + return S_OK; + } + + // No picture region defined, fall back to using the entire video area. + UINT32 width = 0, height = 0; + hr = MFGetAttributeSize(aMediaType, MF_MT_FRAME_SIZE, &width, &height); + ENSURE(SUCCEEDED(hr), hr); + ENSURE(width <= mozilla::MAX_VIDEO_WIDTH, E_FAIL); + ENSURE(height <= mozilla::MAX_VIDEO_HEIGHT, E_FAIL); + aOutPictureRegion = IntRect(0, 0, width, height); + return S_OK; +} + +HRESULT +GetDefaultStride(IMFMediaType* aType, uint32_t* aOutStride) { + // Try to get the default stride from the media type. + UINT32 stride = 0; + HRESULT hr = aType->GetUINT32(MF_MT_DEFAULT_STRIDE, &stride); + if (SUCCEEDED(hr)) { + ENSURE(stride <= mozilla::MAX_VIDEO_WIDTH, E_FAIL); + *aOutStride = stride; + return S_OK; + } + + // Stride attribute not set, calculate it. + GUID subtype = GUID_NULL; + uint32_t width = 0; + uint32_t height = 0; + + hr = aType->GetGUID(MF_MT_SUBTYPE, &subtype); + ENSURE(SUCCEEDED(hr), hr); + + hr = MFGetAttributeSize(aType, MF_MT_FRAME_SIZE, &width, &height); + ENSURE(SUCCEEDED(hr), hr); + ENSURE(width <= mozilla::MAX_VIDEO_WIDTH, E_FAIL); + ENSURE(height <= mozilla::MAX_VIDEO_HEIGHT, E_FAIL); + + LONG lstride = 0; + hr = MFGetStrideForBitmapInfoHeader(subtype.Data1, width, &lstride); + ENSURE(SUCCEEDED(hr), hr); + ENSURE(lstride <= mozilla::MAX_VIDEO_WIDTH, E_FAIL); + ENSURE(lstride >= 0, E_FAIL); + *aOutStride = lstride; + + return hr; +} + +void dump(const uint8_t* data, uint32_t len, const char* filename) { + FILE* f = 0; + fopen_s(&f, filename, "wb"); + fwrite(data, len, 1, f); + fclose(f); +} + +HRESULT +CreateMFT(const CLSID& clsid, const char* aDllName, + CComPtr<IMFTransform>& aOutMFT) { + HMODULE module = ::GetModuleHandleA(aDllName); + if (!module) { + LOG("Failed to get %S\n", aDllName); + return E_FAIL; + } + + typedef HRESULT(WINAPI * DllGetClassObjectFnPtr)( + const CLSID& clsid, const IID& iid, void** object); + + DllGetClassObjectFnPtr GetClassObjPtr = + reinterpret_cast<DllGetClassObjectFnPtr>( + GetProcAddress(module, "DllGetClassObject")); + if (!GetClassObjPtr) { + LOG("Failed to get DllGetClassObject\n"); + return E_FAIL; + } + + CComPtr<IClassFactory> classFactory; + HRESULT hr = GetClassObjPtr( + clsid, __uuidof(IClassFactory), + reinterpret_cast<void**>(static_cast<IClassFactory**>(&classFactory))); + if (FAILED(hr)) { + LOG("Failed to get H264 IClassFactory\n"); + return E_FAIL; + } + + hr = classFactory->CreateInstance( + NULL, __uuidof(IMFTransform), + reinterpret_cast<void**>(static_cast<IMFTransform**>(&aOutMFT))); + if (FAILED(hr)) { + LOG("Failed to get create MFT\n"); + return E_FAIL; + } + + return S_OK; +} + +int32_t GetNumThreads(int32_t aCoreCount) { + return aCoreCount > 4 ? -1 : (std::max)(aCoreCount - 1, 1); +} + +} // namespace wmf diff --git a/media/gmp-clearkey/0.1/WMFUtils.h b/media/gmp-clearkey/0.1/WMFUtils.h new file mode 100644 index 0000000000..0c2819e25c --- /dev/null +++ b/media/gmp-clearkey/0.1/WMFUtils.h @@ -0,0 +1,248 @@ +/* + * Copyright 2013, 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 __WMFUtils_h__ +#define __WMFUtils_h__ + +#include <assert.h> +#include <mfapi.h> +#include <mferror.h> +#include <mfobjects.h> +#include <mftransform.h> +#include <wmcodecdsp.h> + +#include <cstdint> +#include <string> + +#include "VideoLimits.h" +#include "content_decryption_module.h" +#include "gmp-platform.h" +#include "mozilla/Attributes.h" + +void LOG(const char* format, ...); + +#ifdef LOG_SAMPLE_DECODE +# define SAMPLE_LOG LOG +#else +# define SAMPLE_LOG(...) +#endif + +#ifndef CLSID_CMSAACDecMFT +# define WMF_MUST_DEFINE_AAC_MFT_CLSID +extern "C" const CLSID CLSID_CMSAACDecMFT; +#endif + +extern "C" const CLSID CLSID_CMSH264DecMFT; + +namespace wmf { + +// Reimplementation of CComPtr to reduce dependence on system +// shared libraries. +template <class T> +class CComPtr { + public: + CComPtr(CComPtr&& aOther) : mPtr(aOther.Detach()) {} + CComPtr& operator=(CComPtr&& other) { mPtr = other.Detach(); } + + CComPtr(const CComPtr& aOther) : mPtr(nullptr) { Set(aOther.Get()); } + CComPtr() : mPtr(nullptr) {} + MOZ_IMPLICIT CComPtr(T* const& aPtr) : mPtr(nullptr) { Set(aPtr); } + MOZ_IMPLICIT CComPtr(const std::nullptr_t& aNullPtr) : mPtr(aNullPtr) {} + T** operator&() { return &mPtr; } + T* operator->() { return mPtr; } + operator T*() { return mPtr; } + T* operator=(T* const& aPtr) { return Set(aPtr); } + T* operator=(const std::nullptr_t& aPtr) { return mPtr = aPtr; } + + T* Get() const { return mPtr; } + + T* Detach() { + T* tmp = mPtr; + mPtr = nullptr; + return tmp; + } + + ~CComPtr() { + if (mPtr) { + mPtr->Release(); + } + mPtr = nullptr; + } + + private: + T* Set(T* aPtr) { + if (mPtr == aPtr) { + return aPtr; + } + if (mPtr) { + mPtr->Release(); + } + mPtr = aPtr; + if (mPtr) { + mPtr->AddRef(); + } + return mPtr; + } + + T* mPtr; +}; + +class IntRect { + public: + IntRect(int32_t _x, int32_t _y, int32_t _w, int32_t _h) + : x(_x), y(_y), width(_w), height(_h) {} + IntRect() : x(0), y(0), width(0), height(0) {} + int32_t x; + int32_t y; + int32_t width; + int32_t height; +}; + +typedef int64_t Microseconds; + +#ifdef ENSURE +# undef ENSURE +#endif + +#define ENSURE(condition, ret) \ + { \ + if (!(condition)) { \ + LOG("##condition## FAILED %S:%d\n", __FILE__, __LINE__); \ + return ret; \ + } \ + } + +inline bool STATUS_SUCCEEDED(cdm::Status status) { + return status == cdm::Status::kSuccess; +} +inline bool STATUS_FAILED(cdm::Status status) { + return !STATUS_SUCCEEDED(status); +} + +#define MFPLAT_FUNC(_func, _dllname) extern decltype(::_func)* _func; +#include "WMFSymbols.h" +#undef MFPLAT_FUNC + +bool EnsureLibs(); + +HRESULT +GetPictureRegion(IMFMediaType* aMediaType, IntRect& aOutPictureRegion); + +HRESULT +GetDefaultStride(IMFMediaType* aType, uint32_t* aOutStride); + +// Converts from microseconds to hundreds of nanoseconds. +// We use microseconds for our timestamps, whereas WMF uses +// hundreds of nanoseconds. +inline int64_t UsecsToHNs(int64_t aUsecs) { return aUsecs * 10; } + +// Converts from hundreds of nanoseconds to microseconds. +// We use microseconds for our timestamps, whereas WMF uses +// hundreds of nanoseconds. +inline int64_t HNsToUsecs(int64_t hNanoSecs) { return hNanoSecs / 10; } + +inline std::string narrow(std::wstring& wide) { + std::string ns(wide.begin(), wide.end()); + return ns; +} + +inline std::wstring widen(std::string& narrow) { + std::wstring ws(narrow.begin(), narrow.end()); + return ws; +} + +#define ARRAY_LENGTH(array_) (sizeof(array_) / sizeof(array_[0])) + +template <class Type> +class AutoPtr { + public: + AutoPtr() : mPtr(nullptr) {} + + AutoPtr(AutoPtr<Type>& aPtr) : mPtr(aPtr.Forget()) {} + + explicit AutoPtr(Type* aPtr) : mPtr(aPtr) {} + + ~AutoPtr() { + if (mPtr) { + delete mPtr; + } + } + + Type* Forget() { + Type* rv = mPtr; + mPtr = nullptr; + return rv; + } + + AutoPtr<Type>& operator=(Type* aOther) { + Assign(aOther); + return *this; + } + + AutoPtr<Type>& operator=(AutoPtr<Type>& aOther) { + Assign(aOther.Forget()); + return *this; + } + + Type* Get() const { return mPtr; } + + Type* operator->() const { + assert(mPtr); + return Get(); + } + + operator Type*() const { return Get(); } + + Type** Receive() { return &mPtr; } + + private: + void Assign(Type* aPtr) { + if (mPtr) { + delete mPtr; + } + mPtr = aPtr; + } + + Type* mPtr; +}; + +// Video frame microseconds are (currently) in 90kHz units, as used by RTP. +// Use this to convert to microseconds... +inline Microseconds RTPTimeToMicroseconds(int64_t rtptime) { + return (rtptime * 1000000) / 90000; +} + +inline uint32_t MicrosecondsToRTPTime(Microseconds us) { + return uint32_t(0xffffffff & (us * 90000) / 1000000); +} + +void dump(const uint8_t* data, uint32_t len, const char* filename); + +HRESULT +CreateMFT(const CLSID& clsid, const char* aDllName, + CComPtr<IMFTransform>& aOutMFT); + +// Returns the name of the DLL that is needed to decode H.264 on +// the given windows version we're running on. +inline const char* WMFDecoderDllName() { return "msmpeg2vdec.dll"; } + +// Returns the maximum number of threads we want WMF to use for decoding +// given the number of logical processors available. +int32_t GetNumThreads(int32_t aCoreCount); + +} // namespace wmf + +#endif // __WMFUtils_h__ diff --git a/media/gmp-clearkey/0.1/gmp-clearkey.cpp b/media/gmp-clearkey/0.1/gmp-clearkey.cpp new file mode 100644 index 0000000000..70c27602f7 --- /dev/null +++ b/media/gmp-clearkey/0.1/gmp-clearkey.cpp @@ -0,0 +1,173 @@ +/* + * 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> +// This include is required in order for content_decryption_module to work +// on Unix systems. +#include <stddef.h> +#include <stdio.h> +#include <string.h> + +#include <string> +#include <vector> + +#include "content_decryption_module.h" +#include "content_decryption_module_ext.h" +#include "nss.h" + +#include "ClearKeyCDM.h" +#include "ClearKeySessionManager.h" +#include "mozilla/dom/KeySystemNames.h" + +#ifndef XP_WIN +# include <sys/types.h> +# include <sys/stat.h> +# include <unistd.h> +#endif + +#ifdef ENABLE_WMF +# include "WMFUtils.h" +#endif // ENABLE_WMF + +extern "C" { + +CDM_API +void INITIALIZE_CDM_MODULE() {} + +static bool sCanReadHostVerificationFiles = false; + +CDM_API +void* CreateCdmInstance(int cdm_interface_version, const char* key_system, + uint32_t key_system_size, + GetCdmHostFunc get_cdm_host_func, void* user_data) { + CK_LOGE("ClearKey CreateCDMInstance"); + + if (cdm_interface_version != cdm::ContentDecryptionModule_10::kVersion) { + CK_LOGE( + "ClearKey CreateCDMInstance failed due to requesting unsupported " + "version %d.", + cdm_interface_version); + return nullptr; + } +#ifdef ENABLE_WMF + if (!wmf::EnsureLibs()) { + CK_LOGE("Required libraries were not found"); + return nullptr; + } +#endif + + if (NSS_NoDB_Init(nullptr) == SECFailure) { + CK_LOGE("Unable to initialize NSS"); + return nullptr; + } + +#ifdef MOZILLA_OFFICIAL + // Test that we're able to read the host files. + if (!sCanReadHostVerificationFiles) { + return nullptr; + } +#endif + + cdm::Host_10* host = static_cast<cdm::Host_10*>( + get_cdm_host_func(cdm_interface_version, user_data)); + ClearKeyCDM* clearKey = new ClearKeyCDM(host); + + CK_LOGE("Created ClearKeyCDM instance!"); + + if (strncmp(key_system, mozilla::kClearKeyWithProtectionQueryKeySystemName, + key_system_size) == 0) { + CK_LOGE("Enabling protection query on ClearKeyCDM instance!"); + clearKey->EnableProtectionQuery(); + } + + return clearKey; +} + +const size_t TEST_READ_SIZE = 16 * 1024; + +bool CanReadSome(cdm::PlatformFile aFile) { + std::vector<uint8_t> data; + data.resize(TEST_READ_SIZE); +#ifdef XP_WIN + DWORD bytesRead = 0; + return ReadFile(aFile, &data.front(), TEST_READ_SIZE, &bytesRead, nullptr) && + bytesRead > 0; +#else + return read(aFile, &data.front(), TEST_READ_SIZE) > 0; +#endif +} + +void ClosePlatformFile(cdm::PlatformFile aFile) { +#ifdef XP_WIN + CloseHandle(aFile); +#else + close(aFile); +#endif +} + +static uint32_t NumExpectedHostFiles(const cdm::HostFile* aHostFiles, + uint32_t aNumFiles) { +#if !defined(XP_WIN) + // We expect 4 binaries: clearkey, libxul, plugin-container, and Firefox. + return 4; +#else + // Windows running x64 or x86 natively should also have 4 as above. + // For Windows on ARM64, we run an x86 plugin-contianer process under + // emulation, and so we expect one additional binary; the x86 + // xul.dll used by plugin-container.exe. + bool i686underAArch64 = false; + // Assume that we're running under x86 emulation on an aarch64 host if + // one of the paths ends with the x86 plugin-container path we'd expect. + const std::wstring plugincontainer = L"i686\\plugin-container.exe"; + for (uint32_t i = 0; i < aNumFiles; i++) { + const cdm::HostFile& hostFile = aHostFiles[i]; + if (hostFile.file != cdm::kInvalidPlatformFile) { + std::wstring path = hostFile.file_path; + auto offset = path.find(plugincontainer); + if (offset != std::string::npos && + offset == path.size() - plugincontainer.size()) { + i686underAArch64 = true; + break; + } + } + } + return i686underAArch64 ? 5 : 4; +#endif +} + +CDM_API +bool VerifyCdmHost_0(const cdm::HostFile* aHostFiles, uint32_t aNumFiles) { + // Check that we've received the expected number of host files. + bool rv = (aNumFiles == NumExpectedHostFiles(aHostFiles, aNumFiles)); + // Verify that each binary is readable inside the sandbox, + // and close the handle. + for (uint32_t i = 0; i < aNumFiles; i++) { + const cdm::HostFile& hostFile = aHostFiles[i]; + if (hostFile.file != cdm::kInvalidPlatformFile) { + if (!CanReadSome(hostFile.file)) { + rv = false; + } + ClosePlatformFile(hostFile.file); + } + if (hostFile.sig_file != cdm::kInvalidPlatformFile) { + ClosePlatformFile(hostFile.sig_file); + } + } + sCanReadHostVerificationFiles = rv; + return rv; +} + +} // extern "C". diff --git a/media/gmp-clearkey/0.1/gtest/TestClearKeyUtils.cpp b/media/gmp-clearkey/0.1/gtest/TestClearKeyUtils.cpp new file mode 100644 index 0000000000..bff6d0f356 --- /dev/null +++ b/media/gmp-clearkey/0.1/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/media/gmp-clearkey/0.1/gtest/moz.build b/media/gmp-clearkey/0.1/gtest/moz.build new file mode 100644 index 0000000000..5771c4724c --- /dev/null +++ b/media/gmp-clearkey/0.1/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/media/gmp-clearkey/0.1/manifest.json.in b/media/gmp-clearkey/0.1/manifest.json.in new file mode 100644 index 0000000000..dd36af8556 --- /dev/null +++ b/media/gmp-clearkey/0.1/manifest.json.in @@ -0,0 +1,13 @@ +{
+ "name": "clearkey",
+ "description": "ClearKey Gecko Media Plugin",
+ "version": "1",
+ "x-cdm-module-versions": "4",
+ "x-cdm-interface-versions": "10",
+ "x-cdm-host-versions": "10",
+#ifdef ENABLE_WMF
+ "x-cdm-codecs": "avc1"
+#else
+ "x-cdm-codecs": ""
+#endif
+}
\ No newline at end of file diff --git a/media/gmp-clearkey/0.1/moz.build b/media/gmp-clearkey/0.1/moz.build new file mode 100644 index 0000000000..4db6281a90 --- /dev/null +++ b/media/gmp-clearkey/0.1/moz.build @@ -0,0 +1,77 @@ +# -*- 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Audio/Video: GMP") + +GeckoSharedLibrary("clearkey", linkage=None) + +FINAL_TARGET = "dist/bin/gmp-clearkey/0.1" + +FINAL_TARGET_PP_FILES += ["manifest.json.in"] + +UNIFIED_SOURCES += [ + "ClearKeyBase64.cpp", + "ClearKeyCDM.cpp", + "ClearKeyDecryptionManager.cpp", + "ClearKeyPersistence.cpp", + "ClearKeySession.cpp", + "ClearKeySessionManager.cpp", + "ClearKeyStorage.cpp", + "ClearKeyUtils.cpp", + "gmp-clearkey.cpp", +] + +if CONFIG["OS_ARCH"] == "WINNT": + UNIFIED_SOURCES += [ + "VideoDecoder.cpp", + "WMFH264Decoder.cpp", + ] + + SOURCES += [ + "WMFUtils.cpp", + ] + + OS_LIBS += [ + "mfuuid", + "uuid", + ] + + DEFINES["ENABLE_WMF"] = True + + +DEFINES["CDM_IMPLEMENTATION"] = True + +TEST_DIRS += [ + "gtest", +] + +DisableStlWrapping() +DEFINES["MOZ_NO_MOZALLOC"] = True + +USE_LIBS += [ + "nss", + "psshparser", +] + +# Suppress warnings in third-party code. +CFLAGS += [ + "-Wno-pointer-to-int-cast", + "-Wno-sign-compare", +] + +if CONFIG["CC_TYPE"] in ("clang", "gcc"): + CFLAGS += [ + "-include", + "stdio.h", # for sprintf() prototype + "-include", + "unistd.h", # for getpid() prototype + ] +elif CONFIG["CC_TYPE"] == "clang-cl": + CFLAGS += [ + "-FI", + "stdio.h", # for sprintf() prototype + ] |