#include "config.h"
#include "CDMClearKey.h"
#if ENABLE(ENCRYPTED_MEDIA)
#include "CDMKeySystemConfiguration.h"
#include "CDMRestrictions.h"
#include "CDMSessionType.h"
#include "CDMUtilities.h"
#include "InitDataRegistry.h"
#include "Logging.h"
#include "SharedBuffer.h"
#include <algorithm>
#include <iterator>
#include <wtf/MainThread.h>
#include <wtf/NeverDestroyed.h>
#include <wtf/text/Base64.h>
namespace WebCore {
static Optional<Vector<RefPtr<KeyHandle>>> parseLicenseFormat(const JSON::Object& root)
{
auto it = root.find("keys");
if (it == root.end())
return WTF::nullopt;
RefPtr<JSON::Array> keysArray;
if (!it->value->asArray(keysArray))
return WTF::nullopt;
Vector<RefPtr<KeyHandle>> decodedKeys;
bool validFormat = std::all_of(keysArray->begin(), keysArray->end(),
[&decodedKeys] (const auto& value) {
RefPtr<JSON::Object> keyObject;
if (!value->asObject(keyObject))
return false;
String keyType;
if (!keyObject->getString("kty", keyType) || !equalLettersIgnoringASCIICase(keyType, "oct"))
return false;
String keyID, keyValue;
if (!keyObject->getString("kid", keyID) || !keyObject->getString("k", keyValue))
return false;
KeyIDType keyIDData;
Vector<uint8_t> keyHandleValueData;
if (!WTF::base64URLDecode(keyID, { keyIDData }) || !WTF::base64URLDecode(keyValue, { keyHandleValueData }))
return false;
decodedKeys.append(KeyHandle::create(CDMInstanceSession::KeyStatus::Usable, WTFMove(keyIDData), WTFMove(keyHandleValueData)));
return true;
});
if (!validFormat)
return WTF::nullopt;
return decodedKeys;
}
static bool parseLicenseReleaseAcknowledgementFormat(const JSON::Object& root)
{
auto it = root.find("kids");
if (it == root.end())
return false;
RefPtr<JSON::Array> kidsArray;
if (!it->value->asArray(kidsArray))
return false;
return true;
}
static std::pair<unsigned, unsigned> extractKeyidsLocationFromCencInitData(const SharedBuffer& initData)
{
std::pair<unsigned, unsigned> keyIdsMap(0, 0);
if (initData.isEmpty() || initData.size() > std::numeric_limits<unsigned>::max())
return keyIdsMap;
const char* data = initData.data();
unsigned initDataSize = initData.size();
unsigned index = 0;
unsigned psshSize = 0;
bool foundPssh = false;
while (true) {
if (index + 12 + ClearKey::cencSystemIdSize >= initDataSize)
return keyIdsMap;
psshSize = data[index + 2] * 256 + data[index + 3];
if (!psshSize)
return keyIdsMap;
if (!memcmp(&data[index + 12], ClearKey::cencSystemId, ClearKey::cencSystemIdSize)) {
foundPssh = true;
break;
}
index += psshSize;
}
if (!foundPssh)
return keyIdsMap;
index += (12 + ClearKey::cencSystemIdSize);
if (index + 3 >= initDataSize)
return keyIdsMap;
keyIdsMap.first = data[index + 3]; index += 4;
if ((index + (keyIdsMap.first * ClearKey::KeyIDSizeInBytes)) >= initDataSize)
return keyIdsMap;
keyIdsMap.second = index;
return keyIdsMap;
}
static bool isCencInitData(const SharedBuffer& initData)
{
std::pair<unsigned, unsigned> keyIdsMap = extractKeyidsLocationFromCencInitData(initData);
return ((keyIdsMap.first) && (keyIdsMap.second));
}
static Ref<SharedBuffer> extractKeyidsFromCencInitData(const SharedBuffer& initData)
{
Ref<SharedBuffer> keyIds = SharedBuffer::create();
std::pair<unsigned, unsigned> keyIdsMap = extractKeyidsLocationFromCencInitData(initData);
unsigned keyIdCount = keyIdsMap.first;
unsigned index = keyIdsMap.second;
if (!keyIdCount || !index)
return keyIds;
const char* data = initData.data();
auto object = JSON::Object::create();
auto keyIdsArray = JSON::Array::create();
for (unsigned i = 0; i < keyIdCount; i++) {
String keyId = WTF::base64URLEncode(&data[index], ClearKey::KeyIDSizeInBytes);
keyIdsArray->pushString(keyId);
index += ClearKey::KeyIDSizeInBytes;
}
object->setArray("kids", WTFMove(keyIdsArray));
CString jsonData = object->toJSONString().utf8();
keyIds->append(jsonData.data(), jsonData.length());
return keyIds;
}
static Ref<SharedBuffer> extractKeyIdFromWebMInitData(const SharedBuffer& initData)
{
Ref<SharedBuffer> keyIds = SharedBuffer::create();
if (initData.isEmpty() || initData.size() > std::numeric_limits<unsigned>::max())
return keyIds;
auto object = JSON::Object::create();
auto keyIdsArray = JSON::Array::create();
String keyId = WTF::base64URLEncode(initData.data(), initData.size());
keyIdsArray->pushString(keyId);
object->setArray("kids", WTFMove(keyIdsArray));
CString jsonData = object->toJSONString().utf8();
keyIds->append(jsonData.data(), jsonData.length());
return keyIds;
}
CDMFactoryClearKey& CDMFactoryClearKey::singleton()
{
static NeverDestroyed<CDMFactoryClearKey> s_factory;
return s_factory;
}
CDMFactoryClearKey::CDMFactoryClearKey() = default;
CDMFactoryClearKey::~CDMFactoryClearKey() = default;
std::unique_ptr<CDMPrivate> CDMFactoryClearKey::createCDM(const String& keySystem)
{
#ifdef NDEBUG
UNUSED_PARAM(keySystem);
#else
ASSERT(supportsKeySystem(keySystem));
#endif
return makeUnique<CDMPrivateClearKey>();
}
bool CDMFactoryClearKey::supportsKeySystem(const String& keySystem)
{
return equalLettersIgnoringASCIICase(keySystem, "org.w3.clearkey");
}
CDMPrivateClearKey::CDMPrivateClearKey() = default;
CDMPrivateClearKey::~CDMPrivateClearKey() = default;
Vector<AtomString> CDMPrivateClearKey::supportedInitDataTypes() const
{
return {
InitDataRegistry::keyidsName(),
InitDataRegistry::cencName(),
InitDataRegistry::webmName(),
};
}
static bool containsPersistentLicenseType(const Vector<CDMSessionType>& types)
{
return std::any_of(types.begin(), types.end(),
[] (auto& sessionType) { return sessionType == CDMSessionType::PersistentLicense; });
}
bool CDMPrivateClearKey::supportsConfiguration(const CDMKeySystemConfiguration& configuration) const
{
if (configuration.distinctiveIdentifier == CDMRequirement::Required)
return false;
if (configuration.persistentState == CDMRequirement::Required && !containsPersistentLicenseType(configuration.sessionTypes))
return false;
return true;
}
bool CDMPrivateClearKey::supportsConfigurationWithRestrictions(const CDMKeySystemConfiguration& configuration, const CDMRestrictions& restrictions) const
{
if ((configuration.distinctiveIdentifier == CDMRequirement::Optional && restrictions.distinctiveIdentifierDenied)
|| configuration.distinctiveIdentifier == CDMRequirement::Required)
return false;
if (configuration.persistentState == CDMRequirement::Optional && restrictions.persistentStateDenied)
return false;
if (configuration.persistentState == CDMRequirement::Required && !containsPersistentLicenseType(configuration.sessionTypes))
return false;
return true;
}
bool CDMPrivateClearKey::supportsSessionTypeWithConfiguration(const CDMSessionType& sessionType, const CDMKeySystemConfiguration& configuration) const
{
if (sessionType != CDMSessionType::Temporary && sessionType != CDMSessionType::PersistentLicense)
return false;
return supportsConfiguration(configuration);
}
Vector<AtomString> CDMPrivateClearKey::supportedRobustnesses() const
{
return { emptyAtom() };
}
CDMRequirement CDMPrivateClearKey::distinctiveIdentifiersRequirement(const CDMKeySystemConfiguration&, const CDMRestrictions& restrictions) const
{
if (restrictions.distinctiveIdentifierDenied)
return CDMRequirement::NotAllowed;
return CDMRequirement::Optional;
}
CDMRequirement CDMPrivateClearKey::persistentStateRequirement(const CDMKeySystemConfiguration&, const CDMRestrictions& restrictions) const
{
if (restrictions.persistentStateDenied)
return CDMRequirement::NotAllowed;
return CDMRequirement::Optional;
}
bool CDMPrivateClearKey::distinctiveIdentifiersAreUniquePerOriginAndClearable(const CDMKeySystemConfiguration&) const
{
return false;
}
RefPtr<CDMInstance> CDMPrivateClearKey::createInstance()
{
return adoptRef(new CDMInstanceClearKey);
}
void CDMPrivateClearKey::loadAndInitialize()
{
}
bool CDMPrivateClearKey::supportsServerCertificates() const
{
return false;
}
bool CDMPrivateClearKey::supportsSessions() const
{
return true;
}
bool CDMPrivateClearKey::supportsInitData(const AtomString& initDataType, const SharedBuffer& initData) const
{
if (equalLettersIgnoringASCIICase(initDataType, "keyids") && CDMUtilities::parseJSONObject(initData))
return true;
if (equalLettersIgnoringASCIICase(initDataType, "cenc") && isCencInitData(initData))
return true;
if (equalLettersIgnoringASCIICase(initDataType, "webm") && !initData.isEmpty())
return true;
return false;
}
RefPtr<SharedBuffer> CDMPrivateClearKey::sanitizeResponse(const SharedBuffer& response) const
{
if (!CDMUtilities::parseJSONObject(response))
return nullptr;
return response.copy();
}
Optional<String> CDMPrivateClearKey::sanitizeSessionId(const String& sessionId) const
{
bool ok;
sessionId.toUIntStrict(&ok);
if (!ok)
return WTF::nullopt;
return sessionId;
}
CDMInstanceClearKey::CDMInstanceClearKey() : CDMInstanceProxy("org.w3.clearkey"_s) { };
CDMInstanceClearKey::~CDMInstanceClearKey() = default;
void CDMInstanceClearKey::initializeWithConfiguration(const CDMKeySystemConfiguration&, AllowDistinctiveIdentifiers distinctiveIdentifiers, AllowPersistentState persistentState, SuccessCallback&& callback)
{
SuccessValue succeeded = (distinctiveIdentifiers == AllowDistinctiveIdentifiers::No && persistentState == AllowPersistentState::No) ? Succeeded : Failed;
callback(succeeded);
}
void CDMInstanceClearKey::setServerCertificate(Ref<SharedBuffer>&&, SuccessCallback&& callback)
{
callback(Failed);
}
void CDMInstanceClearKey::setStorageDirectory(const String&)
{
}
const String& CDMInstanceClearKey::keySystem() const
{
static const NeverDestroyed<String> s_keySystem { MAKE_STATIC_STRING_IMPL("org.w3.clearkey") };
return s_keySystem;
}
RefPtr<CDMInstanceSession> CDMInstanceClearKey::createSession()
{
auto newSession = adoptRef(new CDMInstanceSessionClearKey(*this));
trackSession(*newSession);
return newSession;
}
void CDMInstanceSessionClearKey::requestLicense(LicenseType, const AtomString& initDataType, Ref<SharedBuffer>&& initData, LicenseCallback&& callback)
{
static uint32_t s_sessionIdValue = 0;
++s_sessionIdValue;
m_sessionID = String::number(s_sessionIdValue);
if (equalLettersIgnoringASCIICase(initDataType, "cenc"))
initData = extractKeyidsFromCencInitData(initData.get());
if (equalLettersIgnoringASCIICase(initDataType, "webm"))
initData = extractKeyIdFromWebMInitData(initData.get());
callOnMainThread(
[weakThis = makeWeakPtr(*this), this, callback = WTFMove(callback), initData = WTFMove(initData)]() mutable {
if (!weakThis)
return;
callback(WTFMove(initData), m_sessionID, false, Succeeded);
});
}
void CDMInstanceSessionClearKey::updateLicense(const String& sessionId, LicenseType, Ref<SharedBuffer>&& response, LicenseUpdateCallback&& callback)
{
#if LOG_DISABLED
UNUSED_PARAM(sessionId);
#endif
auto dispatchCallback =
[this, &callback](bool sessionWasClosed, Optional<KeyStatusVector>&& changedKeys, SuccessValue succeeded) {
callOnMainThread(
[weakThis = makeWeakPtr(*this), callback = WTFMove(callback), sessionWasClosed, changedKeys = WTFMove(changedKeys), succeeded] () mutable {
if (!weakThis)
return;
callback(sessionWasClosed, WTFMove(changedKeys), WTF::nullopt, WTF::nullopt, succeeded);
});
};
RefPtr<JSON::Object> root = CDMUtilities::parseJSONObject(response);
if (!root) {
LOG(EME, "EME - ClearKey - session %s update payload was not valid JSON", sessionId.utf8().data());
dispatchCallback(false, WTF::nullopt, SuccessValue::Failed);
return;
}
LOG(EME, "EME - ClearKey - updating license for session %s which currently contains %u keys", sessionId.utf8().data(), m_keyStore.numKeys());
if (auto decodedKeys = parseLicenseFormat(*root)) {
bool keysChanged = m_keyStore.addKeys(WTFMove(*decodedKeys));
LOG(EME, "EME - ClearKey - session %s has %u keys after update()", sessionId.utf8().data(), m_keyStore.numKeys());
Optional<KeyStatusVector> changedKeys;
if (keysChanged) {
LOG(EME, "EME - ClearKey - session %s has changed keys", sessionId.utf8().data());
parentInstance().mergeKeysFrom(m_keyStore);
changedKeys = m_keyStore.convertToJSKeyStatusVector();
}
dispatchCallback(false, WTFMove(changedKeys), SuccessValue::Succeeded);
return;
}
if (parseLicenseReleaseAcknowledgementFormat(*root)) {
LOG(EME, "EME - ClearKey - session %s release acknowledged, clearing all known keys", sessionId.utf8().data());
parentInstance().removeAllKeysFrom(m_keyStore);
m_keyStore.removeAllKeys();
dispatchCallback(true, WTF::nullopt, SuccessValue::Succeeded);
return;
}
LOG(EME, "EME - ClearKey - session %s update payload was an unrecognized format", sessionId.utf8().data());
dispatchCallback(false, WTF::nullopt, SuccessValue::Failed);
}
void CDMInstanceSessionClearKey::loadSession(LicenseType, const String& sessionId, const String&, LoadSessionCallback&& callback)
{
#ifdef NDEBUG
UNUSED_PARAM(sessionId);
#endif
ASSERT(sessionId == m_sessionID);
KeyStatusVector keyStatusVector = m_keyStore.convertToJSKeyStatusVector();
callOnMainThread([weakThis = makeWeakPtr(*this), callback = WTFMove(callback), &keyStatusVector]() mutable {
if (!weakThis)
return;
callback(WTFMove(keyStatusVector), WTF::nullopt, WTF::nullopt, Succeeded, SessionLoadFailure::None);
});
}
void CDMInstanceSessionClearKey::closeSession(const String&, CloseSessionCallback&& callback)
{
callOnMainThread(
[weakThis = makeWeakPtr(*this), callback = WTFMove(callback)] () mutable {
if (!weakThis)
return;
callback();
});
}
void CDMInstanceSessionClearKey::removeSessionData(const String& sessionId, LicenseType, RemoveSessionDataCallback&& callback)
{
#ifdef NDEBUG
UNUSED_PARAM(sessionId);
#endif
ASSERT(sessionId == m_sessionID);
auto dispatchCallback =
[this, &callback](KeyStatusVector&& keyStatusVector, Optional<Ref<SharedBuffer>>&& message, SuccessValue success) {
callOnMainThread(
[weakThis = makeWeakPtr(*this), callback = WTFMove(callback), keyStatusVector = WTFMove(keyStatusVector), message = WTFMove(message), success]() mutable {
if (!weakThis)
return;
callback(WTFMove(keyStatusVector), WTFMove(message), success);
});
};
KeyStatusVector keyStatusVector = m_keyStore.allKeysAs(CDMInstanceSession::KeyStatus::Released);
RefPtr<SharedBuffer> message;
{
auto rootObject = JSON::Object::create();
{
auto array = JSON::Array::create();
for (const auto& key : m_keyStore) {
ASSERT(key->id().size() <= std::numeric_limits<unsigned>::max());
array->pushString(WTF::base64URLEncode(key->id().data(), key->id().size()));
}
rootObject->setArray("kids", WTFMove(array));
}
String messageString = rootObject->toJSONString();
CString messageCString = messageString.utf8();
message = SharedBuffer::create(messageCString.data(), messageCString.length());
}
m_keyStore.removeAllKeys();
dispatchCallback(WTFMove(keyStatusVector), Ref<SharedBuffer>(*message), SuccessValue::Succeeded);
}
void CDMInstanceSessionClearKey::storeRecordOfKeyUsage(const String&)
{
}
CDMInstanceClearKey& CDMInstanceSessionClearKey::parentInstance() const
{
auto instance = cdmInstanceProxy();
ASSERT(instance);
return static_cast<CDMInstanceClearKey&>(*instance);
}
}
#endif // ENABLE(ENCRYPTED_MEDIA)