242 lines
9.1 KiB
C++
242 lines
9.1 KiB
C++
// Copyright 2022 Google LLC. All Rights Reserved. This file and proprietary
|
|
// source code may only be used and distributed under the Widevine License
|
|
// Agreement.
|
|
|
|
#include "license_holder.h"
|
|
|
|
#include "license_request.h"
|
|
#include "message_dumper.h"
|
|
#include "oec_device_features.h"
|
|
#include "test_base.h"
|
|
|
|
namespace wvcdm {
|
|
namespace {
|
|
const std::string kCencMimeType = "cenc";
|
|
} // namespace
|
|
|
|
LicenseHolder::~LicenseHolder() {}
|
|
|
|
void LicenseHolder::OpenSession() {
|
|
CdmResponseType status = cdm_engine_->OpenSession(
|
|
config_.key_system(), nullptr, &event_listener_, &session_id_);
|
|
ASSERT_EQ(NO_ERROR, status);
|
|
ASSERT_TRUE(cdm_engine_->IsOpenSession(session_id_));
|
|
}
|
|
|
|
void LicenseHolder::CloseSession() {
|
|
CdmResponseType status = cdm_engine_->CloseSession(session_id_);
|
|
ASSERT_EQ(NO_ERROR, status);
|
|
ASSERT_FALSE(cdm_engine_->IsOpenSession(session_id_));
|
|
}
|
|
|
|
void LicenseHolder::FetchLicense() {
|
|
video_widevine::WidevinePsshData pssh;
|
|
pssh.set_content_id(content_id_);
|
|
const std::string init_data_string = MakePSSH(pssh);
|
|
const InitializationData init_data(kCencMimeType, init_data_string);
|
|
init_data.DumpToLogs();
|
|
CdmKeyRequest key_request;
|
|
ASSERT_NO_FATAL_FAILURE(GenerateKeyRequest(init_data, &key_request))
|
|
<< "Failed for " << content_id();
|
|
if (config_.dump_golden_data()) {
|
|
MessageDumper::DumpLicenseRequest(key_request);
|
|
}
|
|
ASSERT_NO_FATAL_FAILURE(GetKeyResponse(key_request))
|
|
<< "Failed for " << content_id();
|
|
if (config_.dump_golden_data()) {
|
|
MessageDumper::DumpLicense(key_response_);
|
|
}
|
|
}
|
|
|
|
void LicenseHolder::LoadLicense() {
|
|
CdmLicenseType license_type;
|
|
ASSERT_EQ(KEY_ADDED, cdm_engine_->AddKey(session_id_, key_response_,
|
|
&license_type, &key_set_id_))
|
|
<< "Failed to load license for " << content_id();
|
|
if (can_persist_) {
|
|
ASSERT_EQ(license_type, kLicenseTypeOffline)
|
|
<< "Failed for " << content_id();
|
|
} else {
|
|
ASSERT_EQ(license_type, kLicenseTypeStreaming)
|
|
<< "Failed for " << content_id();
|
|
}
|
|
}
|
|
|
|
void LicenseHolder::FailLoadLicense() {
|
|
CdmLicenseType license_type;
|
|
ASSERT_NE(KEY_ADDED, cdm_engine_->AddKey(session_id_, key_response_,
|
|
&license_type, &key_set_id_))
|
|
<< "Unexpected success loading license for " << content_id();
|
|
}
|
|
|
|
void LicenseHolder::ReloadLicense() {
|
|
CdmResponseType status = cdm_engine_->RestoreKey(session_id_, key_set_id_);
|
|
ASSERT_EQ(KEY_ADDED, status)
|
|
<< "Failed to reload license for " << content_id();
|
|
}
|
|
|
|
void LicenseHolder::GenerateAndPostRenewalRequest(
|
|
const std::string& policy_id) {
|
|
event_listener_.set_renewal_needed(false);
|
|
CdmKeyRequest request;
|
|
const CdmResponseType result =
|
|
cdm_engine_->GenerateRenewalRequest(session_id_, &request);
|
|
ASSERT_EQ(KEY_MESSAGE, result) << "Failed for " << content_id();
|
|
if (config_.dump_golden_data()) {
|
|
MessageDumper::DumpRenewalRequest(request);
|
|
}
|
|
const std::string url = MakeUrl(config_.renewal_server(), policy_id);
|
|
renewal_in_flight_.reset(new UrlRequest(url));
|
|
ASSERT_TRUE(renewal_in_flight_->is_connected())
|
|
<< "Failed for " << content_id();
|
|
renewal_in_flight_->PostRequest(request.message);
|
|
}
|
|
void LicenseHolder::FetchRenewal() {
|
|
ASSERT_NE(renewal_in_flight_, nullptr) << "Failed for " << content_id();
|
|
ASSERT_NO_FATAL_FAILURE(
|
|
renewal_in_flight_->AssertOkResponse(&renewal_response_))
|
|
<< "Renewal failed for " << content_id();
|
|
}
|
|
|
|
void LicenseHolder::LoadRenewal() {
|
|
LicenseRequest license_request;
|
|
license_request.GetDrmMessage(renewal_response_, renewal_message_);
|
|
if (config_.dump_golden_data()) {
|
|
MessageDumper::DumpRenewal(renewal_message_);
|
|
}
|
|
EXPECT_EQ(KEY_ADDED, cdm_engine_->RenewKey(session_id_, renewal_message_))
|
|
<< "Failed for " << content_id();
|
|
}
|
|
|
|
void LicenseHolder::RemoveLicense() {
|
|
EXPECT_EQ(NO_ERROR, cdm_engine_->RemoveLicense(session_id_))
|
|
<< "Failed for " << content_id();
|
|
}
|
|
|
|
CdmResponseType LicenseHolder::Decrypt(const std::string& key_id) {
|
|
constexpr size_t buffer_size = 500;
|
|
const std::vector<uint8_t> input(buffer_size, 0);
|
|
std::vector<uint8_t> output(buffer_size, 0);
|
|
const std::vector<uint8_t> iv(KEY_IV_SIZE, 0);
|
|
CdmDecryptionParametersV16 params(key_id);
|
|
params.is_secure = false;
|
|
CdmDecryptionSample sample(input.data(), output.data(), 0, input.size(), iv);
|
|
CdmDecryptionSubsample subsample(0, input.size());
|
|
sample.subsamples.push_back(subsample);
|
|
params.samples.push_back(sample);
|
|
return cdm_engine_->DecryptV16(session_id_, params);
|
|
}
|
|
|
|
void LicenseHolder::DecryptSecure(const KeyId& key_id) {
|
|
ASSERT_TRUE(wvoec::global_features.test_secure_buffers);
|
|
constexpr size_t buffer_size = 500;
|
|
const std::vector<uint8_t> input(buffer_size, 0);
|
|
const std::vector<uint8_t> iv(KEY_IV_SIZE, 0);
|
|
|
|
// To create a secure buffer, we need to know the OEMCrypto session id.
|
|
CdmQueryMap query_map;
|
|
cdm_engine_->QueryOemCryptoSessionId(session_id_, &query_map);
|
|
const std::string oec_session_id_string =
|
|
query_map[QUERY_KEY_OEMCRYPTO_SESSION_ID];
|
|
uint32_t oec_session_id = std::stoi(oec_session_id_string);
|
|
|
|
int secure_buffer_fid;
|
|
OEMCrypto_DestBufferDesc output_descriptor;
|
|
output_descriptor.type = OEMCrypto_BufferType_Secure;
|
|
output_descriptor.buffer.secure.secure_buffer_length = buffer_size;
|
|
ASSERT_EQ(
|
|
OEMCrypto_AllocateSecureBuffer(oec_session_id, buffer_size,
|
|
&output_descriptor, &secure_buffer_fid),
|
|
OEMCrypto_SUCCESS);
|
|
|
|
ASSERT_NE(output_descriptor.buffer.secure.secure_buffer, nullptr);
|
|
// It is OK if OEMCrypto changes the maximum size, but there must
|
|
// still be enough room for our data.
|
|
ASSERT_GE(output_descriptor.buffer.secure.secure_buffer_length, buffer_size);
|
|
output_descriptor.buffer.secure.offset = 0;
|
|
|
|
// Now create a sample array for the CDM layer.
|
|
CdmDecryptionParametersV16 params(key_id);
|
|
params.is_secure = true;
|
|
CdmDecryptionSample sample(input.data(),
|
|
output_descriptor.buffer.secure.secure_buffer, 0,
|
|
input.size(), iv);
|
|
CdmDecryptionSubsample subsample(0, input.size());
|
|
sample.subsamples.push_back(subsample);
|
|
params.samples.push_back(sample);
|
|
CdmResponseType status = cdm_engine_->DecryptV16(session_id_, params);
|
|
|
|
// Free the secure buffer before we check the return status.
|
|
EXPECT_EQ(OEMCrypto_FreeSecureBuffer(oec_session_id, &output_descriptor,
|
|
secure_buffer_fid),
|
|
OEMCrypto_SUCCESS);
|
|
|
|
ASSERT_EQ(status, NO_ERROR);
|
|
}
|
|
|
|
void LicenseHolder::FailDecrypt(const KeyId& key_id,
|
|
CdmResponseEnum expected_status) {
|
|
CdmResponseType status = Decrypt(key_id);
|
|
// If the server knows we cannot handle the key, it would not have given us
|
|
// the key. In that case, the status should indicate no key.
|
|
if (status != NEED_KEY) {
|
|
// Otherwise, we should have gotten the expected error.
|
|
ASSERT_EQ(expected_status, status);
|
|
}
|
|
}
|
|
|
|
void LicenseHolder::GenerateKeyRequest(const InitializationData& init_data,
|
|
CdmKeyRequest* key_request) {
|
|
CdmAppParameterMap empty_app_parameters;
|
|
CdmKeySetId empty_key_set_id;
|
|
CdmLicenseType license_type =
|
|
can_persist_ ? kLicenseTypeOffline : kLicenseTypeStreaming;
|
|
CdmResponseType result = cdm_engine_->GenerateKeyRequest(
|
|
session_id_, empty_key_set_id, init_data, license_type,
|
|
empty_app_parameters, key_request);
|
|
ASSERT_EQ(KEY_MESSAGE, result);
|
|
ASSERT_EQ(kKeyRequestTypeInitial, key_request->type);
|
|
}
|
|
|
|
std::string LicenseHolder::MakeUrl(const std::string& server_url,
|
|
const std::string& policy_id) {
|
|
// For tests, we want to specify the policy, but the UAT server only allows us
|
|
// to set the content id as the video_id. So each policy is matched to a
|
|
// single license with the same name. The local license server, on the other
|
|
// hand, wants to see the policy id in the url. So we have to guess which
|
|
// format to use based on the name of the server.
|
|
const std::string path = server_url + config_.client_auth();
|
|
std::string video_query;
|
|
if (!policy_id.empty()) {
|
|
if (path.find("proxy.uat") != std::string::npos) {
|
|
// This is uat or uat-nightly. Set the video_id.
|
|
video_query = "video_id=" + policy_id;
|
|
} else {
|
|
// This is probably a local license server. Set the policy.
|
|
video_query = "policy=" + policy_id;
|
|
}
|
|
// If there is already a parameter, then we don't need to add another
|
|
// question mark.
|
|
return path + ((path.find('?') == std::string::npos) ? '?' : '&') +
|
|
video_query;
|
|
} else {
|
|
return path;
|
|
}
|
|
}
|
|
|
|
void LicenseHolder::GetKeyResponse(const CdmKeyRequest& key_request) {
|
|
// The content id matches the policy id used on UAT.
|
|
const std::string url = MakeUrl(config_.license_server(), "");
|
|
UrlRequest url_request(url);
|
|
ASSERT_TRUE(url_request.is_connected());
|
|
|
|
std::string http_response;
|
|
url_request.PostRequest(key_request.message);
|
|
ASSERT_NO_FATAL_FAILURE(url_request.AssertOkResponse(&http_response))
|
|
<< "Failed for " << content_id();
|
|
LicenseRequest license_request;
|
|
license_request.GetDrmMessage(http_response, key_response_);
|
|
}
|
|
|
|
} // namespace wvcdm
|