Skip to content

Commit

Permalink
Implement ClearKey message format as JSON.
Browse files Browse the repository at this point in the history
The EME-WD specification now describes the format of the license
request provided to the application via the message attribute of
the message event as being JSON formatted. This change updates
the ClearKey implmentation to return the JSON formatted attribute.

For backwards compatibility with the prefixed EME implementation,
the old format (single key id as Uint8Array) is still used for
any prefixed calls.

BUG=358271
TEST=Updated EME layout tests pass

Review URL: https://codereview.chromium.org/427993002

Cr-Commit-Position: refs/heads/master@{#290669}
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@290669 0039d316-1c4b-4281-b951-d872f2087c98
  • Loading branch information
jrummell@chromium.org committed Aug 19, 2014
1 parent 62b2a35 commit 97a749d
Show file tree
Hide file tree
Showing 9 changed files with 338 additions and 19 deletions.
11 changes: 11 additions & 0 deletions content/renderer/media/crypto/proxy_decryptor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,17 @@ void ProxyDecryptor::OnSessionMessage(const std::string& web_session_id,
const std::vector<uint8>& message,
const GURL& destination_url) {
// Assumes that OnSessionCreated() has been called before this.

// For ClearKey, convert the message from JSON into just passing the key
// as the message. If unable to extract the key, return the message unchanged.
if (is_clear_key_) {
std::vector<uint8> key;
if (media::ExtractFirstKeyIdFromLicenseRequest(message, &key)) {
key_message_cb_.Run(web_session_id, key, destination_url);
return;
}
}

key_message_cb_.Run(web_session_id, message, destination_url);
}

Expand Down
7 changes: 4 additions & 3 deletions media/cdm/aes_decryptor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -245,11 +245,11 @@ void AesDecryptor::CreateSession(const std::string& init_data_type,

// For now, the AesDecryptor does not care about |init_data_type| or
// |session_type|; just resolve the promise and then fire a message event
// with the |init_data| as the request.
// using the |init_data| as the key ID in the license request.
// TODO(jrummell): Validate |init_data_type| and |session_type|.
std::vector<uint8> message;
if (init_data && init_data_length)
message.assign(init_data, init_data + init_data_length);
CreateLicenseRequest(init_data, init_data_length, session_type, &message);

promise->resolve(web_session_id);

Expand Down Expand Up @@ -280,7 +280,8 @@ void AesDecryptor::UpdateSession(const std::string& web_session_id,
response_length);

KeyIdAndKeyPairs keys;
if (!ExtractKeysFromJWKSet(key_string, &keys)) {
SessionType session_type = MediaKeys::TEMPORARY_SESSION;
if (!ExtractKeysFromJWKSet(key_string, &keys, &session_type)) {
promise->reject(
INVALID_ACCESS_ERROR, 0, "response is not a valid JSON Web Key Set.");
return;
Expand Down
13 changes: 11 additions & 2 deletions media/cdm/aes_decryptor_unittest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

#include "base/basictypes.h"
#include "base/bind.h"
#include "base/json/json_reader.h"
#include "base/values.h"
#include "media/base/cdm_promise.h"
#include "media/base/decoder_buffer.h"
#include "media/base/decrypt_config.h"
Expand All @@ -24,6 +26,11 @@ using ::testing::StrNe;

MATCHER(IsEmpty, "") { return arg.empty(); }
MATCHER(IsNotEmpty, "") { return !arg.empty(); }
MATCHER(IsJSONDictionary, "") {
std::string result(arg.begin(), arg.end());
scoped_ptr<base::Value> root(base::JSONReader().ReadToValue(result));
return (root.get() && root->GetType() == base::Value::TYPE_DICTIONARY);
}

class GURL;

Expand Down Expand Up @@ -51,7 +58,8 @@ const char kKeyAsJWK[] =
" \"kid\": \"AAECAw\","
" \"k\": \"BAUGBwgJCgsMDQ4PEBESEw\""
" }"
" ]"
" ],"
" \"type\": \"temporary\""
"}";

// Same kid as kKeyAsJWK, key to decrypt kEncryptedData2
Expand Down Expand Up @@ -287,7 +295,8 @@ class AesDecryptorTest : public testing::Test {
std::string CreateSession(const std::vector<uint8>& key_id) {
DCHECK(!key_id.empty());
EXPECT_CALL(*this,
OnSessionMessage(IsNotEmpty(), key_id, GURL::EmptyGURL()));
OnSessionMessage(
IsNotEmpty(), IsJSONDictionary(), GURL::EmptyGURL()));
decryptor_.CreateSession(std::string(),
&key_id[0],
key_id.size(),
Expand Down
103 changes: 101 additions & 2 deletions media/cdm/json_web_key.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "base/base64.h"
#include "base/json/json_reader.h"
#include "base/json/json_string_value_serializer.h"
#include "base/json/string_escape.h"
#include "base/logging.h"
#include "base/memory/scoped_ptr.h"
#include "base/strings/string_util.h"
Expand All @@ -19,7 +20,11 @@ const char kKeyTypeTag[] = "kty";
const char kSymmetricKeyValue[] = "oct";
const char kKeyTag[] = "k";
const char kKeyIdTag[] = "kid";
const char kKeyIdsTag[] = "kids";
const char kBase64Padding = '=';
const char kTypeTag[] = "type";
const char kPersistentType[] = "persistent";
const char kTemporaryType[] = "temporary";

// Encodes |input| into a base64 string without padding.
static std::string EncodeBase64(const uint8* input, int input_length) {
Expand Down Expand Up @@ -120,7 +125,9 @@ static bool ConvertJwkToKeyPair(const base::DictionaryValue& jwk,
return true;
}

bool ExtractKeysFromJWKSet(const std::string& jwk_set, KeyIdAndKeyPairs* keys) {
bool ExtractKeysFromJWKSet(const std::string& jwk_set,
KeyIdAndKeyPairs* keys,
MediaKeys::SessionType* session_type) {
if (!base::IsStringASCII(jwk_set))
return false;

Expand Down Expand Up @@ -156,9 +163,101 @@ bool ExtractKeysFromJWKSet(const std::string& jwk_set, KeyIdAndKeyPairs* keys) {
local_keys.push_back(key_pair);
}

// Successfully processed all JWKs in the set.
// Successfully processed all JWKs in the set. Now check if "type" is
// specified.
base::Value* value = NULL;
std::string type_id;
if (!dictionary->Get(kTypeTag, &value)) {
// Not specified, so use the default type.
*session_type = MediaKeys::TEMPORARY_SESSION;
} else if (!value->GetAsString(&type_id)) {
DVLOG(1) << "Invalid '" << kTypeTag << "' value";
return false;
} else if (type_id == kPersistentType) {
*session_type = MediaKeys::PERSISTENT_SESSION;
} else if (type_id == kTemporaryType) {
*session_type = MediaKeys::TEMPORARY_SESSION;
} else {
DVLOG(1) << "Invalid '" << kTypeTag << "' value: " << type_id;
return false;
}

// All done.
keys->swap(local_keys);
return true;
}

void CreateLicenseRequest(const uint8* key_id,
int key_id_length,
MediaKeys::SessionType session_type,
std::vector<uint8>* license) {
// Create the license request.
scoped_ptr<base::DictionaryValue> request(new base::DictionaryValue());
scoped_ptr<base::ListValue> list(new base::ListValue());
list->AppendString(EncodeBase64(key_id, key_id_length));
request->Set(kKeyIdsTag, list.release());

switch (session_type) {
case MediaKeys::TEMPORARY_SESSION:
request->SetString(kTypeTag, kTemporaryType);
break;
case MediaKeys::PERSISTENT_SESSION:
request->SetString(kTypeTag, kPersistentType);
break;
}

// Serialize the license request as a string.
std::string json;
JSONStringValueSerializer serializer(&json);
serializer.Serialize(*request);

// Convert the serialized license request into std::vector and return it.
std::vector<uint8> result(json.begin(), json.end());
license->swap(result);
}

bool ExtractFirstKeyIdFromLicenseRequest(const std::vector<uint8>& license,
std::vector<uint8>* first_key) {
const std::string license_as_str(
reinterpret_cast<const char*>(!license.empty() ? &license[0] : NULL),
license.size());
if (!base::IsStringASCII(license_as_str))
return false;

scoped_ptr<base::Value> root(base::JSONReader().ReadToValue(license_as_str));
if (!root.get() || root->GetType() != base::Value::TYPE_DICTIONARY)
return false;

// Locate the set from the dictionary.
base::DictionaryValue* dictionary =
static_cast<base::DictionaryValue*>(root.get());
base::ListValue* list_val = NULL;
if (!dictionary->GetList(kKeyIdsTag, &list_val)) {
DVLOG(1) << "Missing '" << kKeyIdsTag << "' parameter or not a list";
return false;
}

// Get the first key.
if (list_val->GetSize() < 1) {
DVLOG(1) << "Empty '" << kKeyIdsTag << "' list";
return false;
}

std::string encoded_key;
if (!list_val->GetString(0, &encoded_key)) {
DVLOG(1) << "First entry in '" << kKeyIdsTag << "' not a string";
return false;
}

std::string decoded_string = DecodeBase64(encoded_key);
if (decoded_string.empty()) {
DVLOG(1) << "Invalid '" << kKeyIdsTag << "' value: " << encoded_key;
return false;
}

std::vector<uint8> result(decoded_string.begin(), decoded_string.end());
first_key->swap(result);
return true;
}

} // namespace media
52 changes: 45 additions & 7 deletions media/cdm/json_web_key.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,42 @@

#include "base/basictypes.h"
#include "media/base/media_export.h"
#include "media/base/media_keys.h"

namespace media {

// The ClearKey license request format (ref:
// https://dvcs.w3.org/hg/html-media/raw-file/default/encrypted-media/encrypted-media.html#clear-key)
// is a JSON object containing the following members:
// "kids" : An array of key IDs. Each element of the array is the base64url
// encoding of the octet sequence containing the key ID value.
// "type" : The requested SessionType.
// An example:
// { "kids":["67ef0gd8pvfd0","77ef0gd8pvfd0"], "type":"temporary" }

// The ClearKey license format is a JSON Web Key (JWK) Set containing
// representation of the symmetric key to be used for decryption.
// For each JWK in the set, the parameter values are as follows:
// "kty" (key type) : "oct" (octet sequence)
// "alg" (algorithm) : "A128KW" (AES key wrap using a 128-bit key)
// "k" (key value) : The base64url encoding of the octet sequence
// containing the symmetric key value.
// "kid" (key ID) : The base64url encoding of the octet sequence
// containing the key ID value.
// The JSON object may have an optional "type" member value, which may be
// any of the SessionType values. If not specified, the default value of
// "temporary" is used.
// A JSON Web Key Set looks like the following in JSON:
// { "keys": [ JWK1, JWK2, ... ] }
// { "keys": [ JWK1, JWK2, ... ], "type":"temporary" }
// A symmetric keys JWK looks like the following in JSON:
// { "kty":"oct",
// "alg":"A128KW",
// "kid":"AQIDBAUGBwgJCgsMDQ4PEA",
// "k":"FBUWFxgZGhscHR4fICEiIw" }

// There may be other properties specified, but they are ignored.
// Ref: http://tools.ietf.org/html/draft-ietf-jose-json-web-key and:
// http://tools.ietf.org/html/draft-jones-jose-json-private-and-symmetric-key
//
// For EME WD, both 'kid' and 'k' are base64 encoded strings, without trailing
// padding.

// Vector of [key_id, key_value] pairs. Values are raw binary data, stored in
// strings for convenience.
Expand All @@ -37,10 +58,27 @@ MEDIA_EXPORT std::string GenerateJWKSet(const uint8* key, int key_length,
const uint8* key_id, int key_id_length);

// Extracts the JSON Web Keys from a JSON Web Key Set. If |input| looks like
// a valid JWK Set, then true is returned and |keys| is updated to contain
// the list of keys found. Otherwise return false.
// a valid JWK Set, then true is returned and |keys| and |session_type| are
// updated to contain the values found. Otherwise return false.
MEDIA_EXPORT bool ExtractKeysFromJWKSet(const std::string& jwk_set,
KeyIdAndKeyPairs* keys);
KeyIdAndKeyPairs* keys,
MediaKeys::SessionType* session_type);

// Create a license request message for the |key_id| and |session_type|
// specified. Currently ClearKey generates a message for each key individually,
// so no need to take a list of |key_id|'s. |license| is updated to contain the
// resulting JSON string.
MEDIA_EXPORT void CreateLicenseRequest(const uint8* key_id,
int key_id_length,
MediaKeys::SessionType session_type,
std::vector<uint8>* license);

// 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.
MEDIA_EXPORT bool ExtractFirstKeyIdFromLicenseRequest(
const std::vector<uint8>& license,
std::vector<uint8>* first_key);

} // namespace media

Expand Down
Loading

0 comments on commit 97a749d

Please sign in to comment.