Commit 05c95bb1 authored by Alex St-Onge's avatar Alex St-Onge Committed by Commit Bot

Add browser test for EME persistent-usage-record session

This change adds a test to exercice the persistent-usage-record path
for EME. The new test create a PUR session and wait for the video to
end and verify that the license-release message contain the usage
record.

Bug: 1091502
Change-Id: I2d6347f39981d9eb1025d37ced254eb6aae766b1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2285250Reviewed-by: default avatarJohn Rummell <jrummell@chromium.org>
Reviewed-by: default avatarXiaohan Wang <xhwang@chromium.org>
Commit-Queue: Alex St-Onge <alstonge@chromium.org>
Cr-Commit-Position: refs/heads/master@{#788870}
parent 3e6d2626
......@@ -79,6 +79,7 @@ const char kExternalClearKeyStorageIdTestKeySystem[] =
const char kNoSessionToLoad[] = "";
#if BUILDFLAG(ENABLE_LIBRARY_CDMS)
const char kPersistentLicense[] = "PersistentLicense";
const char kPersistentUsageRecord[] = "PersistentUsageRecord";
const char kUnknownSession[] = "UnknownSession";
#endif
......@@ -354,6 +355,9 @@ class ECKEncryptedMediaTest : public EncryptedMediaTestBase,
command_line->AppendSwitchASCII(
switches::kOverrideEnabledCdmInterfaceVersion,
base::NumberToString(GetCdmInterfaceVersion()));
command_line->AppendSwitchASCII(
switches::kEnableBlinkFeatures,
"EncryptedMediaPersistentUsageRecordSession");
}
};
......@@ -876,6 +880,11 @@ IN_PROC_BROWSER_TEST_P(ECKEncryptedMediaTest, LoadSessionAfterClose) {
media::kEnded);
}
IN_PROC_BROWSER_TEST_P(ECKEncryptedMediaTest, VerifyPersistentUsageRecord) {
TestPlaybackCase(kExternalClearKeyKeySystem, kPersistentUsageRecord,
media::kEnded);
}
const char kExternalClearKeyDecryptOnlyKeySystem[] =
"org.chromium.externalclearkey.decryptonly";
......
......@@ -407,15 +407,17 @@ void AesDecryptor::RemoveSession(const std::string& session_id,
// "persistent-license"
// Let message be a message containing or reflecting the record
// of license destruction.
// "persistent-usage-record"
// Not supported by AesDecryptor.
std::vector<uint8_t> message;
if (it->second != CdmSessionType::kTemporary) {
if (it->second == CdmSessionType::kPersistentLicense) {
// The license release message is specified in the spec:
// https://w3c.github.io/encrypted-media/#clear-key-release-format.
KeyIdList key_ids;
key_ids.reserve(keys_info.size());
for (const auto& key_info : keys_info)
key_ids.push_back(key_info->key_id);
CreateKeyIdsInitData(key_ids, &message);
message = CreateLicenseReleaseMessage(key_ids);
}
// 4.5. Queue a task to run the following steps:
......@@ -487,6 +489,12 @@ void AesDecryptor::Decrypt(StreamType stream_type,
return;
}
auto now = base::Time::Now();
if (first_decryption_time_.is_null())
first_decryption_time_ = now;
latest_decryption_time_ = now;
DCHECK_EQ(decrypted->timestamp(), encrypted->timestamp());
DCHECK_EQ(decrypted->duration(), encrypted->duration());
std::move(decrypt_cb).Run(kSuccess, std::move(decrypted));
......@@ -642,6 +650,27 @@ CdmKeysInfo AesDecryptor::GenerateKeysInfoList(
return keys_info;
}
void AesDecryptor::GetRecordOfKeyUsage(const std::string& session_id,
KeyIdList& key_ids,
base::Time& first_decryption_time,
base::Time& latest_decryption_time) {
auto it = open_sessions_.find(session_id);
if (it == open_sessions_.end() ||
it->second != CdmSessionType::kPersistentUsageRecord) {
return;
}
base::AutoLock auto_lock(key_map_lock_);
for (const auto& item : key_map_) {
if (item.second->Contains(session_id)) {
key_ids.emplace_back(item.first.begin(), item.first.end());
}
}
first_decryption_time = first_decryption_time_;
latest_decryption_time = latest_decryption_time_;
}
AesDecryptor::DecryptionKey::DecryptionKey(const std::string& secret)
: secret_(secret) {}
......
......@@ -24,6 +24,7 @@
#include "media/base/content_decryption_module.h"
#include "media/base/decryptor.h"
#include "media/base/media_export.h"
#include "media/cdm/json_web_key.h"
namespace crypto {
class SymmetricKey;
......@@ -176,6 +177,13 @@ class MEDIA_EXPORT AesDecryptor : public ContentDecryptionModule,
CdmKeysInfo GenerateKeysInfoList(const std::string& session_id,
CdmKeyInformation::KeyStatus status);
// Returns the record of key usage for persistent-usage-record session. Used
// by ClearKeyPersistentSessionCdm.
void GetRecordOfKeyUsage(const std::string& session_id,
KeyIdList& key_ids,
base::Time& first_decryption_time,
base::Time& latest_decryption_time);
// Callbacks for firing session events.
SessionMessageCB session_message_cb_;
SessionClosedCB session_closed_cb_;
......@@ -196,6 +204,10 @@ class MEDIA_EXPORT AesDecryptor : public ContentDecryptionModule,
CallbackRegistry<EventCB::RunType> event_callbacks_;
// First and latest decryption time for persistent-usage-record
base::Time first_decryption_time_ GUARDED_BY(key_map_lock_);
base::Time latest_decryption_time_ GUARDED_BY(key_map_lock_);
DISALLOW_COPY_AND_ASSIGN(AesDecryptor);
};
......
......@@ -338,10 +338,8 @@ void CreateLicenseRequest(const KeyIdList& key_ids,
license->swap(result);
}
void CreateKeyIdsInitData(const KeyIdList& key_ids,
std::vector<uint8_t>* init_data) {
// Create the init_data.
auto dictionary = std::make_unique<base::DictionaryValue>();
void AddKeyIdsToDictionary(const KeyIdList& key_ids,
base::DictionaryValue* dictionary) {
auto list = std::make_unique<base::ListValue>();
for (const auto& key_id : key_ids) {
std::string key_id_string;
......@@ -353,15 +351,64 @@ void CreateKeyIdsInitData(const KeyIdList& key_ids,
list->AppendString(key_id_string);
}
dictionary->Set(kKeyIdsTag, std::move(list));
}
std::vector<uint8_t> SerializeDictionaryToVector(
const base::DictionaryValue* dictionary) {
// Serialize the dictionary as a string.
std::string json;
JSONStringValueSerializer serializer(&json);
serializer.Serialize(*dictionary);
// Convert the serialized data into std::vector and return it.
std::vector<uint8_t> result(json.begin(), json.end());
init_data->swap(result);
return std::vector<uint8_t>(json.begin(), json.end());
}
void CreateKeyIdsInitData(const KeyIdList& key_ids,
std::vector<uint8_t>* init_data) {
// Create the init_data.
auto dictionary = std::make_unique<base::DictionaryValue>();
AddKeyIdsToDictionary(key_ids, dictionary.get());
auto data = SerializeDictionaryToVector(dictionary.get());
init_data->swap(data);
}
// The format is a JSON object. For sessions of type "persistent-license" and
// "persistent-usage-record", the object shall contain the following member:
//
// "kids"
// An array of key IDs. Each element of the array is the base64url encoding
// of the octet sequence containing the key ID value.
//
// For sessions of type "persistent-usage-record" the object shall also contain
// the following members:
//
// "firstTime"
// The first decryption time expressed as a number giving the time, in
// milliseconds since 01 January, 1970 UTC.
// "latestTime"
// The latest decryption time expressed as a number giving the time, in
// milliseconds since 01 January,
// 1970 UTC. https://w3c.github.io/encrypted-media/#clear-key-release-format
std::vector<uint8_t> CreateLicenseReleaseMessage(
const KeyIdList& key_ids,
const base::Time first_decrypt_time,
const base::Time latest_decrypt_time) {
// Create the init_data.
auto dictionary = std::make_unique<base::DictionaryValue>();
AddKeyIdsToDictionary(key_ids, dictionary.get());
if (!first_decrypt_time.is_null() && !latest_decrypt_time.is_null()) {
// Persistent-Usage-Record
// Time need to be millisecond since 01 January, 1970 UTC
dictionary->SetDouble("firstTime",
first_decrypt_time.ToJsTimeIgnoringNull());
dictionary->SetDouble("latestTime",
latest_decrypt_time.ToJsTimeIgnoringNull());
}
return SerializeDictionaryToVector(dictionary.get());
}
bool ExtractFirstKeyIdFromLicenseRequest(const std::vector<uint8_t>& license,
......
......@@ -11,6 +11,7 @@
#include <utility>
#include <vector>
#include "base/time/time.h"
#include "media/base/media_export.h"
namespace media {
......@@ -94,6 +95,11 @@ MEDIA_EXPORT void CreateLicenseRequest(const KeyIdList& key_ids,
MEDIA_EXPORT void CreateKeyIdsInitData(const KeyIdList& key_ids,
std::vector<uint8_t>* key_ids_init_data);
MEDIA_EXPORT std::vector<uint8_t> CreateLicenseReleaseMessage(
const KeyIdList& key_ids,
const base::Time first_decrypt_time = base::Time(),
const base::Time latest_decrypt_time = base::Time());
// Extract the first key from the license request message. Returns true if
// |license| is a valid license request and contains at least one key,
// otherwise false and |first_key| is not touched.
......
......@@ -12,6 +12,7 @@
#include "base/macros.h"
#include "base/memory/ref_counted.h"
#include "media/base/cdm_promise.h"
#include "media/cdm/json_web_key.h"
namespace media {
......@@ -94,9 +95,12 @@ ClearKeyPersistentSessionCdm::ClearKeyPersistentSessionCdm(
const SessionClosedCB& session_closed_cb,
const SessionKeysChangeCB& session_keys_change_cb,
const SessionExpirationUpdateCB& session_expiration_update_cb)
: cdm_host_proxy_(cdm_host_proxy), session_closed_cb_(session_closed_cb) {
: cdm_host_proxy_(cdm_host_proxy),
session_message_cb_(session_message_cb),
session_closed_cb_(session_closed_cb) {
cdm_ = base::MakeRefCounted<AesDecryptor>(
session_message_cb,
base::Bind(&ClearKeyPersistentSessionCdm::OnSessionMessage,
weak_factory_.GetWeakPtr()),
base::Bind(&ClearKeyPersistentSessionCdm::OnSessionClosed,
weak_factory_.GetWeakPtr()),
session_keys_change_cb, session_expiration_update_cb);
......@@ -116,7 +120,10 @@ void ClearKeyPersistentSessionCdm::CreateSessionAndGenerateRequest(
const std::vector<uint8_t>& init_data,
std::unique_ptr<NewSessionCdmPromise> promise) {
std::unique_ptr<NewSessionCdmPromise> new_promise;
if (session_type != CdmSessionType::kPersistentLicense) {
// TODO(crbug.com/1102976) Add browser test for loading EME
// persistent-usage-record session
if (session_type == CdmSessionType::kTemporary ||
session_type == CdmSessionType::kPersistentUsageRecord) {
new_promise = std::move(promise);
} else {
// Since it's a persistent session, we need to save the session ID after
......@@ -294,7 +301,32 @@ void ClearKeyPersistentSessionCdm::RemoveSession(
auto it = persistent_sessions_.find(session_id);
if (it == persistent_sessions_.end()) {
// Not a persistent session, so simply pass the request on.
// TODO(crbug.com/1102976) Add test for loading PUR session
// Query the record of key usage before calling remove as RemoveSession will
// delete the keys. Steps from
// https://w3c.github.io/encrypted-media/#remove. 4.4.1.2 Follow the steps
// for the value of this object's session type
// "persistent-usage-record"
// Let message be a message containing or reflecting this
// object's record of key usage.
KeyIdList key_ids;
base::Time first_decryption_time;
base::Time latest_decryption_time;
cdm_->GetRecordOfKeyUsage(session_id, key_ids, first_decryption_time,
latest_decryption_time);
cdm_->RemoveSession(session_id, std::move(promise));
// Both times will be null if the session type is not PUR.
if (!first_decryption_time.is_null() && !latest_decryption_time.is_null()) {
std::vector<uint8_t> message = CreateLicenseReleaseMessage(
key_ids, first_decryption_time, latest_decryption_time);
// EME spec specifies that the message event should be fired before the
// promise is resolve but since this is only for testing we can leave this
// here.
session_message_cb_.Run(session_id, CdmMessageType::LICENSE_RELEASE,
message);
}
return;
}
......@@ -336,7 +368,6 @@ void ClearKeyPersistentSessionCdm::OnFileWrittenForRemoveSession(
std::unique_ptr<SimpleCdmPromise> promise,
bool success) {
DCHECK(success);
cdm_->RemoveSession(session_id, std::move(promise));
}
CdmContext* ClearKeyPersistentSessionCdm::GetCdmContext() {
......@@ -354,4 +385,11 @@ void ClearKeyPersistentSessionCdm::OnSessionClosed(
session_closed_cb_.Run(session_id);
}
void ClearKeyPersistentSessionCdm::OnSessionMessage(
const std::string& session_id,
CdmMessageType message_type,
const std::vector<uint8_t>& message) {
session_message_cb_.Run(session_id, message_type, message);
}
} // namespace media
......@@ -101,10 +101,15 @@ class ClearKeyPersistentSessionCdm : public ContentDecryptionModule {
// sessions if it was a persistent session.
void OnSessionClosed(const std::string& session_id);
void OnSessionMessage(const std::string& session_id,
CdmMessageType message_type,
const std::vector<uint8_t>& message);
scoped_refptr<AesDecryptor> cdm_;
CdmHostProxy* const cdm_host_proxy_ = nullptr;
// Callbacks for firing session events. Other events aren't intercepted.
SessionMessageCB session_message_cb_;
SessionClosedCB session_closed_cb_;
// Keep track of current open persistent sessions.
......
......@@ -103,7 +103,11 @@
Utils.timeLog('waiting for video to end.');
video.removeEventListener('ended', Utils.failTest);
Utils.installTitleEventHandler(video, 'ended');
if (testConfig.sessionToLoad == "PersistentUsageRecord") {
video.addEventListener('ended', onEnded);
} else {
Utils.installTitleEventHandler(video, 'ended');
}
video.removeEventListener('timeupdate', onTimeUpdate);
}
......@@ -129,6 +133,16 @@
Utils.timeLog('Event: ' + e.type + ', hidden: ' + document.hidden);
}
function onEnded(e) {
Utils.timeLog('Event: ' + e.type);
PlayerUtils.removeSession(player).then(function() {
Utils.setResultInTitle('ENDED');
}).catch(function(error) {
Utils.timeLog(error);
Utils.failTest('Failed PlayerUtils.removeSession');
});
}
function play(video, playTwice) {
Utils.timeLog('Starting play, hidden: ' + document.hidden);
video.addEventListener('canplay', onLogEvent);
......
......@@ -54,6 +54,7 @@ PlayerUtils.registerEMEEventListeners = function(player) {
player.video.receivedKeyMessage = true;
if (message.messageType == 'license-request' ||
message.messageType == 'license-renewal' ||
message.messageType == 'license-release' ||
message.messageType == 'individualization-request') {
player.video.receivedMessageTypes.add(message.messageType);
} else {
......@@ -140,7 +141,8 @@ PlayerUtils.registerEMEEventListeners = function(player) {
}
try {
if (player.testConfig.sessionToLoad) {
if (player.testConfig.sessionToLoad &&
player.testConfig.sessionToLoad != 'PersistentUsageRecord') {
// Create a session to load using a new MediaKeys.
// TODO(jrummell): Add a test that covers remove().
player.access.createMediaKeys()
......@@ -190,10 +192,16 @@ PlayerUtils.registerEMEEventListeners = function(player) {
Utils.failTest(error, UNIT_TEST_FAILURE);
});
} else {
Utils.timeLog('Creating new media key session for initDataType: ' +
message.initDataType + ', initData: ' +
Utils.getHexString(new Uint8Array(message.initData)));
player.session = message.target.mediaKeys.createSession();
Utils.timeLog(
'Creating new media key session for initDataType: ' +
message.initDataType + ', initData: ' +
Utils.getHexString(new Uint8Array(message.initData)));
if (player.testConfig.sessionToLoad == 'PersistentUsageRecord') {
player.session =
message.target.mediaKeys.createSession('persistent-usage-record');
} else {
player.session = message.target.mediaKeys.createSession();
}
addMediaKeySessionListeners(player.session);
player.session.generateRequest(message.initDataType, message.initData)
.catch(function(error) {
......@@ -259,6 +267,9 @@ PlayerUtils.registerEMEEventListeners = function(player) {
player.testConfig.keySystem == STORAGE_ID_TEST_KEYSYSTEM) {
config.persistentState = 'required';
config.sessionTypes = ['temporary', 'persistent-license'];
if (player.testConfig.sessionToLoad == 'PersistentUsageRecord') {
config.sessionTypes.push('persistent-usage-record');
}
}
return navigator
......@@ -326,3 +337,27 @@ PlayerUtils.createPlayer = function(video, testConfig) {
var Player = getPlayerType(testConfig.keySystem);
return new Player(video, testConfig);
};
PlayerUtils.removeSession = async function(player) {
// Once remove() is called, another 'keystatuseschange' and 'message' events
// will happen.
const waitForKeyStatusChangePromise =
Utils.waitForEvent(player.session, 'keystatuseschange');
const waitForMessagePromise = Utils.waitForEvent(
player.session, 'message', function(e, resolve, reject) {
Utils.timeLog(e.messageType);
if (e.messageType == 'license-release' &&
player.testConfig.sessionToLoad == 'PersistentUsageRecord') {
Utils.verifyUsageRecord(e.message);
}
// TODO: verify license-release message for persistent-license session
resolve();
});
Utils.timeLog('Calling remove()');
const removePromise = player.session.remove();
return Promise.all(
[removePromise, waitForKeyStatusChangePromise, waitForMessagePromise]);
}
......@@ -86,16 +86,16 @@ Utils.createKeyIdsInitializationData = function(keyId) {
return Utils.convertToUint8Array(initData);
};
function convertToString(data) {
return String.fromCharCode.apply(null, Utils.convertToUint8Array(data));
}
Utils.extractFirstLicenseKeyId = function(message) {
// Decodes data (Uint8Array) from base64url string.
function base64urlDecode(data) {
return atob(data.replace(/\-/g, "+").replace(/\_/g, "/"));
}
function convertToString(data) {
return String.fromCharCode.apply(null, Utils.convertToUint8Array(data));
}
try {
var json = JSON.parse(convertToString(message));
// Decode the first element of 'kids', return it as an Uint8Array.
......@@ -106,7 +106,31 @@ Utils.extractFirstLicenseKeyId = function(message) {
}
};
Utils.documentLog = function(log, success, time) {
Utils.verifyUsageRecord =
function(message) {
try {
var json = JSON.parse(convertToString(message));
var first_decrypt_time = new Date(json.firstTime);
var last_decrypt_time = new Date(json.latestTime);
Utils.timeLog('First decrypt time: ' + first_decrypt_time.toISOString());
Utils.timeLog('Last decrypt time: ' + last_decrypt_time.toISOString());
var delta = json.latestTime - json.firstTime;
// The video used for the tests is roughly 2.5 seconds.
if (delta < 2000 || delta > 3000) {
Utils.failTest(
'The usage record reported by the CDM was not in the' +
'expected range')
}
} catch (error) {
Utils.failTest(
'Fail to extract first decrypt time from license-release' +
'message');
}
}
Utils.documentLog = function(log, success, time) {
if (!docLogs)
return;
time = time || Utils.getCurrentTimeString();
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment