Skip to content

Commit

Permalink
Add browser test for EME persistent-usage-record session
Browse files Browse the repository at this point in the history
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/+/2285250
Reviewed-by: John Rummell <jrummell@chromium.org>
Reviewed-by: Xiaohan Wang <xhwang@chromium.org>
Commit-Queue: Alex St-Onge <alstonge@chromium.org>
Cr-Commit-Position: refs/heads/master@{#788870}
  • Loading branch information
alex-chromium authored and Commit Bot committed Jul 16, 2020
1 parent 3e6d262 commit 05c95bb
Show file tree
Hide file tree
Showing 10 changed files with 242 additions and 23 deletions.
9 changes: 9 additions & 0 deletions chrome/browser/media/encrypted_media_browsertest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -354,6 +355,9 @@ class ECKEncryptedMediaTest : public EncryptedMediaTestBase,
command_line->AppendSwitchASCII(
switches::kOverrideEnabledCdmInterfaceVersion,
base::NumberToString(GetCdmInterfaceVersion()));
command_line->AppendSwitchASCII(
switches::kEnableBlinkFeatures,
"EncryptedMediaPersistentUsageRecordSession");
}
};

Expand Down Expand Up @@ -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";

Expand Down
33 changes: 31 additions & 2 deletions media/cdm/aes_decryptor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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) {}

Expand Down
12 changes: 12 additions & 0 deletions media/cdm/aes_decryptor.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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_;
Expand All @@ -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);
};

Expand Down
59 changes: 53 additions & 6 deletions media/cdm/json_web_key.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions media/cdm/json_web_key.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <utility>
#include <vector>

#include "base/time/time.h"
#include "media/base/media_export.h"

namespace media {
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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() {
Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 15 additions & 1 deletion media/test/data/eme_player.html
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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);
Expand Down

0 comments on commit 05c95bb

Please sign in to comment.