Skip to content

Commit

Permalink
Bug 1711912 - Implement output protection query in Clearkey CDM. r=alwu
Browse files Browse the repository at this point in the history
This implements logic in the clear key CDM to emulate protection queries. The
CDM will only do so if configured using the new test key system added prior to
this patch.

Differential Revision: https://phabricator.services.mozilla.com/D122633
  • Loading branch information
Bryce Seager van Dyk committed Aug 19, 2021
1 parent 78c9001 commit 63c7458
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 2 deletions.
24 changes: 22 additions & 2 deletions media/gmp-clearkey/0.1/ClearKeyCDM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ void ClearKeyCDM::TimerExpired(void* aContext) {

Status ClearKeyCDM::Decrypt(const InputBuffer_2& aEncryptedBuffer,
DecryptedBlock* aDecryptedBuffer) {
if (mIsProtectionQueryEnabled) {
// Piggyback this check onto Decrypt calls. If Clearkey implements a timer
// based approach for firing events, we could instead trigger the check
// using that mechanism.
mSessionManager->QueryOutputProtectionStatusIfNeeded();
}
return mSessionManager->Decrypt(aEncryptedBuffer, aDecryptedBuffer);
}

Expand Down Expand Up @@ -118,6 +124,12 @@ void ClearKeyCDM::ResetDecoder(StreamType aDecoderType) {
Status ClearKeyCDM::DecryptAndDecodeFrame(const InputBuffer_2& aEncryptedBuffer,
VideoFrame* aVideoFrame) {
#ifdef ENABLE_WMF
if (mIsProtectionQueryEnabled) {
// Piggyback this check onto Decrypt + Decode. If Clearkey implements a
// timer based approach for firing events, we could instead trigger the
// check using that mechanism.
mSessionManager->QueryOutputProtectionStatusIfNeeded();
}
return mVideoDecoder->Decode(aEncryptedBuffer, aVideoFrame);
#else
return Status::kDecodeError;
Expand All @@ -140,8 +152,16 @@ void ClearKeyCDM::OnPlatformChallengeResponse(

void ClearKeyCDM::OnQueryOutputProtectionStatus(
QueryResult aResult, uint32_t aLinkMask, uint32_t aOutputProtectionMask) {
// This function should never be called and is not supported.
assert(false);
// The higher level GMP machinery should not forward us query information
// unless we've requested it (even if mutiple CDMs exist at once and some
// others are reqeusting this info). If this assert fires we're violating
// that.
MOZ_ASSERT(mIsProtectionQueryEnabled,
"Should only receive a protection status "
"mIsProtectionQueryEnabled is true");
// The session manager handles the guts of this for ClearKey.
mSessionManager->OnQueryOutputProtectionStatus(aResult, aLinkMask,
aOutputProtectionMask);
}

void ClearKeyCDM::OnStorageId(uint32_t aVersion, const uint8_t* aStorageId,
Expand Down
124 changes: 124 additions & 0 deletions media/gmp-clearkey/0.1/ClearKeySessionManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ void ClearKeySessionManager::CreateSession(uint32_t aPromiseId,
}

mSessions[sessionId] = session;
mLastSessionId = sessionId;

const vector<KeyId>& sessionKeys = session->GetKeyIds();
vector<KeyId> neededKeys;
Expand Down Expand Up @@ -238,6 +239,7 @@ void ClearKeySessionManager::PersistentSessionDataLoaded(
new ClearKeySession(aSessionId, SessionType::kPersistentLicense);

mSessions[aSessionId] = session;
mLastSessionId = aSessionId;

uint32_t numKeys = aKeyDataSize / (2 * CENC_KEY_LEN);

Expand Down Expand Up @@ -571,6 +573,7 @@ void ClearKeySessionManager::DecryptingComplete() {
delete it->second;
}
mSessions.clear();
mLastSessionId = std::nullopt;

mDecryptionManager = nullptr;
mHost = nullptr;
Expand All @@ -587,3 +590,124 @@ bool ClearKeySessionManager::MaybeDeferTillInitialized(
mDeferredInitialize.emplace(move(aMaybeDefer));
return true;
}

void ClearKeySessionManager::OnQueryOutputProtectionStatus(
QueryResult aResult, uint32_t aLinkMask, uint32_t aOutputProtectionMask) {
MOZ_ASSERT(mHasOutstandingOutputProtectionQuery,
"Should only be called if a query is outstanding");
CK_LOGD("ClearKeySessionManager::OnQueryOutputProtectionStatus");
mHasOutstandingOutputProtectionQuery = false;

if (aResult == QueryResult::kQueryFailed) {
// Indicate the query failed. This can happen if we're in shutdown.
NotifyOutputProtectionStatus(KeyStatus::kInternalError);
return;
}

if (aLinkMask & OutputLinkTypes::kLinkTypeNetwork) {
NotifyOutputProtectionStatus(KeyStatus::kOutputRestricted);
return;
}

NotifyOutputProtectionStatus(KeyStatus::kUsable);
}

void ClearKeySessionManager::QueryOutputProtectionStatusIfNeeded() {
MOZ_ASSERT(
mHost,
"Should not query protection status if we're shutdown (mHost == null)!");
CK_LOGD(
"ClearKeySessionManager::UpdateOutputProtectionStatusAndQueryIfNeeded");
if (mLastOutputProtectionQueryTime.IsNull()) {
// We haven't perfomed a check yet, get a query going.
MOZ_ASSERT(
!mHasOutstandingOutputProtectionQuery,
"Shouldn't have an outstanding query if we haven't recorded a time");
QueryOutputProtectionStatusFromHost();
return;
}

MOZ_ASSERT(!mLastOutputProtectionQueryTime.IsNull(),
"Should have already handled the case where we don't yet have a "
"previous check time");
const mozilla::TimeStamp now = mozilla::TimeStamp::NowLoRes();
const mozilla::TimeDuration timeSinceQuery =
now - mLastOutputProtectionQueryTime;

// The time between output protection checks to the host. I.e. if this amount
// of time has passed since the last check with the host, another should be
// performed (provided the first check has been handled).
static const mozilla::TimeDuration kOutputProtectionQueryInterval =
mozilla::TimeDuration::FromSeconds(0.2);
// The number of kOutputProtectionQueryInterval intervals we can miss before
// we decide a check has failed. I.e. if this value is 2, if we have not
// received a reply to a check after kOutputProtectionQueryInterval * 2
// time, we consider the check failed.
constexpr uint32_t kMissedIntervalsBeforeFailure = 2;
// The length of time after which we will restrict output until we get a
// query response.
static const mozilla::TimeDuration kTimeToWaitBeforeFailure =
kOutputProtectionQueryInterval * kMissedIntervalsBeforeFailure;

if ((timeSinceQuery > kOutputProtectionQueryInterval) &&
!mHasOutstandingOutputProtectionQuery) {
// We don't have an outstanding query and enough time has passed we should
// query again.
QueryOutputProtectionStatusFromHost();
return;
}

if ((timeSinceQuery > kTimeToWaitBeforeFailure) &&
mHasOutstandingOutputProtectionQuery) {
// A reponse was not received fast enough, notify.
NotifyOutputProtectionStatus(KeyStatus::kInternalError);
}
}

void ClearKeySessionManager::QueryOutputProtectionStatusFromHost() {
MOZ_ASSERT(
mHost,
"Should not query protection status if we're shutdown (mHost == null)!");
CK_LOGD("ClearKeySessionManager::QueryOutputProtectionStatusFromHost");
if (mHost) {
mLastOutputProtectionQueryTime = mozilla::TimeStamp::NowLoRes();
mHost->QueryOutputProtectionStatus();
mHasOutstandingOutputProtectionQuery = true;
}
}

void ClearKeySessionManager::NotifyOutputProtectionStatus(KeyStatus aStatus) {
MOZ_ASSERT(aStatus == KeyStatus::kUsable ||
aStatus == KeyStatus::kOutputRestricted ||
aStatus == KeyStatus::kInternalError,
"aStatus should have an expected value");
CK_LOGD("ClearKeySessionManager::NotifyOutputProtectionStatus");
if (!mLastSessionId.has_value()) {
// If we don't have a session id, either because we're too early, or are
// shutting down, don't notify.
return;
}

string& lastSessionId = mLastSessionId.value();

// Use 'output-protection' as the key ID. This helps tests disambiguate key
// status updates related to this.
const uint8_t kKeyId[] = {'o', 'u', 't', 'p', 'u', 't', '-', 'p', 'r',
'o', 't', 'e', 'c', 't', 'i', 'o', 'n'};
KeyInformation keyInfo = {};
keyInfo.key_id = kKeyId;
keyInfo.key_id_size = std::size(kKeyId);
keyInfo.status = aStatus;

vector<KeyInformation> keyInfos;
keyInfos.push_back(keyInfo);

// At time of writing, Gecko's higher level handling doesn't use this arg.
// However, we set it to false to mimic Chromium's similar case. Since
// Clearkey is used to test the Chromium CDM path, it doesn't hurt to try
// and mimic their behaviour.
bool hasAdditionalUseableKey = false;
mHost->OnSessionKeysChange(lastSessionId.c_str(), lastSessionId.size(),
hasAdditionalUseableKey, keyInfos.data(),
keyInfos.size());
}
45 changes: 45 additions & 0 deletions media/gmp-clearkey/0.1/ClearKeySessionManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

#include <functional>
#include <map>
#include <optional>
#include <queue>
#include <set>
#include <string>
Expand All @@ -33,6 +34,7 @@
#include "ClearKeyUtils.h"
#include "RefCounted.h"
#include "content_decryption_module.h"
#include "mozilla/TimeStamp.h"

class ClearKeySessionManager final : public RefCounted {
public:
Expand Down Expand Up @@ -70,6 +72,23 @@ class ClearKeySessionManager final : public RefCounted {
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();

Expand All @@ -78,6 +97,21 @@ class ClearKeySessionManager final : public RefCounted {
void Serialize(const ClearKeySession* aSession,
std::vector<uint8_t>& aOutKeyData);

// Signals the host to perform an output protection check.
void QueryOutputProtectionStatusFromHost();

// Called to notify the result of an output protection status call. The
// following arguments are expected, along with their intended use:
// - KeyStatus::kUsable indicates that the query was responded to and the
// response showed output is protected.
// - KeyStatus::kOutputRestricted indicates that the query was responded to
// and the response showed output is not protected.
// - KeyStatus::kInternalError indicates a query was not repsonded to on
// time, or that a query was responded to with a failed cdm::QueryResult.
// The status passed to this function will be used to update the status of
// the keyId "output-protection", which tests an observe.
void NotifyOutputProtectionStatus(cdm::KeyStatus aStatus);

RefPtr<ClearKeyDecryptionManager> mDecryptionManager;
RefPtr<ClearKeyPersistence> mPersistence;

Expand All @@ -86,7 +120,18 @@ class ClearKeySessionManager final : public RefCounted {
std::set<KeyId> mKeyIds;
std::map<std::string, ClearKeySession*> mSessions;

// The session id of the last session created or loaded from persistent
// storage. Used to fire test messages at that session.
std::optional<std::string> mLastSessionId;

std::queue<std::function<void()>> mDeferredInitialize;

// If there is an inflight query to the host to check the output protection
// status. Multiple in flight queries should not be allowed, avoid firing
// more if this is true.
bool mHasOutstandingOutputProtectionQuery = false;
// The last time the manager called QueryOutputProtectionStatus on the host.
mozilla::TimeStamp mLastOutputProtectionQueryTime;
};

#endif // __ClearKeyDecryptor_h__

0 comments on commit 63c7458

Please sign in to comment.