Files
android/libwvdrmengine/cdm/core/src/initialization_data.cpp
Fred Gylys-Colwell 135d6c608d Update test data for entitled license test
A new set of license data was created on UAT so that we
could have keys that match those in the license returned by
a License SDK and by those generated by UAT.

It should be more clear now which data is just made up, and
which data has to match some golden values based on the made
up data.

Bug: 338323091
Test: WVTS
Change-Id: Ic112b4594afb99c6f43e011f59ee7592d4809189
2024-08-23 23:26:28 -07:00

746 lines
24 KiB
C++

// Copyright 2018 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine License
// Agreement.
#include "initialization_data.h"
#include <string.h>
#include <string>
#include "buffer_reader.h"
#include "cdm_engine.h"
#include "jsmn.h"
#include "log.h"
#include "platform.h"
#include "properties.h"
#include "string_conversions.h"
#include "wv_cdm_constants.h"
#include "wv_cdm_types.h"
namespace {
const char kKeyFormatVersionsSeparator = '/';
const std::string kBase64String = "base64,";
const uint32_t kFourCcCbc1 = 0x63626331;
const uint32_t kFourCcCbcs = 0x63626373;
// json init data key values
const std::string kProvider = "provider";
const std::string kContentId = "content_id";
const std::string kKeyIds = "key_ids";
// Being conservative, usually we expect 6 + number of Key Ids
const int kDefaultNumJsonTokens = 128;
// Widevine's registered system ID.
const uint8_t kWidevineSystemId[] = {
0xED, 0xEF, 0x8B, 0xA9, 0x79, 0xD6, 0x4A, 0xCE,
0xA3, 0xC8, 0x27, 0xDC, 0xD5, 0x1D, 0x21, 0xED,
};
} // namespace
namespace wvcdm {
// Protobuf generated classes.
using video_widevine::WidevinePsshData;
using video_widevine::WidevinePsshData_Algorithm_AESCTR;
using video_widevine::WidevinePsshData_Type_ENTITLED_KEY;
InitializationData::InitializationData(const std::string& type,
const CdmInitData& data,
const std::string& oec_version)
: type_(type),
is_cenc_(false),
is_hls_(false),
is_webm_(false),
is_audio_(false),
contains_entitled_keys_(false),
hls_method_(kHlsMethodNone) {
if (type == ISO_BMFF_VIDEO_MIME_TYPE || type == ISO_BMFF_AUDIO_MIME_TYPE ||
type == CENC_INIT_DATA_FORMAT) {
is_cenc_ = true;
} else if (type == WEBM_VIDEO_MIME_TYPE || type == WEBM_AUDIO_MIME_TYPE ||
type == WEBM_INIT_DATA_FORMAT) {
is_webm_ = true;
} else if (type == HLS_INIT_DATA_FORMAT) {
is_hls_ = true;
}
if (type == ISO_BMFF_AUDIO_MIME_TYPE || type == WEBM_AUDIO_MIME_TYPE) {
is_audio_ = true;
}
if (data.size() && is_supported()) {
if (is_cenc()) {
bool oec_prefers_entitlements = DetectEntitlementPreference(oec_version);
if (!SelectWidevinePssh(data, oec_prefers_entitlements, &data_)) {
LOGE("Unable to select a supported Widevine PSSH from the init data");
}
} else if (is_webm()) {
data_ = data;
} else if (is_hls()) {
std::string uri;
if (ExtractHlsAttributes(data, &hls_method_, &hls_iv_, &uri)) {
ConstructWidevineInitData(hls_method_, uri, &data_);
}
}
}
}
const std::string InitializationData::WidevineSystemID() {
return std::string(reinterpret_cast<const char*>(kWidevineSystemId),
sizeof(kWidevineSystemId));
}
std::vector<video_widevine::WidevinePsshData_EntitledKey>
InitializationData::ExtractWrappedKeys() const {
std::vector<video_widevine::WidevinePsshData_EntitledKey> keys;
WidevinePsshData cenc_header;
if (!is_cenc_ || !cenc_header.ParseFromString(data_) ||
cenc_header.entitled_keys().size() == 0)
return keys;
keys.reserve(cenc_header.entitled_keys().size());
for (int i = 0; i < cenc_header.entitled_keys().size(); ++i) {
keys.push_back(cenc_header.entitled_keys(i));
}
return keys;
}
// Parse a blob of multiple concatenated PSSH atoms to extract the Widevine
// PSSH. The blob may have multiple Widevine PSSHs. Devices that prefer
// entitlements should choose the |ENTITLED_KEY| PSSH while other devices should
// stick to the |SINGLE| PSSH.
bool InitializationData::SelectWidevinePssh(const CdmInitData& init_data,
bool prefer_entitlements,
CdmInitData* output) {
// Extract the data payloads from the Widevine PSSHs.
std::vector<CdmInitData> pssh_payloads;
if (!ExtractWidevinePsshs(init_data, &pssh_payloads)) {
LOGE("Unable to parse concatenated PSSH boxes");
return false;
}
if (pssh_payloads.empty()) {
LOGE("Widevine PSSH was not found in concatenated PSSH boxes");
return false;
}
// If this device prefers entitlements, search through available PSSHs.
// If present, select the first |ENTITLED_KEY| PSSH.
if (prefer_entitlements && !pssh_payloads.empty()) {
for (size_t i = 0; i < pssh_payloads.size(); ++i) {
WidevinePsshData pssh;
if (!pssh.ParseFromString(pssh_payloads[i])) {
LOGE("Unable to parse PSSH data into a protobuf: index = %zu", i);
continue;
}
if (pssh.type() == WidevinePsshData_Type_ENTITLED_KEY) {
contains_entitled_keys_ = true;
*output = pssh_payloads[i];
return true;
}
}
}
// Either there is only one PSSH, this device does not prefer entitlements,
// or no entitlement PSSH was found. Regardless of how we got here, select the
// first PSSH, which should be a |SINGLE| PSSH.
*output = pssh_payloads[0];
return true;
}
// one PSSH box consists of:
// 4 byte size of the atom, inclusive. (0 means the rest of the buffer.)
// 4 byte atom type, "pssh".
// (optional, if size == 1) 8 byte size of the atom, inclusive.
// 1 byte version, value 0 or 1. (skip if larger.)
// 3 byte flags, value 0. (ignored.)
// 16 byte system id.
// (optional, if version == 1) 4 byte key ID count. (K)
// (optional, if version == 1) K * 16 byte key ID.
// 4 byte size of PSSH data, exclusive. (N)
// N byte PSSH data.
bool InitializationData::ExtractWidevinePsshs(const CdmInitData& init_data,
std::vector<CdmInitData>* psshs) {
if (psshs == nullptr) {
LOGE("Output parameter |psshs| not provided");
return false;
}
psshs->clear();
psshs->reserve(2); // We expect 1 or 2 Widevine PSSHs
const uint8_t* data_start =
reinterpret_cast<const uint8_t*>(init_data.data());
BufferReader reader(data_start, init_data.length());
while (!reader.IsEOF()) {
// LOGV is used intentionally as it is expected that the CDM will try
// several PSSHs until it finds the correct one.
// See b/23419359 for more information.
const size_t start_pos = reader.pos();
// Atom size. Used for bounding the inner reader and knowing how far to skip
// forward after parsing this PSSH.
uint64_t size;
if (!reader.Read4Into8(&size)) {
LOGV("Unable to read the 32-bit atom size");
return false; // We cannot continue reading safely. Abort.
}
// Skip the atom type for now.
if (!reader.SkipBytes(4)) {
LOGV("Unable to skip the atom type");
return false; // We cannot continue reading safely. Abort.
}
if (size == 1) {
// An "atom size" of 1 means the real atom size is a 64-bit number stored
// after the atom type.
if (!reader.Read8(&size)) {
LOGV("Unable to read the 64-bit atom size");
return false; // We cannot continue reading safely. Abort.
}
} else if (size == 0) {
// An "atom size" of 0 means the atom takes up the rest of the buffer.
size = reader.size() - start_pos;
}
const size_t bytes_read = reader.pos() - start_pos;
const size_t bytes_remaining = size - bytes_read;
if (!reader.HasBytes(bytes_remaining)) {
LOGV(
"Invalid atom size: The atom claims to be larger than the "
"remaining init data");
return false; // We cannot continue reading safely. Abort.
}
// Use ExtractWidevinePsshData() to check if this atom is a Widevine PSSH
// and, if so, add its data to the list of PSSHs.
CdmInitData data;
if (ExtractWidevinePsshData(data_start + start_pos, size, &data)) {
psshs->push_back(data);
}
// Skip past the rest of the atom.
if (!reader.SkipBytes(bytes_remaining)) {
LOGV("Unable to skip the rest of the atom");
return false; // We cannot continue reading safely. Abort.
}
}
return true;
}
// Checks if the given reader contains a Widevine PSSH. If so, it extracts the
// data from the box. Returns true if a PSSH was extracted, false if there was
// an error or if the data is not a Widevine PSSH.
bool InitializationData::ExtractWidevinePsshData(const uint8_t* data,
size_t length,
CdmInitData* output) {
// LOGV is used intentionally as it is expected that the CDM will try
// several PSSHs until it finds the correct one.
// See b/23419359 for more information.
BufferReader reader(data, length);
// Read the 32-bit size only so we can check if we need to expect a 64-bit
// size.
uint64_t size_32;
if (!reader.Read4Into8(&size_32)) {
LOGV("Unable to read the 32-bit atom size");
return false;
}
const bool has_size_64 = (size_32 == 1);
// Read the atom type and check that it is "pssh".
std::vector<uint8_t> atom_type;
if (!reader.ReadVec(&atom_type, 4)) {
LOGV("Unable to read the atom type");
return false;
}
if (memcmp(&atom_type[0], "pssh", 4) != 0) {
LOGV("Atom type is not PSSH");
return false;
}
// If there is a 64-bit size, skip it.
if (has_size_64) {
if (!reader.SkipBytes(8)) {
LOGV("Unable to skip the 64-bit atom size");
return false;
}
}
// Read the version number and abort if it is not one we can handle.
uint8_t version;
if (!reader.Read1(&version)) {
LOGV("Unable to read the PSSH version");
return false;
}
if (version > 1) {
LOGV("Unrecognized PSSH version: %d", static_cast<int>(version));
return false;
}
// Skip the flags.
if (!reader.SkipBytes(3)) {
LOGV("Unable to skip the PSSH flags");
return false;
}
// Read the System ID and validate that it is the Widevine System ID.
std::vector<uint8_t> system_id;
if (!reader.ReadVec(&system_id, sizeof(kWidevineSystemId))) {
LOGV("Unable to read the system ID");
return false;
}
if (memcmp(&system_id[0], kWidevineSystemId, sizeof(kWidevineSystemId)) !=
0) {
LOGV("Found a non-Widevine PSSH");
return false;
}
// Skip the key ID fields, if present.
if (version == 1) {
// Read the number of key IDs so we know how far to skip ahead.
uint32_t num_key_ids;
if (!reader.Read4(&num_key_ids)) {
LOGV("Unable to read the number of key IDs");
return false;
}
// Skip the key IDs.
if (!reader.SkipBytes(num_key_ids * 16)) {
LOGV("Unable to skip the key IDs");
return false;
}
}
// Read the size of the PSSH data.
uint32_t data_length;
if (!reader.Read4(&data_length)) {
LOGV("Unable to read the PSSH data size");
return false;
}
// Read the PSSH data.
output->clear();
if (!reader.ReadString(output, data_length)) {
LOGV("Unable to read the PSSH data");
return false;
}
return true;
}
// Parse an EXT-X-KEY tag attribute list. Verify that Widevine supports it
// by validating KEYFORMAT and KEYFORMATVERSION attributes. Extract out
// method, IV, URI and WV init data.
//
// An example of a widevine supported attribute list from an HLS media playlist
// is,
// "EXT-X-KEY: METHOD=SAMPLE-AES, \"
// "URI=”data:text/plain;base64,eyANCiAgICJwcm92aWRlciI6Im1sYmFtaGJvIiwNCiAg"
// "ICJjb250ZW50X2lkIjoiMjAxNV9UZWFycyIsDQogICAia2V5X2lkcyI6DQogICBbDQo"
// "gICAgICAiMzcxZTEzNWUxYTk4NWQ3NWQxOThhN2Y0MTAyMGRjMjMiDQogICBdDQp9DQ"
// "o=, \"
// "IV=0x6df49213a781e338628d0e9c812d328e, \"
// "KEYFORMAT=”com.widevine”, \"
// "KEYFORMATVERSIONS=”1”"
bool InitializationData::ExtractHlsAttributes(const std::string& attribute_list,
CdmHlsMethod* method,
std::vector<uint8_t>* iv,
std::string* uri) {
// LOGV is used intentionally as it is expected that the CDM will try
// several PSSHs until it finds the correct one.
// See b/23419359 for more information.
std::string value;
if (!ExtractQuotedAttribute(attribute_list, HLS_KEYFORMAT_ATTRIBUTE,
&value)) {
LOGV("Unable to read HLS 'keyformat' value");
return false;
}
if (value.compare(0, sizeof(KEY_SYSTEM) - 1, KEY_SYSTEM) != 0) {
LOGV("Unrecognized HLS 'keyformat' value: %s", value.c_str());
return false;
}
// KEYFORMATVERSIONS is an optional parameter. If absent its
// value defaults to "1"
if (ExtractQuotedAttribute(attribute_list, HLS_KEYFORMAT_VERSIONS_ATTRIBUTE,
&value)) {
std::vector<std::string> versions = ExtractKeyFormatVersions(value);
bool supported = false;
for (size_t i = 0; i < versions.size(); ++i) {
if (versions[i] == HLS_KEYFORMAT_VERSION_VALUE_1) {
supported = true;
break;
}
}
if (!supported) {
LOGV("HLS 'keyformat' version is not supported: value = %s",
value.c_str());
return false;
}
}
if (!ExtractAttribute(attribute_list, HLS_METHOD_ATTRIBUTE, &value)) {
LOGV("Unable to read HLS method");
return false;
}
if (value == HLS_METHOD_AES_128) {
*method = kHlsMethodAes128;
} else if (value == HLS_METHOD_SAMPLE_AES) {
*method = kHlsMethodSampleAes;
} else if (value == HLS_METHOD_NONE) {
*method = kHlsMethodNone;
} else {
LOGV("HLS method unrecognized: value = %s", value.c_str());
return false;
}
if (!ExtractHexAttribute(attribute_list, HLS_IV_ATTRIBUTE, iv)) {
LOGV("HLS IV attribute not present");
return false;
}
if (!ExtractQuotedAttribute(attribute_list, HLS_URI_ATTRIBUTE, uri)) {
LOGV("HLS URI attribute not present");
return false;
}
return true;
}
// Extracts a base64 encoded string from URI data. This is then base64 decoded
// and the Json formatted init data is then parsed. The information is used
// to generate a Widevine init data protobuf (WidevineCencHeader).
//
// An example of a widevine supported json formatted init data string is,
//
// {
// "provider":"mlbamhbo",
// "content_id":"MjAxNV9UZWFycw==",
// "key_ids":
// [
// "371e135e1a985d75d198a7f41020dc23"
// ]
// }
bool InitializationData::ConstructWidevineInitData(
CdmHlsMethod method, const std::string& uri, CdmInitData* init_data_proto) {
if (!init_data_proto) {
LOGE("Output parameter |init_data_proto| not provided");
return false;
}
if (method != kHlsMethodAes128 && method != kHlsMethodSampleAes) {
LOGE("Invalid HLS method parameter");
return false;
}
// LOGV is used intentionally as it is expected that the CDM will try
// several PSSHs until it finds the correct one.
// See b/23419359 for more information.
size_t pos = uri.find(kBase64String);
if (pos == std::string::npos) {
LOGV("URI attribute unexpected format: uri = %s", uri.c_str());
return false;
}
std::vector<uint8_t> json_init_data =
wvutil::Base64Decode(uri.substr(pos + kBase64String.size()));
if (json_init_data.size() == 0) {
LOGV("Base64 decode of json data failed");
return false;
}
std::string json_string((const char*)(&json_init_data[0]),
json_init_data.size());
// Parse the Json string using jsmn
jsmn_parser parser;
jsmntok_t tokens[kDefaultNumJsonTokens];
jsmn_init(&parser);
int num_of_tokens =
jsmn_parse(&parser, json_string.c_str(), json_string.size(), tokens,
kDefaultNumJsonTokens);
if (num_of_tokens <= 0) {
LOGV("Json parsing failed: num_of_tokens = %d", num_of_tokens);
return false;
}
std::string provider;
std::string content_id;
std::vector<std::string> key_ids;
enum JsmnParserState {
kParseState,
kProviderState,
kContentIdState,
kKeyIdsState,
} state = kParseState;
int number_of_key_ids = 0;
// Extract the provider, content_id and key_ids
for (int i = 0; i < num_of_tokens; ++i) {
if (tokens[i].start < 0 || tokens[i].end < 0) {
LOGV("Invalid start and/or end of token");
return false;
}
switch (state) {
case kParseState:
if (tokens[i].type == JSMN_STRING) {
std::string token(json_string, tokens[i].start,
tokens[i].end - tokens[i].start);
if (token == kProvider) {
state = kProviderState;
} else if (token == kContentId) {
state = kContentIdState;
} else if (token == kKeyIds) {
state = kKeyIdsState;
}
}
break;
case kProviderState:
if (tokens[i].type == JSMN_STRING) {
provider.assign(json_string, tokens[i].start,
tokens[i].end - tokens[i].start);
}
state = kParseState;
break;
case kContentIdState:
if (tokens[i].type == JSMN_STRING) {
std::string base64_content_id(json_string, tokens[i].start,
tokens[i].end - tokens[i].start);
std::vector<uint8_t> content_id_data =
wvutil::Base64Decode(base64_content_id);
content_id.assign(reinterpret_cast<const char*>(&content_id_data[0]),
content_id_data.size());
}
state = kParseState;
break;
case kKeyIdsState:
if (tokens[i].type == JSMN_ARRAY) {
number_of_key_ids = tokens[i].size;
} else if (tokens[i].type == JSMN_STRING) {
std::string key_id(wvutil::a2bs_hex(json_string.substr(
tokens[i].start, tokens[i].end - tokens[i].start)));
if (key_id.size() == 16) key_ids.push_back(key_id);
--number_of_key_ids;
} else {
state = kParseState;
}
if (number_of_key_ids <= 0) state = kParseState;
break;
}
}
if (provider.size() == 0) {
LOGV("Invalid provider");
return false;
}
if (content_id.size() == 0) {
LOGV("Invalid content ID");
return false;
}
if (key_ids.size() == 0) {
LOGV("No key IDs present");
return false;
}
// Now format as Widevine init data protobuf
WidevinePsshData cenc_header;
// TODO(rfrias): The algorithm and provider are deprecated fields, but proto
// changes have not yet been pushed to production. Set until then.
CORE_UTIL_IGNORE_DEPRECATED
cenc_header.set_algorithm(WidevinePsshData_Algorithm_AESCTR);
cenc_header.set_provider(provider);
CORE_UTIL_RESTORE_WARNINGS
for (size_t i = 0; i < key_ids.size(); ++i) {
cenc_header.add_key_ids(key_ids[i]);
}
cenc_header.set_content_id(content_id);
if (method == kHlsMethodAes128)
cenc_header.set_protection_scheme(kFourCcCbc1);
else
cenc_header.set_protection_scheme(kFourCcCbcs);
cenc_header.SerializeToString(init_data_proto);
return true;
}
bool InitializationData::ExtractQuotedAttribute(
const std::string& attribute_list, const std::string& key,
std::string* value) {
bool result = ExtractAttribute(attribute_list, key, value);
if (value->size() < 2 || value->at(0) != '\"' ||
value->at(value->size() - 1) != '\"')
return false;
*value = value->substr(1, value->size() - 2);
if (value->find('\"') != std::string::npos) return false;
return result;
}
bool InitializationData::ExtractHexAttribute(const std::string& attribute_list,
const std::string& key,
std::vector<uint8_t>* value) {
std::string val;
bool result = ExtractAttribute(attribute_list, key, &val);
if (!result || val.size() <= 2 || val.size() % 2 != 0 || val[0] != '0' ||
((val[1] != 'x') && (val[1] != 'X')))
return false;
for (size_t i = 2; i < val.size(); ++i) {
if (!isxdigit(val[i])) return false;
}
*value = wvutil::a2b_hex(val.substr(2, val.size() - 2));
return result;
}
bool InitializationData::ExtractAttribute(const std::string& attribute_list,
const std::string& key,
std::string* value) {
// validate the key
for (size_t i = 0; i < key.size(); ++i)
if (!isupper(key[i]) && !isdigit(key[i]) && key[i] != '-') return false;
bool found = false;
size_t pos = 0;
// Find the key followed by '='
while (!found) {
pos = attribute_list.find(key, pos);
if (pos == std::string::npos) return false;
pos += key.size();
if (attribute_list[pos] != '=') continue;
found = true;
}
if (attribute_list.size() <= ++pos) return false;
size_t end_pos = pos;
found = false;
// Find the next comma outside the quote or end of string
while (!found) {
end_pos = attribute_list.find(',', end_pos);
if (end_pos != std::string::npos && attribute_list[pos] == '\"' &&
attribute_list[end_pos - 1] != '\"') {
++end_pos;
continue;
}
if (end_pos == std::string::npos)
end_pos = attribute_list.size() - 1;
else
--end_pos;
found = true;
}
*value = attribute_list.substr(pos, end_pos - pos + 1);
// validate the value
for (size_t i = 0; i < value->size(); ++i)
if (!isgraph(value->at(i))) return false;
return true;
}
// Key format versions are individual values or multiple versions
// separated by '/'. "1" or "1/2/5"
std::vector<std::string> InitializationData::ExtractKeyFormatVersions(
const std::string& key_format_versions) {
std::vector<std::string> versions;
size_t pos = 0;
while (pos < key_format_versions.size()) {
size_t next_pos =
key_format_versions.find(kKeyFormatVersionsSeparator, pos);
if (next_pos == std::string::npos) {
versions.push_back(key_format_versions.substr(pos));
break;
} else {
versions.push_back(key_format_versions.substr(pos, next_pos - pos));
pos = next_pos + 1;
}
}
return versions;
}
bool InitializationData::DetectEntitlementPreference(
const std::string& oec_version_string) {
if (oec_version_string.empty()) {
return false;
}
return std::stoul(oec_version_string) >= 14;
}
void InitializationData::DumpToLogs() const {
if (!is_supported()) {
LOGD("InitData: Not supported");
}
if (IsEmpty()) {
LOGD("InitData: Empty");
}
std::string type_info = type();
if (is_cenc()) type_info += " (cenc)";
if (is_hls()) type_info += " (hls)";
if (is_webm()) type_info += " (webm)";
if (is_audio()) type_info += " (audio)";
LOGD("InitData: type = %s", type_info.c_str());
video_widevine::WidevinePsshData pssh;
if (!pssh.ParseFromString(data())) {
LOGD("InitData: invalid pssh: %s", wvutil::b2a_hex(data()).c_str());
return;
}
if (pssh.has_content_id()) {
LOGD("InitData: content_id = '%s'", pssh.content_id().c_str());
}
if (pssh.has_protection_scheme()) {
uint32_t scheme = pssh.protection_scheme();
LOGD("InitData: Protection Scheme: %c%c%c%c",
static_cast<char>(scheme >> 24), static_cast<char>(scheme >> 16),
static_cast<char>(scheme >> 8), static_cast<char>(scheme >> 0));
}
switch (pssh.type()) {
case video_widevine::WidevinePsshData_Type_SINGLE:
// Don't bother printing.
break;
case video_widevine::WidevinePsshData_Type_ENTITLEMENT:
LOGD("InitData: Entitlement License");
break;
case video_widevine::WidevinePsshData_Type_ENTITLED_KEY:
LOGD("InitData: Entitled Key");
break;
default:
LOGE("Undefine pssh type: %d", pssh.type());
break;
}
if (pssh.has_crypto_period_index())
LOGD("InitData: Crypto Period Index %u", pssh.crypto_period_index());
if (pssh.has_crypto_period_seconds())
LOGD("InitData: Crypto Period seconds %u", pssh.crypto_period_seconds());
if (pssh.has_key_sequence())
LOGD("InitData: Key Sequence %u", pssh.key_sequence());
for (int i = 0; i < pssh.key_ids_size(); i++) {
LOGD("InitData: key_id %d: %s", i,
wvutil::b2a_hex(pssh.key_ids(i)).c_str());
}
for (int i = 0; i < pssh.entitled_keys_size(); i++) {
const video_widevine::WidevinePsshData_EntitledKey& key =
pssh.entitled_keys(i);
LOGD("InitData: entitlement_key_id %d: %s -> %s", i,
wvutil::b2a_hex(key.entitlement_key_id()).c_str(),
wvutil::b2a_hex(key.key_id()).c_str());
LOGD("InitData: entitled_key %d: %s", i,
wvutil::b2a_hex(key.key()).c_str());
LOGD("InitData: iv %d: %s", i, wvutil::b2a_hex(key.iv()).c_str());
}
}
} // namespace wvcdm