2418 lines
87 KiB
C++
2418 lines
87 KiB
C++
// Copyright 2018 Google LLC. All Rights Reserved. This file and proprietary
|
|
// source code may only be used and distributed under the Widevine Master
|
|
// License Agreement.
|
|
//
|
|
// This source file provides a basic set of unit tests for the Content
|
|
// Decryption Module (CDM).
|
|
|
|
#include <memory>
|
|
|
|
#include <gmock/gmock.h>
|
|
#include <gtest/gtest.h>
|
|
|
|
#include "OEMCryptoCENC.h"
|
|
#include "cdm.h"
|
|
#include "cdm_test_printers.h"
|
|
#include "config_test_env.h"
|
|
#include "decryption_test_data.h"
|
|
#include "license_protocol.pb.h"
|
|
#include "license_request.h"
|
|
#include "log.h"
|
|
#include "oec_device_features.h"
|
|
#include "platform.h"
|
|
#include "properties_ce.h"
|
|
#include "service_certificate.h"
|
|
#include "string_conversions.h"
|
|
#include "test_base.h"
|
|
#include "test_host.h"
|
|
#include "test_printers.h"
|
|
#include "url_request.h"
|
|
|
|
using namespace testing;
|
|
using namespace wvcdm;
|
|
|
|
namespace widevine {
|
|
|
|
using video_widevine::LicenseError;
|
|
using video_widevine::SignedMessage;
|
|
|
|
namespace {
|
|
|
|
const int kHttpOk = 200;
|
|
|
|
const int kRenewalTestDelayMs = 3 * 60 * 1000;
|
|
const int kExpirationTestDelayMs = 5 * 60 * 1000;
|
|
const int kOfflineLicenseDurationMs = 604800 * 1000;
|
|
|
|
const std::string kDeviceCertFileName = "cert.bin";
|
|
|
|
const Cdm::SessionType kBogusSessionType = static_cast<Cdm::SessionType>(-1);
|
|
const Cdm::InitDataType kBogusInitDataType = static_cast<Cdm::InitDataType>(-1);
|
|
const std::string kBogusSessionId = "asdf";
|
|
const std::string kBogusServiceCertificate = "jkl;";
|
|
|
|
const std::string kCencInitData = a2bs_hex(
|
|
"00000042" // blob size
|
|
"70737368" // "pssh"
|
|
"00000000" // flags
|
|
"edef8ba979d64acea3c827dcd51d21ed" // Widevine system id
|
|
"00000022" // pssh data size
|
|
// pssh data:
|
|
"08011a0d7769646576696e655f746573"
|
|
"74220f73747265616d696e675f636c69"
|
|
"7039");
|
|
const std::string kCencPersistentInitData = a2bs_hex(
|
|
"00000040" // blob size
|
|
"70737368" // "pssh"
|
|
"00000000" // flags
|
|
"edef8ba979d64acea3c827dcd51d21ed" // Widevine system id
|
|
"00000020" // pssh data size
|
|
// pssh data:
|
|
"08011a0d7769646576696e655f746573"
|
|
"74220d6f66666c696e655f636c697036");
|
|
const std::string kInvalidCencInitData = a2bs_hex(
|
|
"0000000c" // blob size
|
|
"61736466" // "asdf" (wrong box type)
|
|
"01020304"); // nonsense
|
|
const std::string kNonWidevineCencInitData = a2bs_hex(
|
|
"00000020" // blob size
|
|
"70737368" // "pssh"
|
|
"00000000" // flags
|
|
"000102030405060708090a0b0c0d0e0f" // unknown system id
|
|
"00000000"); // pssh data size
|
|
const std::string kWebMInitData = a2bs_hex("deadbeefdeadbeefdeadbeefdeadbeef");
|
|
const std::string kKeyIdsInitData =
|
|
"{\"kids\":[\"67ef0gd8pvfd0\",\"77ef0gd8pvfd0\"]}";
|
|
const std::string kHlsInitData =
|
|
"#EXT-X-KEY:METHOD=SAMPLE-AES,KEYFORMAT=\"com.widevine\",KEYFORMATVERSIONS="
|
|
"\"1\",URI=\"data:text/plain;base64,ew0KICAgInByb3ZpZGVyIjogIndpZGV2aW5lX3R"
|
|
"lc3QiLA0KICAgImNvbnRlbnRfaWQiOiAiWW1sblluVmphMkoxYm01NSIsDQogICAia2V5X2lkc"
|
|
"yI6IFsNCiAgICAgICI5Yjc1OTA0MDMyMWE0MDhhNWM3NzY4YjQ1MTEyODdhNiINCiAgIF0NCn0"
|
|
"=\",IV=0x75537a79fa41abc7b598ea72aba0c26f";
|
|
const std::string kCencEntitlementInitData1 = a2bs_hex(
|
|
"000001fb" // blob size
|
|
"70737368" // "pssh"
|
|
"00000000" // flags
|
|
"edef8ba979d64acea3c827dcd51d21ed" // Widevine system id
|
|
"000001db" // pssh data size
|
|
// pssh data:
|
|
"220b47726f7570563254657374381448"
|
|
"e3dc959b065002580272580a10668093"
|
|
"381a8c5be48a0168ce372726ac1210c8"
|
|
"326486bb5d5c4a958f00b1111afc811a"
|
|
"20082cd9d3aed3ebe6239d30fbcf0b22"
|
|
"1d28cbb0360ea1295c2363973346ec00"
|
|
"512210914781334e864c8eb7f768cf26"
|
|
"49073872580a10f872d11d5b1052f2bd"
|
|
"a94e60a0e383021210450897c987a85c"
|
|
"2e9579f968554a12991a2097e603ceea"
|
|
"f35ed8cef1029eae7a0a54701e3d6db6"
|
|
"80e7da1de3b22a8db347fb2210b41c34"
|
|
"29b7bb96972bbaf6587bc0ddf172580a"
|
|
"10bac58b9fce9e5929a42a180e529f19"
|
|
"4712103f11f22988d25659b145ce4854"
|
|
"3e6b141a20416e22768e5a57b08d155e"
|
|
"5210d00658056947ff06d626668bceb3"
|
|
"5eb01c6b57221081fb2ff3fef79d332f"
|
|
"f98be46233596972580a101261c8036d"
|
|
"ae5c8caa968858aa0ca9cc12106d583c"
|
|
"b37c1456519843a81cf49912221a20c2"
|
|
"1116bb54a226e8d879a4cd41d8879920"
|
|
"2ae85b80d83b1b4447e5d7fcad6f6a22"
|
|
"100b27a4c3f44771d2b0c7c34c66af35"
|
|
"b572580a10ab1c8c259c6b5967991389"
|
|
"65bff5ac0c1210b5b4473658565d3786"
|
|
"efaf4b85d8e6e21a203ce6a9085285c2"
|
|
"ece0b650dc83dd7aa8ac849611a8e3f8"
|
|
"3c8f389223c0f3621522101946f0c2a3"
|
|
"d543101cc842bbec2d0b30");
|
|
const std::string kCencEntitlementInitData2 = a2bs_hex(
|
|
"000001fb" // blob size
|
|
"70737368" // "pssh"
|
|
"00000000" // flags
|
|
"edef8ba979d64acea3c827dcd51d21ed" // Widevine system id
|
|
"000001db" // pssh data size
|
|
// pssh data:
|
|
"220b47726f7570563254657374381548"
|
|
"e3dc959b065002580272580a10668093"
|
|
"381a8c5be48a0168ce372726ac1210f8"
|
|
"488775a99855ff94b93ec5bd4993561a"
|
|
"20d15ba631c20e95da0d4857f6a1d25a"
|
|
"a3bccbd3fde18b3fdc1dd8c4f0ede76f"
|
|
"402210d6dd3675f0d1150052e81b9107"
|
|
"6d7fc172580a10f872d11d5b1052f2bd"
|
|
"a94e60a0e383021210ad1f93ad921e53"
|
|
"b097c415b2bf1ef1c61a20b2087b60a2"
|
|
"d253ac2158a1bfa789b150b79701b29e"
|
|
"c852a2662560f8b8977a4c2210051ed3"
|
|
"2628671fbda58f506ba5ea713972580a"
|
|
"10bac58b9fce9e5929a42a180e529f19"
|
|
"47121027cdda7bfe5e5fd4bff2ebc9c7"
|
|
"c020701a20f2cb1184d648a2404517e6"
|
|
"7a39d698332aae6bb890a69bf7ddb536"
|
|
"75b8ac41c62210a80ed7f9b728fdd566"
|
|
"0b01b173ace26372580a101261c8036d"
|
|
"ae5c8caa968858aa0ca9cc1210769a70"
|
|
"0442a25bf5ae17174c70f4cb8e1a206c"
|
|
"7b2012723fc47c83b003ea214204915f"
|
|
"9a63dc373bf219f36ccf5697589aa422"
|
|
"10bcc3c16e836cca264d5493a0c334d3"
|
|
"4872580a10ab1c8c259c6b5967991389"
|
|
"65bff5ac0c1210894b04aef78557c6a7"
|
|
"e6e8855febbcc91a2025cc545ee3cd0c"
|
|
"c323586610ff6a8f8f22a78f5fade2f2"
|
|
"1083f152c52208f16d2210257aacacec"
|
|
"512a2e769396b10e6d9dfa");
|
|
|
|
// This Key ID must match the key retrieved from the license server by
|
|
// kCencInitData.
|
|
const std::vector<uint8_t> kKeyIdCtr =
|
|
a2b_hex("371ea35e1a985d75d198a7f41020dc23");
|
|
// This Key ID must match the key retrieved from the license server by
|
|
// kHlsInitData.
|
|
const std::vector<uint8_t> kKeyIdCbc =
|
|
a2b_hex("9b759040321a408a5c7768b4511287a6");
|
|
// This Key ID must match a key embedded in kCencEntitlementInitData1.
|
|
const std::vector<uint8_t> kKeyIdEntitlement1 =
|
|
a2b_hex("c8326486bb5d5c4a958f00b1111afc81");
|
|
// This Key ID must match a key embedded in kCencEntitlementInitData2.
|
|
const std::vector<uint8_t> kKeyIdEntitlement2 =
|
|
a2b_hex("f8488775a99855ff94b93ec5bd499356");
|
|
|
|
// A default pattern object disables patterns during decryption.
|
|
const Cdm::Pattern kPatternNone;
|
|
// The recommended pattern from CENC 3.0, which is also the pattern used by
|
|
// HLS. Encrypts 1 in every 10 crypto blocks.
|
|
const Cdm::Pattern kPatternRecommended(1, 9);
|
|
// The recommended pattern for HLS Audio, which should be decrypted in CENC 3.0
|
|
// cbcs mode despite not using patterns. This pattern disables patterned
|
|
// decryption by having one encrypted block and no clear blocks.
|
|
const Cdm::Pattern kPatternHlsAudio(1, 0);
|
|
|
|
const std::string kValue = "A Value";
|
|
const std::string kNewValue = "A New Value";
|
|
|
|
const std::string kParamName = "PARAM";
|
|
const std::string kParamName2 = "PARAM2";
|
|
|
|
class CdmTest : public WvCdmTestBase, public Cdm::IEventListener {
|
|
public:
|
|
CdmTest() {}
|
|
~CdmTest() override {}
|
|
|
|
// IEventListener mocks:
|
|
MOCK_METHOD3(onMessage,
|
|
void(const std::string& session_id,
|
|
Cdm::MessageType message_type, const std::string& message));
|
|
MOCK_METHOD2(onKeyStatusesChange,
|
|
void(const std::string& session_id, bool has_new_usable_key));
|
|
MOCK_METHOD1(onRemoveComplete, void(const std::string& session_id));
|
|
|
|
protected:
|
|
void SetUp() override {
|
|
WvCdmTestBase::SetUp();
|
|
|
|
// Clear anything stored, load default device cert.
|
|
g_host->Reset();
|
|
|
|
// Reinit the library.
|
|
Cdm::Status status = Cdm::initialize(
|
|
Cdm::kNoSecureOutput, PropertiesCE::GetClientInfo(), g_host, g_host,
|
|
g_host, static_cast<Cdm::LogLevel>(g_cutoff), g_sandbox_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
|
|
// Make a fresh CDM.
|
|
RecreateCdm(true /* privacy_mode */);
|
|
}
|
|
|
|
void TearDown() override {}
|
|
|
|
void RecreateCdm(bool privacy_mode) {
|
|
CreateAdditionalCdm(privacy_mode, &cdm_);
|
|
cdm_->setServiceCertificate(Cdm::kProvisioningService,
|
|
config_.provisioning_service_certificate());
|
|
cdm_->setServiceCertificate(Cdm::kLicensingService,
|
|
config_.license_service_certificate());
|
|
}
|
|
|
|
void CreateAdditionalCdm(bool privacy_mode, std::unique_ptr<Cdm>* cdm) {
|
|
cdm->reset(Cdm::create(this, g_host, privacy_mode));
|
|
ASSERT_NE((Cdm*)0, cdm->get());
|
|
}
|
|
|
|
bool Fetch(const std::string& url, const std::string& message,
|
|
std::string* response, int* status_code) {
|
|
UrlRequest url_request(url);
|
|
EXPECT_TRUE(url_request.is_connected());
|
|
if (!url_request.is_connected()) {
|
|
return false;
|
|
}
|
|
|
|
url_request.PostRequest(message);
|
|
std::string http_response;
|
|
url_request.GetResponse(&http_response);
|
|
|
|
// Some license servers return 400 for invalid message, some
|
|
// return 500; treat anything other than 200 as an invalid message.
|
|
int http_status_code = url_request.GetStatusCode(http_response);
|
|
if (status_code) {
|
|
*status_code = http_status_code;
|
|
}
|
|
|
|
if (response) {
|
|
if (http_status_code == kHttpOk) {
|
|
// Parse out HTTP and server headers and return the body only.
|
|
std::string reply_body;
|
|
LicenseRequest lic_request;
|
|
lic_request.GetDrmMessage(http_response, reply_body);
|
|
*response = reply_body;
|
|
} else {
|
|
*response = http_response;
|
|
}
|
|
LOGV("Reply body(hex): \n%s\n", b2a_hex(*response).c_str());
|
|
LOGV("Reply body(b64): \n%s\n",
|
|
Base64SafeEncode(
|
|
std::vector<uint8_t>(response->begin(), response->end()))
|
|
.c_str());
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void FetchCertificate(const std::string& url, std::string* response) {
|
|
int status_code;
|
|
bool ok = Fetch(url, "", response, &status_code);
|
|
ASSERT_TRUE(ok);
|
|
if (ok)
|
|
ASSERT_EQ(kHttpOk, status_code) << "Error response: " << *response << "\n"
|
|
<< "url: " << url;
|
|
}
|
|
|
|
bool FetchServiceCertificate(const std::string& url, std::string* response) {
|
|
std::string request;
|
|
int status;
|
|
if (!ServiceCertificate::GetRequest(&request)) {
|
|
LOGE("FAILED to generate service certificate request.");
|
|
return false;
|
|
}
|
|
if (!Fetch(url, request, response, &status)) {
|
|
LOGE("FAILED to get service certificate response: sts=%d", status);
|
|
return false;
|
|
}
|
|
LOGV("Reply body(hex): \n%s\n", b2a_hex(*response).c_str());
|
|
LOGV("Reply body(b64): \n%s\n",
|
|
Base64SafeEncode(
|
|
std::vector<uint8_t>(response->begin(), response->end()))
|
|
.c_str());
|
|
return true;
|
|
}
|
|
|
|
void FetchLicense(const std::string& license_server,
|
|
const std::string& message, std::string* response) {
|
|
int status_code;
|
|
bool ok = Fetch(license_server, message, response, &status_code);
|
|
ASSERT_TRUE(ok);
|
|
if (ok)
|
|
ASSERT_EQ(kHttpOk, status_code) << "Error response: " << *response << "\n"
|
|
<< "license_server: " << license_server;
|
|
}
|
|
|
|
void FetchLicenseFailure(const std::string& message,
|
|
int expected_status_code) {
|
|
int status_code;
|
|
bool ok = Fetch(config_.license_server(), message, nullptr, &status_code);
|
|
ASSERT_TRUE(ok);
|
|
if (ok) ASSERT_EQ(expected_status_code, status_code);
|
|
}
|
|
|
|
void CreateSessionAndGenerateRequest(Cdm::SessionType session_type,
|
|
Cdm::InitDataType init_data_type,
|
|
std::string* session_id,
|
|
std::string* message) {
|
|
Cdm::Status status = cdm_->createSession(session_type, session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
|
|
std::string init_data;
|
|
if (session_type == Cdm::kTemporary) {
|
|
if (init_data_type == Cdm::kCenc) {
|
|
init_data = kCencInitData;
|
|
} else if (init_data_type == Cdm::kHls) {
|
|
init_data = kHlsInitData;
|
|
}
|
|
} else if (session_type == Cdm::kPersistentLicense ||
|
|
session_type == Cdm::kPersistentUsageRecord) {
|
|
if (init_data_type == Cdm::kCenc) {
|
|
init_data = kCencPersistentInitData;
|
|
}
|
|
}
|
|
ASSERT_FALSE(init_data.empty());
|
|
|
|
EXPECT_CALL(*this, onMessage(*session_id, Cdm::kLicenseRequest, _))
|
|
.WillOnce(SaveArg<2>(message));
|
|
status = generateRequestWithRetry(*session_id, init_data_type, init_data);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
}
|
|
|
|
void CreateSessionAndFetchLicense(Cdm::SessionType session_type,
|
|
Cdm::InitDataType init_data_type,
|
|
std::string* session_id,
|
|
std::string* response) {
|
|
std::string message;
|
|
ASSERT_NO_FATAL_FAILURE(CreateSessionAndGenerateRequest(
|
|
session_type, init_data_type, session_id, &message));
|
|
|
|
std::string license_server;
|
|
if (init_data_type == Cdm::kCenc) {
|
|
license_server = config_.license_server();
|
|
} else if (init_data_type == Cdm::kHls) {
|
|
license_server = config_.license_server();
|
|
}
|
|
ASSERT_FALSE(license_server.empty());
|
|
FetchLicense(license_server, message, response);
|
|
}
|
|
|
|
void CreateSessionAndUpdate(Cdm::SessionType session_type,
|
|
Cdm::InitDataType init_data_type,
|
|
std::string* session_id) {
|
|
std::string response;
|
|
ASSERT_NO_FATAL_FAILURE(CreateSessionAndFetchLicense(
|
|
session_type, init_data_type, session_id, &response));
|
|
EXPECT_CALL(*this, onKeyStatusesChange(*session_id, true));
|
|
Cdm::Status status = updateWithRetry(*session_id, response);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
}
|
|
|
|
void FetchLicenseAndUpdate(const std::string& session_id,
|
|
const std::string& message) {
|
|
// Acquire a license.
|
|
std::string response;
|
|
ASSERT_NO_FATAL_FAILURE(
|
|
FetchLicense(config_.license_server(), message, &response));
|
|
|
|
// This license should be accepted, but the keys are not expected to change.
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(0);
|
|
Cdm::Status status = updateWithRetry(session_id, response);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
}
|
|
|
|
std::string GetProvisioningResponse(const std::string& message,
|
|
size_t max_attempts = 10) {
|
|
std::string uri = config_.provisioning_server();
|
|
|
|
LOGV("GetProvisioningResponse: URI: %s", uri.c_str());
|
|
LOGV("GetProvisioningResponse: message:\n%s\n", b2a_hex(message).c_str());
|
|
|
|
std::string reply;
|
|
uri += "&signedRequest=" + message;
|
|
// TODO(b/139361531): Remove loop once provisioning service is stable.
|
|
for (size_t attempt = 1; attempt <= max_attempts; attempt++) {
|
|
FetchCertificate(uri, &reply);
|
|
if (HasFatalFailure()) {
|
|
LOGE("Failed to get provisioning response: attempt = %zu", attempt);
|
|
reply.clear();
|
|
continue;
|
|
} else {
|
|
LOGV("GetProvisioningResponse: response:\n%s\n", reply.c_str());
|
|
break;
|
|
}
|
|
}
|
|
return reply;
|
|
}
|
|
|
|
// This calls cdm->update, and retries several times if there is a
|
|
// failure due to nonce flood errors.
|
|
Cdm::Status updateWithRetry(const std::string& session_id,
|
|
const std::string& response) {
|
|
const int num_retries = 5;
|
|
for (int i = 0; i < num_retries; i++) {
|
|
Cdm::Status status = cdm_->update(session_id, response);
|
|
if (status == Cdm::kQuotaExceeded) {
|
|
sleep(1);
|
|
continue;
|
|
}
|
|
return status;
|
|
}
|
|
return Cdm::kQuotaExceeded;
|
|
}
|
|
|
|
// This calls cdm->generateRequest, and retries several times if there is a
|
|
// failure due to nonce flood errors.
|
|
Cdm::Status generateRequestWithRetry(const std::string& session_id,
|
|
Cdm::InitDataType init_data_type,
|
|
const std::string& init_data) {
|
|
const int num_retries = 5;
|
|
for (int i = 0; i < num_retries; i++) {
|
|
LOGD("attempt %d", i);
|
|
Cdm::Status status =
|
|
cdm_->generateRequest(session_id, init_data_type, init_data);
|
|
if (status == Cdm::kQuotaExceeded) {
|
|
sleep(1);
|
|
continue;
|
|
}
|
|
return status;
|
|
}
|
|
return Cdm::kQuotaExceeded;
|
|
}
|
|
|
|
std::unique_ptr<Cdm> cdm_;
|
|
};
|
|
|
|
struct DecryptParam {
|
|
public:
|
|
DecryptParam(const std::string& short_name_param,
|
|
Cdm::InitDataType init_data_type_param,
|
|
const std::vector<uint8_t>& key_id_param,
|
|
const uint8_t* iv_param, size_t iv_size_param,
|
|
Cdm::EncryptionScheme scheme_param,
|
|
const Cdm::Pattern& pattern_param, const uint8_t* input_param,
|
|
size_t input_size_param, const uint8_t* output_param,
|
|
size_t output_size_param)
|
|
: short_name(short_name_param),
|
|
init_data_type(init_data_type_param),
|
|
key_id(&key_id_param),
|
|
iv(iv_param),
|
|
iv_size(iv_size_param),
|
|
scheme(scheme_param),
|
|
pattern(&pattern_param),
|
|
input(input_param),
|
|
input_size(input_size_param),
|
|
output(output_param),
|
|
output_size(output_size_param) {}
|
|
|
|
const std::string short_name;
|
|
const Cdm::InitDataType init_data_type;
|
|
const std::vector<uint8_t>* const key_id;
|
|
const uint8_t* const iv;
|
|
const size_t iv_size;
|
|
const Cdm::EncryptionScheme scheme;
|
|
const Cdm::Pattern* const pattern;
|
|
const uint8_t* const input;
|
|
const size_t input_size;
|
|
const uint8_t* const output;
|
|
const size_t output_size;
|
|
};
|
|
|
|
void PrintTo(const DecryptParam& value, ::std::ostream* os) {
|
|
*os << value.short_name << " DecryptParam";
|
|
}
|
|
|
|
class CdmTestWithDecryptParam : public CdmTest,
|
|
public WithParamInterface<DecryptParam> {};
|
|
|
|
class CdmTestWithRemoveParam : public CdmTest,
|
|
public WithParamInterface<bool> {};
|
|
|
|
class MockTimerClient : public Cdm::ITimer::IClient {
|
|
public:
|
|
MockTimerClient() {}
|
|
~MockTimerClient() override {}
|
|
|
|
MOCK_METHOD1(onTimerExpired, void(void*));
|
|
};
|
|
|
|
} // namespace
|
|
|
|
TEST_F(CdmTest, TestHostTimer) {
|
|
// Validate that the TestHost timers are processed in the correct order.
|
|
const int64_t kTimerDelayMs = 1000;
|
|
void* kCtx1 = reinterpret_cast<void*>(0x1);
|
|
void* kCtx2 = reinterpret_cast<void*>(0x2);
|
|
|
|
MockTimerClient client;
|
|
|
|
g_host->setTimeout(kTimerDelayMs * 1, &client, kCtx1);
|
|
g_host->setTimeout(kTimerDelayMs * 2, &client, kCtx2);
|
|
|
|
EXPECT_CALL(client, onTimerExpired(kCtx1));
|
|
g_host->ElapseTime(kTimerDelayMs);
|
|
Mock::VerifyAndClear(&client);
|
|
|
|
EXPECT_CALL(client, onTimerExpired(kCtx2));
|
|
g_host->ElapseTime(kTimerDelayMs);
|
|
Mock::VerifyAndClear(&client);
|
|
|
|
EXPECT_CALL(client, onTimerExpired(_)).Times(0);
|
|
g_host->ElapseTime(kTimerDelayMs);
|
|
Mock::VerifyAndClear(&client);
|
|
}
|
|
|
|
TEST_F(CdmTest, Initialize) {
|
|
// Try with an invalid output type.
|
|
Cdm::Status status =
|
|
Cdm::initialize(static_cast<Cdm::SecureOutputType>(-1),
|
|
PropertiesCE::GetClientInfo(), g_host, g_host, g_host,
|
|
static_cast<Cdm::LogLevel>(g_cutoff), g_sandbox_id);
|
|
EXPECT_EQ(Cdm::kTypeError, status);
|
|
|
|
// Try with various client info properties missing.
|
|
Cdm::ClientInfo working_client_info = PropertiesCE::GetClientInfo();
|
|
Cdm::ClientInfo broken_client_info;
|
|
|
|
broken_client_info = working_client_info;
|
|
broken_client_info.product_name.clear();
|
|
status = Cdm::initialize(Cdm::kNoSecureOutput, broken_client_info, g_host,
|
|
g_host, g_host, static_cast<Cdm::LogLevel>(g_cutoff),
|
|
g_sandbox_id);
|
|
EXPECT_EQ(Cdm::kTypeError, status);
|
|
|
|
broken_client_info = working_client_info;
|
|
broken_client_info.company_name.clear();
|
|
status = Cdm::initialize(Cdm::kNoSecureOutput, broken_client_info, g_host,
|
|
g_host, g_host, static_cast<Cdm::LogLevel>(g_cutoff),
|
|
g_sandbox_id);
|
|
EXPECT_EQ(Cdm::kTypeError, status);
|
|
|
|
broken_client_info = working_client_info;
|
|
broken_client_info.device_name.clear(); // Not required
|
|
status = Cdm::initialize(Cdm::kNoSecureOutput, broken_client_info, g_host,
|
|
g_host, g_host, static_cast<Cdm::LogLevel>(g_cutoff),
|
|
g_sandbox_id);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
|
|
broken_client_info = working_client_info;
|
|
broken_client_info.model_name.clear();
|
|
status = Cdm::initialize(Cdm::kNoSecureOutput, broken_client_info, g_host,
|
|
g_host, g_host, static_cast<Cdm::LogLevel>(g_cutoff),
|
|
g_sandbox_id);
|
|
EXPECT_EQ(Cdm::kTypeError, status);
|
|
|
|
broken_client_info = working_client_info;
|
|
broken_client_info.arch_name.clear(); // Not required
|
|
status = Cdm::initialize(Cdm::kNoSecureOutput, broken_client_info, g_host,
|
|
g_host, g_host, static_cast<Cdm::LogLevel>(g_cutoff),
|
|
g_sandbox_id);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
|
|
broken_client_info = working_client_info;
|
|
broken_client_info.build_info.clear(); // Not required
|
|
status = Cdm::initialize(Cdm::kNoSecureOutput, broken_client_info, g_host,
|
|
g_host, g_host, static_cast<Cdm::LogLevel>(g_cutoff),
|
|
g_sandbox_id);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
|
|
// Try with various host interfaces missing.
|
|
status = Cdm::initialize(Cdm::kNoSecureOutput, working_client_info, nullptr,
|
|
g_host, g_host, static_cast<Cdm::LogLevel>(g_cutoff),
|
|
g_sandbox_id);
|
|
EXPECT_EQ(Cdm::kTypeError, status);
|
|
|
|
status = Cdm::initialize(Cdm::kNoSecureOutput, working_client_info, g_host,
|
|
nullptr, g_host,
|
|
static_cast<Cdm::LogLevel>(g_cutoff), g_sandbox_id);
|
|
EXPECT_EQ(Cdm::kTypeError, status);
|
|
|
|
status = Cdm::initialize(Cdm::kNoSecureOutput, working_client_info, g_host,
|
|
g_host, nullptr,
|
|
static_cast<Cdm::LogLevel>(g_cutoff), g_sandbox_id);
|
|
EXPECT_EQ(Cdm::kTypeError, status);
|
|
|
|
// Try all output types.
|
|
status = Cdm::initialize(Cdm::kDirectRender, working_client_info, g_host,
|
|
g_host, g_host, static_cast<Cdm::LogLevel>(g_cutoff),
|
|
g_sandbox_id);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
|
|
status = Cdm::initialize(Cdm::kOpaqueHandle, working_client_info, g_host,
|
|
g_host, g_host, static_cast<Cdm::LogLevel>(g_cutoff),
|
|
g_sandbox_id);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
|
|
// One last init with everything correct and working.
|
|
status = Cdm::initialize(Cdm::kNoSecureOutput, working_client_info, g_host,
|
|
g_host, g_host, static_cast<Cdm::LogLevel>(g_cutoff),
|
|
g_sandbox_id);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
}
|
|
|
|
TEST_F(CdmTest, GetServiceCertificateRequest) {
|
|
// Set a server certificate with privacy mode disabled - should work.
|
|
ASSERT_NO_FATAL_FAILURE(RecreateCdm(false /* privacy_mode */));
|
|
std::string message;
|
|
Cdm::Status status = cdm_->getServiceCertificateRequest(&message);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
}
|
|
|
|
TEST_F(CdmTest, ParseAndLoadServiceCertificateResponse) {
|
|
ASSERT_NO_FATAL_FAILURE(RecreateCdm(true /* privacy_mode */));
|
|
std::string message;
|
|
ASSERT_EQ(cdm_->getServiceCertificateRequest(&message), Cdm::kSuccess);
|
|
|
|
ConfigTestEnv uat_config(kContentProtectionUatServer);
|
|
std::string response;
|
|
int code;
|
|
ASSERT_TRUE(Fetch(uat_config.license_server(), message, &response, &code));
|
|
ASSERT_EQ(code, 200);
|
|
|
|
std::string cert;
|
|
EXPECT_EQ(cdm_->parseAndLoadServiceCertificateResponse(Cdm::kAllServices,
|
|
response, &cert),
|
|
Cdm::kSuccess);
|
|
EXPECT_EQ(cdm_->parseAndLoadServiceCertificateResponse(Cdm::kLicensingService,
|
|
response, &cert),
|
|
Cdm::kSuccess);
|
|
// Can pass NULL and ignore the parsed certificate.
|
|
EXPECT_EQ(cdm_->parseAndLoadServiceCertificateResponse(Cdm::kLicensingService,
|
|
response, nullptr),
|
|
Cdm::kSuccess);
|
|
|
|
EXPECT_EQ(cdm_->parseAndLoadServiceCertificateResponse(
|
|
Cdm::kLicensingService, kBogusServiceCertificate, &cert),
|
|
Cdm::kTypeError);
|
|
}
|
|
|
|
TEST_F(CdmTest, ServiceCertificateRequestResponseUat) {
|
|
ConfigTestEnv uat_config(kContentProtectionUatServer);
|
|
|
|
std::string response;
|
|
ASSERT_TRUE(FetchServiceCertificate(uat_config.license_server(), &response));
|
|
LOGV("response size=%d", response.size());
|
|
#if 0 // enable to extract the service certificate in byte form
|
|
size_t done = 0;
|
|
while (done < response.size()) {
|
|
for (int i = 0; i < 12; i++) {
|
|
if (done >= response.size()) {
|
|
break;
|
|
}
|
|
uint32_t x = static_cast<uint8_t>(response.data()[done]);
|
|
printf("0x%02x, ", x);
|
|
done++;
|
|
}
|
|
printf ("\n");
|
|
}
|
|
#endif
|
|
}
|
|
|
|
TEST_F(CdmTest, ServiceCertificateRequestResponseStaging) {
|
|
ConfigTestEnv staging_config(kContentProtectionStagingServer);
|
|
|
|
std::string response;
|
|
ASSERT_TRUE(
|
|
FetchServiceCertificate(staging_config.license_server(), &response));
|
|
LOGV("response size=%d", response.size());
|
|
#if 0 // enable to extract the service certificate in byte form
|
|
size_t done = 0;
|
|
while (done < response.size()) {
|
|
for (int i = 0; i < 12; i++) {
|
|
if (done >= response.size()) {
|
|
break;
|
|
}
|
|
uint32_t x = static_cast<uint8_t>(response.data()[done]);
|
|
printf("0x%02x, ", x);
|
|
done++;
|
|
}
|
|
printf ("\n");
|
|
}
|
|
#endif
|
|
}
|
|
|
|
TEST_F(CdmTest, SetServiceCertificate) {
|
|
// Set a server certificate with privacy mode disabled - should work.
|
|
ASSERT_NO_FATAL_FAILURE(RecreateCdm(false /* privacy_mode */));
|
|
Cdm::Status status = cdm_->setServiceCertificate(
|
|
Cdm::kAllServices, config_.license_service_certificate());
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
status = cdm_->setServiceCertificate(
|
|
Cdm::kProvisioningService, config_.provisioning_service_certificate());
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
status = cdm_->setServiceCertificate(Cdm::kLicensingService,
|
|
config_.license_service_certificate());
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
|
|
// Can set a server certificate if privacy mode is enabled.
|
|
ASSERT_NO_FATAL_FAILURE(RecreateCdm(true /* privacy_mode */));
|
|
status = cdm_->setServiceCertificate(Cdm::kAllServices,
|
|
config_.license_service_certificate());
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
status = cdm_->setServiceCertificate(
|
|
Cdm::kProvisioningService, config_.provisioning_service_certificate());
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
status = cdm_->setServiceCertificate(Cdm::kLicensingService,
|
|
config_.license_service_certificate());
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
|
|
// It is invalid to set a malformed cert.
|
|
status =
|
|
cdm_->setServiceCertificate(Cdm::kAllServices, kBogusServiceCertificate);
|
|
EXPECT_EQ(Cdm::kTypeError, status);
|
|
status = cdm_->setServiceCertificate(Cdm::kProvisioningService,
|
|
kBogusServiceCertificate);
|
|
EXPECT_EQ(Cdm::kTypeError, status);
|
|
status = cdm_->setServiceCertificate(Cdm::kLicensingService,
|
|
kBogusServiceCertificate);
|
|
EXPECT_EQ(Cdm::kTypeError, status);
|
|
}
|
|
|
|
TEST_F(CdmTest, OpenSessionWithoutServiceCertificate) {
|
|
// Create a CDM instance that does not have any service certificates
|
|
// installed.
|
|
ASSERT_NO_FATAL_FAILURE(CreateAdditionalCdm(true /* privacy_mode */, &cdm_));
|
|
EnsureProvisioned();
|
|
|
|
// Verify that sessions can be opened.
|
|
std::string session_id;
|
|
ASSERT_EQ(Cdm::kSuccess, cdm_->createSession(Cdm::kTemporary, &session_id));
|
|
|
|
// License request generation, however, should fail.
|
|
EXPECT_CALL(*this, onMessage(session_id, _, _)).Times(0);
|
|
EXPECT_EQ(Cdm::kNeedsServiceCertificate,
|
|
generateRequestWithRetry(session_id, Cdm::kCenc, kCencInitData));
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Once a service certificate has been set, the existing session should be
|
|
// able to generate a request.
|
|
ASSERT_EQ(Cdm::kSuccess,
|
|
cdm_->setServiceCertificate(Cdm::kLicensingService,
|
|
config_.license_service_certificate()));
|
|
|
|
EXPECT_CALL(*this, onMessage(session_id, _, _)).Times(AtLeast(1));
|
|
EXPECT_EQ(Cdm::kSuccess,
|
|
generateRequestWithRetry(session_id, Cdm::kCenc, kCencInitData));
|
|
Mock::VerifyAndClear(this);
|
|
}
|
|
|
|
TEST_F(CdmTest, GetRobustnessLevel) {
|
|
Cdm::RobustnessLevel level;
|
|
Cdm::Status status = cdm_->getRobustnessLevel(&level);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
LOGI("Got robustness level %d", level);
|
|
}
|
|
|
|
TEST_F(CdmTest, GetResourceRatingTier) {
|
|
uint32_t tier;
|
|
Cdm::Status status = cdm_->getResourceRatingTier(&tier);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
LOGI("Got resource rating tier %lu", tier);
|
|
}
|
|
|
|
TEST_F(CdmTest, GetOemCryptoBuildInfo) {
|
|
std::string build_info;
|
|
Cdm::Status status = cdm_->getOemCryptoBuildInfo(&build_info);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
LOGI("Got OEMCrypto build info: %s", build_info.c_str());
|
|
}
|
|
|
|
TEST_F(CdmTest, CreateSession) {
|
|
EnsureProvisioned();
|
|
|
|
// Create a temporary session.
|
|
std::string session_id;
|
|
Cdm::Status status = cdm_->createSession(Cdm::kTemporary, &session_id);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
EXPECT_FALSE(session_id.empty());
|
|
|
|
// Create another using the same pointer to an already-filled-out string,
|
|
// and expect the session ID to change.
|
|
std::string original_session_id = session_id;
|
|
status = cdm_->createSession(Cdm::kTemporary, &session_id);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
EXPECT_NE(original_session_id, session_id);
|
|
|
|
// Create a persistent session.
|
|
status = cdm_->createSession(Cdm::kPersistentLicense, &session_id);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
|
|
// Try a NULL pointer for session ID.
|
|
status = cdm_->createSession(Cdm::kTemporary, nullptr);
|
|
EXPECT_EQ(Cdm::kTypeError, status);
|
|
|
|
// Try a bogus session type.
|
|
status = cdm_->createSession(kBogusSessionType, &session_id);
|
|
EXPECT_EQ(Cdm::kNotSupported, status);
|
|
}
|
|
|
|
TEST_F(CdmTest, GenerateRequest) {
|
|
EnsureProvisioned();
|
|
std::string session_id;
|
|
Cdm::Status status = cdm_->createSession(Cdm::kTemporary, &session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
|
|
// Generate a license request for CENC.
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _));
|
|
status = generateRequestWithRetry(session_id, Cdm::kCenc, kCencInitData);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Can't call generateRequest more than once on a session.
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _)).Times(0);
|
|
status = generateRequestWithRetry(session_id, Cdm::kCenc, kCencInitData);
|
|
EXPECT_EQ(Cdm::kInvalidState, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Create a new session and generate a license request for WebM.
|
|
status = cdm_->createSession(Cdm::kTemporary, &session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _));
|
|
status = generateRequestWithRetry(session_id, Cdm::kWebM, kWebMInitData);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Create a new session and try the as-yet-unsupported key-ids format.
|
|
status = cdm_->createSession(Cdm::kTemporary, &session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _)).Times(0);
|
|
status = generateRequestWithRetry(session_id, Cdm::kKeyIds, kKeyIdsInitData);
|
|
EXPECT_EQ(Cdm::kNotSupported, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Create a new session and generate a license request for HLS.
|
|
status = cdm_->createSession(Cdm::kTemporary, &session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _));
|
|
status = generateRequestWithRetry(session_id, Cdm::kHls, kHlsInitData);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Create a new session and try a bogus init data type.
|
|
status = cdm_->createSession(Cdm::kTemporary, &session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _)).Times(0);
|
|
status = generateRequestWithRetry(session_id, kBogusInitDataType, "asdf");
|
|
EXPECT_EQ(Cdm::kTypeError, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// This same session should still be usable with a supported init data type
|
|
// after failing with an unsupported or bogus type.
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _));
|
|
status = generateRequestWithRetry(session_id, Cdm::kCenc, kCencInitData);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Create a new session and try to pass empty init data.
|
|
status = cdm_->createSession(Cdm::kTemporary, &session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _)).Times(0);
|
|
status = generateRequestWithRetry(session_id, Cdm::kCenc, "");
|
|
EXPECT_EQ(Cdm::kTypeError, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Try to pass invalid CENC init data.
|
|
status = cdm_->createSession(Cdm::kTemporary, &session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _)).Times(0);
|
|
status =
|
|
generateRequestWithRetry(session_id, Cdm::kCenc, kInvalidCencInitData);
|
|
EXPECT_EQ(Cdm::kNotSupported, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Try to pass non-Widevine CENC init data.
|
|
status = cdm_->createSession(Cdm::kTemporary, &session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _)).Times(0);
|
|
status = generateRequestWithRetry(session_id, Cdm::kCenc,
|
|
kNonWidevineCencInitData);
|
|
EXPECT_EQ(Cdm::kNotSupported, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Try a bogus session ID.
|
|
EXPECT_CALL(*this, onMessage(_, _, _)).Times(0);
|
|
status = generateRequestWithRetry(kBogusSessionId, Cdm::kCenc, kCencInitData);
|
|
EXPECT_EQ(Cdm::kSessionNotFound, status);
|
|
Mock::VerifyAndClear(this);
|
|
}
|
|
|
|
TEST_F(CdmTest, Update) {
|
|
EnsureProvisioned();
|
|
std::string session_id;
|
|
std::string message;
|
|
ASSERT_NO_FATAL_FAILURE(CreateSessionAndGenerateRequest(
|
|
Cdm::kTemporary, Cdm::kCenc, &session_id, &message));
|
|
|
|
// Acquire a license.
|
|
std::string response;
|
|
ASSERT_NO_FATAL_FAILURE(
|
|
FetchLicense(config_.license_server(), message, &response));
|
|
|
|
// Update the session.
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, true));
|
|
Cdm::Status status = updateWithRetry(session_id, response);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Try updating a bogus session ID.
|
|
status = updateWithRetry(kBogusSessionId, response);
|
|
EXPECT_EQ(Cdm::kSessionNotFound, status);
|
|
|
|
// Try updating with an empty response.
|
|
status = updateWithRetry(session_id, "");
|
|
EXPECT_EQ(Cdm::kTypeError, status);
|
|
|
|
// Try updating with a rejected device certificate.
|
|
{
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, true)).Times(0);
|
|
|
|
LicenseError license_error;
|
|
std::string error_msg;
|
|
SignedMessage signed_message;
|
|
signed_message.set_type(SignedMessage::ERROR_RESPONSE);
|
|
std::string error_response;
|
|
|
|
// Invalid device certificate
|
|
license_error.set_error_code(LicenseError::INVALID_DRM_DEVICE_CERTIFICATE);
|
|
license_error.SerializeToString(&error_msg);
|
|
signed_message.set_msg(error_msg);
|
|
signed_message.SerializeToString(&error_response);
|
|
status = updateWithRetry(session_id, error_response);
|
|
EXPECT_EQ(Cdm::kNeedsDeviceCertificate, status);
|
|
|
|
// Revoked device certificate
|
|
license_error.set_error_code(LicenseError::REVOKED_DRM_DEVICE_CERTIFICATE);
|
|
license_error.SerializeToString(&error_msg);
|
|
signed_message.set_msg(error_msg);
|
|
signed_message.SerializeToString(&error_response);
|
|
status = updateWithRetry(session_id, error_response);
|
|
EXPECT_EQ(Cdm::kUnexpectedError, status);
|
|
|
|
Mock::VerifyAndClear(this);
|
|
}
|
|
|
|
// Create a new session and try updating before generating a request.
|
|
status = cdm_->createSession(Cdm::kTemporary, &session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
status = updateWithRetry(session_id, response);
|
|
ASSERT_EQ(Cdm::kInvalidState, status);
|
|
}
|
|
|
|
TEST_F(CdmTest, Close) {
|
|
EnsureProvisioned();
|
|
// Create a temporary session.
|
|
std::string session_id;
|
|
Cdm::Status status = cdm_->createSession(Cdm::kTemporary, &session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
|
|
// Close it.
|
|
status = cdm_->close(session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
|
|
// Can't generate a license request after close.
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _)).Times(0);
|
|
status = generateRequestWithRetry(session_id, Cdm::kCenc, kCencInitData);
|
|
EXPECT_EQ(Cdm::kSessionNotFound, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Try to close the same session again.
|
|
status = cdm_->close(session_id);
|
|
EXPECT_EQ(Cdm::kSessionNotFound, status);
|
|
|
|
// Try to close a bogus session.
|
|
status = cdm_->close(kBogusSessionId);
|
|
EXPECT_EQ(Cdm::kSessionNotFound, status);
|
|
}
|
|
|
|
TEST_F(CdmTest, LoadTemporary) {
|
|
EnsureProvisioned();
|
|
std::string session_id;
|
|
std::string response;
|
|
ASSERT_NO_FATAL_FAILURE(CreateSessionAndFetchLicense(
|
|
Cdm::kTemporary, Cdm::kCenc, &session_id, &response));
|
|
|
|
// Update the temporary session.
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, true));
|
|
Cdm::Status status = updateWithRetry(session_id, response);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Close the session.
|
|
status = cdm_->close(session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
|
|
// Can't load a temporary session.
|
|
status = cdm_->load(session_id);
|
|
EXPECT_EQ(Cdm::kSessionNotFound, status);
|
|
}
|
|
|
|
TEST_F(CdmTest, LoadPersistent) {
|
|
EnsureProvisioned();
|
|
std::string session_id;
|
|
std::string response;
|
|
|
|
ASSERT_NO_FATAL_FAILURE(CreateSessionAndFetchLicense(
|
|
Cdm::kPersistentLicense, Cdm::kCenc, &session_id, &response));
|
|
|
|
// Update the persistent session.
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, true));
|
|
Cdm::Status status = updateWithRetry(session_id, response);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Should be able to load the session again after closing it.
|
|
status = cdm_->close(session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, true));
|
|
status = cdm_->load(session_id);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Should be able to load the session again after recreating the CDM.
|
|
ASSERT_NO_FATAL_FAILURE(RecreateCdm(true /* privacy_mode */));
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, true));
|
|
status = cdm_->load(session_id);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Should not be able to load the session again clearing storage.
|
|
status = cdm_->close(session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
g_host->Reset();
|
|
EnsureProvisioned();
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(0);
|
|
status = cdm_->load(session_id);
|
|
EXPECT_EQ(Cdm::kSessionNotFound, status);
|
|
Mock::VerifyAndClear(this);
|
|
}
|
|
|
|
TEST_F(CdmTest, LoadWillFireExpiration) {
|
|
EnsureProvisioned();
|
|
// There was a bug where calling load() would not start the PolicyEngine timer
|
|
// because it was only started in update().
|
|
std::string session_id;
|
|
ASSERT_NO_FATAL_FAILURE(
|
|
CreateSessionAndUpdate(Cdm::kPersistentLicense, Cdm::kCenc, &session_id));
|
|
|
|
// Should be able to load the session again after recreating the CDM.
|
|
ASSERT_NO_FATAL_FAILURE(RecreateCdm(true /* privacy_mode */));
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, true));
|
|
ASSERT_EQ(Cdm::kSuccess, cdm_->load(session_id));
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Let the key expire, make sure we get a key status update.
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, false));
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRenewal, _))
|
|
.Times(AtLeast(1));
|
|
g_host->ElapseTime(kOfflineLicenseDurationMs);
|
|
Mock::VerifyAndClear(this);
|
|
}
|
|
|
|
TEST_F(CdmTest, PerOriginLoadPersistent) {
|
|
EnsureProvisioned();
|
|
std::string session_id;
|
|
std::string response;
|
|
Cdm::Status status;
|
|
|
|
ASSERT_NO_FATAL_FAILURE(CreateSessionAndFetchLicense(
|
|
Cdm::kPersistentLicense, Cdm::kCenc, &session_id, &response));
|
|
|
|
// Update and close the persistent session.
|
|
{
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(AtLeast(1));
|
|
|
|
status = updateWithRetry(session_id, response);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
status = cdm_->close(session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
|
|
Mock::VerifyAndClear(this);
|
|
}
|
|
|
|
// Should be able to load the session again after recreating the CDM.
|
|
{
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(AtLeast(1));
|
|
|
|
ASSERT_NO_FATAL_FAILURE(RecreateCdm(true /* privacy_mode */));
|
|
status = cdm_->load(session_id);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
|
|
Mock::VerifyAndClear(this);
|
|
}
|
|
|
|
// Create a second CDM using a different origin's storage and verify that it
|
|
// cannot load the session from above.
|
|
{
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(0);
|
|
|
|
// Create another host to use its storage. This will simulate another
|
|
// origin.
|
|
TestHost other_host;
|
|
|
|
// Create a new CDM that uses the new host and new storage.
|
|
std::unique_ptr<Cdm> other_cdm(
|
|
Cdm::create(this, &other_host, /* privacy_mode */ true));
|
|
ASSERT_TRUE(other_cdm.get());
|
|
status = other_cdm->setServiceCertificate(
|
|
Cdm::kLicensingService, config_.license_service_certificate());
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
|
|
// Should not be able to use the provisioning of another origin.
|
|
status = other_cdm->load(session_id);
|
|
EXPECT_EQ(Cdm::kNeedsDeviceCertificate, status);
|
|
|
|
// Copy the old provisioning to the new storage so we don't get provisioning
|
|
// errors anymore.
|
|
std::string cert_bin;
|
|
g_host->read(kDeviceCertFileName, &cert_bin);
|
|
other_host.write(kDeviceCertFileName, cert_bin);
|
|
|
|
// Should not be able to see sessions from another origin.
|
|
status = other_cdm->load(session_id);
|
|
EXPECT_EQ(Cdm::kSessionNotFound, status);
|
|
|
|
Mock::VerifyAndClear(this);
|
|
}
|
|
}
|
|
|
|
TEST_F(CdmTest, LoadUsageRecord) {
|
|
EnsureProvisioned();
|
|
std::string session_id;
|
|
std::string response;
|
|
|
|
ASSERT_NO_FATAL_FAILURE(CreateSessionAndFetchLicense(
|
|
Cdm::kPersistentUsageRecord, Cdm::kCenc, &session_id, &response));
|
|
|
|
// Update the session.
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, true));
|
|
Cdm::Status status = updateWithRetry(session_id, response);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Should be able to load the session again after closing it.
|
|
status = cdm_->close(session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
|
|
// There should be no usable keys after loading this session.
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(0);
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRelease, _)).Times(0);
|
|
status = cdm_->load(session_id);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Should be able to load the session again after recreating the CDM.
|
|
ASSERT_NO_FATAL_FAILURE(RecreateCdm(true /* privacy_mode */));
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(0);
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRelease, _)).Times(0);
|
|
status = cdm_->load(session_id);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Should not be able to load the session again clearing storage.
|
|
status = cdm_->close(session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
g_host->Reset();
|
|
EnsureProvisioned();
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(0);
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRelease, _)).Times(0);
|
|
status = cdm_->load(session_id);
|
|
EXPECT_EQ(Cdm::kSessionNotFound, status);
|
|
Mock::VerifyAndClear(this);
|
|
}
|
|
|
|
TEST_F(CdmTest, DestroyUsageRecord) {
|
|
EnsureProvisioned();
|
|
std::string session_id;
|
|
std::string response;
|
|
ASSERT_NO_FATAL_FAILURE(CreateSessionAndFetchLicense(
|
|
Cdm::kPersistentUsageRecord, Cdm::kCenc, &session_id, &response));
|
|
|
|
// Update the session.
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, true));
|
|
Cdm::Status status = updateWithRetry(session_id, response);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Should be able to load the session again after closing it.
|
|
status = cdm_->close(session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
|
|
// There should be no usable keys after loading this session.
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(0);
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRelease, _)).Times(0);
|
|
status = cdm_->load(session_id);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Should be able to load the session again after recreating the CDM.
|
|
ASSERT_NO_FATAL_FAILURE(RecreateCdm(true /* privacy_mode */));
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(0);
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRelease, _)).Times(0);
|
|
status = cdm_->deleteUsageRecord(session_id);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
status = cdm_->load(session_id);
|
|
EXPECT_EQ(Cdm::kSessionNotFound, status);
|
|
Mock::VerifyAndClear(this);
|
|
}
|
|
|
|
TEST_F(CdmTest, DestroyAllUsageRecords) {
|
|
EnsureProvisioned();
|
|
std::string session_id;
|
|
std::string response;
|
|
ASSERT_NO_FATAL_FAILURE(CreateSessionAndFetchLicense(
|
|
Cdm::kPersistentUsageRecord, Cdm::kCenc, &session_id, &response));
|
|
|
|
// Update the session.
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, true));
|
|
Cdm::Status status = updateWithRetry(session_id, response);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Should be able to load the session again after closing it.
|
|
status = cdm_->close(session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
|
|
// There should be no usable keys after loading this session.
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(0);
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRelease, _)).Times(0);
|
|
status = cdm_->load(session_id);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Should be able to load the session again after recreating the CDM.
|
|
ASSERT_NO_FATAL_FAILURE(RecreateCdm(true /* privacy_mode */));
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(0);
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRelease, _)).Times(0);
|
|
status = cdm_->deleteAllUsageRecords();
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
status = cdm_->load(session_id);
|
|
EXPECT_EQ(Cdm::kSessionNotFound, status);
|
|
Mock::VerifyAndClear(this);
|
|
}
|
|
|
|
TEST_F(CdmTest, ListUsageRecords) {
|
|
EnsureProvisioned();
|
|
std::string session_id;
|
|
std::string response;
|
|
ASSERT_NO_FATAL_FAILURE(CreateSessionAndFetchLicense(
|
|
Cdm::kPersistentUsageRecord, Cdm::kCenc, &session_id, &response));
|
|
|
|
// Update the session.
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, true));
|
|
Cdm::Status status = updateWithRetry(session_id, response);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Should be able to load the session again after closing it.
|
|
status = cdm_->close(session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
|
|
// There should be no usable keys after loading this session.
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(0);
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRelease, _)).Times(0);
|
|
status = cdm_->load(session_id);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Should be able to load the session again after recreating the CDM.
|
|
ASSERT_NO_FATAL_FAILURE(RecreateCdm(true /* privacy_mode */));
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(0);
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRelease, _)).Times(0);
|
|
|
|
std::vector<std::string> ksids;
|
|
status = cdm_->listUsageRecords(&ksids);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
EXPECT_EQ(ksids.size(), 1UL);
|
|
if (ksids.size() > 0UL) {
|
|
EXPECT_TRUE(ksids[0] == session_id);
|
|
}
|
|
status = cdm_->load(session_id);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
}
|
|
|
|
TEST_F(CdmTest, LoadBogus) {
|
|
EnsureProvisioned();
|
|
EXPECT_CALL(*this, onKeyStatusesChange(_, _)).Times(0);
|
|
Cdm::Status status = cdm_->load(kBogusSessionId);
|
|
EXPECT_EQ(Cdm::kSessionNotFound, status);
|
|
}
|
|
|
|
TEST_F(CdmTest, GetKeyStatuses) {
|
|
EnsureProvisioned();
|
|
std::string session_id;
|
|
ASSERT_NO_FATAL_FAILURE(
|
|
CreateSessionAndUpdate(Cdm::kTemporary, Cdm::kCenc, &session_id));
|
|
|
|
// We should be able to query status and see a usable key.
|
|
Cdm::KeyStatusMap map;
|
|
Cdm::Status status = cdm_->getKeyStatuses(session_id, &map);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
EXPECT_EQ(Cdm::kUsable, map.begin()->second);
|
|
|
|
// The key ID should be the one we are expecting.
|
|
const std::string expected_key_id(
|
|
reinterpret_cast<const char*>(kKeyIdCtr.data()), kKeyIdCtr.size());
|
|
EXPECT_EQ(expected_key_id, map.begin()->first);
|
|
|
|
// Let the key expire.
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, false));
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRenewal, _))
|
|
.Times(AtLeast(1));
|
|
g_host->ElapseTime(kExpirationTestDelayMs);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// We should see expiration reflected in the map.
|
|
status = cdm_->getKeyStatuses(session_id, &map);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
EXPECT_EQ(Cdm::kExpired, map.begin()->second);
|
|
|
|
// We can't get status after closing a session.
|
|
status = cdm_->close(session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
status = cdm_->getKeyStatuses(session_id, &map);
|
|
ASSERT_EQ(Cdm::kSessionNotFound, status);
|
|
}
|
|
|
|
TEST_F(CdmTest, GetExpiration) {
|
|
EnsureProvisioned();
|
|
std::string session_id;
|
|
ASSERT_NO_FATAL_FAILURE(
|
|
CreateSessionAndUpdate(Cdm::kTemporary, Cdm::kCenc, &session_id));
|
|
|
|
// We should be able to query expiration and get a value in the future.
|
|
int64_t expiration;
|
|
Cdm::Status status = cdm_->getExpiration(session_id, &expiration);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
EXPECT_GT(expiration, g_host->now());
|
|
int64_t original_expiration = expiration;
|
|
|
|
// Let the key expire.
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, false));
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRenewal, _))
|
|
.Times(AtLeast(1));
|
|
g_host->ElapseTime(kExpirationTestDelayMs);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// We should see expiration in the past now.
|
|
status = cdm_->getExpiration(session_id, &expiration);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
EXPECT_LE(expiration, g_host->now());
|
|
// Expiration should not have changed.
|
|
EXPECT_EQ(original_expiration, expiration);
|
|
|
|
// We can't get expiration after closing a session.
|
|
status = cdm_->close(session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
status = cdm_->getExpiration(session_id, &expiration);
|
|
ASSERT_EQ(Cdm::kSessionNotFound, status);
|
|
}
|
|
|
|
TEST_P(CdmTestWithRemoveParam, Remove) {
|
|
const bool intermediate_close = GetParam();
|
|
|
|
EnsureProvisioned();
|
|
std::string session_id;
|
|
ASSERT_NO_FATAL_FAILURE(
|
|
CreateSessionAndUpdate(Cdm::kPersistentLicense, Cdm::kCenc, &session_id));
|
|
|
|
// Remove the session. This causes a release message to be generated.
|
|
std::string message;
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, false));
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRelease, _))
|
|
.WillOnce(SaveArg<2>(&message));
|
|
Cdm::Status status = cdm_->remove(session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// The keys should already be unusable.
|
|
Cdm::KeyStatusMap map;
|
|
status = cdm_->getKeyStatuses(session_id, &map);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
EXPECT_EQ(Cdm::kReleased, map.begin()->second);
|
|
|
|
// If we are testing intermediate closing, close the session.
|
|
if (intermediate_close) {
|
|
status = cdm_->close(session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
}
|
|
|
|
// Post the release message to the license server.
|
|
std::string response;
|
|
ASSERT_NO_FATAL_FAILURE(
|
|
FetchLicense(config_.license_server(), message, &response));
|
|
|
|
// If we are testing intermediate closing, reopen the session.
|
|
if (intermediate_close) {
|
|
status = cdm_->load(session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
}
|
|
|
|
// Update the session.
|
|
EXPECT_CALL(*this, onRemoveComplete(session_id));
|
|
status = updateWithRetry(session_id, response);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// The session is now completely gone.
|
|
status = cdm_->close(session_id);
|
|
ASSERT_EQ(Cdm::kSessionNotFound, status);
|
|
status = cdm_->load(session_id);
|
|
ASSERT_EQ(Cdm::kSessionNotFound, status);
|
|
|
|
// Try a bogus session ID.
|
|
status = cdm_->remove(kBogusSessionId);
|
|
EXPECT_EQ(Cdm::kSessionNotFound, status);
|
|
|
|
// Try a new session.
|
|
status = cdm_->createSession(Cdm::kPersistentLicense, &session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
status = cdm_->remove(session_id);
|
|
EXPECT_EQ(Cdm::kInvalidState, status);
|
|
|
|
// Try a temporary session.
|
|
ASSERT_NO_FATAL_FAILURE(
|
|
CreateSessionAndUpdate(Cdm::kTemporary, Cdm::kCenc, &session_id));
|
|
status = cdm_->remove(session_id);
|
|
EXPECT_EQ(Cdm::kRangeError, status);
|
|
}
|
|
|
|
INSTANTIATE_TEST_CASE_P(CdmRemoveTest, CdmTestWithRemoveParam, Bool(),
|
|
testing::PrintToStringParamName());
|
|
|
|
TEST_F(CdmTest, ForceRemove) {
|
|
EnsureProvisioned();
|
|
std::string session_id;
|
|
ASSERT_NO_FATAL_FAILURE(
|
|
CreateSessionAndUpdate(Cdm::kPersistentLicense, Cdm::kCenc, &session_id));
|
|
|
|
// Forcibly remove the session. This should immediately trigger a removal
|
|
// callback and should *not* cause a release message to be generated.
|
|
EXPECT_CALL(*this, onRemoveComplete(session_id)).Times(1);
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRelease, _)).Times(0);
|
|
Cdm::Status status = cdm_->forceRemove(session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// The session is now completely gone.
|
|
status = cdm_->close(session_id);
|
|
ASSERT_EQ(Cdm::kSessionNotFound, status);
|
|
status = cdm_->load(session_id);
|
|
ASSERT_EQ(Cdm::kSessionNotFound, status);
|
|
|
|
// Try a bogus session ID.
|
|
status = cdm_->forceRemove(kBogusSessionId);
|
|
EXPECT_EQ(Cdm::kSessionNotFound, status);
|
|
|
|
// Try a new session.
|
|
status = cdm_->createSession(Cdm::kPersistentLicense, &session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
status = cdm_->forceRemove(session_id);
|
|
EXPECT_EQ(Cdm::kInvalidState, status);
|
|
|
|
// Try a temporary session.
|
|
ASSERT_NO_FATAL_FAILURE(
|
|
CreateSessionAndUpdate(Cdm::kTemporary, Cdm::kCenc, &session_id));
|
|
status = cdm_->forceRemove(session_id);
|
|
EXPECT_EQ(Cdm::kRangeError, status);
|
|
}
|
|
|
|
TEST_F(CdmTest, RemoveUsageRecord) {
|
|
EnsureProvisioned();
|
|
std::string session_id;
|
|
ASSERT_NO_FATAL_FAILURE(CreateSessionAndUpdate(Cdm::kPersistentUsageRecord,
|
|
Cdm::kCenc, &session_id));
|
|
|
|
// Remove the session. This causes a release message to be generated.
|
|
std::string message;
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, false));
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRelease, _))
|
|
.WillOnce(SaveArg<2>(&message));
|
|
Cdm::Status status = cdm_->remove(session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// The keys should already be unusable.
|
|
Cdm::KeyStatusMap map;
|
|
status = cdm_->getKeyStatuses(session_id, &map);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
EXPECT_EQ(Cdm::kReleased, map.begin()->second);
|
|
|
|
// Post the release message to the license server.
|
|
std::string response;
|
|
ASSERT_NO_FATAL_FAILURE(
|
|
FetchLicense(config_.license_server(), message, &response));
|
|
|
|
// Update the session.
|
|
EXPECT_CALL(*this, onRemoveComplete(session_id));
|
|
status = updateWithRetry(session_id, response);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// The session is now completely gone.
|
|
status = cdm_->close(session_id);
|
|
ASSERT_EQ(Cdm::kSessionNotFound, status);
|
|
}
|
|
|
|
TEST_F(CdmTest, RemoveThreeUsageRecords) {
|
|
EnsureProvisioned();
|
|
std::string session_id1, session_id2, session_id3;
|
|
ASSERT_NO_FATAL_FAILURE(CreateSessionAndUpdate(Cdm::kPersistentUsageRecord,
|
|
Cdm::kCenc, &session_id1));
|
|
ASSERT_NO_FATAL_FAILURE(CreateSessionAndUpdate(Cdm::kPersistentUsageRecord,
|
|
Cdm::kCenc, &session_id2));
|
|
ASSERT_NO_FATAL_FAILURE(CreateSessionAndUpdate(Cdm::kPersistentUsageRecord,
|
|
Cdm::kCenc, &session_id3));
|
|
|
|
// Close, reload and remove the middle session first
|
|
EXPECT_EQ(Cdm::kSuccess, cdm_->close(session_id2));
|
|
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id2, _)).Times(0);
|
|
EXPECT_CALL(*this, onMessage(session_id2, Cdm::kLicenseRelease, _)).Times(0);
|
|
Cdm::Status status = cdm_->load(session_id2);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Remove the session. This causes a release message to be generated.
|
|
std::string message;
|
|
EXPECT_CALL(*this, onMessage(session_id2, Cdm::kLicenseRelease, _))
|
|
.WillOnce(SaveArg<2>(&message));
|
|
status = cdm_->remove(session_id2);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// The keys should already be unusable.
|
|
Cdm::KeyStatusMap map;
|
|
status = cdm_->getKeyStatuses(session_id2, &map);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
EXPECT_TRUE(map.empty());
|
|
|
|
// Post the release message to the license server.
|
|
std::string response;
|
|
ASSERT_NO_FATAL_FAILURE(
|
|
FetchLicense(config_.license_server(), message, &response));
|
|
|
|
// Update the session.
|
|
EXPECT_CALL(*this, onRemoveComplete(session_id2));
|
|
status = updateWithRetry(session_id2, response);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// The session is now completely gone.
|
|
status = cdm_->close(session_id2);
|
|
ASSERT_EQ(Cdm::kSessionNotFound, status);
|
|
|
|
// Close, reload and remove the last session next
|
|
EXPECT_EQ(Cdm::kSuccess, cdm_->close(session_id3));
|
|
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id3, _)).Times(0);
|
|
EXPECT_CALL(*this, onMessage(session_id3, Cdm::kLicenseRelease, _)).Times(0);
|
|
status = cdm_->load(session_id3);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Remove the session. This causes a release message to be generated.
|
|
EXPECT_CALL(*this, onMessage(session_id3, Cdm::kLicenseRelease, _))
|
|
.WillOnce(SaveArg<2>(&message));
|
|
status = cdm_->remove(session_id3);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// The keys should already be unusable.
|
|
status = cdm_->getKeyStatuses(session_id3, &map);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
EXPECT_TRUE(map.empty());
|
|
|
|
// Post the release message to the license server.
|
|
ASSERT_NO_FATAL_FAILURE(
|
|
FetchLicense(config_.license_server(), message, &response));
|
|
|
|
// Update the session.
|
|
EXPECT_CALL(*this, onRemoveComplete(session_id3));
|
|
status = updateWithRetry(session_id3, response);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// The session is now completely gone.
|
|
status = cdm_->close(session_id3);
|
|
ASSERT_EQ(Cdm::kSessionNotFound, status);
|
|
|
|
// Close, reload and remove the first session next
|
|
EXPECT_EQ(Cdm::kSuccess, cdm_->close(session_id1));
|
|
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id1, _)).Times(0);
|
|
EXPECT_CALL(*this, onMessage(session_id1, Cdm::kLicenseRelease, _)).Times(0);
|
|
status = cdm_->load(session_id1);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Remove the session. This causes a release message to be generated.
|
|
EXPECT_CALL(*this, onMessage(session_id1, Cdm::kLicenseRelease, _))
|
|
.WillOnce(SaveArg<2>(&message));
|
|
status = cdm_->remove(session_id1);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// The keys should already be unusable.
|
|
status = cdm_->getKeyStatuses(session_id1, &map);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
EXPECT_TRUE(map.empty());
|
|
|
|
// Post the release message to the license server.
|
|
ASSERT_NO_FATAL_FAILURE(
|
|
FetchLicense(config_.license_server(), message, &response));
|
|
|
|
// Update the session.
|
|
EXPECT_CALL(*this, onRemoveComplete(session_id1));
|
|
status = updateWithRetry(session_id1, response);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// The session is now completely gone.
|
|
status = cdm_->close(session_id1);
|
|
ASSERT_EQ(Cdm::kSessionNotFound, status);
|
|
}
|
|
|
|
TEST_F(CdmTest, RemoveIncomplete) {
|
|
EnsureProvisioned();
|
|
std::string session_id;
|
|
ASSERT_NO_FATAL_FAILURE(
|
|
CreateSessionAndUpdate(Cdm::kPersistentLicense, Cdm::kCenc, &session_id));
|
|
|
|
// Remove the session. This causes a release message to be generated.
|
|
std::string message;
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, false));
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRelease, _))
|
|
.WillOnce(SaveArg<2>(&message));
|
|
Cdm::Status status = cdm_->remove(session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// The keys should already be unusable, but they should still exist.
|
|
Cdm::KeyStatusMap map;
|
|
status = cdm_->getKeyStatuses(session_id, &map);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
ASSERT_FALSE(map.empty());
|
|
EXPECT_EQ(Cdm::kReleased, map.begin()->second);
|
|
|
|
// Recreate the CDM.
|
|
ASSERT_NO_FATAL_FAILURE(RecreateCdm(true /* privacy_mode */));
|
|
|
|
// Load the partially removed session.
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(0);
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRelease, _)).Times(0);
|
|
status = cdm_->load(session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// This session has no keys.
|
|
status = cdm_->getKeyStatuses(session_id, &map);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
EXPECT_TRUE(map.empty());
|
|
|
|
// Remove the session again to fire the release message.
|
|
message.clear();
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(0);
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRelease, _))
|
|
.WillOnce(SaveArg<2>(&message));
|
|
status = cdm_->remove(session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
ASSERT_FALSE(message.empty());
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Post the release message to the license server.
|
|
std::string response;
|
|
ASSERT_NO_FATAL_FAILURE(
|
|
FetchLicense(config_.license_server(), message, &response));
|
|
|
|
// Update the session.
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(0);
|
|
EXPECT_CALL(*this, onRemoveComplete(session_id));
|
|
status = updateWithRetry(session_id, response);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// The session is now completely gone.
|
|
status = cdm_->load(session_id);
|
|
ASSERT_EQ(Cdm::kSessionNotFound, status);
|
|
}
|
|
|
|
TEST_F(CdmTest, RemoveUsageRecordIncomplete) {
|
|
EnsureProvisioned();
|
|
std::string session_id;
|
|
ASSERT_NO_FATAL_FAILURE(CreateSessionAndUpdate(Cdm::kPersistentUsageRecord,
|
|
Cdm::kCenc, &session_id));
|
|
|
|
// Remove the session. This causes a release message to be generated.
|
|
std::string message;
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, false));
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRelease, _))
|
|
.WillOnce(SaveArg<2>(&message));
|
|
Cdm::Status status = cdm_->remove(session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// The keys should already be unusable, but they should still exist.
|
|
Cdm::KeyStatusMap map;
|
|
status = cdm_->getKeyStatuses(session_id, &map);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
ASSERT_FALSE(map.empty());
|
|
EXPECT_EQ(Cdm::kReleased, map.begin()->second);
|
|
|
|
// Recreate the CDM.
|
|
ASSERT_NO_FATAL_FAILURE(RecreateCdm(true /* privacy_mode */));
|
|
|
|
// Load the partially removed session.
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(0);
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRelease, _)).Times(0);
|
|
status = cdm_->load(session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Remove the session again to fire a release message.
|
|
message.clear();
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(0);
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRelease, _))
|
|
.WillOnce(SaveArg<2>(&message));
|
|
status = cdm_->remove(session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
ASSERT_FALSE(message.empty());
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// This session has no keys.
|
|
status = cdm_->getKeyStatuses(session_id, &map);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
EXPECT_TRUE(map.empty());
|
|
|
|
// Post the release message to the license server.
|
|
std::string response;
|
|
ASSERT_NO_FATAL_FAILURE(
|
|
FetchLicense(config_.license_server(), message, &response));
|
|
|
|
// Update the session.
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(0);
|
|
EXPECT_CALL(*this, onRemoveComplete(session_id));
|
|
status = updateWithRetry(session_id, response);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// The session is now completely gone.
|
|
status = cdm_->load(session_id);
|
|
ASSERT_EQ(Cdm::kSessionNotFound, status);
|
|
}
|
|
|
|
TEST_F(CdmTest, RemoveNotLoaded) {
|
|
EnsureProvisioned();
|
|
// Create a persistent session and then close it.
|
|
std::string session_id;
|
|
ASSERT_NO_FATAL_FAILURE(
|
|
CreateSessionAndUpdate(Cdm::kPersistentLicense, Cdm::kCenc, &session_id));
|
|
Cdm::Status status = cdm_->close(session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
|
|
// A session must be loaded before removing it. Remove only works on active
|
|
// sessions.
|
|
status = cdm_->remove(session_id);
|
|
EXPECT_EQ(Cdm::kSessionNotFound, status);
|
|
}
|
|
|
|
TEST_F(CdmTest, RequestPersistentLicenseWithWrongInitData) {
|
|
EnsureProvisioned();
|
|
// Generate a request for a persistent license without using the correct
|
|
// persistent content init data.
|
|
std::string session_id;
|
|
Cdm::Status status =
|
|
cdm_->createSession(Cdm::kPersistentLicense, &session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
|
|
std::string message;
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _))
|
|
.WillOnce(SaveArg<2>(&message));
|
|
status = generateRequestWithRetry(session_id, Cdm::kCenc, kCencInitData);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// The license server will reject this.
|
|
FetchLicenseFailure(message, 500);
|
|
}
|
|
|
|
// TODO(b/34949512): Fix this test so it can be re-enabled.
|
|
TEST_F(CdmTest, DISABLED_RequestTemporaryLicenseWithWrongInitData) {
|
|
EnsureProvisioned();
|
|
// Generate a request for a temporary license using persistent init data.
|
|
std::string session_id;
|
|
Cdm::Status status = cdm_->createSession(Cdm::kTemporary, &session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
|
|
std::string message;
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _))
|
|
.WillOnce(SaveArg<2>(&message));
|
|
status =
|
|
generateRequestWithRetry(session_id, Cdm::kCenc, kCencPersistentInitData);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Acquire a license.
|
|
std::string response;
|
|
ASSERT_NO_FATAL_FAILURE(
|
|
FetchLicense(config_.license_server(), message, &response));
|
|
|
|
// This license should not be accepted.
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, false));
|
|
status = updateWithRetry(session_id, response);
|
|
EXPECT_EQ(Cdm::kRangeError, status);
|
|
Mock::VerifyAndClear(this);
|
|
}
|
|
|
|
TEST_F(CdmTest, Renewal) {
|
|
EnsureProvisioned();
|
|
std::string session_id;
|
|
ASSERT_NO_FATAL_FAILURE(
|
|
CreateSessionAndUpdate(Cdm::kTemporary, Cdm::kCenc, &session_id));
|
|
|
|
// We should have a timer.
|
|
EXPECT_NE(0, g_host->NumTimers());
|
|
|
|
// When we elapse time, we should get a renewal message.
|
|
std::string message;
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRenewal, _))
|
|
.WillOnce(SaveArg<2>(&message));
|
|
g_host->ElapseTime(kRenewalTestDelayMs);
|
|
Mock::VerifyAndClear(this);
|
|
ASSERT_FALSE(message.empty()); // Stop the test if no message came through.
|
|
|
|
// When should still have a timer.
|
|
EXPECT_NE(0, g_host->NumTimers());
|
|
|
|
// We should be able to update the session.
|
|
ASSERT_NO_FATAL_FAILURE(FetchLicenseAndUpdate(session_id, message));
|
|
|
|
// After closing the session, there should be no more renewals.
|
|
Cdm::Status status = cdm_->close(session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRenewal, _)).Times(0);
|
|
g_host->ElapseTime(kRenewalTestDelayMs * 10);
|
|
Mock::VerifyAndClear(this);
|
|
}
|
|
|
|
TEST_F(CdmTest, SetAppParameters) {
|
|
EnsureProvisioned();
|
|
// Must use privacy_mode = false to ensure that the message is in plain-text.
|
|
std::string session_id;
|
|
ASSERT_NO_FATAL_FAILURE(RecreateCdm(false /* privacy_mode */));
|
|
Cdm::Status status = cdm_->createSession(Cdm::kTemporary, &session_id);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
|
|
// Set a new app parameter, and check by getting.
|
|
std::string result;
|
|
status = cdm_->setAppParameter(kParamName, kValue);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
status = cdm_->getAppParameter(kParamName, &result);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
ASSERT_EQ(kValue, result);
|
|
|
|
// Try to get using a null result.
|
|
status = cdm_->getAppParameter(kParamName, nullptr);
|
|
ASSERT_EQ(Cdm::kTypeError, status);
|
|
|
|
// Try to get using an empty key.
|
|
status = cdm_->getAppParameter("", &result);
|
|
ASSERT_EQ(Cdm::kTypeError, status);
|
|
|
|
// Try to set using an empty key.
|
|
status = cdm_->setAppParameter("", kValue);
|
|
ASSERT_EQ(Cdm::kTypeError, status);
|
|
|
|
// Try to remove using an empty key.
|
|
status = cdm_->removeAppParameter("");
|
|
ASSERT_EQ(Cdm::kTypeError, status);
|
|
|
|
// Change an existing app parameter.
|
|
status = cdm_->setAppParameter(kParamName, kNewValue);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
status = cdm_->getAppParameter(kParamName, &result);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
ASSERT_EQ(kNewValue, result);
|
|
|
|
// Remove an existing app parameter, check for invalid access when it's gone.
|
|
status = cdm_->removeAppParameter(kParamName);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
status = cdm_->getAppParameter(kParamName, &result);
|
|
ASSERT_EQ(Cdm::kTypeError, status);
|
|
|
|
// Try to remove an absent value.
|
|
status = cdm_->removeAppParameter(kParamName2);
|
|
ASSERT_EQ(Cdm::kTypeError, status);
|
|
|
|
// Set some values to check for.
|
|
status = cdm_->setAppParameter(kParamName, kValue);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
status = cdm_->setAppParameter(kParamName2, kNewValue);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
|
|
// Send a generate request to ensure the parameter is in the message.
|
|
std::string message;
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _))
|
|
.WillOnce(SaveArg<2>(&message));
|
|
status = generateRequestWithRetry(session_id, Cdm::kCenc, kCencInitData);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
EXPECT_TRUE(!message.empty() && message.find(kValue) != std::string::npos);
|
|
Mock::VerifyAndClear(this);
|
|
|
|
// Ensure that the value is still present and correct.
|
|
status = cdm_->getAppParameter(kParamName, &result);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
ASSERT_EQ(kValue, result);
|
|
status = cdm_->getAppParameter(kParamName2, &result);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
ASSERT_EQ(kNewValue, result);
|
|
|
|
// Clear all the parameters.
|
|
status = cdm_->clearAppParameters();
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
status = cdm_->getAppParameter(kParamName, &result);
|
|
ASSERT_EQ(Cdm::kTypeError, status);
|
|
status = cdm_->getAppParameter(kParamName2, &result);
|
|
ASSERT_EQ(Cdm::kTypeError, status);
|
|
}
|
|
|
|
TEST_F(CdmTest, SetVideoResolutionBadSession) {
|
|
EnsureProvisioned();
|
|
std::string session_id;
|
|
ASSERT_NO_FATAL_FAILURE(
|
|
CreateSessionAndUpdate(Cdm::kTemporary, Cdm::kCenc, &session_id));
|
|
|
|
Cdm::Status status = cdm_->setVideoResolution(kBogusSessionId, 100, 100);
|
|
ASSERT_EQ(Cdm::kSessionNotFound, status);
|
|
|
|
cdm_->close(session_id);
|
|
}
|
|
|
|
TEST_F(CdmTest, SetVideoResolutionOK) {
|
|
EnsureProvisioned();
|
|
std::string session_id;
|
|
ASSERT_NO_FATAL_FAILURE(
|
|
CreateSessionAndUpdate(Cdm::kTemporary, Cdm::kCenc, &session_id));
|
|
|
|
Cdm::Status status = cdm_->setVideoResolution(session_id, 100, 100);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
|
|
cdm_->close(session_id);
|
|
}
|
|
|
|
TEST_F(CdmTest, SetVideoResolutionOverflow) {
|
|
EnsureProvisioned();
|
|
std::string session_id;
|
|
ASSERT_NO_FATAL_FAILURE(
|
|
CreateSessionAndUpdate(Cdm::kTemporary, Cdm::kCenc, &session_id));
|
|
|
|
const uint32_t kUintTooLarge = (1 << 16);
|
|
Cdm::Status status =
|
|
cdm_->setVideoResolution(session_id, kUintTooLarge, kUintTooLarge);
|
|
ASSERT_EQ(Cdm::kRangeError, status);
|
|
|
|
cdm_->close(session_id);
|
|
}
|
|
|
|
TEST_F(CdmTest, GetStatusForHdcpResolution) {
|
|
// Unfortunately, since we cannot mock the HDCP state, we cannot validate
|
|
// the validity of the values returned here, only that meaningful values are
|
|
// returned.
|
|
Cdm::KeyStatus key_status;
|
|
|
|
ASSERT_EQ(Cdm::kSuccess,
|
|
cdm_->getStatusForHdcpVersion(Cdm::kHdcp1_x, &key_status));
|
|
EXPECT_THAT(key_status, AnyOf(Cdm::kUsable, Cdm::kOutputRestricted));
|
|
|
|
ASSERT_EQ(Cdm::kSuccess,
|
|
cdm_->getStatusForHdcpVersion(Cdm::kHdcp2_0, &key_status));
|
|
EXPECT_THAT(key_status, AnyOf(Cdm::kUsable, Cdm::kOutputRestricted));
|
|
|
|
ASSERT_EQ(Cdm::kSuccess,
|
|
cdm_->getStatusForHdcpVersion(Cdm::kHdcp2_1, &key_status));
|
|
EXPECT_THAT(key_status, AnyOf(Cdm::kUsable, Cdm::kOutputRestricted));
|
|
|
|
ASSERT_EQ(Cdm::kSuccess,
|
|
cdm_->getStatusForHdcpVersion(Cdm::kHdcp2_2, &key_status));
|
|
EXPECT_THAT(key_status, AnyOf(Cdm::kUsable, Cdm::kOutputRestricted));
|
|
|
|
ASSERT_EQ(Cdm::kSuccess,
|
|
cdm_->getStatusForHdcpVersion(Cdm::kHdcp2_3, &key_status));
|
|
EXPECT_THAT(key_status, AnyOf(Cdm::kUsable, Cdm::kOutputRestricted));
|
|
}
|
|
|
|
TEST_F(CdmTest, HandlesKeyRotationWithOnlyOneLicenseRequest) {
|
|
EnsureProvisioned();
|
|
std::string session_id;
|
|
|
|
// TODO(b/77152154):
|
|
// The CreateSessionAndX helpers need to be reworked so this function can use
|
|
// them.
|
|
|
|
ASSERT_EQ(Cdm::kSuccess, cdm_->createSession(Cdm::kTemporary, &session_id));
|
|
|
|
// Generate a license request for the 1st entitlement init data.
|
|
std::string license_request;
|
|
{
|
|
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _))
|
|
.WillOnce(SaveArg<2>(&license_request));
|
|
ASSERT_EQ(Cdm::kSuccess,
|
|
generateRequestWithRetry(session_id, Cdm::kCenc,
|
|
kCencEntitlementInitData1));
|
|
Mock::VerifyAndClear(this);
|
|
}
|
|
|
|
// Send the request to the license server and receive the license response.
|
|
std::string license_response;
|
|
FetchLicense(config_.license_server(), license_request, &license_response);
|
|
|
|
// Update the session with the new keys.
|
|
{
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, true));
|
|
ASSERT_EQ(Cdm::kSuccess, updateWithRetry(session_id, license_response));
|
|
Mock::VerifyAndClear(this);
|
|
}
|
|
|
|
// Set up subsample
|
|
Cdm::Subsample subsample;
|
|
subsample.protected_bytes = kInputSize;
|
|
|
|
// Set up sample
|
|
Cdm::Sample sample;
|
|
sample.input.data = kInput;
|
|
sample.input.data_length = subsample.protected_bytes;
|
|
sample.input.subsamples = &subsample;
|
|
sample.input.subsamples_length = 1;
|
|
std::vector<uint8_t> output_buffer(sample.input.data_length);
|
|
sample.output.data = output_buffer.data();
|
|
sample.output.data_length = output_buffer.size();
|
|
|
|
// Set up batch to decrypt
|
|
Cdm::DecryptionBatch batch;
|
|
batch.samples = &sample;
|
|
batch.samples_length = 1;
|
|
batch.encryption_scheme = Cdm::kAesCtr;
|
|
|
|
// Attempt multiple decrypts with a key from the first license.
|
|
batch.key_id = kKeyIdEntitlement1.data();
|
|
batch.key_id_length = kKeyIdEntitlement1.size();
|
|
sample.input.iv = kIvEntitlement1;
|
|
sample.input.iv_length = kIvEntitlement1Size;
|
|
ASSERT_EQ(Cdm::kSuccess, cdm_->decrypt(batch));
|
|
const std::vector<uint8_t> expected_output1(
|
|
kOutputEntitlement1, kOutputEntitlement1 + kOutputEntitlement1Size);
|
|
EXPECT_EQ(expected_output1, output_buffer);
|
|
memset(&(output_buffer[0]), 0, output_buffer.size());
|
|
ASSERT_EQ(Cdm::kSuccess, cdm_->decrypt(batch));
|
|
EXPECT_EQ(expected_output1, output_buffer);
|
|
|
|
// Load the second entitlement license using the first. This should not
|
|
// require any server roundtrip.
|
|
ASSERT_EQ(Cdm::kSuccess, cdm_->loadEmbeddedKeys(session_id, Cdm::kCenc,
|
|
kCencEntitlementInitData2));
|
|
|
|
// Attempt multiple decrypts with a key from the second license.
|
|
batch.key_id = kKeyIdEntitlement2.data();
|
|
batch.key_id_length = kKeyIdEntitlement2.size();
|
|
sample.input.iv = kIvEntitlement2;
|
|
sample.input.iv_length = kIvEntitlement2Size;
|
|
memset(&(output_buffer[0]), 0, output_buffer.size());
|
|
ASSERT_EQ(Cdm::kSuccess, cdm_->decrypt(batch));
|
|
const std::vector<uint8_t> expected_output2(
|
|
kOutputEntitlement2, kOutputEntitlement2 + kOutputEntitlement2Size);
|
|
EXPECT_EQ(expected_output2, output_buffer);
|
|
memset(&(output_buffer[0]), 0, output_buffer.size());
|
|
ASSERT_EQ(Cdm::kSuccess, cdm_->decrypt(batch));
|
|
EXPECT_EQ(expected_output2, output_buffer);
|
|
|
|
// Attempt multiple decrypts with a key from the first license again.
|
|
batch.key_id = kKeyIdEntitlement1.data();
|
|
batch.key_id_length = kKeyIdEntitlement1.size();
|
|
sample.input.iv = kIvEntitlement1;
|
|
sample.input.iv_length = kIvEntitlement1Size;
|
|
memset(&(output_buffer[0]), 0, output_buffer.size());
|
|
ASSERT_EQ(Cdm::kSuccess, cdm_->decrypt(batch));
|
|
EXPECT_EQ(expected_output1, output_buffer);
|
|
memset(&(output_buffer[0]), 0, output_buffer.size());
|
|
ASSERT_EQ(Cdm::kSuccess, cdm_->decrypt(batch));
|
|
EXPECT_EQ(expected_output1, output_buffer);
|
|
}
|
|
|
|
TEST_F(CdmTest, ClearPlaybackWithoutAKeyIdOrIv) {
|
|
EnsureProvisioned();
|
|
|
|
// Set up subsample
|
|
Cdm::Subsample subsample;
|
|
subsample.clear_bytes = kInputSize;
|
|
|
|
// Set up sample
|
|
Cdm::Sample sample;
|
|
sample.input.data = kInput;
|
|
sample.input.data_length = subsample.clear_bytes;
|
|
sample.input.subsamples = &subsample;
|
|
sample.input.subsamples_length = 1;
|
|
std::vector<uint8_t> output_buffer(sample.input.data_length);
|
|
sample.output.data = output_buffer.data();
|
|
sample.output.data_length = output_buffer.size();
|
|
|
|
// Set up batch to decrypt
|
|
Cdm::DecryptionBatch batch;
|
|
batch.samples = &sample;
|
|
batch.samples_length = 1;
|
|
|
|
// Note that the use of kInput for the expected output is not an error. This
|
|
// is clear playback, so the data should pass through unchanged.
|
|
std::vector<uint8_t> expected_output(kInput, kInput + kInputSize);
|
|
|
|
// Create a session.
|
|
std::string session_id;
|
|
cdm_->createSession(Cdm::kTemporary, &session_id);
|
|
|
|
// Decrypt without specifying a session or key ID should fail.
|
|
Cdm::Status status = cdm_->decrypt(batch);
|
|
EXPECT_EQ(Cdm::kNoKey, status);
|
|
|
|
// Decrypt with a known session should succeed.
|
|
status = cdm_->decrypt(session_id, batch);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
}
|
|
|
|
TEST_F(CdmTest, EncryptedPlaybackWithoutALicense) {
|
|
EnsureProvisioned();
|
|
|
|
// Set up subsample
|
|
Cdm::Subsample subsample;
|
|
subsample.protected_bytes = kInputSize;
|
|
|
|
// Set up sample
|
|
Cdm::Sample sample;
|
|
sample.input.iv = kIvCenc;
|
|
sample.input.iv_length = kIvCencSize;
|
|
sample.input.data = kInput;
|
|
sample.input.data_length = subsample.protected_bytes;
|
|
sample.input.subsamples = &subsample;
|
|
sample.input.subsamples_length = 1;
|
|
std::vector<uint8_t> output_buffer(sample.input.data_length);
|
|
sample.output.data = output_buffer.data();
|
|
sample.output.data_length = output_buffer.size();
|
|
|
|
// Set up batch to decrypt
|
|
Cdm::DecryptionBatch batch;
|
|
batch.samples = &sample;
|
|
batch.samples_length = 1;
|
|
batch.key_id = kKeyIdCtr.data();
|
|
batch.key_id_length = kKeyIdCtr.size();
|
|
batch.encryption_scheme = Cdm::kAesCtr;
|
|
|
|
// Create a session.
|
|
std::string session_id;
|
|
cdm_->createSession(Cdm::kTemporary, &session_id);
|
|
|
|
// Decrypt without specifying a session should fail.
|
|
Cdm::Status status = cdm_->decrypt(batch);
|
|
EXPECT_EQ(Cdm::kNoKey, status);
|
|
|
|
// Decrypt with a known session should fail.
|
|
status = cdm_->decrypt(session_id, batch);
|
|
EXPECT_EQ(Cdm::kNoKey, status);
|
|
}
|
|
|
|
TEST_F(CdmTest, GetEmptyMetrics) {
|
|
std::string metrics;
|
|
Cdm::Status status = cdm_->getMetrics(&metrics);
|
|
EXPECT_EQ(status, Cdm::kSuccess);
|
|
EXPECT_NE(metrics.length(), 0u);
|
|
}
|
|
|
|
TEST_F(CdmTest, GetMetrics) {
|
|
EnsureProvisioned();
|
|
std::string ignored;
|
|
ASSERT_NO_FATAL_FAILURE(
|
|
CreateSessionAndUpdate(Cdm::kTemporary, Cdm::kCenc, &ignored));
|
|
|
|
std::string metrics;
|
|
Cdm::Status status = cdm_->getMetrics(&metrics);
|
|
EXPECT_EQ(status, Cdm::kSuccess);
|
|
EXPECT_NE(metrics.length(), 0u);
|
|
}
|
|
|
|
TEST_P(CdmTestWithDecryptParam, DecryptToClearBuffer) {
|
|
EnsureProvisioned();
|
|
DecryptParam param = GetParam();
|
|
|
|
// Set up subsample
|
|
Cdm::Subsample subsample;
|
|
if (param.scheme == Cdm::kClear) {
|
|
subsample.clear_bytes = param.input_size;
|
|
} else {
|
|
subsample.protected_bytes = param.input_size;
|
|
}
|
|
|
|
// Set up sample
|
|
Cdm::Sample sample;
|
|
sample.input.iv = param.iv;
|
|
sample.input.iv_length = param.iv_size;
|
|
sample.input.data = param.input;
|
|
sample.input.data_length = param.input_size;
|
|
sample.input.subsamples = &subsample;
|
|
sample.input.subsamples_length = 1;
|
|
std::vector<uint8_t> output_buffer(sample.input.data_length);
|
|
sample.output.data = output_buffer.data();
|
|
sample.output.data_length = output_buffer.size();
|
|
|
|
// Set up batch to decrypt
|
|
Cdm::DecryptionBatch batch;
|
|
batch.samples = &sample;
|
|
batch.samples_length = 1;
|
|
batch.key_id = param.key_id->data();
|
|
batch.key_id_length = param.key_id->size();
|
|
batch.pattern = *param.pattern;
|
|
batch.encryption_scheme = param.scheme;
|
|
|
|
std::vector<uint8_t> expected_output(param.output,
|
|
param.output + param.output_size);
|
|
|
|
// Decrypt without keys loaded should fail.
|
|
Cdm::Status status = cdm_->decrypt(batch);
|
|
ASSERT_EQ(Cdm::kNoKey, status);
|
|
|
|
// Create a session with the right keys.
|
|
std::string session_id;
|
|
ASSERT_NO_FATAL_FAILURE(CreateSessionAndUpdate(
|
|
Cdm::kTemporary, param.init_data_type, &session_id));
|
|
|
|
// Decrypt should now succeed.
|
|
status = cdm_->decrypt(session_id, batch);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
EXPECT_EQ(expected_output, output_buffer);
|
|
|
|
// Decrypt should succeed even without specifying the session ID.
|
|
status = cdm_->decrypt(batch);
|
|
ASSERT_EQ(Cdm::kSuccess, status);
|
|
EXPECT_EQ(expected_output, output_buffer);
|
|
}
|
|
|
|
INSTANTIATE_TEST_CASE_P(
|
|
CdmDecryptTest, CdmTestWithDecryptParam,
|
|
Values(DecryptParam("CENC 3.0 cenc Mode", Cdm::kCenc, kKeyIdCtr, kIvCenc,
|
|
kIvCencSize, Cdm::kAesCtr, kPatternNone, kInput,
|
|
kInputSize, kOutputCenc, kOutputCencSize),
|
|
DecryptParam("CENC 3.0 cbcs Mode", Cdm::kHls, kKeyIdCbc, kIvCbcs,
|
|
kIvCbcsSize, Cdm::kAesCbc, kPatternRecommended, kInput,
|
|
kInputSize, kOutputCbcs, kOutputCbcsSize),
|
|
DecryptParam("HLS Audio (CENC 3.0 cbcs Mode Without a Pattern)",
|
|
Cdm::kHls, kKeyIdCbc, kIvCbc1, kIvCbc1Size,
|
|
Cdm::kAesCbc, kPatternHlsAudio, kInput, kInputSize,
|
|
kOutputCbc1, kOutputCbc1Size),
|
|
DecryptParam("Clear Data w/ Known Key ID", Cdm::kCenc, kKeyIdCtr,
|
|
kIvCenc, kIvCencSize, Cdm::kClear, kPatternNone, kInput,
|
|
kInputSize, kInput, kInputSize)));
|
|
|
|
// TODO (b/119200745):
|
|
// add infrastructure to test secure buffer decrypt for some platforms
|
|
|
|
class CdmIndividualizationTest : public CdmTest {
|
|
protected:
|
|
bool CheckProvisioningSupport() {
|
|
if (wvoec::global_features.loads_certificate) return true;
|
|
LOGW(
|
|
"WARNING: Skipping device provisioning tests because the device "
|
|
"does not support provisioning. If you are using a baked-in "
|
|
"certificate, this is expected. Otherwise, something is wrong.");
|
|
return false;
|
|
}
|
|
|
|
std::string GetProvisioningResponse(const std::string& message) {
|
|
std::string reply;
|
|
std::string uri = config_.provisioning_server();
|
|
|
|
LOGV("GetProvisioningResponse: URI: %s", uri.c_str());
|
|
LOGV("GetProvisioningResponse: message:\n%s\n", message.c_str());
|
|
|
|
uri += "&signedRequest=" + message;
|
|
FetchCertificate(uri, &reply);
|
|
if (HasFatalFailure()) {
|
|
LOGE("GetProvisioningResponse: Failed.");
|
|
return "";
|
|
}
|
|
|
|
LOGV("GetProvisioningResponse: response:\n%s\n", reply.c_str());
|
|
return reply;
|
|
}
|
|
};
|
|
|
|
TEST_F(CdmIndividualizationTest, BasicFlow) {
|
|
if (!CheckProvisioningSupport()) return;
|
|
|
|
// Clear any existing certificates.
|
|
g_host->remove(kDeviceCertFileName);
|
|
|
|
// Provision the device
|
|
std::string message;
|
|
Cdm::Status status = cdm_->getProvisioningRequest(&message);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
std::string reply = GetProvisioningResponse(message);
|
|
ASSERT_FALSE(reply.empty());
|
|
status = cdm_->handleProvisioningResponse(reply);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
|
|
// We should now be able to create a session and generate a request.
|
|
std::string session_id;
|
|
ASSERT_NO_FATAL_FAILURE(CreateSessionAndGenerateRequest(
|
|
Cdm::kTemporary, Cdm::kCenc, &session_id, &message));
|
|
|
|
// Acquire a license and update the session.
|
|
ASSERT_NO_FATAL_FAILURE(
|
|
FetchLicense(config_.license_server(), message, &reply));
|
|
EXPECT_CALL(*this, onKeyStatusesChange(session_id, true));
|
|
status = updateWithRetry(session_id, reply);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
Mock::VerifyAndClear(this);
|
|
}
|
|
|
|
TEST_F(CdmIndividualizationTest, IsProvisioned) {
|
|
if (!CheckProvisioningSupport()) return;
|
|
|
|
// Clear any existing certificates.
|
|
g_host->remove(kDeviceCertFileName);
|
|
|
|
EXPECT_FALSE(cdm_->isProvisioned());
|
|
|
|
// Provision the device
|
|
std::string message;
|
|
Cdm::Status status = cdm_->getProvisioningRequest(&message);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
std::string reply = GetProvisioningResponse(message);
|
|
ASSERT_FALSE(reply.empty());
|
|
status = cdm_->handleProvisioningResponse(reply);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
|
|
EXPECT_TRUE(cdm_->isProvisioned());
|
|
}
|
|
|
|
TEST_F(CdmIndividualizationTest, RemoveProvisioning) {
|
|
if (!CheckProvisioningSupport()) return;
|
|
|
|
// Clear any existing certificates.
|
|
EXPECT_EQ(Cdm::kSuccess, cdm_->removeProvisioning());
|
|
|
|
EXPECT_FALSE(cdm_->isProvisioned());
|
|
|
|
// Provision the device
|
|
std::string message;
|
|
Cdm::Status status = cdm_->getProvisioningRequest(&message);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
std::string reply = GetProvisioningResponse(message);
|
|
ASSERT_FALSE(reply.empty());
|
|
status = cdm_->handleProvisioningResponse(reply);
|
|
EXPECT_EQ(Cdm::kSuccess, status);
|
|
|
|
EXPECT_TRUE(cdm_->isProvisioned());
|
|
|
|
EXPECT_EQ(Cdm::kSuccess, cdm_->removeProvisioning());
|
|
|
|
EXPECT_FALSE(cdm_->isProvisioned());
|
|
}
|
|
|
|
TEST_F(CdmIndividualizationTest, NoCreateSessionWithoutProvisioning) {
|
|
if (!CheckProvisioningSupport()) return;
|
|
|
|
// Clear any existing certificates.
|
|
g_host->remove(kDeviceCertFileName);
|
|
ASSERT_FALSE(cdm_->isProvisioned());
|
|
|
|
std::string session_id;
|
|
EXPECT_EQ(Cdm::kNeedsDeviceCertificate,
|
|
cdm_->createSession(Cdm::kTemporary, &session_id));
|
|
EXPECT_THAT(session_id, IsEmpty());
|
|
}
|
|
|
|
TEST_F(CdmIndividualizationTest, NoLoadWithoutProvisioning) {
|
|
if (!CheckProvisioningSupport()) return;
|
|
|
|
// Clear any existing certificates.
|
|
g_host->remove(kDeviceCertFileName);
|
|
ASSERT_FALSE(cdm_->isProvisioned());
|
|
|
|
EXPECT_EQ(Cdm::kNeedsDeviceCertificate, cdm_->load(kBogusSessionId));
|
|
}
|
|
|
|
} // namespace widevine
|