1417 lines
50 KiB
C++
1417 lines
50 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 "cdm_session.h"
|
|
|
|
#include <assert.h>
|
|
#include <stdlib.h>
|
|
|
|
#include <algorithm>
|
|
#include <iterator>
|
|
#include <string>
|
|
|
|
#include "cdm_engine.h"
|
|
#include "cdm_random.h"
|
|
#include "cdm_usage_table.h"
|
|
#include "clock.h"
|
|
#include "crypto_wrapped_key.h"
|
|
#include "file_store.h"
|
|
#include "log.h"
|
|
#include "properties.h"
|
|
#include "string_conversions.h"
|
|
#include "system_id_extractor.h"
|
|
#include "wv_cdm_constants.h"
|
|
#include "wv_cdm_event_listener.h"
|
|
|
|
// Stringify turns macro arguments into static C strings.
|
|
// Example: STRINGIFY(this_argument) -> "this_argument"
|
|
#define STRINGIFY(PARAM) #PARAM
|
|
|
|
#define RETURN_STATUS_IF_NULL(PARAM) \
|
|
if ((PARAM) == nullptr) { \
|
|
LOGE("Output parameter |" STRINGIFY(PARAM) "| not provided"); \
|
|
return CdmResponseType(PARAMETER_NULL); \
|
|
}
|
|
|
|
#define RETURN_FALSE_IF_NULL(PARAM) \
|
|
if ((PARAM) == nullptr) { \
|
|
LOGE("Output parameter |" STRINGIFY(PARAM) "| not provided"); \
|
|
return false; \
|
|
}
|
|
|
|
namespace wvcdm {
|
|
namespace {
|
|
const size_t kKeySetIdLength = 14;
|
|
|
|
// Helper function for setting the error detail value.
|
|
template <typename T>
|
|
void SetErrorDetail(int* error_detail, T error_code) {
|
|
if (error_detail != nullptr) {
|
|
*error_detail = static_cast<int>(error_code);
|
|
}
|
|
}
|
|
|
|
int DrmKeyTypeToMetricValue(CryptoWrappedKey::Type type) {
|
|
constexpr int kUnknownMetricType = -1;
|
|
constexpr int kRsaMetricType = 0;
|
|
constexpr int kEccMetricType = 1;
|
|
switch (type) {
|
|
case CryptoWrappedKey::kRsa:
|
|
return kRsaMetricType;
|
|
case CryptoWrappedKey::kEcc:
|
|
return kEccMetricType;
|
|
default:
|
|
LOGE("Unexpected DRM key type: %d", static_cast<int>(type));
|
|
return kUnknownMetricType;
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
CdmSession::CdmSession(wvutil::FileSystem* file_system,
|
|
std::shared_ptr<metrics::SessionMetrics> metrics)
|
|
: metrics_(std::move(metrics)),
|
|
initialized_(false),
|
|
closed_(true),
|
|
file_handle_(new DeviceFiles(file_system)),
|
|
license_received_(false),
|
|
is_offline_(false),
|
|
is_release_(false),
|
|
is_temporary_(false),
|
|
security_level_(kSecurityLevelUninitialized),
|
|
requested_security_level_(kLevelDefault),
|
|
is_initial_usage_update_(true),
|
|
is_usage_update_needed_(false) {
|
|
assert(metrics_); // metrics_ must not be null.
|
|
crypto_metrics_ = metrics_->GetCryptoMetrics();
|
|
crypto_session_.reset(CryptoSession::MakeCryptoSession(crypto_metrics_));
|
|
life_span_.Start();
|
|
}
|
|
|
|
CdmSession::~CdmSession() {
|
|
if (has_provider_session_token() && SupportsUsageTable() && !is_release_) {
|
|
UpdateUsageEntryInformation();
|
|
}
|
|
|
|
if (!key_set_id_.empty()) {
|
|
// Unreserve the license ID.
|
|
file_handle_->UnreserveLicenseId(key_set_id_);
|
|
}
|
|
Properties::RemoveSessionPropertySet(session_id_);
|
|
|
|
if (metrics_) {
|
|
M_RECORD(metrics_.get(), cdm_session_life_span_, life_span_.AsMs());
|
|
metrics_->SetCompleted();
|
|
}
|
|
}
|
|
|
|
CdmResponseType CdmSession::Init(
|
|
CdmClientPropertySet* cdm_client_property_set) {
|
|
return Init(cdm_client_property_set, nullptr, nullptr, false);
|
|
}
|
|
|
|
CdmResponseType CdmSession::Init(CdmClientPropertySet* cdm_client_property_set,
|
|
const CdmSessionId* forced_session_id,
|
|
WvCdmEventListener* event_listener,
|
|
bool forced_level3) {
|
|
if (initialized_) {
|
|
LOGE("Failed due to previous initialization");
|
|
return CdmResponseType(REINIT_ERROR);
|
|
}
|
|
|
|
if ((cdm_client_property_set && cdm_client_property_set->security_level() ==
|
|
QUERY_VALUE_SECURITY_LEVEL_L3) ||
|
|
forced_level3) {
|
|
requested_security_level_ = kLevel3;
|
|
security_level_ = kSecurityLevelL3;
|
|
}
|
|
CdmResponseType sts;
|
|
M_TIME(sts = crypto_session_->Open(requested_security_level_),
|
|
crypto_metrics_, crypto_session_open_, sts, requested_security_level_);
|
|
if (NO_ERROR != sts) return sts;
|
|
|
|
security_level_ = crypto_session_->GetSecurityLevel();
|
|
crypto_metrics_->crypto_session_security_level_.Record(security_level_);
|
|
std::string oemcrypto_build;
|
|
if (crypto_session_->GetBuildInformation(requested_security_level_,
|
|
&oemcrypto_build)) {
|
|
metrics_->oemcrypto_build_info_.Record(oemcrypto_build);
|
|
} else {
|
|
metrics_->oemcrypto_build_info_.SetError(false);
|
|
}
|
|
|
|
if (!file_handle_->Init(security_level_)) {
|
|
LOGE("Unable to initialize file handle");
|
|
return CdmResponseType(SESSION_FILE_HANDLE_INIT_ERROR);
|
|
}
|
|
|
|
bool has_support = false;
|
|
if (crypto_session_->HasUsageTableSupport(&has_support) && has_support) {
|
|
usage_table_ = crypto_session_->GetUsageTable();
|
|
}
|
|
|
|
if (cdm_client_property_set != nullptr)
|
|
atsc_mode_enabled_ = cdm_client_property_set->use_atsc_mode();
|
|
|
|
// If a DRM certificate does not exist, indicate that provisioning is needed.
|
|
// The actual validation and loading of a certificate will happen when
|
|
// a key request is generated or an offline license is loaded.
|
|
if (!file_handle_->HasCertificate(atsc_mode_enabled_))
|
|
return CdmResponseType(NEED_PROVISIONING);
|
|
|
|
// Require reprovisioning if the root of trust has changed
|
|
if (HasRootOfTrustBeenRenewed(forced_session_id)) {
|
|
return CdmResponseType(NEED_PROVISIONING);
|
|
}
|
|
|
|
if (forced_session_id) {
|
|
key_set_id_ = *forced_session_id;
|
|
} else {
|
|
const bool ok = GenerateKeySetId(atsc_mode_enabled_, &key_set_id_);
|
|
assert(ok);
|
|
if (!ok) {
|
|
// Assertions may be disabled
|
|
LOGE("Could not generate keyset ID");
|
|
}
|
|
}
|
|
|
|
session_id_ =
|
|
Properties::AlwaysUseKeySetIds() ? key_set_id_ : GenerateSessionId();
|
|
metrics_->SetSessionId(session_id_);
|
|
|
|
if (session_id_.empty()) {
|
|
LOGE("Empty session ID");
|
|
return CdmResponseType(EMPTY_SESSION_ID);
|
|
}
|
|
if (cdm_client_property_set)
|
|
Properties::AddSessionPropertySet(session_id_, cdm_client_property_set);
|
|
|
|
if (!mock_license_parser_in_use_)
|
|
license_parser_.reset(new CdmLicense(session_id_));
|
|
if (!mock_policy_engine_in_use_)
|
|
policy_engine_.reset(
|
|
new PolicyEngine(session_id_, event_listener, crypto_session_.get()));
|
|
|
|
std::string service_certificate;
|
|
if (!Properties::GetServiceCertificate(session_id_, &service_certificate))
|
|
service_certificate.clear();
|
|
|
|
if (!license_parser_->Init(Properties::UsePrivacyMode(session_id_),
|
|
service_certificate, crypto_session_.get(),
|
|
policy_engine_.get()))
|
|
return CdmResponseType(LICENSE_PARSER_INIT_ERROR);
|
|
|
|
license_received_ = false;
|
|
is_initial_decryption_ = true;
|
|
initialized_ = true;
|
|
closed_ = false;
|
|
return CdmResponseType(NO_ERROR);
|
|
}
|
|
|
|
CdmResponseType CdmSession::RestoreOfflineSession(const CdmKeySetId& key_set_id,
|
|
CdmLicenseType license_type,
|
|
int* error_detail) {
|
|
if (!initialized_) {
|
|
LOGE("CDM session not initialized");
|
|
return CdmResponseType(NOT_INITIALIZED_ERROR);
|
|
}
|
|
if (!key_set_id_.empty()) {
|
|
file_handle_->UnreserveLicenseId(key_set_id_);
|
|
}
|
|
if (has_license_been_loaded_ || has_license_been_restored_) {
|
|
LOGE(
|
|
"Disallow multiple offline license restores or restoring a license if "
|
|
"a license has already been loaded");
|
|
return CdmResponseType(RESTORE_OFFLINE_LICENSE_ERROR_3);
|
|
}
|
|
|
|
key_set_id_ = key_set_id;
|
|
|
|
DeviceFiles::CdmLicenseData license_data;
|
|
DeviceFiles::ResponseType sub_error_code = DeviceFiles::kNoError;
|
|
|
|
if (!file_handle_->RetrieveLicense(key_set_id, &license_data,
|
|
&sub_error_code)) {
|
|
LOGE("Failed to retrieve license: sub_error_code = %s, key_set_id = %s",
|
|
DeviceFiles::ResponseTypeToString(sub_error_code),
|
|
IdToString(key_set_id));
|
|
SetErrorDetail(error_detail, sub_error_code);
|
|
switch (sub_error_code) {
|
|
case DeviceFiles::kFileNotFound:
|
|
case DeviceFiles::kFileNotFoundEAcces:
|
|
case DeviceFiles::kFileNotFoundEFault:
|
|
case DeviceFiles::kFileNotFoundELoop:
|
|
case DeviceFiles::kFileNotFoundENameTooLong:
|
|
case DeviceFiles::kFileNotFoundENoEnt:
|
|
case DeviceFiles::kFileNotFoundENoMem:
|
|
case DeviceFiles::kFileNotFoundENotDir:
|
|
case DeviceFiles::kFileNotFoundEOverflow:
|
|
case DeviceFiles::kFileNotFoundOther:
|
|
return (CdmResponseType(KEYSET_ID_NOT_FOUND_4));
|
|
default:
|
|
return (CdmResponseType(GET_LICENSE_ERROR));
|
|
}
|
|
}
|
|
offline_init_data_ = std::move(license_data.pssh_data);
|
|
key_request_ = std::move(license_data.license_request);
|
|
key_response_ = std::move(license_data.license);
|
|
offline_key_renewal_request_ =
|
|
std::move(license_data.license_renewal_request);
|
|
offline_key_renewal_response_ = std::move(license_data.license_renewal);
|
|
offline_release_server_url_ = std::move(license_data.release_server_url);
|
|
app_parameters_ = std::move(license_data.app_parameters);
|
|
usage_entry_ = std::move(license_data.usage_entry);
|
|
usage_entry_index_ = license_data.usage_entry_index;
|
|
|
|
CdmResponseType result = crypto_session_->MarkOfflineSession();
|
|
if (result != NO_ERROR) return result;
|
|
|
|
result = LoadPrivateOrLegacyKey(license_data.drm_certificate,
|
|
license_data.wrapped_private_key);
|
|
if (result != NO_ERROR) return result;
|
|
|
|
// Attempts to restore a released offline license are treated as a release
|
|
// retry.
|
|
if (Properties::allow_restore_of_offline_licenses_with_release()) {
|
|
if (license_data.state == kLicenseStateReleasing) {
|
|
license_type = kLicenseTypeRelease;
|
|
}
|
|
}
|
|
|
|
// Only restore offline licenses if they are active or this is a release
|
|
// retry.
|
|
if (license_type != kLicenseTypeRelease &&
|
|
license_data.state != kLicenseStateActive) {
|
|
LOGE("Invalid offline license state: state = %s, license_type = %s",
|
|
CdmOfflineLicenseStateToString(license_data.state),
|
|
CdmLicenseTypeToString(license_type));
|
|
return CdmResponseType(GET_RELEASED_LICENSE_ERROR);
|
|
}
|
|
|
|
std::string provider_session_token;
|
|
bool sign_fake_request = false; // TODO(b/169483174): remove this variable.
|
|
if (SupportsUsageTable()) {
|
|
if (!license_parser_->ExtractProviderSessionToken(
|
|
key_response_, &provider_session_token)) {
|
|
provider_session_token.clear();
|
|
sign_fake_request = true; // TODO(b/169483174): remove this line.
|
|
} else if (!VerifyOfflineUsageEntry()) {
|
|
LOGE("License usage entry is invalid, cannot restore");
|
|
return CdmResponseType(LICENSE_USAGE_ENTRY_MISSING);
|
|
} else {
|
|
CdmResponseType sts = usage_table_->LoadEntry(
|
|
crypto_session_.get(), usage_entry_, usage_entry_index_);
|
|
crypto_metrics_->usage_table_header_load_entry_.Increment(sts);
|
|
if (sts == LOAD_USAGE_ENTRY_INVALID_SESSION) {
|
|
LOGE("License loaded in different session: key_set_id = %s",
|
|
IdToString(key_set_id));
|
|
return CdmResponseType(USAGE_ENTRY_ALREADY_LOADED);
|
|
}
|
|
if (sts != NO_ERROR) {
|
|
LOGE("Failed to load usage entry: status = %d", static_cast<int>(sts));
|
|
return sts;
|
|
}
|
|
}
|
|
} else {
|
|
sign_fake_request = true; // TODO(b/169483174): remove this block.
|
|
}
|
|
// TODO(b/169483174): remove this code in v17. For OEMCrypto v16, an offline
|
|
// license would not work because the rental clock in OEMCrypto is only
|
|
// started when the license request is signed. We will sign a fake license
|
|
// request if the device does not support usage tables, or if the license does
|
|
// not have a usage entry.
|
|
if (sign_fake_request) {
|
|
std::string fake_message("empty message");
|
|
std::string core_message;
|
|
std::string license_request_signature;
|
|
bool should_specify_algorithm;
|
|
OEMCrypto_SignatureHashAlgorithm algorithm = OEMCrypto_SHA1;
|
|
uint32_t nonce;
|
|
// Sign a fake message so that OEMCrypto will start the rental clock. The
|
|
// signature and generated core message are ignored.
|
|
result = crypto_session_->GenerateNonce(&nonce);
|
|
if (result != NO_ERROR) return result;
|
|
result = crypto_session_->PrepareAndSignLicenseRequest(
|
|
fake_message, &core_message, &license_request_signature,
|
|
should_specify_algorithm, algorithm);
|
|
if (result != NO_ERROR) return result;
|
|
}
|
|
|
|
if (!license_data.exported_license_data.empty()) {
|
|
result =
|
|
crypto_session_->LoadLicenseData(license_data.exported_license_data);
|
|
if (result != NO_ERROR) return result;
|
|
}
|
|
|
|
if (license_type == kLicenseTypeRelease) {
|
|
result = license_parser_->RestoreLicenseForRelease(
|
|
license_data.drm_certificate, key_request_, key_response_);
|
|
|
|
if (result != NO_ERROR) {
|
|
SetErrorDetail(error_detail, result.code());
|
|
return CdmResponseType(RELEASE_LICENSE_ERROR_1);
|
|
}
|
|
} else {
|
|
result = license_parser_->RestoreOfflineLicense(
|
|
license_data.drm_certificate, key_request_, key_response_,
|
|
offline_key_renewal_response_, license_data.playback_start_time,
|
|
license_data.last_playback_time, license_data.grace_period_end_time,
|
|
this);
|
|
if (result != NO_ERROR) {
|
|
SetErrorDetail(error_detail, result.code());
|
|
return CdmResponseType(RESTORE_OFFLINE_LICENSE_ERROR_2);
|
|
}
|
|
}
|
|
|
|
if (!provider_session_token.empty() && SupportsUsageTable()) {
|
|
CdmResponseType sts = usage_table_->UpdateEntry(
|
|
usage_entry_index_, crypto_session_.get(), &usage_entry_);
|
|
if (sts != NO_ERROR) {
|
|
LOGE("Failed to update usage entry: status = %d", static_cast<int>(sts));
|
|
return sts;
|
|
}
|
|
if (!StoreLicense(license_data.state, error_detail)) {
|
|
LOGW("Unable to save updated usage info");
|
|
}
|
|
}
|
|
|
|
license_received_ = true;
|
|
is_offline_ = true;
|
|
is_release_ = license_type == kLicenseTypeRelease;
|
|
has_license_been_restored_ = true;
|
|
return CdmResponseType(KEY_ADDED);
|
|
}
|
|
|
|
CdmResponseType CdmSession::RestoreUsageSession(
|
|
const DeviceFiles::CdmUsageData& usage_data, int* error_detail) {
|
|
if (!initialized_) {
|
|
LOGE("CDM session not initialized");
|
|
return CdmResponseType(NOT_INITIALIZED_ERROR);
|
|
}
|
|
if (!key_set_id_.empty()) {
|
|
file_handle_->UnreserveLicenseId(key_set_id_);
|
|
}
|
|
key_set_id_ = usage_data.key_set_id;
|
|
key_request_ = usage_data.license_request;
|
|
key_response_ = usage_data.license;
|
|
usage_entry_ = usage_data.usage_entry;
|
|
usage_entry_index_ = usage_data.usage_entry_index;
|
|
usage_provider_session_token_ = usage_data.provider_session_token;
|
|
|
|
CdmResponseType status = LoadPrivateOrLegacyKey(
|
|
usage_data.drm_certificate, usage_data.wrapped_private_key);
|
|
if (status != NO_ERROR) return status;
|
|
|
|
CdmResponseType sts(NO_ERROR);
|
|
if (SupportsUsageTable()) {
|
|
sts = usage_table_->LoadEntry(crypto_session_.get(), usage_entry_,
|
|
usage_entry_index_);
|
|
crypto_metrics_->usage_table_header_load_entry_.Increment(sts);
|
|
if (sts != NO_ERROR) {
|
|
LOGE("Failed to load usage entry: status = %d", static_cast<int>(sts));
|
|
return sts;
|
|
}
|
|
}
|
|
|
|
sts = license_parser_->RestoreLicenseForRelease(usage_data.drm_certificate,
|
|
key_request_, key_response_);
|
|
|
|
if (sts != NO_ERROR) {
|
|
SetErrorDetail(error_detail, sts);
|
|
return CdmResponseType(RELEASE_LICENSE_ERROR_2);
|
|
}
|
|
|
|
if (SupportsUsageTable()) {
|
|
sts = usage_table_->UpdateEntry(usage_entry_index_, crypto_session_.get(),
|
|
&usage_entry_);
|
|
if (sts != NO_ERROR) {
|
|
LOGE("Failed to update usage entry: status = %d", static_cast<int>(sts));
|
|
return sts;
|
|
}
|
|
if (!UpdateUsageInfo()) {
|
|
LOGW("Unable to save updated usage info");
|
|
}
|
|
}
|
|
|
|
license_received_ = true;
|
|
is_offline_ = false;
|
|
is_release_ = true;
|
|
return CdmResponseType(KEY_ADDED);
|
|
}
|
|
|
|
// This is a thin wrapper that initiates the latency metric.
|
|
CdmResponseType CdmSession::GenerateKeyRequest(
|
|
const InitializationData& init_data, CdmLicenseType license_type,
|
|
const CdmAppParameterMap& app_parameters, CdmKeyRequest* key_request) {
|
|
CdmResponseType result = GenerateKeyRequestInternal(
|
|
init_data, license_type, app_parameters, key_request);
|
|
// Note that GenerateReleaseRequest and GenerateRenewalRequest will initialize
|
|
// the timer themselves. This is duplicate because there are duplicate paths
|
|
// for calling GenerateReleaseRequest and GenerateRenewalRequest.
|
|
if (result == KEY_MESSAGE) {
|
|
key_request_type_ = key_request->type;
|
|
license_request_latency_.Start(); // Start or restart timer.
|
|
}
|
|
return result;
|
|
}
|
|
|
|
CdmResponseType CdmSession::GenerateKeyRequestInternal(
|
|
const InitializationData& init_data, CdmLicenseType license_type,
|
|
const CdmAppParameterMap& app_parameters, CdmKeyRequest* key_request) {
|
|
if (!initialized_) {
|
|
LOGE("CDM session not initialized");
|
|
return CdmResponseType(NOT_INITIALIZED_ERROR);
|
|
}
|
|
RETURN_STATUS_IF_NULL(key_request);
|
|
|
|
switch (license_type) {
|
|
case kLicenseTypeTemporary:
|
|
is_temporary_ = true;
|
|
is_offline_ = false;
|
|
break;
|
|
case kLicenseTypeStreaming:
|
|
is_offline_ = false;
|
|
break;
|
|
case kLicenseTypeOffline:
|
|
is_offline_ = true;
|
|
break;
|
|
case kLicenseTypeRelease:
|
|
is_release_ = true;
|
|
break;
|
|
// TODO(b/132071885): Once a license has been received, CdmSession assumes
|
|
// that a call to this method is to generate a license renewal/release
|
|
// or key rotation. Key rotation can be indicated by specifing a license
|
|
// type kLicenseTypeEmbeddedKeyData or interrogating the PSSH
|
|
// (See "else if (license_received_)" below). b/132071885 is to evaluate
|
|
// whether both mechanisms are needed.
|
|
case kLicenseTypeEmbeddedKeyData:
|
|
return license_parser_->HandleEmbeddedKeyData(init_data);
|
|
default:
|
|
LOGE("Unrecognized license type: %d", static_cast<int>(license_type));
|
|
return CdmResponseType(INVALID_LICENSE_TYPE);
|
|
}
|
|
|
|
if (is_release_) {
|
|
return GenerateReleaseRequest(key_request);
|
|
}
|
|
if (license_received_) {
|
|
// A call to GenerateKeyRequest after the initial license has been received
|
|
// is either a renewal/release request or a key rotation event
|
|
if (init_data.contains_entitled_keys()) {
|
|
key_request->message.clear();
|
|
key_request->type = kKeyRequestTypeNone;
|
|
key_request->url.clear();
|
|
return license_parser_->HandleEmbeddedKeyData(init_data);
|
|
}
|
|
return GenerateRenewalRequest(key_request);
|
|
}
|
|
// Otherwise, initialize license request.
|
|
key_request->type = kKeyRequestTypeInitial;
|
|
|
|
if (!init_data.is_supported()) {
|
|
LOGW("Unsupported init data type: %s", init_data.type().c_str());
|
|
return CdmResponseType(UNSUPPORTED_INIT_DATA);
|
|
}
|
|
if (init_data.IsEmpty() && !license_parser_->HasInitData()) {
|
|
LOGW("Init data absent");
|
|
return CdmResponseType(INIT_DATA_NOT_FOUND);
|
|
}
|
|
if (is_offline_ && key_set_id_.empty()) {
|
|
LOGE("Key set ID not set");
|
|
return CdmResponseType(KEY_REQUEST_ERROR_1);
|
|
}
|
|
// Attempt to load provisioned private key if available.
|
|
CdmResponseType status = LoadPrivateKey();
|
|
if (status != NO_ERROR) return status;
|
|
|
|
app_parameters_ = app_parameters;
|
|
status = license_parser_->PrepareKeyRequest(
|
|
init_data, drm_certificate_, license_type, app_parameters,
|
|
&key_request->message, &key_request->url);
|
|
if (status != KEY_MESSAGE) return status;
|
|
|
|
key_request_ = key_request->message;
|
|
if (is_offline_) {
|
|
offline_init_data_ = init_data.data();
|
|
offline_release_server_url_ = key_request->url;
|
|
}
|
|
return CdmResponseType(KEY_MESSAGE);
|
|
}
|
|
|
|
// This thin wrapper allows us to update metrics.
|
|
CdmResponseType CdmSession::AddKey(const CdmKeyResponse& key_response) {
|
|
CdmResponseType sts = AddKeyInternal(key_response);
|
|
UpdateRequestLatencyTiming(sts);
|
|
return sts;
|
|
}
|
|
|
|
// AddKeyInternal() - Accept license response and extract key info.
|
|
CdmResponseType CdmSession::AddKeyInternal(const CdmKeyResponse& key_response) {
|
|
if (!initialized_) {
|
|
LOGE("Not initialized");
|
|
return CdmResponseType(NOT_INITIALIZED_ERROR);
|
|
}
|
|
|
|
if (is_release_) {
|
|
const CdmResponseType sts = ReleaseKey(key_response);
|
|
return (sts == NO_ERROR) ? CdmResponseType(KEY_ADDED) : sts;
|
|
}
|
|
if (license_received_) { // renewal
|
|
return RenewKey(key_response);
|
|
}
|
|
// If usage table header+entries are supported, preprocess the license
|
|
// to see if it has a provider session token. If so a new entry needs
|
|
// to be created.
|
|
CdmResponseType sts;
|
|
std::string provider_session_token;
|
|
if (SupportsUsageTable()) {
|
|
if (license_parser_->ExtractProviderSessionToken(key_response,
|
|
&provider_session_token) &&
|
|
!provider_session_token.empty()) {
|
|
std::string app_id;
|
|
GetApplicationId(&app_id);
|
|
sts = usage_table_->AddEntry(crypto_session_.get(), is_offline_,
|
|
key_set_id_,
|
|
DeviceFiles::GetUsageInfoFileName(app_id),
|
|
key_response, &usage_entry_index_);
|
|
crypto_metrics_->usage_table_header_add_entry_.Increment(sts);
|
|
if (sts != NO_ERROR) return sts;
|
|
}
|
|
}
|
|
sts = license_parser_->HandleKeyResponse(key_response);
|
|
|
|
// Update the license sdk and service versions.
|
|
const video_widevine::VersionInfo& version_info =
|
|
license_parser_->GetServiceVersion();
|
|
metrics_->license_sdk_version_.Record(version_info.license_sdk_version());
|
|
metrics_->license_service_version_.Record(
|
|
version_info.license_service_version());
|
|
|
|
// Update or invalidate entry if usage table header+entries are supported
|
|
if (!provider_session_token.empty() && SupportsUsageTable()) {
|
|
if (sts != KEY_ADDED) {
|
|
const CdmResponseType invalidate_sts = usage_table_->InvalidateEntry(
|
|
usage_entry_index_, true, file_handle_.get(), crypto_metrics_);
|
|
crypto_metrics_->usage_table_header_delete_entry_.Increment(
|
|
invalidate_sts);
|
|
if (invalidate_sts != NO_ERROR) {
|
|
LOGW("Invalidate usage entry failed: status = %d",
|
|
static_cast<int>(invalidate_sts));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (sts != KEY_ADDED)
|
|
return (sts == KEY_ERROR) ? CdmResponseType(ADD_KEY_ERROR) : sts;
|
|
|
|
// If we are L1 or export is not supported, this call will do nothing.
|
|
sts = crypto_session_->SaveLicenseData(&exported_license_data_);
|
|
if (sts != NO_ERROR) return sts;
|
|
|
|
license_received_ = true;
|
|
key_response_ = key_response;
|
|
|
|
LOGV("Key added: provider_session_token = %s (size = %zu)",
|
|
IdToString(license_parser_->provider_session_token()),
|
|
license_parser_->provider_session_token().size());
|
|
|
|
if ((is_offline_ || has_provider_session_token()) && !is_temporary_) {
|
|
if (has_provider_session_token() && SupportsUsageTable()) {
|
|
usage_table_->UpdateEntry(usage_entry_index_, crypto_session_.get(),
|
|
&usage_entry_);
|
|
}
|
|
|
|
if (!is_offline_)
|
|
usage_provider_session_token_ = license_parser_->provider_session_token();
|
|
|
|
sts = StoreLicense();
|
|
if (sts != NO_ERROR) return sts;
|
|
}
|
|
|
|
has_license_been_loaded_ = true;
|
|
return CdmResponseType(KEY_ADDED);
|
|
}
|
|
|
|
CdmResponseType CdmSession::QueryStatus(CdmQueryMap* query_response) {
|
|
if (!initialized_) {
|
|
LOGE("CDM session not initialized");
|
|
return CdmResponseType(NOT_INITIALIZED_ERROR);
|
|
}
|
|
RETURN_STATUS_IF_NULL(query_response);
|
|
|
|
switch (security_level_) {
|
|
case kSecurityLevelL1:
|
|
(*query_response)[QUERY_KEY_SECURITY_LEVEL] =
|
|
QUERY_VALUE_SECURITY_LEVEL_L1;
|
|
break;
|
|
case kSecurityLevelL2:
|
|
(*query_response)[QUERY_KEY_SECURITY_LEVEL] =
|
|
QUERY_VALUE_SECURITY_LEVEL_L2;
|
|
break;
|
|
case kSecurityLevelL3:
|
|
(*query_response)[QUERY_KEY_SECURITY_LEVEL] =
|
|
QUERY_VALUE_SECURITY_LEVEL_L3;
|
|
break;
|
|
case kSecurityLevelUninitialized:
|
|
case kSecurityLevelUnknown:
|
|
(*query_response)[QUERY_KEY_SECURITY_LEVEL] =
|
|
QUERY_VALUE_SECURITY_LEVEL_UNKNOWN;
|
|
break;
|
|
default:
|
|
return CdmResponseType(INVALID_QUERY_KEY);
|
|
}
|
|
return CdmResponseType(NO_ERROR);
|
|
}
|
|
|
|
CdmResponseType CdmSession::SetServiceCertificate(
|
|
const std::string& service_certificate) {
|
|
return license_parser_->SetServiceCertificate(service_certificate);
|
|
}
|
|
|
|
CdmResponseType CdmSession::QueryKeyStatus(CdmQueryMap* query_response) {
|
|
return policy_engine_->Query(query_response);
|
|
}
|
|
|
|
CdmResponseType CdmSession::QueryKeyAllowedUsage(
|
|
const std::string& key_id, CdmKeyAllowedUsage* key_usage) {
|
|
return policy_engine_->QueryKeyAllowedUsage(key_id, key_usage);
|
|
}
|
|
|
|
CdmResponseType CdmSession::QueryOemCryptoSessionId(
|
|
CdmQueryMap* query_response) {
|
|
if (!initialized_) {
|
|
LOGE("CDM session not initialized");
|
|
return CdmResponseType(NOT_INITIALIZED_ERROR);
|
|
}
|
|
RETURN_STATUS_IF_NULL(query_response);
|
|
|
|
(*query_response)[QUERY_KEY_OEMCRYPTO_SESSION_ID] =
|
|
std::to_string(crypto_session_->oec_session_id());
|
|
return CdmResponseType(NO_ERROR);
|
|
}
|
|
|
|
// Decrypt() - Accept encrypted buffer and return decrypted data.
|
|
CdmResponseType CdmSession::Decrypt(const CdmDecryptionParametersV16& params) {
|
|
if (!initialized_) {
|
|
return CdmResponseType(NOT_INITIALIZED_ERROR);
|
|
}
|
|
|
|
bool is_protected = std::any_of(
|
|
std::begin(params.samples), std::end(params.samples),
|
|
[](const CdmDecryptionSample& sample) {
|
|
return std::any_of(std::begin(sample.subsamples),
|
|
std::end(sample.subsamples),
|
|
[](const CdmDecryptionSubsample& subsample) {
|
|
return subsample.protected_bytes > 0;
|
|
});
|
|
});
|
|
|
|
// Protected playback may not begin until either the start time passes or the
|
|
// license is updated, so we treat this Decrypt call as invalid.
|
|
// For the clear lead, we allow playback even if the key_id is not found or if
|
|
// the security level is not high enough yet.
|
|
if (is_protected) {
|
|
if (!policy_engine_->CanDecryptContent(params.key_id)) {
|
|
if (policy_engine_->IsLicenseForFuture())
|
|
return CdmResponseType(DECRYPT_NOT_READY);
|
|
if (!policy_engine_->IsSufficientOutputProtection(params.key_id)) {
|
|
LOGE("Key use prohibited as HDCP or resolution requirements not met");
|
|
return CdmResponseType(INSUFFICIENT_OUTPUT_PROTECTION);
|
|
}
|
|
return CdmResponseType(NEED_KEY);
|
|
}
|
|
if (!policy_engine_->CanUseKeyForSecurityLevel(params.key_id)) {
|
|
LOGE(
|
|
"Key use prohibited as security level requirements in the policy"
|
|
" not met");
|
|
return CdmResponseType(KEY_PROHIBITED_FOR_SECURITY_LEVEL);
|
|
}
|
|
}
|
|
|
|
const CdmResponseType status = crypto_session_->Decrypt(params);
|
|
|
|
if (status == NO_ERROR) {
|
|
if (is_initial_decryption_) {
|
|
is_initial_decryption_ = !policy_engine_->BeginDecryption();
|
|
}
|
|
has_decrypted_since_last_report_ = true;
|
|
if (!is_usage_update_needed_) {
|
|
is_usage_update_needed_ = has_provider_session_token();
|
|
}
|
|
last_decrypt_failed_ = false;
|
|
} else {
|
|
if (!last_decrypt_failed_) {
|
|
// Only log on the first failure. Certain failures are likely
|
|
// to occur in succession of each other.
|
|
LOGE("Decryption failed: sid = %s, status = %d", IdToString(session_id_),
|
|
static_cast<int>(status));
|
|
}
|
|
last_decrypt_failed_ = true;
|
|
wvutil::Clock clock;
|
|
const int64_t current_time = clock.GetCurrentTime();
|
|
if (policy_engine_->HasLicenseOrRentalOrPlaybackDurationExpired(
|
|
current_time)) {
|
|
return CdmResponseType(NEED_KEY);
|
|
}
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
// License renewal
|
|
// GenerateRenewalRequest() - Construct valid renewal request for the current
|
|
// session keys.
|
|
CdmResponseType CdmSession::GenerateRenewalRequest(CdmKeyRequest* key_request) {
|
|
if (!initialized_) {
|
|
LOGE("CDM session not initialized");
|
|
return CdmResponseType(NOT_INITIALIZED_ERROR);
|
|
}
|
|
RETURN_STATUS_IF_NULL(key_request);
|
|
|
|
CdmResponseType status = license_parser_->PrepareKeyUpdateRequest(
|
|
true, app_parameters_, nullptr, &key_request->message, &key_request->url);
|
|
|
|
key_request->type = kKeyRequestTypeRenewal;
|
|
|
|
if (KEY_MESSAGE != status) return status;
|
|
|
|
if (is_offline_) {
|
|
offline_key_renewal_request_ = key_request->message;
|
|
}
|
|
key_request_type_ = key_request->type;
|
|
license_request_latency_.Start(); // Start or restart timer.
|
|
return CdmResponseType(KEY_MESSAGE);
|
|
}
|
|
|
|
// RenewKey() - Accept renewal response and update key info.
|
|
CdmResponseType CdmSession::RenewKey(const CdmKeyResponse& key_response) {
|
|
if (!initialized_) {
|
|
LOGE("CDM session not initialized");
|
|
return CdmResponseType(NOT_INITIALIZED_ERROR);
|
|
}
|
|
CdmResponseType sts = license_parser_->HandleKeyUpdateResponse(
|
|
/* is renewal */ true, key_response);
|
|
|
|
// Record the timing on success.
|
|
UpdateRequestLatencyTiming(sts);
|
|
|
|
if (sts != KEY_ADDED)
|
|
return (sts == KEY_ERROR) ? CdmResponseType(RENEW_KEY_ERROR_1) : sts;
|
|
|
|
if (is_offline_) {
|
|
offline_key_renewal_response_ = key_response;
|
|
if (!StoreLicense(kLicenseStateActive, nullptr /* error_detail */))
|
|
return CdmResponseType(RENEW_KEY_ERROR_2);
|
|
}
|
|
return CdmResponseType(KEY_ADDED);
|
|
}
|
|
|
|
CdmResponseType CdmSession::GenerateReleaseRequest(CdmKeyRequest* key_request) {
|
|
if (!initialized_) {
|
|
LOGE("CDM session not initialized");
|
|
return CdmResponseType(NOT_INITIALIZED_ERROR);
|
|
}
|
|
RETURN_STATUS_IF_NULL(key_request);
|
|
is_release_ = true;
|
|
license_request_latency_.Clear();
|
|
CdmResponseType status = license_parser_->PrepareKeyUpdateRequest(
|
|
false, app_parameters_, usage_table_ == nullptr ? nullptr : this,
|
|
&key_request->message, &key_request->url);
|
|
|
|
key_request->type = kKeyRequestTypeRelease;
|
|
|
|
if (KEY_MESSAGE != status) return status;
|
|
|
|
if (has_provider_session_token() && SupportsUsageTable()) {
|
|
status = usage_table_->UpdateEntry(usage_entry_index_,
|
|
crypto_session_.get(), &usage_entry_);
|
|
|
|
if (status != NO_ERROR) {
|
|
LOGE("Update usage entry failed: status = %d", static_cast<int>(status));
|
|
return status;
|
|
}
|
|
}
|
|
|
|
if (is_offline_) { // Mark license as being released
|
|
if (!StoreLicense(kLicenseStateReleasing, nullptr))
|
|
return CdmResponseType(RELEASE_KEY_REQUEST_ERROR);
|
|
} else if (!usage_provider_session_token_.empty()) {
|
|
if (SupportsUsageTable()) {
|
|
if (!UpdateUsageInfo()) return CdmResponseType(RELEASE_USAGE_INFO_FAILED);
|
|
}
|
|
}
|
|
|
|
key_request_type_ = key_request->type;
|
|
license_request_latency_.Start(); // Start or restart timer.
|
|
|
|
return CdmResponseType(KEY_MESSAGE);
|
|
}
|
|
|
|
// ReleaseKey() - Accept release response and release license.
|
|
CdmResponseType CdmSession::ReleaseKey(const CdmKeyResponse& key_response) {
|
|
if (!initialized_) {
|
|
LOGE("CDM session not initialized");
|
|
return CdmResponseType(NOT_INITIALIZED_ERROR);
|
|
}
|
|
CdmResponseType sts = license_parser_->HandleKeyUpdateResponse(
|
|
/* is renewal */ false, key_response);
|
|
// Record the timing on success.
|
|
UpdateRequestLatencyTiming(sts);
|
|
|
|
if (sts != KEY_ADDED)
|
|
return (sts == KEY_ERROR) ? CdmResponseType(RELEASE_KEY_ERROR) : sts;
|
|
|
|
return RemoveLicense();
|
|
}
|
|
|
|
CdmResponseType CdmSession::DeleteUsageEntry(uint32_t usage_entry_index) {
|
|
if (!initialized_) {
|
|
LOGE("CDM session not initialized");
|
|
return CdmResponseType(NOT_INITIALIZED_ERROR);
|
|
}
|
|
if (!SupportsUsageTable()) {
|
|
LOGE("Cannot delete entry, usage table not supported");
|
|
return CdmResponseType(INCORRECT_USAGE_SUPPORT_TYPE_1);
|
|
}
|
|
|
|
// The usage entry cannot be deleted if it has a crypto session handling
|
|
// it, so close and reopen session.
|
|
UpdateUsageEntryInformation();
|
|
|
|
crypto_session_->Close();
|
|
CdmResponseType sts = ResetCryptoSession();
|
|
if (sts != NO_ERROR) return sts;
|
|
|
|
if (usage_table_ == nullptr) {
|
|
LOGE("Usage table header unavailable");
|
|
return CdmResponseType(INCORRECT_USAGE_SUPPORT_TYPE_1);
|
|
}
|
|
|
|
sts = usage_table_->InvalidateEntry(usage_entry_index, true,
|
|
file_handle_.get(), crypto_metrics_);
|
|
crypto_metrics_->usage_table_header_delete_entry_.Increment(sts);
|
|
return sts;
|
|
}
|
|
|
|
bool CdmSession::IsKeyLoaded(const KeyId& key_id) {
|
|
return license_parser_->IsKeyLoaded(key_id);
|
|
}
|
|
|
|
int64_t CdmSession::GetDurationRemaining() {
|
|
if (policy_engine_->IsLicenseForFuture()) return 0;
|
|
return policy_engine_->GetLicenseOrRentalOrPlaybackDurationRemaining();
|
|
}
|
|
|
|
CdmSessionId CdmSession::GenerateSessionId() {
|
|
static int session_num = 1;
|
|
return SESSION_ID_PREFIX + std::to_string(++session_num);
|
|
}
|
|
|
|
bool CdmSession::GenerateKeySetId(bool atsc_mode_enabled,
|
|
CdmKeySetId* key_set_id) {
|
|
RETURN_FALSE_IF_NULL(key_set_id);
|
|
|
|
while (key_set_id->empty()) {
|
|
constexpr size_t random_size =
|
|
(kKeySetIdLength - sizeof(KEY_SET_ID_PREFIX)) / 2;
|
|
std::string random_data = wvutil::CdmRandom::RandomData(random_size);
|
|
if (random_data.size() != random_size) {
|
|
LOGE("Error generating random id.");
|
|
return false;
|
|
}
|
|
if (atsc_mode_enabled)
|
|
*key_set_id = ATSC_KEY_SET_ID_PREFIX + wvutil::b2a_hex(random_data);
|
|
else
|
|
*key_set_id = KEY_SET_ID_PREFIX + wvutil::b2a_hex(random_data);
|
|
|
|
// key set collision
|
|
if (file_handle_->LicenseExists(*key_set_id)) {
|
|
key_set_id->clear();
|
|
}
|
|
}
|
|
// Reserve the license ID to avoid collisions.
|
|
file_handle_->ReserveLicenseId(*key_set_id);
|
|
return true;
|
|
}
|
|
|
|
CdmResponseType CdmSession::StoreLicense() {
|
|
if (is_temporary_) {
|
|
LOGE("Session type prohibits storage");
|
|
return CdmResponseType(STORAGE_PROHIBITED);
|
|
}
|
|
|
|
if (is_offline_) {
|
|
if (key_set_id_.empty()) {
|
|
LOGE("No key set ID");
|
|
return CdmResponseType(EMPTY_KEYSET_ID);
|
|
}
|
|
|
|
if (!license_parser_->is_offline()) {
|
|
LOGE("License policy prohibits storage");
|
|
return CdmResponseType(OFFLINE_LICENSE_PROHIBITED);
|
|
}
|
|
|
|
if (!StoreLicense(kLicenseStateActive, nullptr)) {
|
|
LOGE("Unable to store license");
|
|
return CdmResponseType(STORE_LICENSE_ERROR_1);
|
|
}
|
|
return CdmResponseType(NO_ERROR);
|
|
} // if (is_offline_)
|
|
|
|
std::string provider_session_token =
|
|
license_parser_->provider_session_token();
|
|
if (provider_session_token.empty()) {
|
|
LOGE("No provider session token and not offline");
|
|
return CdmResponseType(STORE_LICENSE_ERROR_2);
|
|
}
|
|
|
|
std::string app_id;
|
|
GetApplicationId(&app_id);
|
|
if (!file_handle_->StoreUsageInfo(
|
|
provider_session_token, key_request_, key_response_,
|
|
DeviceFiles::GetUsageInfoFileName(app_id), key_set_id_, usage_entry_,
|
|
usage_entry_index_, drm_certificate_, wrapped_private_key_)) {
|
|
LOGE("Unable to store usage info");
|
|
// Usage info file is corrupt. Delete current usage entry and file.
|
|
if (SupportsUsageTable()) {
|
|
DeleteUsageEntry(usage_entry_index_);
|
|
} else {
|
|
LOGW("Cannot store, usage table not supported");
|
|
}
|
|
std::vector<std::string> provider_session_tokens;
|
|
file_handle_->DeleteAllUsageInfoForApp(
|
|
DeviceFiles::GetUsageInfoFileName(app_id), &provider_session_tokens);
|
|
|
|
return CdmResponseType(STORE_USAGE_INFO_ERROR);
|
|
}
|
|
return CdmResponseType(NO_ERROR);
|
|
}
|
|
|
|
bool CdmSession::StoreLicense(CdmOfflineLicenseState state, int* error_detail) {
|
|
DeviceFiles::ResponseType error_detail_alt = DeviceFiles::kNoError;
|
|
DeviceFiles::CdmLicenseData license_data{
|
|
key_set_id_,
|
|
state,
|
|
/* pssh_data = */ offline_init_data_,
|
|
/* license_request = */ key_request_,
|
|
/* license = */ key_response_,
|
|
/* license_renewal_request = */ offline_key_renewal_request_,
|
|
/* license_renewal = */ offline_key_renewal_response_,
|
|
offline_release_server_url_,
|
|
policy_engine_->GetPlaybackStartTime(),
|
|
policy_engine_->GetLastPlaybackTime(),
|
|
policy_engine_->GetGracePeriodEndTime(),
|
|
app_parameters_,
|
|
usage_entry_,
|
|
usage_entry_index_,
|
|
drm_certificate_,
|
|
wrapped_private_key_,
|
|
exported_license_data_};
|
|
|
|
bool result = file_handle_->StoreLicense(license_data, &error_detail_alt);
|
|
if (error_detail != nullptr) {
|
|
*error_detail = static_cast<int>(error_detail_alt);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
CdmResponseType CdmSession::RemoveKeys() {
|
|
crypto_session_->Close();
|
|
return ResetCryptoSession();
|
|
}
|
|
|
|
CdmResponseType CdmSession::RemoveLicense() {
|
|
if (is_offline_ || has_provider_session_token()) {
|
|
if (has_provider_session_token() && SupportsUsageTable()) {
|
|
DeleteUsageEntry(usage_entry_index_);
|
|
}
|
|
DeleteLicenseFile();
|
|
}
|
|
return CdmResponseType(NO_ERROR);
|
|
}
|
|
|
|
bool CdmSession::DeleteLicenseFile() {
|
|
if (!is_offline_ && !has_provider_session_token()) return false;
|
|
|
|
if (is_offline_) {
|
|
return file_handle_->DeleteLicense(key_set_id_);
|
|
} else {
|
|
std::string app_id;
|
|
GetApplicationId(&app_id);
|
|
return file_handle_->DeleteUsageInfo(
|
|
DeviceFiles::GetUsageInfoFileName(app_id), key_set_id_);
|
|
}
|
|
}
|
|
|
|
void CdmSession::NotifyResolution(uint32_t width, uint32_t height) {
|
|
policy_engine_->NotifyResolution(width, height);
|
|
}
|
|
|
|
void CdmSession::OnTimerEvent(bool update_usage) {
|
|
if (update_usage && has_decrypted_since_last_report_) {
|
|
policy_engine_->DecryptionEvent();
|
|
has_decrypted_since_last_report_ = false;
|
|
if (is_offline_ && !is_release_) {
|
|
StoreLicense(kLicenseStateActive, nullptr);
|
|
}
|
|
}
|
|
policy_engine_->OnTimerEvent();
|
|
}
|
|
|
|
void CdmSession::OnKeyReleaseEvent(const CdmKeySetId& key_set_id) {
|
|
if (key_set_id_ == key_set_id) {
|
|
policy_engine_->NotifySessionExpiration();
|
|
}
|
|
}
|
|
|
|
void CdmSession::GetApplicationId(std::string* app_id) {
|
|
if (app_id && !Properties::GetApplicationId(session_id_, app_id)) {
|
|
*app_id = "";
|
|
}
|
|
}
|
|
|
|
CdmResponseType CdmSession::UpdateUsageEntryInformation() {
|
|
if (!has_provider_session_token() || !SupportsUsageTable()) {
|
|
LOGE("Unexpected state: usage_support = %s, PST present = %s, ",
|
|
SupportsUsageTable() ? "true" : "false",
|
|
has_provider_session_token() ? "yes" : "no");
|
|
return CdmResponseType(INCORRECT_USAGE_SUPPORT_TYPE_2);
|
|
}
|
|
|
|
CdmResponseType sts(NO_ERROR);
|
|
// TODO(blueeyes): Add measurements to all UpdateEntry calls in a way that
|
|
// allos us to isolate this particular use case within
|
|
// UpdateUsageEntryInformation.
|
|
M_TIME(sts = usage_table_->UpdateEntry(usage_entry_index_,
|
|
crypto_session_.get(), &usage_entry_),
|
|
crypto_metrics_, usage_table_header_update_entry_, sts);
|
|
|
|
if (sts != NO_ERROR) return sts;
|
|
|
|
if (is_offline_)
|
|
StoreLicense(is_release_ ? kLicenseStateReleasing : kLicenseStateActive,
|
|
nullptr);
|
|
else if (!usage_provider_session_token_.empty())
|
|
UpdateUsageInfo();
|
|
|
|
return CdmResponseType(NO_ERROR);
|
|
}
|
|
|
|
CdmResponseType CdmSession::GenericEncrypt(const std::string& in_buffer,
|
|
const std::string& key_id,
|
|
const std::string& iv,
|
|
CdmEncryptionAlgorithm algorithm,
|
|
std::string* out_buffer) {
|
|
RETURN_STATUS_IF_NULL(out_buffer);
|
|
CdmResponseType sts;
|
|
M_TIME(sts = crypto_session_->GenericEncrypt(in_buffer, key_id, iv, algorithm,
|
|
out_buffer),
|
|
crypto_metrics_, crypto_session_generic_encrypt_, sts,
|
|
metrics::Pow2Bucket(in_buffer.size()), algorithm);
|
|
return sts;
|
|
}
|
|
|
|
CdmResponseType CdmSession::GenericDecrypt(const std::string& in_buffer,
|
|
const std::string& key_id,
|
|
const std::string& iv,
|
|
CdmEncryptionAlgorithm algorithm,
|
|
std::string* out_buffer) {
|
|
RETURN_STATUS_IF_NULL(out_buffer);
|
|
CdmResponseType sts;
|
|
M_TIME(sts = crypto_session_->GenericDecrypt(in_buffer, key_id, iv, algorithm,
|
|
out_buffer),
|
|
crypto_metrics_, crypto_session_generic_decrypt_, sts,
|
|
metrics::Pow2Bucket(in_buffer.size()), algorithm);
|
|
return sts;
|
|
}
|
|
|
|
CdmResponseType CdmSession::GenericSign(const std::string& message,
|
|
const std::string& key_id,
|
|
CdmSigningAlgorithm algorithm,
|
|
std::string* signature) {
|
|
RETURN_STATUS_IF_NULL(signature);
|
|
CdmResponseType sts;
|
|
M_TIME(
|
|
sts = crypto_session_->GenericSign(message, key_id, algorithm, signature),
|
|
crypto_metrics_, crypto_session_generic_sign_, sts,
|
|
metrics::Pow2Bucket(message.size()), algorithm);
|
|
return sts;
|
|
}
|
|
|
|
CdmResponseType CdmSession::GenericVerify(const std::string& message,
|
|
const std::string& key_id,
|
|
CdmSigningAlgorithm algorithm,
|
|
const std::string& signature) {
|
|
CdmResponseType sts;
|
|
M_TIME(sts = crypto_session_->GenericVerify(message, key_id, algorithm,
|
|
signature),
|
|
crypto_metrics_, crypto_session_generic_verify_, sts,
|
|
metrics::Pow2Bucket(message.size()), algorithm);
|
|
return sts;
|
|
}
|
|
|
|
CdmResponseType CdmSession::SetDecryptHash(uint32_t frame_number,
|
|
const std::string& hash) {
|
|
return crypto_session_->SetDecryptHash(frame_number, hash);
|
|
}
|
|
|
|
CdmResponseType CdmSession::GetDecryptHashError(std::string* error_string) {
|
|
return crypto_session_->GetDecryptHashError(error_string);
|
|
}
|
|
|
|
bool CdmSession::UpdateUsageInfo() {
|
|
std::string app_id;
|
|
GetApplicationId(&app_id);
|
|
|
|
DeviceFiles::CdmUsageData usage_data;
|
|
usage_data.provider_session_token = usage_provider_session_token_;
|
|
usage_data.license_request = key_request_;
|
|
usage_data.license = key_response_;
|
|
usage_data.key_set_id = key_set_id_;
|
|
usage_data.usage_entry = usage_entry_;
|
|
usage_data.usage_entry_index = usage_entry_index_;
|
|
|
|
return file_handle_->UpdateUsageInfo(
|
|
DeviceFiles::GetUsageInfoFileName(app_id), usage_data);
|
|
}
|
|
|
|
void CdmSession::UpdateRequestLatencyTiming(CdmResponseType sts) {
|
|
if (sts == KEY_ADDED && license_request_latency_.IsStarted()) {
|
|
metrics_->cdm_session_license_request_latency_ms_.Record(
|
|
license_request_latency_.AsMs(), key_request_type_);
|
|
}
|
|
license_request_latency_.Clear();
|
|
}
|
|
|
|
bool CdmSession::VerifyOfflineUsageEntry() {
|
|
// Check that the current license is the same as the expected
|
|
// entry in the usage table. It is possible that the license has
|
|
// been removed from the usage table but the license file remains.
|
|
if (usage_entry_index_ >= usage_table_->size()) {
|
|
LOGD("License usage entry does not exist: entry_index = %u, size = %zu",
|
|
usage_entry_index_, usage_table_->size());
|
|
return false;
|
|
}
|
|
const CdmUsageEntryInfo& entry_info =
|
|
usage_table_->entry_info_list().at(usage_entry_index_);
|
|
if (entry_info.storage_type != kStorageLicense ||
|
|
entry_info.key_set_id != key_set_id_) {
|
|
LOGD("License usage entry does not match");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
CdmResponseType CdmSession::LoadPrivateKey() {
|
|
std::string drm_certificate;
|
|
CryptoWrappedKey private_key;
|
|
uint32_t system_id;
|
|
|
|
if (file_handle_->RetrieveCertificate(atsc_mode_enabled_, &drm_certificate,
|
|
&private_key, nullptr, &system_id) !=
|
|
DeviceFiles::kCertificateValid) {
|
|
return CdmResponseType(NEED_PROVISIONING);
|
|
}
|
|
|
|
return LoadPrivateKey(drm_certificate, private_key);
|
|
}
|
|
|
|
CdmResponseType CdmSession::LoadPrivateOrLegacyKey(
|
|
const std::string& certificate, const CryptoWrappedKey& private_key) {
|
|
// Use provided key if valid
|
|
if (!certificate.empty() && private_key.IsValid())
|
|
return LoadPrivateKey(certificate, private_key);
|
|
|
|
// Otherwise use key from legacy certificate
|
|
std::string drm_certificate;
|
|
CryptoWrappedKey wrapped_private_key;
|
|
if (file_handle_->RetrieveLegacyCertificate(
|
|
&drm_certificate, &wrapped_private_key, nullptr, nullptr) !=
|
|
DeviceFiles::kCertificateValid)
|
|
return CdmResponseType(NEED_PROVISIONING);
|
|
|
|
return LoadPrivateKey(drm_certificate, wrapped_private_key);
|
|
}
|
|
|
|
CdmResponseType CdmSession::LoadPrivateKey(
|
|
const std::string& drm_certificate, const CryptoWrappedKey& private_key) {
|
|
CdmResponseType load_cert_sts;
|
|
M_TIME(
|
|
load_cert_sts = crypto_session_->LoadCertificatePrivateKey(private_key),
|
|
crypto_metrics_, crypto_session_load_certificate_private_key_,
|
|
load_cert_sts);
|
|
|
|
switch (load_cert_sts.code()) {
|
|
case NO_ERROR:
|
|
metrics_->drm_certificate_key_type_.Record(
|
|
DrmKeyTypeToMetricValue(private_key.type()));
|
|
|
|
drm_certificate_ = drm_certificate;
|
|
wrapped_private_key_ = private_key;
|
|
return CdmResponseType(NO_ERROR);
|
|
case SESSION_LOST_STATE_ERROR:
|
|
case SYSTEM_INVALIDATED_ERROR:
|
|
return load_cert_sts;
|
|
default:
|
|
return CdmResponseType(NEED_PROVISIONING);
|
|
}
|
|
}
|
|
|
|
// Use a change in system ID as an indication that Root of Trust
|
|
// has been renewed.
|
|
bool CdmSession::HasRootOfTrustBeenRenewed(bool is_load) {
|
|
if (atsc_mode_enabled_) return false;
|
|
|
|
// Ignore System ID changes for non-Rikers L3 as the root of trust might not
|
|
// have changed even if the system ID has. Also ignore for the Rikers L3 when
|
|
// loading an existing license since we can still load it without renewing.
|
|
if (crypto_session_->GetSecurityLevel() == kSecurityLevelL3 &&
|
|
(crypto_session_->GetPreProvisionTokenType() !=
|
|
kClientTokenDrmCertificateReprovisioning ||
|
|
is_load)) {
|
|
return false;
|
|
}
|
|
|
|
std::string drm_certificate;
|
|
CryptoWrappedKey private_key;
|
|
uint32_t drm_cert_system_id;
|
|
if (file_handle_->RetrieveCertificate(
|
|
atsc_mode_enabled_, &drm_certificate, &private_key, nullptr,
|
|
&drm_cert_system_id) != DeviceFiles::kCertificateValid) {
|
|
LOGE("Failed to retrieve DRM certificate");
|
|
return true;
|
|
}
|
|
|
|
wvutil::FileSystem global_file_system;
|
|
SystemIdExtractor system_id_extractor(kLevelDefault, crypto_session_.get(),
|
|
&global_file_system);
|
|
|
|
SystemIdExtractor* extractor = &system_id_extractor;
|
|
if (mock_system_id_extractor_) {
|
|
extractor = mock_system_id_extractor_.get();
|
|
}
|
|
|
|
uint32_t system_id;
|
|
if (!extractor->ExtractSystemId(&system_id)) {
|
|
LOGW("ExtractSystemId failed");
|
|
return false;
|
|
}
|
|
|
|
if (system_id == drm_cert_system_id) return false;
|
|
|
|
LOGI(
|
|
"System Id changed from %d to %d. Removing certificates and "
|
|
"reprovisioning",
|
|
drm_cert_system_id, system_id);
|
|
file_handle_->RemoveCertificate();
|
|
return true;
|
|
}
|
|
|
|
CdmResponseType CdmSession::LoadCastPrivateKey(
|
|
const CryptoWrappedKey& private_key) {
|
|
return crypto_session_->LoadCertificatePrivateKey(private_key);
|
|
}
|
|
|
|
CdmResponseType CdmSession::GenerateRsaSignature(const std::string& message,
|
|
std::string* signature,
|
|
RSA_Padding_Scheme scheme) {
|
|
return crypto_session_->GenerateRsaSignature(message, signature, scheme);
|
|
}
|
|
|
|
CdmResponseType CdmSession::ResetCryptoSession() {
|
|
LOGD("Resetting crypto session: session_id = %s, ksid = %s",
|
|
IdToString(session_id_), IdToString(key_set_id_));
|
|
if (mock_crypto_session_in_use_) {
|
|
// If the crypto session is not reset, then there is nothing to do.
|
|
return CdmResponseType(NO_ERROR);
|
|
}
|
|
CdmResponseType sts;
|
|
crypto_session_.reset(CryptoSession::MakeCryptoSession(crypto_metrics_));
|
|
usage_table_ = nullptr;
|
|
M_TIME(sts = crypto_session_->Open(requested_security_level_),
|
|
crypto_metrics_, crypto_session_open_, sts, requested_security_level_);
|
|
|
|
CdmResponseType final_sts(NO_ERROR);
|
|
if (sts != NO_ERROR) {
|
|
// Challenging case, still need to reset other components.
|
|
LOGE("Failed to open crypto session: sts = %s", sts.ToString().c_str());
|
|
final_sts = sts;
|
|
} else {
|
|
// Reset all component dependent on the crypto session.
|
|
security_level_ = crypto_session_->GetSecurityLevel();
|
|
crypto_metrics_->crypto_session_security_level_.Record(security_level_);
|
|
|
|
if (!file_handle_->Init(security_level_)) {
|
|
LOGE("Unable to initialize file handle");
|
|
final_sts = CdmResponseType(SESSION_FILE_HANDLE_INIT_ERROR);
|
|
}
|
|
|
|
if (!file_handle_->HasCertificate(atsc_mode_enabled_)) {
|
|
LOGE("Missing certificate: atsc_mode_enabled = %s",
|
|
BoolToString(atsc_mode_enabled_));
|
|
final_sts = CdmResponseType(NEED_PROVISIONING);
|
|
}
|
|
|
|
bool has_support = false;
|
|
if (crypto_session_->HasUsageTableSupport(&has_support) && has_support) {
|
|
usage_table_ = crypto_session_->GetUsageTable();
|
|
}
|
|
}
|
|
|
|
// Even if the session is not open, other members need new session pointer.
|
|
if (mock_policy_engine_in_use_) {
|
|
// Simply pass the new pointer.
|
|
policy_engine_->set_crypto_session(crypto_session_.get());
|
|
} else {
|
|
// Attempt to maintain event listener.
|
|
WvCdmEventListener* event_listener =
|
|
policy_engine_ ? policy_engine_->event_listener() : nullptr;
|
|
policy_engine_.reset(
|
|
new PolicyEngine(session_id_, event_listener, crypto_session_.get()));
|
|
}
|
|
|
|
if (mock_license_parser_in_use_) {
|
|
license_parser_->set_crypto_session(crypto_session_.get());
|
|
license_parser_->set_policy_engine(policy_engine_.get());
|
|
} else {
|
|
license_parser_.reset(new CdmLicense(session_id_));
|
|
std::string service_certificate;
|
|
if (!Properties::GetServiceCertificate(session_id_, &service_certificate))
|
|
service_certificate.clear();
|
|
if (!license_parser_->Init(Properties::UsePrivacyMode(session_id_),
|
|
service_certificate, crypto_session_.get(),
|
|
policy_engine_.get())) {
|
|
LOGE("Failed to initialize license parser");
|
|
final_sts = CdmResponseType(LICENSE_PARSER_INIT_ERROR);
|
|
}
|
|
}
|
|
|
|
return final_sts;
|
|
}
|
|
|
|
// For testing only - takes ownership of pointers
|
|
|
|
void CdmSession::set_license_parser(CdmLicense* license_parser) {
|
|
license_parser_.reset(license_parser);
|
|
mock_license_parser_in_use_ = true;
|
|
}
|
|
|
|
void CdmSession::set_crypto_session(CryptoSession* crypto_session) {
|
|
crypto_session_.reset(crypto_session);
|
|
mock_crypto_session_in_use_ = true;
|
|
}
|
|
|
|
void CdmSession::set_policy_engine(PolicyEngine* policy_engine) {
|
|
policy_engine_.reset(policy_engine);
|
|
mock_policy_engine_in_use_ = true;
|
|
}
|
|
|
|
void CdmSession::set_file_handle(DeviceFiles* file_handle) {
|
|
file_handle_.reset(file_handle);
|
|
}
|
|
|
|
void CdmSession::set_system_id_extractor(SystemIdExtractor* extractor) {
|
|
mock_system_id_extractor_.reset(extractor);
|
|
}
|
|
} // namespace wvcdm
|