From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- dom/media/eme/CDMCaps.cpp | 112 ++ dom/media/eme/CDMCaps.h | 82 ++ dom/media/eme/CDMProxy.h | 325 ++++++ dom/media/eme/DecryptorProxyCallback.h | 54 + dom/media/eme/DetailedPromise.cpp | 87 ++ dom/media/eme/DetailedPromise.h | 104 ++ dom/media/eme/EMEUtils.cpp | 248 +++++ dom/media/eme/EMEUtils.h | 109 ++ dom/media/eme/KeySystemConfig.cpp | 360 ++++++ dom/media/eme/KeySystemConfig.h | 189 ++++ dom/media/eme/KeySystemNames.h | 48 + dom/media/eme/MediaEncryptedEvent.cpp | 106 ++ dom/media/eme/MediaEncryptedEvent.h | 60 + dom/media/eme/MediaKeyError.cpp | 27 + dom/media/eme/MediaKeyError.h | 33 + dom/media/eme/MediaKeyMessageEvent.cpp | 101 ++ dom/media/eme/MediaKeyMessageEvent.h | 64 ++ dom/media/eme/MediaKeySession.cpp | 622 +++++++++++ dom/media/eme/MediaKeySession.h | 142 +++ dom/media/eme/MediaKeyStatusMap.cpp | 99 ++ dom/media/eme/MediaKeyStatusMap.h | 94 ++ dom/media/eme/MediaKeySystemAccess.cpp | 1161 ++++++++++++++++++++ dom/media/eme/MediaKeySystemAccess.h | 84 ++ dom/media/eme/MediaKeySystemAccessManager.cpp | 702 ++++++++++++ dom/media/eme/MediaKeySystemAccessManager.h | 237 ++++ .../eme/MediaKeySystemAccessPermissionRequest.cpp | 91 ++ .../eme/MediaKeySystemAccessPermissionRequest.h | 74 ++ dom/media/eme/MediaKeys.cpp | 847 ++++++++++++++ dom/media/eme/MediaKeys.h | 236 ++++ dom/media/eme/clearkey/ArrayUtils.h | 22 + dom/media/eme/clearkey/BigEndian.h | 59 + dom/media/eme/clearkey/ClearKeyBase64.cpp | 90 ++ dom/media/eme/clearkey/ClearKeyBase64.h | 29 + .../eme/clearkey/ClearKeyDecryptionManager.cpp | 289 +++++ dom/media/eme/clearkey/ClearKeyDecryptionManager.h | 111 ++ dom/media/eme/clearkey/ClearKeyPersistence.cpp | 153 +++ dom/media/eme/clearkey/ClearKeyPersistence.h | 64 ++ dom/media/eme/clearkey/ClearKeySession.cpp | 72 ++ dom/media/eme/clearkey/ClearKeySession.h | 56 + dom/media/eme/clearkey/ClearKeySessionManager.cpp | 713 ++++++++++++ dom/media/eme/clearkey/ClearKeySessionManager.h | 138 +++ dom/media/eme/clearkey/ClearKeyStorage.cpp | 194 ++++ dom/media/eme/clearkey/ClearKeyStorage.h | 42 + dom/media/eme/clearkey/ClearKeyUtils.cpp | 661 +++++++++++ dom/media/eme/clearkey/ClearKeyUtils.h | 107 ++ dom/media/eme/clearkey/RefCounted.h | 85 ++ dom/media/eme/clearkey/gtest/TestClearKeyUtils.cpp | 81 ++ dom/media/eme/clearkey/gtest/moz.build | 15 + dom/media/eme/clearkey/moz.build | 37 + .../eme/mediadrm/MediaDrmCDMCallbackProxy.cpp | 115 ++ dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.h | 63 ++ dom/media/eme/mediadrm/MediaDrmCDMProxy.cpp | 453 ++++++++ dom/media/eme/mediadrm/MediaDrmCDMProxy.h | 186 ++++ dom/media/eme/mediadrm/MediaDrmProxySupport.cpp | 272 +++++ dom/media/eme/mediadrm/MediaDrmProxySupport.h | 68 ++ dom/media/eme/mediadrm/moz.build | 19 + dom/media/eme/mediafoundation/WMFCDMImpl.cpp | 151 +++ dom/media/eme/mediafoundation/WMFCDMImpl.h | 124 +++ dom/media/eme/mediafoundation/WMFCDMProxy.cpp | 414 +++++++ dom/media/eme/mediafoundation/WMFCDMProxy.h | 149 +++ .../eme/mediafoundation/WMFCDMProxyCallback.cpp | 72 ++ .../eme/mediafoundation/WMFCDMProxyCallback.h | 38 + dom/media/eme/mediafoundation/moz.build | 21 + dom/media/eme/moz.build | 56 + 64 files changed, 11717 insertions(+) create mode 100644 dom/media/eme/CDMCaps.cpp create mode 100644 dom/media/eme/CDMCaps.h create mode 100644 dom/media/eme/CDMProxy.h create mode 100644 dom/media/eme/DecryptorProxyCallback.h create mode 100644 dom/media/eme/DetailedPromise.cpp create mode 100644 dom/media/eme/DetailedPromise.h create mode 100644 dom/media/eme/EMEUtils.cpp create mode 100644 dom/media/eme/EMEUtils.h create mode 100644 dom/media/eme/KeySystemConfig.cpp create mode 100644 dom/media/eme/KeySystemConfig.h create mode 100644 dom/media/eme/KeySystemNames.h create mode 100644 dom/media/eme/MediaEncryptedEvent.cpp create mode 100644 dom/media/eme/MediaEncryptedEvent.h create mode 100644 dom/media/eme/MediaKeyError.cpp create mode 100644 dom/media/eme/MediaKeyError.h create mode 100644 dom/media/eme/MediaKeyMessageEvent.cpp create mode 100644 dom/media/eme/MediaKeyMessageEvent.h create mode 100644 dom/media/eme/MediaKeySession.cpp create mode 100644 dom/media/eme/MediaKeySession.h create mode 100644 dom/media/eme/MediaKeyStatusMap.cpp create mode 100644 dom/media/eme/MediaKeyStatusMap.h create mode 100644 dom/media/eme/MediaKeySystemAccess.cpp create mode 100644 dom/media/eme/MediaKeySystemAccess.h create mode 100644 dom/media/eme/MediaKeySystemAccessManager.cpp create mode 100644 dom/media/eme/MediaKeySystemAccessManager.h create mode 100644 dom/media/eme/MediaKeySystemAccessPermissionRequest.cpp create mode 100644 dom/media/eme/MediaKeySystemAccessPermissionRequest.h create mode 100644 dom/media/eme/MediaKeys.cpp create mode 100644 dom/media/eme/MediaKeys.h create mode 100644 dom/media/eme/clearkey/ArrayUtils.h create mode 100644 dom/media/eme/clearkey/BigEndian.h create mode 100644 dom/media/eme/clearkey/ClearKeyBase64.cpp create mode 100644 dom/media/eme/clearkey/ClearKeyBase64.h create mode 100644 dom/media/eme/clearkey/ClearKeyDecryptionManager.cpp create mode 100644 dom/media/eme/clearkey/ClearKeyDecryptionManager.h create mode 100644 dom/media/eme/clearkey/ClearKeyPersistence.cpp create mode 100644 dom/media/eme/clearkey/ClearKeyPersistence.h create mode 100644 dom/media/eme/clearkey/ClearKeySession.cpp create mode 100644 dom/media/eme/clearkey/ClearKeySession.h create mode 100644 dom/media/eme/clearkey/ClearKeySessionManager.cpp create mode 100644 dom/media/eme/clearkey/ClearKeySessionManager.h create mode 100644 dom/media/eme/clearkey/ClearKeyStorage.cpp create mode 100644 dom/media/eme/clearkey/ClearKeyStorage.h create mode 100644 dom/media/eme/clearkey/ClearKeyUtils.cpp create mode 100644 dom/media/eme/clearkey/ClearKeyUtils.h create mode 100644 dom/media/eme/clearkey/RefCounted.h create mode 100644 dom/media/eme/clearkey/gtest/TestClearKeyUtils.cpp create mode 100644 dom/media/eme/clearkey/gtest/moz.build create mode 100644 dom/media/eme/clearkey/moz.build create mode 100644 dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.cpp create mode 100644 dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.h create mode 100644 dom/media/eme/mediadrm/MediaDrmCDMProxy.cpp create mode 100644 dom/media/eme/mediadrm/MediaDrmCDMProxy.h create mode 100644 dom/media/eme/mediadrm/MediaDrmProxySupport.cpp create mode 100644 dom/media/eme/mediadrm/MediaDrmProxySupport.h create mode 100644 dom/media/eme/mediadrm/moz.build create mode 100644 dom/media/eme/mediafoundation/WMFCDMImpl.cpp create mode 100644 dom/media/eme/mediafoundation/WMFCDMImpl.h create mode 100644 dom/media/eme/mediafoundation/WMFCDMProxy.cpp create mode 100644 dom/media/eme/mediafoundation/WMFCDMProxy.h create mode 100644 dom/media/eme/mediafoundation/WMFCDMProxyCallback.cpp create mode 100644 dom/media/eme/mediafoundation/WMFCDMProxyCallback.h create mode 100644 dom/media/eme/mediafoundation/moz.build create mode 100644 dom/media/eme/moz.build (limited to 'dom/media/eme') diff --git a/dom/media/eme/CDMCaps.cpp b/dom/media/eme/CDMCaps.cpp new file mode 100644 index 0000000000..2752ada476 --- /dev/null +++ b/dom/media/eme/CDMCaps.cpp @@ -0,0 +1,112 @@ +/* -*- 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 "mozilla/CDMCaps.h" +#include "mozilla/EMEUtils.h" +#include "nsThreadUtils.h" +#include "SamplesWaitingForKey.h" + +namespace mozilla { + +CDMCaps::CDMCaps() = default; + +CDMCaps::~CDMCaps() = default; + +// Keys with MediaKeyStatus::Usable, MediaKeyStatus::Output_downscaled, +// or MediaKeyStatus::Output_restricted status can be used by the CDM +// to decrypt or decrypt-and-decode samples. +static bool IsUsableStatus(dom::MediaKeyStatus aStatus) { + return aStatus == dom::MediaKeyStatus::Usable || + aStatus == dom::MediaKeyStatus::Output_restricted || + aStatus == dom::MediaKeyStatus::Output_downscaled; +} + +bool CDMCaps::IsKeyUsable(const CencKeyId& aKeyId) { + for (const KeyStatus& keyStatus : mKeyStatuses) { + if (keyStatus.mId == aKeyId) { + return IsUsableStatus(keyStatus.mStatus); + } + } + return false; +} + +bool CDMCaps::SetKeyStatus(const CencKeyId& aKeyId, const nsString& aSessionId, + const dom::Optional& aStatus) { + if (!aStatus.WasPassed()) { + // Called from ForgetKeyStatus. + // Return true if the element is found to notify key changes. + return mKeyStatuses.RemoveElement( + KeyStatus(aKeyId, aSessionId, dom::MediaKeyStatus::Internal_error)); + } + + KeyStatus key(aKeyId, aSessionId, aStatus.Value()); + auto index = mKeyStatuses.IndexOf(key); + if (index != mKeyStatuses.NoIndex) { + if (mKeyStatuses[index].mStatus == aStatus.Value()) { + // No change. + return false; + } + auto oldStatus = mKeyStatuses[index].mStatus; + mKeyStatuses[index].mStatus = aStatus.Value(); + // The old key status was one for which we can decrypt media. We don't + // need to do the "notify usable" step below, as it should be impossible + // for us to have anything waiting on this key to become usable, since it + // was already usable. + if (IsUsableStatus(oldStatus)) { + return true; + } + } else { + mKeyStatuses.AppendElement(key); + } + + // Only call NotifyUsable() for a key when we are going from non-usable + // to usable state. + if (!IsUsableStatus(aStatus.Value())) { + return true; + } + + auto& waiters = mWaitForKeys; + size_t i = 0; + while (i < waiters.Length()) { + auto& w = waiters[i]; + if (w.mKeyId == aKeyId) { + w.mListener->NotifyUsable(aKeyId); + waiters.RemoveElementAt(i); + } else { + i++; + } + } + return true; +} + +void CDMCaps::NotifyWhenKeyIdUsable(const CencKeyId& aKey, + SamplesWaitingForKey* aListener) { + MOZ_ASSERT(!IsKeyUsable(aKey)); + MOZ_ASSERT(aListener); + mWaitForKeys.AppendElement(WaitForKeys(aKey, aListener)); +} + +void CDMCaps::GetKeyStatusesForSession(const nsAString& aSessionId, + nsTArray& aOutKeyStatuses) { + for (const KeyStatus& keyStatus : mKeyStatuses) { + if (keyStatus.mSessionId.Equals(aSessionId)) { + aOutKeyStatuses.AppendElement(keyStatus); + } + } +} + +bool CDMCaps::RemoveKeysForSession(const nsString& aSessionId) { + bool changed = false; + nsTArray statuses; + GetKeyStatusesForSession(aSessionId, statuses); + for (const KeyStatus& status : statuses) { + changed |= SetKeyStatus(status.mId, aSessionId, + dom::Optional()); + } + return changed; +} + +} // namespace mozilla diff --git a/dom/media/eme/CDMCaps.h b/dom/media/eme/CDMCaps.h new file mode 100644 index 0000000000..b16c5153b1 --- /dev/null +++ b/dom/media/eme/CDMCaps.h @@ -0,0 +1,82 @@ +/* -*- 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 CDMCaps_h_ +#define CDMCaps_h_ + +#include "nsTArray.h" +#include "nsString.h" +#include "SamplesWaitingForKey.h" + +#include "mozilla/Monitor.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/MediaKeyStatusMapBinding.h" // For MediaKeyStatus +#include "mozilla/dom/BindingDeclarations.h" // For Optional + +namespace mozilla { + +// CDM capabilities; what keys a CDMProxy can use. +// Must be locked to access state. +class CDMCaps { + public: + CDMCaps(); + ~CDMCaps(); + + struct KeyStatus { + KeyStatus(const CencKeyId& aId, const nsString& aSessionId, + dom::MediaKeyStatus aStatus) + : mId(aId.Clone()), mSessionId(aSessionId), mStatus(aStatus) {} + KeyStatus(const KeyStatus& aOther) + : mId(aOther.mId.Clone()), + mSessionId(aOther.mSessionId), + mStatus(aOther.mStatus) {} + bool operator==(const KeyStatus& aOther) const { + return mId == aOther.mId && mSessionId == aOther.mSessionId; + }; + + CencKeyId mId; + nsString mSessionId; + dom::MediaKeyStatus mStatus; + }; + + bool IsKeyUsable(const CencKeyId& aKeyId); + + // Returns true if key status changed, + // i.e. the key status changed from usable to expired. + bool SetKeyStatus(const CencKeyId& aKeyId, const nsString& aSessionId, + const dom::Optional& aStatus); + + void GetKeyStatusesForSession(const nsAString& aSessionId, + nsTArray& aOutKeyStatuses); + + // Ensures all keys for a session are marked as 'unknown', i.e. removed. + // Returns true if a key status was changed. + bool RemoveKeysForSession(const nsString& aSessionId); + + // Notifies the SamplesWaitingForKey when key become usable. + void NotifyWhenKeyIdUsable(const CencKeyId& aKey, + SamplesWaitingForKey* aSamplesWaiting); + + private: + struct WaitForKeys { + WaitForKeys(const CencKeyId& aKeyId, SamplesWaitingForKey* aListener) + : mKeyId(aKeyId.Clone()), mListener(aListener) {} + CencKeyId mKeyId; + RefPtr mListener; + }; + + nsTArray mKeyStatuses; + + nsTArray mWaitForKeys; + + // It is not safe to copy this object. + CDMCaps(const CDMCaps&) = delete; + CDMCaps& operator=(const CDMCaps&) = delete; +}; + +} // namespace mozilla + +#endif diff --git a/dom/media/eme/CDMProxy.h b/dom/media/eme/CDMProxy.h new file mode 100644 index 0000000000..27ebef27de --- /dev/null +++ b/dom/media/eme/CDMProxy.h @@ -0,0 +1,325 @@ +/* -*- 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 CDMProxy_h_ +#define CDMProxy_h_ + +#include "mozilla/CDMCaps.h" +#include "mozilla/DataMutex.h" +#include "mozilla/MozPromise.h" + +#include "mozilla/dom/MediaKeyMessageEvent.h" +#include "mozilla/dom/MediaKeys.h" + +#include "nsIThread.h" + +namespace mozilla { +class ErrorResult; +class MediaRawData; +class ChromiumCDMProxy; +#ifdef MOZ_WMF_CDM +class WMFCDMProxy; +#endif + +namespace eme { +enum DecryptStatus { + Ok = 0, + GenericErr = 1, + NoKeyErr = 2, + AbortedErr = 3, +}; +} + +using eme::DecryptStatus; + +struct DecryptResult { + DecryptResult(DecryptStatus aStatus, MediaRawData* aSample) + : mStatus(aStatus), mSample(aSample) {} + DecryptStatus mStatus; + RefPtr mSample; +}; + +typedef MozPromise + DecryptPromise; + +class CDMKeyInfo { + public: + explicit CDMKeyInfo(const nsTArray& aKeyId) + : mKeyId(aKeyId.Clone()), mStatus() {} + + CDMKeyInfo(const nsTArray& aKeyId, + const dom::Optional& aStatus) + : mKeyId(aKeyId.Clone()), mStatus(aStatus.Value()) {} + + // The copy-ctor and copy-assignment operator for Optional are declared as + // delete, so override CDMKeyInfo copy-ctor for nsTArray operations. + CDMKeyInfo(const CDMKeyInfo& aKeyInfo) { + mKeyId = aKeyInfo.mKeyId.Clone(); + if (aKeyInfo.mStatus.WasPassed()) { + mStatus.Construct(aKeyInfo.mStatus.Value()); + } + } + + nsTArray mKeyId; + dom::Optional mStatus; +}; + +// Time is defined as the number of milliseconds since the +// Epoch (00:00:00 UTC, January 1, 1970). +typedef int64_t UnixTime; + +// Proxies calls CDM, and proxies calls back. +// Note: Promises are passed in via a PromiseId, so that the ID can be +// passed via IPC to the CDM, which can then signal when to reject or +// resolve the promise using its PromiseId. +class CDMProxy { + protected: + typedef dom::PromiseId PromiseId; + typedef dom::MediaKeySessionType MediaKeySessionType; + + public: + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + // Main thread only. + // Loads the CDM corresponding to mKeySystem. + // Calls MediaKeys::OnCDMCreated() when the CDM is created. + virtual void Init(PromiseId aPromiseId, const nsAString& aOrigin, + const nsAString& aTopLevelOrigin, + const nsAString& aName) = 0; + + // Main thread only. + // Uses the CDM to create a key session. + // Calls MediaKeys::OnSessionActivated() when session is created. + // Assumes ownership of (std::move()s) aInitData's contents. + virtual void CreateSession(uint32_t aCreateSessionToken, + MediaKeySessionType aSessionType, + PromiseId aPromiseId, + const nsAString& aInitDataType, + nsTArray& aInitData) = 0; + + // Main thread only. + // Uses the CDM to load a presistent session stored on disk. + // Calls MediaKeys::OnSessionActivated() when session is loaded. + virtual void LoadSession(PromiseId aPromiseId, + dom::MediaKeySessionType aSessionType, + const nsAString& aSessionId) = 0; + + // Main thread only. + // Sends a new certificate to the CDM. + // Calls MediaKeys->ResolvePromise(aPromiseId) after the CDM has + // processed the request. + // Assumes ownership of (std::move()s) aCert's contents. + virtual void SetServerCertificate(PromiseId aPromiseId, + nsTArray& aCert) = 0; + + // Main thread only. + // Sends an update to the CDM. + // Calls MediaKeys->ResolvePromise(aPromiseId) after the CDM has + // processed the request. + // Assumes ownership of (std::move()s) aResponse's contents. + virtual void UpdateSession(const nsAString& aSessionId, PromiseId aPromiseId, + nsTArray& aResponse) = 0; + + // Main thread only. + // Calls MediaKeys->ResolvePromise(aPromiseId) after the CDM has + // processed the request. + // If processing this operation results in the session actually closing, + // we also call MediaKeySession::OnClosed(), which in turn calls + // MediaKeys::OnSessionClosed(). + virtual void CloseSession(const nsAString& aSessionId, + PromiseId aPromiseId) = 0; + + // Main thread only. + // Removes all data for a persisent session. + // Calls MediaKeys->ResolvePromise(aPromiseId) after the CDM has + // processed the request. + virtual void RemoveSession(const nsAString& aSessionId, + PromiseId aPromiseId) = 0; + + // Main thread only. + // Called to signal a request for output protection information from the CDM. + // This should forward the call up the stack where the query should be + // performed and then responded to via `NotifyOutputProtectionStatus`. + virtual void QueryOutputProtectionStatus() = 0; + + // NotifyOutputProtectionStatus enums. Explicit values are specified to make + // it easy to match values in logs. + enum class OutputProtectionCheckStatus : uint8_t { + CheckFailed = 0, + CheckSuccessful = 1, + }; + + enum class OutputProtectionCaptureStatus : uint8_t { + CapturePossilbe = 0, + CaptureNotPossible = 1, + Unused = 2, + }; + // End NotifyOutputProtectionStatus enums + + // Main thread only. + // Notifies this proxy of the protection status for the media the CDM is + // associated with. This can be called in response to + // `QueryOutputProtectionStatus`, but can also be called without an + // associated query. In both cases the information will be forwarded to + // the CDM host machinery and used to handle requests from the CDM. + // @param aCheckStatus did the check succeed or not. + // @param aCaptureStatus if the check succeeded, this reflects if capture + // of media could take place. This doesn't mean capture is taking place. + // Callers should be conservative with this value such that it's okay to pass + // CapturePossilbe even if capture is not happening, but should never pass + // CaptureNotPossible if it could happen. If the check failed, this value is + // not used, and callers should pass Unused to indicate this. + virtual void NotifyOutputProtectionStatus( + OutputProtectionCheckStatus aCheckStatus, + OutputProtectionCaptureStatus aCaptureStatus) = 0; + + // Main thread only. + virtual void Shutdown() = 0; + + // Main thread only. + virtual void Terminated() = 0; + + // Threadsafe. + const nsCString& GetNodeId() const { return mNodeId; }; + + // Main thread only. + virtual void OnSetSessionId(uint32_t aCreateSessionToken, + const nsAString& aSessionId) = 0; + + // Main thread only. + virtual void OnResolveLoadSessionPromise(uint32_t aPromiseId, + bool aSuccess) = 0; + + // Main thread only. + virtual void OnSessionMessage(const nsAString& aSessionId, + dom::MediaKeyMessageType aMessageType, + const nsTArray& aMessage) = 0; + + // Main thread only. + virtual void OnExpirationChange(const nsAString& aSessionId, + UnixTime aExpiryTime) = 0; + + // Main thread only. + virtual void OnSessionClosed(const nsAString& aSessionId) = 0; + + // Main thread only. + virtual void OnSessionError(const nsAString& aSessionId, nsresult aException, + uint32_t aSystemCode, const nsAString& aMsg) = 0; + + // Main thread only. + virtual void OnRejectPromise(uint32_t aPromiseId, ErrorResult&& aException, + const nsCString& aMsg) = 0; + + virtual RefPtr Decrypt(MediaRawData* aSample) = 0; + + // Owner thread only. + virtual void OnDecrypted(uint32_t aId, DecryptStatus aResult, + const nsTArray& aDecryptedData) = 0; + + // Reject promise with the given ErrorResult. + // + // Can be called from any thread. + virtual void RejectPromise(PromiseId aId, ErrorResult&& aException, + const nsCString& aReason) = 0; + + // Resolves promise with "undefined". + // Can be called from any thread. + virtual void ResolvePromise(PromiseId aId) = 0; + + // Threadsafe. + const nsString& KeySystem() const { return mKeySystem; }; + + DataMutex& Capabilites() { return mCapabilites; }; + + // Main thread only. + virtual void OnKeyStatusesChange(const nsAString& aSessionId) = 0; + + // Main thread only. + // Calls MediaKeys->ResolvePromiseWithKeyStatus(aPromiseId, aKeyStatus) after + // the CDM has processed the request. + virtual void GetStatusForPolicy(PromiseId aPromiseId, + const dom::HDCPVersion& aMinHdcpVersion) = 0; + +#ifdef DEBUG + virtual bool IsOnOwnerThread() = 0; +#endif + + virtual ChromiumCDMProxy* AsChromiumCDMProxy() { return nullptr; } + +#ifdef MOZ_WMF_CDM + virtual WMFCDMProxy* AsWMFCDMProxy() { return nullptr; } +#endif + + virtual bool IsHardwareDecryptionSupported() const { return false; } + + protected: + // Main thread only. + CDMProxy(dom::MediaKeys* aKeys, const nsAString& aKeySystem, + bool aDistinctiveIdentifierRequired, bool aPersistentStateRequired) + : mKeys(aKeys), + mKeySystem(aKeySystem), + mCapabilites("CDMProxy::mCDMCaps"), + mDistinctiveIdentifierRequired(aDistinctiveIdentifierRequired), + mPersistentStateRequired(aPersistentStateRequired), + mMainThread(GetMainThreadSerialEventTarget()) { + MOZ_ASSERT(NS_IsMainThread()); + } + + virtual ~CDMProxy() {} + + // Helper to enforce that a raw pointer is only accessed on the main thread. + template + class MainThreadOnlyRawPtr { + public: + explicit MainThreadOnlyRawPtr(Type* aPtr) : mPtr(aPtr) { + MOZ_ASSERT(NS_IsMainThread()); + } + + bool IsNull() const { + MOZ_ASSERT(NS_IsMainThread()); + return !mPtr; + } + + void Clear() { + MOZ_ASSERT(NS_IsMainThread()); + mPtr = nullptr; + } + + Type* operator->() const MOZ_NO_ADDREF_RELEASE_ON_RETURN { + MOZ_ASSERT(NS_IsMainThread()); + return mPtr; + } + + private: + Type* mPtr; + }; + + // Our reference back to the MediaKeys object. + // WARNING: This is a non-owning reference that is cleared by MediaKeys + // destructor. only use on main thread, and always nullcheck before using! + MainThreadOnlyRawPtr mKeys; + + const nsString mKeySystem; + + // Onwer specified thread. e.g. Gecko Media Plugin thread. + // All interactions with the out-of-process EME plugin must come from this + // thread. + RefPtr mOwnerThread; + + nsCString mNodeId; + + DataMutex mCapabilites; + + const bool mDistinctiveIdentifierRequired; + const bool mPersistentStateRequired; + + // The main thread associated with the root document. + const nsCOMPtr mMainThread; +}; + +} // namespace mozilla + +#endif // CDMProxy_h_ diff --git a/dom/media/eme/DecryptorProxyCallback.h b/dom/media/eme/DecryptorProxyCallback.h new file mode 100644 index 0000000000..88acdd7967 --- /dev/null +++ b/dom/media/eme/DecryptorProxyCallback.h @@ -0,0 +1,54 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 DecryptorProxyCallback_h_ +#define DecryptorProxyCallback_h_ + +#include "mozilla/dom/MediaKeyStatusMapBinding.h" // For MediaKeyStatus +#include "mozilla/dom/MediaKeyMessageEventBinding.h" // For MediaKeyMessageType +#include "mozilla/CDMProxy.h" + +namespace mozilla { +class ErrorResult; +} + +class DecryptorProxyCallback { + public: + virtual ~DecryptorProxyCallback() {} + + virtual void SetSessionId(uint32_t aCreateSessionId, + const nsCString& aSessionId) = 0; + + virtual void ResolveLoadSessionPromise(uint32_t aPromiseId, + bool aSuccess) = 0; + + virtual void ResolvePromise(uint32_t aPromiseId) = 0; + + virtual void RejectPromise(uint32_t aPromiseId, + mozilla::ErrorResult&& aException, + const nsCString& aSessionId) = 0; + + virtual void SessionMessage(const nsCString& aSessionId, + mozilla::dom::MediaKeyMessageType aMessageType, + const nsTArray& aMessage) = 0; + + virtual void ExpirationChange(const nsCString& aSessionId, + mozilla::UnixTime aExpiryTime) = 0; + + virtual void SessionClosed(const nsCString& aSessionId) = 0; + + virtual void SessionError(const nsCString& aSessionId, nsresult aException, + uint32_t aSystemCode, + const nsCString& aMessage) = 0; + + virtual void Decrypted(uint32_t aId, mozilla::DecryptStatus aResult, + const nsTArray& aDecryptedData) = 0; + + virtual void BatchedKeyStatusChanged( + const nsCString& aSessionId, + const nsTArray& aKeyInfos) = 0; +}; + +#endif diff --git a/dom/media/eme/DetailedPromise.cpp b/dom/media/eme/DetailedPromise.cpp new file mode 100644 index 0000000000..0692a1dd07 --- /dev/null +++ b/dom/media/eme/DetailedPromise.cpp @@ -0,0 +1,87 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 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 "DetailedPromise.h" + +#include "VideoUtils.h" +#include "mozilla/dom/DOMException.h" +#include "nsPrintfCString.h" + +namespace mozilla::dom { + +DetailedPromise::DetailedPromise(nsIGlobalObject* aGlobal, + const nsACString& aName) + : Promise(aGlobal), + mName(aName), + mResponded(false), + mStartTime(TimeStamp::Now()) {} + +DetailedPromise::DetailedPromise(nsIGlobalObject* aGlobal, + const nsACString& aName, + Telemetry::HistogramID aSuccessLatencyProbe, + Telemetry::HistogramID aFailureLatencyProbe) + : DetailedPromise(aGlobal, aName) { + mSuccessLatencyProbe.Construct(aSuccessLatencyProbe); + mFailureLatencyProbe.Construct(aFailureLatencyProbe); +} + +DetailedPromise::~DetailedPromise() { + // It would be nice to assert that mResponded is identical to + // GetPromiseState() == PromiseState::Rejected. But by now we've been + // unlinked, so don't have a reference to our actual JS Promise object + // anymore. + MaybeReportTelemetry(kFailed); +} + +void DetailedPromise::LogRejectionReason(uint32_t aErrorCode, + const nsACString& aReason) { + nsPrintfCString msg("%s promise rejected 0x%" PRIx32 " '%s'", mName.get(), + aErrorCode, PromiseFlatCString(aReason).get()); + EME_LOG("%s", msg.get()); + + MaybeReportTelemetry(kFailed); + + LogToBrowserConsole(NS_ConvertUTF8toUTF16(msg)); +} + +void DetailedPromise::MaybeReject(nsresult aArg, const nsACString& aReason) { + LogRejectionReason(static_cast(aArg), aReason); + + Promise::MaybeRejectWithDOMException(aArg, aReason); +} + +void DetailedPromise::MaybeReject(ErrorResult&& aArg, + const nsACString& aReason) { + LogRejectionReason(aArg.ErrorCodeAsInt(), aReason); + Promise::MaybeReject(std::move(aArg)); +} + +/* static */ +already_AddRefed DetailedPromise::Create( + nsIGlobalObject* aGlobal, ErrorResult& aRv, const nsACString& aName) { + RefPtr promise = new DetailedPromise(aGlobal, aName); + promise->CreateWrapper(aRv); + return aRv.Failed() ? nullptr : promise.forget(); +} + +void DetailedPromise::MaybeReportTelemetry(eStatus aStatus) { + if (mResponded) { + return; + } + mResponded = true; + if (!mSuccessLatencyProbe.WasPassed() || !mFailureLatencyProbe.WasPassed()) { + return; + } + uint32_t latency = (TimeStamp::Now() - mStartTime).ToMilliseconds(); + EME_LOG("%s %s latency %ums reported via telemetry", mName.get(), + ((aStatus == kSucceeded) ? "succcess" : "failure"), latency); + Telemetry::HistogramID tid = (aStatus == kSucceeded) + ? mSuccessLatencyProbe.Value() + : mFailureLatencyProbe.Value(); + Telemetry::Accumulate(tid, latency); +} + +} // namespace mozilla::dom diff --git a/dom/media/eme/DetailedPromise.h b/dom/media/eme/DetailedPromise.h new file mode 100644 index 0000000000..02c774755f --- /dev/null +++ b/dom/media/eme/DetailedPromise.h @@ -0,0 +1,104 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 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 __DetailedPromise_h__ +#define __DetailedPromise_h__ + +#include "mozilla/dom/Promise.h" +#include "mozilla/Telemetry.h" +#include "EMEUtils.h" + +namespace mozilla::dom { + +/* + * This is pretty horrible; bug 1160445. + * Extend Promise to add custom DOMException messages on rejection. + * Get rid of this once we've ironed out EME errors in the wild. + */ +class DetailedPromise : public Promise { + public: + static already_AddRefed Create(nsIGlobalObject* aGlobal, + ErrorResult& aRv, + const nsACString& aName); + + template + void MaybeResolve(T&& aArg) { + EME_LOG("%s promise resolved", mName.get()); + MaybeReportTelemetry(eStatus::kSucceeded); + Promise::MaybeResolve(std::forward(aArg)); + } + + void MaybeReject(nsresult aArg) = delete; + void MaybeReject(nsresult aArg, const nsACString& aReason); + + void MaybeReject(ErrorResult&& aArg) = delete; + void MaybeReject(ErrorResult&& aArg, const nsACString& aReason); + + // Facilities for rejecting with various spec-defined exception values. +#define DOMEXCEPTION(name, err) \ + inline void MaybeRejectWith##name(const nsACString& aMessage) { \ + LogRejectionReason(static_cast(err), aMessage); \ + Promise::MaybeRejectWith##name(aMessage); \ + } \ + template \ + void MaybeRejectWith##name(const char(&aMessage)[N]) { \ + MaybeRejectWith##name(nsLiteralCString(aMessage)); \ + } + +#include "mozilla/dom/DOMExceptionNames.h" + +#undef DOMEXCEPTION + + template + void MaybeRejectWithTypeError(Ts&&... aMessageArgs) = delete; + + inline void MaybeRejectWithTypeError(const nsACString& aMessage) { + ErrorResult res; + res.ThrowTypeError(aMessage); + MaybeReject(std::move(res), aMessage); + } + + template + void MaybeRejectWithTypeError(const char (&aMessage)[N]) { + MaybeRejectWithTypeError(nsLiteralCString(aMessage)); + } + + template + void MaybeRejectWithRangeError(Ts&&... aMessageArgs) = delete; + + inline void MaybeRejectWithRangeError(const nsACString& aMessage) { + ErrorResult res; + res.ThrowRangeError(aMessage); + MaybeReject(std::move(res), aMessage); + } + + template + void MaybeRejectWithRangeError(const char (&aMessage)[N]) { + MaybeRejectWithRangeError(nsLiteralCString(aMessage)); + } + + private: + explicit DetailedPromise(nsIGlobalObject* aGlobal, const nsACString& aName); + + explicit DetailedPromise(nsIGlobalObject* aGlobal, const nsACString& aName, + Telemetry::HistogramID aSuccessLatencyProbe, + Telemetry::HistogramID aFailureLatencyProbe); + virtual ~DetailedPromise(); + + enum eStatus { kSucceeded, kFailed }; + void MaybeReportTelemetry(eStatus aStatus); + void LogRejectionReason(uint32_t aErrorCode, const nsACString& aReason); + + nsCString mName; + bool mResponded; + TimeStamp mStartTime; + Optional mSuccessLatencyProbe; + Optional mFailureLatencyProbe; +}; + +} // namespace mozilla::dom + +#endif // __DetailedPromise_h__ diff --git a/dom/media/eme/EMEUtils.cpp b/dom/media/eme/EMEUtils.cpp new file mode 100644 index 0000000000..294951e7e6 --- /dev/null +++ b/dom/media/eme/EMEUtils.cpp @@ -0,0 +1,248 @@ +/* -*- 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 "mozilla/EMEUtils.h" + +#include "jsfriendapi.h" +#include "MediaData.h" +#include "mozilla/StaticPrefs_media.h" +#include "mozilla/dom/KeySystemNames.h" +#include "mozilla/dom/UnionTypes.h" + +#ifdef MOZ_WMF_CDM +# include "mozilla/PMFCDM.h" +# include "KeySystemConfig.h" +#endif + +namespace mozilla { + +LogModule* GetEMELog() { + static LazyLogModule log("EME"); + return log; +} + +LogModule* GetEMEVerboseLog() { + static LazyLogModule log("EMEV"); + return log; +} + +void CopyArrayBufferViewOrArrayBufferData( + const dom::ArrayBufferViewOrArrayBuffer& aBufferOrView, + nsTArray& aOutData) { + aOutData.Clear(); + Unused << dom::AppendTypedArrayDataTo(aBufferOrView, aOutData); +} + +bool IsClearkeyKeySystem(const nsAString& aKeySystem) { + if (StaticPrefs::media_clearkey_test_key_systems_enabled()) { + return aKeySystem.EqualsLiteral(kClearKeyKeySystemName) || + aKeySystem.EqualsLiteral(kClearKeyWithProtectionQueryKeySystemName); + } + return aKeySystem.EqualsLiteral(kClearKeyKeySystemName); +} + +bool IsWidevineKeySystem(const nsAString& aKeySystem) { + return aKeySystem.EqualsLiteral(kWidevineKeySystemName); +} + +#ifdef MOZ_WMF_CDM +bool IsPlayReadyKeySystemAndSupported(const nsAString& aKeySystem) { + if (!StaticPrefs::media_eme_playready_enabled()) { + return false; + } + // 1=enabled encrypted and clear, 2=enabled encrytped. + if (StaticPrefs::media_wmf_media_engine_enabled() != 1 && + StaticPrefs::media_wmf_media_engine_enabled() != 2) { + return false; + } + return IsPlayReadyKeySystem(aKeySystem); +} + +bool IsPlayReadyKeySystem(const nsAString& aKeySystem) { + return aKeySystem.EqualsLiteral(kPlayReadyKeySystemName) || + aKeySystem.EqualsLiteral(kPlayReadyKeySystemHardware) || + aKeySystem.EqualsLiteral(kPlayReadyHardwareClearLeadKeySystemName); +} + +bool IsWidevineExperimentKeySystemAndSupported(const nsAString& aKeySystem) { + if (!StaticPrefs::media_eme_widevine_experiment_enabled()) { + return false; + } + // 1=enabled encrypted and clear, 2=enabled encrytped. + if (StaticPrefs::media_wmf_media_engine_enabled() != 1 && + StaticPrefs::media_wmf_media_engine_enabled() != 2) { + return false; + } + return IsWidevineExperimentKeySystem(aKeySystem); +} + +bool IsWidevineExperimentKeySystem(const nsAString& aKeySystem) { + return aKeySystem.EqualsLiteral(kWidevineExperimentKeySystemName) || + aKeySystem.EqualsLiteral(kWidevineExperiment2KeySystemName); +} + +bool IsWMFClearKeySystemAndSupported(const nsAString& aKeySystem) { + if (!StaticPrefs::media_eme_wmf_clearkey_enabled()) { + return false; + } + // 1=enabled encrypted and clear, 2=enabled encrytped. + if (StaticPrefs::media_wmf_media_engine_enabled() != 1 && + StaticPrefs::media_wmf_media_engine_enabled() != 2) { + return false; + } + return aKeySystem.EqualsLiteral(kClearKeyKeySystemName); +} +#endif + +nsString KeySystemToProxyName(const nsAString& aKeySystem) { + if (IsClearkeyKeySystem(aKeySystem)) { +#ifdef MOZ_WMF_CDM + if (StaticPrefs::media_eme_wmf_clearkey_enabled()) { + return u"mfcdm-clearkey"_ns; + } +#endif + return u"gmp-clearkey"_ns; + } + if (IsWidevineKeySystem(aKeySystem)) { + return u"gmp-widevinecdm"_ns; + } +#ifdef MOZ_WMF_CDM + if (IsPlayReadyKeySystemAndSupported(aKeySystem)) { + return u"mfcdm-playready"_ns; + } + if (IsWidevineExperimentKeySystemAndSupported(aKeySystem)) { + return u"mfcdm-widevine"_ns; + } +#endif + MOZ_ASSERT_UNREACHABLE("Not supported key system!"); + return u""_ns; +} + +#define ENUM_TO_STR(enumVal) \ + case enumVal: \ + return #enumVal + +const char* ToMediaKeyStatusStr(dom::MediaKeyStatus aStatus) { + switch (aStatus) { + ENUM_TO_STR(dom::MediaKeyStatus::Usable); + ENUM_TO_STR(dom::MediaKeyStatus::Expired); + ENUM_TO_STR(dom::MediaKeyStatus::Released); + ENUM_TO_STR(dom::MediaKeyStatus::Output_restricted); + ENUM_TO_STR(dom::MediaKeyStatus::Output_downscaled); + ENUM_TO_STR(dom::MediaKeyStatus::Status_pending); + ENUM_TO_STR(dom::MediaKeyStatus::Internal_error); + default: + return "Undefined MediaKeyStatus!"; + } +} + +#undef ENUM_TO_STR + +bool IsHardwareDecryptionSupported( + const dom::MediaKeySystemConfiguration& aConfig) { + bool supportHardwareDecryption = false; + for (const auto& capabilities : aConfig.mAudioCapabilities) { + if (capabilities.mRobustness.EqualsLiteral("HW_SECURE_ALL")) { + supportHardwareDecryption = true; + break; + } + } + for (const auto& capabilities : aConfig.mVideoCapabilities) { + if (capabilities.mRobustness.EqualsLiteral("3000") || + capabilities.mRobustness.EqualsLiteral("HW_SECURE_ALL") || + capabilities.mRobustness.EqualsLiteral("HW_SECURE_DECODE")) { + supportHardwareDecryption = true; + break; + } + } + return supportHardwareDecryption; +} + +const char* EncryptionSchemeStr(const CryptoScheme& aScheme) { + switch (aScheme) { + case CryptoScheme::None: + return "none"; + case CryptoScheme::Cenc: + return "cenc"; + case CryptoScheme::Cbcs: + return "cbcs"; + default: + return "not-defined!"; + } +} + +#ifdef MOZ_WMF_CDM +void MFCDMCapabilitiesIPDLToKeySystemConfig( + const MFCDMCapabilitiesIPDL& aCDMConfig, + KeySystemConfig& aKeySystemConfig) { + aKeySystemConfig.mKeySystem = aCDMConfig.keySystem(); + + for (const auto& type : aCDMConfig.initDataTypes()) { + aKeySystemConfig.mInitDataTypes.AppendElement(type); + } + + for (const auto& type : aCDMConfig.sessionTypes()) { + aKeySystemConfig.mSessionTypes.AppendElement(type); + } + + for (const auto& c : aCDMConfig.videoCapabilities()) { + if (!c.robustness().IsEmpty() && + !aKeySystemConfig.mVideoRobustness.Contains(c.robustness())) { + aKeySystemConfig.mVideoRobustness.AppendElement(c.robustness()); + } + aKeySystemConfig.mMP4.SetCanDecryptAndDecode( + NS_ConvertUTF16toUTF8(c.contentType())); + } + for (const auto& c : aCDMConfig.audioCapabilities()) { + if (!c.robustness().IsEmpty() && + !aKeySystemConfig.mAudioRobustness.Contains(c.robustness())) { + aKeySystemConfig.mAudioRobustness.AppendElement(c.robustness()); + } + aKeySystemConfig.mMP4.SetCanDecryptAndDecode( + NS_ConvertUTF16toUTF8(c.contentType())); + } + aKeySystemConfig.mPersistentState = aCDMConfig.persistentState(); + aKeySystemConfig.mDistinctiveIdentifier = aCDMConfig.distinctiveID(); + for (const auto& scheme : aCDMConfig.encryptionSchemes()) { + aKeySystemConfig.mEncryptionSchemes.AppendElement( + NS_ConvertUTF8toUTF16(EncryptionSchemeStr(scheme))); + } + aKeySystemConfig.mIsHDCP22Compatible = aCDMConfig.isHDCP22Compatible(); + EME_LOG("New Capabilities=%s", + NS_ConvertUTF16toUTF8(aKeySystemConfig.GetDebugInfo()).get()); +} +#endif + +bool DoesKeySystemSupportClearLead(const nsAString& aKeySystem) { + // I believe that Widevine L3 supports clear-lead, but I couldn't find any + // official documentation to prove that. The only one I can find is that Shaka + // player mentions the clear lead feature. So we expect L3 should have that as + // well. But for HWDRM, Widevine L1 and SL3000 needs to rely on special checks + // to know whether clearlead is supported. That will be implemented by + // querying for special key system names. + // https://shaka-project.github.io/shaka-packager/html/documentation.html +#ifdef MOZ_WMF_CDM + if (aKeySystem.EqualsLiteral(kWidevineExperiment2KeySystemName) || + aKeySystem.EqualsLiteral(kPlayReadyHardwareClearLeadKeySystemName)) { + return true; + } +#endif + return aKeySystem.EqualsLiteral(kWidevineKeySystemName); +} + +bool CheckIfHarewareDRMConfigExists( + const nsTArray& aConfigs) { + bool foundHWDRMconfig = false; + for (const auto& config : aConfigs) { + if (IsHardwareDecryptionSupported(config)) { + foundHWDRMconfig = true; + break; + } + } + return foundHWDRMconfig; +} + +} // namespace mozilla diff --git a/dom/media/eme/EMEUtils.h b/dom/media/eme/EMEUtils.h new file mode 100644 index 0000000000..ca5c684a9f --- /dev/null +++ b/dom/media/eme/EMEUtils.h @@ -0,0 +1,109 @@ +/* -*- 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 EME_LOG_H_ +#define EME_LOG_H_ + +#include "mozilla/Logging.h" +#include "mozilla/dom/MediaKeyStatusMapBinding.h" +#include "mozilla/dom/MediaKeySystemAccessBinding.h" +#include "nsString.h" +#include "nsTArray.h" + +namespace mozilla { + +enum class CryptoScheme : uint8_t; +#ifdef MOZ_WMF_CDM +class MFCDMCapabilitiesIPDL; +#endif +struct KeySystemConfig; + +namespace dom { +class ArrayBufferViewOrArrayBuffer; +} + +#ifndef EME_LOG +LogModule* GetEMELog(); +# define EME_LOG(...) \ + MOZ_LOG(GetEMELog(), mozilla::LogLevel::Debug, (__VA_ARGS__)) +# define EME_LOG_ENABLED() MOZ_LOG_TEST(GetEMELog(), mozilla::LogLevel::Debug) +#endif + +#ifndef EME_VERBOSE_LOG +LogModule* GetEMEVerboseLog(); +# define EME_VERBOSE_LOG(...) \ + MOZ_LOG(GetEMEVerboseLog(), mozilla::LogLevel::Debug, (__VA_ARGS__)) +#else +# ifndef EME_LOG +# define EME_LOG(...) +# endif + +# ifndef EME_VERBOSE_LOG +# define EME_VERBOSE_LOG(...) +# endif +#endif + +// Helper function to extract a copy of data coming in from JS in an +// (ArrayBuffer or ArrayBufferView) IDL typed function argument. +// +// Only call this on a properly initialized ArrayBufferViewOrArrayBuffer. +void CopyArrayBufferViewOrArrayBufferData( + const dom::ArrayBufferViewOrArrayBuffer& aBufferOrView, + nsTArray& aOutData); + +nsString KeySystemToProxyName(const nsAString& aKeySystem); + +bool IsClearkeyKeySystem(const nsAString& aKeySystem); + +bool IsWidevineKeySystem(const nsAString& aKeySystem); + +#ifdef MOZ_WMF_CDM +bool IsPlayReadyKeySystemAndSupported(const nsAString& aKeySystem); + +bool IsPlayReadyKeySystem(const nsAString& aKeySystem); + +bool IsWidevineExperimentKeySystemAndSupported(const nsAString& aKeySystem); + +bool IsWidevineExperimentKeySystem(const nsAString& aKeySystem); + +bool IsWMFClearKeySystemAndSupported(const nsAString& aKeySystem); +#endif + +// Note: Primetime is now unsupported, but we leave it in the enum so +// that the telemetry enum values are not changed; doing so would break +// existing telemetry probes. +enum CDMType { + eClearKey = 0, + ePrimetime = 1, // Note: Unsupported. + eWidevine = 2, + eUnknown = 3 +}; + +CDMType ToCDMTypeTelemetryEnum(const nsString& aKeySystem); + +const char* ToMediaKeyStatusStr(dom::MediaKeyStatus aStatus); + +// Return true if given config supports hardware decryption (SL3000 or L1). +bool IsHardwareDecryptionSupported( + const dom::MediaKeySystemConfiguration& aConfig); + +const char* EncryptionSchemeStr(const CryptoScheme& aScheme); + +#ifdef MOZ_WMF_CDM +void MFCDMCapabilitiesIPDLToKeySystemConfig( + const MFCDMCapabilitiesIPDL& aCDMConfig, KeySystemConfig& aKeySystemConfig); +#endif + +bool DoesKeySystemSupportClearLead(const nsAString& aKeySystem); + +// Return true if there is any config in the given configs has hardware DRM +// associated robustness. +bool CheckIfHarewareDRMConfigExists( + const nsTArray& aConfigs); + +} // namespace mozilla + +#endif // EME_LOG_H_ diff --git a/dom/media/eme/KeySystemConfig.cpp b/dom/media/eme/KeySystemConfig.cpp new file mode 100644 index 0000000000..b1b1f9ff63 --- /dev/null +++ b/dom/media/eme/KeySystemConfig.cpp @@ -0,0 +1,360 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "KeySystemConfig.h" + +#include "EMEUtils.h" +#include "GMPUtils.h" +#include "KeySystemNames.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/StaticPrefs_media.h" +#include "nsPrintfCString.h" + +#ifdef XP_WIN +# include "WMFDecoderModule.h" +#endif +#ifdef MOZ_WIDGET_ANDROID +# include "AndroidDecoderModule.h" +# include "mozilla/java/MediaDrmProxyWrappers.h" +# include "nsMimeTypes.h" +#endif + +#ifdef MOZ_WMF_CDM +# include "mediafoundation/WMFCDMImpl.h" +#endif + +namespace mozilla { + +/* static */ +bool KeySystemConfig::Supports(const nsAString& aKeySystem) { +#ifdef MOZ_WIDGET_ANDROID + // No GMP on Android, check if we can use MediaDrm for this keysystem. + if (mozilla::java::MediaDrmProxy::IsSchemeSupported( + NS_ConvertUTF16toUTF8(aKeySystem))) { + return true; + } +#else +# ifdef MOZ_WMF_CDM + // Test only, pretend we have already installed CDMs. + if (StaticPrefs::media_eme_wmf_use_mock_cdm_for_external_cdms()) { + return true; + } +# endif + // Check if Widevine L3 or Clearkey has been downloaded via GMP downloader. + if (IsWidevineKeySystem(aKeySystem) || IsClearkeyKeySystem(aKeySystem)) { + return HaveGMPFor(nsCString(CHROMIUM_CDM_API), + {NS_ConvertUTF16toUTF8(aKeySystem)}); + } +#endif + +#if MOZ_WMF_CDM + // Check if Widevine L1 has been downloaded via GMP downloader. + if (IsWidevineExperimentKeySystemAndSupported(aKeySystem)) { + return HaveGMPFor(nsCString(kWidevineExperimentAPIName), + {nsCString(kWidevineExperimentKeySystemName)}); + } + + if ((IsPlayReadyKeySystemAndSupported(aKeySystem) || + IsWMFClearKeySystemAndSupported(aKeySystem)) && + WMFCDMImpl::Supports(aKeySystem)) { + return true; + } +#endif + + return false; +} + +/* static */ +bool KeySystemConfig::CreateKeySystemConfigs( + const nsAString& aKeySystem, nsTArray& aOutConfigs) { + if (!Supports(aKeySystem)) { + return false; + } + + if (IsClearkeyKeySystem(aKeySystem)) { + KeySystemConfig* config = aOutConfigs.AppendElement(); + config->mKeySystem = aKeySystem; + config->mInitDataTypes.AppendElement(u"cenc"_ns); + config->mInitDataTypes.AppendElement(u"keyids"_ns); + config->mInitDataTypes.AppendElement(u"webm"_ns); + config->mPersistentState = Requirement::Optional; + config->mDistinctiveIdentifier = Requirement::NotAllowed; + config->mSessionTypes.AppendElement(SessionType::Temporary); + config->mEncryptionSchemes.AppendElement(u"cenc"_ns); + config->mEncryptionSchemes.AppendElement(u"cbcs"_ns); + config->mEncryptionSchemes.AppendElement(u"cbcs-1-9"_ns); + if (StaticPrefs::media_clearkey_persistent_license_enabled()) { + config->mSessionTypes.AppendElement(SessionType::PersistentLicense); + } +#if defined(XP_WIN) + // Clearkey CDM uses WMF's H.264 decoder on Windows. + if (WMFDecoderModule::CanCreateMFTDecoder(WMFStreamType::H264)) { + config->mMP4.SetCanDecryptAndDecode(EME_CODEC_H264); + } else { + config->mMP4.SetCanDecrypt(EME_CODEC_H264); + } +#else + config->mMP4.SetCanDecrypt(EME_CODEC_H264); +#endif + config->mMP4.SetCanDecrypt(EME_CODEC_AAC); + config->mMP4.SetCanDecrypt(EME_CODEC_FLAC); + config->mMP4.SetCanDecrypt(EME_CODEC_OPUS); + config->mMP4.SetCanDecrypt(EME_CODEC_VP9); + config->mWebM.SetCanDecrypt(EME_CODEC_VORBIS); + config->mWebM.SetCanDecrypt(EME_CODEC_OPUS); + config->mWebM.SetCanDecrypt(EME_CODEC_VP8); + config->mWebM.SetCanDecrypt(EME_CODEC_VP9); + + if (StaticPrefs::media_clearkey_test_key_systems_enabled()) { + // Add testing key systems. These offer the same capabilities as the + // base clearkey system, so just clone clearkey and change the name. + KeySystemConfig clearkeyWithProtectionQuery{*config}; + clearkeyWithProtectionQuery.mKeySystem.AssignLiteral( + kClearKeyWithProtectionQueryKeySystemName); + aOutConfigs.AppendElement(std::move(clearkeyWithProtectionQuery)); + } + return true; + } + + if (IsWidevineKeySystem(aKeySystem)) { + KeySystemConfig* config = aOutConfigs.AppendElement(); + config->mKeySystem = aKeySystem; + config->mInitDataTypes.AppendElement(u"cenc"_ns); + config->mInitDataTypes.AppendElement(u"keyids"_ns); + config->mInitDataTypes.AppendElement(u"webm"_ns); + config->mPersistentState = Requirement::Optional; + config->mDistinctiveIdentifier = Requirement::NotAllowed; + config->mSessionTypes.AppendElement(SessionType::Temporary); +#ifdef MOZ_WIDGET_ANDROID + config->mSessionTypes.AppendElement(SessionType::PersistentLicense); +#endif + config->mAudioRobustness.AppendElement(u"SW_SECURE_CRYPTO"_ns); + config->mVideoRobustness.AppendElement(u"SW_SECURE_CRYPTO"_ns); + config->mVideoRobustness.AppendElement(u"SW_SECURE_DECODE"_ns); + config->mEncryptionSchemes.AppendElement(u"cenc"_ns); + config->mEncryptionSchemes.AppendElement(u"cbcs"_ns); + config->mEncryptionSchemes.AppendElement(u"cbcs-1-9"_ns); + +#if defined(MOZ_WIDGET_ANDROID) + // MediaDrm.isCryptoSchemeSupported only allows passing + // "video/mp4" or "video/webm" for mimetype string. + // See + // https://developer.android.com/reference/android/media/MediaDrm.html#isCryptoSchemeSupported(java.util.UUID, + // java.lang.String) for more detail. + typedef struct { + const nsCString& mMimeType; + const nsCString& mEMECodecType; + const char16_t* mCodecType; + KeySystemConfig::ContainerSupport* mSupportType; + } DataForValidation; + + DataForValidation validationList[] = { + {nsCString(VIDEO_MP4), EME_CODEC_H264, java::MediaDrmProxy::AVC, + &config->mMP4}, + {nsCString(VIDEO_MP4), EME_CODEC_VP9, java::MediaDrmProxy::AVC, + &config->mMP4}, + {nsCString(AUDIO_MP4), EME_CODEC_AAC, java::MediaDrmProxy::AAC, + &config->mMP4}, + {nsCString(AUDIO_MP4), EME_CODEC_FLAC, java::MediaDrmProxy::FLAC, + &config->mMP4}, + {nsCString(AUDIO_MP4), EME_CODEC_OPUS, java::MediaDrmProxy::OPUS, + &config->mMP4}, + {nsCString(VIDEO_WEBM), EME_CODEC_VP8, java::MediaDrmProxy::VP8, + &config->mWebM}, + {nsCString(VIDEO_WEBM), EME_CODEC_VP9, java::MediaDrmProxy::VP9, + &config->mWebM}, + {nsCString(AUDIO_WEBM), EME_CODEC_VORBIS, java::MediaDrmProxy::VORBIS, + &config->mWebM}, + {nsCString(AUDIO_WEBM), EME_CODEC_OPUS, java::MediaDrmProxy::OPUS, + &config->mWebM}, + }; + + for (const auto& data : validationList) { + if (java::MediaDrmProxy::IsCryptoSchemeSupported(kWidevineKeySystemName, + data.mMimeType)) { + if (!AndroidDecoderModule::SupportsMimeType(data.mMimeType).isEmpty()) { + data.mSupportType->SetCanDecryptAndDecode(data.mEMECodecType); + } else { + data.mSupportType->SetCanDecrypt(data.mEMECodecType); + } + } + } +#else +# if defined(XP_WIN) + // Widevine CDM doesn't include an AAC decoder. So if WMF can't + // decode AAC, and a codec wasn't specified, be conservative + // and reject the MediaKeys request, since we assume Widevine + // will be used with AAC. + if (WMFDecoderModule::CanCreateMFTDecoder(WMFStreamType::AAC)) { + config->mMP4.SetCanDecrypt(EME_CODEC_AAC); + } +# else + config->mMP4.SetCanDecrypt(EME_CODEC_AAC); +# endif + config->mMP4.SetCanDecrypt(EME_CODEC_FLAC); + config->mMP4.SetCanDecrypt(EME_CODEC_OPUS); + config->mMP4.SetCanDecryptAndDecode(EME_CODEC_H264); + config->mMP4.SetCanDecryptAndDecode(EME_CODEC_VP9); + config->mWebM.SetCanDecrypt(EME_CODEC_VORBIS); + config->mWebM.SetCanDecrypt(EME_CODEC_OPUS); + config->mWebM.SetCanDecryptAndDecode(EME_CODEC_VP8); + config->mWebM.SetCanDecryptAndDecode(EME_CODEC_VP9); +#endif + return true; + } +#ifdef MOZ_WMF_CDM + if (IsPlayReadyKeySystemAndSupported(aKeySystem) || + IsWidevineExperimentKeySystemAndSupported(aKeySystem)) { + RefPtr cdm = MakeRefPtr(aKeySystem); + return cdm->GetCapabilities(aOutConfigs); + } +#endif + return false; +} + +bool KeySystemConfig::IsSameKeySystem(const nsAString& aKeySystem) const { +#ifdef MOZ_WMF_CDM + // We want to map Widevine experiment key system to normal Widevine key system + // as well. + if (IsWidevineExperimentKeySystemAndSupported(mKeySystem)) { + return mKeySystem.Equals(aKeySystem) || + aKeySystem.EqualsLiteral(kWidevineKeySystemName); + } +#endif + return mKeySystem.Equals(aKeySystem); +} + +/* static */ +void KeySystemConfig::GetGMPKeySystemConfigs(dom::Promise* aPromise) { + MOZ_ASSERT(aPromise); + nsTArray keySystemConfigs; + const nsTArray keySystemNames{ + NS_ConvertUTF8toUTF16(kClearKeyKeySystemName), + NS_ConvertUTF8toUTF16(kWidevineKeySystemName), + }; + FallibleTArray cdmInfo; + for (const auto& name : keySystemNames) { +#ifdef MOZ_WMF_CDM + if (IsWMFClearKeySystemAndSupported(name)) { + // Using wmf clearkey, not gmp clearkey. + continue; + } +#endif + if (KeySystemConfig::CreateKeySystemConfigs(name, keySystemConfigs)) { + auto* info = cdmInfo.AppendElement(fallible); + if (!info) { + aPromise->MaybeReject(NS_ERROR_OUT_OF_MEMORY); + return; + } + MOZ_ASSERT(keySystemConfigs.Length() == cdmInfo.Length()); + info->mKeySystemName = name; + info->mCapabilities = keySystemConfigs.LastElement().GetDebugInfo(); + info->mClearlead = DoesKeySystemSupportClearLead(name); + // TODO : ask real CDM + info->mIsHDCP22Compatible = false; + } + } + aPromise->MaybeResolve(cdmInfo); +} + +nsString KeySystemConfig::GetDebugInfo() const { + nsString debugInfo; + debugInfo.AppendLiteral(" key-system="); + debugInfo.Append(mKeySystem); + debugInfo.AppendLiteral(" init-data-type=["); + for (size_t idx = 0; idx < mInitDataTypes.Length(); idx++) { + debugInfo.Append(mInitDataTypes[idx]); + if (idx + 1 < mInitDataTypes.Length()) { + debugInfo.AppendLiteral(","); + } + } + debugInfo.AppendLiteral("]"); + debugInfo.AppendASCII( + nsPrintfCString(" persistent=%s", RequirementToStr(mPersistentState)) + .get()); + debugInfo.AppendASCII( + nsPrintfCString(" distinctive=%s", + RequirementToStr(mDistinctiveIdentifier)) + .get()); + debugInfo.AppendLiteral(" sessionType=["); + for (size_t idx = 0; idx < mSessionTypes.Length(); idx++) { + debugInfo.AppendASCII( + nsPrintfCString("%s", SessionTypeToStr(mSessionTypes[idx])).get()); + if (idx + 1 < mSessionTypes.Length()) { + debugInfo.AppendLiteral(","); + } + } + debugInfo.AppendLiteral("]"); + debugInfo.AppendLiteral(" video-robustness="); + for (size_t idx = 0; idx < mVideoRobustness.Length(); idx++) { + debugInfo.Append(mVideoRobustness[idx]); + if (idx + 1 < mVideoRobustness.Length()) { + debugInfo.AppendLiteral(","); + } + } + debugInfo.AppendLiteral(" audio-robustness="); + for (size_t idx = 0; idx < mAudioRobustness.Length(); idx++) { + debugInfo.Append(mAudioRobustness[idx]); + if (idx + 1 < mAudioRobustness.Length()) { + debugInfo.AppendLiteral(","); + } + } + debugInfo.AppendLiteral(" scheme=["); + for (size_t idx = 0; idx < mEncryptionSchemes.Length(); idx++) { + debugInfo.Append(mEncryptionSchemes[idx]); + if (idx + 1 < mEncryptionSchemes.Length()) { + debugInfo.AppendLiteral(","); + } + } + debugInfo.AppendLiteral("]"); + debugInfo.AppendLiteral(" MP4={"); + debugInfo.Append(NS_ConvertUTF8toUTF16(mMP4.GetDebugInfo())); + debugInfo.AppendLiteral("}"); + debugInfo.AppendLiteral(" WEBM={"); + debugInfo.Append(NS_ConvertUTF8toUTF16(mWebM.GetDebugInfo())); + debugInfo.AppendLiteral("}"); + debugInfo.AppendASCII( + nsPrintfCString(" isHDCP22Compatible=%d", mIsHDCP22Compatible)); + return debugInfo; +} + +KeySystemConfig::SessionType ConvertToKeySystemConfigSessionType( + dom::MediaKeySessionType aType) { + switch (aType) { + case dom::MediaKeySessionType::Temporary: + return KeySystemConfig::SessionType::Temporary; + case dom::MediaKeySessionType::Persistent_license: + return KeySystemConfig::SessionType::PersistentLicense; + default: + MOZ_ASSERT_UNREACHABLE("Invalid session type"); + return KeySystemConfig::SessionType::Temporary; + } +} + +const char* SessionTypeToStr(KeySystemConfig::SessionType aType) { + switch (aType) { + case KeySystemConfig::SessionType::Temporary: + return "Temporary"; + case KeySystemConfig::SessionType::PersistentLicense: + return "PersistentLicense"; + default: + MOZ_ASSERT_UNREACHABLE("Invalid session type"); + return "Invalid"; + } +} + +const char* RequirementToStr(KeySystemConfig::Requirement aRequirement) { + switch (aRequirement) { + case KeySystemConfig::Requirement::Required: + return "required"; + case KeySystemConfig::Requirement::Optional: + return "optional"; + default: + return "not-allowed"; + } +} + +} // namespace mozilla diff --git a/dom/media/eme/KeySystemConfig.h b/dom/media/eme/KeySystemConfig.h new file mode 100644 index 0000000000..8bd7d98217 --- /dev/null +++ b/dom/media/eme/KeySystemConfig.h @@ -0,0 +1,189 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 DOM_MEDIA_EME_KEYSYSTEMCONFIG_H_ +#define DOM_MEDIA_EME_KEYSYSTEMCONFIG_H_ + +#include "nsString.h" +#include "nsTArray.h" +#include "mozilla/dom/MediaKeysBinding.h" + +namespace mozilla { + +struct KeySystemConfig { + public: + // EME MediaKeysRequirement: + // https://www.w3.org/TR/encrypted-media/#dom-mediakeysrequirement + enum class Requirement { + Required = 1, + Optional = 2, + NotAllowed = 3, + }; + + // EME MediaKeySessionType: + // https://www.w3.org/TR/encrypted-media/#dom-mediakeysessiontype + enum class SessionType { + Temporary = 1, + PersistentLicense = 2, + }; + + using EMECodecString = nsCString; + static constexpr auto EME_CODEC_AAC = "aac"_ns; + static constexpr auto EME_CODEC_OPUS = "opus"_ns; + static constexpr auto EME_CODEC_VORBIS = "vorbis"_ns; + static constexpr auto EME_CODEC_FLAC = "flac"_ns; + static constexpr auto EME_CODEC_H264 = "h264"_ns; + static constexpr auto EME_CODEC_VP8 = "vp8"_ns; + static constexpr auto EME_CODEC_VP9 = "vp9"_ns; + static constexpr auto EME_CODEC_HEVC = "hevc"_ns; + + using EMEEncryptionSchemeString = nsCString; + static constexpr auto EME_ENCRYPTION_SCHEME_CENC = "cenc"_ns; + static constexpr auto EME_ENCRYPTION_SCHEME_CBCS = "cbcs"_ns; + + // A codec can be decrypted-and-decoded by the CDM, or only decrypted + // by the CDM and decoded by Gecko. Not both. + struct ContainerSupport { + ContainerSupport() = default; + ~ContainerSupport() = default; + ContainerSupport(const ContainerSupport& aOther) { + mCodecsDecoded = aOther.mCodecsDecoded.Clone(); + mCodecsDecrypted = aOther.mCodecsDecrypted.Clone(); + } + ContainerSupport& operator=(const ContainerSupport& aOther) { + if (this == &aOther) { + return *this; + } + mCodecsDecoded = aOther.mCodecsDecoded.Clone(); + mCodecsDecrypted = aOther.mCodecsDecrypted.Clone(); + return *this; + } + ContainerSupport(ContainerSupport&& aOther) = default; + ContainerSupport& operator=(ContainerSupport&&) = default; + + bool IsSupported() const { + return !mCodecsDecoded.IsEmpty() || !mCodecsDecrypted.IsEmpty(); + } + + // CDM decrypts and decodes using a DRM robust decoder, and passes decoded + // samples back to Gecko for rendering. + bool DecryptsAndDecodes(const EMECodecString& aCodec) const { + return mCodecsDecoded.Contains(aCodec); + } + + // CDM decrypts and passes the decrypted samples back to Gecko for decoding. + bool Decrypts(const EMECodecString& aCodec) const { + return mCodecsDecrypted.Contains(aCodec); + } + + void SetCanDecryptAndDecode(const EMECodecString& aCodec) { + // Can't both decrypt and decrypt-and-decode a codec. + MOZ_ASSERT(!Decrypts(aCodec)); + // Prevent duplicates. + MOZ_ASSERT(!DecryptsAndDecodes(aCodec)); + mCodecsDecoded.AppendElement(aCodec); + } + + void SetCanDecrypt(const EMECodecString& aCodec) { + // Prevent duplicates. + MOZ_ASSERT(!Decrypts(aCodec)); + // Can't both decrypt and decrypt-and-decode a codec. + MOZ_ASSERT(!DecryptsAndDecodes(aCodec)); + mCodecsDecrypted.AppendElement(aCodec); + } + + EMECodecString GetDebugInfo() const { + EMECodecString info; + info.AppendLiteral("decoding-and-decrypting:["); + for (size_t idx = 0; idx < mCodecsDecoded.Length(); idx++) { + info.Append(mCodecsDecoded[idx]); + if (idx + 1 < mCodecsDecoded.Length()) { + info.AppendLiteral(","); + } + } + info.AppendLiteral("],"); + info.AppendLiteral("decrypting-only:["); + for (size_t idx = 0; idx < mCodecsDecrypted.Length(); idx++) { + info.Append(mCodecsDecrypted[idx]); + if (idx + 1 < mCodecsDecrypted.Length()) { + info.AppendLiteral(","); + } + } + info.AppendLiteral("]"); + return info; + } + + private: + nsTArray mCodecsDecoded; + nsTArray mCodecsDecrypted; + }; + + // Return true if given key system is supported on the current device. + static bool Supports(const nsAString& aKeySystem); + static bool CreateKeySystemConfigs(const nsAString& aKeySystem, + nsTArray& aOutConfigs); + static void GetGMPKeySystemConfigs(dom::Promise* aPromise); + + KeySystemConfig() = default; + ~KeySystemConfig() = default; + explicit KeySystemConfig(const KeySystemConfig& aOther) { + mKeySystem = aOther.mKeySystem; + mInitDataTypes = aOther.mInitDataTypes.Clone(); + mPersistentState = aOther.mPersistentState; + mDistinctiveIdentifier = aOther.mDistinctiveIdentifier; + mSessionTypes = aOther.mSessionTypes.Clone(); + mVideoRobustness = aOther.mVideoRobustness.Clone(); + mAudioRobustness = aOther.mAudioRobustness.Clone(); + mEncryptionSchemes = aOther.mEncryptionSchemes.Clone(); + mMP4 = aOther.mMP4; + mWebM = aOther.mWebM; + } + KeySystemConfig& operator=(const KeySystemConfig& aOther) { + if (this == &aOther) { + return *this; + } + mKeySystem = aOther.mKeySystem; + mInitDataTypes = aOther.mInitDataTypes.Clone(); + mPersistentState = aOther.mPersistentState; + mDistinctiveIdentifier = aOther.mDistinctiveIdentifier; + mSessionTypes = aOther.mSessionTypes.Clone(); + mVideoRobustness = aOther.mVideoRobustness.Clone(); + mAudioRobustness = aOther.mAudioRobustness.Clone(); + mEncryptionSchemes = aOther.mEncryptionSchemes.Clone(); + mMP4 = aOther.mMP4; + mWebM = aOther.mWebM; + return *this; + } + KeySystemConfig(KeySystemConfig&&) = default; + KeySystemConfig& operator=(KeySystemConfig&&) = default; + + nsString GetDebugInfo() const; + + // Return true if the given key system is equal to `mKeySystem`, or it can be + // mapped to the same key system + bool IsSameKeySystem(const nsAString& aKeySystem) const; + + nsString mKeySystem; + nsTArray mInitDataTypes; + Requirement mPersistentState = Requirement::NotAllowed; + Requirement mDistinctiveIdentifier = Requirement::NotAllowed; + nsTArray mSessionTypes; + nsTArray mVideoRobustness; + nsTArray mAudioRobustness; + nsTArray mEncryptionSchemes; + ContainerSupport mMP4; + ContainerSupport mWebM; + bool mIsHDCP22Compatible = false; +}; + +KeySystemConfig::SessionType ConvertToKeySystemConfigSessionType( + dom::MediaKeySessionType aType); +const char* SessionTypeToStr(KeySystemConfig::SessionType aType); +const char* RequirementToStr(KeySystemConfig::Requirement aRequirement); + +} // namespace mozilla + +#endif // DOM_MEDIA_EME_KEYSYSTEMCONFIG_H_ diff --git a/dom/media/eme/KeySystemNames.h b/dom/media/eme/KeySystemNames.h new file mode 100644 index 0000000000..182fe231e2 --- /dev/null +++ b/dom/media/eme/KeySystemNames.h @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 DOM_MEDIA_EME_KEY_SYSTEM_NAMES_H_ +#define DOM_MEDIA_EME_KEY_SYSTEM_NAMES_H_ + +// Header for key system names. Keep these separate from some of our other +// EME utils because want to use these strings in contexts where other utils +// may not build correctly. Specifically at time of writing: +// - The GMP doesn't have prefs available, so we want to avoid utils that +// touch the pref service. +// - The clear key CDM links a limited subset of what normal Fx does, so we +// need to avoid any utils that touch things like XUL. + +namespace mozilla { +// EME Key System Strings. +inline constexpr char kClearKeyKeySystemName[] = "org.w3.clearkey"; +inline constexpr char kClearKeyWithProtectionQueryKeySystemName[] = + "org.mozilla.clearkey_with_protection_query"; +inline constexpr char kWidevineKeySystemName[] = "com.widevine.alpha"; +#ifdef MOZ_WMF_CDM +// A sub key system of `kWidevineKeySystem` only used in experiments. +inline constexpr char kWidevineExperimentKeySystemName[] = + "com.widevine.alpha.experiment"; +// A sub key system of `kWidevineKeySystem` only used in experiments to support +// hardware decryption with codecs that support clear lead. +inline constexpr char kWidevineExperiment2KeySystemName[] = + "com.widevine.alpha.experiment2"; +// API name used for searching GMP Wideivine L1 plugin. +inline constexpr char kWidevineExperimentAPIName[] = "windows-mf-cdm"; + +// https://learn.microsoft.com/en-us/playready/overview/key-system-strings +inline constexpr char kPlayReadyKeySystemName[] = + "com.microsoft.playready.recommendation"; +inline constexpr char kPlayReadyKeySystemHardware[] = + "com.microsoft.playready.recommendation.3000"; + +// A sub key system of `kPlayReadyKeySystemName` only used in experiments to +// support hardware decryption with codecs that support clear lead. +inline constexpr char kPlayReadyHardwareClearLeadKeySystemName[] = + "com.microsoft.playready.recommendation.3000.clearlead"; +#endif +} // namespace mozilla + +#endif // DOM_MEDIA_EME_KEY_SYSTEM_NAMES_H_ diff --git a/dom/media/eme/MediaEncryptedEvent.cpp b/dom/media/eme/MediaEncryptedEvent.cpp new file mode 100644 index 0000000000..bef98bcd55 --- /dev/null +++ b/dom/media/eme/MediaEncryptedEvent.cpp @@ -0,0 +1,106 @@ +/* -*- 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 "MediaEncryptedEvent.h" +#include "mozilla/dom/MediaEncryptedEventBinding.h" +#include "nsContentUtils.h" +#include "js/ArrayBuffer.h" +#include "jsfriendapi.h" +#include "nsINode.h" +#include "mozilla/dom/MediaKeys.h" +#include "mozilla/HoldDropJSObjects.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(MediaEncryptedEvent) + +NS_IMPL_ADDREF_INHERITED(MediaEncryptedEvent, Event) +NS_IMPL_RELEASE_INHERITED(MediaEncryptedEvent, Event) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(MediaEncryptedEvent, Event) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(MediaEncryptedEvent, Event) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mInitData) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(MediaEncryptedEvent, Event) + mozilla::DropJSObjects(tmp); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaEncryptedEvent) +NS_INTERFACE_MAP_END_INHERITING(Event) + +MediaEncryptedEvent::MediaEncryptedEvent(EventTarget* aOwner) + : Event(aOwner, nullptr, nullptr) { + mozilla::HoldJSObjects(this); +} + +MediaEncryptedEvent::~MediaEncryptedEvent() { mozilla::DropJSObjects(this); } + +JSObject* MediaEncryptedEvent::WrapObjectInternal( + JSContext* aCx, JS::Handle aGivenProto) { + return MediaEncryptedEvent_Binding::Wrap(aCx, this, aGivenProto); +} + +already_AddRefed MediaEncryptedEvent::Constructor( + EventTarget* aOwner) { + RefPtr e = new MediaEncryptedEvent(aOwner); + e->InitEvent(u"encrypted"_ns, CanBubble::eNo, Cancelable::eNo); + e->SetTrusted(true); + return e.forget(); +} + +already_AddRefed MediaEncryptedEvent::Constructor( + EventTarget* aOwner, const nsAString& aInitDataType, + const nsTArray& aInitData) { + RefPtr e = new MediaEncryptedEvent(aOwner); + e->InitEvent(u"encrypted"_ns, CanBubble::eNo, Cancelable::eNo); + e->mInitDataType = aInitDataType; + e->mRawInitData = aInitData.Clone(); + e->SetTrusted(true); + return e.forget(); +} + +already_AddRefed MediaEncryptedEvent::Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const MediaKeyNeededEventInit& aEventInitDict, ErrorResult& aRv) { + nsCOMPtr owner = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr e = new MediaEncryptedEvent(owner); + bool trusted = e->Init(owner); + e->InitEvent(aType, aEventInitDict.mBubbles, aEventInitDict.mCancelable); + e->mInitDataType = aEventInitDict.mInitDataType; + if (!aEventInitDict.mInitData.IsNull()) { + JS::Rooted buffer(aGlobal.Context(), + aEventInitDict.mInitData.Value().Obj()); + e->mInitData = JS::CopyArrayBuffer(aGlobal.Context(), buffer); + if (!e->mInitData) { + aRv.NoteJSContextException(aGlobal.Context()); + return nullptr; + } + } + e->SetTrusted(trusted); + return e.forget(); +} + +void MediaEncryptedEvent::GetInitDataType(nsString& aRetVal) const { + aRetVal = mInitDataType; +} + +void MediaEncryptedEvent::GetInitData(JSContext* cx, + JS::MutableHandle aData, + ErrorResult& aRv) { + if (mRawInitData.Length()) { + mInitData = ArrayBuffer::Create(cx, this, mRawInitData, aRv); + if (aRv.Failed()) { + return; + } + mRawInitData.Clear(); + } + aData.set(mInitData); +} + +} // namespace mozilla::dom diff --git a/dom/media/eme/MediaEncryptedEvent.h b/dom/media/eme/MediaEncryptedEvent.h new file mode 100644 index 0000000000..0fcb5db051 --- /dev/null +++ b/dom/media/eme/MediaEncryptedEvent.h @@ -0,0 +1,60 @@ +/* -*- 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 mozilla_dom_MediaKeyNeededEvent_h__ +#define mozilla_dom_MediaKeyNeededEvent_h__ + +#include +#include "js/RootingAPI.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Assertions.h" +#include "mozilla/dom/Event.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsStringFwd.h" +#include "nsTArray.h" + +namespace mozilla::dom { +struct MediaKeyNeededEventInit; + +class MediaEncryptedEvent final : public Event { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(MediaEncryptedEvent, + Event) + protected: + virtual ~MediaEncryptedEvent(); + explicit MediaEncryptedEvent(EventTarget* aOwner); + + nsString mInitDataType; + JS::Heap mInitData; + + public: + JSObject* WrapObjectInternal(JSContext* aCx, + JS::Handle aGivenProto) override; + + static already_AddRefed Constructor(EventTarget* aOwner); + + static already_AddRefed Constructor( + EventTarget* aOwner, const nsAString& aInitDataType, + const nsTArray& aInitData); + + static already_AddRefed Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const MediaKeyNeededEventInit& aEventInitDict, ErrorResult& aRv); + + void GetInitDataType(nsString& aRetVal) const; + + void GetInitData(JSContext* cx, JS::MutableHandle aData, + ErrorResult& aRv); + + private: + nsTArray mRawInitData; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_MediaKeyNeededEvent_h__ diff --git a/dom/media/eme/MediaKeyError.cpp b/dom/media/eme/MediaKeyError.cpp new file mode 100644 index 0000000000..7172b0d05b --- /dev/null +++ b/dom/media/eme/MediaKeyError.cpp @@ -0,0 +1,27 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "MediaKeyError.h" +#include "mozilla/dom/MediaKeyErrorBinding.h" +#include "nsContentUtils.h" + +namespace mozilla::dom { + +MediaKeyError::MediaKeyError(EventTarget* aOwner, uint32_t aSystemCode) + : Event(aOwner, nullptr, nullptr), mSystemCode(aSystemCode) { + InitEvent(u"error"_ns, CanBubble::eNo, Cancelable::eNo); +} + +MediaKeyError::~MediaKeyError() = default; + +uint32_t MediaKeyError::SystemCode() const { return mSystemCode; } + +JSObject* MediaKeyError::WrapObjectInternal(JSContext* aCx, + JS::Handle aGivenProto) { + return MediaKeyError_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/media/eme/MediaKeyError.h b/dom/media/eme/MediaKeyError.h new file mode 100644 index 0000000000..ef4ecf1375 --- /dev/null +++ b/dom/media/eme/MediaKeyError.h @@ -0,0 +1,33 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 mozilla_dom_MediaKeyError_h +#define mozilla_dom_MediaKeyError_h + +#include "mozilla/Attributes.h" +#include "nsWrapperCache.h" +#include "mozilla/dom/Event.h" +#include "js/TypeDecls.h" + +namespace mozilla::dom { + +class MediaKeyError final : public Event { + public: + MediaKeyError(EventTarget* aOwner, uint32_t aSystemCode); + ~MediaKeyError(); + + JSObject* WrapObjectInternal(JSContext* aCx, + JS::Handle aGivenProto) override; + + uint32_t SystemCode() const; + + private: + uint32_t mSystemCode; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/eme/MediaKeyMessageEvent.cpp b/dom/media/eme/MediaKeyMessageEvent.cpp new file mode 100644 index 0000000000..0884e3b0e0 --- /dev/null +++ b/dom/media/eme/MediaKeyMessageEvent.cpp @@ -0,0 +1,101 @@ +/* -*- 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 "mozilla/dom/MediaKeyMessageEvent.h" +#include "mozilla/dom/MediaKeyMessageEventBinding.h" +#include "js/ArrayBuffer.h" +#include "js/RootingAPI.h" +#include "jsfriendapi.h" +#include "mozilla/dom/Nullable.h" +#include "mozilla/dom/PrimitiveConversions.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/dom/TypedArray.h" +#include "nsContentUtils.h" +#include "mozilla/dom/MediaKeys.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(MediaKeyMessageEvent) + +NS_IMPL_ADDREF_INHERITED(MediaKeyMessageEvent, Event) +NS_IMPL_RELEASE_INHERITED(MediaKeyMessageEvent, Event) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(MediaKeyMessageEvent, Event) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(MediaKeyMessageEvent, Event) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mMessage) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(MediaKeyMessageEvent, Event) + mozilla::DropJSObjects(tmp); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeyMessageEvent) +NS_INTERFACE_MAP_END_INHERITING(Event) + +MediaKeyMessageEvent::MediaKeyMessageEvent(EventTarget* aOwner) + : Event(aOwner, nullptr, nullptr), + mMessageType(static_cast(0)) { + mozilla::HoldJSObjects(this); +} + +MediaKeyMessageEvent::~MediaKeyMessageEvent() { mozilla::DropJSObjects(this); } + +MediaKeyMessageEvent* MediaKeyMessageEvent::AsMediaKeyMessageEvent() { + return this; +} + +JSObject* MediaKeyMessageEvent::WrapObjectInternal( + JSContext* aCx, JS::Handle aGivenProto) { + return MediaKeyMessageEvent_Binding::Wrap(aCx, this, aGivenProto); +} + +already_AddRefed MediaKeyMessageEvent::Constructor( + EventTarget* aOwner, MediaKeyMessageType aMessageType, + const nsTArray& aMessage) { + RefPtr e = new MediaKeyMessageEvent(aOwner); + e->InitEvent(u"message"_ns, false, false); + e->mMessageType = aMessageType; + e->mRawMessage = aMessage.Clone(); + e->SetTrusted(true); + return e.forget(); +} + +already_AddRefed MediaKeyMessageEvent::Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const MediaKeyMessageEventInit& aEventInitDict, ErrorResult& aRv) { + nsCOMPtr owner = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr e = new MediaKeyMessageEvent(owner); + bool trusted = e->Init(owner); + e->InitEvent(aType, aEventInitDict.mBubbles, aEventInitDict.mCancelable); + JS::Rooted buffer(aGlobal.Context(), + aEventInitDict.mMessage.Obj()); + e->mMessage = JS::CopyArrayBuffer(aGlobal.Context(), buffer); + if (!e->mMessage) { + aRv.NoteJSContextException(aGlobal.Context()); + return nullptr; + } + e->mMessageType = aEventInitDict.mMessageType; + e->SetTrusted(trusted); + e->SetComposed(aEventInitDict.mComposed); + return e.forget(); +} + +void MediaKeyMessageEvent::GetMessage(JSContext* cx, + JS::MutableHandle aMessage, + ErrorResult& aRv) { + if (!mMessage) { + mMessage = ArrayBuffer::Create(cx, this, mRawMessage, aRv); + if (aRv.Failed()) { + return; + } + mRawMessage.Clear(); + } + aMessage.set(mMessage); +} + +} // namespace mozilla::dom diff --git a/dom/media/eme/MediaKeyMessageEvent.h b/dom/media/eme/MediaKeyMessageEvent.h new file mode 100644 index 0000000000..400439b065 --- /dev/null +++ b/dom/media/eme/MediaKeyMessageEvent.h @@ -0,0 +1,64 @@ +/* -*- 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 mozilla_dom_MediaKeyMessageEvent_h__ +#define mozilla_dom_MediaKeyMessageEvent_h__ + +#include "mozilla/Attributes.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" +#include "nsCOMPtr.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/TypedArray.h" +#include "js/TypeDecls.h" +#include "mozilla/dom/MediaKeyMessageEventBinding.h" + +namespace mozilla { +class ErrorResult; + +namespace dom { + +struct MediaKeyMessageEventInit; + +class MediaKeyMessageEvent final : public Event { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(MediaKeyMessageEvent, + Event) + protected: + virtual ~MediaKeyMessageEvent(); + explicit MediaKeyMessageEvent(EventTarget* aOwner); + + MediaKeyMessageType mMessageType; + JS::Heap mMessage; + + public: + virtual MediaKeyMessageEvent* AsMediaKeyMessageEvent(); + + JSObject* WrapObjectInternal(JSContext* aCx, + JS::Handle aGivenProto) override; + + static already_AddRefed Constructor( + EventTarget* aOwner, MediaKeyMessageType aMessageType, + const nsTArray& aMessage); + + static already_AddRefed Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const MediaKeyMessageEventInit& aEventInitDict, ErrorResult& aRv); + + MediaKeyMessageType MessageType() const { return mMessageType; } + + void GetMessage(JSContext* cx, JS::MutableHandle aMessage, + ErrorResult& aRv); + + private: + nsTArray mRawMessage; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_MediaKeyMessageEvent_h__ diff --git a/dom/media/eme/MediaKeySession.cpp b/dom/media/eme/MediaKeySession.cpp new file mode 100644 index 0000000000..908df5f7c8 --- /dev/null +++ b/dom/media/eme/MediaKeySession.cpp @@ -0,0 +1,622 @@ +/* -*- 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 "mozilla/dom/MediaKeySession.h" + +#include +#include + +#include "GMPUtils.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/CDMProxy.h" +#include "mozilla/EMEUtils.h" +#include "mozilla/Encoding.h" +#include "mozilla/dom/HTMLMediaElement.h" +#include "mozilla/dom/KeyIdsInitDataBinding.h" +#include "mozilla/dom/MediaEncryptedEvent.h" +#include "mozilla/dom/MediaKeyError.h" +#include "mozilla/dom/MediaKeyMessageEvent.h" +#include "mozilla/dom/MediaKeyStatusMap.h" +#include "mozilla/dom/MediaKeySystemAccess.h" +#include "nsCycleCollectionParticipant.h" +#include "nsPrintfCString.h" +#include "psshparser/PsshParser.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaKeySession, DOMEventTargetHelper, + mMediaKeyError, mKeys, mKeyStatusMap, + mClosed) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeySession) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_ADDREF_INHERITED(MediaKeySession, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(MediaKeySession, DOMEventTargetHelper) + +// Count of number of instances. Used to give each instance a +// unique token. +static uint32_t sMediaKeySessionNum = 0; + +// Max length of keyId in EME "keyIds" or WebM init data format, as enforced +// by web platform tests. +static const uint32_t MAX_KEY_ID_LENGTH = 512; + +// Max length of CENC PSSH init data tolerated, as enforced by web +// platform tests. +static const uint32_t MAX_CENC_INIT_DATA_LENGTH = 64 * 1024; + +MediaKeySession::MediaKeySession(nsPIDOMWindowInner* aParent, MediaKeys* aKeys, + const nsAString& aKeySystem, + MediaKeySessionType aSessionType, + ErrorResult& aRv) + : DOMEventTargetHelper(aParent), + mKeys(aKeys), + mKeySystem(aKeySystem), + mSessionType(aSessionType), + mToken(sMediaKeySessionNum++), + mIsClosed(false), + mUninitialized(true), + mKeyStatusMap(new MediaKeyStatusMap(aParent)), + mExpiration(JS::GenericNaN()) { + EME_LOG("MediaKeySession[%p,''] ctor", this); + + MOZ_ASSERT(aParent); + if (aRv.Failed()) { + return; + } + mClosed = MakePromise(aRv, "MediaKeys.createSession"_ns); +} + +void MediaKeySession::SetSessionId(const nsAString& aSessionId) { + EME_LOG("MediaKeySession[%p,'%s'] session Id set", this, + NS_ConvertUTF16toUTF8(aSessionId).get()); + + if (NS_WARN_IF(!mSessionId.IsEmpty())) { + return; + } + mSessionId = aSessionId; + mKeys->OnSessionIdReady(this); +} + +MediaKeySession::~MediaKeySession() { + EME_LOG("MediaKeySession[%p,'%s'] dtor", this, + NS_ConvertUTF16toUTF8(mSessionId).get()); +} + +MediaKeyError* MediaKeySession::GetError() const { return mMediaKeyError; } + +void MediaKeySession::GetSessionId(nsString& aSessionId) const { + aSessionId = GetSessionId(); +} + +const nsString& MediaKeySession::GetSessionId() const { return mSessionId; } + +JSObject* MediaKeySession::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return MediaKeySession_Binding::Wrap(aCx, this, aGivenProto); +} + +double MediaKeySession::Expiration() const { return mExpiration; } + +Promise* MediaKeySession::Closed() const { return mClosed; } + +void MediaKeySession::UpdateKeyStatusMap() { + MOZ_ASSERT(!IsClosed()); + if (!mKeys->GetCDMProxy()) { + return; + } + + nsTArray keyStatuses; + { + auto caps = mKeys->GetCDMProxy()->Capabilites().Lock(); + caps->GetKeyStatusesForSession(mSessionId, keyStatuses); + } + + mKeyStatusMap->Update(keyStatuses); + + if (EME_LOG_ENABLED()) { + nsAutoCString message( + nsPrintfCString("MediaKeySession[%p,'%s'] key statuses change {", this, + NS_ConvertUTF16toUTF8(mSessionId).get())); + for (const CDMCaps::KeyStatus& status : keyStatuses) { + message.AppendPrintf( + " (%s,%s)", ToHexString(status.mId).get(), + nsCString(MediaKeyStatusValues::GetString(status.mStatus)).get()); + } + message.AppendLiteral(" }"); + // Use %s so we aren't exposing random strings to printf interpolation. + EME_LOG("%s", message.get()); + } +} + +MediaKeyStatusMap* MediaKeySession::KeyStatuses() const { + return mKeyStatusMap; +} + +// The user agent MUST thoroughly validate the Initialization Data before +// passing it to the CDM. This includes verifying that the length and +// values of fields are reasonable, verifying that values are within +// reasonable limits, and stripping irrelevant, unsupported, or unknown +// data or fields. It is RECOMMENDED that user agents pre-parse, sanitize, +// and/or generate a fully sanitized version of the Initialization Data. +// If the Initialization Data format specified by initDataType supports +// multiple entries, the user agent SHOULD remove entries that are not +// needed by the CDM. The user agent MUST NOT re-order entries within +// the Initialization Data. +static bool ValidateInitData(const nsTArray& aInitData, + const nsAString& aInitDataType) { + if (aInitDataType.LowerCaseEqualsLiteral("webm")) { + // WebM initData consists of a single keyId. Ensure it's of reasonable + // length. + return aInitData.Length() <= MAX_KEY_ID_LENGTH; + } else if (aInitDataType.LowerCaseEqualsLiteral("cenc")) { + // Limit initData to less than 64KB. + if (aInitData.Length() > MAX_CENC_INIT_DATA_LENGTH) { + return false; + } + std::vector> keyIds; + return ParseCENCInitData(aInitData.Elements(), aInitData.Length(), keyIds); + } else if (aInitDataType.LowerCaseEqualsLiteral("keyids")) { + if (aInitData.Length() > MAX_KEY_ID_LENGTH) { + return false; + } + // Ensure that init data matches the expected JSON format. + mozilla::dom::KeyIdsInitData keyIds; + nsString json; + nsDependentCSubstring raw( + reinterpret_cast(aInitData.Elements()), + aInitData.Length()); + if (NS_FAILED(UTF_8_ENCODING->DecodeWithBOMRemoval(raw, json))) { + return false; + } + if (!keyIds.Init(json)) { + return false; + } + if (keyIds.mKids.Length() == 0) { + return false; + } + for (const auto& kid : keyIds.mKids) { + if (kid.IsEmpty()) { + return false; + } + } + } + return true; +} + +// Generates a license request based on the initData. A message of type +// "license-request" or "individualization-request" will always be queued +// if the algorithm succeeds and the promise is resolved. +already_AddRefed MediaKeySession::GenerateRequest( + const nsAString& aInitDataType, + const ArrayBufferViewOrArrayBuffer& aInitData, ErrorResult& aRv) { + RefPtr promise( + MakePromise(aRv, "MediaKeySession.generateRequest"_ns)); + if (aRv.Failed()) { + return nullptr; + } + + // If this object is closed, return a promise rejected with an + // InvalidStateError. + if (IsClosed()) { + EME_LOG("MediaKeySession[%p,'%s'] GenerateRequest() failed, closed", this, + NS_ConvertUTF16toUTF8(mSessionId).get()); + promise->MaybeRejectWithInvalidStateError( + "Session is closed in MediaKeySession.generateRequest()"); + return promise.forget(); + } + + // If this object's uninitialized value is false, return a promise rejected + // with an InvalidStateError. + if (!mUninitialized) { + EME_LOG("MediaKeySession[%p,'%s'] GenerateRequest() failed, uninitialized", + this, NS_ConvertUTF16toUTF8(mSessionId).get()); + promise->MaybeRejectWithInvalidStateError( + "Session is already initialized in MediaKeySession.generateRequest()"); + return promise.forget(); + } + + // Let this object's uninitialized value be false. + mUninitialized = false; + + // If initDataType is the empty string, return a promise rejected + // with a newly created TypeError. + if (aInitDataType.IsEmpty()) { + promise->MaybeRejectWithTypeError( + "Empty initDataType passed to MediaKeySession.generateRequest()"); + EME_LOG( + "MediaKeySession[%p,'%s'] GenerateRequest() failed, empty initDataType", + this, NS_ConvertUTF16toUTF8(mSessionId).get()); + return promise.forget(); + } + + // If initData is an empty array, return a promise rejected with + // a newly created TypeError. + nsTArray data; + CopyArrayBufferViewOrArrayBufferData(aInitData, data); + if (data.IsEmpty()) { + promise->MaybeRejectWithTypeError( + "Empty initData passed to MediaKeySession.generateRequest()"); + EME_LOG("MediaKeySession[%p,'%s'] GenerateRequest() failed, empty initData", + this, NS_ConvertUTF16toUTF8(mSessionId).get()); + return promise.forget(); + } + + // If the Key System implementation represented by this object's + // cdm implementation value does not support initDataType as an + // Initialization Data Type, return a promise rejected with a + // NotSupportedError. String comparison is case-sensitive. + if (!MediaKeySystemAccess::KeySystemSupportsInitDataType(mKeySystem, + aInitDataType)) { + promise->MaybeRejectWithNotSupportedError( + "Unsupported initDataType passed to MediaKeySession.generateRequest()"); + EME_LOG( + "MediaKeySession[%p,'%s'] GenerateRequest() failed, unsupported " + "initDataType", + this, NS_ConvertUTF16toUTF8(mSessionId).get()); + return promise.forget(); + } + + // Let init data be a copy of the contents of the initData parameter. + // Note: Handled by the CopyArrayBufferViewOrArrayBufferData call above. + + // Let session type be this object's session type. + + // Let promise be a new promise. + + // Run the following steps in parallel: + + // If the init data is not valid for initDataType, reject promise with + // a newly created TypeError. + if (!ValidateInitData(data, aInitDataType)) { + // If the preceding step failed, reject promise with a newly created + // TypeError. + promise->MaybeRejectWithTypeError( + "initData sanitization failed in MediaKeySession.generateRequest()"); + EME_LOG( + "MediaKeySession[%p,'%s'] GenerateRequest() initData sanitization " + "failed", + this, NS_ConvertUTF16toUTF8(mSessionId).get()); + return promise.forget(); + } + + // Let sanitized init data be a validated and sanitized version of init data. + + // If sanitized init data is empty, reject promise with a NotSupportedError. + + // Note: Remaining steps of generateRequest method continue in CDM. + + // Convert initData to hex for easier logging. + // Note: CreateSession() std::move()s the data out of the array, so we have + // to copy it here. + nsAutoCString hexInitData(ToHexString(data)); + PromiseId pid = mKeys->StorePromise(promise); + mKeys->ConnectPendingPromiseIdWithToken(pid, Token()); + mKeys->GetCDMProxy()->CreateSession(Token(), mSessionType, pid, aInitDataType, + data); + + EME_LOG( + "MediaKeySession[%p,'%s'] GenerateRequest() sent, " + "promiseId=%d initData='%s' initDataType='%s'", + this, NS_ConvertUTF16toUTF8(mSessionId).get(), pid, hexInitData.get(), + NS_ConvertUTF16toUTF8(aInitDataType).get()); + + return promise.forget(); +} + +already_AddRefed MediaKeySession::Load(const nsAString& aSessionId, + ErrorResult& aRv) { + RefPtr promise(MakePromise(aRv, "MediaKeySession.load"_ns)); + if (aRv.Failed()) { + return nullptr; + } + + // 1. If this object is closed, return a promise rejected with an + // InvalidStateError. + if (IsClosed()) { + promise->MaybeRejectWithInvalidStateError( + "Session is closed in MediaKeySession.load()"); + EME_LOG("MediaKeySession[%p,'%s'] Load() failed, closed", this, + NS_ConvertUTF16toUTF8(aSessionId).get()); + return promise.forget(); + } + + // 2.If this object's uninitialized value is false, return a promise rejected + // with an InvalidStateError. + if (!mUninitialized) { + promise->MaybeRejectWithInvalidStateError( + "Session is already initialized in MediaKeySession.load()"); + EME_LOG("MediaKeySession[%p,'%s'] Load() failed, uninitialized", this, + NS_ConvertUTF16toUTF8(aSessionId).get()); + return promise.forget(); + } + + // 3.Let this object's uninitialized value be false. + mUninitialized = false; + + // 4. If sessionId is the empty string, return a promise rejected with a newly + // created TypeError. + if (aSessionId.IsEmpty()) { + promise->MaybeRejectWithTypeError( + "Trying to load a session with empty session ID"); + // "The sessionId parameter is empty." + EME_LOG("MediaKeySession[%p,''] Load() failed, no sessionId", this); + return promise.forget(); + } + + // 5. If the result of running the Is persistent session type? algorithm + // on this object's session type is false, return a promise rejected with + // a newly created TypeError. + if (mSessionType == MediaKeySessionType::Temporary) { + promise->MaybeRejectWithTypeError( + "Trying to load() into a non-persistent session"); + EME_LOG( + "MediaKeySession[%p,''] Load() failed, can't load in a non-persistent " + "session", + this); + return promise.forget(); + } + + // Note: We don't support persistent sessions in any keysystem, so all calls + // to Load() should reject with a TypeError in the preceding check. Omitting + // implementing the rest of the specified MediaKeySession::Load() algorithm. + + // We now know the sessionId being loaded into this session. Remove the + // session from its owning MediaKey's set of sessions awaiting a sessionId. + RefPtr session(mKeys->GetPendingSession(Token())); + MOZ_ASSERT(session == this, "Session should be awaiting id on its own token"); + + // Associate with the known sessionId. + SetSessionId(aSessionId); + + PromiseId pid = mKeys->StorePromise(promise); + mKeys->GetCDMProxy()->LoadSession(pid, mSessionType, aSessionId); + + EME_LOG("MediaKeySession[%p,'%s'] Load() sent to CDM, promiseId=%d", this, + NS_ConvertUTF16toUTF8(mSessionId).get(), pid); + + return promise.forget(); +} + +already_AddRefed MediaKeySession::Update( + const ArrayBufferViewOrArrayBuffer& aResponse, ErrorResult& aRv) { + RefPtr promise( + MakePromise(aRv, "MediaKeySession.update"_ns)); + if (aRv.Failed()) { + return nullptr; + } + + if (!IsCallable()) { + // If this object's callable value is false, return a promise rejected + // with a new DOMException whose name is InvalidStateError. + EME_LOG( + "MediaKeySession[%p,''] Update() called before sessionId set by CDM", + this); + promise->MaybeRejectWithInvalidStateError( + "MediaKeySession.Update() called before sessionId set by CDM"); + return promise.forget(); + } + + nsTArray data; + if (IsClosed() || !mKeys->GetCDMProxy()) { + promise->MaybeRejectWithInvalidStateError( + "Session is closed or was not properly initialized"); + EME_LOG( + "MediaKeySession[%p,'%s'] Update() failed, session is closed or was " + "not properly initialised.", + this, NS_ConvertUTF16toUTF8(mSessionId).get()); + return promise.forget(); + } + CopyArrayBufferViewOrArrayBufferData(aResponse, data); + if (data.IsEmpty()) { + promise->MaybeRejectWithTypeError( + "Empty response buffer passed to MediaKeySession.update()"); + EME_LOG("MediaKeySession[%p,'%s'] Update() failed, empty response buffer", + this, NS_ConvertUTF16toUTF8(mSessionId).get()); + return promise.forget(); + } + + // Convert response to hex for easier logging. + // Note: UpdateSession() std::move()s the data out of the array, so we have + // to copy it here. + nsAutoCString hexResponse(ToHexString(data)); + + PromiseId pid = mKeys->StorePromise(promise); + mKeys->GetCDMProxy()->UpdateSession(mSessionId, pid, data); + + EME_LOG( + "MediaKeySession[%p,'%s'] Update() sent to CDM, " + "promiseId=%d Response='%s'", + this, NS_ConvertUTF16toUTF8(mSessionId).get(), pid, hexResponse.get()); + + return promise.forget(); +} + +already_AddRefed MediaKeySession::Close(ErrorResult& aRv) { + RefPtr promise(MakePromise(aRv, "MediaKeySession.close"_ns)); + if (aRv.Failed()) { + return nullptr; + } + // 1. Let session be the associated MediaKeySession object. + // 2. If session is closed, return a resolved promise. + if (IsClosed()) { + EME_LOG("MediaKeySession[%p,'%s'] Close() already closed", this, + NS_ConvertUTF16toUTF8(mSessionId).get()); + promise->MaybeResolveWithUndefined(); + return promise.forget(); + } + // 3. If session's callable value is false, return a promise rejected + // with an InvalidStateError. + if (!IsCallable()) { + EME_LOG("MediaKeySession[%p,''] Close() called before sessionId set by CDM", + this); + promise->MaybeRejectWithInvalidStateError( + "MediaKeySession.Close() called before sessionId set by CDM"); + return promise.forget(); + } + if (!mKeys->GetCDMProxy()) { + EME_LOG("MediaKeySession[%p,'%s'] Close() null CDMProxy", this, + NS_ConvertUTF16toUTF8(mSessionId).get()); + promise->MaybeRejectWithInvalidStateError( + "MediaKeySession.Close() lost reference to CDM"); + return promise.forget(); + } + // 4. Let promise be a new promise. + PromiseId pid = mKeys->StorePromise(promise); + // 5. Run the following steps in parallel: + // 5.1 Let cdm be the CDM instance represented by session's cdm instance + // value. 5.2 Use cdm to close the session associated with session. + mKeys->GetCDMProxy()->CloseSession(mSessionId, pid); + + EME_LOG("MediaKeySession[%p,'%s'] Close() sent to CDM, promiseId=%d", this, + NS_ConvertUTF16toUTF8(mSessionId).get(), pid); + + // Session Closed algorithm is run when CDM causes us to run + // OnSessionClosed(). + + // 6. Return promise. + return promise.forget(); +} + +void MediaKeySession::OnClosed() { + if (IsClosed()) { + return; + } + EME_LOG("MediaKeySession[%p,'%s'] session close operation complete.", this, + NS_ConvertUTF16toUTF8(mSessionId).get()); + mIsClosed = true; + mKeys->OnSessionClosed(this); + mKeys = nullptr; + mClosed->MaybeResolveWithUndefined(); +} + +bool MediaKeySession::IsClosed() const { return mIsClosed; } + +already_AddRefed MediaKeySession::Remove(ErrorResult& aRv) { + RefPtr promise( + MakePromise(aRv, "MediaKeySession.remove"_ns)); + if (aRv.Failed()) { + return nullptr; + } + if (!IsCallable()) { + // If this object's callable value is false, return a promise rejected + // with a new DOMException whose name is InvalidStateError. + EME_LOG( + "MediaKeySession[%p,''] Remove() called before sessionId set by CDM", + this); + promise->MaybeRejectWithInvalidStateError( + "MediaKeySession.Remove() called before sessionId set by CDM"); + return promise.forget(); + } + if (mSessionType != MediaKeySessionType::Persistent_license) { + promise->MaybeRejectWithInvalidAccessError( + "Calling MediaKeySession.remove() on non-persistent session"); + // "The operation is not supported on session type sessions." + EME_LOG("MediaKeySession[%p,'%s'] Remove() failed, sesion not persisrtent.", + this, NS_ConvertUTF16toUTF8(mSessionId).get()); + return promise.forget(); + } + if (IsClosed() || !mKeys->GetCDMProxy()) { + promise->MaybeRejectWithInvalidStateError( + "MediaKeySession.remove() called but session is not active"); + // "The session is closed." + EME_LOG("MediaKeySession[%p,'%s'] Remove() failed, already session closed.", + this, NS_ConvertUTF16toUTF8(mSessionId).get()); + return promise.forget(); + } + PromiseId pid = mKeys->StorePromise(promise); + mKeys->GetCDMProxy()->RemoveSession(mSessionId, pid); + EME_LOG("MediaKeySession[%p,'%s'] Remove() sent to CDM, promiseId=%d.", this, + NS_ConvertUTF16toUTF8(mSessionId).get(), pid); + + return promise.forget(); +} + +void MediaKeySession::DispatchKeyMessage(MediaKeyMessageType aMessageType, + const nsTArray& aMessage) { + if (EME_LOG_ENABLED()) { + EME_LOG( + "MediaKeySession[%p,'%s'] DispatchKeyMessage() type=%s message='%s'", + this, NS_ConvertUTF16toUTF8(mSessionId).get(), + nsCString(MediaKeyMessageTypeValues::GetString(aMessageType)).get(), + ToHexString(aMessage).get()); + } + + RefPtr event( + MediaKeyMessageEvent::Constructor(this, aMessageType, aMessage)); + RefPtr asyncDispatcher = + new AsyncEventDispatcher(this, event.forget()); + asyncDispatcher->PostDOMEvent(); +} + +void MediaKeySession::DispatchKeyError(uint32_t aSystemCode) { + EME_LOG("MediaKeySession[%p,'%s'] DispatchKeyError() systemCode=%u.", this, + NS_ConvertUTF16toUTF8(mSessionId).get(), aSystemCode); + + auto event = MakeRefPtr(this, aSystemCode); + RefPtr asyncDispatcher = + new AsyncEventDispatcher(this, event.forget()); + asyncDispatcher->PostDOMEvent(); +} + +void MediaKeySession::DispatchKeyStatusesChange() { + if (IsClosed()) { + return; + } + + UpdateKeyStatusMap(); + + RefPtr asyncDispatcher = + new AsyncEventDispatcher(this, u"keystatuseschange"_ns, CanBubble::eNo); + asyncDispatcher->PostDOMEvent(); +} + +uint32_t MediaKeySession::Token() const { return mToken; } + +already_AddRefed MediaKeySession::MakePromise( + ErrorResult& aRv, const nsACString& aName) { + nsCOMPtr global = GetParentObject(); + if (!global) { + NS_WARNING("Passed non-global to MediaKeys ctor!"); + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + return DetailedPromise::Create(global, aRv, aName); +} + +void MediaKeySession::SetExpiration(double aExpiration) { + EME_LOG("MediaKeySession[%p,'%s'] SetExpiry(%.12lf) (%.2lf hours from now)", + this, NS_ConvertUTF16toUTF8(mSessionId).get(), aExpiration, + (aExpiration - 1000.0 * double(time(0))) / (1000.0 * 60 * 60)); + mExpiration = aExpiration; +} + +EventHandlerNonNull* MediaKeySession::GetOnkeystatuseschange() { + return GetEventHandler(nsGkAtoms::onkeystatuseschange); +} + +void MediaKeySession::SetOnkeystatuseschange(EventHandlerNonNull* aCallback) { + SetEventHandler(nsGkAtoms::onkeystatuseschange, aCallback); +} + +EventHandlerNonNull* MediaKeySession::GetOnmessage() { + return GetEventHandler(nsGkAtoms::onmessage); +} + +void MediaKeySession::SetOnmessage(EventHandlerNonNull* aCallback) { + SetEventHandler(nsGkAtoms::onmessage, aCallback); +} + +nsCString ToCString(MediaKeySessionType aType) { + return nsCString(MediaKeySessionTypeValues::GetString(aType)); +} + +nsString ToString(MediaKeySessionType aType) { + return NS_ConvertUTF8toUTF16(ToCString(aType)); +} + +} // namespace mozilla::dom diff --git a/dom/media/eme/MediaKeySession.h b/dom/media/eme/MediaKeySession.h new file mode 100644 index 0000000000..e19488c311 --- /dev/null +++ b/dom/media/eme/MediaKeySession.h @@ -0,0 +1,142 @@ +/* -*- 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 mozilla_dom_MediaKeySession_h +#define mozilla_dom_MediaKeySession_h + +#include "DecoderDoctorLogger.h" +#include "mozilla/Attributes.h" +#include "nsCycleCollectionParticipant.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "nsCOMPtr.h" +#include "mozilla/dom/TypedArray.h" +#include "mozilla/Mutex.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/DetailedPromise.h" +#include "mozilla/dom/MediaKeySessionBinding.h" +#include "mozilla/dom/MediaKeysBinding.h" +#include "mozilla/dom/MediaKeyMessageEventBinding.h" + +struct JSContext; + +namespace mozilla { +class ErrorResult; + +namespace dom { +class MediaKeySession; +} // namespace dom +DDLoggedTypeName(dom::MediaKeySession); + +namespace dom { + +class ArrayBufferViewOrArrayBuffer; +class MediaKeyError; +class MediaKeyStatusMap; + +nsCString ToCString(MediaKeySessionType aType); + +nsString ToString(MediaKeySessionType aType); + +class MediaKeySession final : public DOMEventTargetHelper, + public DecoderDoctorLifeLogger { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MediaKeySession, + DOMEventTargetHelper) + public: + MediaKeySession(nsPIDOMWindowInner* aParent, MediaKeys* aKeys, + const nsAString& aKeySystem, MediaKeySessionType aSessionType, + ErrorResult& aRv); + + void SetSessionId(const nsAString& aSessionId); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + // Mark this as resultNotAddRefed to return raw pointers + MediaKeyError* GetError() const; + + MediaKeyStatusMap* KeyStatuses() const; + + void GetSessionId(nsString& aRetval) const; + + const nsString& GetSessionId() const; + + // Number of ms since epoch at which expiration occurs, or NaN if unknown. + // TODO: The type of this attribute is still under contention. + // https://www.w3.org/Bugs/Public/show_bug.cgi?id=25902 + double Expiration() const; + + Promise* Closed() const; + + already_AddRefed GenerateRequest( + const nsAString& aInitDataType, + const ArrayBufferViewOrArrayBuffer& aInitData, ErrorResult& aRv); + + already_AddRefed Load(const nsAString& aSessionId, ErrorResult& aRv); + + already_AddRefed Update(const ArrayBufferViewOrArrayBuffer& response, + ErrorResult& aRv); + + already_AddRefed Close(ErrorResult& aRv); + + already_AddRefed Remove(ErrorResult& aRv); + + void DispatchKeyMessage(MediaKeyMessageType aMessageType, + const nsTArray& aMessage); + + void DispatchKeyError(uint32_t system_code); + + void DispatchKeyStatusesChange(); + + void OnClosed(); + + bool IsClosed() const; + + void SetExpiration(double aExpiry); + + mozilla::dom::EventHandlerNonNull* GetOnkeystatuseschange(); + void SetOnkeystatuseschange(mozilla::dom::EventHandlerNonNull* aCallback); + + mozilla::dom::EventHandlerNonNull* GetOnmessage(); + void SetOnmessage(mozilla::dom::EventHandlerNonNull* aCallback); + + // Process-unique identifier. + uint32_t Token() const; + + private: + ~MediaKeySession(); + + void UpdateKeyStatusMap(); + + bool IsCallable() const { + // The EME spec sets the "callable value" to true whenever the CDM sets + // the sessionId. When the session is initialized, sessionId is empty and + // callable is thus false. + return !mSessionId.IsEmpty(); + } + + already_AddRefed MakePromise(ErrorResult& aRv, + const nsACString& aName); + + RefPtr mClosed; + + RefPtr mMediaKeyError; + RefPtr mKeys; + const nsString mKeySystem; + nsString mSessionId; + const MediaKeySessionType mSessionType; + const uint32_t mToken; + bool mIsClosed; + bool mUninitialized; + RefPtr mKeyStatusMap; + double mExpiration; +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/media/eme/MediaKeyStatusMap.cpp b/dom/media/eme/MediaKeyStatusMap.cpp new file mode 100644 index 0000000000..6ee9d76b77 --- /dev/null +++ b/dom/media/eme/MediaKeyStatusMap.cpp @@ -0,0 +1,99 @@ +/* -*- 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 "mozilla/dom/MediaKeyStatusMap.h" +#include "nsPIDOMWindow.h" +#include "mozilla/dom/UnionTypes.h" +#include "mozilla/dom/ToJSValue.h" +#include "mozilla/EMEUtils.h" +#include "GMPUtils.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaKeyStatusMap) +NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaKeyStatusMap) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeyStatusMap) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MediaKeyStatusMap, mParent) + +MediaKeyStatusMap::MediaKeyStatusMap(nsPIDOMWindowInner* aParent) + : mParent(aParent) {} + +MediaKeyStatusMap::~MediaKeyStatusMap() = default; + +JSObject* MediaKeyStatusMap::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return MediaKeyStatusMap_Binding::Wrap(aCx, this, aGivenProto); +} + +nsPIDOMWindowInner* MediaKeyStatusMap::GetParentObject() const { + return mParent; +} + +const MediaKeyStatusMap::KeyStatus* MediaKeyStatusMap::FindKey( + const ArrayBufferViewOrArrayBuffer& aKey) const { + MOZ_ASSERT(aKey.IsArrayBuffer() || aKey.IsArrayBufferView()); + + return ProcessTypedArrays(aKey, + [&](const Span& aData, + JS::AutoCheckCannotGC&&) -> const KeyStatus* { + for (const KeyStatus& status : mStatuses) { + if (aData == Span(status.mKeyId)) { + return &status; + } + } + return nullptr; + }); +} + +void MediaKeyStatusMap::Get(const ArrayBufferViewOrArrayBuffer& aKey, + OwningMediaKeyStatusOrUndefined& aOutValue, + ErrorResult& aOutRv) const { + const KeyStatus* status = FindKey(aKey); + if (!status) { + aOutValue.SetUndefined(); + return; + } + + aOutValue.SetAsMediaKeyStatus() = status->mStatus; +} + +bool MediaKeyStatusMap::Has(const ArrayBufferViewOrArrayBuffer& aKey) const { + return FindKey(aKey); +} + +uint32_t MediaKeyStatusMap::GetIterableLength() const { + return mStatuses.Length(); +} + +TypedArrayCreator MediaKeyStatusMap::GetKeyAtIndex( + uint32_t aIndex) const { + MOZ_ASSERT(aIndex < GetIterableLength()); + return TypedArrayCreator(mStatuses[aIndex].mKeyId); +} + +nsString MediaKeyStatusMap::GetKeyIDAsHexString(uint32_t aIndex) const { + MOZ_ASSERT(aIndex < GetIterableLength()); + return NS_ConvertUTF8toUTF16(ToHexString(mStatuses[aIndex].mKeyId)); +} + +MediaKeyStatus MediaKeyStatusMap::GetValueAtIndex(uint32_t aIndex) const { + MOZ_ASSERT(aIndex < GetIterableLength()); + return mStatuses[aIndex].mStatus; +} + +uint32_t MediaKeyStatusMap::Size() const { return mStatuses.Length(); } + +void MediaKeyStatusMap::Update(const nsTArray& aKeys) { + mStatuses.Clear(); + for (const auto& key : aKeys) { + mStatuses.InsertElementSorted(KeyStatus(key.mId, key.mStatus)); + } +} + +} // namespace mozilla::dom diff --git a/dom/media/eme/MediaKeyStatusMap.h b/dom/media/eme/MediaKeyStatusMap.h new file mode 100644 index 0000000000..1ffab4331e --- /dev/null +++ b/dom/media/eme/MediaKeyStatusMap.h @@ -0,0 +1,94 @@ +/* -*- 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 mozilla_dom_MediaKeyStatuses_h +#define mozilla_dom_MediaKeyStatuses_h + +#include "mozilla/Attributes.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" + +#include "mozilla/dom/TypedArray.h" +#include "mozilla/dom/MediaKeyStatusMapBinding.h" +#include "mozilla/CDMCaps.h" + +class nsPIDOMWindowInner; + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class ArrayBufferViewOrArrayBuffer; + +// The MediaKeyStatusMap WebIDL interface; maps a keyId to its status. +// Note that the underlying "map" is stored in an array, since we assume +// that a MediaKeySession won't have many key statuses to report. +class MediaKeyStatusMap final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(MediaKeyStatusMap) + + public: + explicit MediaKeyStatusMap(nsPIDOMWindowInner* aParent); + + protected: + ~MediaKeyStatusMap(); + + public: + nsPIDOMWindowInner* GetParentObject() const; + + JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + void Get(const ArrayBufferViewOrArrayBuffer& aKey, + OwningMediaKeyStatusOrUndefined& aOutValue, + ErrorResult& aOutRv) const; + bool Has(const ArrayBufferViewOrArrayBuffer& aKey) const; + uint32_t Size() const; + + uint32_t GetIterableLength() const; + TypedArrayCreator GetKeyAtIndex(uint32_t aIndex) const; + nsString GetKeyIDAsHexString(uint32_t aIndex) const; + MediaKeyStatus GetValueAtIndex(uint32_t aIndex) const; + + void Update(const nsTArray& keys); + + private: + nsCOMPtr mParent; + + struct KeyStatus { + KeyStatus(const nsTArray& aKeyId, MediaKeyStatus aStatus) + : mKeyId(aKeyId.Clone()), mStatus(aStatus) {} + bool operator==(const KeyStatus& aOther) const { + return aOther.mKeyId == mKeyId; + } + bool operator<(const KeyStatus& aOther) const { + // Copy chromium and compare keys' bytes. + // Update once https://github.com/w3c/encrypted-media/issues/69 + // is resolved. + const nsTArray& other = aOther.mKeyId; + const nsTArray& self = mKeyId; + size_t length = std::min(other.Length(), self.Length()); + int cmp = memcmp(self.Elements(), other.Elements(), length); + if (cmp != 0) { + return cmp < 0; + } + return self.Length() <= other.Length(); + } + nsTArray mKeyId; + MediaKeyStatus mStatus; + }; + + const KeyStatus* FindKey(const ArrayBufferViewOrArrayBuffer& aKey) const; + + nsTArray mStatuses; +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/media/eme/MediaKeySystemAccess.cpp b/dom/media/eme/MediaKeySystemAccess.cpp new file mode 100644 index 0000000000..b58ff76424 --- /dev/null +++ b/dom/media/eme/MediaKeySystemAccess.cpp @@ -0,0 +1,1161 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "mozilla/dom/MediaKeySystemAccess.h" + +#include + +#include "DecoderDoctorDiagnostics.h" +#include "DecoderTraits.h" +#include "KeySystemConfig.h" +#include "MP4Decoder.h" +#include "MediaContainerType.h" +#include "WebMDecoder.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/EMEUtils.h" +#include "mozilla/Preferences.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPrefs_media.h" +#include "mozilla/dom/KeySystemNames.h" +#include "mozilla/dom/MediaKeySession.h" +#include "mozilla/dom/MediaKeySystemAccessBinding.h" +#include "mozilla/dom/MediaKeySystemAccessManager.h" +#include "mozilla/dom/MediaSource.h" +#include "nsDOMString.h" +#include "nsIObserverService.h" +#include "nsMimeTypes.h" +#include "nsReadableUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsUnicharUtils.h" + +#ifdef XP_WIN +# include "WMFDecoderModule.h" +#endif +#ifdef MOZ_WIDGET_ANDROID +# include "mozilla/java/MediaDrmProxyWrappers.h" +#endif + +namespace mozilla::dom { + +#define LOG(msg, ...) \ + EME_LOG("MediaKeySystemAccess::%s " msg, __func__, ##__VA_ARGS__) + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MediaKeySystemAccess, mParent) +NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaKeySystemAccess) +NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaKeySystemAccess) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeySystemAccess) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +static nsCString ToCString(const MediaKeySystemConfiguration& aConfig); + +MediaKeySystemAccess::MediaKeySystemAccess( + nsPIDOMWindowInner* aParent, const nsAString& aKeySystem, + const MediaKeySystemConfiguration& aConfig) + : mParent(aParent), mKeySystem(aKeySystem), mConfig(aConfig) { + LOG("Created MediaKeySystemAccess for keysystem=%s config=%s", + NS_ConvertUTF16toUTF8(mKeySystem).get(), + mozilla::dom::ToCString(mConfig).get()); +} + +MediaKeySystemAccess::~MediaKeySystemAccess() = default; + +JSObject* MediaKeySystemAccess::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return MediaKeySystemAccess_Binding::Wrap(aCx, this, aGivenProto); +} + +nsPIDOMWindowInner* MediaKeySystemAccess::GetParentObject() const { + return mParent; +} + +void MediaKeySystemAccess::GetKeySystem(nsString& aOutKeySystem) const { + aOutKeySystem.Assign(mKeySystem); +} + +void MediaKeySystemAccess::GetConfiguration( + MediaKeySystemConfiguration& aConfig) { + aConfig = mConfig; +} + +already_AddRefed MediaKeySystemAccess::CreateMediaKeys( + ErrorResult& aRv) { + RefPtr keys(new MediaKeys(mParent, mKeySystem, mConfig)); + return keys->Init(aRv); +} + +enum class SecureLevel { + Software, + Hardware, +}; + +static MediaKeySystemStatus EnsureCDMInstalled(const nsAString& aKeySystem, + const SecureLevel aSecure, + nsACString& aOutMessage) { + if (aSecure == SecureLevel::Software && + !KeySystemConfig::Supports(aKeySystem)) { + aOutMessage = "CDM is not installed"_ns; + return MediaKeySystemStatus::Cdm_not_installed; + } + +#ifdef MOZ_WMF_CDM + if (aSecure == SecureLevel::Hardware) { + // Ensure we check the hardware key system name. + nsAutoString hardwareKeySystem; + if (IsWidevineKeySystem(aKeySystem) || + IsWidevineExperimentKeySystemAndSupported(aKeySystem)) { + hardwareKeySystem = + NS_ConvertUTF8toUTF16(kWidevineExperimentKeySystemName); + } else if (IsPlayReadyKeySystemAndSupported(aKeySystem)) { + hardwareKeySystem = NS_ConvertUTF8toUTF16(kPlayReadyKeySystemHardware); + } else { + MOZ_ASSERT_UNREACHABLE("Not supported key system for HWDRM!"); + } + if (!KeySystemConfig::Supports(hardwareKeySystem)) { + aOutMessage = "CDM is not installed"_ns; + return MediaKeySystemStatus::Cdm_not_installed; + } + } +#endif + + return MediaKeySystemStatus::Available; +} + +/* static */ +MediaKeySystemStatus MediaKeySystemAccess::GetKeySystemStatus( + const MediaKeySystemAccessRequest& aRequest, nsACString& aOutMessage) { + const nsString& keySystem = aRequest.mKeySystem; + + MOZ_ASSERT(StaticPrefs::media_eme_enabled() || + IsClearkeyKeySystem(keySystem)); + + LOG("checking if CDM is installed or disabled for %s", + NS_ConvertUTF16toUTF8(keySystem).get()); + if (IsClearkeyKeySystem(keySystem)) { + return EnsureCDMInstalled(keySystem, SecureLevel::Software, aOutMessage); + } + + // This is used to determine if we need to download Widevine L1. + bool shouldCheckL1Installation = false; +#ifdef MOZ_WMF_CDM + if (StaticPrefs::media_eme_widevine_experiment_enabled()) { + shouldCheckL1Installation = + CheckIfHarewareDRMConfigExists(aRequest.mConfigs) || + IsWidevineExperimentKeySystemAndSupported(keySystem); + } +#endif + + // Check Widevine L3 + if (IsWidevineKeySystem(keySystem) && !shouldCheckL1Installation) { + if (Preferences::GetBool("media.gmp-widevinecdm.visible", false)) { + if (!Preferences::GetBool("media.gmp-widevinecdm.enabled", false)) { + aOutMessage = "Widevine EME disabled"_ns; + return MediaKeySystemStatus::Cdm_disabled; + } + return EnsureCDMInstalled(keySystem, SecureLevel::Software, aOutMessage); +#ifdef MOZ_WIDGET_ANDROID + } else if (Preferences::GetBool("media.mediadrm-widevinecdm.visible", + false)) { + bool supported = + mozilla::java::MediaDrmProxy::IsSchemeSupported(keySystem); + if (!supported) { + aOutMessage = nsLiteralCString( + "KeySystem or Minimum API level not met for Widevine EME"); + return MediaKeySystemStatus::Cdm_not_supported; + } + return MediaKeySystemStatus::Available; +#endif + } + } + +#ifdef MOZ_WMF_CDM + // Check PlayReady, which is a built-in CDM on most Windows versions. + if (IsPlayReadyKeySystemAndSupported(keySystem) && + KeySystemConfig::Supports(keySystem)) { + return MediaKeySystemStatus::Available; + } + + // Check Widevine L1. This can be a request for experimental key systems, or + // for a normal key system with hardware robustness. + if ((IsWidevineExperimentKeySystemAndSupported(keySystem) || + IsWidevineKeySystem(keySystem)) && + shouldCheckL1Installation) { + // TODO : if L3 hasn't been installed as well, should we fallback to install + // L3? + if (!Preferences::GetBool("media.gmp-widevinecdm-l1.enabled", false)) { + aOutMessage = "Widevine L1 EME disabled"_ns; + return MediaKeySystemStatus::Cdm_disabled; + } + return EnsureCDMInstalled(keySystem, SecureLevel::Hardware, aOutMessage); + } +#endif + + return MediaKeySystemStatus::Cdm_not_supported; +} + +static KeySystemConfig::EMECodecString ToEMEAPICodecString( + const nsString& aCodec) { + if (IsAACCodecString(aCodec)) { + return KeySystemConfig::EME_CODEC_AAC; + } + if (aCodec.EqualsLiteral("opus")) { + return KeySystemConfig::EME_CODEC_OPUS; + } + if (aCodec.EqualsLiteral("vorbis")) { + return KeySystemConfig::EME_CODEC_VORBIS; + } + if (aCodec.EqualsLiteral("flac")) { + return KeySystemConfig::EME_CODEC_FLAC; + } + if (IsH264CodecString(aCodec)) { + return KeySystemConfig::EME_CODEC_H264; + } + if (IsVP8CodecString(aCodec)) { + return KeySystemConfig::EME_CODEC_VP8; + } + if (IsVP9CodecString(aCodec)) { + return KeySystemConfig::EME_CODEC_VP9; + } +#ifdef MOZ_WMF + if (IsH265CodecString(aCodec)) { + return KeySystemConfig::EME_CODEC_HEVC; + } +#endif + return ""_ns; +} + +static nsTArray GetSupportedKeySystems() { + nsTArray keySystemConfigs; + + const nsTArray keySystemNames{ + NS_ConvertUTF8toUTF16(kClearKeyKeySystemName), + NS_ConvertUTF8toUTF16(kWidevineKeySystemName), +#ifdef MOZ_WMF_CDM + NS_ConvertUTF8toUTF16(kPlayReadyKeySystemName), + NS_ConvertUTF8toUTF16(kPlayReadyKeySystemHardware), + NS_ConvertUTF8toUTF16(kPlayReadyHardwareClearLeadKeySystemName), + NS_ConvertUTF8toUTF16(kWidevineExperimentKeySystemName), + NS_ConvertUTF8toUTF16(kWidevineExperiment2KeySystemName), +#endif + }; + for (const auto& name : keySystemNames) { + Unused << KeySystemConfig::CreateKeySystemConfigs(name, keySystemConfigs); + } + return keySystemConfigs; +} + +static bool GetKeySystemConfigs( + const nsAString& aKeySystem, + nsTArray& aOutKeySystemConfig) { + bool foundConfigs = false; + for (auto& config : GetSupportedKeySystems()) { + if (config.IsSameKeySystem(aKeySystem)) { + aOutKeySystemConfig.AppendElement(std::move(config)); + foundConfigs = true; + } + } + return foundConfigs; +} + +/* static */ +bool MediaKeySystemAccess::KeySystemSupportsInitDataType( + const nsAString& aKeySystem, const nsAString& aInitDataType) { + nsTArray implementations; + GetKeySystemConfigs(aKeySystem, implementations); + bool containInitType = false; + for (const auto& config : implementations) { + if (config.mInitDataTypes.Contains(aInitDataType)) { + containInitType = true; + break; + } + } + return containInitType; +} + +enum CodecType { Audio, Video, Invalid }; + +static bool CanDecryptAndDecode( + const nsString& aKeySystem, const nsString& aContentType, + CodecType aCodecType, + const KeySystemConfig::ContainerSupport& aContainerSupport, + const nsTArray& aCodecs, + DecoderDoctorDiagnostics* aDiagnostics) { + MOZ_ASSERT(aCodecType != Invalid); + for (const KeySystemConfig::EMECodecString& codec : aCodecs) { + MOZ_ASSERT(!codec.IsEmpty()); + + if (aContainerSupport.DecryptsAndDecodes(codec)) { + // GMP can decrypt-and-decode this codec. + continue; + } + + if (aContainerSupport.Decrypts(codec)) { + IgnoredErrorResult rv; + MediaSource::IsTypeSupported(aContentType, aDiagnostics, rv); + if (!rv.Failed()) { + // GMP can decrypt and is allowed to return compressed samples to + // Gecko to decode, and Gecko has a decoder. + continue; + } + } + + // Neither the GMP nor Gecko can both decrypt and decode. We don't + // support this codec. + +#if defined(XP_WIN) + // Widevine CDM doesn't include an AAC decoder. So if WMF can't + // decode AAC, and a codec wasn't specified, be conservative + // and reject the MediaKeys request, since we assume Widevine + // will be used with AAC. + if (codec == KeySystemConfig::EME_CODEC_AAC && + IsWidevineKeySystem(aKeySystem) && + !WMFDecoderModule::CanCreateMFTDecoder(WMFStreamType::AAC)) { + if (aDiagnostics) { + aDiagnostics->SetKeySystemIssue( + DecoderDoctorDiagnostics::eWidevineWithNoWMF); + } + } +#endif + return false; + } + return true; +} + +// Returns if an encryption scheme is supported per: +// https://github.com/WICG/encrypted-media-encryption-scheme/blob/master/explainer.md +// To be supported the scheme should be one of: +// - null +// - missing (which will result in the nsString being set to void and thus null) +// - one of the schemes supported by the CDM +// If the pref to enable this behavior is not set, then the value should be +// empty/null, as the dict member will not be exposed. In this case we will +// always report support as we would before this feature was implemented. +static bool SupportsEncryptionScheme( + const nsString& aEncryptionScheme, + const nsTArray& aSupportedEncryptionSchemes) { + MOZ_ASSERT( + DOMStringIsNull(aEncryptionScheme) || + StaticPrefs::media_eme_encrypted_media_encryption_scheme_enabled(), + "Encryption scheme checking support must be preffed on for " + "encryptionScheme to be a non-null string"); + if (DOMStringIsNull(aEncryptionScheme)) { + // "A missing or null value indicates that any encryption scheme is + // acceptable." + return true; + } + return aSupportedEncryptionSchemes.Contains(aEncryptionScheme); +} + +static bool ToSessionType(const nsAString& aSessionType, + MediaKeySessionType& aOutType) { + if (aSessionType.Equals(ToString(MediaKeySessionType::Temporary))) { + aOutType = MediaKeySessionType::Temporary; + return true; + } + if (aSessionType.Equals(ToString(MediaKeySessionType::Persistent_license))) { + aOutType = MediaKeySessionType::Persistent_license; + return true; + } + return false; +} + +// 5.1.1 Is persistent session type? +static bool IsPersistentSessionType(MediaKeySessionType aSessionType) { + return aSessionType == MediaKeySessionType::Persistent_license; +} + +static bool ContainsSessionType( + const nsTArray& aTypes, + const MediaKeySessionType& aSessionType) { + return (aSessionType == MediaKeySessionType::Persistent_license && + aTypes.Contains(KeySystemConfig::SessionType::PersistentLicense)) || + (aSessionType == MediaKeySessionType::Temporary && + aTypes.Contains(KeySystemConfig::SessionType::Temporary)); +} + +CodecType GetMajorType(const MediaMIMEType& aMIMEType) { + if (aMIMEType.HasAudioMajorType()) { + return Audio; + } + if (aMIMEType.HasVideoMajorType()) { + return Video; + } + return Invalid; +} + +static CodecType GetCodecType(const KeySystemConfig::EMECodecString& aCodec) { + if (aCodec.Equals(KeySystemConfig::EME_CODEC_AAC) || + aCodec.Equals(KeySystemConfig::EME_CODEC_OPUS) || + aCodec.Equals(KeySystemConfig::EME_CODEC_VORBIS) || + aCodec.Equals(KeySystemConfig::EME_CODEC_FLAC)) { + return Audio; + } + if (aCodec.Equals(KeySystemConfig::EME_CODEC_H264) || + aCodec.Equals(KeySystemConfig::EME_CODEC_VP8) || + aCodec.Equals(KeySystemConfig::EME_CODEC_VP9) || + aCodec.Equals(KeySystemConfig::EME_CODEC_HEVC)) { + return Video; + } + return Invalid; +} + +static bool AllCodecsOfType( + const nsTArray& aCodecs, + const CodecType aCodecType) { + for (const KeySystemConfig::EMECodecString& codec : aCodecs) { + if (GetCodecType(codec) != aCodecType) { + return false; + } + } + return true; +} + +static bool IsParameterUnrecognized(const nsAString& aContentType) { + nsAutoString contentType(aContentType); + contentType.StripWhitespace(); + + nsTArray params; + nsAString::const_iterator start, end, semicolon, equalSign; + contentType.BeginReading(start); + contentType.EndReading(end); + semicolon = start; + // Find any substring between ';' & '='. + while (semicolon != end) { + if (FindCharInReadable(';', semicolon, end)) { + equalSign = ++semicolon; + if (FindCharInReadable('=', equalSign, end)) { + params.AppendElement(Substring(semicolon, equalSign)); + semicolon = equalSign; + } + } + } + + for (auto param : params) { + if (!param.LowerCaseEqualsLiteral("codecs") && + !param.LowerCaseEqualsLiteral("profiles")) { + return true; + } + } + return false; +} + +// 3.1.1.3 Get Supported Capabilities for Audio/Video Type +static Sequence GetSupportedCapabilities( + const CodecType aCodecType, + const nsTArray& aRequestedCapabilities, + const MediaKeySystemConfiguration& aPartialConfig, + const KeySystemConfig& aKeySystem, DecoderDoctorDiagnostics* aDiagnostics, + const std::function& aDeprecationLogFn) { + // Let local accumulated configuration be a local copy of partial + // configuration. (Note: It's not necessary for us to maintain a local copy, + // as we don't need to test whether capabilites from previous calls to this + // algorithm work with the capabilities currently being considered in this + // call. ) + + // Let supported media capabilities be an empty sequence of + // MediaKeySystemMediaCapability dictionaries. + Sequence supportedCapabilities; + + // For each requested media capability in requested media capabilities: + for (const MediaKeySystemMediaCapability& capabilities : + aRequestedCapabilities) { + // Let content type be requested media capability's contentType member. + const nsString& contentTypeString = capabilities.mContentType; + // Let robustness be requested media capability's robustness member. + const nsString& robustness = capabilities.mRobustness; + // Optional encryption scheme extension, see + // https://github.com/WICG/encrypted-media-encryption-scheme/blob/master/explainer.md + // This will only be exposed to JS if + // media.eme.encrypted-media-encryption-scheme.enabled is preffed on. + const nsString encryptionScheme = capabilities.mEncryptionScheme; + // If content type is the empty string, return null. + if (contentTypeString.IsEmpty()) { + EME_LOG( + "MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s','%s') rejected; " + "audio or video capability has empty contentType.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentTypeString).get(), + NS_ConvertUTF16toUTF8(robustness).get(), + NS_ConvertUTF16toUTF8(encryptionScheme).get()); + return Sequence(); + } + // If content type is an invalid or unrecognized MIME type, continue + // to the next iteration. + Maybe maybeContainerType = + MakeMediaContainerType(contentTypeString); + if (!maybeContainerType) { + EME_LOG( + "MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; " + "failed to parse contentTypeString as MIME type.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentTypeString).get(), + NS_ConvertUTF16toUTF8(robustness).get(), + NS_ConvertUTF16toUTF8(encryptionScheme).get()); + continue; + } + const MediaContainerType& containerType = *maybeContainerType; + bool invalid = false; + nsTArray codecs; + for (const auto& codecString : + containerType.ExtendedType().Codecs().Range()) { + KeySystemConfig::EMECodecString emeCodec = + ToEMEAPICodecString(nsString(codecString)); + if (emeCodec.IsEmpty()) { + invalid = true; + EME_LOG( + "MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; " + "'%s' is an invalid codec string.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentTypeString).get(), + NS_ConvertUTF16toUTF8(robustness).get(), + NS_ConvertUTF16toUTF8(encryptionScheme).get(), + NS_ConvertUTF16toUTF8(codecString).get()); + break; + } + codecs.AppendElement(emeCodec); + } + if (invalid) { + continue; + } + + // If the user agent does not support container, continue to the next + // iteration. The case-sensitivity of string comparisons is determined by + // the appropriate RFC. (Note: Per RFC 6838 [RFC6838], "Both top-level type + // and subtype names are case-insensitive."'. We're using + // nsContentTypeParser and that is case-insensitive and converts all its + // parameter outputs to lower case.) + const bool supportedInMP4 = + MP4Decoder::IsSupportedType(containerType, aDiagnostics); + if (supportedInMP4 && !aKeySystem.mMP4.IsSupported()) { + EME_LOG( + "MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; " + "MP4 requested but unsupported.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentTypeString).get(), + NS_ConvertUTF16toUTF8(robustness).get(), + NS_ConvertUTF16toUTF8(encryptionScheme).get()); + continue; + } + const bool isWebM = WebMDecoder::IsSupportedType(containerType); + if (isWebM && !aKeySystem.mWebM.IsSupported()) { + EME_LOG( + "MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s,'%s') unsupported; " + "WebM requested but unsupported.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentTypeString).get(), + NS_ConvertUTF16toUTF8(robustness).get(), + NS_ConvertUTF16toUTF8(encryptionScheme).get()); + continue; + } + if (!supportedInMP4 && !isWebM) { + EME_LOG( + "MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; " + "Unsupported or unrecognized container requested.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentTypeString).get(), + NS_ConvertUTF16toUTF8(robustness).get(), + NS_ConvertUTF16toUTF8(encryptionScheme).get()); + continue; + } + + // Let parameters be the RFC 6381[RFC6381] parameters, if any, specified by + // content type. + // If the user agent does not recognize one or more parameters, continue to + // the next iteration. + if (IsParameterUnrecognized(contentTypeString)) { + continue; + } + + // Let media types be the set of codecs and codec constraints specified by + // parameters. The case-sensitivity of string comparisons is determined by + // the appropriate RFC or other specification. + // (Note: codecs array is 'parameter'). + + // If media types is empty: + if (codecs.IsEmpty()) { + // Log deprecation warning to encourage authors to not do this! + aDeprecationLogFn("MediaEMENoCodecsDeprecatedWarning"); + // TODO: Remove this once we're sure it doesn't break the web. + // If container normatively implies a specific set of codecs and codec + // constraints: Let parameters be that set. + if (supportedInMP4) { + if (aCodecType == Audio) { + codecs.AppendElement(KeySystemConfig::EME_CODEC_AAC); + } else if (aCodecType == Video) { + codecs.AppendElement(KeySystemConfig::EME_CODEC_H264); + } + } else if (isWebM) { + if (aCodecType == Audio) { + codecs.AppendElement(KeySystemConfig::EME_CODEC_VORBIS); + } else if (aCodecType == Video) { + codecs.AppendElement(KeySystemConfig::EME_CODEC_VP8); + } + } + // Otherwise: Continue to the next iteration. + // (Note: all containers we support have implied codecs, so don't continue + // here.) + } + + // If container type is not strictly a audio/video type, continue to the + // next iteration. + const auto majorType = GetMajorType(containerType.Type()); + if (majorType == Invalid) { + EME_LOG( + "MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; " + "MIME type is not an audio or video MIME type.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentTypeString).get(), + NS_ConvertUTF16toUTF8(robustness).get(), + NS_ConvertUTF16toUTF8(encryptionScheme).get()); + continue; + } + if (majorType != aCodecType || !AllCodecsOfType(codecs, aCodecType)) { + EME_LOG( + "MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; " + "MIME type mixes audio codecs in video capabilities " + "or video codecs in audio capabilities.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentTypeString).get(), + NS_ConvertUTF16toUTF8(robustness).get(), + NS_ConvertUTF16toUTF8(encryptionScheme).get()); + continue; + } + // If robustness is not the empty string and contains an unrecognized + // value or a value not supported by implementation, continue to the + // next iteration. String comparison is case-sensitive. + if (!robustness.IsEmpty()) { + if (majorType == Audio && + !aKeySystem.mAudioRobustness.Contains(robustness)) { + EME_LOG( + "MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; " + "unsupported robustness string.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentTypeString).get(), + NS_ConvertUTF16toUTF8(robustness).get(), + NS_ConvertUTF16toUTF8(encryptionScheme).get()); + continue; + } + if (majorType == Video && + !aKeySystem.mVideoRobustness.Contains(robustness)) { + EME_LOG( + "MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; " + "unsupported robustness string.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentTypeString).get(), + NS_ConvertUTF16toUTF8(robustness).get(), + NS_ConvertUTF16toUTF8(encryptionScheme).get()); + continue; + } + // Note: specified robustness requirements are satisfied. + } + + // If preffed on: "In the Get Supported Capabilities for Audio/Video Type + // algorithm, implementations must skip capabilities specifying unsupported + // encryption schemes." + if (!SupportsEncryptionScheme(encryptionScheme, + aKeySystem.mEncryptionSchemes)) { + EME_LOG( + "MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; " + "encryption scheme unsupported by CDM requested.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentTypeString).get(), + NS_ConvertUTF16toUTF8(robustness).get(), + NS_ConvertUTF16toUTF8(encryptionScheme).get()); + continue; + } + + // If the user agent and implementation definitely support playback of + // encrypted media data for the combination of container, media types, + // robustness and local accumulated configuration in combination with + // restrictions... + const auto& containerSupport = + supportedInMP4 ? aKeySystem.mMP4 : aKeySystem.mWebM; + if (!CanDecryptAndDecode(aKeySystem.mKeySystem, contentTypeString, + majorType, containerSupport, codecs, + aDiagnostics)) { + EME_LOG( + "MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; " + "codec unsupported by CDM requested.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentTypeString).get(), + NS_ConvertUTF16toUTF8(robustness).get(), + NS_ConvertUTF16toUTF8(encryptionScheme).get()); + continue; + } + + // ... add requested media capability to supported media capabilities. + if (!supportedCapabilities.AppendElement(capabilities, mozilla::fallible)) { + NS_WARNING("GetSupportedCapabilities: Malloc failure"); + return Sequence(); + } + + // Note: omitting steps 3.13.2, our robustness is not sophisticated enough + // to require considering all requirements together. + } + return supportedCapabilities; +} + +// "Get Supported Configuration and Consent" algorithm, steps 4-7 for +// distinctive identifier, and steps 8-11 for persistent state. The steps +// are the same for both requirements/features, so we factor them out into +// a single function. +static bool CheckRequirement( + const MediaKeysRequirement aRequirement, + const KeySystemConfig::Requirement aKeySystemRequirement, + MediaKeysRequirement& aOutRequirement) { + // Let requirement be the value of candidate configuration's member. + MediaKeysRequirement requirement = aRequirement; + // If requirement is "optional" and feature is not allowed according to + // restrictions, set requirement to "not-allowed". + if (aRequirement == MediaKeysRequirement::Optional && + aKeySystemRequirement == KeySystemConfig::Requirement::NotAllowed) { + requirement = MediaKeysRequirement::Not_allowed; + } + + // Follow the steps for requirement from the following list: + switch (requirement) { + case MediaKeysRequirement::Required: { + // If the implementation does not support use of requirement in + // combination with accumulated configuration and restrictions, return + // NotSupported. + if (aKeySystemRequirement == KeySystemConfig::Requirement::NotAllowed) { + return false; + } + break; + } + case MediaKeysRequirement::Optional: { + // Continue with the following steps. + break; + } + case MediaKeysRequirement::Not_allowed: { + // If the implementation requires use of feature in combination with + // accumulated configuration and restrictions, return NotSupported. + if (aKeySystemRequirement == KeySystemConfig::Requirement::Required) { + return false; + } + break; + } + default: { + return false; + } + } + + // Set the requirement member of accumulated configuration to equal + // calculated requirement. + aOutRequirement = requirement; + + return true; +} + +// 3.1.1.2, step 12 +// Follow the steps for the first matching condition from the following list: +// If the sessionTypes member is present in candidate configuration. +// Let session types be candidate configuration's sessionTypes member. +// Otherwise let session types be ["temporary"]. +// Note: This returns an empty array on malloc failure. +static Sequence UnboxSessionTypes( + const Optional>& aSessionTypes) { + Sequence sessionTypes; + if (aSessionTypes.WasPassed()) { + sessionTypes = aSessionTypes.Value(); + } else { + // Note: fallible. Results in an empty array. + (void)sessionTypes.AppendElement(ToString(MediaKeySessionType::Temporary), + mozilla::fallible); + } + return sessionTypes; +} + +// 3.1.1.2 Get Supported Configuration and Consent +static bool GetSupportedConfig( + const KeySystemConfig& aKeySystem, + const MediaKeySystemConfiguration& aCandidate, + MediaKeySystemConfiguration& aOutConfig, + DecoderDoctorDiagnostics* aDiagnostics, bool aInPrivateBrowsing, + const std::function& aDeprecationLogFn) { + EME_LOG("Compare implementation '%s'\n with request '%s'", + NS_ConvertUTF16toUTF8(aKeySystem.GetDebugInfo()).get(), + ToCString(aCandidate).get()); + // Let accumulated configuration be a new MediaKeySystemConfiguration + // dictionary. + MediaKeySystemConfiguration config; + // Set the label member of accumulated configuration to equal the label member + // of candidate configuration. + config.mLabel = aCandidate.mLabel; + // If the initDataTypes member of candidate configuration is non-empty, run + // the following steps: + if (!aCandidate.mInitDataTypes.IsEmpty()) { + // Let supported types be an empty sequence of DOMStrings. + nsTArray supportedTypes; + // For each value in candidate configuration's initDataTypes member: + for (const nsString& initDataType : aCandidate.mInitDataTypes) { + // Let initDataType be the value. + // If the implementation supports generating requests based on + // initDataType, add initDataType to supported types. String comparison is + // case-sensitive. The empty string is never supported. + if (aKeySystem.mInitDataTypes.Contains(initDataType)) { + supportedTypes.AppendElement(initDataType); + } + } + // If supported types is empty, return NotSupported. + if (supportedTypes.IsEmpty()) { + EME_LOG( + "MediaKeySystemConfiguration (label='%s') rejected; " + "no supported initDataTypes provided.", + NS_ConvertUTF16toUTF8(aCandidate.mLabel).get()); + return false; + } + // Set the initDataTypes member of accumulated configuration to supported + // types. + if (!config.mInitDataTypes.Assign(supportedTypes)) { + return false; + } + } + + if (!CheckRequirement(aCandidate.mDistinctiveIdentifier, + aKeySystem.mDistinctiveIdentifier, + config.mDistinctiveIdentifier)) { + EME_LOG( + "MediaKeySystemConfiguration (label='%s') rejected; " + "distinctiveIdentifier requirement not satisfied.", + NS_ConvertUTF16toUTF8(aCandidate.mLabel).get()); + return false; + } + + if (!CheckRequirement(aCandidate.mPersistentState, + aKeySystem.mPersistentState, config.mPersistentState)) { + EME_LOG( + "MediaKeySystemConfiguration (label='%s') rejected; " + "persistentState requirement not satisfied.", + NS_ConvertUTF16toUTF8(aCandidate.mLabel).get()); + return false; + } + + if (config.mPersistentState == MediaKeysRequirement::Required && + aInPrivateBrowsing) { + EME_LOG( + "MediaKeySystemConfiguration (label='%s') rejected; " + "persistentState requested in Private Browsing window.", + NS_ConvertUTF16toUTF8(aCandidate.mLabel).get()); + return false; + } + + Sequence sessionTypes(UnboxSessionTypes(aCandidate.mSessionTypes)); + if (sessionTypes.IsEmpty()) { + // Malloc failure. + return false; + } + + // For each value in session types: + for (const auto& sessionTypeString : sessionTypes) { + // Let session type be the value. + MediaKeySessionType sessionType; + if (!ToSessionType(sessionTypeString, sessionType)) { + // (Assume invalid sessionType is unsupported as per steps below). + EME_LOG( + "MediaKeySystemConfiguration (label='%s') rejected; " + "invalid session type specified.", + NS_ConvertUTF16toUTF8(aCandidate.mLabel).get()); + return false; + } + // If accumulated configuration's persistentState value is "not-allowed" + // and the Is persistent session type? algorithm returns true for session + // type return NotSupported. + if (config.mPersistentState == MediaKeysRequirement::Not_allowed && + IsPersistentSessionType(sessionType)) { + EME_LOG( + "MediaKeySystemConfiguration (label='%s') rejected; " + "persistent session requested but keysystem doesn't" + "support persistent state.", + NS_ConvertUTF16toUTF8(aCandidate.mLabel).get()); + return false; + } + // If the implementation does not support session type in combination + // with accumulated configuration and restrictions for other reasons, + // return NotSupported. + if (!ContainsSessionType(aKeySystem.mSessionTypes, sessionType)) { + EME_LOG( + "MediaKeySystemConfiguration (label='%s') rejected; " + "session type '%s' unsupported by keySystem.", + NS_ConvertUTF16toUTF8(aCandidate.mLabel).get(), + NS_ConvertUTF16toUTF8(sessionTypeString).get()); + return false; + } + // If accumulated configuration's persistentState value is "optional" + // and the result of running the Is persistent session type? algorithm + // on session type is true, change accumulated configuration's + // persistentState value to "required". + if (config.mPersistentState == MediaKeysRequirement::Optional && + IsPersistentSessionType(sessionType)) { + config.mPersistentState = MediaKeysRequirement::Required; + } + } + // Set the sessionTypes member of accumulated configuration to session types. + config.mSessionTypes.Construct(std::move(sessionTypes)); + + // If the videoCapabilities and audioCapabilities members in candidate + // configuration are both empty, return NotSupported. + if (aCandidate.mAudioCapabilities.IsEmpty() && + aCandidate.mVideoCapabilities.IsEmpty()) { + // TODO: Most sites using EME still don't pass capabilities, so we + // can't reject on it yet without breaking them. So add this later. + // Log deprecation warning to encourage authors to not do this! + aDeprecationLogFn("MediaEMENoCapabilitiesDeprecatedWarning"); + } + + // If the videoCapabilities member in candidate configuration is non-empty: + if (!aCandidate.mVideoCapabilities.IsEmpty()) { + // Let video capabilities be the result of executing the Get Supported + // Capabilities for Audio/Video Type algorithm on Video, candidate + // configuration's videoCapabilities member, accumulated configuration, + // and restrictions. + Sequence caps = + GetSupportedCapabilities(Video, aCandidate.mVideoCapabilities, config, + aKeySystem, aDiagnostics, aDeprecationLogFn); + // If video capabilities is null, return NotSupported. + if (caps.IsEmpty()) { + EME_LOG( + "MediaKeySystemConfiguration (label='%s') rejected; " + "no supported video capabilities.", + NS_ConvertUTF16toUTF8(aCandidate.mLabel).get()); + return false; + } + // Set the videoCapabilities member of accumulated configuration to video + // capabilities. + config.mVideoCapabilities = std::move(caps); + } else { + // Otherwise: + // Set the videoCapabilities member of accumulated configuration to an empty + // sequence. + } + + // If the audioCapabilities member in candidate configuration is non-empty: + if (!aCandidate.mAudioCapabilities.IsEmpty()) { + // Let audio capabilities be the result of executing the Get Supported + // Capabilities for Audio/Video Type algorithm on Audio, candidate + // configuration's audioCapabilities member, accumulated configuration, and + // restrictions. + Sequence caps = + GetSupportedCapabilities(Audio, aCandidate.mAudioCapabilities, config, + aKeySystem, aDiagnostics, aDeprecationLogFn); + // If audio capabilities is null, return NotSupported. + if (caps.IsEmpty()) { + EME_LOG( + "MediaKeySystemConfiguration (label='%s') rejected; " + "no supported audio capabilities.", + NS_ConvertUTF16toUTF8(aCandidate.mLabel).get()); + return false; + } + // Set the audioCapabilities member of accumulated configuration to audio + // capabilities. + config.mAudioCapabilities = std::move(caps); + } else { + // Otherwise: + // Set the audioCapabilities member of accumulated configuration to an empty + // sequence. + } + + // If accumulated configuration's distinctiveIdentifier value is "optional", + // follow the steps for the first matching condition from the following list: + if (config.mDistinctiveIdentifier == MediaKeysRequirement::Optional) { + // If the implementation requires use Distinctive Identifier(s) or + // Distinctive Permanent Identifier(s) for any of the combinations + // in accumulated configuration + if (aKeySystem.mDistinctiveIdentifier == + KeySystemConfig::Requirement::Required) { + // Change accumulated configuration's distinctiveIdentifier value to + // "required". + config.mDistinctiveIdentifier = MediaKeysRequirement::Required; + } else { + // Otherwise, change accumulated configuration's distinctiveIdentifier + // value to "not-allowed". + config.mDistinctiveIdentifier = MediaKeysRequirement::Not_allowed; + } + } + + // If accumulated configuration's persistentState value is "optional", follow + // the steps for the first matching condition from the following list: + if (config.mPersistentState == MediaKeysRequirement::Optional) { + // If the implementation requires persisting state for any of the + // combinations in accumulated configuration + if (aKeySystem.mPersistentState == KeySystemConfig::Requirement::Required) { + // Change accumulated configuration's persistentState value to "required". + config.mPersistentState = MediaKeysRequirement::Required; + } else { + // Otherwise, change accumulated configuration's persistentState + // value to "not-allowed". + config.mPersistentState = MediaKeysRequirement::Not_allowed; + } + } + + // Note: Omitting steps 20-22. We don't ask for consent. + +#if defined(XP_WIN) + // Widevine CDM doesn't include an AAC decoder. So if WMF can't decode AAC, + // and a codec wasn't specified, be conservative and reject the MediaKeys + // request. + if (IsWidevineKeySystem(aKeySystem.mKeySystem) && + (aCandidate.mAudioCapabilities.IsEmpty() || + aCandidate.mVideoCapabilities.IsEmpty()) && + !WMFDecoderModule::CanCreateMFTDecoder(WMFStreamType::AAC)) { + if (aDiagnostics) { + aDiagnostics->SetKeySystemIssue( + DecoderDoctorDiagnostics::eWidevineWithNoWMF); + } + EME_LOG( + "MediaKeySystemConfiguration (label='%s') rejected; " + "WMF required for Widevine decoding, but it's not available.", + NS_ConvertUTF16toUTF8(aCandidate.mLabel).get()); + return false; + } +#endif + + // Return accumulated configuration. + aOutConfig = config; + + return true; +} + +/* static */ +bool MediaKeySystemAccess::GetSupportedConfig( + const nsAString& aKeySystem, + const Sequence& aConfigs, + MediaKeySystemConfiguration& aOutConfig, + DecoderDoctorDiagnostics* aDiagnostics, bool aIsPrivateBrowsing, + const std::function& aDeprecationLogFn) { + nsTArray implementations; + if (!GetKeySystemConfigs(aKeySystem, implementations)) { + return false; + } + for (const auto& implementation : implementations) { + for (const MediaKeySystemConfiguration& candidate : aConfigs) { + if (mozilla::dom::GetSupportedConfig( + implementation, candidate, aOutConfig, aDiagnostics, + aIsPrivateBrowsing, aDeprecationLogFn)) { + return true; + } + } + } + return false; +} + +/* static */ +void MediaKeySystemAccess::NotifyObservers(nsPIDOMWindowInner* aWindow, + const nsAString& aKeySystem, + MediaKeySystemStatus aStatus) { + RequestMediaKeySystemAccessNotification data; + data.mKeySystem = aKeySystem; + data.mStatus = aStatus; + nsAutoString json; + data.ToJSON(json); + EME_LOG("MediaKeySystemAccess::NotifyObservers() %s", + NS_ConvertUTF16toUTF8(json).get()); + nsCOMPtr obs = services::GetObserverService(); + if (obs) { + obs->NotifyObservers(aWindow, MediaKeys::kMediaKeysRequestTopic, + json.get()); + } +} + +static nsCString ToCString(const nsString& aString) { + nsCString str("'"); + str.Append(NS_ConvertUTF16toUTF8(aString)); + str.AppendLiteral("'"); + return str; +} + +static nsCString ToCString(const MediaKeysRequirement aValue) { + nsCString str("'"); + str.AppendASCII(MediaKeysRequirementValues::GetString(aValue)); + str.AppendLiteral("'"); + return str; +} + +static nsCString ToCString(const MediaKeySystemMediaCapability& aValue) { + nsCString str; + str.AppendLiteral("{contentType="); + str.Append(ToCString(aValue.mContentType)); + str.AppendLiteral(", robustness="); + str.Append(ToCString(aValue.mRobustness)); + str.AppendLiteral(", encryptionScheme="); + str.Append(ToCString(aValue.mEncryptionScheme)); + str.AppendLiteral("}"); + return str; +} + +template +static nsCString ToCString(const Sequence& aSequence) { + nsCString str; + str.AppendLiteral("["); + StringJoinAppend(str, ","_ns, aSequence, + [](nsACString& dest, const Type& element) { + dest.Append(ToCString(element)); + }); + str.AppendLiteral("]"); + return str; +} + +template +static nsCString ToCString(const Optional>& aOptional) { + nsCString str; + if (aOptional.WasPassed()) { + str.Append(ToCString(aOptional.Value())); + } else { + str.AppendLiteral("[]"); + } + return str; +} + +static nsCString ToCString(const MediaKeySystemConfiguration& aConfig) { + nsCString str; + str.AppendLiteral("{label="); + str.Append(ToCString(aConfig.mLabel)); + + str.AppendLiteral(", initDataTypes="); + str.Append(ToCString(aConfig.mInitDataTypes)); + + str.AppendLiteral(", audioCapabilities="); + str.Append(ToCString(aConfig.mAudioCapabilities)); + + str.AppendLiteral(", videoCapabilities="); + str.Append(ToCString(aConfig.mVideoCapabilities)); + + str.AppendLiteral(", distinctiveIdentifier="); + str.Append(ToCString(aConfig.mDistinctiveIdentifier)); + + str.AppendLiteral(", persistentState="); + str.Append(ToCString(aConfig.mPersistentState)); + + str.AppendLiteral(", sessionTypes="); + str.Append(ToCString(aConfig.mSessionTypes)); + + str.AppendLiteral("}"); + + return str; +} + +/* static */ +nsCString MediaKeySystemAccess::ToCString( + const Sequence& aConfig) { + return mozilla::dom::ToCString(aConfig); +} + +#undef LOG + +} // namespace mozilla::dom diff --git a/dom/media/eme/MediaKeySystemAccess.h b/dom/media/eme/MediaKeySystemAccess.h new file mode 100644 index 0000000000..954ff7adf3 --- /dev/null +++ b/dom/media/eme/MediaKeySystemAccess.h @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 mozilla_dom_MediaKeySystemAccess_h +#define mozilla_dom_MediaKeySystemAccess_h + +#include "mozilla/Attributes.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" + +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/MediaKeySystemAccessBinding.h" +#include "mozilla/dom/MediaKeysRequestStatusBinding.h" + +#include "js/TypeDecls.h" + +namespace mozilla { + +class DecoderDoctorDiagnostics; +class ErrorResult; + +namespace dom { + +struct MediaKeySystemAccessRequest; + +class MediaKeySystemAccess final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(MediaKeySystemAccess) + + public: + explicit MediaKeySystemAccess(nsPIDOMWindowInner* aParent, + const nsAString& aKeySystem, + const MediaKeySystemConfiguration& aConfig); + + protected: + ~MediaKeySystemAccess(); + + public: + nsPIDOMWindowInner* GetParentObject() const; + + JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + void GetKeySystem(nsString& aRetVal) const; + + void GetConfiguration(MediaKeySystemConfiguration& aConfig); + + already_AddRefed CreateMediaKeys(ErrorResult& aRv); + + static MediaKeySystemStatus GetKeySystemStatus( + const MediaKeySystemAccessRequest& aRequest, + nsACString& aOutExceptionMessage); + + static void NotifyObservers(nsPIDOMWindowInner* aWindow, + const nsAString& aKeySystem, + MediaKeySystemStatus aStatus); + + static bool GetSupportedConfig( + const nsAString& aKeySystem, + const Sequence& aConfigs, + MediaKeySystemConfiguration& aOutConfig, + DecoderDoctorDiagnostics* aDiagnostics, bool aIsPrivateBrowsing, + const std::function& aDeprecationLogFn); + + static bool KeySystemSupportsInitDataType(const nsAString& aKeySystem, + const nsAString& aInitDataType); + + static nsCString ToCString( + const Sequence& aConfig); + + private: + nsCOMPtr mParent; + const nsString mKeySystem; + const MediaKeySystemConfiguration mConfig; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_MediaKeySystemAccess_h diff --git a/dom/media/eme/MediaKeySystemAccessManager.cpp b/dom/media/eme/MediaKeySystemAccessManager.cpp new file mode 100644 index 0000000000..2bc12d57d7 --- /dev/null +++ b/dom/media/eme/MediaKeySystemAccessManager.cpp @@ -0,0 +1,702 @@ +/* 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 "MediaKeySystemAccessManager.h" + +#include "DecoderDoctorDiagnostics.h" +#include "MediaKeySystemAccessPermissionRequest.h" +#include "VideoUtils.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/KeySystemNames.h" +#include "mozilla/DetailedPromise.h" +#include "mozilla/EMEUtils.h" +#include "mozilla/Preferences.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPrefs_media.h" +#include "mozilla/Unused.h" +#ifdef XP_WIN +# include "mozilla/WindowsVersion.h" +#endif +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsTHashMap.h" +#include "nsIObserverService.h" +#include "nsIScriptError.h" +#include "nsPrintfCString.h" +#include "nsServiceManagerUtils.h" + +namespace mozilla::dom { + +MediaKeySystemAccessManager::PendingRequest::PendingRequest( + DetailedPromise* aPromise, const nsAString& aKeySystem, + const Sequence& aConfigs) + : MediaKeySystemAccessRequest(aKeySystem, aConfigs), mPromise(aPromise) { + MOZ_COUNT_CTOR(MediaKeySystemAccessManager::PendingRequest); +} + +MediaKeySystemAccessManager::PendingRequest::~PendingRequest() { + MOZ_COUNT_DTOR(MediaKeySystemAccessManager::PendingRequest); +} + +void MediaKeySystemAccessManager::PendingRequest::CancelTimer() { + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } +} + +void MediaKeySystemAccessManager::PendingRequest:: + RejectPromiseWithInvalidAccessError(const nsACString& aReason) { + if (mPromise) { + mPromise->MaybeRejectWithInvalidAccessError(aReason); + } +} + +void MediaKeySystemAccessManager::PendingRequest:: + RejectPromiseWithNotSupportedError(const nsACString& aReason) { + if (mPromise) { + mPromise->MaybeRejectWithNotSupportedError(aReason); + } +} + +void MediaKeySystemAccessManager::PendingRequest::RejectPromiseWithTypeError( + const nsACString& aReason) { + if (mPromise) { + mPromise->MaybeRejectWithTypeError(aReason); + } +} + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeySystemAccessManager) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver) + NS_INTERFACE_MAP_ENTRY(nsIObserver) + NS_INTERFACE_MAP_ENTRY(nsINamed) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaKeySystemAccessManager) +NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaKeySystemAccessManager) + +NS_IMPL_CYCLE_COLLECTION_CLASS(MediaKeySystemAccessManager) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(MediaKeySystemAccessManager) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow) + for (size_t i = 0; i < tmp->mPendingInstallRequests.Length(); i++) { + tmp->mPendingInstallRequests[i]->CancelTimer(); + tmp->mPendingInstallRequests[i]->RejectPromiseWithInvalidAccessError( + nsLiteralCString( + "Promise still outstanding at MediaKeySystemAccessManager GC")); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingInstallRequests[i]->mPromise) + } + tmp->mPendingInstallRequests.Clear(); + for (size_t i = 0; i < tmp->mPendingAppApprovalRequests.Length(); i++) { + tmp->mPendingAppApprovalRequests[i]->RejectPromiseWithInvalidAccessError( + nsLiteralCString( + "Promise still outstanding at MediaKeySystemAccessManager GC")); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingAppApprovalRequests[i]->mPromise) + } + tmp->mPendingAppApprovalRequests.Clear(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(MediaKeySystemAccessManager) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow) + for (size_t i = 0; i < tmp->mPendingInstallRequests.Length(); i++) { + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingInstallRequests[i]->mPromise) + } + for (size_t i = 0; i < tmp->mPendingAppApprovalRequests.Length(); i++) { + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingAppApprovalRequests[i]->mPromise) + } +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +#define MKSAM_LOG_DEBUG(msg, ...) \ + EME_LOG("MediaKeySystemAccessManager::%s " msg, __func__, ##__VA_ARGS__) + +MediaKeySystemAccessManager::MediaKeySystemAccessManager( + nsPIDOMWindowInner* aWindow) + : mWindow(aWindow) { + MOZ_ASSERT(NS_IsMainThread()); +} + +MediaKeySystemAccessManager::~MediaKeySystemAccessManager() { + MOZ_ASSERT(NS_IsMainThread()); + Shutdown(); +} + +void MediaKeySystemAccessManager::Request( + DetailedPromise* aPromise, const nsAString& aKeySystem, + const Sequence& aConfigs) { + MOZ_ASSERT(NS_IsMainThread()); + CheckDoesWindowSupportProtectedMedia( + MakeUnique(aPromise, aKeySystem, aConfigs)); +} + +void MediaKeySystemAccessManager::CheckDoesWindowSupportProtectedMedia( + UniquePtr aRequest) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aRequest); + MKSAM_LOG_DEBUG("aRequest->mKeySystem=%s", + NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get()); + + // In Windows OS, some Firefox windows that host content cannot support + // protected content, so check the status of support for this window. + // On other platforms windows should always support protected media. +#ifdef XP_WIN + RefPtr browser(BrowserChild::GetFrom(mWindow)); + if (!browser) { + if (!XRE_IsParentProcess() || XRE_IsE10sParentProcess()) { + // In this case, there is no browser because the Navigator object has + // been disconnected from its window. Thus, reject the promise. + aRequest->mPromise->MaybeRejectWithTypeError( + "Browsing context is no longer available"); + } else { + // In this case, there is no browser because e10s is off. Proceed with + // the request with support since this scenario is always supported. + MKSAM_LOG_DEBUG("Allowing protected media on Windows with e10s off."); + + OnDoesWindowSupportProtectedMedia(true, std::move(aRequest)); + } + + return; + } + + RefPtr self(this); + + MKSAM_LOG_DEBUG( + "Checking with browser if this window supports protected media."); + browser->DoesWindowSupportProtectedMedia()->Then( + GetCurrentSerialEventTarget(), __func__, + [self, request = std::move(aRequest)]( + const BrowserChild::IsWindowSupportingProtectedMediaPromise:: + ResolveOrRejectValue& value) mutable { + if (value.IsResolve()) { + self->OnDoesWindowSupportProtectedMedia(value.ResolveValue(), + std::move(request)); + } else { + EME_LOG( + "MediaKeySystemAccessManager::DoesWindowSupportProtectedMedia-" + "ResolveOrRejectLambda Failed to make IPC call to " + "IsWindowSupportingProtectedMedia: " + "reason=%d", + static_cast(value.RejectValue())); + // Treat as failure. + self->OnDoesWindowSupportProtectedMedia(false, std::move(request)); + } + }); + +#else + // Non-Windows OS windows always support protected media. + MKSAM_LOG_DEBUG( + "Allowing protected media because all non-Windows OS windows support " + "protected media."); + OnDoesWindowSupportProtectedMedia(true, std::move(aRequest)); +#endif +} + +void MediaKeySystemAccessManager::OnDoesWindowSupportProtectedMedia( + bool aIsSupportedInWindow, UniquePtr aRequest) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aRequest); + MKSAM_LOG_DEBUG("aIsSupportedInWindow=%s aRequest->mKeySystem=%s", + aIsSupportedInWindow ? "true" : "false", + NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get()); + + if (!aIsSupportedInWindow) { + aRequest->RejectPromiseWithNotSupportedError( + "EME is not supported in this window"_ns); + return; + } + + RequestMediaKeySystemAccess(std::move(aRequest)); +} + +void MediaKeySystemAccessManager::CheckDoesAppAllowProtectedMedia( + UniquePtr aRequest) { + // At time of writing, only GeckoView is expected to leverage the need to + // approve EME requests from the application. However, this functionality + // can be tested on all platforms by manipulating the + // media.eme.require-app-approval + test prefs associated with + // MediaKeySystemPermissionRequest. + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aRequest); + MKSAM_LOG_DEBUG("aRequest->mKeySystem=%s", + NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get()); + + if (!StaticPrefs::media_eme_require_app_approval()) { + MKSAM_LOG_DEBUG( + "media.eme.require-app-approval is false, allowing request."); + // We don't require app approval as the pref is not set. Treat as app + // approving by passing true to the handler. + OnDoesAppAllowProtectedMedia(true, std::move(aRequest)); + return; + } + + if (mAppAllowsProtectedMediaPromiseRequest.Exists()) { + // We already have a pending permission request, we don't need to fire + // another. Just wait for the existing permission request to be handled + // and the result from that will be used to handle the current request. + MKSAM_LOG_DEBUG( + "mAppAllowsProtectedMediaPromiseRequest already exists. aRequest " + "addded to queue and will be handled when exising permission request " + "is serviced."); + mPendingAppApprovalRequests.AppendElement(std::move(aRequest)); + return; + } + + RefPtr appApprovalRequest = + MediaKeySystemAccessPermissionRequest::Create(mWindow); + if (!appApprovalRequest) { + MKSAM_LOG_DEBUG( + "Failed to create app approval request! Blocking eme request as " + "fallback."); + aRequest->RejectPromiseWithInvalidAccessError(nsLiteralCString( + "Failed to create approval request to send to app embedding Gecko.")); + return; + } + + // If we're not using testing prefs (which take precedence over cached + // values) and have a cached value, handle based on the cached value. + if (appApprovalRequest->CheckPromptPrefs() == + MediaKeySystemAccessPermissionRequest::PromptResult::Pending && + mAppAllowsProtectedMedia) { + MKSAM_LOG_DEBUG( + "Short circuiting based on mAppAllowsProtectedMedia cached value"); + OnDoesAppAllowProtectedMedia(*mAppAllowsProtectedMedia, + std::move(aRequest)); + return; + } + + // Store the approval request, it will be handled when we get a response + // from the app. + mPendingAppApprovalRequests.AppendElement(std::move(aRequest)); + + RefPtr p = + appApprovalRequest->GetPromise(); + p->Then( + GetCurrentSerialEventTarget(), __func__, + // Allow callback + [this, + self = RefPtr(this)](bool aRequestResult) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aRequestResult, "Result should be true on allow callback!"); + mAppAllowsProtectedMediaPromiseRequest.Complete(); + // Cache result. + mAppAllowsProtectedMedia = Some(aRequestResult); + // For each pending request, handle it based on the app's response. + for (UniquePtr& approvalRequest : + mPendingAppApprovalRequests) { + OnDoesAppAllowProtectedMedia(*mAppAllowsProtectedMedia, + std::move(approvalRequest)); + } + self->mPendingAppApprovalRequests.Clear(); + }, + // Cancel callback + [this, + self = RefPtr(this)](bool aRequestResult) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!aRequestResult, + "Result should be false on cancel callback!"); + mAppAllowsProtectedMediaPromiseRequest.Complete(); + // Cache result. + mAppAllowsProtectedMedia = Some(aRequestResult); + // For each pending request, handle it based on the app's response. + for (UniquePtr& approvalRequest : + mPendingAppApprovalRequests) { + OnDoesAppAllowProtectedMedia(*mAppAllowsProtectedMedia, + std::move(approvalRequest)); + } + self->mPendingAppApprovalRequests.Clear(); + }) + ->Track(mAppAllowsProtectedMediaPromiseRequest); + + // Prefs not causing short circuit, no cached value, go ahead and request + // permission. + MKSAM_LOG_DEBUG("Dispatching async request for app approval"); + if (NS_FAILED(appApprovalRequest->Start())) { + // This shouldn't happen unless we're shutting down or similar edge cases. + // If this is regularly being hit then something is wrong and should be + // investigated. + MKSAM_LOG_DEBUG( + "Failed to start app approval request! Eme approval will be left in " + "limbo!"); + } +} + +void MediaKeySystemAccessManager::OnDoesAppAllowProtectedMedia( + bool aIsAllowed, UniquePtr aRequest) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aRequest); + MKSAM_LOG_DEBUG("aIsAllowed=%s aRequest->mKeySystem=%s", + aIsAllowed ? "true" : "false", + NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get()); + if (!aIsAllowed) { + aRequest->RejectPromiseWithNotSupportedError( + nsLiteralCString("The application embedding this user agent has " + "blocked MediaKeySystemAccess")); + return; + } + + ProvideAccess(std::move(aRequest)); +} + +void MediaKeySystemAccessManager::RequestMediaKeySystemAccess( + UniquePtr aRequest) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aRequest); + MKSAM_LOG_DEBUG("aIsSupportedInWindow=%s", + NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get()); + + // 1. If keySystem is the empty string, return a promise rejected with a newly + // created TypeError. + if (aRequest->mKeySystem.IsEmpty()) { + aRequest->mPromise->MaybeRejectWithTypeError("Key system string is empty"); + // Don't notify DecoderDoctor, as there's nothing we or the user can + // do to fix this situation; the site is using the API wrong. + return; + } + // 2. If supportedConfigurations is empty, return a promise rejected with a + // newly created TypeError. + if (aRequest->mConfigs.IsEmpty()) { + aRequest->mPromise->MaybeRejectWithTypeError( + "Candidate MediaKeySystemConfigs is empty"); + // Don't notify DecoderDoctor, as there's nothing we or the user can + // do to fix this situation; the site is using the API wrong. + return; + } + + // 3. Let document be the calling context's Document. + // 4. Let origin be the origin of document. + // 5. Let promise be a new promise. + // 6. Run the following steps in parallel: + + DecoderDoctorDiagnostics diagnostics; + + // 1. If keySystem is not one of the Key Systems supported by the user + // agent, reject promise with a NotSupportedError. String comparison is + // case-sensitive. + if (!IsWidevineKeySystem(aRequest->mKeySystem) && +#ifdef MOZ_WMF_CDM + !IsPlayReadyKeySystemAndSupported(aRequest->mKeySystem) && + !IsWidevineExperimentKeySystemAndSupported(aRequest->mKeySystem) && +#endif + !IsClearkeyKeySystem(aRequest->mKeySystem)) { + // Not to inform user, because nothing to do if the keySystem is not + // supported. + aRequest->RejectPromiseWithNotSupportedError( + "Key system is unsupported"_ns); + diagnostics.StoreMediaKeySystemAccess( + mWindow->GetExtantDoc(), aRequest->mKeySystem, false, __func__); + return; + } + + if (!StaticPrefs::media_eme_enabled() && + !IsClearkeyKeySystem(aRequest->mKeySystem)) { + // EME disabled by user, send notification to chrome so UI can inform user. + // Clearkey is allowed even when EME is disabled because we want the pref + // "media.eme.enabled" only taking effect on proprietary DRMs. + // We don't show the notification if the pref is locked. + if (!Preferences::IsLocked("media.eme.enabled")) { + MediaKeySystemAccess::NotifyObservers(mWindow, aRequest->mKeySystem, + MediaKeySystemStatus::Api_disabled); + } + aRequest->RejectPromiseWithNotSupportedError("EME has been preffed off"_ns); + diagnostics.StoreMediaKeySystemAccess( + mWindow->GetExtantDoc(), aRequest->mKeySystem, false, __func__); + return; + } + + nsAutoCString message; + MediaKeySystemStatus status = + MediaKeySystemAccess::GetKeySystemStatus(*aRequest, message); + + nsPrintfCString msg( + "MediaKeySystemAccess::GetKeySystemStatus(%s) " + "result=%s msg='%s'", + NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get(), + nsCString(MediaKeySystemStatusValues::GetString(status)).get(), + message.get()); + LogToBrowserConsole(NS_ConvertUTF8toUTF16(msg)); + EME_LOG("%s", msg.get()); + + // We may need to install Widevine CDM to continue. + if (status == MediaKeySystemStatus::Cdm_not_installed && + (IsWidevineKeySystem(aRequest->mKeySystem) +#ifdef MOZ_WMF_CDM + || IsWidevineExperimentKeySystemAndSupported(aRequest->mKeySystem) +#endif + )) { + // These are cases which could be resolved by downloading a new(er) CDM. + // When we send the status to chrome, chrome's GMPProvider will attempt to + // download or update the CDM. In AwaitInstall() we add listeners to wait + // for the update to complete, and we'll call this function again with + // aType==Subsequent once the download has completed and the GMPService + // has had a new plugin added. AwaitInstall() sets a timer to fail if the + // update/download takes too long or fails. + + if (aRequest->mRequestType != PendingRequest::RequestType::Initial) { + MOZ_ASSERT(aRequest->mRequestType == + PendingRequest::RequestType::Subsequent); + // CDM is not installed, but this is a subsequent request. We've waited, + // but can't service this request! Give up. Chrome will still be showing a + // "I can't play, updating" notification. + aRequest->RejectPromiseWithNotSupportedError( + "Timed out while waiting for a CDM update"_ns); + diagnostics.StoreMediaKeySystemAccess( + mWindow->GetExtantDoc(), aRequest->mKeySystem, false, __func__); + return; + } + + nsString keySystem = aRequest->mKeySystem; +#ifdef MOZ_WMF_CDM + // If cdm-not-install is for HWDRM, that means we want to install Widevine + // L1, which requires using hardware key system name for GMP to look up the + // plugin. + if (CheckIfHarewareDRMConfigExists(aRequest->mConfigs)) { + keySystem = NS_ConvertUTF8toUTF16(kWidevineExperimentKeySystemName); + } +#endif + if (AwaitInstall(std::move(aRequest))) { + // Notify chrome that we're going to wait for the CDM to download/update. + EME_LOG("Await %s for installation", + NS_ConvertUTF16toUTF8(keySystem).get()); + MediaKeySystemAccess::NotifyObservers(mWindow, keySystem, status); + } else { + // Failed to await the install. Log failure and give up trying to service + // this request. + EME_LOG("Failed to await %s for installation", + NS_ConvertUTF16toUTF8(keySystem).get()); + diagnostics.StoreMediaKeySystemAccess(mWindow->GetExtantDoc(), keySystem, + false, __func__); + } + return; + } + if (status != MediaKeySystemStatus::Available) { + // Failed due to user disabling something, send a notification to + // chrome, so we can show some UI to explain how the user can rectify + // the situation. + EME_LOG("Notify CDM failure for %s and reject the promise", + NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get()); + MediaKeySystemAccess::NotifyObservers(mWindow, aRequest->mKeySystem, + status); + aRequest->RejectPromiseWithNotSupportedError(message); + return; + } + + nsCOMPtr doc = mWindow->GetExtantDoc(); + nsTHashMap warnings; + std::function deprecationWarningLogFn = + [&](const char* aMsgName) { + EME_LOG( + "MediaKeySystemAccessManager::DeprecationWarningLambda Logging " + "deprecation warning '%s' to WebConsole.", + aMsgName); + warnings.InsertOrUpdate(aMsgName, true); + AutoTArray params; + nsString& uri = *params.AppendElement(); + if (doc) { + Unused << doc->GetDocumentURI(uri); + } + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "Media"_ns, + doc, nsContentUtils::eDOM_PROPERTIES, + aMsgName, params); + }; + + bool isPrivateBrowsing = + mWindow->GetExtantDoc() && + mWindow->GetExtantDoc()->NodePrincipal()->GetPrivateBrowsingId() > 0; + // 2. Let implementation be the implementation of keySystem. + // 3. For each value in supportedConfigurations: + // 1. Let candidate configuration be the value. + // 2. Let supported configuration be the result of executing the Get + // Supported Configuration algorithm on implementation, candidate + // configuration, and origin. + // 3. If supported configuration is not NotSupported, run the following + // steps: + // 1. Let access be a new MediaKeySystemAccess object, and initialize it + // as follows: + // 1. Set the keySystem attribute to keySystem. + // 2. Let the configuration value be supported configuration. + // 3. Let the cdm implementation value be implementation. + // 2. Resolve promise with access and abort the parallel steps of this + // algorithm. + MediaKeySystemConfiguration config; + if (MediaKeySystemAccess::GetSupportedConfig( + aRequest->mKeySystem, aRequest->mConfigs, config, &diagnostics, + isPrivateBrowsing, deprecationWarningLogFn)) { + aRequest->mSupportedConfig = Some(config); + // The app gets the final say on if we provide access or not. + CheckDoesAppAllowProtectedMedia(std::move(aRequest)); + return; + } + // 4. Reject promise with a NotSupportedError. + + // Not to inform user, because nothing to do if the corresponding keySystem + // configuration is not supported. + aRequest->RejectPromiseWithNotSupportedError( + "Key system configuration is not supported"_ns); + diagnostics.StoreMediaKeySystemAccess(mWindow->GetExtantDoc(), + aRequest->mKeySystem, false, __func__); +} + +void MediaKeySystemAccessManager::ProvideAccess( + UniquePtr aRequest) { + MOZ_ASSERT(aRequest); + MOZ_ASSERT( + aRequest->mSupportedConfig, + "The request needs a supported config if we're going to provide access!"); + MKSAM_LOG_DEBUG("aRequest->mKeySystem=%s", + NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get()); + + DecoderDoctorDiagnostics diagnostics; + + RefPtr access(new MediaKeySystemAccess( + mWindow, aRequest->mKeySystem, aRequest->mSupportedConfig.ref())); + aRequest->mPromise->MaybeResolve(access); + diagnostics.StoreMediaKeySystemAccess(mWindow->GetExtantDoc(), + aRequest->mKeySystem, true, __func__); +} + +bool MediaKeySystemAccessManager::AwaitInstall( + UniquePtr aRequest) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aRequest); + MKSAM_LOG_DEBUG("aRequest->mKeySystem=%s", + NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get()); + + if (!EnsureObserversAdded()) { + NS_WARNING("Failed to add pref observer"); + aRequest->RejectPromiseWithNotSupportedError(nsLiteralCString( + "Failed trying to setup CDM update: failed adding observers")); + return false; + } + + nsCOMPtr timer; + NS_NewTimerWithObserver(getter_AddRefs(timer), this, 60 * 1000, + nsITimer::TYPE_ONE_SHOT); + if (!timer) { + NS_WARNING("Failed to create timer to await CDM install."); + aRequest->RejectPromiseWithNotSupportedError(nsLiteralCString( + "Failed trying to setup CDM update: failed timer creation")); + return false; + } + + MOZ_DIAGNOSTIC_ASSERT( + aRequest->mTimer == nullptr, + "Timer should not already be set on a request we're about to await"); + aRequest->mTimer = timer; + + mPendingInstallRequests.AppendElement(std::move(aRequest)); + return true; +} + +void MediaKeySystemAccessManager::RetryRequest( + UniquePtr aRequest) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aRequest); + MKSAM_LOG_DEBUG("aRequest->mKeySystem=%s", + NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get()); + // Cancel and null timer if it exists. + aRequest->CancelTimer(); + // Indicate that this is a request that's being retried. + aRequest->mRequestType = PendingRequest::RequestType::Subsequent; + RequestMediaKeySystemAccess(std::move(aRequest)); +} + +nsresult MediaKeySystemAccessManager::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) { + MOZ_ASSERT(NS_IsMainThread()); + MKSAM_LOG_DEBUG("%s", aTopic); + + if (!strcmp(aTopic, "gmp-changed")) { + // Filter out the requests where the CDM's install-status is no longer + // "unavailable". This will be the CDMs which have downloaded since the + // initial request. + // Note: We don't have a way to communicate from chrome that the CDM has + // failed to download, so we'll just let the timeout fail us in that case. + nsTArray> requests; + for (size_t i = mPendingInstallRequests.Length(); i-- > 0;) { + nsAutoCString message; + MediaKeySystemStatus status = MediaKeySystemAccess::GetKeySystemStatus( + *mPendingInstallRequests[i], message); + if (status == MediaKeySystemStatus::Cdm_not_installed) { + // Not yet installed, don't retry. Keep waiting until timeout. + continue; + } + // Status has changed, retry request. + requests.AppendElement(std::move(mPendingInstallRequests[i])); + mPendingInstallRequests.RemoveElementAt(i); + } + // Retry all pending requests, but this time fail if the CDM is not + // installed. + for (size_t i = requests.Length(); i-- > 0;) { + RetryRequest(std::move(requests[i])); + } + } else if (!strcmp(aTopic, "timer-callback")) { + // Find the timer that expired and re-run the request for it. + nsCOMPtr timer(do_QueryInterface(aSubject)); + for (size_t i = 0; i < mPendingInstallRequests.Length(); i++) { + if (mPendingInstallRequests[i]->mTimer == timer) { + EME_LOG("MediaKeySystemAccessManager::AwaitInstall resuming request"); + UniquePtr request = + std::move(mPendingInstallRequests[i]); + mPendingInstallRequests.RemoveElementAt(i); + RetryRequest(std::move(request)); + break; + } + } + } + return NS_OK; +} + +nsresult MediaKeySystemAccessManager::GetName(nsACString& aName) { + aName.AssignLiteral("MediaKeySystemAccessManager"); + return NS_OK; +} + +bool MediaKeySystemAccessManager::EnsureObserversAdded() { + MOZ_ASSERT(NS_IsMainThread()); + if (mAddedObservers) { + return true; + } + + nsCOMPtr obsService = + mozilla::services::GetObserverService(); + if (NS_WARN_IF(!obsService)) { + return false; + } + mAddedObservers = + NS_SUCCEEDED(obsService->AddObserver(this, "gmp-changed", false)); + return mAddedObservers; +} + +void MediaKeySystemAccessManager::Shutdown() { + MOZ_ASSERT(NS_IsMainThread()); + MKSAM_LOG_DEBUG(""); + for (const UniquePtr& installRequest : + mPendingInstallRequests) { + // Cancel all requests; we're shutting down. + installRequest->CancelTimer(); + installRequest->RejectPromiseWithInvalidAccessError(nsLiteralCString( + "Promise still outstanding at MediaKeySystemAccessManager shutdown")); + } + mPendingInstallRequests.Clear(); + for (const UniquePtr& approvalRequest : + mPendingAppApprovalRequests) { + approvalRequest->RejectPromiseWithInvalidAccessError(nsLiteralCString( + "Promise still outstanding at MediaKeySystemAccessManager shutdown")); + } + mPendingAppApprovalRequests.Clear(); + mAppAllowsProtectedMediaPromiseRequest.DisconnectIfExists(); + if (mAddedObservers) { + nsCOMPtr obsService = + mozilla::services::GetObserverService(); + if (obsService) { + obsService->RemoveObserver(this, "gmp-changed"); + mAddedObservers = false; + } + } +} + +} // namespace mozilla::dom + +#undef MKSAM_LOG_DEBUG diff --git a/dom/media/eme/MediaKeySystemAccessManager.h b/dom/media/eme/MediaKeySystemAccessManager.h new file mode 100644 index 0000000000..77feded701 --- /dev/null +++ b/dom/media/eme/MediaKeySystemAccessManager.h @@ -0,0 +1,237 @@ +/* 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 DOM_MEDIA_MEDIAKEYSYSTEMACCESSMANAGER_H_ +#define DOM_MEDIA_MEDIAKEYSYSTEMACCESSMANAGER_H_ + +#include "mozilla/dom/MediaKeySystemAccess.h" +#include "mozilla/MozPromise.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIObserver.h" +#include "nsISupportsImpl.h" +#include "nsITimer.h" + +namespace mozilla::dom { + +class DetailedPromise; +class TestGMPVideoDecoder; + +/** + * MediaKeySystemAccessManager implements the functionality for + * Navigator.requestMediaKeySystemAccess(). The navigator may perform its own + * logic before passing the request to this class, but the majority of + * processing happens the MediaKeySystemAccessManager. The manager is expected + * to be run entirely on the main thread of the content process for whichever + * window it is associated with. + * + * As well as implementing the Navigator.requestMediaKeySystemAccess() + * algorithm, the manager performs Gecko specific logic. For example, the EME + * specification does not specify a process to check if a CDM is installed as + * part of requesting access, but that is an important part of obtaining access + * for Gecko, and is handled by the manager. + * + * A request made to the manager can be thought of as entering a pipeline. + * In this pipeline the request must pass through various stages that can + * reject the request and remove it from the pipeline. If a request is not + * rejected by the end of the pipeline it is approved/resolved. + * + * The pipeline is structured in such a way that each step should be executed + * even if it will quickly be exited. For example, the step that checks if a + * window supports protected media is an instant approve on non-Windows OSes, + * but we want to execute the function representing that step to ensure a + * deterministic execution and logging path. The hope is this reduces + * complexity for when we need to debug or change the code. + * + * While the pipeline metaphor generally holds, the implementation details of + * the manager mean that processing is not always linear: a request may be + * re-injected earlier into the pipeline for reprocessing. This can happen + * if the request was pending some other operation, e.g. CDM install, after + * which we wish to reprocess that request. However, we strive to keep it + * as linear as possible. + * + * A high level version of the happy path pipeline is depicted below. If a + * request were to fail any of the steps below it would be rejected and ejected + * from the pipeline. + * + * Request arrives from navigator + * + + * | + * v + * Check if window supports protected media + * + + * +<-------------------+ + * v | + * Check request args are sane | + * + | + * | Wait for CDM and retry + * v | + * Check if CDM is installed | + * + | + * | | + * +--------------------+ + * | + * v + * Check if CDM supports args + * + + * | + * v + * Check if app allows protected media + * (used by GeckoView) + * + + * | + * v + * Provide access + * + */ + +struct MediaKeySystemAccessRequest { + MediaKeySystemAccessRequest( + const nsAString& aKeySystem, + const Sequence& aConfigs) + : mKeySystem(aKeySystem), mConfigs(aConfigs) {} + virtual ~MediaKeySystemAccessRequest() = default; + // The KeySystem passed for this request. + const nsString mKeySystem; + // The config(s) passed for this request. + const Sequence mConfigs; +}; + +class MediaKeySystemAccessManager final : public nsIObserver, public nsINamed { + public: + explicit MediaKeySystemAccessManager(nsPIDOMWindowInner* aWindow); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(MediaKeySystemAccessManager, + nsIObserver) + NS_DECL_NSIOBSERVER + NS_DECL_NSINAMED + + // Entry point for the navigator to call into the manager. + void Request(DetailedPromise* aPromise, const nsAString& aKeySystem, + const Sequence& aConfig); + + void Shutdown(); + + private: + // Encapsulates the information for a Navigator.requestMediaKeySystemAccess() + // request that is being processed. + struct PendingRequest : public MediaKeySystemAccessRequest { + enum class RequestType { Initial, Subsequent }; + + PendingRequest(DetailedPromise* aPromise, const nsAString& aKeySystem, + const Sequence& aConfigs); + ~PendingRequest(); + + // The JS promise associated with this request. + RefPtr mPromise; + + // If the request is + // - A first attempt request from JS: RequestType::Initial. + // - A request we're reprocessing due to a GMP being installed: + // RequestType::Subsequent. + RequestType mRequestType = RequestType::Initial; + + // If we find a supported config for this request during processing it + // should be stored here. Only if we have a supported config should a + // request have access provided. + Maybe mSupportedConfig; + + // Will be set to trigger a timeout and re-processing of the request if the + // request is pending on some potentially time consuming operation, e.g. + // CDM install. + nsCOMPtr mTimer = nullptr; + + // Convenience methods to reject the wrapped promise. + void RejectPromiseWithInvalidAccessError(const nsACString& aReason); + void RejectPromiseWithNotSupportedError(const nsACString& aReason); + void RejectPromiseWithTypeError(const nsACString& aReason); + + void CancelTimer(); + }; + + // Check if the application (e.g. a GeckoView app) allows protected media in + // this window. + // + // This function is always expected to be executed as part of the pipeline of + // processing a request, but its behavior differs depending on prefs set. + // + // If the `media_eme_require_app_approval` pref is false, then the function + // assumes app approval and early returns. Otherwise the function will + // create a permission request to be approved by the embedding app. If the + // test prefs detailed in MediaKeySystemAccessPermissionRequest.h are set + // then they will control handling, otherwise it is up to the embedding + // app to handle the request. + // + // At the time of writing, only GeckoView based apps are expected to pref + // on this behavior. + // + // This function is expected to run late/last in the pipeline so that if we + // ask the app for permission we don't fail after the app okays the request. + // This is to avoid cases where a user may be prompted by the app to approve + // eme, this check then passes, but we fail later in the pipeline, leaving + // the user wondering why their approval didn't work. + void CheckDoesAppAllowProtectedMedia(UniquePtr aRequest); + + // Handles the result of the app allowing or disallowing protected media. + // If there are pending requests in mPendingAppApprovalRequests then this + // needs to be called on each. + void OnDoesAppAllowProtectedMedia(bool aIsAllowed, + UniquePtr aRequest); + + // Checks if the Window associated with this manager supports protected media + // and calls into OnDoesWindowSupportEncryptedMedia with the result. + void CheckDoesWindowSupportProtectedMedia(UniquePtr aRequest); + + // Handle the result of checking if the window associated with this manager + // supports protected media. If the window doesn't support protected media + // this will reject the request, otherwise the request will continue to be + // processed. + void OnDoesWindowSupportProtectedMedia(bool aIsSupportedInWindow, + UniquePtr aRequest); + + // Performs the 'requestMediaKeySystemAccess' algorithm detailed in the EME + // specification. Gecko may need to install a CDM to satisfy this check. If + // CDM install is needed this function may be called again for the same + // request once the CDM is installed or a timeout is reached. + void RequestMediaKeySystemAccess(UniquePtr aRequest); + + // Approves aRequest and provides MediaKeySystemAccess by resolving the + // promise associated with the request. + void ProvideAccess(UniquePtr aRequest); + + ~MediaKeySystemAccessManager(); + + bool EnsureObserversAdded(); + + bool AwaitInstall(UniquePtr aRequest); + + void RetryRequest(UniquePtr aRequest); + + // Requests waiting on approval from the application to be processed. + nsTArray> mPendingAppApprovalRequests; + + // Requests waiting on CDM installation to be processed. + nsTArray> mPendingInstallRequests; + + nsCOMPtr mWindow; + bool mAddedObservers = false; + + // Has the app approved protected media playback? If it has we cache the + // value so we don't need to check again. + Maybe mAppAllowsProtectedMedia; + + // If we're waiting for permission from the app to enable EME this holder + // should contain the request. + // + // Note the type in the holder should match + // MediaKeySystemAccessPermissionRequest::RequestPromise, but we can't + // include MediaKeySystemAccessPermissionRequest's header here without + // breaking the build, so we do this hack. + MozPromiseRequestHolder> + mAppAllowsProtectedMediaPromiseRequest; +}; + +} // namespace mozilla::dom + +#endif // DOM_MEDIA_MEDIAKEYSYSTEMACCESSMANAGER_H_ diff --git a/dom/media/eme/MediaKeySystemAccessPermissionRequest.cpp b/dom/media/eme/MediaKeySystemAccessPermissionRequest.cpp new file mode 100644 index 0000000000..063bf93f7e --- /dev/null +++ b/dom/media/eme/MediaKeySystemAccessPermissionRequest.cpp @@ -0,0 +1,91 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "MediaKeySystemAccessPermissionRequest.h" + +#include "nsGlobalWindowInner.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaKeySystemAccessPermissionRequest, + ContentPermissionRequestBase) + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0( + MediaKeySystemAccessPermissionRequest, ContentPermissionRequestBase) + +/* static */ +already_AddRefed +MediaKeySystemAccessPermissionRequest::Create(nsPIDOMWindowInner* aWindow) { + // Could conceivably be created off main thread then used on main thread + // later. If we really need to do that at some point we could relax this + // assert. + AssertIsOnMainThread(); + if (!aWindow) { + return nullptr; + } + + nsGlobalWindowInner* innerWindow = nsGlobalWindowInner::Cast(aWindow); + if (!innerWindow->GetPrincipal()) { + return nullptr; + } + + RefPtr request = + new MediaKeySystemAccessPermissionRequest(innerWindow); + return request.forget(); +} + +MediaKeySystemAccessPermissionRequest::MediaKeySystemAccessPermissionRequest( + nsGlobalWindowInner* aWindow) + : ContentPermissionRequestBase(aWindow->GetPrincipal(), aWindow, + "media.eme.require-app-approval"_ns, + "media-key-system-access"_ns) {} + +MediaKeySystemAccessPermissionRequest:: + ~MediaKeySystemAccessPermissionRequest() { + AssertIsOnMainThread(); + // If a request has not been serviced by the time it is destroyed, treat it + // as if the request was denied. + Cancel(); +} + +already_AddRefed +MediaKeySystemAccessPermissionRequest::GetPromise() { + return mPromiseHolder.Ensure(__func__); +} + +nsresult MediaKeySystemAccessPermissionRequest::Start() { + // Check test prefs to see if we should short circuit. We want to do this + // before checking the cached value so we can have pref changes take effect + // without refreshing the page. + MediaKeySystemAccessPermissionRequest::PromptResult promptResult = + CheckPromptPrefs(); + if (promptResult == + MediaKeySystemAccessPermissionRequest::PromptResult::Granted) { + return Allow(JS::UndefinedHandleValue); + } + if (promptResult == + MediaKeySystemAccessPermissionRequest::PromptResult::Denied) { + return Cancel(); + } + + return nsContentPermissionUtils::AskPermission(this, mWindow); +} + +NS_IMETHODIMP +MediaKeySystemAccessPermissionRequest::Allow(JS::Handle aChoices) { + AssertIsOnMainThread(); + mPromiseHolder.ResolveIfExists(true, __func__); + return NS_OK; +} + +NS_IMETHODIMP +MediaKeySystemAccessPermissionRequest::Cancel() { + AssertIsOnMainThread(); + mPromiseHolder.RejectIfExists(false, __func__); + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/media/eme/MediaKeySystemAccessPermissionRequest.h b/dom/media/eme/MediaKeySystemAccessPermissionRequest.h new file mode 100644 index 0000000000..088cf50ab6 --- /dev/null +++ b/dom/media/eme/MediaKeySystemAccessPermissionRequest.h @@ -0,0 +1,74 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 DOM_MEDIA_EME_MEDIAKEYSYSTEMACCESSPERMISSIONREQUEST_H_ +#define DOM_MEDIA_EME_MEDIAKEYSYSTEMACCESSPERMISSIONREQUEST_H_ + +#include "mozilla/MozPromise.h" +#include "nsContentPermissionHelper.h" + +class nsGlobalWindowInner; + +namespace mozilla::dom { + +/** + * This class encapsulates a permission request to allow media key system + * access. The intention is not for this class to be used in all cases of EME, + * but only when we need to seek explicit approval from an application using + * Gecko, such as an application embedding via GeckoView. + * + * media.eme.require-app-approval should be used to gate this functionality in + * gecko code, and is also used as the testing pref for + * ContentPermissionRequestBase. I.e. CheckPromptPrefs() will respond to having + * `media.eme.require-app-approval.prompt.testing` and + * `media.eme.require-app-approval.prompt.testing.allow` being set to true or + * false and will return an appropriate value to allow for test code to short + * circuit showing a prompt. Note that the code using this class needs to call + * CheckPromptPrefs and implement test specific logic, it is *not* handled by + * this class or ContentPermissionRequestBase. + * + * Expects to be used on main thread as ContentPermissionRequestBase uses + * PContentPermissionRequest which is managed by PContent which is main thread + * to main thread communication. + */ +class MediaKeySystemAccessPermissionRequest + : public ContentPermissionRequestBase { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED( + MediaKeySystemAccessPermissionRequest, ContentPermissionRequestBase) + + using RequestPromise = MozPromise; + + // Create a MediaKeySystemAccessPermissionRequest. + // @param aWindow The window associated with this request. + static already_AddRefed Create( + nsPIDOMWindowInner* aWindow); + + // Returns a promise that will be resolved if this request is allowed or + // rejected in the case the request is denied. If allowed the promise + // will resolve with true, otherwise it will resolve with false. + already_AddRefed GetPromise(); + + // Helper function that triggers the request. This function will check + // prefs and cancel or allow the request if the appropriate prefs are set, + // otherwise it will fire the request to the associated window. + nsresult Start(); + + // nsIContentPermissionRequest methods + NS_IMETHOD Cancel(void) override; + NS_IMETHOD Allow(JS::Handle choices) override; + + private: + explicit MediaKeySystemAccessPermissionRequest(nsGlobalWindowInner* aWindow); + ~MediaKeySystemAccessPermissionRequest(); + + MozPromiseHolder mPromiseHolder; +}; + +} // namespace mozilla::dom + +#endif // DOM_MEDIA_EME_MEDIAKEYSYSTEMACCESSPERMISSIONREQUEST_H_ diff --git a/dom/media/eme/MediaKeys.cpp b/dom/media/eme/MediaKeys.cpp new file mode 100644 index 0000000000..c4340885a3 --- /dev/null +++ b/dom/media/eme/MediaKeys.cpp @@ -0,0 +1,847 @@ +/* -*- 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 "mozilla/dom/MediaKeys.h" + +#include "ChromiumCDMProxy.h" +#include "GMPCrashHelper.h" +#include "mozilla/EMEUtils.h" +#include "mozilla/JSONStringWriteFuncs.h" +#include "mozilla/Telemetry.h" +#include "mozilla/dom/DOMException.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/HTMLMediaElement.h" +#include "mozilla/dom/MediaKeyError.h" +#include "mozilla/dom/MediaKeyMessageEvent.h" +#include "mozilla/dom/MediaKeySession.h" +#include "mozilla/dom/MediaKeyStatusMap.h" +#include "mozilla/dom/MediaKeySystemAccess.h" +#include "mozilla/dom/MediaKeysBinding.h" +#include "mozilla/dom/UnionTypes.h" +#include "mozilla/dom/WindowContext.h" +#include "mozilla/dom/WindowGlobalChild.h" +#include "nsContentCID.h" +#include "nsContentTypeParser.h" +#include "nsContentUtils.h" +#include "nsIScriptObjectPrincipal.h" +#include "nsPrintfCString.h" +#include "nsServiceManagerUtils.h" + +#ifdef MOZ_WIDGET_ANDROID +# include "mozilla/MediaDrmCDMProxy.h" +#endif +#ifdef XP_WIN +# include "mozilla/WindowsVersion.h" +#endif +#ifdef MOZ_WMF_CDM +# include "mozilla/WMFCDMProxy.h" +#endif + +namespace mozilla::dom { + +// We don't use NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE because we need to +// disconnect our MediaKeys instances from the inner window (mparent) before +// we unlink it. +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(MediaKeys) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(MediaKeys) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mElement) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mKeySessions) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromises) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingSessions) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(MediaKeys) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mElement) + tmp->DisconnectInnerWindow(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mKeySessions) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPromises) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingSessions) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER + NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_PTR +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaKeys) +NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaKeys) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeys) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) + NS_INTERFACE_MAP_ENTRY(nsIObserver) +NS_INTERFACE_MAP_END + +MediaKeys::MediaKeys(nsPIDOMWindowInner* aParent, const nsAString& aKeySystem, + const MediaKeySystemConfiguration& aConfig) + : mParent(aParent), + mKeySystem(aKeySystem), + mCreatePromiseId(0), + mConfig(aConfig) { + EME_LOG("MediaKeys[%p] constructed keySystem=%s", this, + NS_ConvertUTF16toUTF8(mKeySystem).get()); +} + +MediaKeys::~MediaKeys() { + MOZ_ASSERT(NS_IsMainThread()); + + DisconnectInnerWindow(); + Shutdown(); + EME_LOG("MediaKeys[%p] destroyed", this); +} + +NS_IMETHODIMP MediaKeys::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!strcmp(aTopic, kMediaKeysResponseTopic), + "Should only listen for responses to MediaKey requests"); + EME_LOG("MediaKeys[%p] observing message with aTopic=%s aData=%s", this, + aTopic, NS_ConvertUTF16toUTF8(aData).get()); + if (!strcmp(aTopic, kMediaKeysResponseTopic)) { + if (!mProxy) { + // This may happen if we're notified during shutdown or startup. If this + // is happening outside of those scenarios there's a bug. + EME_LOG( + "MediaKeys[%p] can't notify CDM of observed message as mProxy is " + "unset", + this); + return NS_OK; + } + + if (u"capture-possible"_ns.Equals(aData)) { + mProxy->NotifyOutputProtectionStatus( + CDMProxy::OutputProtectionCheckStatus::CheckSuccessful, + CDMProxy::OutputProtectionCaptureStatus::CapturePossilbe); + } else if (u"capture-not-possible"_ns.Equals(aData)) { + mProxy->NotifyOutputProtectionStatus( + CDMProxy::OutputProtectionCheckStatus::CheckSuccessful, + CDMProxy::OutputProtectionCaptureStatus::CaptureNotPossible); + } else { + MOZ_ASSERT_UNREACHABLE("No code paths should lead to the failure case"); + // This should be unreachable, but gracefully handle in case. + mProxy->NotifyOutputProtectionStatus( + CDMProxy::OutputProtectionCheckStatus::CheckFailed, + CDMProxy::OutputProtectionCaptureStatus::Unused); + } + } + return NS_OK; +} + +void MediaKeys::ConnectInnerWindow() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr innerWindowParent = GetParentObject(); + MOZ_ASSERT(innerWindowParent, + "We should only be connecting when we have an inner window!"); + innerWindowParent->AddMediaKeysInstance(this); +} + +void MediaKeys::DisconnectInnerWindow() { + MOZ_ASSERT(NS_IsMainThread()); + + if (!GetParentObject()) { + // We don't have a parent. We've been cycle collected, or the window + // already notified us of its destruction and we cleared the ref. + return; + } + + GetParentObject()->RemoveMediaKeysInstance(this); +} + +void MediaKeys::OnInnerWindowDestroy() { + MOZ_ASSERT(NS_IsMainThread()); + + EME_LOG("MediaKeys[%p] OnInnerWindowDestroy()", this); + + // The InnerWindow should clear its reference to this object after this call, + // so we don't need to explicitly call DisconnectInnerWindow before nulling. + mParent = nullptr; + + // Don't call shutdown directly because (at time of writing) mProxy can + // spin the event loop when it's shutdown. This can change the world state + // in the middle of window destruction, which we do not want. + GetMainThreadSerialEventTarget()->Dispatch( + NewRunnableMethod("MediaKeys::Shutdown", this, &MediaKeys::Shutdown)); +} + +void MediaKeys::Terminated() { + EME_LOG("MediaKeys[%p] CDM crashed unexpectedly", this); + + KeySessionHashMap keySessions; + // Remove entries during iteration will screw it. Make a copy first. + for (const RefPtr& session : mKeySessions.Values()) { + // XXX Could the RefPtr still be moved here? + keySessions.InsertOrUpdate(session->GetSessionId(), RefPtr{session}); + } + for (const RefPtr& session : keySessions.Values()) { + session->OnClosed(); + } + keySessions.Clear(); + MOZ_ASSERT(mKeySessions.Count() == 0); + + // Notify the element about that CDM has terminated. + if (mElement) { + mElement->DecodeError(NS_ERROR_DOM_MEDIA_CDM_ERR); + } + + Shutdown(); +} + +void MediaKeys::Shutdown() { + EME_LOG("MediaKeys[%p]::Shutdown()", this); + if (mProxy) { + mProxy->Shutdown(); + mProxy = nullptr; + } + + nsCOMPtr observerService = + mozilla::services::GetObserverService(); + if (observerService && mObserverAdded) { + observerService->RemoveObserver(this, kMediaKeysResponseTopic); + } + + // Hold a self reference to keep us alive after we clear the self reference + // for each promise. This ensures we stay alive until we're done shutting + // down. + RefPtr selfReference = this; + + for (const RefPtr& promise : mPromises.Values()) { + promise->MaybeRejectWithInvalidStateError( + "Promise still outstanding at MediaKeys shutdown"); + Release(); + } + mPromises.Clear(); +} + +nsPIDOMWindowInner* MediaKeys::GetParentObject() const { return mParent; } + +JSObject* MediaKeys::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return MediaKeys_Binding::Wrap(aCx, this, aGivenProto); +} + +void MediaKeys::GetKeySystem(nsString& aOutKeySystem) const { + aOutKeySystem.Assign(mKeySystem); +} + +already_AddRefed MediaKeys::SetServerCertificate( + const ArrayBufferViewOrArrayBuffer& aCert, ErrorResult& aRv) { + RefPtr promise( + MakePromise(aRv, "MediaKeys.setServerCertificate"_ns)); + if (aRv.Failed()) { + return nullptr; + } + + if (!mProxy) { + NS_WARNING("Tried to use a MediaKeys without a CDM"); + promise->MaybeRejectWithInvalidStateError( + "Null CDM in MediaKeys.setServerCertificate()"); + return promise.forget(); + } + + nsTArray data; + CopyArrayBufferViewOrArrayBufferData(aCert, data); + if (data.IsEmpty()) { + promise->MaybeRejectWithTypeError( + "Empty certificate passed to MediaKeys.setServerCertificate()"); + return promise.forget(); + } + + mProxy->SetServerCertificate(StorePromise(promise), data); + return promise.forget(); +} + +already_AddRefed MediaKeys::MakePromise( + ErrorResult& aRv, const nsACString& aName) { + nsCOMPtr global = do_QueryInterface(GetParentObject()); + if (!global) { + NS_WARNING("Passed non-global to MediaKeys ctor!"); + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + return DetailedPromise::Create(global, aRv, aName); +} + +PromiseId MediaKeys::StorePromise(DetailedPromise* aPromise) { + static uint32_t sEMEPromiseCount = 1; + MOZ_ASSERT(aPromise); + uint32_t id = sEMEPromiseCount++; + + EME_LOG("MediaKeys[%p]::StorePromise() id=%" PRIu32, this, id); + + // Keep MediaKeys alive for the lifetime of its promises. Any still-pending + // promises are rejected in Shutdown(). + EME_LOG("MediaKeys[%p]::StorePromise() calling AddRef()", this); + AddRef(); + +#ifdef DEBUG + // We should not have already stored this promise! + for (const RefPtr& promise : mPromises.Values()) { + MOZ_ASSERT(promise != aPromise); + } +#endif + + mPromises.InsertOrUpdate(id, RefPtr{aPromise}); + return id; +} + +void MediaKeys::ConnectPendingPromiseIdWithToken(PromiseId aId, + uint32_t aToken) { + // Should only be called from MediaKeySession::GenerateRequest. + mPromiseIdToken.InsertOrUpdate(aId, aToken); + EME_LOG( + "MediaKeys[%p]::ConnectPendingPromiseIdWithToken() id=%u => token(%u)", + this, aId, aToken); +} + +already_AddRefed MediaKeys::RetrievePromise(PromiseId aId) { + EME_LOG("MediaKeys[%p]::RetrievePromise(aId=%" PRIu32 ")", this, aId); + if (!mPromises.Contains(aId)) { + EME_LOG("MediaKeys[%p]::RetrievePromise(aId=%" PRIu32 + ") tried to retrieve non-existent promise!", + this, aId); + NS_WARNING(nsPrintfCString( + "Tried to retrieve a non-existent promise id=%" PRIu32, aId) + .get()); + return nullptr; + } + RefPtr promise; + mPromises.Remove(aId, getter_AddRefs(promise)); + EME_LOG("MediaKeys[%p]::RetrievePromise(aId=%" PRIu32 ") calling Release()", + this, aId); + Release(); + return promise.forget(); +} + +void MediaKeys::RejectPromise(PromiseId aId, ErrorResult&& aException, + const nsCString& aReason) { + uint32_t errorCodeAsInt = aException.ErrorCodeAsInt(); + EME_LOG("MediaKeys[%p]::RejectPromise(%" PRIu32 ", 0x%" PRIx32 ")", this, aId, + errorCodeAsInt); + + RefPtr promise(RetrievePromise(aId)); + if (!promise) { + EME_LOG("MediaKeys[%p]::RejectPromise(%" PRIu32 ", 0x%" PRIx32 + ") couldn't retrieve promise! Bailing!", + this, aId, errorCodeAsInt); + return; + } + + // This promise could be a createSession or loadSession promise, + // so we might have a pending session waiting to be resolved into + // the promise on success. We've been directed to reject to promise, + // so we can throw away the corresponding session object. + uint32_t token = 0; + if (mPromiseIdToken.Get(aId, &token)) { + MOZ_ASSERT(mPendingSessions.Contains(token)); + mPendingSessions.Remove(token); + mPromiseIdToken.Remove(aId); + } + + MOZ_ASSERT(aException.Failed()); + promise->MaybeReject(std::move(aException), aReason); + + if (mCreatePromiseId == aId) { + // Note: This will probably destroy the MediaKeys object! + EME_LOG("MediaKeys[%p]::RejectPromise(%" PRIu32 ", 0x%" PRIx32 + ") calling Release()", + this, aId, errorCodeAsInt); + Release(); + } +} + +void MediaKeys::OnSessionIdReady(MediaKeySession* aSession) { + if (!aSession) { + NS_WARNING("Invalid MediaKeySession passed to OnSessionIdReady()"); + return; + } + if (mKeySessions.Contains(aSession->GetSessionId())) { + NS_WARNING("MediaKeySession's made ready multiple times!"); + return; + } + if (mPendingSessions.Contains(aSession->Token())) { + NS_WARNING( + "MediaKeySession made ready when it wasn't waiting to be ready!"); + return; + } + if (aSession->GetSessionId().IsEmpty()) { + NS_WARNING( + "MediaKeySession with invalid sessionId passed to OnSessionIdReady()"); + return; + } + mKeySessions.InsertOrUpdate(aSession->GetSessionId(), RefPtr{aSession}); +} + +void MediaKeys::ResolvePromise(PromiseId aId) { + EME_LOG("MediaKeys[%p]::ResolvePromise(%" PRIu32 ")", this, aId); + + RefPtr promise(RetrievePromise(aId)); + MOZ_ASSERT(!mPromises.Contains(aId)); + if (!promise) { + return; + } + + uint32_t token = 0; + if (!mPromiseIdToken.Get(aId, &token)) { + promise->MaybeResolveWithUndefined(); + return; + } else if (!mPendingSessions.Contains(token)) { + // Pending session for CreateSession() should be removed when sessionId + // is ready. + promise->MaybeResolveWithUndefined(); + mPromiseIdToken.Remove(aId); + return; + } + mPromiseIdToken.Remove(aId); + + // We should only resolve LoadSession calls via this path, + // not CreateSession() promises. + RefPtr session; + mPendingSessions.Remove(token, getter_AddRefs(session)); + if (!session || session->GetSessionId().IsEmpty()) { + NS_WARNING("Received activation for non-existent session!"); + promise->MaybeRejectWithInvalidAccessError( + "CDM LoadSession() returned a different session ID than requested"); + return; + } + mKeySessions.InsertOrUpdate(session->GetSessionId(), RefPtr{session}); + promise->MaybeResolve(session); +} + +class MediaKeysGMPCrashHelper : public GMPCrashHelper { + public: + explicit MediaKeysGMPCrashHelper(MediaKeys* aMediaKeys) + : mMediaKeys(aMediaKeys) { + MOZ_ASSERT(NS_IsMainThread()); // WeakPtr isn't thread safe. + } + already_AddRefed GetPluginCrashedEventTarget() override { + MOZ_ASSERT(NS_IsMainThread()); // WeakPtr isn't thread safe. + EME_LOG("MediaKeysGMPCrashHelper::GetPluginCrashedEventTarget()"); + return (mMediaKeys && mMediaKeys->GetParentObject()) + ? do_AddRef(mMediaKeys->GetParentObject()) + : nullptr; + } + + private: + WeakPtr mMediaKeys; +}; + +already_AddRefed MediaKeys::CreateCDMProxy() { + const bool isHardwareDecryptionSupported = + IsHardwareDecryptionSupported(mConfig); + EME_LOG("MediaKeys[%p]::CreateCDMProxy(), isHardwareDecryptionSupported=%d", + this, isHardwareDecryptionSupported); + RefPtr proxy; +#ifdef MOZ_WIDGET_ANDROID + if (IsWidevineKeySystem(mKeySystem)) { + proxy = new MediaDrmCDMProxy( + this, mKeySystem, + mConfig.mDistinctiveIdentifier == MediaKeysRequirement::Required, + mConfig.mPersistentState == MediaKeysRequirement::Required); + } else +#endif +#ifdef MOZ_WMF_CDM + if (IsPlayReadyKeySystemAndSupported(mKeySystem) || + IsWidevineExperimentKeySystemAndSupported(mKeySystem) || + (IsWidevineKeySystem(mKeySystem) && isHardwareDecryptionSupported) || + IsWMFClearKeySystemAndSupported(mKeySystem)) { + proxy = new WMFCDMProxy(this, mKeySystem, mConfig); + } else +#endif + { + proxy = new ChromiumCDMProxy( + this, mKeySystem, new MediaKeysGMPCrashHelper(this), + mConfig.mDistinctiveIdentifier == MediaKeysRequirement::Required, + mConfig.mPersistentState == MediaKeysRequirement::Required); + } + return proxy.forget(); +} + +already_AddRefed MediaKeys::Init(ErrorResult& aRv) { + EME_LOG("MediaKeys[%p]::Init()", this); + RefPtr promise(MakePromise(aRv, "MediaKeys::Init()"_ns)); + if (aRv.Failed()) { + return nullptr; + } + + // Determine principal (at creation time) of the MediaKeys object. + nsCOMPtr sop = do_QueryInterface(GetParentObject()); + if (!sop) { + promise->MaybeRejectWithInvalidStateError( + "Couldn't get script principal in MediaKeys::Init"); + return promise.forget(); + } + mPrincipal = sop->GetPrincipal(); + + // Begin figuring out the top level principal. + nsCOMPtr window = GetParentObject(); + + // If we're in a top level document, getting the top level principal is easy. + // However, we're not in a top level doc this becomes more complicated. If + // we're not top level we need to get the top level principal, this can be + // done by reading the principal of the load info, which we can get of a + // document's channel. + // + // There is an edge case we need to watch out for here where this code can be + // run in an about:blank document before it has done its async load. In this + // case the document will not yet have a load info. We address this below by + // walking up a level in the window context chain. See + // https://bugzilla.mozilla.org/show_bug.cgi?id=1675360 + // for more info. + Document* document = window->GetExtantDoc(); + if (!document) { + NS_WARNING("Failed to get document when creating MediaKeys"); + promise->MaybeRejectWithInvalidStateError( + "Couldn't get document in MediaKeys::Init"); + return promise.forget(); + } + + WindowGlobalChild* windowGlobalChild = window->GetWindowGlobalChild(); + if (!windowGlobalChild) { + NS_WARNING("Failed to get window global child when creating MediaKeys"); + promise->MaybeRejectWithInvalidStateError( + "Couldn't get window global child in MediaKeys::Init"); + return promise.forget(); + } + + if (windowGlobalChild->SameOriginWithTop()) { + // We're in the same origin as the top window context, so our principal + // is also the top principal. + mTopLevelPrincipal = mPrincipal; + } else { + // We have a different origin than the top doc, try and find the top level + // principal by looking it up via load info, which we read off a channel. + nsIChannel* channel = document->GetChannel(); + + WindowContext* windowContext = document->GetWindowContext(); + if (!windowContext) { + NS_WARNING("Failed to get window context when creating MediaKeys"); + promise->MaybeRejectWithInvalidStateError( + "Couldn't get window context in MediaKeys::Init"); + return promise.forget(); + } + while (!channel) { + // We don't have a channel, this can happen if we're in an about:blank + // page that hasn't yet had its async load performed. Try and get + // the channel from our parent doc. We should be able to do this because + // an about:blank is considered the same origin as its parent. We do this + // recursively to cover pages do silly things like nesting blank iframes + // and not waiting for loads. + + // Move our window context up a level. + windowContext = windowContext->GetParentWindowContext(); + if (!windowContext || !windowContext->GetExtantDoc()) { + NS_WARNING( + "Failed to get parent window context's document when creating " + "MediaKeys"); + promise->MaybeRejectWithInvalidStateError( + "Couldn't get parent window context's document in " + "MediaKeys::Init (likely due to an nested about about:blank frame " + "that hasn't loaded yet)"); + return promise.forget(); + } + + Document* parentDoc = windowContext->GetExtantDoc(); + channel = parentDoc->GetChannel(); + } + + MOZ_RELEASE_ASSERT( + channel, "Should either have a channel or should have returned by now"); + + nsCOMPtr loadInfo = channel->LoadInfo(); + MOZ_RELEASE_ASSERT(loadInfo, "Channels should always have LoadInfo"); + mTopLevelPrincipal = loadInfo->GetTopLevelPrincipal(); + if (!mTopLevelPrincipal) { + NS_WARNING("Failed to get top level principal when creating MediaKeys"); + promise->MaybeRejectWithInvalidStateError( + "Couldn't get top level principal in MediaKeys::Init"); + return promise.forget(); + } + } + + // We should have figured out our top level principal. + if (!mPrincipal || !mTopLevelPrincipal) { + NS_WARNING("Failed to get principals when creating MediaKeys"); + promise->MaybeRejectWithInvalidStateError( + "Couldn't get principal(s) in MediaKeys::Init"); + return promise.forget(); + } + + nsAutoCString origin; + nsresult rv = mPrincipal->GetOrigin(origin); + if (NS_FAILED(rv)) { + promise->MaybeRejectWithInvalidStateError( + "Couldn't get principal origin string in MediaKeys::Init"); + return promise.forget(); + } + nsAutoCString topLevelOrigin; + rv = mTopLevelPrincipal->GetOrigin(topLevelOrigin); + if (NS_FAILED(rv)) { + promise->MaybeRejectWithInvalidStateError( + "Couldn't get top-level principal origin string in MediaKeys::Init"); + return promise.forget(); + } + + EME_LOG("MediaKeys[%p]::Create() (%s, %s)", this, origin.get(), + topLevelOrigin.get()); + + mProxy = CreateCDMProxy(); + + // The CDMProxy's initialization is asynchronous. The MediaKeys is + // refcounted, and its instance is returned to JS by promise once + // it's been initialized. No external refs exist to the MediaKeys while + // we're waiting for the promise to be resolved, so we must hold a + // reference to the new MediaKeys object until it's been created, + // or its creation has failed. Store the id of the promise returned + // here, and hold a self-reference until that promise is resolved or + // rejected. + MOZ_ASSERT(!mCreatePromiseId, "Should only be created once!"); + mCreatePromiseId = StorePromise(promise); + EME_LOG("MediaKeys[%p]::Init() calling AddRef()", this); + AddRef(); + mProxy->Init(mCreatePromiseId, NS_ConvertUTF8toUTF16(origin), + NS_ConvertUTF8toUTF16(topLevelOrigin), + KeySystemToProxyName(mKeySystem)); + + ConnectInnerWindow(); + + return promise.forget(); +} + +void MediaKeys::OnCDMCreated(PromiseId aId, const uint32_t aPluginId) { + EME_LOG("MediaKeys[%p]::OnCDMCreated(aId=%" PRIu32 ", aPluginId=%" PRIu32 ")", + this, aId, aPluginId); + RefPtr promise(RetrievePromise(aId)); + if (!promise) { + return; + } + RefPtr keys(this); + + promise->MaybeResolve(keys); + if (mCreatePromiseId == aId) { + EME_LOG("MediaKeys[%p]::OnCDMCreated(aId=%" PRIu32 ", aPluginId=%" PRIu32 + ") calling Release()", + this, aId, aPluginId); + Release(); + } + + MediaKeySystemAccess::NotifyObservers(mParent, mKeySystem, + MediaKeySystemStatus::Cdm_created); +} + +static bool IsSessionTypeSupported(const MediaKeySessionType aSessionType, + const MediaKeySystemConfiguration& aConfig) { + if (aSessionType == MediaKeySessionType::Temporary) { + // Temporary is always supported. + return true; + } + if (!aConfig.mSessionTypes.WasPassed()) { + // No other session types supported. + return false; + } + return aConfig.mSessionTypes.Value().Contains(ToString(aSessionType)); +} + +already_AddRefed MediaKeys::CreateSession( + MediaKeySessionType aSessionType, ErrorResult& aRv) { + EME_LOG("MediaKeys[%p]::CreateSession(aSessionType=%" PRIu8 ")", this, + static_cast(aSessionType)); + if (!IsSessionTypeSupported(aSessionType, mConfig)) { + EME_LOG("MediaKeys[%p]::CreateSession() failed, unsupported session type", + this); + aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + return nullptr; + } + + if (!mProxy) { + NS_WARNING("Tried to use a MediaKeys which lost its CDM"); + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + EME_LOG("MediaKeys[%p] Creating session", this); + + RefPtr session = new MediaKeySession( + GetParentObject(), this, mKeySystem, aSessionType, aRv); + + if (aRv.Failed()) { + return nullptr; + } + DDLINKCHILD("session", session.get()); + + // Add session to the set of sessions awaiting their sessionId being ready. + EME_LOG("MediaKeys[%p]::CreateSession(aSessionType=%" PRIu8 + ") putting session with token=%" PRIu32 " into mPendingSessions", + this, static_cast(aSessionType), session->Token()); + mPendingSessions.InsertOrUpdate(session->Token(), RefPtr{session}); + + return session.forget(); +} + +void MediaKeys::OnSessionLoaded(PromiseId aId, bool aSuccess) { + EME_LOG("MediaKeys[%p]::OnSessionLoaded() resolve promise id=%" PRIu32, this, + aId); + + ResolvePromiseWithResult(aId, aSuccess); +} + +void MediaKeys::OnSessionClosed(MediaKeySession* aSession) { + nsAutoString id; + aSession->GetSessionId(id); + mKeySessions.Remove(id); +} + +already_AddRefed MediaKeys::GetSession( + const nsAString& aSessionId) { + RefPtr session; + mKeySessions.Get(aSessionId, getter_AddRefs(session)); + return session.forget(); +} + +already_AddRefed MediaKeys::GetPendingSession( + uint32_t aToken) { + EME_LOG("MediaKeys[%p]::GetPendingSession(aToken=%" PRIu32 ")", this, aToken); + RefPtr session; + mPendingSessions.Get(aToken, getter_AddRefs(session)); + mPendingSessions.Remove(aToken); + return session.forget(); +} + +bool MediaKeys::IsBoundToMediaElement() const { + MOZ_ASSERT(NS_IsMainThread()); + return mElement != nullptr; +} + +nsresult MediaKeys::Bind(HTMLMediaElement* aElement) { + MOZ_ASSERT(NS_IsMainThread()); + if (IsBoundToMediaElement()) { + return NS_ERROR_FAILURE; + } + + mElement = aElement; + + return NS_OK; +} + +void MediaKeys::Unbind() { + MOZ_ASSERT(NS_IsMainThread()); + mElement = nullptr; +} + +void MediaKeys::CheckIsElementCapturePossible() { + MOZ_ASSERT(NS_IsMainThread()); + EME_LOG("MediaKeys[%p]::IsElementCapturePossible()", this); + // Note, HTMLMediaElement prevents capture of its content via Capture APIs + // on the element if it has a media keys attached (see bug 1071482). So we + // don't need to check those cases here (they are covered by tests). + + nsCOMPtr observerService = + mozilla::services::GetObserverService(); + + if (!observerService) { + // This can happen if we're in shutdown which means we may be going away + // soon anyway, but respond saying capture is possible since we can't + // forward the check further. + if (mProxy) { + mProxy->NotifyOutputProtectionStatus( + CDMProxy::OutputProtectionCheckStatus::CheckFailed, + CDMProxy::OutputProtectionCaptureStatus::Unused); + } + return; + } + if (!mObserverAdded) { + nsresult rv = + observerService->AddObserver(this, kMediaKeysResponseTopic, false); + if (NS_FAILED(rv)) { + if (mProxy) { + mProxy->NotifyOutputProtectionStatus( + CDMProxy::OutputProtectionCheckStatus::CheckFailed, + CDMProxy::OutputProtectionCaptureStatus::Unused); + } + return; + } + mObserverAdded = true; + } + + if (mCaptureCheckRequestJson.IsEmpty()) { + // Lazily populate the JSON the first time we need it. + JSONStringWriteFunc json; + JSONWriter jw{json}; + jw.Start(); + jw.StringProperty("status", "is-capture-possible"); + jw.StringProperty("keySystem", NS_ConvertUTF16toUTF8(mKeySystem)); + jw.End(); + mCaptureCheckRequestJson = NS_ConvertUTF8toUTF16(json.StringCRef()); + } + + MOZ_DIAGNOSTIC_ASSERT(!mCaptureCheckRequestJson.IsEmpty()); + observerService->NotifyObservers(mParent.get(), kMediaKeysRequestTopic, + mCaptureCheckRequestJson.get()); +} + +void MediaKeys::GetSessionsInfo(nsString& sessionsInfo) { + for (const auto& keySession : mKeySessions.Values()) { + nsString sessionID; + keySession->GetSessionId(sessionID); + sessionsInfo.AppendLiteral("(sid="); + sessionsInfo.Append(sessionID); + MediaKeyStatusMap* keyStatusMap = keySession->KeyStatuses(); + for (uint32_t i = 0; i < keyStatusMap->GetIterableLength(); i++) { + nsString keyID = keyStatusMap->GetKeyIDAsHexString(i); + sessionsInfo.AppendLiteral("(kid="); + sessionsInfo.Append(keyID); + sessionsInfo.AppendLiteral(" status="); + sessionsInfo.AppendASCII( + MediaKeyStatusValues::GetString(keyStatusMap->GetValueAtIndex(i))); + sessionsInfo.AppendLiteral(")"); + } + sessionsInfo.AppendLiteral(")"); + } +} + +// https://w3c.github.io/encrypted-media/#dom-mediakeys-getstatusforpolicy +already_AddRefed MediaKeys::GetStatusForPolicy( + const MediaKeysPolicy& aPolicy, ErrorResult& aRv) { + RefPtr promise( + MakePromise(aRv, "MediaKeys::GetStatusForPolicy()"_ns)); + if (aRv.Failed()) { + return nullptr; + } + + // 1. If policy has no present members, return a promise rejected with a newly + // created TypeError. + if (!aPolicy.mMinHdcpVersion.WasPassed()) { + promise->MaybeRejectWithTypeError("No minHdcpVersion in MediaKeysPolicy"); + return promise.forget(); + } + + if (!mProxy) { + NS_WARNING("Tried to use a MediaKeys without a CDM"); + promise->MaybeRejectWithInvalidStateError( + "Null CDM in MediaKeys.GetStatusForPolicy()"); + return promise.forget(); + } + + EME_LOG("GetStatusForPolicy minHdcpVersion = %s.", + HDCPVersionValues::GetString(aPolicy.mMinHdcpVersion.Value()).data()); + mProxy->GetStatusForPolicy(StorePromise(promise), + aPolicy.mMinHdcpVersion.Value()); + return promise.forget(); +} + +void MediaKeys::ResolvePromiseWithKeyStatus(PromiseId aId, + MediaKeyStatus aMediaKeyStatus) { + RefPtr promise(RetrievePromise(aId)); + if (!promise) { + return; + } + RefPtr keys(this); + EME_LOG( + "MediaKeys[%p]::ResolvePromiseWithKeyStatus() resolve promise id=%" PRIu32 + ", keystatus=%" PRIu8, + this, aId, static_cast(aMediaKeyStatus)); + promise->MaybeResolve(aMediaKeyStatus); +} + +} // namespace mozilla::dom diff --git a/dom/media/eme/MediaKeys.h b/dom/media/eme/MediaKeys.h new file mode 100644 index 0000000000..5a44b3c227 --- /dev/null +++ b/dom/media/eme/MediaKeys.h @@ -0,0 +1,236 @@ +/* -*- 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 mozilla_dom_mediakeys_h__ +#define mozilla_dom_mediakeys_h__ + +#include "DecoderDoctorLogger.h" +#include "mozilla/Attributes.h" +#include "mozilla/DetailedPromise.h" +#include "mozilla/RefPtr.h" +#include "mozilla/WeakPtr.h" +#include "mozilla/dom/MediaKeyStatusMapBinding.h" // For MediaKeyStatus +#include "mozilla/dom/MediaKeySystemAccessBinding.h" +#include "mozilla/dom/MediaKeysBinding.h" +#include "mozilla/dom/Promise.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIObserver.h" +#include "nsRefPtrHashtable.h" +#include "nsTHashMap.h" +#include "nsWrapperCache.h" + +namespace mozilla { + +class CDMProxy; + +namespace dom { +class MediaKeys; +} // namespace dom +DDLoggedTypeName(dom::MediaKeys); + +namespace dom { + +class ArrayBufferViewOrArrayBuffer; +class MediaKeySession; +struct MediaKeysPolicy; +class HTMLMediaElement; + +typedef nsRefPtrHashtable KeySessionHashMap; +typedef nsRefPtrHashtable PromiseHashMap; +typedef nsRefPtrHashtable + PendingKeySessionsHashMap; +typedef nsTHashMap PendingPromiseIdTokenHashMap; +typedef uint32_t PromiseId; + +// This class is used on the main thread only. +// Note: its addref/release is not (and can't be) thread safe! +class MediaKeys final : public nsIObserver, + public nsWrapperCache, + public SupportsWeakPtr, + public DecoderDoctorLifeLogger { + ~MediaKeys(); + + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(MediaKeys) + + NS_DECL_NSIOBSERVER + + MediaKeys(nsPIDOMWindowInner* aParentWindow, const nsAString& aKeySystem, + const MediaKeySystemConfiguration& aConfig); + + already_AddRefed Init(ErrorResult& aRv); + + nsPIDOMWindowInner* GetParentObject() const; + + JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + nsresult Bind(HTMLMediaElement* aElement); + void Unbind(); + + // Checks if there's any activity happening that could capture the media + // the keys are associated with and then expose that media outside of the + // origin it is in. + // + // This method does not return the results of the check, but the MediaKeys + // will notify mProxy of the results using `NotifyOutputProtectionStatus`. + void CheckIsElementCapturePossible(); + + // Javascript: readonly attribute DOMString keySystem; + void GetKeySystem(nsString& retval) const; + + // JavaScript: MediaKeys.createSession() + already_AddRefed CreateSession( + MediaKeySessionType aSessionType, ErrorResult& aRv); + + // JavaScript: MediaKeys.SetServerCertificate() + already_AddRefed SetServerCertificate( + const ArrayBufferViewOrArrayBuffer& aServerCertificate, ErrorResult& aRv); + + already_AddRefed GetSession(const nsAString& aSessionId); + + // Removes and returns MediaKeySession from the set of sessions awaiting + // their sessionId to be assigned. + already_AddRefed GetPendingSession(uint32_t aToken); + + // Called once a Init() operation succeeds. + void OnCDMCreated(PromiseId aId, const uint32_t aPluginId); + + // Called once the CDM generates a sessionId while servicing a + // MediaKeySession.generateRequest() or MediaKeySession.load() call, + // once the sessionId of a MediaKeySession is known. + void OnSessionIdReady(MediaKeySession* aSession); + + // Called once a LoadSession succeeds. + void OnSessionLoaded(PromiseId aId, bool aSuccess); + + // Called once a session has closed. + void OnSessionClosed(MediaKeySession* aSession); + + CDMProxy* GetCDMProxy() { return mProxy; } + + // Makes a new promise, or nullptr on failure. + already_AddRefed MakePromise(ErrorResult& aRv, + const nsACString& aName); + // Stores promise in mPromises, returning an ID that can be used to retrieve + // it later. The ID is passed to the CDM, so that it can signal specific + // promises to be resolved. + PromiseId StorePromise(DetailedPromise* aPromise); + + // Stores a map from promise id to pending session token. Using this + // mapping, when a promise is rejected via its ID, we can check if the + // promise corresponds to a pending session and retrieve that session + // via the mapped-to token, and remove the pending session from the + // list of sessions awaiting a session id. + void ConnectPendingPromiseIdWithToken(PromiseId aId, uint32_t aToken); + + // Reject promise with the given exception. + void RejectPromise(PromiseId aId, ErrorResult&& aException, + const nsCString& aReason); + // Resolves promise with "undefined". + void ResolvePromise(PromiseId aId); + + void Shutdown(); + + // Called by CDMProxy when CDM crashes or shuts down. It is different from + // Shutdown which is called from the script/dom side. + void Terminated(); + + // Returns true if this MediaKeys has been bound to a media element. + bool IsBoundToMediaElement() const; + + // Indicates to a MediaKeys instance that the inner window parent of that + // instance is being destroyed, this should prompt the keys to shutdown. + void OnInnerWindowDestroy(); + + void GetSessionsInfo(nsString& sessionsInfo); + + // JavaScript: MediaKeys.GetStatusForPolicy() + already_AddRefed GetStatusForPolicy(const MediaKeysPolicy& aPolicy, + ErrorResult& aR); + // Called by CDMProxy when CDM successfully GetStatusForPolicy. + void ResolvePromiseWithKeyStatus(PromiseId aId, + dom::MediaKeyStatus aMediaKeyStatus); + + template + void ResolvePromiseWithResult(PromiseId aId, const T& aResult) { + RefPtr promise(RetrievePromise(aId)); + if (!promise) { + return; + } + promise->MaybeResolve(aResult); + } + + // The topic used for requests related to mediakeys -- observe this to be + // notified of such requests. + constexpr static const char* kMediaKeysRequestTopic = "mediakeys-request"; + + private: + // Instantiate CDMProxy instance. + // It could be MediaDrmCDMProxy (Widevine on Fennec) or ChromiumCDMProxy (the + // rest). + already_AddRefed CreateCDMProxy(); + + // Removes promise from mPromises, and returns it. + already_AddRefed RetrievePromise(PromiseId aId); + + // Helpers to connect and disconnect to the parent inner window. An inner + // window should track (via weak ptr) MediaKeys created within it so we can + // ensure MediaKeys are shutdown if that window is destroyed. + void ConnectInnerWindow(); + void DisconnectInnerWindow(); + + // Owning ref to proxy. The proxy has a weak reference back to the MediaKeys, + // and the MediaKeys destructor clears the proxy's reference to the MediaKeys. + RefPtr mProxy; + + // The HTMLMediaElement the MediaKeys are associated with. Note that a + // MediaKeys instance may not be associated with any HTMLMediaElement so + // this can be null (we also cannot rely on a media element to drive shutdown + // for this reason). + RefPtr mElement; + + // The inner window associated with an instance of MediaKeys. We will + // shutdown the media keys when this Window is destroyed. We do this from the + // window rather than a document to address the case where media keys can be + // created in an about:blank document that then performs an async load -- this + // recreates the document, but the inner window is preserved in such a case. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1675360 for more info. + nsCOMPtr mParent; + const nsString mKeySystem; + KeySessionHashMap mKeySessions; + PromiseHashMap mPromises; + PendingKeySessionsHashMap mPendingSessions; + PromiseId mCreatePromiseId; + + // The principal of the relevant settings object. + RefPtr mPrincipal; + // The principal of the top level page. This can differ from mPrincipal if + // we're in an iframe. + RefPtr mTopLevelPrincipal; + + const MediaKeySystemConfiguration mConfig; + + PendingPromiseIdTokenHashMap mPromiseIdToken; + + // The topic a MediaKeys instance will observe to receive updates from + // EncryptedMediaChild. + constexpr static const char* kMediaKeysResponseTopic = "mediakeys-response"; + // Tracks if we've added an observer for responses from the associated + // EncryptedMediaChild. When true an observer is already in place, otherwise + // the observer has not yet been added. + bool mObserverAdded = false; + // Stores the json request we will send to EncryptedMediaChild when querying + // output protection. Lazily populated upon first use. + nsString mCaptureCheckRequestJson; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_mediakeys_h__ diff --git a/dom/media/eme/clearkey/ArrayUtils.h b/dom/media/eme/clearkey/ArrayUtils.h new file mode 100644 index 0000000000..ae5f33b68e --- /dev/null +++ b/dom/media/eme/clearkey/ArrayUtils.h @@ -0,0 +1,22 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __ArrayUtils_h__ +#define __ArrayUtils_h__ + +#define MOZ_ARRAY_LENGTH(array_) (sizeof(array_) / sizeof(array_[0])) + +#endif diff --git a/dom/media/eme/clearkey/BigEndian.h b/dom/media/eme/clearkey/BigEndian.h new file mode 100644 index 0000000000..5ea1b6042f --- /dev/null +++ b/dom/media/eme/clearkey/BigEndian.h @@ -0,0 +1,59 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __BigEndian_h__ +#define __BigEndian_h__ + +#include + +namespace mozilla { + +class BigEndian { + public: + static uint32_t readUint32(const void* aPtr) { + const uint8_t* p = reinterpret_cast(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(aPtr); + return uint32_t(p[0]) << 8 | uint32_t(p[1]); + } + + static uint64_t readUint64(const void* aPtr) { + const uint8_t* p = reinterpret_cast(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(aPtr); + p[0] = uint8_t(aValue >> 56) & 0xff; + p[1] = uint8_t(aValue >> 48) & 0xff; + p[2] = uint8_t(aValue >> 40) & 0xff; + p[3] = uint8_t(aValue >> 32) & 0xff; + p[4] = uint8_t(aValue >> 24) & 0xff; + p[5] = uint8_t(aValue >> 16) & 0xff; + p[6] = uint8_t(aValue >> 8) & 0xff; + p[7] = uint8_t(aValue) & 0xff; + } +}; + +} // namespace mozilla + +#endif // __BigEndian_h__ diff --git a/dom/media/eme/clearkey/ClearKeyBase64.cpp b/dom/media/eme/clearkey/ClearKeyBase64.cpp new file mode 100644 index 0000000000..bb610afd1b --- /dev/null +++ b/dom/media/eme/clearkey/ClearKeyBase64.cpp @@ -0,0 +1,90 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ClearKeyBase64.h" + +#include + +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& 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::iterator out = aOutDecoded.begin(); + for (size_t i = 0; i < encoded.length(); i++) { + if (!shift) { + *out = encoded[i] << 2; + } else { + *out |= encoded[i] >> (6 - shift); + out++; + if (out == aOutDecoded.end()) { + // Hit last 6bit octed in encoded, which is padding and can be ignored. + break; + } + *out = encoded[i] << (shift + 2); + } + shift = (shift + 2) % 8; + } + + return true; +} diff --git a/dom/media/eme/clearkey/ClearKeyBase64.h b/dom/media/eme/clearkey/ClearKeyBase64.h new file mode 100644 index 0000000000..39309c031e --- /dev/null +++ b/dom/media/eme/clearkey/ClearKeyBase64.h @@ -0,0 +1,29 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __ClearKeyBase64_h__ +#define __ClearKeyBase64_h__ + +#include + +#include +#include + +// Decodes a base64 encoded string. Returns true on success. +bool DecodeBase64(const std::string& aEncoded, + std::vector& aOutDecoded); + +#endif diff --git a/dom/media/eme/clearkey/ClearKeyDecryptionManager.cpp b/dom/media/eme/clearkey/ClearKeyDecryptionManager.cpp new file mode 100644 index 0000000000..7eeca88985 --- /dev/null +++ b/dom/media/eme/clearkey/ClearKeyDecryptionManager.cpp @@ -0,0 +1,289 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ClearKeyDecryptionManager.h" + +#include +#include + +#include +#include + +#include "mozilla/CheckedInt.h" +#include "mozilla/Span.h" +#include "psshparser/PsshParser.h" + +using namespace cdm; + +bool AllZero(const std::vector& 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& 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 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 data = reinterpret_cast(aBuffer); + if (!data.isValid()) { + return Status::kDecryptError; + } + const uintptr_t endBuffer = + reinterpret_cast(aBuffer + aBufferSize); + + if (aMetadata.NumSubsamples() == 0) { + if (data.value() > endBuffer) { + return Status::kDecryptError; + } + mozilla::Span encryptedSpan = + mozilla::Span(reinterpret_cast(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 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 encryptedSpan = mozilla::Span( + reinterpret_cast(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 data = reinterpret_cast(aBuffer); + const uintptr_t endBuffer = + reinterpret_cast(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 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(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 iv(aMetadata.mIV); + iv.insert(iv.end(), CENC_KEY_LEN - aMetadata.mIV.size(), 0); + + if (!ClearKeyUtils::DecryptAES(mKey, tmp, iv)) { + return Status::kDecryptError; + } + + if (aMetadata.NumSubsamples()) { + // Take the decrypted buffer, split up into subsamples, and insert those + // subsamples back into their original position in the original buffer. + uint8_t* data = aBuffer; + uint8_t* iter = &tmp[0]; + for (size_t i = 0; i < aMetadata.NumSubsamples(); i++) { + data += aMetadata.mClearBytes[i]; + uint32_t cipherBytes = aMetadata.mCipherBytes[i]; + + memcpy(data, iter, cipherBytes); + + data += cipherBytes; + iter += cipherBytes; + } + } else { + memcpy(aBuffer, &tmp[0], aBufferSize); + } + + return Status::kSuccess; +} diff --git a/dom/media/eme/clearkey/ClearKeyDecryptionManager.h b/dom/media/eme/clearkey/ClearKeyDecryptionManager.h new file mode 100644 index 0000000000..c21ff3119a --- /dev/null +++ b/dom/media/eme/clearkey/ClearKeyDecryptionManager.h @@ -0,0 +1,111 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __ClearKeyDecryptionManager_h__ +#define __ClearKeyDecryptionManager_h__ + +// This include is required in order for content_decryption_module to work +// on Unix systems. +#include + +#include + +#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 mKeyId; + std::vector mIV; + uint32_t mCryptByteBlock; + uint32_t mSkipByteBlock; + std::vector mClearBytes; + std::vector 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& aBuffer, + const CryptoMetaData& aMetadata); + + private: + bool IsExpectingKeyForKeyId(const KeyId& aKeyId) const; + + std::map mDecryptors; +}; + +#endif // __ClearKeyDecryptionManager_h__ diff --git a/dom/media/eme/clearkey/ClearKeyPersistence.cpp b/dom/media/eme/clearkey/ClearKeyPersistence.cpp new file mode 100644 index 0000000000..0b81f81399 --- /dev/null +++ b/dom/media/eme/clearkey/ClearKeyPersistence.cpp @@ -0,0 +1,153 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ClearKeyPersistence.h" + +#include +#include +#include + +#include + +#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&& 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 self(this); + function 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 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 onIndexSuccess = []() { + CK_LOGD("ClearKeyPersistence: Wrote index file"); + }; + + function 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 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&& aOnInitialized) { + if (aPersistentStateAllowed && + mPersistentKeyState == PersistentKeyState::UNINITIALIZED) { + mPersistentKeyState = LOADING; + ReadAllRecordsFromIndex(std::move(aOnInitialized)); + } else { + mPersistentKeyState = PersistentKeyState::LOADED; + aOnInitialized(); + } +} + +bool ClearKeyPersistence::IsLoaded() const { + return mPersistentKeyState == PersistentKeyState::LOADED; +} + +string ClearKeyPersistence::GetNewSessionId(SessionType aSessionType) { + static uint32_t sNextSessionId = 1; + + // Ensure we don't re-use a session id that was persisted. + while (Contains(mPersistentSessionIds, sNextSessionId)) { + sNextSessionId++; + } + + string sessionId; + stringstream ss; + ss << sNextSessionId; + ss >> sessionId; + + if (aSessionType == SessionType::kPersistentLicense) { + mPersistentSessionIds.insert(sNextSessionId); + + // Save the updated index file. + WriteIndex(); + } + + sNextSessionId++; + + return sessionId; +} + +bool ClearKeyPersistence::IsPersistentSessionId(const string& aSessionId) { + return Contains(mPersistentSessionIds, atoi(aSessionId.c_str())); +} + +void ClearKeyPersistence::PersistentSessionRemoved(string& aSessionId) { + mPersistentSessionIds.erase(atoi(aSessionId.c_str())); + + // Update the index file. + WriteIndex(); +} diff --git a/dom/media/eme/clearkey/ClearKeyPersistence.h b/dom/media/eme/clearkey/ClearKeyPersistence.h new file mode 100644 index 0000000000..b832db8c11 --- /dev/null +++ b/dom/media/eme/clearkey/ClearKeyPersistence.h @@ -0,0 +1,64 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __ClearKeyPersistence_h__ +#define __ClearKeyPersistence_h__ + +// This include is required in order for content_decryption_module to work +// on Unix systems. +#include + +#include +#include +#include +#include + +#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&& 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 mPersistentSessionIds; + + void ReadAllRecordsFromIndex(std::function&& aOnComplete); + void WriteIndex(); +}; + +#endif // __ClearKeyPersistence_h__ diff --git a/dom/media/eme/clearkey/ClearKeySession.cpp b/dom/media/eme/clearkey/ClearKeySession.cpp new file mode 100644 index 0000000000..59e104fc54 --- /dev/null +++ b/dom/media/eme/clearkey/ClearKeySession.cpp @@ -0,0 +1,72 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#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 keyId; + keyId.assign(aInitData, aInitData + aInitDataSize); + mKeyIds.push_back(keyId); + } + + if (mKeyIds.empty()) { + CK_LOGD("ClearKeySession::Init, failed to get keyId"); + return false; + } +#ifdef WMF_CLEARKEY_DEBUG + for (const auto& keyId : mKeyIds) { + CK_LOGARRAY("ClearKeySession::Init, KeyId : ", keyId.data(), keyId.size()); + } +#endif + return true; +} + +SessionType ClearKeySession::Type() const { return mSessionType; } + +void ClearKeySession::AddKeyId(const KeyId& aKeyId) { + mKeyIds.push_back(aKeyId); +} diff --git a/dom/media/eme/clearkey/ClearKeySession.h b/dom/media/eme/clearkey/ClearKeySession.h new file mode 100644 index 0000000000..3a130aa45a --- /dev/null +++ b/dom/media/eme/clearkey/ClearKeySession.h @@ -0,0 +1,56 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __ClearKeySession_h__ +#define __ClearKeySession_h__ + +// This include is required in order for content_decryption_module to work +// on Unix systems. +#include + +#include +#include + +#include "content_decryption_module.h" + +#include "ClearKeyUtils.h" + +class ClearKeySession { + public: + explicit ClearKeySession(const std::string& aSessionId, + cdm::SessionType aSessionType); + + ~ClearKeySession(); + + const std::vector& 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 mKeyIds; + + const cdm::SessionType mSessionType; +}; + +#endif // __ClearKeySession_h__ diff --git a/dom/media/eme/clearkey/ClearKeySessionManager.cpp b/dom/media/eme/clearkey/ClearKeySessionManager.cpp new file mode 100644 index 0000000000..54a15f4fcf --- /dev/null +++ b/dom/media/eme/clearkey/ClearKeySessionManager.cpp @@ -0,0 +1,713 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ClearKeySessionManager.h" + +#include +#include +#include +#include + +#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 self(this); + function onPersistentStateLoaded = [self]() { + while (!self->mDeferredInitialize.empty()) { + function 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 initData(aInitData, aInitData + aInitDataSize); + + RefPtr self(this); + function 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& sessionKeys = session->GetKeyIds(); + vector 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 self(this); + function 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 success = + [self, sessionId, aPromiseId](const uint8_t* data, uint32_t size) { + self->PersistentSessionDataLoaded(aPromiseId, sessionId, data, size); + }; + + function 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 keyInfos; + vector 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 response(aResponse, aResponse + aResponseSize); + + // Hold a reference to the SessionManager so it isn't released before we + // callback. + RefPtr self(this); + function 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 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 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 keydata; + Serialize(session, keydata); + + function resolve = [self, aPromiseId]() { + if (!self->mHost) { + return; + } + self->mHost->OnResolvePromise(aPromiseId); + }; + + function 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& aOutKeyData) { + const std::vector& 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 self(this); + function 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 self(this); + function 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 emptyKeydata; + + function resolve = [self, aPromiseId]() { + if (!self->mHost) { + return; + } + self->mHost->OnResolvePromise(aPromiseId); + }; + + function 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&& 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 keyInfos; + keyInfos.push_back(keyInfo); + + // At time of writing, Gecko's higher level handling doesn't use this arg. + // However, we set it to false to mimic Chromium's similar case. Since + // Clearkey is used to test the Chromium CDM path, it doesn't hurt to try + // and mimic their behaviour. + bool hasAdditionalUseableKey = false; + mHost->OnSessionKeysChange(lastSessionId.c_str(), lastSessionId.size(), + hasAdditionalUseableKey, keyInfos.data(), + keyInfos.size()); +} diff --git a/dom/media/eme/clearkey/ClearKeySessionManager.h b/dom/media/eme/clearkey/ClearKeySessionManager.h new file mode 100644 index 0000000000..3a930f666d --- /dev/null +++ b/dom/media/eme/clearkey/ClearKeySessionManager.h @@ -0,0 +1,138 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __ClearKeyDecryptor_h__ +#define __ClearKeyDecryptor_h__ + +// This include is required in order for content_decryption_module to work +// on Unix systems. +#include + +#include +#include +#include +#include +#include +#include + +#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&& aMaybeDefer); + void Serialize(const ClearKeySession* aSession, + std::vector& 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 mDecryptionManager; + RefPtr mPersistence; + + cdm::Host_10* mHost = nullptr; + + std::set mKeyIds; + std::map mSessions; + + // The session id of the last session created or loaded from persistent + // storage. Used to fire test messages at that session. + std::optional mLastSessionId; + + std::queue> mDeferredInitialize; + + // If there is an inflight query to the host to check the output protection + // status. Multiple in flight queries should not be allowed, avoid firing + // more if this is true. + bool mHasOutstandingOutputProtectionQuery = false; + // The last time the manager called QueryOutputProtectionStatus on the host. + mozilla::TimeStamp mLastOutputProtectionQueryTime; +}; + +#endif // __ClearKeyDecryptor_h__ diff --git a/dom/media/eme/clearkey/ClearKeyStorage.cpp b/dom/media/eme/clearkey/ClearKeyStorage.cpp new file mode 100644 index 0000000000..1375d33c57 --- /dev/null +++ b/dom/media/eme/clearkey/ClearKeyStorage.cpp @@ -0,0 +1,194 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ClearKeyStorage.h" + +#include +// This include is required in order for content_decryption_module to work +// on Unix systems. +#include + +#include + +#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& aData, function&& aOnSuccess, + function&& 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& aData, + function&& aOnSuccess, + function&& 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 mOnSuccess; + function mOnFailure; + + const vector mData; +}; + +void WriteData(Host_10* aHost, string& aRecordName, + const vector& aData, function&& aOnSuccess, + function&& 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&& aOnSuccess, + function&& 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&& aOnSuccess, + function&& 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 mOnSuccess; + function mOnFailure; +}; + +void ReadData(Host_10* aHost, string& aRecordName, + function&& aOnSuccess, + function&& aOnFailure) { + ReadRecordClient::Read(aHost, aRecordName, std::move(aOnSuccess), + std::move(aOnFailure)); +} diff --git a/dom/media/eme/clearkey/ClearKeyStorage.h b/dom/media/eme/clearkey/ClearKeyStorage.h new file mode 100644 index 0000000000..bd8e99b901 --- /dev/null +++ b/dom/media/eme/clearkey/ClearKeyStorage.h @@ -0,0 +1,42 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __ClearKeyStorage_h__ +#define __ClearKeyStorage_h__ + +#include + +#include +#include +#include + +#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& aData, + std::function&& aOnSuccess, + std::function&& aOnFailure); + +// Reads data from a file and fires the appropriate callback when complete. +void ReadData(cdm::Host_10* aHost, std::string& aRecordName, + std::function&& aOnSuccess, + std::function&& aOnFailure); + +#endif // __ClearKeyStorage_h__ diff --git a/dom/media/eme/clearkey/ClearKeyUtils.cpp b/dom/media/eme/clearkey/ClearKeyUtils.cpp new file mode 100644 index 0000000000..3f9588bdf9 --- /dev/null +++ b/dom/media/eme/clearkey/ClearKeyUtils.cpp @@ -0,0 +1,661 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ClearKeyUtils.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#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 +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& aKey, + const vector& aIV, + mozilla::Span aSubsample, + uint32_t aCryptByteBlock, + uint32_t aSkipByteBlock) { + if (aKey.size() != CENC_KEY_LEN || aIV.size() != CENC_KEY_LEN) { + CK_LOGE("Key and IV size should be 16!"); + return false; + } + + std::unique_ptr> 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> 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> ctx( + PK11_CreateContextBySymKey(CKM_AES_CBC, CKA_DECRYPT, key.get(), &ivItem)); + + if (!ctx) { + CK_LOGE("Failed to get PK11Context!"); + return false; + } + + assert(aCryptByteBlock <= 0xFF); + assert(aSkipByteBlock <= 0xFF); + + uint8_t* encryptedSubsample = &aSubsample[0]; + const uint32_t BLOCK_SIZE = 16; + const uint32_t skipBytes = aSkipByteBlock * BLOCK_SIZE; + const uint32_t totalBlocks = aSubsample.Length() / BLOCK_SIZE; + uint32_t blocksProcessed = 0; + + if (aSkipByteBlock == 0) { + // ISO/IEC 23001 - 7 Section 9.6.1 + // 'When the fields default_crypt_byte_block and default_skip_byte_block in + // a version 1 Track Encryption Box('tenc') are non - zero numbers, pattern + // encryption SHALL be applied.' + // So if both are 0, then everything is encrypted. Similarly, if skip is 0 + // and crypt is non-0, everything is encrypted. + // In this case we can just decrypt all the blocks in one call. This is the + // same outcome as decrypting them using smaller steps, as either way the + // CBC result should be the same. + MOZ_ASSERT(skipBytes == 0); + aCryptByteBlock = totalBlocks; + } + + while (blocksProcessed < totalBlocks) { + uint32_t blocksToDecrypt = aCryptByteBlock <= totalBlocks - blocksProcessed + ? aCryptByteBlock + : totalBlocks - blocksProcessed; + uint32_t bytesToDecrypt = blocksToDecrypt * BLOCK_SIZE; + int outLen; + SECStatus rv; + rv = PK11_CipherOp(ctx.get(), encryptedSubsample, &outLen, bytesToDecrypt, + encryptedSubsample, bytesToDecrypt); + if (rv != SECSuccess) { + CK_LOGE("PK11_CipherOp() failed"); + return false; + } + + encryptedSubsample += skipBytes + bytesToDecrypt; + blocksProcessed += aSkipByteBlock + blocksToDecrypt; + } + + return true; +} + +/* static */ +bool ClearKeyUtils::DecryptAES(const vector& aKey, + vector& aData, vector& 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 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(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& 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& 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& 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& aOutKeyIds) { + // Consume start of array. + EXPECT_SYMBOL(aCtx, '['); + + while (true) { + string label; + vector 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& 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(aBytes[i]); + ss << " "; + } + + return ss.str(); +} diff --git a/dom/media/eme/clearkey/ClearKeyUtils.h b/dom/media/eme/clearkey/ClearKeyUtils.h new file mode 100644 index 0000000000..04c227c71d --- /dev/null +++ b/dom/media/eme/clearkey/ClearKeyUtils.h @@ -0,0 +1,107 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __ClearKeyUtils_h__ +#define __ClearKeyUtils_h__ + +#include +// stdef.h is required for content_decryption_module to work on Unix systems. +#include +#include + +#include +#include + +#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 KeyId; +typedef std::vector 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& aKey, + const std::vector& aIV, + mozilla::Span aSubsampleEncryptedRange, + uint32_t aCryptByteBlocks, uint32_t aSkipByteBlocks); + + static bool DecryptAES(const std::vector& aKey, + std::vector& aData, + std::vector& aIV); + + static bool ParseKeyIdsInitData(const uint8_t* aInitData, + uint32_t aInitDataSize, + std::vector& aOutKeyIds); + + static void MakeKeyRequest(const std::vector& aKeyIds, + std::string& aOutRequest, + cdm::SessionType aSessionType); + + static bool ParseJWK(const uint8_t* aKeyData, uint32_t aKeyDataSize, + std::vector& 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 +inline bool Contains(const Container& aContainer, const Element& aElement) { + return aContainer.find(aElement) != aContainer.end(); +} + +template +inline void Assign(std::vector& aVec, const T* aData, size_t aLength) { + aVec.assign(aData, aData + aLength); +} + +#endif // __ClearKeyUtils_h__ diff --git a/dom/media/eme/clearkey/RefCounted.h b/dom/media/eme/clearkey/RefCounted.h new file mode 100644 index 0000000000..25f308164c --- /dev/null +++ b/dom/media/eme/clearkey/RefCounted.h @@ -0,0 +1,85 @@ +/* + * Copyright 2015, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __RefCount_h__ +#define __RefCount_h__ + +#include +#include + +#include + +#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 mRefCount; +}; + +template +class RefPtr { + public: + RefPtr(const RefPtr& src) { Set(src.mPtr); } + + explicit RefPtr(T* aPtr) { Set(aPtr); } + RefPtr() { Set(nullptr); } + + ~RefPtr() { Set(nullptr); } + T* operator->() const { return mPtr; } + T** operator&() { return &mPtr; } + T* operator->() { return mPtr; } + operator T*() { return mPtr; } + + T* Get() const { return mPtr; } + + RefPtr& operator=(T* aVal) { + Set(aVal); + return *this; + } + + private: + T* Set(T* aPtr) { + if (mPtr == aPtr) { + return aPtr; + } + if (mPtr) { + mPtr->Release(); + } + mPtr = aPtr; + if (mPtr) { + aPtr->AddRef(); + } + return mPtr; + } + + T* mPtr = nullptr; +}; + +#endif // __RefCount_h__ diff --git a/dom/media/eme/clearkey/gtest/TestClearKeyUtils.cpp b/dom/media/eme/clearkey/gtest/TestClearKeyUtils.cpp new file mode 100644 index 0000000000..bff6d0f356 --- /dev/null +++ b/dom/media/eme/clearkey/gtest/TestClearKeyUtils.cpp @@ -0,0 +1,81 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include +#include +#include + +#include "../ClearKeyBase64.cpp" + +using namespace std; + +struct B64Test { + string b64; + vector 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 v; + bool rv = DecodeBase64(string(test.b64), v); + EXPECT_EQ(test.shouldPass, rv); + if (test.shouldPass) { + EXPECT_EQ(test.raw.size(), v.size()); + for (size_t k = 0; k < test.raw.size(); k++) { + EXPECT_EQ(test.raw[k], v[k]); + } + } + } +} diff --git a/dom/media/eme/clearkey/gtest/moz.build b/dom/media/eme/clearkey/gtest/moz.build new file mode 100644 index 0000000000..5771c4724c --- /dev/null +++ b/dom/media/eme/clearkey/gtest/moz.build @@ -0,0 +1,15 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +UNIFIED_SOURCES += [ + "TestClearKeyUtils.cpp", +] + +FINAL_LIBRARY = "xul-gtest" + +LOCAL_INCLUDES += [ + "..", +] diff --git a/dom/media/eme/clearkey/moz.build b/dom/media/eme/clearkey/moz.build new file mode 100644 index 0000000000..57160624ea --- /dev/null +++ b/dom/media/eme/clearkey/moz.build @@ -0,0 +1,37 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXPORTS += [ + "ArrayUtils.h", + "BigEndian.h", + "ClearKeyBase64.h", + "ClearKeyDecryptionManager.h", + "ClearKeyPersistence.h", + "ClearKeySession.h", + "ClearKeySessionManager.h", + "ClearKeyStorage.h", + "ClearKeyUtils.h", + "RefCounted.h", +] + +UNIFIED_SOURCES += [ + "ClearKeyBase64.cpp", + "ClearKeyDecryptionManager.cpp", + "ClearKeyPersistence.cpp", + "ClearKeySession.cpp", + "ClearKeySessionManager.cpp", + "ClearKeyStorage.cpp", + "ClearKeyUtils.cpp", +] + +TEST_DIRS += [ + "gtest", +] + +USE_LIBS += [ + "nss", + "psshparser", +] + +Library("gecko-clearkey") diff --git a/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.cpp b/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.cpp new file mode 100644 index 0000000000..e5431f50fd --- /dev/null +++ b/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.cpp @@ -0,0 +1,115 @@ +/* -*- 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 "MediaDrmCDMCallbackProxy.h" +#include "MediaDrmCDMProxy.h" +#include "nsString.h" +#include "mozilla/dom/MediaKeys.h" +#include "mozilla/dom/MediaKeySession.h" +#include "nsContentCID.h" +#include "nsServiceManagerUtils.h" +#include "MainThreadUtils.h" +#include "mozilla/EMEUtils.h" + +namespace mozilla { + +MediaDrmCDMCallbackProxy::MediaDrmCDMCallbackProxy(MediaDrmCDMProxy* aProxy) + : mProxy(aProxy) {} + +MediaDrmCDMCallbackProxy::~MediaDrmCDMCallbackProxy() = default; + +void MediaDrmCDMCallbackProxy::SetSessionId(uint32_t aToken, + const nsCString& aSessionId) { + MOZ_ASSERT(NS_IsMainThread()); + mProxy->OnSetSessionId(aToken, NS_ConvertUTF8toUTF16(aSessionId)); +} + +void MediaDrmCDMCallbackProxy::ResolveLoadSessionPromise(uint32_t aPromiseId, + bool aSuccess) { + MOZ_ASSERT(NS_IsMainThread()); + mProxy->OnResolveLoadSessionPromise(aPromiseId, aSuccess); +} + +void MediaDrmCDMCallbackProxy::ResolvePromise(uint32_t aPromiseId) { + // Note: CDMProxy proxies this from non-main threads to main thread. + mProxy->ResolvePromise(aPromiseId); +} + +void MediaDrmCDMCallbackProxy::RejectPromise(uint32_t aPromiseId, + ErrorResult&& aException, + const nsCString& aMessage) { + MOZ_ASSERT(NS_IsMainThread()); + mProxy->OnRejectPromise(aPromiseId, std::move(aException), aMessage); +} + +void MediaDrmCDMCallbackProxy::SessionMessage( + const nsCString& aSessionId, dom::MediaKeyMessageType aMessageType, + const nsTArray& aMessage) { + MOZ_ASSERT(NS_IsMainThread()); + // For removing constness + nsTArray message(aMessage.Clone()); + mProxy->OnSessionMessage(NS_ConvertUTF8toUTF16(aSessionId), aMessageType, + message); +} + +void MediaDrmCDMCallbackProxy::ExpirationChange(const nsCString& aSessionId, + UnixTime aExpiryTime) { + MOZ_ASSERT(NS_IsMainThread()); + mProxy->OnExpirationChange(NS_ConvertUTF8toUTF16(aSessionId), aExpiryTime); +} + +void MediaDrmCDMCallbackProxy::SessionClosed(const nsCString& aSessionId) { + MOZ_ASSERT(NS_IsMainThread()); + bool keyStatusesChange = false; + { + auto caps = mProxy->Capabilites().Lock(); + keyStatusesChange = + caps->RemoveKeysForSession(NS_ConvertUTF8toUTF16(aSessionId)); + } + if (keyStatusesChange) { + mProxy->OnKeyStatusesChange(NS_ConvertUTF8toUTF16(aSessionId)); + } + mProxy->OnSessionClosed(NS_ConvertUTF8toUTF16(aSessionId)); +} + +void MediaDrmCDMCallbackProxy::SessionError(const nsCString& aSessionId, + nsresult aException, + uint32_t aSystemCode, + const nsCString& aMessage) { + MOZ_ASSERT(NS_IsMainThread()); + mProxy->OnSessionError(NS_ConvertUTF8toUTF16(aSessionId), aException, + aSystemCode, NS_ConvertUTF8toUTF16(aMessage)); +} + +void MediaDrmCDMCallbackProxy::BatchedKeyStatusChanged( + const nsCString& aSessionId, const nsTArray& aKeyInfos) { + MOZ_ASSERT(NS_IsMainThread()); + BatchedKeyStatusChangedInternal(aSessionId, aKeyInfos); +} + +void MediaDrmCDMCallbackProxy::BatchedKeyStatusChangedInternal( + const nsCString& aSessionId, const nsTArray& aKeyInfos) { + bool keyStatusesChange = false; + { + auto caps = mProxy->Capabilites().Lock(); + for (size_t i = 0; i < aKeyInfos.Length(); i++) { + keyStatusesChange |= caps->SetKeyStatus(aKeyInfos[i].mKeyId, + NS_ConvertUTF8toUTF16(aSessionId), + aKeyInfos[i].mStatus); + } + } + if (keyStatusesChange) { + mProxy->OnKeyStatusesChange(NS_ConvertUTF8toUTF16(aSessionId)); + } +} + +void MediaDrmCDMCallbackProxy::Decrypted( + uint32_t aId, DecryptStatus aResult, + const nsTArray& aDecryptedData) { + MOZ_ASSERT_UNREACHABLE("Fennec could not handle decrypted event"); +} + +} // namespace mozilla diff --git a/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.h b/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.h new file mode 100644 index 0000000000..e963e21e11 --- /dev/null +++ b/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.h @@ -0,0 +1,63 @@ +/* -*- 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 MediaDrmCDMCallbackProxy_h_ +#define MediaDrmCDMCallbackProxy_h_ + +#include "mozilla/CDMProxy.h" +#include "mozilla/DecryptorProxyCallback.h" + +namespace mozilla { +class CDMProxy; +class ErrorResult; +class MediaDrmCDMProxy; + +// Proxies call backs from the MediaDrmProxy -> MediaDrmProxySupport back to the +// MediaKeys object on the main thread. We used annotation calledFrom = "gecko" +// to ensure running on main thread. +class MediaDrmCDMCallbackProxy final : public DecryptorProxyCallback { + public: + void SetSessionId(uint32_t aCreateSessionToken, + const nsCString& aSessionId) override; + + void ResolveLoadSessionPromise(uint32_t aPromiseId, bool aSuccess) override; + + void ResolvePromise(uint32_t aPromiseId) override; + + void RejectPromise(uint32_t aPromiseId, ErrorResult&& aException, + const nsCString& aSessionId) override; + + void SessionMessage(const nsCString& aSessionId, + dom::MediaKeyMessageType aMessageType, + const nsTArray& aMessage) override; + + void ExpirationChange(const nsCString& aSessionId, + UnixTime aExpiryTime) override; + + void SessionClosed(const nsCString& aSessionId) override; + + void SessionError(const nsCString& aSessionId, nsresult aException, + uint32_t aSystemCode, const nsCString& aMessage) override; + + void Decrypted(uint32_t aId, DecryptStatus aResult, + const nsTArray& aDecryptedData) override; + + void BatchedKeyStatusChanged(const nsCString& aSessionId, + const nsTArray& aKeyInfos) override; + + ~MediaDrmCDMCallbackProxy(); + + private: + friend class MediaDrmCDMProxy; + explicit MediaDrmCDMCallbackProxy(MediaDrmCDMProxy* aProxy); + + void BatchedKeyStatusChangedInternal(const nsCString& aSessionId, + const nsTArray& aKeyInfos); + const RefPtr mProxy; +}; + +} // namespace mozilla +#endif diff --git a/dom/media/eme/mediadrm/MediaDrmCDMProxy.cpp b/dom/media/eme/mediadrm/MediaDrmCDMProxy.cpp new file mode 100644 index 0000000000..6ef47a84c3 --- /dev/null +++ b/dom/media/eme/mediadrm/MediaDrmCDMProxy.cpp @@ -0,0 +1,453 @@ +/* -*- 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 "mozilla/dom/MediaKeySession.h" +#include "mozilla/MediaDrmCDMProxy.h" +#include "MediaDrmCDMCallbackProxy.h" + +namespace mozilla { + +MediaDrmSessionType ToMediaDrmSessionType( + dom::MediaKeySessionType aSessionType) { + switch (aSessionType) { + case dom::MediaKeySessionType::Temporary: + return kKeyStreaming; + case dom::MediaKeySessionType::Persistent_license: + return kKeyOffline; + default: + return kKeyStreaming; + }; +} + +MediaDrmCDMProxy::MediaDrmCDMProxy(dom::MediaKeys* aKeys, + const nsAString& aKeySystem, + bool aDistinctiveIdentifierRequired, + bool aPersistentStateRequired) + : CDMProxy(aKeys, aKeySystem, aDistinctiveIdentifierRequired, + aPersistentStateRequired), + mCDM(nullptr), + mShutdownCalled(false) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_COUNT_CTOR(MediaDrmCDMProxy); +} + +MediaDrmCDMProxy::~MediaDrmCDMProxy() { MOZ_COUNT_DTOR(MediaDrmCDMProxy); } + +void MediaDrmCDMProxy::Init(PromiseId aPromiseId, const nsAString& aOrigin, + const nsAString& aTopLevelOrigin, + const nsAString& aName) { + MOZ_ASSERT(NS_IsMainThread()); + NS_ENSURE_TRUE_VOID(!mKeys.IsNull()); + + EME_LOG("MediaDrmCDMProxy::Init (%s, %s) %s", + NS_ConvertUTF16toUTF8(aOrigin).get(), + NS_ConvertUTF16toUTF8(aTopLevelOrigin).get(), + NS_ConvertUTF16toUTF8(aName).get()); + + // Create a thread to work with cdm. + if (!mOwnerThread) { + nsresult rv = + NS_NewNamedThread("MDCDMThread", getter_AddRefs(mOwnerThread)); + if (NS_FAILED(rv)) { + RejectPromiseWithStateError( + aPromiseId, nsLiteralCString( + "Couldn't create CDM thread MediaDrmCDMProxy::Init")); + return; + } + } + + mCDM = mozilla::MakeUnique(mKeySystem); + nsCOMPtr task( + NewRunnableMethod("MediaDrmCDMProxy::md_Init", this, + &MediaDrmCDMProxy::md_Init, aPromiseId)); + mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); +} + +void MediaDrmCDMProxy::CreateSession(uint32_t aCreateSessionToken, + MediaKeySessionType aSessionType, + PromiseId aPromiseId, + const nsAString& aInitDataType, + nsTArray& aInitData) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mOwnerThread); + + UniquePtr data(new CreateSessionData()); + data->mSessionType = aSessionType; + data->mCreateSessionToken = aCreateSessionToken; + data->mPromiseId = aPromiseId; + data->mInitDataType = NS_ConvertUTF16toUTF8(aInitDataType); + data->mInitData = std::move(aInitData); + + nsCOMPtr task(NewRunnableMethod&&>( + "MediaDrmCDMProxy::md_CreateSession", this, + &MediaDrmCDMProxy::md_CreateSession, std::move(data))); + mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); +} + +void MediaDrmCDMProxy::LoadSession(PromiseId aPromiseId, + dom::MediaKeySessionType aSessionType, + const nsAString& aSessionId) { + // TODO: Implement LoadSession. + RejectPromiseWithStateError( + aPromiseId, "Currently Fennec does not support LoadSession"_ns); +} + +void MediaDrmCDMProxy::SetServerCertificate(PromiseId aPromiseId, + nsTArray& aCert) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mOwnerThread); + + mOwnerThread->Dispatch(NewRunnableMethod>( + "MediaDrmCDMProxy::md_SetServerCertificate", this, + &MediaDrmCDMProxy::md_SetServerCertificate, + aPromiseId, std::move(aCert)), + NS_DISPATCH_NORMAL); +} + +void MediaDrmCDMProxy::UpdateSession(const nsAString& aSessionId, + PromiseId aPromiseId, + nsTArray& aResponse) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mOwnerThread); + NS_ENSURE_TRUE_VOID(!mKeys.IsNull()); + + UniquePtr data(new UpdateSessionData()); + data->mPromiseId = aPromiseId; + data->mSessionId = NS_ConvertUTF16toUTF8(aSessionId); + data->mResponse = std::move(aResponse); + + nsCOMPtr task(NewRunnableMethod&&>( + "MediaDrmCDMProxy::md_UpdateSession", this, + &MediaDrmCDMProxy::md_UpdateSession, std::move(data))); + mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); +} + +void MediaDrmCDMProxy::CloseSession(const nsAString& aSessionId, + PromiseId aPromiseId) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mOwnerThread); + NS_ENSURE_TRUE_VOID(!mKeys.IsNull()); + + UniquePtr data(new SessionOpData()); + data->mPromiseId = aPromiseId; + data->mSessionId = NS_ConvertUTF16toUTF8(aSessionId); + + nsCOMPtr task(NewRunnableMethod&&>( + "MediaDrmCDMProxy::md_CloseSession", this, + &MediaDrmCDMProxy::md_CloseSession, std::move(data))); + mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); +} + +void MediaDrmCDMProxy::RemoveSession(const nsAString& aSessionId, + PromiseId aPromiseId) { + // TODO: Implement RemoveSession. + RejectPromiseWithStateError( + aPromiseId, "Currently Fennec does not support RemoveSession"_ns); +} + +void MediaDrmCDMProxy::QueryOutputProtectionStatus() { + // TODO(bryce): determine if this is needed for Android and implement as + // needed. See also `NotifyOutputProtectionStatus`. +} + +void MediaDrmCDMProxy::NotifyOutputProtectionStatus( + OutputProtectionCheckStatus aCheckStatus, + OutputProtectionCaptureStatus aCaptureStatus) { + // TODO(bryce): determine if this is needed for Android and implement as + // needed. See also `QueryOutputProtectionStatus`. +} + +void MediaDrmCDMProxy::Shutdown() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mOwnerThread); + nsCOMPtr task(NewRunnableMethod( + "MediaDrmCDMProxy::md_Shutdown", this, &MediaDrmCDMProxy::md_Shutdown)); + + mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); + mOwnerThread->Shutdown(); + mOwnerThread = nullptr; + mKeys.Clear(); +} + +void MediaDrmCDMProxy::Terminated() { + // TODO: Implement Terminated. + // Should find a way to handle the case when remote side MediaDrm crashed. +} + +void MediaDrmCDMProxy::OnSetSessionId(uint32_t aCreateSessionToken, + const nsAString& aSessionId) { + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + + RefPtr session( + mKeys->GetPendingSession(aCreateSessionToken)); + if (session) { + session->SetSessionId(aSessionId); + } +} + +void MediaDrmCDMProxy::OnResolveLoadSessionPromise(uint32_t aPromiseId, + bool aSuccess) { + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + mKeys->OnSessionLoaded(aPromiseId, aSuccess); +} + +void MediaDrmCDMProxy::OnSessionMessage(const nsAString& aSessionId, + dom::MediaKeyMessageType aMessageType, + const nsTArray& aMessage) { + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + RefPtr session(mKeys->GetSession(aSessionId)); + if (session) { + session->DispatchKeyMessage(aMessageType, aMessage); + } +} + +void MediaDrmCDMProxy::OnExpirationChange(const nsAString& aSessionId, + UnixTime aExpiryTime) { + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + RefPtr session(mKeys->GetSession(aSessionId)); + if (session) { + session->SetExpiration(static_cast(aExpiryTime)); + } +} + +void MediaDrmCDMProxy::OnSessionClosed(const nsAString& aSessionId) { + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + RefPtr session(mKeys->GetSession(aSessionId)); + if (session) { + session->OnClosed(); + } +} + +void MediaDrmCDMProxy::OnSessionError(const nsAString& aSessionId, + nsresult aException, uint32_t aSystemCode, + const nsAString& aMsg) { + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + RefPtr session(mKeys->GetSession(aSessionId)); + if (session) { + session->DispatchKeyError(aSystemCode); + } +} + +void MediaDrmCDMProxy::OnRejectPromise(uint32_t aPromiseId, + ErrorResult&& aException, + const nsCString& aMsg) { + MOZ_ASSERT(NS_IsMainThread()); + RejectPromise(aPromiseId, std::move(aException), aMsg); +} + +RefPtr MediaDrmCDMProxy::Decrypt(MediaRawData* aSample) { + MOZ_ASSERT_UNREACHABLE("Fennec could not handle decrypting individually"); + return nullptr; +} + +void MediaDrmCDMProxy::OnDecrypted(uint32_t aId, DecryptStatus aResult, + const nsTArray& aDecryptedData) { + MOZ_ASSERT_UNREACHABLE("Fennec could not handle decrypted event"); +} + +void MediaDrmCDMProxy::RejectPromise(PromiseId aId, ErrorResult&& aException, + const nsCString& aReason) { + if (NS_IsMainThread()) { + if (!mKeys.IsNull()) { + mKeys->RejectPromise(aId, std::move(aException), aReason); + } + } else { + nsCOMPtr task( + new RejectPromiseTask(this, aId, std::move(aException), aReason)); + mMainThread->Dispatch(task.forget(), NS_DISPATCH_NORMAL); + } +} + +void MediaDrmCDMProxy::RejectPromiseWithStateError(PromiseId aId, + const nsCString& aReason) { + ErrorResult rv; + rv.ThrowInvalidStateError(aReason); + RejectPromise(aId, std::move(rv), aReason); +} + +void MediaDrmCDMProxy::ResolvePromise(PromiseId aId) { + if (NS_IsMainThread()) { + if (!mKeys.IsNull()) { + mKeys->ResolvePromise(aId); + } else { + NS_WARNING("MediaDrmCDMProxy unable to resolve promise!"); + } + } else { + nsCOMPtr task; + task = + NewRunnableMethod("MediaDrmCDMProxy::ResolvePromise", this, + &MediaDrmCDMProxy::ResolvePromise, aId); + mMainThread->Dispatch(task.forget(), NS_DISPATCH_NORMAL); + } +} + +template +void MediaDrmCDMProxy::ResolvePromiseWithResult(PromiseId aId, + const T& aResult) { + if (NS_IsMainThread()) { + if (!mKeys.IsNull()) { + mKeys->ResolvePromiseWithResult(aId, aResult); + } else { + NS_WARNING("MediaDrmCDMProxy unable to resolve promise!"); + } + return; + } + + nsCOMPtr task; + task = NewRunnableMethod( + "MediaDrmCDMProxy::ResolvePromiseWithResult", this, + &MediaDrmCDMProxy::ResolvePromiseWithResult, aId, aResult); + mMainThread->Dispatch(task.forget(), NS_DISPATCH_NORMAL); +} + +void MediaDrmCDMProxy::OnKeyStatusesChange(const nsAString& aSessionId) { + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + RefPtr session(mKeys->GetSession(aSessionId)); + if (session) { + session->DispatchKeyStatusesChange(); + } +} + +void MediaDrmCDMProxy::GetStatusForPolicy( + PromiseId aPromiseId, const dom::HDCPVersion& aMinHdcpVersion) { + // TODO: Implement GetStatusForPolicy. + constexpr auto err = + "Currently Fennec does not support GetStatusForPolicy"_ns; + + ErrorResult rv; + rv.ThrowNotSupportedError(err); + RejectPromise(aPromiseId, std::move(rv), err); +} + +#ifdef DEBUG +bool MediaDrmCDMProxy::IsOnOwnerThread() { + return NS_GetCurrentThread() == mOwnerThread; +} +#endif + +const nsString& MediaDrmCDMProxy::GetMediaDrmStubId() const { + MOZ_ASSERT(mCDM); + return mCDM->GetMediaDrmStubId(); +} + +void MediaDrmCDMProxy::OnCDMCreated(uint32_t aPromiseId) { + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + + if (mCDM) { + mKeys->OnCDMCreated(aPromiseId, 0); + return; + } + + // No CDM? Just reject the promise. + constexpr auto err = "Null CDM in OnCDMCreated()"_ns; + ErrorResult rv; + rv.ThrowInvalidStateError(err); + mKeys->RejectPromise(aPromiseId, std::move(rv), err); +} + +void MediaDrmCDMProxy::md_Init(uint32_t aPromiseId) { + MOZ_ASSERT(IsOnOwnerThread()); + MOZ_ASSERT(mCDM); + + UniquePtr callback( + new MediaDrmCDMCallbackProxy(this)); + mCDM->Init(std::move(callback)); + nsCOMPtr task( + NewRunnableMethod("MediaDrmCDMProxy::OnCDMCreated", this, + &MediaDrmCDMProxy::OnCDMCreated, aPromiseId)); + mMainThread->Dispatch(task.forget(), NS_DISPATCH_NORMAL); +} + +void MediaDrmCDMProxy::md_CreateSession(UniquePtr&& aData) { + MOZ_ASSERT(IsOnOwnerThread()); + + if (!mCDM) { + RejectPromiseWithStateError(aData->mPromiseId, + "Null CDM in md_CreateSession"_ns); + return; + } + + mCDM->CreateSession(aData->mCreateSessionToken, aData->mPromiseId, + aData->mInitDataType, aData->mInitData, + ToMediaDrmSessionType(aData->mSessionType)); +} + +void MediaDrmCDMProxy::md_SetServerCertificate(PromiseId aPromiseId, + const nsTArray& aCert) { + MOZ_ASSERT(IsOnOwnerThread()); + + if (!mCDM) { + RejectPromiseWithStateError(aPromiseId, + "Null CDM in md_SetServerCertificate"_ns); + return; + } + + if (mCDM->SetServerCertificate(aCert)) { + ResolvePromiseWithResult(aPromiseId, true); + } else { + RejectPromiseWithStateError( + aPromiseId, "MediaDrmCDMProxy unable to set server certificate"_ns); + } +} + +void MediaDrmCDMProxy::md_UpdateSession(UniquePtr&& aData) { + MOZ_ASSERT(IsOnOwnerThread()); + + if (!mCDM) { + RejectPromiseWithStateError(aData->mPromiseId, + "Null CDM in md_UpdateSession"_ns); + return; + } + mCDM->UpdateSession(aData->mPromiseId, aData->mSessionId, aData->mResponse); +} + +void MediaDrmCDMProxy::md_CloseSession(UniquePtr&& aData) { + MOZ_ASSERT(IsOnOwnerThread()); + + if (!mCDM) { + RejectPromiseWithStateError(aData->mPromiseId, + "Null CDM in md_CloseSession"_ns); + return; + } + mCDM->CloseSession(aData->mPromiseId, aData->mSessionId); +} + +void MediaDrmCDMProxy::md_Shutdown() { + MOZ_ASSERT(IsOnOwnerThread()); + MOZ_ASSERT(mCDM); + if (mShutdownCalled) { + return; + } + mShutdownCalled = true; + mCDM->Shutdown(); + mCDM = nullptr; +} + +} // namespace mozilla diff --git a/dom/media/eme/mediadrm/MediaDrmCDMProxy.h b/dom/media/eme/mediadrm/MediaDrmCDMProxy.h new file mode 100644 index 0000000000..89d562eac0 --- /dev/null +++ b/dom/media/eme/mediadrm/MediaDrmCDMProxy.h @@ -0,0 +1,186 @@ +/* -*- 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 MediaDrmCDMProxy_h_ +#define MediaDrmCDMProxy_h_ + +#include +#include "mozilla/jni/Types.h" +#include "mozilla/CDMProxy.h" +#include "mozilla/CDMCaps.h" +#include "mozilla/dom/MediaKeys.h" +#include "mozilla/dom/MediaKeySession.h" +#include "mozilla/MediaDrmProxySupport.h" +#include "mozilla/UniquePtr.h" + +#include "MediaCodec.h" +#include "nsString.h" + +namespace mozilla { + +class MediaDrmCDMCallbackProxy; +class MediaDrmCDMProxy final : public CDMProxy { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaDrmCDMProxy, override) + + MediaDrmCDMProxy(dom::MediaKeys* aKeys, const nsAString& aKeySystem, + bool aDistinctiveIdentifierRequired, + bool aPersistentStateRequired); + + void Init(PromiseId aPromiseId, const nsAString& aOrigin, + const nsAString& aTopLevelOrigin, + const nsAString& aGMPName) override; + + void CreateSession(uint32_t aCreateSessionToken, + MediaKeySessionType aSessionType, PromiseId aPromiseId, + const nsAString& aInitDataType, + nsTArray& aInitData) override; + + void LoadSession(PromiseId aPromiseId, dom::MediaKeySessionType aSessionType, + const nsAString& aSessionId) override; + + void SetServerCertificate(PromiseId aPromiseId, + nsTArray& aCert) override; + + void UpdateSession(const nsAString& aSessionId, PromiseId aPromiseId, + nsTArray& aResponse) override; + + void CloseSession(const nsAString& aSessionId, PromiseId aPromiseId) override; + + void RemoveSession(const nsAString& aSessionId, + PromiseId aPromiseId) override; + + void QueryOutputProtectionStatus() override; + + void NotifyOutputProtectionStatus( + OutputProtectionCheckStatus aCheckStatus, + OutputProtectionCaptureStatus aCaptureStatus) override; + + void Shutdown() override; + + void Terminated() override; + + void OnSetSessionId(uint32_t aCreateSessionToken, + const nsAString& aSessionId) override; + + void OnResolveLoadSessionPromise(uint32_t aPromiseId, bool aSuccess) override; + + void OnSessionMessage(const nsAString& aSessionId, + dom::MediaKeyMessageType aMessageType, + const nsTArray& aMessage) override; + + void OnExpirationChange(const nsAString& aSessionId, + UnixTime aExpiryTime) override; + + void OnSessionClosed(const nsAString& aSessionId) override; + + void OnSessionError(const nsAString& aSessionId, nsresult aException, + uint32_t aSystemCode, const nsAString& aMsg) override; + + void OnRejectPromise(uint32_t aPromiseId, ErrorResult&& aException, + const nsCString& aMsg) override; + + RefPtr Decrypt(MediaRawData* aSample) override; + void OnDecrypted(uint32_t aId, DecryptStatus aResult, + const nsTArray& aDecryptedData) override; + + void RejectPromise(PromiseId aId, ErrorResult&& aException, + const nsCString& aReason) override; + // Reject promise with an InvalidStateError and the given message. + void RejectPromiseWithStateError(PromiseId aId, const nsCString& aReason); + + // Resolves promise with "undefined". + // Can be called from any thread. + void ResolvePromise(PromiseId aId) override; + + void OnKeyStatusesChange(const nsAString& aSessionId) override; + + void GetStatusForPolicy(PromiseId aPromiseId, + const dom::HDCPVersion& aMinHdcpVersion) override; + +#ifdef DEBUG + bool IsOnOwnerThread() override; +#endif + + const nsString& GetMediaDrmStubId() const; + + private: + virtual ~MediaDrmCDMProxy(); + + void OnCDMCreated(uint32_t aPromiseId); + + template + void ResolvePromiseWithResult(PromiseId aId, const T& aResult); + + struct CreateSessionData { + MediaKeySessionType mSessionType; + uint32_t mCreateSessionToken; + PromiseId mPromiseId; + nsCString mInitDataType; + nsTArray mInitData; + }; + + struct UpdateSessionData { + PromiseId mPromiseId; + nsCString mSessionId; + nsTArray mResponse; + }; + + struct SessionOpData { + PromiseId mPromiseId; + nsCString mSessionId; + }; + + class RejectPromiseTask : public Runnable { + public: + RejectPromiseTask(MediaDrmCDMProxy* aProxy, PromiseId aId, + ErrorResult&& aException, const nsCString& aReason) + : Runnable("RejectPromiseTask"), + mProxy(aProxy), + mId(aId), + mException(std::move(aException)), + mReason(aReason) {} + NS_IMETHOD Run() override { + // Moving into or out of a non-copyable ErrorResult will assert that both + // ErorResults are from our current thread. Avoid the assertion by moving + // into a current-thread CopyableErrorResult first. Note that this is + // safe, because CopyableErrorResult never holds state that can't move + // across threads. + CopyableErrorResult rv(std::move(mException)); + mProxy->RejectPromise(mId, std::move(rv), mReason); + return NS_OK; + } + + private: + RefPtr mProxy; + PromiseId mId; + // We use a CopyableErrorResult here, because we're going to dispatch to a + // different thread and normal ErrorResult doesn't support that. + // CopyableErrorResult ensures that it only stores values that are safe to + // move across threads. + CopyableErrorResult mException; + nsCString mReason; + }; + + nsCString mNodeId; + UniquePtr mCDM; + bool mShutdownCalled; + + // ===================================================================== + // For MediaDrmProxySupport + void md_Init(uint32_t aPromiseId); + void md_CreateSession(UniquePtr&& aData); + void md_SetServerCertificate(PromiseId aPromiseId, + const nsTArray& aCert); + void md_UpdateSession(UniquePtr&& aData); + void md_CloseSession(UniquePtr&& aData); + void md_Shutdown(); + // ===================================================================== +}; + +} // namespace mozilla + +#endif // MediaDrmCDMProxy_h_ diff --git a/dom/media/eme/mediadrm/MediaDrmProxySupport.cpp b/dom/media/eme/mediadrm/MediaDrmProxySupport.cpp new file mode 100644 index 0000000000..717a57df35 --- /dev/null +++ b/dom/media/eme/mediadrm/MediaDrmProxySupport.cpp @@ -0,0 +1,272 @@ +/* -*- 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 "MediaDrmProxySupport.h" +#include "MediaDrmCDMCallbackProxy.h" +#include "mozilla/EMEUtils.h" +#include "mozilla/java/MediaDrmProxyNatives.h" +#include "mozilla/java/SessionKeyInfoWrappers.h" +#include "MediaCodec.h" // For MediaDrm::KeyStatus + +namespace mozilla { + +LogModule* GetMDRMNLog() { + static LazyLogModule log("MediaDrmProxySupport"); + return log; +} + +class MediaDrmJavaCallbacksSupport + : public java::MediaDrmProxy::NativeMediaDrmProxyCallbacks::Natives< + MediaDrmJavaCallbacksSupport> { + public: + typedef java::MediaDrmProxy::NativeMediaDrmProxyCallbacks::Natives< + MediaDrmJavaCallbacksSupport> + MediaDrmProxyNativeCallbacks; + using MediaDrmProxyNativeCallbacks::AttachNative; + using MediaDrmProxyNativeCallbacks::DisposeNative; + + explicit MediaDrmJavaCallbacksSupport( + UniquePtr&& aDecryptorProxyCallback) + : mDecryptorProxyCallback(std::move(aDecryptorProxyCallback)) { + MOZ_ASSERT(mDecryptorProxyCallback); + } + /* + * Native implementation, called by Java. + */ + void OnSessionCreated(int aCreateSessionToken, int aPromiseId, + jni::ByteArray::Param aSessionId, + jni::ByteArray::Param aRequest); + + void OnSessionUpdated(int aPromiseId, jni::ByteArray::Param aSessionId); + + void OnSessionClosed(int aPromiseId, jni::ByteArray::Param aSessionId); + + void OnSessionMessage( + jni::ByteArray::Param aSessionId, + int /*mozilla::dom::MediaKeyMessageType*/ aSessionMessageType, + jni::ByteArray::Param aRequest); + + void OnSessionError(jni::ByteArray::Param aSessionId, + jni::String::Param aMessage); + + void OnSessionBatchedKeyChanged(jni::ByteArray::Param, + jni::ObjectArray::Param); + + void OnRejectPromise(int aPromiseId, jni::String::Param aMessage); + + private: + UniquePtr mDecryptorProxyCallback; +}; // MediaDrmJavaCallbacksSupport + +void MediaDrmJavaCallbacksSupport::OnSessionCreated( + int aCreateSessionToken, int aPromiseId, jni::ByteArray::Param aSessionId, + jni::ByteArray::Param aRequest) { + MOZ_ASSERT(NS_IsMainThread()); + auto reqDataArray = aRequest->GetElements(); + nsCString sessionId( + reinterpret_cast(aSessionId->GetElements().Elements()), + aSessionId->Length()); + MDRMN_LOG("SessionId(%s) closed", sessionId.get()); + + mDecryptorProxyCallback->SetSessionId(aCreateSessionToken, sessionId); + mDecryptorProxyCallback->ResolvePromise(aPromiseId); +} + +void MediaDrmJavaCallbacksSupport::OnSessionUpdated( + int aPromiseId, jni::ByteArray::Param aSessionId) { + MOZ_ASSERT(NS_IsMainThread()); + MDRMN_LOG( + "SessionId(%s) closed", + nsCString(reinterpret_cast(aSessionId->GetElements().Elements()), + aSessionId->Length()) + .get()); + mDecryptorProxyCallback->ResolvePromise(aPromiseId); +} + +void MediaDrmJavaCallbacksSupport::OnSessionClosed( + int aPromiseId, jni::ByteArray::Param aSessionId) { + MOZ_ASSERT(NS_IsMainThread()); + nsCString sessionId( + reinterpret_cast(aSessionId->GetElements().Elements()), + aSessionId->Length()); + MDRMN_LOG("SessionId(%s) closed", sessionId.get()); + mDecryptorProxyCallback->ResolvePromise(aPromiseId); + mDecryptorProxyCallback->SessionClosed(sessionId); +} + +void MediaDrmJavaCallbacksSupport::OnSessionMessage( + jni::ByteArray::Param aSessionId, + int /*mozilla::dom::MediaKeyMessageType*/ aMessageType, + jni::ByteArray::Param aRequest) { + MOZ_ASSERT(NS_IsMainThread()); + nsCString sessionId( + reinterpret_cast(aSessionId->GetElements().Elements()), + aSessionId->Length()); + auto reqDataArray = aRequest->GetElements(); + + nsTArray retRequest; + retRequest.AppendElements(reinterpret_cast(reqDataArray.Elements()), + reqDataArray.Length()); + + mDecryptorProxyCallback->SessionMessage( + sessionId, static_cast(aMessageType), + retRequest); +} + +void MediaDrmJavaCallbacksSupport::OnSessionError( + jni::ByteArray::Param aSessionId, jni::String::Param aMessage) { + MOZ_ASSERT(NS_IsMainThread()); + nsCString sessionId( + reinterpret_cast(aSessionId->GetElements().Elements()), + aSessionId->Length()); + nsCString errorMessage = aMessage->ToCString(); + MDRMN_LOG("SessionId(%s)", sessionId.get()); + // TODO: We cannot get system error code from media drm API. + // Currently use -1 as an error code. + mDecryptorProxyCallback->SessionError( + sessionId, NS_ERROR_DOM_INVALID_STATE_ERR, -1, errorMessage); +} + +// TODO: MediaDrm.KeyStatus defined the status code not included +// dom::MediaKeyStatus::Released and dom::MediaKeyStatus::Output_downscaled. +// Should keep tracking for this if it will be changed in the future. +static dom::MediaKeyStatus MediaDrmKeyStatusToMediaKeyStatus(int aStatusCode) { + using mozilla::java::sdk::MediaDrm; + switch (aStatusCode) { + case MediaDrm::KeyStatus::STATUS_USABLE: + return dom::MediaKeyStatus::Usable; + case MediaDrm::KeyStatus::STATUS_EXPIRED: + return dom::MediaKeyStatus::Expired; + case MediaDrm::KeyStatus::STATUS_OUTPUT_NOT_ALLOWED: + return dom::MediaKeyStatus::Output_restricted; + case MediaDrm::KeyStatus::STATUS_INTERNAL_ERROR: + return dom::MediaKeyStatus::Internal_error; + case MediaDrm::KeyStatus::STATUS_PENDING: + return dom::MediaKeyStatus::Status_pending; + default: + return dom::MediaKeyStatus::Internal_error; + } +} + +void MediaDrmJavaCallbacksSupport::OnSessionBatchedKeyChanged( + jni::ByteArray::Param aSessionId, jni::ObjectArray::Param aKeyInfos) { + MOZ_ASSERT(NS_IsMainThread()); + nsCString sessionId( + reinterpret_cast(aSessionId->GetElements().Elements()), + aSessionId->Length()); + nsTArray keyInfosObjectArray(aKeyInfos->GetElements()); + + nsTArray keyInfosArray; + + for (auto&& keyInfoObject : keyInfosObjectArray) { + java::SessionKeyInfo::LocalRef keyInfo(std::move(keyInfoObject)); + mozilla::jni::ByteArray::LocalRef keyIdByteArray = keyInfo->KeyId(); + nsTArray keyIdInt8Array = keyIdByteArray->GetElements(); + // Cast nsTArray to nsTArray + nsTArray* keyId = + reinterpret_cast*>(&keyIdInt8Array); + auto keyStatus = keyInfo->Status(); // int32_t + keyInfosArray.AppendElement( + CDMKeyInfo(*keyId, dom::Optional( + MediaDrmKeyStatusToMediaKeyStatus(keyStatus)))); + } + + mDecryptorProxyCallback->BatchedKeyStatusChanged(sessionId, keyInfosArray); +} + +void MediaDrmJavaCallbacksSupport::OnRejectPromise( + int aPromiseId, jni::String::Param aMessage) { + MOZ_ASSERT(NS_IsMainThread()); + nsCString reason = aMessage->ToCString(); + MDRMN_LOG("OnRejectPromise aMessage(%s) ", reason.get()); + // Current implementation assume all the reject from MediaDrm is due to + // invalid state. Other cases should be handled before calling into + // MediaDrmProxy API. + ErrorResult rv; + rv.ThrowInvalidStateError(reason); + mDecryptorProxyCallback->RejectPromise(aPromiseId, std::move(rv), reason); +} + +MediaDrmProxySupport::MediaDrmProxySupport(const nsAString& aKeySystem) + : mKeySystem(aKeySystem), mDestroyed(false) { + mJavaCallbacks = java::MediaDrmProxy::NativeMediaDrmProxyCallbacks::New(); + + mBridgeProxy = java::MediaDrmProxy::Create(mKeySystem, mJavaCallbacks); + + MOZ_ASSERT(mBridgeProxy, "mBridgeProxy should not be null"); + mMediaDrmStubId = mBridgeProxy->GetStubId()->ToString(); +} + +MediaDrmProxySupport::~MediaDrmProxySupport() { + MOZ_ASSERT(mDestroyed, "Shutdown() should be called before !!"); + MediaDrmJavaCallbacksSupport::DisposeNative(mJavaCallbacks); +} + +nsresult MediaDrmProxySupport::Init( + UniquePtr&& aCallback) { + MOZ_ASSERT(mJavaCallbacks); + + MediaDrmJavaCallbacksSupport::AttachNative( + mJavaCallbacks, + mozilla::MakeUnique(std::move(aCallback))); + return mBridgeProxy != nullptr ? NS_OK : NS_ERROR_FAILURE; +} + +void MediaDrmProxySupport::CreateSession(uint32_t aCreateSessionToken, + uint32_t aPromiseId, + const nsCString& aInitDataType, + const nsTArray& aInitData, + MediaDrmSessionType aSessionType) { + MOZ_ASSERT(mBridgeProxy); + + auto initDataBytes = mozilla::jni::ByteArray::New( + reinterpret_cast(&aInitData[0]), aInitData.Length()); + // TODO: aSessionType is not used here. + // Refer to + // http://androidxref.com/5.1.1_r6/xref/external/chromium_org/media/base/android/java/src/org/chromium/media/MediaDrmBridge.java#420 + // it is hard code to streaming type. + mBridgeProxy->CreateSession(aCreateSessionToken, aPromiseId, + NS_ConvertUTF8toUTF16(aInitDataType), + initDataBytes); +} + +void MediaDrmProxySupport::UpdateSession(uint32_t aPromiseId, + const nsCString& aSessionId, + const nsTArray& aResponse) { + MOZ_ASSERT(mBridgeProxy); + + auto response = mozilla::jni::ByteArray::New( + reinterpret_cast(aResponse.Elements()), + aResponse.Length()); + mBridgeProxy->UpdateSession(aPromiseId, NS_ConvertUTF8toUTF16(aSessionId), + response); +} + +void MediaDrmProxySupport::CloseSession(uint32_t aPromiseId, + const nsCString& aSessionId) { + MOZ_ASSERT(mBridgeProxy); + + mBridgeProxy->CloseSession(aPromiseId, NS_ConvertUTF8toUTF16(aSessionId)); +} + +void MediaDrmProxySupport::Shutdown() { + MOZ_ASSERT(mBridgeProxy); + + if (mDestroyed) { + return; + } + mBridgeProxy->Destroy(); + mDestroyed = true; +} + +bool MediaDrmProxySupport::SetServerCertificate( + const nsTArray& aCert) { + jni::ByteArray::LocalRef cert = jni::ByteArray::New( + reinterpret_cast(aCert.Elements()), aCert.Length()); + return mBridgeProxy->SetServerCertificate(cert); +} + +} // namespace mozilla diff --git a/dom/media/eme/mediadrm/MediaDrmProxySupport.h b/dom/media/eme/mediadrm/MediaDrmProxySupport.h new file mode 100644 index 0000000000..e994036f69 --- /dev/null +++ b/dom/media/eme/mediadrm/MediaDrmProxySupport.h @@ -0,0 +1,68 @@ +/* -*- 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 MediaDrmProxySupport_H +#define MediaDrmProxySupport_H + +#include "mozilla/DecryptorProxyCallback.h" +#include "mozilla/java/MediaDrmProxyWrappers.h" +#include "mozilla/Logging.h" +#include "mozilla/UniquePtr.h" +#include "nsString.h" + +namespace mozilla { + +enum MediaDrmSessionType { + kKeyStreaming = 1, + kKeyOffline = 2, + kKeyRelease = 3, +}; + +#ifndef MDRMN_LOG +LogModule* GetMDRMNLog(); +# define MDRMN_LOG(x, ...) \ + MOZ_LOG(GetMDRMNLog(), mozilla::LogLevel::Debug, \ + ("[MediaDrmProxySupport][%s]" x, __FUNCTION__, ##__VA_ARGS__)) +#endif + +class MediaDrmCDMCallbackProxy; + +class MediaDrmProxySupport final { + public: + explicit MediaDrmProxySupport(const nsAString& aKeySystem); + ~MediaDrmProxySupport(); + + /* + * APIs to act as GMPDecryptorAPI, discarding unnecessary calls. + */ + nsresult Init(UniquePtr&& aCallback); + + void CreateSession(uint32_t aCreateSessionToken, uint32_t aPromiseId, + const nsCString& aInitDataType, + const nsTArray& aInitData, + MediaDrmSessionType aSessionType); + + void UpdateSession(uint32_t aPromiseId, const nsCString& aSessionId, + const nsTArray& aResponse); + + void CloseSession(uint32_t aPromiseId, const nsCString& aSessionId); + + void Shutdown(); + + const nsString& GetMediaDrmStubId() const { return mMediaDrmStubId; } + + bool SetServerCertificate(const nsTArray& aCert); + + private: + const nsString mKeySystem; + java::MediaDrmProxy::GlobalRef mBridgeProxy; + java::MediaDrmProxy::NativeMediaDrmProxyCallbacks::GlobalRef mJavaCallbacks; + bool mDestroyed; + nsString mMediaDrmStubId; +}; + +} // namespace mozilla +#endif // MediaDrmProxySupport_H diff --git a/dom/media/eme/mediadrm/moz.build b/dom/media/eme/mediadrm/moz.build new file mode 100644 index 0000000000..c8f433f25f --- /dev/null +++ b/dom/media/eme/mediadrm/moz.build @@ -0,0 +1,19 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXPORTS.mozilla += [ + "MediaDrmCDMCallbackProxy.h", + "MediaDrmCDMProxy.h", + "MediaDrmProxySupport.h", +] + +UNIFIED_SOURCES += [ + "MediaDrmCDMCallbackProxy.cpp", + "MediaDrmCDMProxy.cpp", + "MediaDrmProxySupport.cpp", +] + +FINAL_LIBRARY = "xul" diff --git a/dom/media/eme/mediafoundation/WMFCDMImpl.cpp b/dom/media/eme/mediafoundation/WMFCDMImpl.cpp new file mode 100644 index 0000000000..1fe42aa8e2 --- /dev/null +++ b/dom/media/eme/mediafoundation/WMFCDMImpl.cpp @@ -0,0 +1,151 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "WMFCDMImpl.h" + +#include + +#include "mozilla/AppShutdown.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/dom/MediaKeySession.h" +#include "mozilla/dom/KeySystemNames.h" + +namespace mozilla { + +/* static */ +bool WMFCDMImpl::Supports(const nsAString& aKeySystem) { + MOZ_ASSERT(NS_IsMainThread()); + if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) { + return false; + } + + static std::map sSupports; + static bool sSetRunOnShutdown = false; + if (!sSetRunOnShutdown) { + GetMainThreadSerialEventTarget()->Dispatch( + NS_NewRunnableFunction("WMFCDMImpl::Supports", [&] { + RunOnShutdown([&] { sSupports.clear(); }, + ShutdownPhase::XPCOMShutdown); + })); + sSetRunOnShutdown = true; + } + + nsString key(aKeySystem); + if (const auto& s = sSupports.find(key); s != sSupports.end()) { + return s->second; + } + + RefPtr cdm = MakeRefPtr(aKeySystem); + nsTArray configs; + bool s = cdm->GetCapabilities(configs); + sSupports[key] = s; + return s; +} + +bool WMFCDMImpl::GetCapabilities(nsTArray& aOutConfigs) { + MOZ_ASSERT(NS_IsMainThread()); + if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) { + return false; + } + + static std::unordered_map> + sKeySystemConfigs{}; + static bool sSetRunOnShutdown = false; + if (!sSetRunOnShutdown) { + GetMainThreadSerialEventTarget()->Dispatch( + NS_NewRunnableFunction("WMFCDMImpl::GetCapabilities", [&] { + RunOnShutdown([&] { sKeySystemConfigs.clear(); }, + ShutdownPhase::XPCOMShutdown); + })); + sSetRunOnShutdown = true; + } + + // Retrieve result from our cached key system + auto keySystem = std::string{NS_ConvertUTF16toUTF8(mKeySystem).get()}; + if (auto rv = sKeySystemConfigs.find(keySystem); + rv != sKeySystemConfigs.end()) { + EME_LOG("Return cached capabilities for %s", keySystem.c_str()); + for (const auto& config : rv->second) { + aOutConfigs.AppendElement(config); + EME_LOG("-- capabilities (%s)", + NS_ConvertUTF16toUTF8(config.GetDebugInfo()).get()); + } + return true; + } + + // Not cached result, ask the remote process. + nsCOMPtr backgroundTaskQueue; + NS_CreateBackgroundTaskQueue(__func__, getter_AddRefs(backgroundTaskQueue)); + if (!mCDM) { + mCDM = MakeRefPtr(mKeySystem); + } + bool ok = false; + static const bool sIsHwSecure[2] = {false, true}; + for (const auto& isHWSecure : sIsHwSecure) { + media::Await( + do_AddRef(backgroundTaskQueue), mCDM->GetCapabilities(isHWSecure), + [&ok, &aOutConfigs, keySystem, + isHWSecure](const MFCDMCapabilitiesIPDL& capabilities) { + EME_LOG("capabilities: keySystem=%s (hw-secure=%d)", + keySystem.c_str(), isHWSecure); + for (const auto& v : capabilities.videoCapabilities()) { + EME_LOG("capabilities: video=%s", + NS_ConvertUTF16toUTF8(v.contentType()).get()); + } + for (const auto& a : capabilities.audioCapabilities()) { + EME_LOG("capabilities: audio=%s", + NS_ConvertUTF16toUTF8(a.contentType()).get()); + } + for (const auto& v : capabilities.encryptionSchemes()) { + EME_LOG("capabilities: encryptionScheme=%s", + EncryptionSchemeStr(v)); + } + KeySystemConfig* config = aOutConfigs.AppendElement(); + MFCDMCapabilitiesIPDLToKeySystemConfig(capabilities, *config); + sKeySystemConfigs[keySystem].AppendElement(*config); + // This is equal to "com.microsoft.playready.recommendation.3000", so + // we can store it directly without asking the remote process again. + if (keySystem.compare(kPlayReadyKeySystemName) == 0 && isHWSecure) { + config->mKeySystem.AssignLiteral(kPlayReadyKeySystemHardware); + sKeySystemConfigs["com.microsoft.playready.recommendation.3000"] + .AppendElement(*config); + } + ok = true; + }, + [](nsresult rv) { + EME_LOG("Fail to get key system capabilities. rv=%x", uint32_t(rv)); + }); + } + return ok; +} + +RefPtr WMFCDMImpl::Init( + const WMFCDMImpl::InitParams& aParams) { + if (!mCDM) { + mCDM = MakeRefPtr(mKeySystem); + } + RefPtr self = this; + mCDM->Init(aParams.mOrigin, aParams.mInitDataTypes, + aParams.mPersistentStateRequired + ? KeySystemConfig::Requirement::Required + : KeySystemConfig::Requirement::Optional, + aParams.mDistinctiveIdentifierRequired + ? KeySystemConfig::Requirement::Required + : KeySystemConfig::Requirement::Optional, + aParams.mAudioCapabilities, aParams.mVideoCapabilities, + aParams.mProxyCallback) + ->Then( + mCDM->ManagerThread(), __func__, + [self, this](const MFCDMInitIPDL& init) { + mInitPromiseHolder.ResolveIfExists(true, __func__); + }, + [self, this](const nsresult rv) { + mInitPromiseHolder.RejectIfExists(rv, __func__); + }); + return mInitPromiseHolder.Ensure(__func__); +} + +} // namespace mozilla diff --git a/dom/media/eme/mediafoundation/WMFCDMImpl.h b/dom/media/eme/mediafoundation/WMFCDMImpl.h new file mode 100644 index 0000000000..452629ec84 --- /dev/null +++ b/dom/media/eme/mediafoundation/WMFCDMImpl.h @@ -0,0 +1,124 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 DOM_MEDIA_EME_MEDIAFOUNDATION_WMFCDMIMPL_H_ +#define DOM_MEDIA_EME_MEDIAFOUNDATION_WMFCDMIMPL_H_ + +#include "MediaData.h" +#include "mozilla/Assertions.h" +#include "mozilla/EMEUtils.h" +#include "mozilla/KeySystemConfig.h" +#include "mozilla/media/MediaUtils.h" +#include "mozilla/MFCDMChild.h" +#include "nsString.h" +#include "nsThreadUtils.h" + +namespace mozilla { + +class WMFCDMProxyCallback; + +/** + * WMFCDMImpl is a helper class for MFCDM protocol clients. It creates, manages, + * and calls MFCDMChild object in the content process on behalf of the client, + * and performs conversion between EME and MFCDM types and constants. This class + * can be used in two ways (1) call Supports/GetCapabilities to know the + * information about given key system or config (2) do session-related + * operations. In this case, Init() MUST be called first. + */ +class WMFCDMImpl final { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WMFCDMImpl); + + explicit WMFCDMImpl(const nsAString& aKeySystem) : mKeySystem(aKeySystem) {} + + static bool Supports(const nsAString& aKeySystem); + // TODO: make this async? + bool GetCapabilities(nsTArray& aOutConfigs); + + using InitPromise = GenericPromise; + struct InitParams { + nsString mOrigin; + CopyableTArray mInitDataTypes; + bool mPersistentStateRequired; + bool mDistinctiveIdentifierRequired; + WMFCDMProxyCallback* mProxyCallback; + CopyableTArray mAudioCapabilities; + CopyableTArray mVideoCapabilities; + }; + + RefPtr Init(const InitParams& aParams); + + // Following functions MUST be called after calling Init(). + RefPtr CreateSession( + uint32_t aPromiseId, const KeySystemConfig::SessionType aSessionType, + const nsAString& aInitDataType, const nsTArray& aInitData) { + MOZ_DIAGNOSTIC_ASSERT(mCDM); + return mCDM->CreateSessionAndGenerateRequest(aPromiseId, aSessionType, + aInitDataType, aInitData); + } + + RefPtr LoadSession( + uint32_t aPromiseId, const KeySystemConfig::SessionType aSessionType, + const nsAString& aSessionId) { + MOZ_DIAGNOSTIC_ASSERT(mCDM); + return mCDM->LoadSession(aPromiseId, aSessionType, aSessionId); + } + + RefPtr UpdateSession(uint32_t aPromiseId, + const nsAString& aSessionId, + nsTArray& aResponse) { + MOZ_DIAGNOSTIC_ASSERT(mCDM); + return mCDM->UpdateSession(aPromiseId, aSessionId, aResponse); + } + + RefPtr CloseSession(uint32_t aPromiseId, + const nsAString& aSessionId) { + MOZ_DIAGNOSTIC_ASSERT(mCDM); + return mCDM->CloseSession(aPromiseId, aSessionId); + } + + RefPtr RemoveSession(uint32_t aPromiseId, + const nsAString& aSessionId) { + MOZ_DIAGNOSTIC_ASSERT(mCDM); + return mCDM->RemoveSession(aPromiseId, aSessionId); + } + + RefPtr SetServerCertificate(uint32_t aPromiseId, + nsTArray& aCert) { + MOZ_DIAGNOSTIC_ASSERT(mCDM); + return mCDM->SetServerCertificate(aPromiseId, aCert); + } + + RefPtr GetStatusForPolicy( + uint32_t aPromiseId, const dom::HDCPVersion& aMinHdcpVersion) { + MOZ_DIAGNOSTIC_ASSERT(mCDM); + return mCDM->GetStatusForPolicy(aPromiseId, aMinHdcpVersion); + } + + uint64_t Id() { + MOZ_DIAGNOSTIC_ASSERT(mCDM, + "Should be called only after Init() is resolved"); + MOZ_DIAGNOSTIC_ASSERT(mCDM->Id() != 0, + "Should be called only after Init() is resolved"); + return mCDM->Id(); + } + + private: + ~WMFCDMImpl() { + if (mCDM) { + mCDM->Shutdown(); + } + }; + + const nsString mKeySystem; + RefPtr mCDM; + + MozPromiseHolder mInitPromiseHolder; +}; + +} // namespace mozilla + +#endif // DOM_MEDIA_EME_MEDIAFOUNDATION_WMFCDMIMPL_H_ diff --git a/dom/media/eme/mediafoundation/WMFCDMProxy.cpp b/dom/media/eme/mediafoundation/WMFCDMProxy.cpp new file mode 100644 index 0000000000..21207ecc22 --- /dev/null +++ b/dom/media/eme/mediafoundation/WMFCDMProxy.cpp @@ -0,0 +1,414 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "WMFCDMProxy.h" + +#include "mozilla/dom/MediaKeysBinding.h" +#include "mozilla/dom/MediaKeySession.h" +#include "mozilla/dom/MediaKeySystemAccessBinding.h" +#include "mozilla/EMEUtils.h" +#include "mozilla/WMFCDMProxyCallback.h" +#include "mozilla/WindowsVersion.h" +#include "WMFCDMImpl.h" +#include "WMFCDMProxyCallback.h" + +namespace mozilla { + +#define LOG(msg, ...) \ + EME_LOG("WMFCDMProxy[%p]@%s: " msg, this, __func__, ##__VA_ARGS__) + +#define RETURN_IF_SHUTDOWN() \ + do { \ + MOZ_ASSERT(NS_IsMainThread()); \ + if (mIsShutdown) { \ + return; \ + } \ + } while (false) + +#define PERFORM_ON_CDM(operation, promiseId, ...) \ + do { \ + mCDM->operation(promiseId, __VA_ARGS__) \ + ->Then( \ + mMainThread, __func__, \ + [self = RefPtr{this}, this, promiseId]() { \ + RETURN_IF_SHUTDOWN(); \ + if (mKeys.IsNull()) { \ + EME_LOG("WMFCDMProxy(this=%p, pid=%" PRIu32 \ + ") : abort the " #operation " due to empty key", \ + this, promiseId); \ + return; \ + } \ + ResolvePromise(promiseId); \ + }, \ + [self = RefPtr{this}, this, promiseId]() { \ + RETURN_IF_SHUTDOWN(); \ + RejectPromiseWithStateError( \ + promiseId, nsLiteralCString("WMFCDMProxy::" #operation ": " \ + "failed to " #operation)); \ + }); \ + } while (false) + +WMFCDMProxy::WMFCDMProxy(dom::MediaKeys* aKeys, const nsAString& aKeySystem, + const dom::MediaKeySystemConfiguration& aConfig) + : CDMProxy( + aKeys, aKeySystem, + aConfig.mDistinctiveIdentifier == dom::MediaKeysRequirement::Required, + aConfig.mPersistentState == dom::MediaKeysRequirement::Required), + mConfig(aConfig) { + MOZ_ASSERT(NS_IsMainThread()); +} + +WMFCDMProxy::~WMFCDMProxy() {} + +void WMFCDMProxy::Init(PromiseId aPromiseId, const nsAString& aOrigin, + const nsAString& aTopLevelOrigin, + const nsAString& aName) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!aOrigin.IsEmpty()); + + MOZ_ASSERT(!mOwnerThread); + if (NS_FAILED( + NS_NewNamedThread("WMFCDMThread", getter_AddRefs(mOwnerThread)))) { + RejectPromiseWithStateError( + aPromiseId, + nsLiteralCString("WMFCDMProxy::Init: couldn't create CDM thread")); + return; + } + + mCDM = MakeRefPtr(mKeySystem); + mProxyCallback = new WMFCDMProxyCallback(this); + // SWDRM has a PMP process leakage problem on Windows 10 due to the internal + // issue in the media foundation. Microsoft only lands the solution on Windows + // 11 and doesn't have plan to port it back to Windows 10. Therefore, for + // PlayReady, we need to force to covert SL2000 to SL3000 for our underlying + // CDM to avoid that issue. In addition, as we only support L1 for Widevine, + // this issue won't happen on it. + Maybe forcedRobustness = + IsPlayReadyKeySystemAndSupported(mKeySystem) && !IsWin11OrLater() + ? Some(nsString(u"3000")) + : Nothing(); + WMFCDMImpl::InitParams params{ + nsString(aOrigin), + mConfig.mInitDataTypes, + mPersistentStateRequired, + mDistinctiveIdentifierRequired, + mProxyCallback, + GenerateMFCDMMediaCapabilities(mConfig.mAudioCapabilities), + GenerateMFCDMMediaCapabilities(mConfig.mVideoCapabilities, + forcedRobustness)}; + mCDM->Init(params)->Then( + mMainThread, __func__, + [self = RefPtr{this}, this, aPromiseId](const bool) { + MOZ_ASSERT(mCDM->Id() > 0); + mKeys->OnCDMCreated(aPromiseId, mCDM->Id()); + }, + [self = RefPtr{this}, this, aPromiseId](const nsresult rv) { + RejectPromiseWithStateError( + aPromiseId, + nsLiteralCString("WMFCDMProxy::Init: WMFCDM init error")); + }); +} + +CopyableTArray +WMFCDMProxy::GenerateMFCDMMediaCapabilities( + const dom::Sequence& aCapabilities, + const Maybe& forcedRobustness) { + CopyableTArray outCapabilites; + for (const auto& capabilities : aCapabilities) { + if (!forcedRobustness) { + EME_LOG("WMFCDMProxy::Init %p, robustness=%s", this, + NS_ConvertUTF16toUTF8(capabilities.mRobustness).get()); + outCapabilites.AppendElement(MFCDMMediaCapability{ + capabilities.mContentType, capabilities.mRobustness}); + } else { + EME_LOG("WMFCDMProxy::Init %p, force to robustness=%s", this, + NS_ConvertUTF16toUTF8(*forcedRobustness).get()); + outCapabilites.AppendElement( + MFCDMMediaCapability{capabilities.mContentType, *forcedRobustness}); + } + } + return outCapabilites; +} + +void WMFCDMProxy::ResolvePromise(PromiseId aId) { + auto resolve = [self = RefPtr{this}, this, aId]() { + RETURN_IF_SHUTDOWN(); + EME_LOG("WMFCDMProxy::ResolvePromise(this=%p, pid=%" PRIu32 ")", this, aId); + if (!mKeys.IsNull()) { + mKeys->ResolvePromise(aId); + } else { + NS_WARNING("WMFCDMProxy unable to resolve promise!"); + } + }; + + if (NS_IsMainThread()) { + resolve(); + return; + } + mMainThread->Dispatch( + NS_NewRunnableFunction("WMFCDMProxy::ResolvePromise", resolve)); +} + +void WMFCDMProxy::ResolvePromiseWithKeyStatus( + const PromiseId& aId, const dom::MediaKeyStatus& aStatus) { + auto resolve = [self = RefPtr{this}, this, aId, aStatus]() { + RETURN_IF_SHUTDOWN(); + EME_LOG("WMFCDMProxy::ResolvePromiseWithKeyStatus(this=%p, pid=%" PRIu32 + ", status=%s)", + this, aId, ToMediaKeyStatusStr(aStatus)); + if (!mKeys.IsNull()) { + mKeys->ResolvePromiseWithKeyStatus(aId, aStatus); + } else { + NS_WARNING("WMFCDMProxy unable to resolve promise!"); + } + }; + + if (NS_IsMainThread()) { + resolve(); + return; + } + mMainThread->Dispatch(NS_NewRunnableFunction( + "WMFCDMProxy::ResolvePromiseWithKeyStatus", resolve)); +} + +void WMFCDMProxy::RejectPromise(PromiseId aId, ErrorResult&& aException, + const nsCString& aReason) { + if (!NS_IsMainThread()) { + // Use CopyableErrorResult to store our exception in the runnable, + // because ErrorResult is not OK to move across threads. + mMainThread->Dispatch( + NewRunnableMethod, + nsCString>("WMFCDMProxy::RejectPromise", this, + &WMFCDMProxy::RejectPromiseOnMainThread, + aId, std::move(aException), aReason), + NS_DISPATCH_NORMAL); + return; + } + EME_LOG("WMFCDMProxy::RejectPromise(this=%p, pid=%" PRIu32 + ", code=0x%x, " + "reason='%s')", + this, aId, aException.ErrorCodeAsInt(), aReason.get()); + if (!mKeys.IsNull()) { + mKeys->RejectPromise(aId, std::move(aException), aReason); + } else { + // We don't have a MediaKeys object to pass the exception to, so silence + // the exception to avoid it asserting due to being unused. + aException.SuppressException(); + } +} + +void WMFCDMProxy::RejectPromiseOnMainThread(PromiseId aId, + CopyableErrorResult&& aException, + const nsCString& aReason) { + RETURN_IF_SHUTDOWN(); + // Moving into or out of a non-copyable ErrorResult will assert that both + // ErorResults are from our current thread. Avoid the assertion by moving + // into a current-thread CopyableErrorResult first. Note that this is safe, + // because CopyableErrorResult never holds state that can't move across + // threads. + CopyableErrorResult rv(std::move(aException)); + RejectPromise(aId, std::move(rv), aReason); +} + +void WMFCDMProxy::RejectPromiseWithStateError(PromiseId aId, + const nsCString& aReason) { + ErrorResult rv; + rv.ThrowInvalidStateError(aReason); + RejectPromise(aId, std::move(rv), aReason); +} + +void WMFCDMProxy::CreateSession(uint32_t aCreateSessionToken, + MediaKeySessionType aSessionType, + PromiseId aPromiseId, + const nsAString& aInitDataType, + nsTArray& aInitData) { + MOZ_ASSERT(NS_IsMainThread()); + RETURN_IF_SHUTDOWN(); + const auto sessionType = ConvertToKeySystemConfigSessionType(aSessionType); + EME_LOG("WMFCDMProxy::CreateSession(this=%p, pid=%" PRIu32 + "), sessionType=%s", + this, aPromiseId, SessionTypeToStr(sessionType)); + mCDM->CreateSession(aPromiseId, sessionType, aInitDataType, aInitData) + ->Then( + mMainThread, __func__, + [self = RefPtr{this}, this, aCreateSessionToken, + aPromiseId](nsString sessionID) { + RETURN_IF_SHUTDOWN(); + if (mKeys.IsNull()) { + EME_LOG("WMFCDMProxy(this=%p, pid=%" PRIu32 + ") : abort the create session due to " + "empty key", + this, aPromiseId); + return; + } + if (RefPtr session = + mKeys->GetPendingSession(aCreateSessionToken)) { + session->SetSessionId(std::move(sessionID)); + } + ResolvePromise(aPromiseId); + }, + [self = RefPtr{this}, this, aPromiseId]() { + RETURN_IF_SHUTDOWN(); + RejectPromiseWithStateError( + aPromiseId, + nsLiteralCString( + "WMFCDMProxy::CreateSession: cannot create session")); + }); +} + +void WMFCDMProxy::LoadSession(PromiseId aPromiseId, + dom::MediaKeySessionType aSessionType, + const nsAString& aSessionId) { + MOZ_ASSERT(NS_IsMainThread()); + RETURN_IF_SHUTDOWN(); + const auto sessionType = ConvertToKeySystemConfigSessionType(aSessionType); + EME_LOG("WMFCDMProxy::LoadSession(this=%p, pid=%" PRIu32 + "), sessionType=%s, sessionId=%s", + this, aPromiseId, SessionTypeToStr(sessionType), + NS_ConvertUTF16toUTF8(aSessionId).get()); + PERFORM_ON_CDM(LoadSession, aPromiseId, sessionType, aSessionId); +} + +void WMFCDMProxy::UpdateSession(const nsAString& aSessionId, + PromiseId aPromiseId, + nsTArray& aResponse) { + MOZ_ASSERT(NS_IsMainThread()); + RETURN_IF_SHUTDOWN(); + EME_LOG("WMFCDMProxy::UpdateSession(this=%p, pid=%" PRIu32 + "), sessionId=%s, responseLen=%zu", + this, aPromiseId, NS_ConvertUTF16toUTF8(aSessionId).get(), + aResponse.Length()); + PERFORM_ON_CDM(UpdateSession, aPromiseId, aSessionId, aResponse); +} + +void WMFCDMProxy::CloseSession(const nsAString& aSessionId, + PromiseId aPromiseId) { + MOZ_ASSERT(NS_IsMainThread()); + RETURN_IF_SHUTDOWN(); + EME_LOG("WMFCDMProxy::CloseSession(this=%p, pid=%" PRIu32 "), sessionId=%s", + this, aPromiseId, NS_ConvertUTF16toUTF8(aSessionId).get()); + PERFORM_ON_CDM(CloseSession, aPromiseId, aSessionId); +} + +void WMFCDMProxy::RemoveSession(const nsAString& aSessionId, + PromiseId aPromiseId) { + MOZ_ASSERT(NS_IsMainThread()); + RETURN_IF_SHUTDOWN(); + EME_LOG("WMFCDMProxy::RemoveSession(this=%p, pid=%" PRIu32 "), sessionId=%s", + this, aPromiseId, NS_ConvertUTF16toUTF8(aSessionId).get()); + PERFORM_ON_CDM(RemoveSession, aPromiseId, aSessionId); +} + +void WMFCDMProxy::Shutdown() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mIsShutdown); + if (mProxyCallback) { + mProxyCallback->Shutdown(); + mProxyCallback = nullptr; + } + mIsShutdown = true; +} + +void WMFCDMProxy::OnSessionMessage(const nsAString& aSessionId, + dom::MediaKeyMessageType aMessageType, + const nsTArray& aMessage) { + MOZ_ASSERT(NS_IsMainThread()); + RETURN_IF_SHUTDOWN(); + if (mKeys.IsNull()) { + return; + } + if (RefPtr session = mKeys->GetSession(aSessionId)) { + LOG("Notify key message for session Id=%s", + NS_ConvertUTF16toUTF8(aSessionId).get()); + session->DispatchKeyMessage(aMessageType, aMessage); + } +} + +void WMFCDMProxy::OnKeyStatusesChange(const nsAString& aSessionId) { + MOZ_ASSERT(NS_IsMainThread()); + RETURN_IF_SHUTDOWN(); + if (mKeys.IsNull()) { + return; + } + if (RefPtr session = mKeys->GetSession(aSessionId)) { + LOG("Notify key statuses for session Id=%s", + NS_ConvertUTF16toUTF8(aSessionId).get()); + session->DispatchKeyStatusesChange(); + } +} + +void WMFCDMProxy::OnExpirationChange(const nsAString& aSessionId, + UnixTime aExpiryTime) { + MOZ_ASSERT(NS_IsMainThread()); + RETURN_IF_SHUTDOWN(); + if (mKeys.IsNull()) { + return; + } + if (RefPtr session = mKeys->GetSession(aSessionId)) { + LOG("Notify expiration for session Id=%s", + NS_ConvertUTF16toUTF8(aSessionId).get()); + session->SetExpiration(static_cast(aExpiryTime)); + } +} + +void WMFCDMProxy::SetServerCertificate(PromiseId aPromiseId, + nsTArray& aCert) { + MOZ_ASSERT(NS_IsMainThread()); + RETURN_IF_SHUTDOWN(); + EME_LOG("WMFCDMProxy::SetServerCertificate(this=%p, pid=%" PRIu32 ")", this, + aPromiseId); + mCDM->SetServerCertificate(aPromiseId, aCert) + ->Then( + mMainThread, __func__, + [self = RefPtr{this}, this, aPromiseId]() { + RETURN_IF_SHUTDOWN(); + ResolvePromise(aPromiseId); + }, + [self = RefPtr{this}, this, aPromiseId]() { + RETURN_IF_SHUTDOWN(); + RejectPromiseWithStateError( + aPromiseId, + nsLiteralCString("WMFCDMProxy::SetServerCertificate failed!")); + }); +} + +void WMFCDMProxy::GetStatusForPolicy(PromiseId aPromiseId, + const dom::HDCPVersion& aMinHdcpVersion) { + MOZ_ASSERT(NS_IsMainThread()); + RETURN_IF_SHUTDOWN(); + EME_LOG("WMFCDMProxy::GetStatusForPolicy(this=%p, pid=%" PRIu32 + ", minHDCP=%s)", + this, aPromiseId, + dom::HDCPVersionValues::GetString(aMinHdcpVersion).data()); + mCDM->GetStatusForPolicy(aPromiseId, aMinHdcpVersion) + ->Then( + mMainThread, __func__, + [self = RefPtr{this}, this, aPromiseId]() { + RETURN_IF_SHUTDOWN(); + ResolvePromiseWithKeyStatus(aPromiseId, + dom::MediaKeyStatus::Usable); + }, + [self = RefPtr{this}, this, aPromiseId]() { + RETURN_IF_SHUTDOWN(); + ResolvePromiseWithKeyStatus(aPromiseId, + dom::MediaKeyStatus::Output_restricted); + }); +} + +bool WMFCDMProxy::IsHardwareDecryptionSupported() const { + return mozilla::IsHardwareDecryptionSupported(mConfig); +} + +uint64_t WMFCDMProxy::GetCDMProxyId() const { + MOZ_DIAGNOSTIC_ASSERT(mCDM); + return mCDM->Id(); +} + +#undef LOG +#undef RETURN_IF_SHUTDOWN +#undef PERFORM_ON_CDM + +} // namespace mozilla diff --git a/dom/media/eme/mediafoundation/WMFCDMProxy.h b/dom/media/eme/mediafoundation/WMFCDMProxy.h new file mode 100644 index 0000000000..d41d50997b --- /dev/null +++ b/dom/media/eme/mediafoundation/WMFCDMProxy.h @@ -0,0 +1,149 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 DOM_MEDIA_EME_MEDIAFOUNDATION_WMFCDMPROXY_H_ +#define DOM_MEDIA_EME_MEDIAFOUNDATION_WMFCDMPROXY_H_ + +#include "mozilla/CDMProxy.h" +#include "mozilla/CDMCaps.h" +#include "mozilla/MozPromise.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/MediaKeys.h" +#include "mozilla/dom/MediaKeySession.h" +#include "mozilla/RefPtr.h" +#include "mozilla/WMFCDMImpl.h" + +#include "nsString.h" + +namespace dom { +struct MediaKeySystemMediaCapability; +}; + +namespace mozilla { + +class WMFCDMProxyCallback; + +class WMFCDMProxy : public CDMProxy { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WMFCDMProxy, override) + + WMFCDMProxy(dom::MediaKeys* aKeys, const nsAString& aKeySystem, + const dom::MediaKeySystemConfiguration& aConfig); + + // CDMProxy interface + void Init(PromiseId aPromiseId, const nsAString& aOrigin, + const nsAString& aTopLevelOrigin, const nsAString& aName) override; + + void CreateSession(uint32_t aCreateSessionToken, + MediaKeySessionType aSessionType, PromiseId aPromiseId, + const nsAString& aInitDataType, + nsTArray& aInitData) override; + + void LoadSession(PromiseId aPromiseId, dom::MediaKeySessionType aSessionType, + const nsAString& aSessionId) override; + + void SetServerCertificate(PromiseId aPromiseId, + nsTArray& aCert) override; + + void UpdateSession(const nsAString& aSessionId, PromiseId aPromiseId, + nsTArray& aResponse) override; + + void CloseSession(const nsAString& aSessionId, PromiseId aPromiseId) override; + + void RemoveSession(const nsAString& aSessionId, + PromiseId aPromiseId) override; + + void QueryOutputProtectionStatus() override {} + + void NotifyOutputProtectionStatus( + OutputProtectionCheckStatus aCheckStatus, + OutputProtectionCaptureStatus aCaptureStatus) override {} + + void Shutdown() override; + + void Terminated() override {} + + void OnSetSessionId(uint32_t aCreateSessionToken, + const nsAString& aSessionId) override {} + + void OnResolveLoadSessionPromise(uint32_t aPromiseId, + bool aSuccess) override {} + + void OnSessionMessage(const nsAString& aSessionId, + dom::MediaKeyMessageType aMessageType, + const nsTArray& aMessage) override; + + void OnExpirationChange(const nsAString& aSessionId, + UnixTime aExpiryTime) override; + + void OnSessionClosed(const nsAString& aSessionId) override {} + + void OnSessionError(const nsAString& aSessionId, nsresult aException, + uint32_t aSystemCode, const nsAString& aMsg) override {} + + void OnRejectPromise(uint32_t aPromiseId, ErrorResult&& aException, + const nsCString& aMsg) override {} + + RefPtr Decrypt(MediaRawData* aSample) override { + return nullptr; + } + void OnDecrypted(uint32_t aId, DecryptStatus aResult, + const nsTArray& aDecryptedData) override {} + + void ResolvePromise(PromiseId aId) override; + void RejectPromise(PromiseId aId, ErrorResult&& aException, + const nsCString& aReason) override; + + void OnKeyStatusesChange(const nsAString& aSessionId) override; + + void GetStatusForPolicy(PromiseId aPromiseId, + const dom::HDCPVersion& aMinHdcpVersion) override; + +#ifdef DEBUG + bool IsOnOwnerThread() override { + return NS_GetCurrentThread() == mOwnerThread; + } +#endif + + WMFCDMProxy* AsWMFCDMProxy() override { return this; } + + bool IsHardwareDecryptionSupported() const override; + + // Can only be called after initialization succeeded. + uint64_t GetCDMProxyId() const; + + private: + virtual ~WMFCDMProxy(); + + template + void ResolvePromiseWithResult(PromiseId aId, const T& aResult) {} + void RejectPromiseOnMainThread(PromiseId aId, + CopyableErrorResult&& aException, + const nsCString& aReason); + // Reject promise with an InvalidStateError and the given message. + void RejectPromiseWithStateError(PromiseId aId, const nsCString& aReason); + + // For HDCP GetStatusForPolicy usage. + void ResolvePromiseWithKeyStatus(const PromiseId& aId, + const dom::MediaKeyStatus& aStatus); + + CopyableTArray GenerateMFCDMMediaCapabilities( + const dom::Sequence& aCapabilities, + const Maybe& forcedRobustness = Nothing()); + + RefPtr mCDM; + + const dom::MediaKeySystemConfiguration mConfig; + + RefPtr mProxyCallback; + + // It can only be used on the main thread. + bool mIsShutdown = false; +}; + +} // namespace mozilla + +#endif // DOM_MEDIA_EME_MEDIAFOUNDATION_WMFCDMPROXY_H_ diff --git a/dom/media/eme/mediafoundation/WMFCDMProxyCallback.cpp b/dom/media/eme/mediafoundation/WMFCDMProxyCallback.cpp new file mode 100644 index 0000000000..6464cec4c7 --- /dev/null +++ b/dom/media/eme/mediafoundation/WMFCDMProxyCallback.cpp @@ -0,0 +1,72 @@ +/* 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 "WMFCDMProxyCallback.h" + +#include "mozilla/WMFCDMProxy.h" + +#define RETURN_IF_NULL(proxy) \ + if (!proxy) { \ + return; \ + } + +namespace mozilla { + +WMFCDMProxyCallback::WMFCDMProxyCallback(WMFCDMProxy* aProxy) : mProxy(aProxy) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mProxy); +} + +WMFCDMProxyCallback::~WMFCDMProxyCallback() { MOZ_ASSERT(!mProxy); } + +void WMFCDMProxyCallback::OnSessionMessage(const MFCDMKeyMessage& aMessage) { + NS_DispatchToMainThread(NS_NewRunnableFunction( + "WMFCDMProxyCallback::OnSessionMessage", + [self = RefPtr{this}, this, message = aMessage]() { + RETURN_IF_NULL(mProxy); + mProxy->OnSessionMessage(message.sessionId(), message.type(), + std::move(message.message())); + })); +} + +void WMFCDMProxyCallback::OnSessionKeyStatusesChange( + const MFCDMKeyStatusChange& aKeyStatuses) { + NS_DispatchToMainThread(NS_NewRunnableFunction( + "WMFCDMProxyCallback::OnSessionKeyStatusesChange", + [self = RefPtr{this}, this, keyStatuses = aKeyStatuses]() { + RETURN_IF_NULL(mProxy); + bool keyStatusesChange = false; + { + auto caps = mProxy->Capabilites().Lock(); + for (const auto& keyInfo : keyStatuses.keyInfo()) { + keyStatusesChange |= caps->SetKeyStatus( + keyInfo.keyId(), keyStatuses.sessionId(), + dom::Optional(keyInfo.status())); + } + } + if (keyStatusesChange) { + mProxy->OnKeyStatusesChange(keyStatuses.sessionId()); + } + })); +} + +void WMFCDMProxyCallback::OnSessionKeyExpiration( + const MFCDMKeyExpiration& aExpiration) { + NS_DispatchToMainThread(NS_NewRunnableFunction( + "WMFCDMProxyCallback::OnSessionKeyExpiration", + [self = RefPtr{this}, this, expiration = aExpiration]() { + RETURN_IF_NULL(mProxy); + mProxy->OnExpirationChange( + expiration.sessionId(), + expiration.expiredTimeMilliSecondsSinceEpoch()); + })); +} + +void WMFCDMProxyCallback::Shutdown() { + MOZ_ASSERT(NS_IsMainThread()); + mProxy = nullptr; +} + +#undef RETURN_IF_NULL +} // namespace mozilla diff --git a/dom/media/eme/mediafoundation/WMFCDMProxyCallback.h b/dom/media/eme/mediafoundation/WMFCDMProxyCallback.h new file mode 100644 index 0000000000..6e76f63ccc --- /dev/null +++ b/dom/media/eme/mediafoundation/WMFCDMProxyCallback.h @@ -0,0 +1,38 @@ +/* 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 DOM_MEDIA_EME_MEDIAFOUNDATION_WMFCDMPROXYCALLBACK_H_ +#define DOM_MEDIA_EME_MEDIAFOUNDATION_WMFCDMPROXYCALLBACK_H_ + +#include "mozilla/PMFCDM.h" +#include "nsThreadUtils.h" + +namespace mozilla { + +class WMFCDMProxy; + +// This class is used to notify CDM related events called from MFCDMChild, and +// it will forward the relative calls to WMFCDMProxy on the main thread. +class WMFCDMProxyCallback final { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WMFCDMProxyCallback); + + explicit WMFCDMProxyCallback(WMFCDMProxy* aProxy); + + void OnSessionMessage(const MFCDMKeyMessage& aMessage); + + void OnSessionKeyStatusesChange(const MFCDMKeyStatusChange& aKeyStatuses); + + void OnSessionKeyExpiration(const MFCDMKeyExpiration& aExpiration); + + void Shutdown(); + + private: + ~WMFCDMProxyCallback(); + RefPtr mProxy; +}; + +} // namespace mozilla + +#endif // DOM_MEDIA_EME_MEDIAFOUNDATION_WMFCDMPROXYCALLBACK_H_ diff --git a/dom/media/eme/mediafoundation/moz.build b/dom/media/eme/mediafoundation/moz.build new file mode 100644 index 0000000000..8c54b381ff --- /dev/null +++ b/dom/media/eme/mediafoundation/moz.build @@ -0,0 +1,21 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXPORTS.mozilla += [ + "WMFCDMImpl.h", + "WMFCDMProxy.h", + "WMFCDMProxyCallback.h", +] + +UNIFIED_SOURCES += [ + "WMFCDMImpl.cpp", + "WMFCDMProxy.cpp", + "WMFCDMProxyCallback.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/dom/media/eme/moz.build b/dom/media/eme/moz.build new file mode 100644 index 0000000000..9965e85ae4 --- /dev/null +++ b/dom/media/eme/moz.build @@ -0,0 +1,56 @@ +# -*- 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/. + +EXPORTS.mozilla.dom += [ + "KeySystemNames.h", + "MediaEncryptedEvent.h", + "MediaKeyError.h", + "MediaKeyMessageEvent.h", + "MediaKeys.h", + "MediaKeySession.h", + "MediaKeyStatusMap.h", + "MediaKeySystemAccess.h", + "MediaKeySystemAccessManager.h", + "MediaKeySystemAccessPermissionRequest.h", +] + +EXPORTS.mozilla += [ + "CDMCaps.h", + "CDMProxy.h", + "DecryptorProxyCallback.h", + "DetailedPromise.h", + "EMEUtils.h", + "KeySystemConfig.h", +] + +UNIFIED_SOURCES += [ + "CDMCaps.cpp", + "DetailedPromise.cpp", + "EMEUtils.cpp", + "KeySystemConfig.cpp", + "MediaEncryptedEvent.cpp", + "MediaKeyError.cpp", + "MediaKeyMessageEvent.cpp", + "MediaKeys.cpp", + "MediaKeySession.cpp", + "MediaKeyStatusMap.cpp", + "MediaKeySystemAccess.cpp", + "MediaKeySystemAccessManager.cpp", + "MediaKeySystemAccessPermissionRequest.cpp", +] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "android": + DIRS += ["mediadrm"] + LOCAL_INCLUDES += ["/dom/media/platforms/android"] + +if CONFIG["MOZ_WMF_CDM"]: + DIRS += ["mediafoundation"] + +DIRS += ["clearkey"] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" -- cgit v1.2.3