From 160df9f57a775924fcb9435ef2f418686ac2c030 Mon Sep 17 00:00:00 2001 From: "John W. Bruce" Date: Fri, 24 Jul 2020 14:30:03 -0700 Subject: [PATCH] Source release 16.3.0 --- CHANGELOG.md | 54 + README.md | 8 +- ...idevine_CE_CDM_IntegrationGuide_16.3.0.pdf | Bin 461411 -> 461696 bytes build.py | 8 +- cdm/cdm.gyp | 31 +- cdm/cdm_unittests.gyp | 16 +- cdm/include/cdm_version.h | 2 +- cdm/platform_properties.gypi | 5 + cdm/src/cdm.cpp | 6 +- cdm/src/properties_ce.cpp | 7 +- cdm/test/cdm_test.cpp | 118 +- cdm/test/test_host.cpp | 22 +- cdm/test/test_host.h | 11 + cdm/util_unittests.gypi | 4 +- core/include/cdm_client_property_set.h | 1 + core/include/cdm_engine.h | 7 +- core/include/cdm_engine_metrics_decorator.h | 13 +- core/include/cdm_session.h | 8 +- core/include/crypto_session.h | 23 +- core/include/device_files.h | 13 +- core/include/license.h | 3 + core/include/usage_table_header.h | 96 +- core/include/wv_cdm_constants.h | 2 + core/include/wv_cdm_types.h | 26 +- core/src/cdm_engine.cpp | 60 +- core/src/cdm_session.cpp | 71 +- core/src/crypto_session.cpp | 268 +- core/src/device_files.cpp | 30 +- core/src/license.cpp | 40 +- core/src/usage_table_header.cpp | 802 +++-- .../cdm_engine_metrics_decorator_unittest.cpp | 31 +- core/test/cdm_session_unittest.cpp | 22 +- .../certificate_provisioning_unittest.cpp | 18 +- core/test/crypto_session_unittest.cpp | 4 - core/test/device_files_unittest.cpp | 370 +- core/test/http_socket.cpp | 70 +- core/test/license_request.h | 4 +- core/test/parallel_operations_test.cpp | 52 +- core/test/service_certificate_unittest.cpp | 1 + core/test/test_base.cpp | 135 +- core/test/test_base.h | 3 - core/test/test_printers.cpp | 34 +- core/test/url_request.cpp | 9 +- core/test/usage_table_header_unittest.cpp | 3175 ++++++++++------- oemcrypto/include/OEMCryptoCENC.h | 2 +- oemcrypto/include/OEMCryptoCENCCommon.h | 3 +- oemcrypto/test/fuzz_tests/README.md | 187 + .../test/fuzz_tests/oemcrypto_fuzz_helper.cc | 8 + .../test/fuzz_tests/oemcrypto_fuzz_helper.h | 94 + .../test/fuzz_tests/oemcrypto_fuzz_structs.h | 31 + .../test/fuzz_tests/oemcrypto_fuzztests.gyp | 46 +- .../test/fuzz_tests/oemcrypto_fuzztests.gypi | 90 +- .../oemcrypto_license_request_fuzz.cc | 28 + .../fuzz_tests/oemcrypto_load_license_fuzz.cc | 30 + .../oemcrypto_load_provisioning_fuzz.cc | 27 + .../fuzz_tests/oemcrypto_load_renewal_fuzz.cc | 42 + .../oemcrypto_provisioning_request_fuzz.cc | 28 + .../oemcrypto_renewal_request_fuzz.cc | 27 + oemcrypto/test/oec_device_features.cpp | 50 +- oemcrypto/test/oec_key_deriver.cpp | 6 + oemcrypto/test/oec_session_util.cpp | 261 +- oemcrypto/test/oec_session_util.h | 35 +- .../oemcrypto_corpus_generator_helper.cpp | 34 + .../test/oemcrypto_corpus_generator_helper.h | 25 + .../test/oemcrypto_session_tests_helper.cpp | 2 +- oemcrypto/test/oemcrypto_test.cpp | 269 +- oemcrypto/test/oemcrypto_test_main.cpp | 4 + oemcrypto/test/oemcrypto_unittests.gypi | 2 + platforms/x86-64/settings.gypi | 4 +- third_party/boringssl/boringssl.gyp | 19 +- third_party/gmock.gyp | 14 +- third_party/gyp/xcode_emulation.py | 2 + util/test/test_sleep.cpp | 108 +- util/test/test_sleep.h | 32 +- 74 files changed, 4632 insertions(+), 2561 deletions(-) rename Widevine_CE_CDM_IntegrationGuide_16.2.0.pdf => Widevine_CE_CDM_IntegrationGuide_16.3.0.pdf (85%) create mode 100644 oemcrypto/test/fuzz_tests/README.md create mode 100644 oemcrypto/test/fuzz_tests/oemcrypto_fuzz_helper.cc create mode 100644 oemcrypto/test/fuzz_tests/oemcrypto_fuzz_helper.h create mode 100644 oemcrypto/test/fuzz_tests/oemcrypto_fuzz_structs.h create mode 100644 oemcrypto/test/fuzz_tests/oemcrypto_license_request_fuzz.cc create mode 100644 oemcrypto/test/fuzz_tests/oemcrypto_load_license_fuzz.cc create mode 100644 oemcrypto/test/fuzz_tests/oemcrypto_load_provisioning_fuzz.cc create mode 100644 oemcrypto/test/fuzz_tests/oemcrypto_load_renewal_fuzz.cc create mode 100644 oemcrypto/test/fuzz_tests/oemcrypto_provisioning_request_fuzz.cc create mode 100644 oemcrypto/test/fuzz_tests/oemcrypto_renewal_request_fuzz.cc create mode 100644 oemcrypto/test/oemcrypto_corpus_generator_helper.cpp create mode 100644 oemcrypto/test/oemcrypto_corpus_generator_helper.h diff --git a/CHANGELOG.md b/CHANGELOG.md index b6e23347..849f8114 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,60 @@ [TOC] +## 16.3.0 (2020-07-24) + +Features: + - CE CDM 16.3.0 updates the included version of OEMCrypto and its tests to + v16.3. CE CDM 16.3.0 *requires* OEMCrypto v16.3 or later. Widevine will not + be supporting OEMCrypto v16.2 any longer. Upgrading to CE CDM 16.3.0 and + OEMCrypto v16.3 is required for all partners using the 16.x release series. + - OEMCrypto v16.3 includes several updates to the ODK code. Don't forget to + update your OEMCrypto integrations. + - The algorithms that drive the usage tables in the CE CDM are more robust, + particularly in cases involving deleting entries and/or the table becoming + fragmented. + +Bugfixes: + - Fixed a `validate_nonce` error when using `load_refresh_keys` with certain + license services. + - Fixed an issue where clear subsamples that don't make up a full sample might + be accepted when the later encrypted subsamples would be rejected. + - Fixed an issue preventing `device_files.cpp` from compiling with certain C++ + STL implementations. + - Fixed an issue where nonce-free offline licenses (such as those used by + ATSC 3.0) would fail to load in the v16 ODK. + - Fixed issues where compiling with recent GCC releases and with stringent + warning checks enabled would trigger warnings that were treated as errors, + failing compilation. + - Fixed an issue where the OEMCrypto tests were deriving keys too eagerly, + causing OEMCrypto implementations with very strict state-progression checks + to fail. + - Fixed an issue that was causing the following tests to fail when used with + recent license service builds: + - `CdmTest.RemoveUsageRecord` + - `CdmTest.RemoveThreeUsageRecords` + - `CdmTest.RemoveIncomplete` + - `CdmTest.RemoveUsageRecordIncomplete` + - `CdmRemoveTest/CdmTestWithRemoveParam.Remove/false, where GetParam() = false` + - `CdmRemoveTest/CdmTestWithRemoveParam.Remove/true, where GetParam() = true` + - Fixed an issue with accessing the usage table when OEMCrypto had reached the + maximum number of open sessions. + - Fixed an error that could occur if an offline license's file persisted after + its usage entry had been removed. + - Fixed a buffer overrun in the test code. + - Fixed a memory leak in the test code. + - Fixed a buffer overrun in the OEMCrypto Reference implementation. We will again + remind you that the OEMCrypto Reference implementation is *not* intended for production use. + - The test `DecryptNoAnalogToClearAPI13` was no longer valid and has been + removed. + - Fixed an issue where offline licenses with a rental duration and no PST + would instantly expire because they were treated as having been rented + in 1970. + - Fixed a rare issue that could occur with Device IDs between 33 and 64 bytes + long, inclusive. + - The CE CDM now correctly handles the case when OEMCrypto reports an + unlimited usage table capacity. + ## 16.2.0 (2020-04-10) **Note:** CE CDM 16.2.0 is the first release of the CE CDM 16 series. It is diff --git a/README.md b/README.md index 28a0e9fd..3311bd59 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,20 @@ -# Widevine CE CDM 16.2.0 +# Widevine CE CDM 16.3.0 -Released 2020-04-10 +Released 2020-07-24 ## Getting started This project contains the sources for building a Widevine CDM module. Read the following to learn more about the contents of this project and how to use them: -[Widevine_CE_CDM_IntegrationGuide_16.2.0.pdf][integration-guide]\ +[Widevine_CE_CDM_IntegrationGuide_16.3.0.pdf][integration-guide]\ Documents the CDM API and describes how to integrate the CDM into a system. [CHANGELOG.md][changelog]\ Lists the major changes for each release. -[integration-guide]: ./Widevine_CE_CDM_IntegrationGuide_16.2.0.pdf +[integration-guide]: ./Widevine_CE_CDM_IntegrationGuide_16.3.0.pdf [changelog]: ./CHANGELOG.md ## Reference OEMCrypto Implementation diff --git a/Widevine_CE_CDM_IntegrationGuide_16.2.0.pdf b/Widevine_CE_CDM_IntegrationGuide_16.3.0.pdf similarity index 85% rename from Widevine_CE_CDM_IntegrationGuide_16.2.0.pdf rename to Widevine_CE_CDM_IntegrationGuide_16.3.0.pdf index 18954d32ea15f608696669f6221d39ccdd129abd..e37306279a5eded9ded9cccc83daedac82104f02 100644 GIT binary patch delta 67131 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 delta 66813 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

pP!G+4Km6OA?Lx^6&A!XTyZ$qC@re5@~ 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 diff --git a/build.py b/build.py index c282828e..3c95b3be 100755 --- a/build.py +++ b/build.py @@ -1,4 +1,4 @@ -#!/usr/bin/python3 +#!/usr/bin/python3 -B # 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. @@ -247,6 +247,9 @@ def main(args): 'jobs will spawn without limit.') parser.add_argument('-D', action='append', default=[], help='Pass variable definitions to gyp.') + parser.add_argument('-e', '--extra_gyp', action='append', default=[], + help='External gyp file that are processed after the ' + 'standard gyp files. (Maybe be specified multiple times)') parser.add_argument('-ft', '--fuzz_tests', default=False, action='store_true', help='Set this flag if you want to build fuzz tests.') @@ -270,6 +273,9 @@ def main(args): os.path.join(OEMCRYPTO_FUZZTEST_DIR_PATH, 'oemcrypto_fuzztests.gyp')) else: gyp_args.append(os.path.join(CDM_TOP_PATH, 'cdm', 'cdm_unittests.gyp')) + for var in options.extra_gyp: + gyp_args.append(var) + for var in options.D: gyp_args.append('-D' + var) diff --git a/cdm/cdm.gyp b/cdm/cdm.gyp index b3c7cba8..0e788716 100644 --- a/cdm/cdm.gyp +++ b/cdm/cdm.gyp @@ -52,6 +52,14 @@ 'proto_in_dir': '../metrics/src', }, }, + { + 'target_name': 'widevine_utils', + 'type': 'static_library', + 'include_dirs': [ + '../util/include', + ], + 'sources': [ '<@(wvutil_sources)'], + }, { 'target_name': 'widevine_cdm_core', 'type': 'static_library', @@ -60,6 +68,7 @@ 'device_files', 'license_protocol', 'metrics_proto', + 'widevine_utils', ], 'include_dirs': [ '../core/include', @@ -82,8 +91,6 @@ }, 'sources': [ '<@(wvcdm_sources)', - '<@(wvutil_sources)', - '../core/src/oemcrypto_adapter_static.cpp', '../third_party/jsmn/jsmn.h', '../third_party/jsmn/jsmn.c', ], @@ -98,9 +105,29 @@ '../core/src/privacy_crypto_<(privacy_crypto_impl).cpp', ], }], # end else + ['oemcrypto_adapter_type=="dynamic"', { + 'sources': [ + '../core/src/oemcrypto_adapter_dynamic.cpp', + ], + }], + ['oemcrypto_adapter_type=="static"', { + 'sources': [ + '../core/src/oemcrypto_adapter_static.cpp', + ], + }], + # TODO(b/139814713): For testing and internal use only. + ['oemcrypto_adapter_type=="static_v15"', { + 'sources': [ + '../core/src/oemcrypto_adapter_static.cpp', + '../core/src/oemcrypto_adapter_static_v16.cpp', + ], + }], ], }, # widevine_cdm_core target { + # This is the widevine_ce_cdm built as a static library. This name does + # not mean that it uses oemcrypto's static adapter. Control over which + # adapter is used comes from the settings file for the platform. 'target_name': 'widevine_ce_cdm_static', 'type': 'static_library', 'standalone_static_library': 1, diff --git a/cdm/cdm_unittests.gyp b/cdm/cdm_unittests.gyp index 678f0f42..42dd7c13 100644 --- a/cdm/cdm_unittests.gyp +++ b/cdm/cdm_unittests.gyp @@ -11,8 +11,6 @@ 'variables': { # Directory where OEMCrypto header, test, and reference files lives. 'oemcrypto_dir': '../oemcrypto', - # Label as 'static' or 'dynamic' to use the respective OEMCrypto adapter. - 'oemcrypto_adapter': 'static', # Directory where widevine utilities live. 'util_dir': '../util', 'metrics_target': 'cdm.gyp:metrics_proto', @@ -66,27 +64,28 @@ 'oec_ref', ], }], - ['oemcrypto_lib=="level3"', { + # TODO(b/139814713): For testing and internal use only. + ['oemcrypto_adapter_type=="static_v15"', { 'defines': [ # This is used by the unit tests to use some v15 functions. 'TEST_OEMCRYPTO_V15', ], + }], + ['oemcrypto_lib=="level3"', { 'sources': [ # The test impl of OEMCrypto_Level3FileSystem and its factory. 'test/level3_file_system_ce_test.h', 'test/level3_file_system_ce_test.cpp', 'test/level3_file_system_ce_test_factory.cpp', - # TODO(139814713): Remove once Level 3 supports version 16. - '../core/src/oemcrypto_adapter_static_v16.cpp', ], 'conditions': [ - ['oemcrypto_adapter=="static"', { + ['oemcrypto_adapter_type=="dynamic" ', { 'dependencies': [ - '../oemcrypto/level3/oec_level3.gyp:oec_level3_static', + '../oemcrypto/level3/oec_level3.gyp:oec_level3_dynamic', ], }, { 'dependencies': [ - '../oemcrypto/level3/oec_level3.gyp:oec_level3_dynamic', + '../oemcrypto/level3/oec_level3.gyp:oec_level3_static', ], }], ], @@ -114,6 +113,5 @@ 'oec_ref' ], }, - ], } diff --git a/cdm/include/cdm_version.h b/cdm/include/cdm_version.h index 725e49d4..b0a71283 100644 --- a/cdm/include/cdm_version.h +++ b/cdm/include/cdm_version.h @@ -1,5 +1,5 @@ // Widevine CE CDM Version #ifndef CDM_VERSION -# define CDM_VERSION "16.2.0" +# define CDM_VERSION "16.3.0" #endif #define EME_VERSION "https://www.w3.org/TR/2017/REC-encrypted-media-20170918" diff --git a/cdm/platform_properties.gypi b/cdm/platform_properties.gypi index b4d694f1..2c1adf60 100644 --- a/cdm/platform_properties.gypi +++ b/cdm/platform_properties.gypi @@ -41,6 +41,11 @@ # You only need to set this value if you set 'oemcrypto_lib' to 'target' # above. 'oemcrypto_gyp_target%': '', + # Choose the oemcrypto adapter type. Valid values are: + # + # 'static' - (default). Statically link oemcrypto into the tests. + # other values - for internal testing. + 'oemcrypto_adapter_type%': 'static', # Override this to indicate what CPU architecture's assembly-language files # should be used when building assembly language files. Or, set it to diff --git a/cdm/src/cdm.cpp b/cdm/src/cdm.cpp index 5b04cc67..e388462e 100644 --- a/cdm/src/cdm.cpp +++ b/cdm/src/cdm.cpp @@ -102,6 +102,8 @@ class PropertySet final : public CdmClientPropertySet { return empty_string_; } + bool use_atsc_mode() const override { return false; } + private: bool use_privacy_mode_; std::string licensing_service_certificate_; @@ -498,7 +500,7 @@ Cdm::Status CdmImpl::getProvisioningRequest(std::string* request) { std::string ignored_base_url; CdmResponseType result = cdm_engine_->GetProvisioningRequest( kCertificateWidevine, empty_authority, provisioning_service_certificate_, - request, &ignored_base_url); + kLevelDefault, request, &ignored_base_url); if (result == CERT_PROVISIONING_NONCE_GENERATION_ERROR) { LOGE("Nonce quota exceeded"); return kResourceContention; @@ -517,7 +519,7 @@ Cdm::Status CdmImpl::handleProvisioningResponse(const std::string& response) { std::string ignored_wrapped_key; CdmResponseType result = cdm_engine_->HandleProvisioningResponse( - response, &ignored_cert, &ignored_wrapped_key); + response, kLevelDefault, &ignored_cert, &ignored_wrapped_key); if (result == SYSTEM_INVALIDATED_ERROR) { LOGE("System invalidated"); return kSystemStateLost; diff --git a/cdm/src/properties_ce.cpp b/cdm/src/properties_ce.cpp index 60d22e44..f6ab1481 100644 --- a/cdm/src/properties_ce.cpp +++ b/cdm/src/properties_ce.cpp @@ -149,9 +149,10 @@ bool Properties::GetFactoryKeyboxPath(std::string*) { } // static -bool Properties::GetOEMCryptoPath(std::string*) { - // Unused on CE devices. - return false; +bool Properties::GetOEMCryptoPath(std::string* path) { + if (path == nullptr) return false; + *path = "liboemcrypto.so"; + return true; } // static diff --git a/cdm/test/cdm_test.cpp b/cdm/test/cdm_test.cpp index e9cb515e..8cd047b3 100644 --- a/cdm/test/cdm_test.cpp +++ b/cdm/test/cdm_test.cpp @@ -26,6 +26,7 @@ #include "test_base.h" #include "test_host.h" #include "test_printers.h" +#include "test_sleep.h" #include "url_request.h" using namespace testing; @@ -58,7 +59,7 @@ const std::string kCencInitData = a2bs_hex( "edef8ba979d64acea3c827dcd51d21ed" // Widevine system id "00000022" // pssh data size // pssh data: - "08011a0d7769646576696e655f746573" + "08011a0d7769646576696e655f746573" // "streaming_clip9" "74220f73747265616d696e675f636c69" "7039"); const std::string kCencPersistentInitData = a2bs_hex( @@ -68,7 +69,7 @@ const std::string kCencPersistentInitData = a2bs_hex( "edef8ba979d64acea3c827dcd51d21ed" // Widevine system id "00000020" // pssh data size // pssh data: - "08011a0d7769646576696e655f746573" + "08011a0d7769646576696e655f746573" // "offline_clip6" "74220d6f66666c696e655f636c697036"); const std::string kInvalidCencInitData = a2bs_hex( "0000000c" // blob size @@ -83,7 +84,7 @@ const std::string kNonWidevineCencInitData = a2bs_hex( const std::string kWebMInitData = a2bs_hex("deadbeefdeadbeefdeadbeefdeadbeef"); const std::string kKeyIdsInitData = "{\"kids\":[\"67ef0gd8pvfd0\",\"77ef0gd8pvfd0\"]}"; -const std::string kHlsInitData = +const std::string kHlsInitData = // content_id = "bigbuckbunny" "#EXT-X-KEY:METHOD=SAMPLE-AES,KEYFORMAT=\"com.widevine\",KEYFORMATVERSIONS=" "\"1\",URI=\"data:text/plain;base64,ew0KICAgInByb3ZpZGVyIjogIndpZGV2aW5lX3R" "lc3QiLA0KICAgImNvbnRlbnRfaWQiOiAiWW1sblluVmphMkoxYm01NSIsDQogICAia2V5X2lkc" @@ -331,19 +332,27 @@ class CdmTest : public WvCdmTestBase, public Cdm::IEventListener { Cdm::Status status = cdm_->createSession(session_type, session_id); ASSERT_EQ(Cdm::kSuccess, status); + std::string init_data_type_name; std::string init_data; if (session_type == Cdm::kTemporary) { if (init_data_type == Cdm::kCenc) { + init_data_type_name = CENC_INIT_DATA_FORMAT; init_data = kCencInitData; } else if (init_data_type == Cdm::kHls) { + init_data_type_name = HLS_INIT_DATA_FORMAT; init_data = kHlsInitData; } } else if (session_type == Cdm::kPersistentLicense || session_type == Cdm::kPersistentUsageRecord) { if (init_data_type == Cdm::kCenc) { init_data = kCencPersistentInitData; + init_data_type_name = CENC_INIT_DATA_FORMAT; } } + if (g_cutoff >= LOG_DEBUG) { + InitializationData parsed_init_data(init_data_type_name, init_data); + parsed_init_data.DumpToLogs(); + } ASSERT_FALSE(init_data.empty()); EXPECT_CALL(*this, onMessage(*session_id, Cdm::kLicenseRequest, _)) @@ -397,26 +406,19 @@ class CdmTest : public WvCdmTestBase, public Cdm::IEventListener { Mock::VerifyAndClear(this); } - std::string GetProvisioningResponse(const std::string& message, - size_t max_attempts = 10) { + std::string GetProvisioningResponse(const std::string& message) { 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; - } + std::string reply; + FetchCertificate(uri, &reply); + if (HasFatalFailure()) { + LOGE("Failed to get provisioning response"); + reply.clear(); + } else { + LOGV("GetProvisioningResponse: response:\n%s\n", reply.c_str()); } return reply; } @@ -443,6 +445,10 @@ class CdmTest : public WvCdmTestBase, public Cdm::IEventListener { Cdm::InitDataType init_data_type, const std::string& init_data) { const int num_retries = 5; + if (init_data_type == Cdm::kCenc && g_cutoff >= LOG_DEBUG) { + InitializationData parsed_init_data(CENC_INIT_DATA_FORMAT, init_data); + parsed_init_data.DumpToLogs(); + } for (int i = 0; i < num_retries; i++) { LOGD("attempt %d", i); Cdm::Status status = @@ -512,18 +518,19 @@ class MockTimerClient : public Cdm::ITimer::IClient { MOCK_METHOD1(onTimerExpired, void(void*)); }; -} // namespace - TEST_F(CdmTest, TestHostTimer) { - // Validate that the TestHost timers are processed in the correct order. + // Validate that the TestHost timers are processed in the correct order and + // on the correct timeouts. const int64_t kTimerDelayMs = 1000; void* kCtx1 = reinterpret_cast(0x1); void* kCtx2 = reinterpret_cast(0x2); + void* kCtx4 = reinterpret_cast(0x4); MockTimerClient client; g_host->setTimeout(kTimerDelayMs * 1, &client, kCtx1); g_host->setTimeout(kTimerDelayMs * 2, &client, kCtx2); + g_host->setTimeout(kTimerDelayMs * 4, &client, kCtx4); EXPECT_CALL(client, onTimerExpired(kCtx1)); g_host->ElapseTime(kTimerDelayMs); @@ -536,6 +543,14 @@ TEST_F(CdmTest, TestHostTimer) { EXPECT_CALL(client, onTimerExpired(_)).Times(0); g_host->ElapseTime(kTimerDelayMs); Mock::VerifyAndClear(&client); + + EXPECT_CALL(client, onTimerExpired(kCtx4)); + g_host->ElapseTime(kTimerDelayMs); + Mock::VerifyAndClear(&client); + + EXPECT_CALL(client, onTimerExpired(_)).Times(0); + g_host->ElapseTime(kTimerDelayMs); + Mock::VerifyAndClear(&client); } TEST_F(CdmTest, Initialize) { @@ -1649,6 +1664,65 @@ TEST_F(CdmTest, RemoveThreeUsageRecords) { ASSERT_EQ(Cdm::kSessionNotFound, status); } +// Reload an offline license that does not have a usage entry. +TEST_F(CdmTest, LoadPersistentNoNonce) { + EnsureProvisioned(); + + std::string session_id; + ASSERT_EQ(Cdm::kSuccess, + cdm_->createSession(Cdm::kPersistentLicense, &session_id)); + + video_widevine::WidevinePsshData pssh; + // offline_clip1 does not have a provider session token, so it will not + // generate a usage table entry. + pssh.set_content_id("offline_clip1"); + const std::string init_data = MakePSSH(pssh); + 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, init_data)); + 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); + } + + // Should be able to load the session again after closing it. + Cdm::Status 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 after 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, RemoveIncomplete) { EnsureProvisioned(); std::string session_id; @@ -2414,4 +2488,6 @@ TEST_F(CdmIndividualizationTest, NoLoadWithoutProvisioning) { EXPECT_EQ(Cdm::kNeedsDeviceCertificate, cdm_->load(kBogusSessionId)); } +} // namespace + } // namespace widevine diff --git a/cdm/test/test_host.cpp b/cdm/test/test_host.cpp index 16bf835f..2d25db4b 100644 --- a/cdm/test/test_host.cpp +++ b/cdm/test/test_host.cpp @@ -13,7 +13,7 @@ using namespace widevine; namespace { -const std::string kCertificateFilename = "cert.bin"; +constexpr char kCertificateFilename[] = "cert.bin"; } // namespace @@ -33,7 +33,7 @@ void TestHost::Reset() { } files_.clear(); - files_[kCertificateFilename.c_str()] = + files_[kCertificateFilename] = (device_cert_.size() > 0) ? device_cert_ : std::string((const char*)kDeviceCert, kDeviceCertSize); @@ -46,10 +46,14 @@ void TestHost::ElapseTime(int64_t milliseconds) { now_ = goal_time; } else { Timer t = timers_.top(); - timers_.pop(); ASSERT_GE(t.expiry_time(), now_); - now_ = t.expiry_time(); - t.client()->onTimerExpired(t.context()); + if (t.expiry_time() <= goal_time) { + timers_.pop(); + now_ = t.expiry_time(); + t.client()->onTimerExpired(t.context()); + } else { + now_ = goal_time; + } } } } @@ -68,7 +72,7 @@ bool TestHost::read(const std::string& name, std::string* data) { bool TestHost::write(const std::string& name, const std::string& data) { LOGV("write file: %s", name.c_str()); files_[name] = data; - if (save_device_cert_ && kCertificateFilename.compare(name) == 0) { + if (save_device_cert_ && name.compare(kCertificateFilename) == 0) { device_cert_ = data; save_device_cert_ = false; } @@ -87,10 +91,10 @@ bool TestHost::remove(const std::string& name) { if (name.empty()) { // If no name, delete all files (see DeviceFiles::DeleteAllFiles()) files_.clear(); - } else { - files_.erase(name); + return true; } - return true; + + return files_.erase(name) > 0; } int32_t TestHost::size(const std::string& name) { diff --git a/cdm/test/test_host.h b/cdm/test/test_host.h index f9d005de..ff7316ff 100644 --- a/cdm/test/test_host.h +++ b/cdm/test/test_host.h @@ -4,12 +4,17 @@ #ifndef WVCDM_CDM_TEST_TEST_HOST_H_ #define WVCDM_CDM_TEST_TEST_HOST_H_ +#include #include +#include #include #include "cdm.h" #include "test_sleep.h" +// This provides a host environment for running CDM tests. It implements the +// IStorage, IClock and ITimer interfaces that a host would normally implement, +// while allowing them to be manipulated by the test. class TestHost : public widevine::Cdm::IStorage, public widevine::Cdm::IClock, public widevine::Cdm::ITimer, @@ -19,11 +24,15 @@ class TestHost : public widevine::Cdm::IStorage, ~TestHost(); void Reset(); + // Used for manipulating and inspecting timer states during testing. void ElapseTime(int64_t milliseconds) override; int NumTimers() const; + // This should be called before trying to write the cert.bin file. This is + // used when testing device provisioning. void SaveProvisioningInformation() { save_device_cert_ = true; } + // widevine::Cdm::IStorage bool read(const std::string& name, std::string* data) override; bool write(const std::string& name, const std::string& data) override; bool exists(const std::string& name) override; @@ -31,8 +40,10 @@ class TestHost : public widevine::Cdm::IStorage, int32_t size(const std::string& name) override; bool list(std::vector* names) override; + // widevine::Cdm::IClock int64_t now() override; + // widevine::Cdm::ITimer void setTimeout(int64_t delay_ms, IClient* client, void* context) override; void cancel(IClient* client) override; diff --git a/cdm/util_unittests.gypi b/cdm/util_unittests.gypi index 1c9f60d3..d480cc9d 100644 --- a/cdm/util_unittests.gypi +++ b/cdm/util_unittests.gypi @@ -4,7 +4,9 @@ { 'sources': [ '../util/test/base64_test.cpp', - '../util/test/cdm_random_unittest.cpp' + '../util/test/cdm_random_unittest.cpp', + # TODO(b/119200528): Needs test vectors + # '../util/test/file_store_unittest.cpp', ], 'include_dirs': [ '../util/include' diff --git a/core/include/cdm_client_property_set.h b/core/include/cdm_client_property_set.h index 38398ebe..e6c7ba4c 100644 --- a/core/include/cdm_client_property_set.h +++ b/core/include/cdm_client_property_set.h @@ -24,6 +24,7 @@ class CdmClientPropertySet { virtual uint32_t session_sharing_id() const = 0; virtual void set_session_sharing_id(uint32_t id) = 0; virtual const std::string& app_id() const = 0; + virtual bool use_atsc_mode() const = 0; }; } // namespace wvcdm diff --git a/core/include/cdm_engine.h b/core/include/cdm_engine.h index 963fa74a..16a4e1f1 100644 --- a/core/include/cdm_engine.h +++ b/core/include/cdm_engine.h @@ -179,12 +179,14 @@ class CdmEngine { // Generate and return a valid provisioning request. virtual CdmResponseType GetProvisioningRequest( CdmCertificateType cert_type, const std::string& cert_authority, - const std::string& service_certificate, CdmProvisioningRequest* request, + const std::string& service_certificate, + SecurityLevel requested_security_level, CdmProvisioningRequest* request, std::string* default_url); // Verify and process a provisioning response. virtual CdmResponseType HandleProvisioningResponse( - const CdmProvisioningResponse& response, std::string* cert, + const CdmProvisioningResponse& response, + SecurityLevel requested_security_level, std::string* cert, std::string* wrapped_key); // Return true if there is a device certificate on the current @@ -388,7 +390,6 @@ class CdmEngine { CdmSessionMap session_map_; CdmReleaseKeySetMap release_key_sets_; std::unique_ptr cert_provisioning_; - SecurityLevel cert_provisioning_requested_security_level_; FileSystem* file_system_; Clock clock_; std::string spoid_; diff --git a/core/include/cdm_engine_metrics_decorator.h b/core/include/cdm_engine_metrics_decorator.h index befbd9f6..5eb7d9a0 100644 --- a/core/include/cdm_engine_metrics_decorator.h +++ b/core/include/cdm_engine_metrics_decorator.h @@ -156,21 +156,24 @@ class CdmEngineMetricsImpl : public T { CdmResponseType GetProvisioningRequest(CdmCertificateType cert_type, const std::string& cert_authority, const std::string& service_certificate, + SecurityLevel requested_security_level, CdmProvisioningRequest* request, std::string* default_url) override { CdmResponseType sts; - M_TIME(sts = T::GetProvisioningRequest(cert_type, cert_authority, - service_certificate, request, - default_url), + M_TIME(sts = T::GetProvisioningRequest( + cert_type, cert_authority, service_certificate, + requested_security_level, request, default_url), metrics_, cdm_engine_get_provisioning_request_, sts); return sts; } CdmResponseType HandleProvisioningResponse( - const CdmProvisioningResponse& response, std::string* cert, + const CdmProvisioningResponse& response, + SecurityLevel requested_security_level, std::string* cert, std::string* wrapped_key) override { CdmResponseType sts; - M_TIME(sts = T::HandleProvisioningResponse(response, cert, wrapped_key), + M_TIME(sts = T::HandleProvisioningResponse( + response, requested_security_level, cert, wrapped_key), metrics_, cdm_engine_handle_provisioning_response_, sts); return sts; } diff --git a/core/include/cdm_session.h b/core/include/cdm_session.h index 8859093a..6325e49e 100644 --- a/core/include/cdm_session.h +++ b/core/include/cdm_session.h @@ -219,7 +219,7 @@ class CdmSession { private: friend class CdmSessionTest; - bool GenerateKeySetId(CdmKeySetId* key_set_id); + bool GenerateKeySetId(bool atsc_mode_enabled, CdmKeySetId* key_set_id); CdmResponseType StoreLicense(); @@ -233,6 +233,12 @@ class CdmSession { virtual CdmResponseType AddKeyInternal(const CdmKeyResponse& key_response); void UpdateRequestLatencyTiming(CdmResponseType sts); + // Checks that the usage entry in the usage table header matches the + // information of the currently loaded license for this session. + // Returns false if there is any unexpected mismatch of information, + // true otherwise. + bool VerifyOfflineUsageEntry(); + // These setters are for testing only. Takes ownership of the pointers. void set_license_parser(CdmLicense* license_parser); void set_crypto_session(CryptoSession* crypto_session); diff --git a/core/include/crypto_session.h b/core/include/crypto_session.h index 1fecdd25..1e44b94a 100644 --- a/core/include/crypto_session.h +++ b/core/include/crypto_session.h @@ -89,7 +89,20 @@ class CryptoSession { virtual bool GetApiMinorVersion(SecurityLevel requested_level, uint32_t* minor_version); + // This method will return, for devices with a + // * keybox: the 32 byte device ID from the keybox. + // * OEM certificate: + // - that implements |OEMCrypto_GetDeviceID|: the (1 to 64 byte) device ID. + // - that does not implement |OEMCrypto_GetDeviceID|: the OEM public + // certificate. virtual CdmResponseType GetInternalDeviceUniqueId(std::string* device_id); + + // This method will return, for devices with a + // * keybox: the 32 byte device ID from the keybox. + // * OEM certificate: + // - that implements |OEMCrypto_GetDeviceID|: the (1 to 64 byte) device ID. + // - that does not implement |OEMCrypto_GetDeviceID|: the 32 byte hash + // of the OEM public certificate. virtual CdmResponseType GetExternalDeviceUniqueId(std::string* device_id); virtual bool GetSystemId(uint32_t* system_id); virtual CdmResponseType GetProvisioningId(std::string* provisioning_id); @@ -242,11 +255,17 @@ class CryptoSession { virtual UsageTableHeader* GetUsageTableHeader() { return usage_table_header_; } - + // The following crypto methods do not require an open session to + // complete the operations. virtual CdmResponseType CreateUsageTableHeader( + SecurityLevel requested_security_level, CdmUsageTableHeader* usage_table_header); virtual CdmResponseType LoadUsageTableHeader( + SecurityLevel requested_security_level, const CdmUsageTableHeader& usage_table_header); + virtual CdmResponseType ShrinkUsageTableHeader( + SecurityLevel requested_security_level, uint32_t new_entry_count, + CdmUsageTableHeader* usage_table_header); // Usage entry. virtual CdmResponseType CreateUsageEntry(uint32_t* entry_number); @@ -256,8 +275,6 @@ class CryptoSession { CdmUsageTableHeader* usage_table_header, CdmUsageEntry* usage_entry); // Adjust usage entries in usage table header. - virtual CdmResponseType ShrinkUsageTableHeader( - uint32_t new_entry_count, CdmUsageTableHeader* usage_table_header); virtual CdmResponseType MoveUsageEntry(uint32_t new_entry_number); virtual bool GetAnalogOutputCapabilities(bool* can_support_output, diff --git a/core/include/device_files.h b/core/include/device_files.h index 5de50572..70f55e36 100644 --- a/core/include/device_files.h +++ b/core/include/device_files.h @@ -95,13 +95,16 @@ class DeviceFiles { return Init(security_level); } + // ATSC certificates are installed by the ATSC service. They can be read + // and used but not written or removed. virtual bool StoreCertificate(const std::string& certificate, const std::string& wrapped_private_key); - virtual bool RetrieveCertificate(std::string* certificate, + virtual bool RetrieveCertificate(bool atsc_mode_enabled, + std::string* certificate, std::string* wrapped_private_key, std::string* serial_number, uint32_t* system_id); - virtual bool HasCertificate(); + virtual bool HasCertificate(bool atsc_mode_enabled); virtual bool RemoveCertificate(); virtual bool StoreLicense(const CdmLicenseData& license_data, @@ -256,7 +259,7 @@ class DeviceFiles { bool RemoveFile(const std::string& name); ssize_t GetFileSize(const std::string& name); - static std::string GetCertificateFileName(); + static std::string GetCertificateFileName(bool atsc_mode_enabled); static std::string GetHlsAttributesFileNameExtension(); static std::string GetLicenseFileNameExtension(); static std::string GetUsageTableFileName(); @@ -264,8 +267,8 @@ class DeviceFiles { #if defined(UNIT_TEST) FRIEND_TEST(DeviceFilesSecurityLevelTest, SecurityLevel); - FRIEND_TEST(DeviceCertificateStoreTest, StoreCertificate); - FRIEND_TEST(DeviceCertificateTest, DISABLED_ReadCertificate); + FRIEND_TEST(DeviceCertificateTest, StoreCertificate); + FRIEND_TEST(DeviceCertificateTest, ReadCertificate); FRIEND_TEST(DeviceCertificateTest, HasCertificate); FRIEND_TEST(DeviceFilesStoreTest, StoreLicense); FRIEND_TEST(DeviceFilesHlsAttributesTest, Delete); diff --git a/core/include/license.h b/core/include/license.h index 95c307e4..9d566803 100644 --- a/core/include/license.h +++ b/core/include/license.h @@ -176,6 +176,9 @@ class CdmLicense { // HandleKeyResponse VersionInfo latest_service_version_; + // The nonce used in the original license request. + uint32_t license_nonce_; + #if defined(UNIT_TEST) friend class CdmLicenseTestPeer; #endif diff --git a/core/include/usage_table_header.h b/core/include/usage_table_header.h index 931048a7..d63bdcb1 100644 --- a/core/include/usage_table_header.h +++ b/core/include/usage_table_header.h @@ -73,23 +73,37 @@ class UsageTableHeader { // The licenses or usage info records specified by |usage_entry_number| // should not be in use by any open CryptoSession objects when calls - // to DeleteEntry and MoveEntry are made. - virtual CdmResponseType DeleteEntry(uint32_t usage_entry_number, - DeviceFiles* handle, - metrics::CryptoMetrics* metrics); + // to InvalidateEntry and MoveEntry are made. + // If |defrag_table| is true, the table will be defragmented after + // the entry has been invalidated. + virtual CdmResponseType InvalidateEntry(uint32_t usage_entry_number, + bool defrag_table, + DeviceFiles* device_files, + metrics::CryptoMetrics* metrics); - // Test only method. This method emulates the behavior of DeleteEntry + // Test only method. This method emulates the behavior of InvalidateEntry // without actually invoking OEMCrypto (through CryptoSession) // or storage (through DeviceFiles). It modifies internal data structures - // when DeleteEntry is mocked. This allows one to test methods that are - // dependent on DeleteEntry without having to set expectations - // for the objects that DeleteEntry depends on. - void DeleteEntryForTest(uint32_t usage_entry_number); + // when InvalidateEntry is mocked. This allows one to test methods that are + // dependent on InvalidateEntry without having to set expectations + // for the objects that InvalidateEntry depends on. + void InvalidateEntryForTest(uint32_t usage_entry_number); size_t size() { return usage_entry_info_.size(); } size_t potential_table_capacity() const { return potential_table_capacity_; } + bool HasUnlimitedTableCapacity() const { + return potential_table_capacity_ == 0; + } + + // Returns the number of entries currently tracked by the CDM that + // are related to usage info (streaming licenses). + size_t UsageInfoCount() const; + // Returns the number of entries currently tracked by the CDM that + // are related to offline licenses. + size_t OfflineEntryCount() const; + const std::vector& usage_entry_info() const { return usage_entry_info_; } @@ -104,39 +118,58 @@ class UsageTableHeader { static bool DetermineLicenseToRemoveForTesting( const std::vector& usage_entry_info_list, - int64_t current_time, size_t unexpired_threshold, size_t removal_count, - std::vector* removal_candidates) { + int64_t current_time, size_t unexpired_threshold, + uint32_t* entry_to_remove) { return DetermineLicenseToRemove(usage_entry_info_list, current_time, - unexpired_threshold, removal_count, - removal_candidates); + unexpired_threshold, entry_to_remove); } private: CdmResponseType MoveEntry(uint32_t from /* usage entry number */, const CdmUsageEntry& from_usage_entry, uint32_t to /* usage entry number */, - DeviceFiles* handle, + DeviceFiles* device_files, metrics::CryptoMetrics* metrics); - CdmResponseType GetEntry(uint32_t usage_entry_number, DeviceFiles* handle, + CdmResponseType GetEntry(uint32_t usage_entry_number, + DeviceFiles* device_files, CdmUsageEntry* usage_entry); - CdmResponseType StoreEntry(uint32_t usage_entry_number, DeviceFiles* handle, + CdmResponseType StoreEntry(uint32_t usage_entry_number, + DeviceFiles* device_files, const CdmUsageEntry& usage_entry); + // Stores the usage table and it's info. This will increment + // |store_table_counter_| if successful. + bool StoreTable(DeviceFiles* device_files); + CdmResponseType Shrink(metrics::CryptoMetrics* metrics, uint32_t number_of_usage_entries_to_delete); + // Must lock table before calling. + CdmResponseType DefragTable(DeviceFiles* device_files, + metrics::CryptoMetrics* metrics); + + // This will use the LRU algorithm to decide which entry is to be + // evicted. + CdmResponseType ReleaseOldestEntry(metrics::CryptoMetrics* metrics); + virtual bool is_inited() { return is_inited_; } // Performs and LRU upgrade on all loaded CdmUsageEntryInfo from a // device file that had not yet been upgraded to use the LRU data. virtual bool LruUpgradeAllUsageEntries(); - virtual bool GetRemovalCandidates(std::vector* removal_candidates); + virtual bool GetRemovalCandidate(uint32_t* entry_to_remove); int64_t GetCurrentTime() { return clock_ref_->GetCurrentTime(); } - // Uses an LRU-base algorithm to determine which licenses should be + // Sets LRU related metrics based on the provided |staleness| (in + // seconds) and |storage_type| of the entry removed. + void RecordLruEventMetrics(metrics::CryptoMetrics* metrics, + uint64_t staleness, + CdmUsageEntryStorageType storage_type); + + // Uses an LRU-base algorithm to determine which license should be // removed. This is intended to be used if the usage table is full // and a new entry needs to be added. // @@ -151,8 +184,6 @@ class UsageTableHeader { // 2) Unexpired offline licenses will only be considered for // removal if the number of unexpired offline licenses exceeds // |unexpired_threshold|. - // The number of licenses to be considered will be less than or - // equal to the requested |removal_count|. // // Unknown storage types will be considered above all other entry // types. @@ -165,26 +196,22 @@ class UsageTableHeader { // [in] unexpired_threshold: The maximum number of unexpired // offline licenses that are present, before offline // licenses would be considered for removal. - // [in] removal_count: The desired number of removal candidate to - // find. Note that the actual number will be anywhere - // between 1 and |removal_count|. Must be greater than or - // equal to 1. - // [out] removal_candidates: List of usage entry numbers of the - // entries to be removed. Assume to be unaffected if the + // [out] entry_to_remove: Usage entry index of the entry selected + // to be removed. Assume to be unaffected if the // function returns |false|. // // Returns: - // |true| if at least one removal candidate can be determined. + // |true| if an entry has been determined to be removed. // Otherwise returns |false|. static bool DetermineLicenseToRemove( const std::vector& usage_entry_info_list, - int64_t current_time, size_t unexpired_threshold, size_t removal_count, - std::vector* removal_candidates); + int64_t current_time, size_t unexpired_threshold, + uint32_t* entry_to_remove); // This handle and file system is only to be used when accessing // usage_table_header. Usage entries should use the file system provided // by CdmSession. - std::unique_ptr file_handle_; + std::unique_ptr device_files_; std::unique_ptr file_system_; CdmSecurityLevel security_level_; SecurityLevel requested_security_level_; @@ -199,7 +226,7 @@ class UsageTableHeader { // Synchonizes access to the Usage Table Header and bookkeeping // data-structures - std::mutex usage_table_header_lock_; + mutable std::mutex usage_table_header_lock_; metrics::CryptoMetrics alternate_crypto_metrics_; @@ -217,6 +244,11 @@ class UsageTableHeader { // assumed to be |kMinimumUsageTableEntriesSupported|. size_t potential_table_capacity_ = 0u; + // Counts the number of successful calls to |StoreTable()|. Used + // to reduce the number of calls to device files for certain + // table operations. + uint32_t store_table_counter_ = 0u; + #if defined(UNIT_TEST) // Test related declarations friend class UsageTableHeaderTest; @@ -228,7 +260,7 @@ class UsageTableHeader { // These setters are for testing only. Takes ownership of the pointers. void SetDeviceFiles(DeviceFiles* device_files) { - file_handle_.reset(device_files); + device_files_.reset(device_files); } void SetCryptoSession(CryptoSession* crypto_session) { test_crypto_session_.reset(crypto_session); diff --git a/core/include/wv_cdm_constants.h b/core/include/wv_cdm_constants.h index 5d427e20..3db2a051 100644 --- a/core/include/wv_cdm_constants.h +++ b/core/include/wv_cdm_constants.h @@ -43,8 +43,10 @@ static const uint32_t RESOURCE_RATING_TIER_MAX = RESOURCE_RATING_TIER_VERY_HIGH; static const uint32_t OEM_CRYPTO_API_VERSION_SUPPORTS_RESOURCE_RATING_TIER = 15; static const char SESSION_ID_PREFIX[] = "sid"; +static const char ATSC_KEY_SET_ID_PREFIX[] = "atscksid"; static const char KEY_SET_ID_PREFIX[] = "ksid"; static const char KEY_SYSTEM[] = "com.widevine"; +static const char ATSC_APP_PACKAGE_NAME[] = "org.atsc"; // define query keys, values here static const std::string QUERY_KEY_LICENSE_TYPE = diff --git a/core/include/wv_cdm_types.h b/core/include/wv_cdm_types.h index a6ce404e..3517005f 100644 --- a/core/include/wv_cdm_types.h +++ b/core/include/wv_cdm_types.h @@ -253,7 +253,7 @@ enum CdmResponseType { INVALID_SESSION_1 = 199, NO_DEVICE_KEY_1 = 200, NO_CONTENT_KEY_2 = 201, - INSUFFICIENT_CRYPTO_RESOURCES_2 = 202, + /* previously INSUFFICIENT_CRYPTO_RESOURCES_2 = 202, */ INVALID_PARAMETERS_ENG_13 = 203, INVALID_PARAMETERS_ENG_14 = 204, INVALID_PARAMETERS_ENG_15 = 205, @@ -272,7 +272,7 @@ enum CdmResponseType { LOAD_USAGE_HEADER_UNKNOWN_ERROR = 218, /* previously INVALID_PARAMETERS_ENG_17 = 219, */ /* preivously INVALID_PARAMETERS_ENG_18 = 220, */ - INSUFFICIENT_CRYPTO_RESOURCES_3 = 221, + /* previously INSUFFICIENT_CRYPTO_RESOURCES_3 = 221, */ CREATE_USAGE_ENTRY_UNKNOWN_ERROR = 222, LOAD_USAGE_ENTRY_GENERATION_SKEW = 223, LOAD_USAGE_ENTRY_SIGNATURE_FAILURE = 224, @@ -281,7 +281,7 @@ enum CdmResponseType { /* previsouly INVALID_PARAMETERS_ENG_20 = 227, */ UPDATE_USAGE_ENTRY_UNKNOWN_ERROR = 228, /* previously INVALID_PARAMETERS_ENG_21 = 229, */ - SHRINK_USAGE_TABLER_HEADER_UNKNOWN_ERROR = 230, + SHRINK_USAGE_TABLE_HEADER_UNKNOWN_ERROR = 230, MOVE_USAGE_ENTRY_UNKNOWN_ERROR = 231, COPY_OLD_USAGE_ENTRY_UNKNOWN_ERROR = 232, INVALID_PARAMETERS_ENG_22 = 233, @@ -331,12 +331,12 @@ enum CdmResponseType { /* previously LICENSE_REQUEST_INVALID_SUBLICENSE = 277, */ CERT_PROVISIONING_EMPTY_SERVICE_CERTIFICATE = 278, LOAD_SYSTEM_ID_ERROR = 279, - INSUFFICIENT_CRYPTO_RESOURCES_4 = 280, - INSUFFICIENT_CRYPTO_RESOURCES_5 = 281, + /* previously INSUFFICIENT_CRYPTO_RESOURCES_4 = 280, */ + /* previously INSUFFICIENT_CRYPTO_RESOURCES_5 = 281, */ REMOVE_USAGE_INFO_ERROR_1 = 282, REMOVE_USAGE_INFO_ERROR_2 = 283, REMOVE_USAGE_INFO_ERROR_3 = 284, - INSUFFICIENT_CRYPTO_RESOURCES_6 = 285, + /* previously INSUFFICIENT_CRYPTO_RESOURCES_6 = 285, */ NOT_AN_ENTITLEMENT_SESSION = 286, NO_MATCHING_ENTITLEMENT_KEY = 287, LOAD_ENTITLED_CONTENT_KEYS_ERROR = 288, @@ -408,6 +408,12 @@ enum CdmResponseType { CANNOT_DECRYPT_ZERO_SUBSAMPLES = 354, SAMPLE_AND_SUBSAMPLE_SIZE_MISMATCH = 355, INVALID_IV_SIZE = 356, + PROVISIONING_NOT_ALLOWED_FOR_ATSC = 357, + // 357 was |LOAD_USAGE_ENTRY_INVALID_SESSION| in early R builds + MOVE_USAGE_ENTRY_DESTINATION_IN_USE = 358, + SHRINK_USAGE_TABLE_HEADER_ENTRY_IN_USE = 359, + LICENSE_USAGE_ENTRY_MISSING = 360, + LOAD_USAGE_ENTRY_INVALID_SESSION = 361, // Don't forget to add new values to // * core/test/test_printers.cpp. // * android/include/mapErrors-inl.h @@ -529,6 +535,14 @@ struct CdmUsageEntryInfo { // else storage_type == kStorageTypeUnknown return true; } + + void Clear() { + storage_type = kStorageTypeUnknown; + key_set_id.clear(); + usage_info_file_name.clear(); + last_use_time = 0; + offline_license_expiry_time = 0; + } }; enum CdmKeySecurityLevel { diff --git a/core/src/cdm_engine.cpp b/core/src/cdm_engine.cpp index 0317f6d5..e9f4bd09 100644 --- a/core/src/cdm_engine.cpp +++ b/core/src/cdm_engine.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -62,6 +63,7 @@ class UsagePropertySet : public CdmClientPropertySet { void set_session_sharing_id(uint32_t /* id */) override {} const std::string& app_id() const override { return app_id_; } void set_app_id(const std::string& appId) { app_id_ = appId; } + bool use_atsc_mode() const override { return false; } private: std::string app_id_; @@ -73,7 +75,6 @@ CdmEngine::CdmEngine(FileSystem* file_system, std::shared_ptr metrics) : metrics_(metrics), cert_provisioning_(), - cert_provisioning_requested_security_level_(kLevelDefault), file_system_(file_system), spoid_(EMPTY_SPOID), usage_session_(), @@ -136,8 +137,6 @@ CdmResponseType CdmEngine::OpenSession(const CdmKeySystem& key_system, new_session->Init(property_set, forced_session_id, event_listener); if (sts != NO_ERROR) { if (sts == NEED_PROVISIONING) { - cert_provisioning_requested_security_level_ = - new_session->GetRequestedSecurityLevel(); // Reserve a session ID so the CDM can return success. if (session_id) *session_id = new_session->GenerateSessionId(); } else { @@ -172,10 +171,13 @@ CdmResponseType CdmEngine::OpenKeySetSession( key_set_in_use = release_key_sets_.find(key_set_id) != release_key_sets_.end(); } - if (key_set_in_use) CloseKeySetSession(key_set_id); + if (key_set_in_use) { + LOGD("Reopening existing key session"); + CloseKeySetSession(key_set_id); + } CdmSessionId session_id; - CdmResponseType sts = + const CdmResponseType sts = OpenSession(KEY_SYSTEM, property_set, event_listener, nullptr /* forced_session_id */, &session_id); @@ -294,10 +296,6 @@ CdmResponseType CdmEngine::GenerateKeyRequest( if (KEY_ADDED == sts) { return sts; } else if (KEY_MESSAGE != sts) { - if (sts == NEED_PROVISIONING) { - cert_provisioning_requested_security_level_ = - session->GetRequestedSecurityLevel(); - } LOGE("Key request generation failed, status = %d", static_cast(sts)); return sts; } @@ -414,10 +412,6 @@ CdmResponseType CdmEngine::RestoreKey(const CdmSessionId& session_id, &error_detail); session->GetMetrics()->cdm_session_restore_offline_session_.Increment( sts, error_detail); - if (sts == NEED_PROVISIONING) { - cert_provisioning_requested_security_level_ = - session->GetRequestedSecurityLevel(); - } if (sts != KEY_ADDED && sts != GET_RELEASED_LICENSE_ERROR) { LOGE("Restore offline session failed: status = %d", static_cast(sts)); } @@ -703,6 +697,12 @@ CdmResponseType CdmEngine::QueryStatus(SecurityLevel security_level, LOGW("GetMaxUsageTableEntries failed"); return UNKNOWN_ERROR; } + if (max_number_of_usage_entries == 0) { + // Zero indicates that the table is dynamically allocated and does + // not have a defined limit. Setting to max value of int32_t to + // be able to fit into a Java int. + max_number_of_usage_entries = std::numeric_limits::max(); + } *query_response = std::to_string(max_number_of_usage_entries); return NO_ERROR; } else if (query_token == QUERY_KEY_OEMCRYPTO_API_MINOR_VERSION) { @@ -895,7 +895,8 @@ bool CdmEngine::IsSecurityLevelSupported(CdmSecurityLevel level) { */ CdmResponseType CdmEngine::GetProvisioningRequest( CdmCertificateType cert_type, const std::string& cert_authority, - const std::string& service_certificate, CdmProvisioningRequest* request, + const std::string& service_certificate, + SecurityLevel requested_security_level, CdmProvisioningRequest* request, std::string* default_url) { LOGI("Getting provisioning request"); if (!request) { @@ -915,7 +916,7 @@ CdmResponseType CdmEngine::GetProvisioningRequest( if (status != NO_ERROR) return status; } CdmResponseType ret = cert_provisioning_->GetProvisioningRequest( - cert_provisioning_requested_security_level_, cert_type, cert_authority, + requested_security_level, cert_type, cert_authority, file_system_->origin(), spoid_, request, default_url); if (ret != NO_ERROR) { cert_provisioning_.reset(); // Release resources. @@ -931,7 +932,8 @@ CdmResponseType CdmEngine::GetProvisioningRequest( * Returns NO_ERROR for success and CdmResponseType error code if fails. */ CdmResponseType CdmEngine::HandleProvisioningResponse( - const CdmProvisioningResponse& response, std::string* cert, + const CdmProvisioningResponse& response, + SecurityLevel requested_security_level, std::string* cert, std::string* wrapped_key) { LOGI("Handling provision request"); if (response.empty()) { @@ -955,10 +957,9 @@ CdmResponseType CdmEngine::HandleProvisioningResponse( std::unique_ptr crypto_session( CryptoSession::MakeCryptoSession(metrics_->GetCryptoMetrics())); CdmResponseType status; - M_TIME(status = crypto_session->Open( - cert_provisioning_requested_security_level_), + M_TIME(status = crypto_session->Open(requested_security_level), metrics_->GetCryptoMetrics(), crypto_session_open_, status, - cert_provisioning_requested_security_level_); + requested_security_level); if (NO_ERROR != status) { LOGE("Provisioning object missing and crypto session open failed"); return EMPTY_PROVISIONING_CERTIFICATE_2; @@ -1124,14 +1125,19 @@ CdmResponseType CdmEngine::RemoveOfflineLicense( property_set.set_security_level( security_level == kSecurityLevelL3 ? kLevel3 : kLevelDefault); DeviceFiles handle(file_system_); + + if (!handle.Init(security_level)) { + LOGE("Cannot initialize device files: security_level = %s", + security_level == kSecurityLevelL3 ? "L3" : "Default"); + return REMOVE_OFFLINE_LICENSE_ERROR_1; + } + CdmResponseType sts = OpenKeySetSession(key_set_id, &property_set, nullptr /* event listener */); if (sts != NO_ERROR) { - if (!handle.Init(security_level)) { - LOGE("Cannot initialize device files"); - } + LOGE("Failed to open key set session: status = %d", static_cast(sts)); handle.DeleteLicense(key_set_id); - return REMOVE_OFFLINE_LICENSE_ERROR_1; + return sts; } CdmSessionId session_id; @@ -1151,6 +1157,14 @@ CdmResponseType CdmEngine::RemoveOfflineLicense( session_id = iter->second.first; sts = RemoveLicense(session_id); } + } else if (sts == LICENSE_USAGE_ENTRY_MISSING) { + // It is possible that the CDM is tracking a key set ID, but has + // removed the usage information associated with it. In this case, + // it will no longer be possible to load the license for release; + // and the file should simply be deleted. + LOGW("License usage entry is missing, deleting license file"); + handle.DeleteLicense(key_set_id); + sts = NO_ERROR; } if (sts != NO_ERROR) { diff --git a/core/src/cdm_session.cpp b/core/src/cdm_session.cpp index 33b066b4..29a9f2f0 100644 --- a/core/src/cdm_session.cpp +++ b/core/src/cdm_session.cpp @@ -159,8 +159,12 @@ CdmResponseType CdmSession::Init(CdmClientPropertySet* cdm_client_property_set, // License server client ID token is a stored certificate. Stage it or // indicate that provisioning is needed. Get token from stored certificate std::string wrapped_key; - if (!file_handle_->RetrieveCertificate(&client_token, &wrapped_key, - &serial_number, nullptr)) { + bool atsc_mode_enabled = false; + if (cdm_client_property_set != nullptr) + atsc_mode_enabled = cdm_client_property_set->use_atsc_mode(); + if (!file_handle_->RetrieveCertificate(atsc_mode_enabled, &client_token, + &wrapped_key, &serial_number, + nullptr)) { return NEED_PROVISIONING; } CdmResponseType load_cert_sts; @@ -186,7 +190,7 @@ CdmResponseType CdmSession::Init(CdmClientPropertySet* cdm_client_property_set, if (forced_session_id) { key_set_id_ = *forced_session_id; } else { - bool ok = GenerateKeySetId(&key_set_id_); + bool ok = GenerateKeySetId(atsc_mode_enabled, &key_set_id_); (void)ok; // ok is now used when assertions are turned off. assert(ok); } @@ -282,6 +286,17 @@ CdmResponseType CdmSession::RestoreOfflineSession(const CdmKeySetId& key_set_id, key_response_, &provider_session_token) || usage_table_header_ == nullptr) { provider_session_token.clear(); + std::string fake_message("empty message"); + std::string core_message; + std::string license_request_signature; + // Sign a fake message so that OEMCrypto will start the rental clock. The + // signature and generated core message are ignored. + CdmResponseType status = crypto_session_->PrepareAndSignLicenseRequest( + fake_message, &core_message, &license_request_signature); + if (status != NO_ERROR) return status; + } else if (!VerifyOfflineUsageEntry()) { + LOGE("License usage entry is invalid, cannot restore"); + return LICENSE_USAGE_ENTRY_MISSING; } else { CdmResponseType sts = usage_table_header_->LoadEntry( crypto_session_.get(), usage_entry_, usage_entry_number_); @@ -529,16 +544,18 @@ CdmResponseType CdmSession::AddKeyInternal(const CdmKeyResponse& key_response) { metrics_->license_sdk_version_.Record( version_info.license_service_version()); - // Update or delete entry if usage table header+entries are supported + // Update or invalidate entry if usage table header+entries are supported if (usage_support_type_ == kUsageEntrySupport && !provider_session_token.empty() && usage_table_header_ != nullptr) { if (sts != KEY_ADDED) { - CdmResponseType delete_sts = usage_table_header_->DeleteEntry( - usage_entry_number_, file_handle_.get(), crypto_metrics_); - crypto_metrics_->usage_table_header_delete_entry_.Increment(delete_sts); - if (delete_sts != NO_ERROR) { - LOGW("Delete usage entry failed: status = %d", - static_cast(delete_sts)); + const CdmResponseType invalidate_sts = + usage_table_header_->InvalidateEntry( + usage_entry_number_, true, file_handle_.get(), crypto_metrics_); + crypto_metrics_->usage_table_header_delete_entry_.Increment( + invalidate_sts); + if (invalidate_sts != NO_ERROR) { + LOGW("Invalidate usage entry failed: status = %d", + static_cast(invalidate_sts)); } } } @@ -824,8 +841,8 @@ CdmResponseType CdmSession::DeleteUsageEntry(uint32_t usage_entry_number) { return INCORRECT_USAGE_SUPPORT_TYPE_1; } - sts = usage_table_header_->DeleteEntry(usage_entry_number, file_handle_.get(), - crypto_metrics_); + sts = usage_table_header_->InvalidateEntry( + usage_entry_number, true, file_handle_.get(), crypto_metrics_); crypto_metrics_->usage_table_header_delete_entry_.Increment(sts); return sts; } @@ -844,7 +861,8 @@ CdmSessionId CdmSession::GenerateSessionId() { return SESSION_ID_PREFIX + IntToString(++session_num); } -bool CdmSession::GenerateKeySetId(CdmKeySetId* key_set_id) { +bool CdmSession::GenerateKeySetId(bool atsc_mode_enabled, + CdmKeySetId* key_set_id) { RETURN_FALSE_IF_NULL(key_set_id); std::vector random_data( @@ -856,7 +874,10 @@ bool CdmSession::GenerateKeySetId(CdmKeySetId* key_set_id) { return false; } - *key_set_id = KEY_SET_ID_PREFIX + b2a_hex(random_data); + if (atsc_mode_enabled) + *key_set_id = ATSC_KEY_SET_ID_PREFIX + b2a_hex(random_data); + else + *key_set_id = KEY_SET_ID_PREFIX + b2a_hex(random_data); // key set collision if (file_handle_->LicenseExists(*key_set_id)) { @@ -960,11 +981,10 @@ CdmResponseType CdmSession::RemoveKeys() { } CdmResponseType CdmSession::RemoveLicense() { - CdmResponseType sts = NO_ERROR; if (is_offline_ || has_provider_session_token()) { if (usage_support_type_ == kUsageEntrySupport && has_provider_session_token()) { - sts = DeleteUsageEntry(usage_entry_number_); + DeleteUsageEntry(usage_entry_number_); } DeleteLicenseFile(); } @@ -1131,6 +1151,25 @@ void CdmSession::UpdateRequestLatencyTiming(CdmResponseType sts) { license_request_latency_.Clear(); } +bool CdmSession::VerifyOfflineUsageEntry() { + // Check that the current license is the same as the expected + // entry in the usage table. It is possible that the license has + // been removed from the usage table but the license file remains. + if (usage_entry_number_ >= usage_table_header_->size()) { + LOGD("License usage entry does not exist: entry_number = %u, size = %zu", + usage_entry_number_, usage_table_header_->size()); + return false; + } + const CdmUsageEntryInfo& usage_entry_info = + usage_table_header_->usage_entry_info().at(usage_entry_number_); + if (usage_entry_info.storage_type != kStorageLicense || + usage_entry_info.key_set_id != key_set_id_) { + LOGD("License usage entry does not match"); + return false; + } + return true; +} + // For testing only - takes ownership of pointers void CdmSession::set_license_parser(CdmLicense* license_parser) { diff --git a/core/src/crypto_session.cpp b/core/src/crypto_session.cpp index c8f39394..9d72bce3 100644 --- a/core/src/crypto_session.cpp +++ b/core/src/crypto_session.cpp @@ -708,12 +708,10 @@ uint8_t CryptoSession::GetSecurityPatchLevel() { } CdmResponseType CryptoSession::Open(SecurityLevel requested_security_level) { - LOGD( - "Opening crypto session: requested_security_level: " - "requested_security_level = %s", - requested_security_level == kLevel3 - ? QUERY_VALUE_SECURITY_LEVEL_L3.c_str() - : QUERY_VALUE_SECURITY_LEVEL_DEFAULT.c_str()); + LOGD("Opening crypto session: requested_security_level = %s", + requested_security_level == kLevel3 + ? QUERY_VALUE_SECURITY_LEVEL_L3.c_str() + : QUERY_VALUE_SECURITY_LEVEL_DEFAULT.c_str()); RETURN_IF_UNINITIALIZED(UNKNOWN_ERROR); if (open_) return NO_ERROR; @@ -750,12 +748,13 @@ CdmResponseType CryptoSession::Open(SecurityLevel requested_security_level) { open_ = true; // Get System ID and save it. - if (GetSystemIdInternal(&system_id_) == NO_ERROR) { + result = GetSystemIdInternal(&system_id_); + if (result == NO_ERROR) { metrics_->crypto_session_system_id_.Record(system_id_); } else { LOGE("Failed to fetch system ID"); - metrics_->crypto_session_system_id_.SetError(LOAD_SYSTEM_ID_ERROR); - return LOAD_SYSTEM_ID_ERROR; + metrics_->crypto_session_system_id_.SetError(result); + return result; } // Set up request ID @@ -802,35 +801,33 @@ CdmResponseType CryptoSession::Open(SecurityLevel requested_security_level) { CdmSecurityLevel security_level = GetSecurityLevel(); if (security_level == kSecurityLevelL1 || security_level == kSecurityLevelL3) { - { - // This block cannot use |WithStaticFieldWriteLock| because it needs - // to unlock the lock partway through. - LOGV("Static field write lock: Open() initializing usage table"); - std::unique_lock auto_lock(static_field_mutex_); + // This block cannot use |WithStaticFieldWriteLock| because it needs + // to unlock the lock partway through. + LOGV("Static field write lock: Open() initializing usage table"); + std::unique_lock auto_lock(static_field_mutex_); - UsageTableHeader** header = security_level == kSecurityLevelL1 - ? &usage_table_header_l1_ - : &usage_table_header_l3_; - if (*header == nullptr) { - *header = new UsageTableHeader(); - // Ignore errors since we do not know when a session is opened, - // if it is intended to be used for offline/usage session related - // or otherwise. - auto_lock.unlock(); - bool is_usage_table_header_inited = - (*header)->Init(security_level, this); - auto_lock.lock(); - if (!is_usage_table_header_inited) { - delete *header; - *header = nullptr; - usage_table_header_ = nullptr; - return NO_ERROR; - } + UsageTableHeader** header = security_level == kSecurityLevelL1 + ? &usage_table_header_l1_ + : &usage_table_header_l3_; + if (*header == nullptr) { + *header = new UsageTableHeader(); + // Ignore errors since we do not know when a session is opened, + // if it is intended to be used for offline/usage session related + // or otherwise. + auto_lock.unlock(); + bool is_usage_table_header_inited = + (*header)->Init(security_level, this); + auto_lock.lock(); + if (!is_usage_table_header_inited) { + delete *header; + *header = nullptr; + usage_table_header_ = nullptr; + return NO_ERROR; } - usage_table_header_ = *header; - metrics_->usage_table_header_initial_size_.Record((*header)->size()); } - } + usage_table_header_ = *header; + metrics_->usage_table_header_initial_size_.Record((*header)->size()); + } // End |static_field_mutex_| block. } } else { metrics_->oemcrypto_usage_table_support_.SetError(result); @@ -943,7 +940,7 @@ CdmResponseType CryptoSession::LoadKeys( update_usage_table_after_close_session_ = true; return KEY_ADDED; case OEMCrypto_ERROR_TOO_MANY_KEYS: - return INSUFFICIENT_CRYPTO_RESOURCES_4; + return INSUFFICIENT_CRYPTO_RESOURCES; case OEMCrypto_ERROR_USAGE_TABLE_UNRECOVERABLE: // Handle vendor specific error return NEED_PROVISIONING; @@ -983,7 +980,7 @@ CdmResponseType CryptoSession::LoadLicense(const std::string& signed_message, return LOAD_LICENSE_ERROR; case OEMCrypto_ERROR_TOO_MANY_KEYS: LOGE("Too many keys in license"); - return INSUFFICIENT_CRYPTO_RESOURCES_4; + return INSUFFICIENT_CRYPTO_RESOURCES; default: break; } @@ -1185,16 +1182,15 @@ CdmResponseType CryptoSession::PrepareAndSignProvisioningRequest( CdmResponseType CryptoSession::LoadEntitledContentKeys( const std::vector& key_array) { - OEMCryptoResult sts; - WithOecSessionLock("LoadEntitledContentKeys", [&] { - sts = key_session_->LoadEntitledContentKeys(key_array); - }); + const OEMCryptoResult sts = WithOecSessionLock( + "LoadEntitledContentKeys", + [&] { return key_session_->LoadEntitledContentKeys(key_array); }); switch (sts) { case OEMCrypto_SUCCESS: return KEY_ADDED; case OEMCrypto_ERROR_INSUFFICIENT_RESOURCES: - return INSUFFICIENT_CRYPTO_RESOURCES_6; + return INSUFFICIENT_CRYPTO_RESOURCES; case OEMCrypto_ERROR_INVALID_CONTEXT: return NOT_AN_ENTITLEMENT_SESSION; case OEMCrypto_KEY_NOT_ENTITLED: @@ -1243,39 +1239,46 @@ CdmResponseType CryptoSession::LoadCertificatePrivateKey( // Private. CdmResponseType CryptoSession::SelectKey(const std::string& key_id, CdmCipherMode cipher_mode) { - OEMCryptoResult sts; - WithOecSessionLock( - "SelectKey", [&] { sts = key_session_->SelectKey(key_id, cipher_mode); }); + const OEMCryptoResult sts = WithOecSessionLock("SelectKey", [&] { + return key_session_->SelectKey(key_id, cipher_mode); + }); switch (sts) { + // SelectKey errors. case OEMCrypto_SUCCESS: return NO_ERROR; case OEMCrypto_ERROR_KEY_EXPIRED: return NEED_KEY; - case OEMCrypto_ERROR_INSUFFICIENT_HDCP: - return INSUFFICIENT_OUTPUT_PROTECTION; - case OEMCrypto_ERROR_ANALOG_OUTPUT: - return ANALOG_OUTPUT_ERROR; case OEMCrypto_ERROR_INVALID_SESSION: return INVALID_SESSION_1; case OEMCrypto_ERROR_NO_DEVICE_KEY: return NO_DEVICE_KEY_1; case OEMCrypto_ERROR_NO_CONTENT_KEY: return NO_CONTENT_KEY_2; - case OEMCrypto_KEY_NOT_LOADED: // obsolete. - return NO_CONTENT_KEY_3; - case OEMCrypto_ERROR_INSUFFICIENT_RESOURCES: - return INSUFFICIENT_CRYPTO_RESOURCES_2; - case OEMCrypto_ERROR_UNKNOWN_FAILURE: - return UNKNOWN_SELECT_KEY_ERROR_1; - case OEMCrypto_ERROR_SESSION_LOST_STATE: - return SESSION_LOST_STATE_ERROR; - case OEMCrypto_ERROR_SYSTEM_INVALIDATED: - return SYSTEM_INVALIDATED_ERROR; case OEMCrypto_ERROR_CONTROL_INVALID: case OEMCrypto_ERROR_KEYBOX_INVALID: - default: return UNKNOWN_SELECT_KEY_ERROR_2; + case OEMCrypto_ERROR_INSUFFICIENT_RESOURCES: + return INSUFFICIENT_CRYPTO_RESOURCES; + case OEMCrypto_ERROR_UNKNOWN_FAILURE: + return UNKNOWN_SELECT_KEY_ERROR_1; + case OEMCrypto_ERROR_ANALOG_OUTPUT: + return ANALOG_OUTPUT_ERROR; + case OEMCrypto_ERROR_INSUFFICIENT_HDCP: + return INSUFFICIENT_OUTPUT_PROTECTION; + // LoadEntitledContentKeys errors. + // |key_session_| may make calls to OEMCrypto_LoadEntitledContentKeys + // if the key selected has not yet been loaded. + case OEMCrypto_ERROR_INVALID_CONTEXT: + return NOT_AN_ENTITLEMENT_SESSION; + case OEMCrypto_KEY_NOT_ENTITLED: + return NO_MATCHING_ENTITLEMENT_KEY; + // Obsolete errors. + case OEMCrypto_KEY_NOT_LOADED: + return NO_CONTENT_KEY_3; + // Catch all else. + default: + return MapOEMCryptoResult(sts, UNKNOWN_SELECT_KEY_ERROR_2, "SelectKey"); } } @@ -1530,7 +1533,7 @@ CdmResponseType CryptoSession::Decrypt( case OEMCrypto_WARNING_MIXED_OUTPUT_PROTECTION: return NO_ERROR; case OEMCrypto_ERROR_INSUFFICIENT_RESOURCES: - return INSUFFICIENT_CRYPTO_RESOURCES_5; + return INSUFFICIENT_CRYPTO_RESOURCES; case OEMCrypto_ERROR_KEY_EXPIRED: return NEED_KEY; case OEMCrypto_ERROR_INVALID_SESSION: @@ -2005,6 +2008,11 @@ bool CryptoSession::GetMaximumUsageTableEntries(SecurityLevel security_level, metrics_->oemcrypto_maximum_usage_table_header_size_.Record( *number_of_entries); + if (*number_of_entries == 0) { + // Special value, indicating that the table size is not directly + // limited. + return true; + } return *number_of_entries >= kMinimumUsageTableEntriesSupported; } @@ -2349,18 +2357,20 @@ CdmResponseType CryptoSession::GetUsageSupportType( } CdmResponseType CryptoSession::CreateUsageTableHeader( + SecurityLevel requested_security_level, CdmUsageTableHeader* usage_table_header) { - LOGV("Creating usage table header: id = %u", oec_session_id_); - + LOGV("Creating usage table header: requested_security_level = %s", + requested_security_level == kLevel3 + ? QUERY_VALUE_SECURITY_LEVEL_L3.c_str() + : QUERY_VALUE_SECURITY_LEVEL_DEFAULT.c_str()); RETURN_IF_NULL(usage_table_header, PARAMETER_NULL); usage_table_header->resize(kEstimatedInitialUsageTableHeader); - size_t usage_table_header_size = usage_table_header->size(); OEMCryptoResult result; WithOecWriteLock("CreateUsageTableHeader Attempt 1", [&] { result = OEMCrypto_CreateUsageTableHeader( - requested_security_level_, + requested_security_level, reinterpret_cast( const_cast(usage_table_header->data())), &usage_table_header_size); @@ -2371,7 +2381,7 @@ CdmResponseType CryptoSession::CreateUsageTableHeader( usage_table_header->resize(usage_table_header_size); WithOecWriteLock("CreateUsageTableHeader Attempt 2", [&] { result = OEMCrypto_CreateUsageTableHeader( - requested_security_level_, + requested_security_level, reinterpret_cast( const_cast(usage_table_header->data())), &usage_table_header_size); @@ -2379,22 +2389,28 @@ CdmResponseType CryptoSession::CreateUsageTableHeader( }); } - if (result == OEMCrypto_SUCCESS) { - usage_table_header->resize(usage_table_header_size); + switch (result) { + case OEMCrypto_SUCCESS: + usage_table_header->resize(usage_table_header_size); + return NO_ERROR; + default: + return MapOEMCryptoResult(result, CREATE_USAGE_TABLE_ERROR, + "CreateUsageTableHeader"); } - - return MapOEMCryptoResult(result, CREATE_USAGE_TABLE_ERROR, - "CreateUsageTableHeader"); } CdmResponseType CryptoSession::LoadUsageTableHeader( + SecurityLevel requested_security_level, const CdmUsageTableHeader& usage_table_header) { - LOGV("Loading usage table header: id = %u", oec_session_id_); + LOGV("Loading usage table header: requested_security_level = %s", + requested_security_level == kLevel3 + ? QUERY_VALUE_SECURITY_LEVEL_L3.c_str() + : QUERY_VALUE_SECURITY_LEVEL_DEFAULT.c_str()); OEMCryptoResult result; WithOecWriteLock("LoadUsageTableHeader", [&] { result = OEMCrypto_LoadUsageTableHeader( - requested_security_level_, + requested_security_level, reinterpret_cast(usage_table_header.data()), usage_table_header.size()); metrics_->oemcrypto_load_usage_table_header_.Increment(result); @@ -2427,6 +2443,48 @@ CdmResponseType CryptoSession::LoadUsageTableHeader( } } +CdmResponseType CryptoSession::ShrinkUsageTableHeader( + SecurityLevel requested_security_level, uint32_t new_entry_count, + CdmUsageTableHeader* usage_table_header) { + LOGV("Shrinking usage table header: requested_security_level = %s", + requested_security_level == kLevel3 + ? QUERY_VALUE_SECURITY_LEVEL_L3.c_str() + : QUERY_VALUE_SECURITY_LEVEL_DEFAULT.c_str()); + RETURN_IF_NULL(usage_table_header, PARAMETER_NULL); + + size_t usage_table_header_len = 0; + OEMCryptoResult result; + WithOecWriteLock("ShrinkUsageTableHeader Attempt 1", [&] { + result = OEMCrypto_ShrinkUsageTableHeader(requested_security_level, + new_entry_count, nullptr, + &usage_table_header_len); + metrics_->oemcrypto_shrink_usage_table_header_.Increment(result); + }); + + if (result == OEMCrypto_ERROR_SHORT_BUFFER) { + usage_table_header->resize(usage_table_header_len); + WithOecWriteLock("ShrinkUsageTableHeader Attempt 2", [&] { + result = OEMCrypto_ShrinkUsageTableHeader( + requested_security_level, new_entry_count, + reinterpret_cast( + const_cast(usage_table_header->data())), + &usage_table_header_len); + metrics_->oemcrypto_shrink_usage_table_header_.Increment(result); + }); + } + + switch (result) { + case OEMCrypto_SUCCESS: + usage_table_header->resize(usage_table_header_len); + return NO_ERROR; + case OEMCrypto_ERROR_ENTRY_IN_USE: + return SHRINK_USAGE_TABLE_HEADER_ENTRY_IN_USE; + default: + return MapOEMCryptoResult(result, SHRINK_USAGE_TABLE_HEADER_UNKNOWN_ERROR, + "ShrinkUsageTableHeader"); + } +} + CdmResponseType CryptoSession::CreateUsageEntry(uint32_t* entry_number) { LOGV("Creating usage entry: id = %u", oec_session_id_); @@ -2447,7 +2505,7 @@ CdmResponseType CryptoSession::CreateUsageEntry(uint32_t* entry_number) { case OEMCrypto_SUCCESS: return NO_ERROR; case OEMCrypto_ERROR_INSUFFICIENT_RESOURCES: - return INSUFFICIENT_CRYPTO_RESOURCES_3; + return INSUFFICIENT_CRYPTO_RESOURCES; case OEMCrypto_ERROR_SESSION_LOST_STATE: return SESSION_LOST_STATE_ERROR; case OEMCrypto_ERROR_SYSTEM_INVALIDATED: @@ -2483,18 +2541,19 @@ CdmResponseType CryptoSession::LoadUsageEntry( case OEMCrypto_SUCCESS: case OEMCrypto_WARNING_GENERATION_SKEW: return NO_ERROR; + case OEMCrypto_ERROR_INVALID_SESSION: + // This case is special, as it could imply that the provided + // session ID is invalid (CDM internal bug), or that the entry + // being loaded is already in use in a different session. + // It is up to the caller to handle this. + return LOAD_USAGE_ENTRY_INVALID_SESSION; case OEMCrypto_ERROR_GENERATION_SKEW: return LOAD_USAGE_ENTRY_GENERATION_SKEW; case OEMCrypto_ERROR_SIGNATURE_FAILURE: return LOAD_USAGE_ENTRY_SIGNATURE_FAILURE; - case OEMCrypto_ERROR_INSUFFICIENT_RESOURCES: - return INSUFFICIENT_CRYPTO_RESOURCES_3; - case OEMCrypto_ERROR_SESSION_LOST_STATE: - return SESSION_LOST_STATE_ERROR; - case OEMCrypto_ERROR_SYSTEM_INVALIDATED: - return SYSTEM_INVALIDATED_ERROR; default: - return LOAD_USAGE_ENTRY_UNKNOWN_ERROR; + return MapOEMCryptoResult(result, LOAD_USAGE_ENTRY_UNKNOWN_ERROR, + "LoadUsageEntry"); } } @@ -2540,42 +2599,6 @@ CdmResponseType CryptoSession::UpdateUsageEntry( "UpdateUsageEntry"); } -CdmResponseType CryptoSession::ShrinkUsageTableHeader( - uint32_t new_entry_count, CdmUsageTableHeader* usage_table_header) { - LOGV("Shrinking usage table header: id = %u", oec_session_id_); - - RETURN_IF_NULL(usage_table_header, PARAMETER_NULL); - - size_t usage_table_header_len = 0; - OEMCryptoResult result; - WithOecWriteLock("ShrinkUsageTableHeader Attempt 1", [&] { - result = OEMCrypto_ShrinkUsageTableHeader(requested_security_level_, - new_entry_count, nullptr, - &usage_table_header_len); - metrics_->oemcrypto_shrink_usage_table_header_.Increment(result); - }); - - if (result == OEMCrypto_ERROR_SHORT_BUFFER) { - usage_table_header->resize(usage_table_header_len); - - WithOecWriteLock("ShrinkUsageTableHeader Attempt 2", [&] { - result = OEMCrypto_ShrinkUsageTableHeader( - requested_security_level_, new_entry_count, - reinterpret_cast( - const_cast(usage_table_header->data())), - &usage_table_header_len); - metrics_->oemcrypto_shrink_usage_table_header_.Increment(result); - }); - } - - if (result == OEMCrypto_SUCCESS) { - usage_table_header->resize(usage_table_header_len); - } - - return MapOEMCryptoResult(result, SHRINK_USAGE_TABLER_HEADER_UNKNOWN_ERROR, - "ShrinkUsageTableHeader"); -} - CdmResponseType CryptoSession::MoveUsageEntry(uint32_t new_entry_number) { LOGV("Moving usage entry: id = %u", oec_session_id_); @@ -2585,8 +2608,15 @@ CdmResponseType CryptoSession::MoveUsageEntry(uint32_t new_entry_number) { metrics_->oemcrypto_move_entry_.Increment(result); }); - return MapOEMCryptoResult(result, MOVE_USAGE_ENTRY_UNKNOWN_ERROR, - "MoveUsageEntry"); + switch (result) { + case OEMCrypto_ERROR_ENTRY_IN_USE: + LOGW("OEMCrypto_MoveEntry failed: Destination index in use: index = %u", + new_entry_number); + return MOVE_USAGE_ENTRY_DESTINATION_IN_USE; + default: + return MapOEMCryptoResult(result, MOVE_USAGE_ENTRY_UNKNOWN_ERROR, + "MoveUsageEntry"); + } } bool CryptoSession::GetAnalogOutputCapabilities(bool* can_support_output, diff --git a/core/src/device_files.cpp b/core/src/device_files.cpp index c49c34db..923de336 100644 --- a/core/src/device_files.cpp +++ b/core/src/device_files.cpp @@ -6,6 +6,7 @@ #include +#include #include #include "certificate_provisioning.h" @@ -70,6 +71,7 @@ using video_widevine_client::sdk:: namespace { +const char kAtscCertificateFileName[] = "atsccert.bin"; const char kCertificateFileName[] = "cert.bin"; const char kHlsAttributesFileNameExt[] = ".hal"; const char kUsageInfoFileNamePrefix[] = "usage"; @@ -126,19 +128,25 @@ bool DeviceFiles::StoreCertificate(const std::string& certificate, std::string serialized_file; file.SerializeToString(&serialized_file); - return StoreFileWithHash(GetCertificateFileName(), serialized_file) == + return StoreFileWithHash(GetCertificateFileName(false), serialized_file) == kNoError; } -bool DeviceFiles::RetrieveCertificate(std::string* certificate, +bool DeviceFiles::RetrieveCertificate(bool atsc_mode_enabled, + std::string* certificate, std::string* wrapped_private_key, std::string* serial_number, uint32_t* system_id) { RETURN_FALSE_IF_UNINITIALIZED(); + if (!HasCertificate(atsc_mode_enabled)) { + return false; + } + video_widevine_client::sdk::File file; - if (RetrieveHashedFile(GetCertificateFileName(), &file) != kNoError) { - LOGE("Unable to retrieve certificate file"); + if (RetrieveHashedFile(GetCertificateFileName(atsc_mode_enabled), &file) != + kNoError) { + LOGW("Unable to retrieve certificate file"); return false; } @@ -166,14 +174,16 @@ bool DeviceFiles::RetrieveCertificate(std::string* certificate, device_certificate.certificate(), serial_number, system_id); } -bool DeviceFiles::HasCertificate() { +bool DeviceFiles::HasCertificate(bool atsc_mode_enabled) { RETURN_FALSE_IF_UNINITIALIZED(); - return FileExists(GetCertificateFileName()); + + return FileExists(GetCertificateFileName(atsc_mode_enabled)); } bool DeviceFiles::RemoveCertificate() { RETURN_FALSE_IF_UNINITIALIZED() - return RemoveFile(GetCertificateFileName()); + + return RemoveFile(GetCertificateFileName(false)); } bool DeviceFiles::StoreLicense(const CdmLicenseData& license_data, @@ -1103,7 +1113,7 @@ DeviceFiles::ResponseType DeviceFiles::RetrieveHashedFile( path += name; if (!file_system_->Exists(path)) { - LOGE("File does not exist: path = %s", path.c_str()); + LOGW("File does not exist: path = %s", path.c_str()); return kFileNotFound; } @@ -1214,8 +1224,8 @@ ssize_t DeviceFiles::GetFileSize(const std::string& name) { return file_system_->FileSize(path); } -std::string DeviceFiles::GetCertificateFileName() { - return kCertificateFileName; +std::string DeviceFiles::GetCertificateFileName(bool atsc_mode_enabled) { + return atsc_mode_enabled ? kAtscCertificateFileName : kCertificateFileName; } std::string DeviceFiles::GetUsageTableFileName() { return kUsageTableFileName; } diff --git a/core/src/license.cpp b/core/src/license.cpp index 901d04f1..a934571c 100644 --- a/core/src/license.cpp +++ b/core/src/license.cpp @@ -237,6 +237,7 @@ bool CdmLicense::Init(const std::string& client_token, crypto_session_ = session; policy_engine_ = policy_engine; use_privacy_mode_ = use_privacy_mode; + license_nonce_ = 0; initialized_ = true; return true; } @@ -313,8 +314,7 @@ CdmResponseType CdmLicense::PrepareKeyRequest( // Get/set the nonce. This value will be reflected in the Key Control Block // of the license response. - uint32_t nonce; - status = crypto_session_->GenerateNonce(&nonce); + status = crypto_session_->GenerateNonce(&license_nonce_); switch (status) { case NO_ERROR: @@ -325,9 +325,7 @@ CdmResponseType CdmLicense::PrepareKeyRequest( default: return LICENSE_REQUEST_NONCE_GENERATION_ERROR; } - license_request.set_key_control_nonce(nonce); - LOGD("nonce = %u", nonce); - + license_request.set_key_control_nonce(license_nonce_); license_request.set_protocol_version(video_widevine::VERSION_2_1); // License request is complete. Serialize it. @@ -462,11 +460,11 @@ CdmResponseType CdmLicense::PrepareKeyUpdateRequest( LOGW("Unknown API Version"); api_version = 15; } - uint32_t nonce = 0; if (api_version < 16) { // For a pre-v16 license, get/set the nonce. This value will be reflected // in the Key Control Block of the license response. - const CdmResponseType status = crypto_session_->GenerateNonce(&nonce); + const CdmResponseType status = + crypto_session_->GenerateNonce(&license_nonce_); switch (status) { case NO_ERROR: break; @@ -477,8 +475,7 @@ CdmResponseType CdmLicense::PrepareKeyUpdateRequest( return LICENSE_RENEWAL_NONCE_GENERATION_ERROR; } } - license_request.set_key_control_nonce(nonce); - LOGD("nonce = %u", nonce); + license_request.set_key_control_nonce(license_nonce_); license_request.set_protocol_version(video_widevine::VERSION_2_1); // License request is complete. Serialize it. @@ -705,12 +702,13 @@ CdmResponseType CdmLicense::HandleKeyUpdateResponse( return INVALID_LICENSE_TYPE; } - // At this point of the license life-cycle (handling a renewal or - // release), we should already know if the license is v15 or not. - // If license is v16, then there should be a |core_message| - // present; otherwise there might have beeen some tampering with the - // request or response. - if (supports_core_messages() && + // At this point of the license life-cycle (handling a renewal), we should + // already know if the license is v15 or not. If license is v16, then a + // renewal should have a |core_message| present; otherwise there might have + // been some tampering with the request or response. On the other hand, a + // release is processed without loading the license, so OEMCrypto does not + // know if it is v15 or v16, and will not add a core message. + if (is_renewal && supports_core_messages() && (!signed_response.has_oemcrypto_core_message() || signed_response.oemcrypto_core_message().empty())) { LOGE("Renewal response is missing |core_message| field"); @@ -723,8 +721,9 @@ CdmResponseType CdmLicense::HandleKeyUpdateResponse( } const std::string& signed_message = signed_response.msg(); const std::string core_message = - supports_core_messages() ? signed_response.oemcrypto_core_message() - : std::string(); + signed_response.has_oemcrypto_core_message() + ? signed_response.oemcrypto_core_message() + : std::string(); const std::string& signature = signed_response.signature(); License license; @@ -811,6 +810,13 @@ CdmResponseType CdmLicense::RestoreOfflineLicense( } key_request_ = signed_request.msg(); + LicenseRequest original_license_request; + if (!original_license_request.ParseFromString(key_request_)) { + LOGW("Could not parse original request."); + } else { + license_nonce_ = original_license_request.key_control_nonce(); + } + CdmResponseType sts = HandleKeyResponse(license_response); if (sts != KEY_ADDED) return sts; diff --git a/core/src/usage_table_header.cpp b/core/src/usage_table_header.cpp index 74b5d08e..9842622b 100644 --- a/core/src/usage_table_header.cpp +++ b/core/src/usage_table_header.cpp @@ -5,6 +5,7 @@ #include "usage_table_header.h" #include +#include #include "cdm_random.h" #include "crypto_session.h" @@ -16,15 +17,12 @@ namespace wvcdm { namespace { std::string kEmptyString; -size_t kMaxCryptoRetries = 3; wvcdm::CdmKeySetId kDummyKeySetId = "DummyKsid"; std::string kOldUsageEntryServerMacKey(wvcdm::MAC_KEY_SIZE, 0); std::string kOldUsageEntryClientMacKey(wvcdm::MAC_KEY_SIZE, 0); std::string kOldUsageEntryPoviderSessionToken = "nahZ6achSheiqua3TohQuei0ahwohv"; constexpr int64_t kDefaultExpireDuration = 33 * 24 * 60 * 60; // 33 Days -// Number of elements to consider for removal using the LRU algorithm. -constexpr size_t kLruRemovalSetSize = 3; // Fraction of table capacity of number of unexpired offline licenses // before they are considered to be removed. This could occur if // there are not enough expired offline or streaming licenses to @@ -34,6 +32,11 @@ constexpr size_t kLruRemovalSetSize = 3; // nears the capacity of the usage table). constexpr double kLruUnexpiredThresholdFraction = 0.75; +// Maximum number of entries to be moved during a defrag operation. +// This is to prevent the system from stalling too long if the defrag +// occurs during an active application session. +constexpr size_t kMaxDefragEntryMoves = 5; + // Convert |license_message| -> SignedMessage -> License. bool ParseLicenseFromLicenseMessage(const CdmKeyResponse& license_message, video_widevine::License* license) { @@ -128,6 +131,15 @@ bool RetrieveUsageInfoLicense(DeviceFiles* device_files, return true; } +bool EntryIsUsageInfo(const CdmUsageEntryInfo& info) { + // Used for stl filters. + return info.storage_type == kStorageUsageInfo; +} + +bool EntryIsOfflineLicense(const CdmUsageEntryInfo& info) { + // Used for stl filters. + return info.storage_type == kStorageLicense; +} } // namespace UsageTableHeader::UsageTableHeader() @@ -136,7 +148,7 @@ UsageTableHeader::UsageTableHeader() is_inited_(false), clock_ref_(&clock_) { file_system_.reset(new FileSystem()); - file_handle_.reset(new DeviceFiles(file_system_.get())); + device_files_.reset(new DeviceFiles(file_system_.get())); } bool UsageTableHeader::Init(CdmSecurityLevel security_level, @@ -164,16 +176,26 @@ bool UsageTableHeader::Init(CdmSecurityLevel security_level, if (!crypto_session->GetMaximumUsageTableEntries( requested_security_level_, &potential_table_capacity_)) { + LOGW( + "Could not determine usage table capacity, assuming default: " + "default = %zu", + kMinimumUsageTableEntriesSupported); potential_table_capacity_ = kMinimumUsageTableEntriesSupported; + } else if (potential_table_capacity_ == 0) { + LOGD("Usage table capacity is unlimited: security_level = %d", + static_cast(security_level)); } else if (potential_table_capacity_ < kMinimumUsageTableEntriesSupported) { LOGW( "Reported usage table capacity is smaller than minimally required: " "capacity = %zu, minimum = %zu", potential_table_capacity_, kMinimumUsageTableEntriesSupported); potential_table_capacity_ = kMinimumUsageTableEntriesSupported; + } else { + LOGD("Usage table capacity: %zu, security_level = %d", + potential_table_capacity_, static_cast(security_level)); } - if (!file_handle_->Init(security_level)) { + if (!device_files_->Init(security_level)) { LOGE("Failed to initialize device files"); return false; } @@ -183,10 +205,11 @@ bool UsageTableHeader::Init(CdmSecurityLevel security_level, if (metrics == nullptr) metrics = &alternate_crypto_metrics_; bool run_lru_upgrade = false; - if (file_handle_->RetrieveUsageTableInfo( + if (device_files_->RetrieveUsageTableInfo( &usage_table_header_, &usage_entry_info_, &run_lru_upgrade)) { LOGI("Number of usage entries: %zu", usage_entry_info_.size()); - status = crypto_session->LoadUsageTableHeader(usage_table_header_); + status = crypto_session->LoadUsageTableHeader(requested_security_level_, + usage_table_header_); bool lru_success = true; if (status == NO_ERROR && run_lru_upgrade) { @@ -200,11 +223,14 @@ bool UsageTableHeader::Init(CdmSecurityLevel security_level, } } - // If the usage table header has been successfully loaded, and is at - // minimum capacity (>200), we need to make sure we can still add and - // remove entries. If not, clear files/data and recreate usage header table. + // If the usage table header has been successfully loaded, and is + // at minimum capacity (>200) or the table size does not have a + // hard limit, we need to make sure we can still add and remove + // entries. If not, clear files/data and recreate usage header + // table. if (status == NO_ERROR && lru_success) { - if (usage_entry_info_.size() > potential_table_capacity()) { + if (HasUnlimitedTableCapacity() || + usage_entry_info_.size() > potential_table_capacity()) { uint32_t temporary_usage_entry_number; // Create a new temporary usage entry, close the session and then @@ -229,8 +255,16 @@ bool UsageTableHeader::Init(CdmSecurityLevel security_level, } } if (result == NO_ERROR) { - result = DeleteEntry(temporary_usage_entry_number, file_handle_.get(), - metrics); + result = InvalidateEntry(temporary_usage_entry_number, + /* defrag_table = */ true, + device_files_.get(), metrics); + if (usage_entry_info_.size() > temporary_usage_entry_number) { + // The entry should have been deleted from the usage table, + // not just marked as type unknown. Failure to call + // Shrink() may be an indicator of other issues. + LOGE("Temporary entry was not deleted"); + result = UNKNOWN_ERROR; + } } if (result != NO_ERROR) { LOGE( @@ -245,19 +279,21 @@ bool UsageTableHeader::Init(CdmSecurityLevel security_level, if (status != NO_ERROR || !lru_success) { LOGE("Failed to load usage table: security_level = %d, status = %d", static_cast(security_level), static_cast(status)); - file_handle_->DeleteAllLicenses(); - file_handle_->DeleteAllUsageInfo(); - file_handle_->DeleteUsageTableInfo(); + device_files_->DeleteAllLicenses(); + device_files_->DeleteAllUsageInfo(); + device_files_->DeleteUsageTableInfo(); usage_entry_info_.clear(); usage_table_header_.clear(); - status = crypto_session->CreateUsageTableHeader(&usage_table_header_); + status = crypto_session->CreateUsageTableHeader(requested_security_level_, + &usage_table_header_); if (status != NO_ERROR) return false; - file_handle_->StoreUsageTableInfo(usage_table_header_, usage_entry_info_); + StoreTable(device_files_.get()); } } else { - status = crypto_session->CreateUsageTableHeader(&usage_table_header_); + status = crypto_session->CreateUsageTableHeader(requested_security_level_, + &usage_table_header_); if (status != NO_ERROR) return false; - file_handle_->StoreUsageTableInfo(usage_table_header_, usage_entry_info_); + StoreTable(device_files_.get()); } is_inited_ = true; @@ -275,66 +311,12 @@ CdmResponseType UsageTableHeader::AddEntry( CdmResponseType status = crypto_session->CreateUsageEntry(usage_entry_number); - if (status == INSUFFICIENT_CRYPTO_RESOURCES_3) { - // If usage entry creation fails due to insufficient resources, release an - // entry based on LRU. - std::vector removal_candidates; - if (!GetRemovalCandidates(&removal_candidates)) { - LOGE("Could not determine which license to remove"); - return status; - } - - // Variables for metrics. - const size_t usage_info_count = - std::count_if(usage_entry_info_.cbegin(), usage_entry_info_.cend(), - [](const CdmUsageEntryInfo& usage_entry) { - return usage_entry.storage_type == kStorageUsageInfo; - }); - const size_t license_count = - std::count_if(usage_entry_info_.cbegin(), usage_entry_info_.cend(), - [](const CdmUsageEntryInfo& usage_entry) { - return usage_entry.storage_type == kStorageLicense; - }); - int64_t staleness_of_removed; - CdmUsageEntryStorageType storage_type_of_removed; - const int64_t current_time = GetCurrentTime(); - - for (size_t i = 0; i < removal_candidates.size() && - status == INSUFFICIENT_CRYPTO_RESOURCES_3; - ++i) { - const uint32_t entry_number_to_delete = removal_candidates[i]; - // Calculate metric values. - staleness_of_removed = - current_time - - usage_entry_info_[entry_number_to_delete].last_use_time; - storage_type_of_removed = - usage_entry_info_[entry_number_to_delete].storage_type; - - if (DeleteEntry(entry_number_to_delete, file_handle_.get(), metrics) == - NO_ERROR) { - // If the entry was deleted, it is still possible for the create new - // entry to fail. If so, we must ensure that the previously last - // entry was not in the |removal_candidates| as it has now been swapped - // with the deleted entry. - for (uint32_t& entry_number : removal_candidates) { - if (entry_number == usage_entry_info_.size()) { - entry_number = entry_number_to_delete; - } - } - } + if (status == INSUFFICIENT_CRYPTO_RESOURCES) { + LOGW("Usage table may be full, releasing oldest entry: size = %zu", + usage_entry_info_.size()); + status = ReleaseOldestEntry(metrics); + if (status == NO_ERROR) { status = crypto_session->CreateUsageEntry(usage_entry_number); - - // Record metrics on success. - if (status == NO_ERROR) { - metrics->usage_table_header_lru_usage_info_count_.Record( - usage_info_count); - metrics->usage_table_header_lru_offline_license_count_.Record( - license_count); - metrics->usage_table_header_lru_evicted_entry_staleness_.Record( - staleness_of_removed); - metrics->usage_table_header_lru_evicted_entry_type_.Record( - static_cast(storage_type_of_removed)); - } } } @@ -358,9 +340,7 @@ CdmResponseType UsageTableHeader::AddEntry( const size_t number_of_entries = usage_entry_info_.size(); usage_entry_info_.resize(*usage_entry_number + 1); for (size_t i = number_of_entries; i < usage_entry_info_.size() - 1; ++i) { - usage_entry_info_[i].storage_type = kStorageTypeUnknown; - usage_entry_info_[i].key_set_id.clear(); - usage_entry_info_[i].usage_info_file_name.clear(); + usage_entry_info_[i].Clear(); } } else /* *usage_entry_number == usage_entry_info_.size() */ { usage_entry_info_.resize(*usage_entry_number + 1); @@ -392,8 +372,20 @@ CdmResponseType UsageTableHeader::AddEntry( } } + // Call to update the usage table header, but don't store the usage + // entry. If the entry is used by the CDM, the CDM session will make + // subsequent calls to update the usage entry and store that entry. + std::string usage_entry; + status = crypto_session->UpdateUsageEntry(&usage_table_header_, &usage_entry); + if (status != NO_ERROR) { + LOGE("Failed to update new usage entry: usage_entry_number = %u", + *usage_entry_number); + usage_entry_info_[*usage_entry_number].Clear(); + return status; + } + LOGI("New usage entry: usage_entry_number = %u", *usage_entry_number); - file_handle_->StoreUsageTableInfo(usage_table_header_, usage_entry_info_); + StoreTable(device_files_.get()); return NO_ERROR; } @@ -415,27 +407,9 @@ CdmResponseType UsageTableHeader::LoadEntry(CryptoSession* crypto_session, metrics::CryptoMetrics* metrics = crypto_session->GetCryptoMetrics(); if (metrics == nullptr) metrics = &alternate_crypto_metrics_; - CdmResponseType status = + const CdmResponseType status = crypto_session->LoadUsageEntry(usage_entry_number, usage_entry); - // If loading a usage entry fails due to insufficient resources, release a - // random entry different from |usage_entry_number| and try again. If there - // are no more entries to release, we fail. - for (uint32_t retry_count = 0; retry_count < kMaxCryptoRetries && - status == INSUFFICIENT_CRYPTO_RESOURCES_3; - ++retry_count) { - if (usage_entry_info_.size() <= 1) break; - // Get a random entry from the other entries. - uint32_t entry_number_to_delete = - CdmRandom::RandomInRange(usage_entry_info_.size() - 2); - if (entry_number_to_delete >= usage_entry_number) { - // Exclude |usage_entry_number|. - ++entry_number_to_delete; - } - DeleteEntry(entry_number_to_delete, file_handle_.get(), metrics); - status = crypto_session->LoadUsageEntry(usage_entry_number, usage_entry); - } - if (status == NO_ERROR) { usage_entry_info_[usage_entry_number].last_use_time = GetCurrentTime(); } @@ -460,15 +434,19 @@ CdmResponseType UsageTableHeader::UpdateEntry(uint32_t usage_entry_number, if (status != NO_ERROR) return status; usage_entry_info_[usage_entry_number].last_use_time = GetCurrentTime(); - file_handle_->StoreUsageTableInfo(usage_table_header_, usage_entry_info_); + StoreTable(device_files_.get()); return NO_ERROR; } -CdmResponseType UsageTableHeader::DeleteEntry(uint32_t usage_entry_number, - DeviceFiles* handle, - metrics::CryptoMetrics* metrics) { - LOGI("Locking to delete entry: usage_entry_number = %u", usage_entry_number); +CdmResponseType UsageTableHeader::InvalidateEntry( + uint32_t usage_entry_number, bool defrag_table, DeviceFiles* device_files, + metrics::CryptoMetrics* metrics) { + LOGI("Locking to invalidate entry: usage_entry_number = %u", + usage_entry_number); std::unique_lock auto_lock(usage_table_header_lock_); + // OEMCrypto does not have any concept of "deleting" an entry. + // Instead, the CDM marks the entry's meta data as invalid (storage + // type unknown) and then performs a "defrag" of the OEMCrypto table. if (usage_entry_number >= usage_entry_info_.size()) { LOGE( "Usage entry number is larger than table size: " @@ -477,55 +455,52 @@ CdmResponseType UsageTableHeader::DeleteEntry(uint32_t usage_entry_number, return USAGE_INVALID_PARAMETERS_1; } - // Find the last valid entry number, in order to swap - size_t swap_entry_number = usage_entry_info_.size() - 1; - CdmUsageEntry swap_usage_entry; - bool swap_usage_entry_valid = false; + usage_entry_info_[usage_entry_number].Clear(); - while (!swap_usage_entry_valid && swap_entry_number > usage_entry_number) { - switch (usage_entry_info_[swap_entry_number].storage_type) { - case kStorageLicense: - case kStorageUsageInfo: { - CdmResponseType status = - GetEntry(swap_entry_number, handle, &swap_usage_entry); - if (status == NO_ERROR) swap_usage_entry_valid = true; - break; - } - case kStorageTypeUnknown: - default: - break; + if (defrag_table) { + // The defrag operation calls many OEMCrypto functions that are + // unrelated to the caller, the only error that will be returned is + // a SYSTEM_INVALIDATED_ERROR. As long as the storage type is + // properly set to unknown, the operation is considered successful. + // SYSTEM_INVALIDATED_ERROR is a special type of error that must be + // sent back to the caller for the CDM as a whole to handle. + const uint32_t pre_defrag_store_counter = store_table_counter_; + const CdmResponseType status = DefragTable(device_files, metrics); + if (pre_defrag_store_counter == store_table_counter_) { + // It is possible that DefragTable() does not result in any + // changes to the table, and as a result, it will not store the + // invalidated entry. + LOGD("Table was not stored during defrag, storing now"); + StoreTable(device_files); } - if (!swap_usage_entry_valid) --swap_entry_number; + if (status == SYSTEM_INVALIDATED_ERROR) { + LOGE("Invalidate entry failed due to system invalidation error"); + return SYSTEM_INVALIDATED_ERROR; + } + } else { + StoreTable(device_files); } - uint32_t number_of_entries_to_be_deleted = - usage_entry_info_.size() - usage_entry_number; + return NO_ERROR; +} - if (swap_usage_entry_valid) { - CdmResponseType status = MoveEntry(swap_entry_number, swap_usage_entry, - usage_entry_number, handle, metrics); - // If unable to move entry, unset storage type of entry to be deleted and - // resize |usage_entry_info_| so that swap usage entry is the last entry. - if (status != NO_ERROR) { - usage_entry_info_[usage_entry_number].storage_type = kStorageTypeUnknown; - usage_entry_info_[usage_entry_number].key_set_id.clear(); - if (usage_entry_info_.size() - 1 == swap_entry_number) { - file_handle_->StoreUsageTableInfo(usage_table_header_, - usage_entry_info_); - } else { - Shrink(metrics, usage_entry_info_.size() - swap_entry_number - 1); - } - return NO_ERROR; - } - number_of_entries_to_be_deleted = - usage_entry_info_.size() - swap_entry_number; - } - return Shrink(metrics, number_of_entries_to_be_deleted); +size_t UsageTableHeader::UsageInfoCount() const { + LOGI("Locking to count usage info (streaming license) entries"); + std::unique_lock auto_lock(usage_table_header_lock_); + return std::count_if(usage_entry_info_.cbegin(), usage_entry_info_.cend(), + EntryIsUsageInfo); +} + +size_t UsageTableHeader::OfflineEntryCount() const { + LOGI("Locking to count offline license entries"); + std::unique_lock auto_lock(usage_table_header_lock_); + return std::count_if(usage_entry_info_.cbegin(), usage_entry_info_.cend(), + EntryIsOfflineLicense); } CdmResponseType UsageTableHeader::MoveEntry( uint32_t from_usage_entry_number, const CdmUsageEntry& from_usage_entry, - uint32_t to_usage_entry_number, DeviceFiles* handle, + uint32_t to_usage_entry_number, DeviceFiles* device_files, metrics::CryptoMetrics* metrics) { LOGI( "Moving usage entry: " @@ -537,13 +512,18 @@ CdmResponseType UsageTableHeader::MoveEntry( std::unique_ptr scoped_crypto_session; CryptoSession* crypto_session = test_crypto_session_.get(); if (crypto_session == nullptr) { - scoped_crypto_session.reset((CryptoSession::MakeCryptoSession(metrics))); + scoped_crypto_session.reset(CryptoSession::MakeCryptoSession(metrics)); crypto_session = scoped_crypto_session.get(); } - crypto_session->Open(requested_security_level_); + CdmResponseType status = crypto_session->Open(requested_security_level_); + if (status != NO_ERROR) { + LOGE("Cannot open session for move: usage_entry_number = %u", + from_usage_entry_number); + return status; + } - CdmResponseType status = + status = crypto_session->LoadUsageEntry(from_usage_entry_number, from_usage_entry); if (status != NO_ERROR) { @@ -564,6 +544,7 @@ CdmResponseType UsageTableHeader::MoveEntry( usage_entry_info_[to_usage_entry_number] = usage_entry_info_[from_usage_entry_number]; + usage_entry_info_[from_usage_entry_number].Clear(); CdmUsageEntry usage_entry; status = crypto_session->UpdateUsageEntry(&usage_table_header_, &usage_entry); @@ -574,15 +555,15 @@ CdmResponseType UsageTableHeader::MoveEntry( return status; } - file_handle_->StoreUsageTableInfo(usage_table_header_, usage_entry_info_); - - StoreEntry(to_usage_entry_number, handle, usage_entry); + // Store the usage table and usage entry after successful move. + StoreTable(device_files); + StoreEntry(to_usage_entry_number, device_files, usage_entry); return NO_ERROR; } CdmResponseType UsageTableHeader::GetEntry(uint32_t usage_entry_number, - DeviceFiles* handle, + DeviceFiles* device_files, CdmUsageEntry* usage_entry) { LOGI("Getting usage entry: usage_entry_number = %u, storage_type: %d", usage_entry_number, @@ -594,7 +575,7 @@ CdmResponseType UsageTableHeader::GetEntry(uint32_t usage_entry_number, case kStorageLicense: { DeviceFiles::CdmLicenseData license_data; DeviceFiles::ResponseType sub_error_code = DeviceFiles::kNoError; - if (!handle->RetrieveLicense( + if (!device_files->RetrieveLicense( usage_entry_info_[usage_entry_number].key_set_id, &license_data, &sub_error_code)) { LOGE("Failed to retrieve license: status = %d", @@ -611,7 +592,7 @@ CdmResponseType UsageTableHeader::GetEntry(uint32_t usage_entry_number, CdmKeyMessage license_request; CdmKeyResponse license; - if (!handle->RetrieveUsageInfoByKeySetId( + if (!device_files->RetrieveUsageInfoByKeySetId( usage_entry_info_[usage_entry_number].usage_info_file_name, usage_entry_info_[usage_entry_number].key_set_id, &provider_session_token, &license_request, &license, usage_entry, @@ -642,7 +623,7 @@ CdmResponseType UsageTableHeader::GetEntry(uint32_t usage_entry_number, } CdmResponseType UsageTableHeader::StoreEntry(uint32_t usage_entry_number, - DeviceFiles* handle, + DeviceFiles* device_files, const CdmUsageEntry& usage_entry) { LOGI("Storing usage entry: usage_entry_number = %u, storage type: %d", usage_entry_number, @@ -655,7 +636,7 @@ CdmResponseType UsageTableHeader::StoreEntry(uint32_t usage_entry_number, DeviceFiles::CdmLicenseData license_data; DeviceFiles::ResponseType sub_error_code = DeviceFiles::kNoError; - if (!handle->RetrieveLicense( + if (!device_files->RetrieveLicense( usage_entry_info_[usage_entry_number].key_set_id, &license_data, &sub_error_code)) { LOGE("Failed to retrieve license: status = %d", @@ -667,7 +648,7 @@ CdmResponseType UsageTableHeader::StoreEntry(uint32_t usage_entry_number, license_data.usage_entry = usage_entry; license_data.usage_entry_number = usage_entry_number; - if (!handle->StoreLicense(license_data, &sub_error_code)) { + if (!device_files->StoreLicense(license_data, &sub_error_code)) { LOGE("Failed to store license: status = %d", static_cast(sub_error_code)); return USAGE_STORE_LICENSE_FAILED; @@ -679,7 +660,7 @@ CdmResponseType UsageTableHeader::StoreEntry(uint32_t usage_entry_number, uint32_t entry_number; std::string provider_session_token, init_data, key_request, key_response, key_renewal_request; - if (!handle->RetrieveUsageInfoByKeySetId( + if (!device_files->RetrieveUsageInfoByKeySetId( usage_entry_info_[usage_entry_number].usage_info_file_name, usage_entry_info_[usage_entry_number].key_set_id, &provider_session_token, &key_request, &key_response, &entry, @@ -687,10 +668,10 @@ CdmResponseType UsageTableHeader::StoreEntry(uint32_t usage_entry_number, LOGE("Failed to retrieve usage information"); return USAGE_STORE_ENTRY_RETRIEVE_USAGE_INFO_FAILED; } - handle->DeleteUsageInfo( + device_files->DeleteUsageInfo( usage_entry_info_[usage_entry_number].usage_info_file_name, provider_session_token); - if (!handle->StoreUsageInfo( + if (!device_files->StoreUsageInfo( provider_session_token, key_request, key_response, usage_entry_info_[usage_entry_number].usage_info_file_name, usage_entry_info_[usage_entry_number].key_set_id, usage_entry, @@ -711,6 +692,18 @@ CdmResponseType UsageTableHeader::StoreEntry(uint32_t usage_entry_number, return NO_ERROR; } +bool UsageTableHeader::StoreTable(DeviceFiles* device_files) { + LOGV("Storing usage table information"); + const bool result = + device_files->StoreUsageTableInfo(usage_table_header_, usage_entry_info_); + if (result) { + ++store_table_counter_; + } else { + LOGW("Failed to store usage table info"); + } + return result; +} + CdmResponseType UsageTableHeader::Shrink( metrics::CryptoMetrics* metrics, uint32_t number_of_usage_entries_to_delete) { @@ -731,9 +724,6 @@ CdmResponseType UsageTableHeader::Shrink( if (number_of_usage_entries_to_delete == 0) return NO_ERROR; - usage_entry_info_.resize(usage_entry_info_.size() - - number_of_usage_entries_to_delete); - // crypto_session points to an object whose scope is this method or a test // object whose scope is the lifetime of this class std::unique_ptr scoped_crypto_session; @@ -743,19 +733,273 @@ CdmResponseType UsageTableHeader::Shrink( crypto_session = scoped_crypto_session.get(); } - CdmResponseType status = crypto_session->Open(requested_security_level_); - if (status != NO_ERROR) return status; + const size_t new_size = + usage_entry_info_.size() - number_of_usage_entries_to_delete; + const CdmResponseType status = crypto_session->ShrinkUsageTableHeader( + requested_security_level_, new_size, &usage_table_header_); - status = crypto_session->ShrinkUsageTableHeader(usage_entry_info_.size(), - &usage_table_header_); - if (status != NO_ERROR) return status; + if (status == NO_ERROR) { + usage_entry_info_.resize(new_size); + StoreTable(device_files_.get()); + } + return status; +} - file_handle_->StoreUsageTableInfo(usage_table_header_, usage_entry_info_); +CdmResponseType UsageTableHeader::DefragTable(DeviceFiles* device_files, + metrics::CryptoMetrics* metrics) { + LOGV("Defragging table: current_size = %zu", usage_entry_info_.size()); + // Defragging the usage table involves moving valid entries near the + // end of the usage table to the position of invalid entries near the + // front of the table. After the entries are moved, the CDM shrinks + // the table to cut off all trailing invalid entries at the end of + // the table. + + // Special case 0: Empty table, do nothing. + if (usage_entry_info_.empty()) { + LOGD("Table empty, nothing to defrag"); + return NO_ERROR; + } + + // Step 1: Create a list of entries to be removed from the table. + // Priority is given to the entries near the beginning of the table. + // To avoid large delays from the swapping process, we limit the + // quantity of entries to remove to |kMaxDefragEntryMoves| or fewer. + std::vector entries_to_remove; + for (uint32_t i = 0; i < usage_entry_info_.size() && + entries_to_remove.size() < kMaxDefragEntryMoves; + ++i) { + if (usage_entry_info_[i].storage_type == kStorageTypeUnknown) { + entries_to_remove.push_back(i); + } + } + + // Special case 1: There are no entries that are invalid; nothing + // needs to be done. + if (entries_to_remove.empty()) { + LOGD("No entries are invalid"); + return NO_ERROR; + } + + // Step 2: Create a list of entries to be moved from the end of the + // table to the positions identified for removal. + std::vector entries_to_move; + for (uint32_t i = 0; i < usage_entry_info_.size() && + entries_to_move.size() < entries_to_remove.size(); + ++i) { + // Search from the end of the table. + const uint32_t entry_index = usage_entry_info_.size() - i - 1; + if (usage_entry_info_[entry_index].storage_type != kStorageTypeUnknown) { + entries_to_move.push_back(entry_index); + } + } + + // Special case 2: There are no valid entries in the table. In this case, + // the whole table can be removed. + if (entries_to_move.empty()) { + LOGD("No valid entries found, shrinking entire table: size = %zu", + usage_entry_info_.size()); + return Shrink(metrics, usage_entry_info_.size()); + } + + // Step 3: Ignore invalid entries that are after the last valid + // entry. No entry is to be moved to a greater index than it already + // has, and entries after the last valid entry will be removed when + // the shrink operation is applied to the table. + // Note: Special case 4 will handle any non-trivial cases related to + // interweaving of valid and invalid entries. + const uint32_t last_valid_entry = entries_to_move.front(); + while (!entries_to_remove.empty() && + entries_to_remove.back() > last_valid_entry) { + entries_to_remove.pop_back(); + } + + // Special case 3: All of the invalid entries are after the last valid + // entry. In this case, no movement is required and the table can just + // be shrunk to the last valid entry. + if (entries_to_remove.empty()) { + const size_t to_remove = usage_entry_info_.size() - last_valid_entry - 1; + LOGD("Removing all entries after the last valid entry: count = %zu", + to_remove); + return Shrink(metrics, to_remove); + } + + // Step 4: Move the valid entries to overwrite the invalid entries. + // Moving the highest numbered valid entry to the lowest numbered + // invalid entry. + + // Reversing vectors to make accessing and popping easier. This + // will put the lowest number invalid entry and the highest number + // valid entry at the back of their respective vectors. + std::reverse(entries_to_remove.begin(), entries_to_remove.end()); + std::reverse(entries_to_move.begin(), entries_to_move.end()); + while (!entries_to_remove.empty() && !entries_to_move.empty()) { + // Entries are popped after use only. + const uint32_t to_entry_number = entries_to_remove.back(); + const uint32_t from_entry_number = entries_to_move.back(); + + // Special case 4: We don't want to move any entries to a higher + // index than their current. Once this occurs, we can stop the + // loop. + if (to_entry_number > from_entry_number) { + LOGD("Entries will not be moved further down the table"); + break; + } + + CdmUsageEntry from_entry; + CdmResponseType status = + GetEntry(from_entry_number, device_files, &from_entry); + if (status != NO_ERROR) { + LOGW("Could not get entry: entry_number = %u", from_entry_number); + // It is unlikely that an unretrievable entry will suddenly + // become retrievable later on when it is needed. + usage_entry_info_[from_entry_number].Clear(); + entries_to_move.pop_back(); + continue; + } + + status = MoveEntry(from_entry_number, from_entry, to_entry_number, + device_files, metrics); + switch (status) { + case NO_ERROR: { + entries_to_remove.pop_back(); + entries_to_move.pop_back(); + break; + } + // Handle errors associated with the valid "from" entry. + case LOAD_USAGE_ENTRY_INVALID_SESSION: { + // This is a special error code when returned from LoadEntry() + // indicating that the entry is already in use in a different + // session. In this case, skip the entry and move on. + LOGD("From entry already in use: from_entry_number = %u", + from_entry_number); + entries_to_move.pop_back(); + break; + } + case LOAD_USAGE_ENTRY_GENERATION_SKEW: + case LOAD_USAGE_ENTRY_SIGNATURE_FAILURE: + case LOAD_USAGE_ENTRY_UNKNOWN_ERROR: { + // The entry (from the CDM's point of view) is invalid and + // can no longer be used. Safe to continue loop. + // TODO(b/152256186): Remove local files associated with this + // entry. + usage_entry_info_[from_entry_number].Clear(); + LOGW("From entry was corrupted: from_entry_number = %u", + from_entry_number); + entries_to_move.pop_back(); + break; + } + // Handle errors associated with the invalid "to" entry. + case MOVE_USAGE_ENTRY_DESTINATION_IN_USE: { + // The usage entry specified by |to_entry_number| is currently + // being used by another session. This is unlikely, but still + // possible. Given that this entry is already marked as unknown + // storage type, it will likely be removed at a later time. + LOGD("To entry already in use: to_entry_number = %u", to_entry_number); + entries_to_remove.pop_back(); + break; + } + case MOVE_USAGE_ENTRY_UNKNOWN_ERROR: { + // Something else wrong occurred when moving to the destination + // entry. This could be a problem with from entry or the to + // entry. Both should be skipped on the next iteration. + LOGW( + "Move failed, skipping both to entry and from entry: " + "to_entry_number = %u, from_entry_number = %u", + to_entry_number, from_entry_number); + entries_to_remove.pop_back(); + entries_to_move.pop_back(); + break; + } + // Handle other possible errors from the operations. + case INSUFFICIENT_CRYPTO_RESOURCES: { + // Cannot open any new sessions. The loop should end, but + // an attempt to shrink the table should still be made. + LOGW("Cannot open new session for table clean up"); + entries_to_remove.clear(); + entries_to_move.clear(); + break; + } + default: { + // For all other cases, it may not be safe to proceed, even to + // shrink the table. + LOGE("Unrecoverable error occurred while defragging table: status = %d", + static_cast(status)); + return status; + } + } // End switch case. + } // End while loop. + + // Step 5: Find the new last valid entry. + uint32_t new_last_valid_entry = usage_entry_info_.size(); + for (uint32_t i = 0; i < usage_entry_info_.size(); ++i) { + const uint32_t entry_index = usage_entry_info_.size() - i - 1; + if (usage_entry_info_[entry_index].storage_type != kStorageTypeUnknown) { + new_last_valid_entry = entry_index; + break; + } + } + + // Special case 5: No entries in the table are valid. This could + // have occurred if entries during the move process were found to be + // invalid. In this case, remove the whole table. + if (new_last_valid_entry == usage_entry_info_.size()) { + LOGD( + "All entries have been invalidated, shrinking entire table: size = %zu", + usage_entry_info_.size()); + return Shrink(metrics, usage_entry_info_.size()); + } + + const size_t to_remove = usage_entry_info_.size() - new_last_valid_entry - 1; + + // Special case 6: It is possible that the last entry in the table + // is valid and currently loaded in the table by another session. + // The loop above would have tried to move it but had failed. In + // this case, nothing more to do. + if (to_remove == 0) { + LOGD("Defrag completed without shrinking table"); + StoreTable(device_files); + return NO_ERROR; + } + + // Step 6: Shrink table to the new size. + LOGD("Clean up complete, shrinking table: count = %zu", to_remove); + return Shrink(metrics, to_remove); +} // End Defrag(). + +CdmResponseType UsageTableHeader::ReleaseOldestEntry( + metrics::CryptoMetrics* metrics) { + LOGV("Releasing oldest entry"); + uint32_t entry_number_to_delete; + if (!GetRemovalCandidate(&entry_number_to_delete)) { + LOGE("Could not determine which license to remove"); + return UNKNOWN_ERROR; + } + const CdmUsageEntryInfo& usage_entry_info = + usage_entry_info_[entry_number_to_delete]; + + const int64_t current_time = GetCurrentTime(); + // Capture metric values now, as the |usage_entry_info| reference will + // change after the call to invalidate. + const int64_t staleness = current_time - usage_entry_info.last_use_time; + const CdmUsageEntryStorageType storage_type = usage_entry_info.storage_type; + + const CdmResponseType status = + InvalidateEntry(entry_number_to_delete, /* defrag_table = */ true, + device_files_.get(), metrics); + + if (status != NO_ERROR) { + LOGE("Failed to invalidate oldest entry: status = %d", + static_cast(status)); + return status; + } + + // Record metrics on success. + RecordLruEventMetrics(metrics, staleness, storage_type); return NO_ERROR; } // Test only method. -void UsageTableHeader::DeleteEntryForTest(uint32_t usage_entry_number) { +void UsageTableHeader::InvalidateEntryForTest(uint32_t usage_entry_number) { LOGV("Deleting entry for test: usage_entry_number = %u", usage_entry_number); if (usage_entry_number >= usage_entry_info_.size()) { LOGE( @@ -764,7 +1008,8 @@ void UsageTableHeader::DeleteEntryForTest(uint32_t usage_entry_number) { usage_entry_number, usage_entry_info_.size()); return; } - // Move last entry into deleted location and shrink usage entries + // Move last entry into invalidated entry location and shrink usage + // entries. usage_entry_info_[usage_entry_number] = usage_entry_info_[usage_entry_info_.size() - 1]; usage_entry_info_.resize(usage_entry_info_.size() - 1); @@ -788,13 +1033,13 @@ bool UsageTableHeader::LruUpgradeAllUsageEntries() { switch (usage_entry_info.storage_type) { case kStorageLicense: { retrieve_response = RetrieveOfflineLicense( - file_handle_.get(), usage_entry_info.key_set_id, &license_message, + device_files_.get(), usage_entry_info.key_set_id, &license_message, &retrieved_entry_number); break; } case kStorageUsageInfo: { retrieve_response = RetrieveUsageInfoLicense( - file_handle_.get(), usage_entry_info.usage_info_file_name, + device_files_.get(), usage_entry_info.usage_info_file_name, usage_entry_info.key_set_id, &license_message, &retrieved_entry_number); break; @@ -868,7 +1113,7 @@ bool UsageTableHeader::LruUpgradeAllUsageEntries() { for (size_t usage_entry_number : bad_license_file_entries) { CdmUsageEntryInfo& usage_entry_info = usage_entry_info_[usage_entry_number]; if (usage_entry_info.storage_type == kStorageLicense) { - file_handle_->DeleteLicense(usage_entry_info.key_set_id); + device_files_->DeleteLicense(usage_entry_info.key_set_id); } else if (usage_entry_info.storage_type == kStorageUsageInfo) { // To reduce write cycles, the deletion of usage info will be done // in bulk. @@ -881,148 +1126,139 @@ bool UsageTableHeader::LruUpgradeAllUsageEntries() { } it->second.push_back(usage_entry_info.key_set_id); } // else kStorageUnknown { Nothing special }. - usage_entry_info.storage_type = kStorageTypeUnknown; - usage_entry_info.key_set_id.clear(); - usage_entry_info.usage_info_file_name.clear(); + usage_entry_info.Clear(); } for (const auto& p : usage_info_clean_up) { - file_handle_->DeleteMultipleUsageInfoByKeySetIds(p.first, p.second); + device_files_->DeleteMultipleUsageInfoByKeySetIds(p.first, p.second); } return true; } -bool UsageTableHeader::GetRemovalCandidates( - std::vector* removal_candidates) { +bool UsageTableHeader::GetRemovalCandidate(uint32_t* entry_to_remove) { LOGI("Locking to determine removal candidates"); std::unique_lock auto_lock(usage_table_header_lock_); const size_t lru_unexpired_threshold = - kLruUnexpiredThresholdFraction * potential_table_capacity(); + HasUnlimitedTableCapacity() + ? kLruUnexpiredThresholdFraction * size() + : kLruUnexpiredThresholdFraction * potential_table_capacity(); return DetermineLicenseToRemove(usage_entry_info_, GetCurrentTime(), - lru_unexpired_threshold, kLruRemovalSetSize, - removal_candidates); + lru_unexpired_threshold, entry_to_remove); +} + +void UsageTableHeader::RecordLruEventMetrics( + metrics::CryptoMetrics* metrics, uint64_t staleness, + CdmUsageEntryStorageType storage_type) { + if (metrics == nullptr) return; + metrics->usage_table_header_lru_usage_info_count_.Record(UsageInfoCount()); + metrics->usage_table_header_lru_offline_license_count_.Record( + OfflineEntryCount()); + metrics->usage_table_header_lru_evicted_entry_staleness_.Record(staleness); + metrics->usage_table_header_lru_evicted_entry_type_.Record( + static_cast(storage_type)); } // Static. bool UsageTableHeader::DetermineLicenseToRemove( const std::vector& usage_entry_info_list, - int64_t current_time, size_t unexpired_threshold, size_t removal_count, - std::vector* removal_candidates) { - if (removal_candidates == nullptr) { - LOGE("Output parameter |removal_candidates| is null"); - return false; - } - if (removal_count == 0) { - LOGE("|removal_count| cannot be zero"); + int64_t current_time, size_t unexpired_threshold, + uint32_t* entry_to_remove) { + if (entry_to_remove == nullptr) { + LOGE("Output parameter |entry_to_remove| is null"); return false; } if (usage_entry_info_list.empty()) { return false; } - removal_candidates->clear(); - std::vector unknown_storage_entry_numbers; - // |entry_numbers| contains expired offline and streaming license. - std::vector entry_numbers; - std::vector unexpired_offline_license_entry_numbers; - - // Separate the entries based on their priority properties. - for (uint32_t entry_number = 0; entry_number < usage_entry_info_list.size(); - ++entry_number) { - const CdmUsageEntryInfo& usage_entry_info = - usage_entry_info_list[entry_number]; - if (usage_entry_info.storage_type == kStorageLicense) { - if (usage_entry_info.offline_license_expiry_time > current_time) { - // Unexpired offline. - unexpired_offline_license_entry_numbers.push_back(entry_number); - } else { - // Expired offline. - entry_numbers.push_back(entry_number); - } - } else if (usage_entry_info.storage_type == kStorageUsageInfo) { - // Streaming. - entry_numbers.push_back(entry_number); - } else { - // Unknown entries. - unknown_storage_entry_numbers.push_back(entry_number); - } - } - - // Select any entries of unknown storage type. - if (!unknown_storage_entry_numbers.empty()) { - if (unknown_storage_entry_numbers.size() >= removal_count) { - // Case: There are enough entries with unknown storage types to - // fill the removal set. - removal_candidates->insert( - removal_candidates->begin(), unknown_storage_entry_numbers.begin(), - unknown_storage_entry_numbers.begin() + removal_count); - return true; - } - // Fill whatever are available, and check for more. - *removal_candidates = std::move(unknown_storage_entry_numbers); - } - - // Sort licenses based on last used time. - const auto compare_last_used = [&](uint32_t i, uint32_t j) { + // Returns true if entry of first index is more stale than the + // entry of the second index. + const auto is_more_stale = [&](uint32_t i, uint32_t j) -> bool { return usage_entry_info_list[i].last_use_time < usage_entry_info_list[j].last_use_time; }; - // Check if unexpired licenses should be considered too. - if (unexpired_offline_license_entry_numbers.size() > unexpired_threshold) { - std::sort(unexpired_offline_license_entry_numbers.begin(), - unexpired_offline_license_entry_numbers.end(), compare_last_used); - if (unexpired_offline_license_entry_numbers.size() > removal_count) { - unexpired_offline_license_entry_numbers.resize(removal_count); + // Find the most stale expired offline / streaming license and the + // most stale unexpired offline entry. Count the number of unexpired + // entries. If any entry is of storage type unknown, then it should + // be removed. + constexpr uint32_t kNoEntry = std::numeric_limits::max(); + uint32_t stalest_expired_offline_license = kNoEntry; + uint32_t stalest_unexpired_offline_license = kNoEntry; + uint32_t stalest_streaming_license = kNoEntry; + size_t unexpired_license_count = 0; + + for (uint32_t entry_number = 0; entry_number < usage_entry_info_list.size(); + ++entry_number) { + const CdmUsageEntryInfo& usage_entry_info = + usage_entry_info_list[entry_number]; + + if (usage_entry_info.storage_type != kStorageLicense && + usage_entry_info.storage_type != kStorageUsageInfo) { + // Unknown storage type entries. Remove this entry. + *entry_to_remove = entry_number; + return true; + } + if (usage_entry_info.storage_type == kStorageLicense && + usage_entry_info.offline_license_expiry_time > current_time) { + // Unexpired offline. + ++unexpired_license_count; + if (stalest_unexpired_offline_license == kNoEntry || + is_more_stale(entry_number, stalest_unexpired_offline_license)) { + stalest_unexpired_offline_license = entry_number; + } + } else if (usage_entry_info.storage_type == kStorageLicense) { + // Expired offline. + if (stalest_expired_offline_license == kNoEntry || + is_more_stale(entry_number, stalest_expired_offline_license)) { + stalest_expired_offline_license = entry_number; + } + } else { + // Streaming. + if (stalest_streaming_license == kNoEntry || + is_more_stale(entry_number, stalest_streaming_license)) { + stalest_streaming_license = entry_number; + } } - // Merge the sets. - entry_numbers.insert(entry_numbers.end(), - unexpired_offline_license_entry_numbers.begin(), - unexpired_offline_license_entry_numbers.end()); } - // Sort expired offline and streaming license based on last used time. - std::sort(entry_numbers.begin(), entry_numbers.end(), compare_last_used); - - if ((entry_numbers.size() + removal_candidates->size()) <= removal_count) { - // Under testing conditions, it is possible for there to be fewer usage - // entries than there are being requested. - - // Move whatever values are available to the removal candidates. - removal_candidates->insert(removal_candidates->end(), entry_numbers.begin(), - entry_numbers.end()); - return removal_candidates->size() > 0; + if (stalest_expired_offline_license == kNoEntry && + stalest_streaming_license == kNoEntry && + unexpired_license_count <= unexpired_threshold) { + // Unexpected situation, could be an issue with the threshold. + LOGW( + "Table only contains unexpired offline licenses, " + "but threshold not met: size = %zu, count = %zu, threshold = %zu", + usage_entry_info_list.size(), unexpired_license_count, + unexpired_threshold); + *entry_to_remove = stalest_unexpired_offline_license; + return true; } - const size_t remaining_removal_count = - removal_count - removal_candidates->size(); - - // Based on the last use time the |remaining_removal_count|-th - // least recently used entry, filter out all elements which have - // been used more recently than it. This might result in a set - // which is larger than the desired size. - const int64_t cutoff_last_use_time = - usage_entry_info_list[entry_numbers[remaining_removal_count - 1]] - .last_use_time; - const auto equal_to_cutoff = [&](uint32_t entry_number) { - return usage_entry_info_list[entry_number].last_use_time == - cutoff_last_use_time; + const auto select_most_stale = [&](uint32_t a, uint32_t b) -> uint32_t { + if (a == kNoEntry) return b; + if (b == kNoEntry) return a; + return is_more_stale(a, b) ? a : b; }; - const auto first_cutoff_it = - std::find_if(entry_numbers.begin(), entry_numbers.end(), equal_to_cutoff); + // Only consider an unexpired entry if the threshold is reached. + if (unexpired_license_count > unexpired_threshold) { + const uint32_t temp = select_most_stale(stalest_unexpired_offline_license, + stalest_streaming_license); + *entry_to_remove = select_most_stale(temp, stalest_expired_offline_license); + } else { + *entry_to_remove = select_most_stale(stalest_streaming_license, + stalest_expired_offline_license); + } - const auto after_last_cutoff_it = - std::find_if_not(first_cutoff_it, entry_numbers.end(), equal_to_cutoff); - - // To avoid always selecting the greatest entry number (due to the - // sort & reverse), we randomize the set. - std::shuffle(first_cutoff_it, after_last_cutoff_it, - std::default_random_engine(CdmRandom::Rand())); - - removal_candidates->insert(removal_candidates->end(), entry_numbers.cbegin(), - entry_numbers.cbegin() + remaining_removal_count); + if (*entry_to_remove == kNoEntry) { + // Illegal state check. The loop above should have found at least + // one entry given that |usage_entry_info_list| is not empty. + LOGE("No entry could be used for removal: size = %zu", + usage_entry_info_list.size()); + return false; + } return true; } diff --git a/core/test/cdm_engine_metrics_decorator_unittest.cpp b/core/test/cdm_engine_metrics_decorator_unittest.cpp index 2ca27d89..4b9d2d4b 100644 --- a/core/test/cdm_engine_metrics_decorator_unittest.cpp +++ b/core/test/cdm_engine_metrics_decorator_unittest.cpp @@ -38,6 +38,8 @@ class MockCdmClientPropertySet : public CdmClientPropertySet { MOCK_CONST_METHOD0(is_session_sharing_enabled, bool()); MOCK_CONST_METHOD0(session_sharing_id, uint32_t()); MOCK_METHOD1(set_session_sharing_id, void(uint32_t)); + MOCK_CONST_METHOD0(use_atsc_mode, bool()); + MOCK_METHOD1(set_use_atsc_mode, void(bool)); MOCK_CONST_METHOD0(app_id, const std::string&()); }; @@ -76,13 +78,13 @@ class MockCdmEngineImpl : public CdmEngine { MOCK_METHOD1(RemoveKeys, CdmResponseType(const CdmSessionId&)); MOCK_METHOD2(QueryKeyStatus, CdmResponseType(const CdmSessionId&, CdmQueryMap*)); - MOCK_METHOD5(GetProvisioningRequest, + MOCK_METHOD6(GetProvisioningRequest, CdmResponseType(CdmCertificateType, const std::string&, - const std::string&, CdmProvisioningRequest*, - std::string*)); - MOCK_METHOD3(HandleProvisioningResponse, - CdmResponseType(const CdmProvisioningResponse&, std::string*, - std::string*)); + const std::string&, SecurityLevel, + CdmProvisioningRequest*, std::string*)); + MOCK_METHOD4(HandleProvisioningResponse, + CdmResponseType(const CdmProvisioningResponse&, SecurityLevel, + std::string*, std::string*)); MOCK_METHOD1(Unprovision, CdmResponseType(CdmSecurityLevel)); MOCK_METHOD4(ListUsageIds, CdmResponseType(const std::string&, CdmSecurityLevel, @@ -311,16 +313,17 @@ TEST_F(WvCdmEngineMetricsImplTest, GetProvisioningRequest) { std::string default_url; EXPECT_CALL(*test_cdm_metrics_engine_, - GetProvisioningRequest(Eq(kCertificateX509), - Eq("fake certificate authority"), - Eq("fake service certificate"), - Eq(&request), Eq(&default_url))) + GetProvisioningRequest( + Eq(kCertificateX509), Eq("fake certificate authority"), + Eq("fake service certificate"), Eq(wvcdm::kLevelDefault), + Eq(&request), Eq(&default_url))) .WillOnce(Return(wvcdm::UNKNOWN_ERROR)); ASSERT_EQ(wvcdm::UNKNOWN_ERROR, test_cdm_metrics_engine_->GetProvisioningRequest( kCertificateX509, "fake certificate authority", - "fake service certificate", &request, &default_url)); + "fake service certificate", wvcdm::kLevelDefault, &request, + &default_url)); drm_metrics::WvCdmMetrics metrics_proto; test_cdm_metrics_engine_->GetMetricsSnapshot(&metrics_proto); @@ -340,12 +343,14 @@ TEST_F(WvCdmEngineMetricsImplTest, HandleProvisioningResponse) { EXPECT_CALL(*test_cdm_metrics_engine_, HandleProvisioningResponse(Eq("fake provisioning response"), - Eq(&cert), Eq(&wrapped_key))) + Eq(wvcdm::kLevelDefault), Eq(&cert), + Eq(&wrapped_key))) .WillOnce(Return(wvcdm::UNKNOWN_ERROR)); ASSERT_EQ(wvcdm::UNKNOWN_ERROR, test_cdm_metrics_engine_->HandleProvisioningResponse( - "fake provisioning response", &cert, &wrapped_key)); + "fake provisioning response", wvcdm::kLevelDefault, &cert, + &wrapped_key)); drm_metrics::WvCdmMetrics metrics_proto; test_cdm_metrics_engine_->GetMetricsSnapshot(&metrics_proto); diff --git a/core/test/cdm_session_unittest.cpp b/core/test/cdm_session_unittest.cpp index f0ac0731..674939e8 100644 --- a/core/test/cdm_session_unittest.cpp +++ b/core/test/cdm_session_unittest.cpp @@ -114,8 +114,8 @@ class MockDeviceFiles : public DeviceFiles { MockDeviceFiles() : DeviceFiles(nullptr) {} MOCK_METHOD1(Init, bool(CdmSecurityLevel)); - MOCK_METHOD4(RetrieveCertificate, - bool(std::string*, std::string*, std::string*, uint32_t*)); + MOCK_METHOD5(RetrieveCertificate, + bool(bool, std::string*, std::string*, std::string*, uint32_t*)); }; class MockUsageTableHeader : public UsageTableHeader { @@ -217,8 +217,8 @@ TEST_F(CdmSessionTest, InitWithBuiltInCertificate) { EXPECT_CALL(*crypto_session_, GetPreProvisionTokenType()) .WillOnce(Return(kClientTokenDrmCert)); EXPECT_CALL(*file_handle_, - RetrieveCertificate(NotNull(), NotNull(), NotNull(), _)) - .WillOnce(DoAll(SetArgPointee<0>(kToken), SetArgPointee<1>(kWrappedKey), + RetrieveCertificate(false, NotNull(), NotNull(), NotNull(), _)) + .WillOnce(DoAll(SetArgPointee<1>(kToken), SetArgPointee<2>(kWrappedKey), Return(true))); EXPECT_CALL(*crypto_session_, LoadCertificatePrivateKey(StrEq(kWrappedKey))) .InSequence(crypto_session_seq) @@ -245,8 +245,8 @@ TEST_F(CdmSessionTest, InitWithCertificate) { .WillOnce(Return(kClientTokenKeybox)); EXPECT_CALL(*file_handle_, Init(Eq(level))).WillOnce(Return(true)); EXPECT_CALL(*file_handle_, - RetrieveCertificate(NotNull(), NotNull(), NotNull(), _)) - .WillOnce(DoAll(SetArgPointee<0>(kToken), SetArgPointee<1>(kWrappedKey), + RetrieveCertificate(false, NotNull(), NotNull(), NotNull(), _)) + .WillOnce(DoAll(SetArgPointee<1>(kToken), SetArgPointee<2>(kWrappedKey), Return(true))); EXPECT_CALL(*crypto_session_, LoadCertificatePrivateKey(StrEq(kWrappedKey))) .InSequence(crypto_session_seq) @@ -272,8 +272,8 @@ TEST_F(CdmSessionTest, ReInitFail) { .WillOnce(Return(kClientTokenKeybox)); EXPECT_CALL(*file_handle_, Init(Eq(level))).WillOnce(Return(true)); EXPECT_CALL(*file_handle_, - RetrieveCertificate(NotNull(), NotNull(), NotNull(), _)) - .WillOnce(DoAll(SetArgPointee<0>(kToken), SetArgPointee<1>(kWrappedKey), + RetrieveCertificate(false, NotNull(), NotNull(), NotNull(), _)) + .WillOnce(DoAll(SetArgPointee<1>(kToken), SetArgPointee<2>(kWrappedKey), Return(true))); EXPECT_CALL(*crypto_session_, LoadCertificatePrivateKey(StrEq(kWrappedKey))) .InSequence(crypto_session_seq) @@ -307,7 +307,7 @@ TEST_F(CdmSessionTest, InitNeedsProvisioning) { .WillOnce(Return(kClientTokenKeybox)); EXPECT_CALL(*file_handle_, Init(Eq(level))).WillOnce(Return(true)); EXPECT_CALL(*file_handle_, - RetrieveCertificate(NotNull(), NotNull(), NotNull(), _)) + RetrieveCertificate(false, NotNull(), NotNull(), NotNull(), _)) .WillOnce(Return(false)); ASSERT_EQ(NEED_PROVISIONING, cdm_session_->Init(nullptr)); @@ -327,8 +327,8 @@ TEST_F(CdmSessionTest, UpdateUsageEntry) { .WillOnce(Return(kClientTokenKeybox)); EXPECT_CALL(*file_handle_, Init(Eq(level))).WillOnce(Return(true)); EXPECT_CALL(*file_handle_, - RetrieveCertificate(NotNull(), NotNull(), NotNull(), _)) - .WillOnce(DoAll(SetArgPointee<0>(kToken), SetArgPointee<1>(kWrappedKey), + RetrieveCertificate(false, NotNull(), NotNull(), NotNull(), _)) + .WillOnce(DoAll(SetArgPointee<1>(kToken), SetArgPointee<2>(kWrappedKey), Return(true))); EXPECT_CALL(*crypto_session_, LoadCertificatePrivateKey(StrEq(kWrappedKey))) .InSequence(crypto_session_seq) diff --git a/core/test/certificate_provisioning_unittest.cpp b/core/test/certificate_provisioning_unittest.cpp index 13ff2464..6c7d0b28 100644 --- a/core/test/certificate_provisioning_unittest.cpp +++ b/core/test/certificate_provisioning_unittest.cpp @@ -40,16 +40,19 @@ class MockCryptoSession : public TestCryptoSession { MockCryptoSession(metrics::CryptoMetrics* metrics) : TestCryptoSession(metrics) {} MOCK_METHOD1(Open, CdmResponseType(SecurityLevel)); - MOCK_METHOD1(LoadUsageTableHeader, - CdmResponseType(const CdmUsageTableHeader&)); - MOCK_METHOD1(CreateUsageTableHeader, CdmResponseType(CdmUsageTableHeader*)); + // Usage Table Header. + MOCK_METHOD2(CreateUsageTableHeader, + CdmResponseType(SecurityLevel, CdmUsageTableHeader*)); + MOCK_METHOD2(LoadUsageTableHeader, + CdmResponseType(SecurityLevel, const CdmUsageTableHeader&)); + MOCK_METHOD3(ShrinkUsageTableHeader, + CdmResponseType(SecurityLevel, uint32_t, CdmUsageTableHeader*)); + // Usage Entry. MOCK_METHOD1(CreateUsageEntry, CdmResponseType(uint32_t*)); MOCK_METHOD2(LoadUsageEntry, CdmResponseType(uint32_t, const CdmUsageEntry&)); MOCK_METHOD2(UpdateUsageEntry, CdmResponseType(CdmUsageTableHeader*, CdmUsageEntry*)); MOCK_METHOD1(MoveUsageEntry, CdmResponseType(uint32_t)); - MOCK_METHOD2(ShrinkUsageTableHeader, - CdmResponseType(uint32_t, CdmUsageTableHeader*)); }; class TestStubCryptoSessionFactory : public CryptoSessionFactory { @@ -62,18 +65,19 @@ class TestStubCryptoSessionFactory : public CryptoSessionFactory { using ::testing::_; class CertificateProvisioningTest : public WvCdmTestBase { - public: protected: void SetUp() override { WvCdmTestBase::SetUp(); CryptoSession::SetCryptoSessionFactory(new TestStubCryptoSessionFactory()); + metrics_.reset(new metrics::CryptoMetrics()); certificate_provisioning_.reset( - new CertificateProvisioning(new metrics::CryptoMetrics())); + new CertificateProvisioning(metrics_.get())); } void TearDown() override {} + std::unique_ptr metrics_; std::unique_ptr certificate_provisioning_; }; diff --git a/core/test/crypto_session_unittest.cpp b/core/test/crypto_session_unittest.cpp index 4cf703db..646b0b62 100644 --- a/core/test/crypto_session_unittest.cpp +++ b/core/test/crypto_session_unittest.cpp @@ -316,8 +316,6 @@ TEST_F(CryptoSessionMetricsTest, OpenSessionValidMetrics) { EXPECT_EQ(OEMCrypto_Keybox, metrics_proto.oemcrypto_provisioning_method().int_value()); EXPECT_EQ(1, metrics_proto.oemcrypto_get_key_data_time_us().size()); - EXPECT_EQ( - 1u, metrics_proto.oemcrypto_get_key_data_time_us(0).operation_count()); } else if (token_type == kClientTokenOemCert) { // Recent devices all have a system id between 1k and 6 or 7k. Errors // we are trying to catch are 0, byte swapped 32 bit numbers, or @@ -365,8 +363,6 @@ TEST_F(CryptoSessionMetricsTest, GetProvisioningTokenValidMetrics) { uint32_t system_id = FindKeyboxSystemID(); EXPECT_EQ(system_id, metrics_proto.crypto_session_system_id().int_value()); EXPECT_EQ(1, metrics_proto.oemcrypto_get_key_data_time_us().size()); - EXPECT_EQ( - 2u, metrics_proto.oemcrypto_get_key_data_time_us(0).operation_count()); } else if (token_type == kClientTokenOemCert) { // Recent devices all have a system id between 1k and 6 or 7k. Errors // we are trying to catch are 0, byte swapped 32 bit numbers, or diff --git a/core/test/device_files_unittest.cpp b/core/test/device_files_unittest.cpp index 6dc0ec11..1c8d6ed2 100644 --- a/core/test/device_files_unittest.cpp +++ b/core/test/device_files_unittest.cpp @@ -9,6 +9,7 @@ #include #include +#include #include "arraysize.h" #include "cdm_random.h" @@ -25,34 +26,31 @@ namespace { const uint32_t kCertificateLen = 700; const uint32_t kWrappedKeyLen = 500; -const uint32_t kProtobufEstimatedOverhead = 200; - const std::string kEmptyString; // Structurally valid test certificate. // The data elements in this module are used to test the storage and // retrieval of certificates and licenses -const std::string kTestCertificate = - "124B035F3D256A656F0E505A085E7A6C482B61035E0C4A540F7803137F4C3B45206B7F33" - "347F4D7A005E56400F0955011F4E07072D0D46781817460974326A516E3944385760280E" - "4F166B380F033D045231201E6146041C3A6F01345C59300D32592732192C0F2310586306" - "7B31467B1477010D6F1D1944272509572A26217E1E6F7B666F46153E7749106E48760468" - "19467E164A731773155B3236537D5128682014174D125063380E48356A370B5015416A7F" - "672F132E37364E154B41540F440E47092775531508495F1E55576F363C0C190C3A332179" - "415B343905563E37645E68007053315A1A20286E7C3B4320424A5F7F36635558686C3565" - "762122237D344A411C0F00342135776753461D105C21111E5024434E5E0F275D12061658" - "4435410F210E5228532D214F505D0F0B3C34032C7C597F6159665E664C682C5A6C03212E" - "71333C3A642D796A65642E151827086E2D671C130B172C43192C792D294440630163526D" - "0658537A073E0F32231E7426593230692A4468386D3511542F1A6F71440128466E510445" - "294F4465113D1B1A711D4D67691363093B680854322B041C2F72524A513E5F0E407C6233" - "1728520E6C0C09107C26737B78287231661952283619647A6241391940297D2067036D44" - "3C64766918236C51175A636F000A2E5A4C5B725D5500652B1C39283037723F0255092976" - "6F2D204F0E616F1233206B75661B0F755E1E3807491079663A191C0B2D5E363B3768663A" - "4E222A1D32015D3D783E5148313F05713B140347231C59243648313C23770F554E012715" - "3350597775274A580306202E65265957291F490F642A2E7C6700716400617C7E6A303266" - "523B102906195E003C2D111A7D4740122C6941003726602B59263B5C09473D4E025E3541" - "701B122D340A3D145436137002687E4C470D2F6F4C357A3245384D737B734E2274301179" - "402473486311156E5A0C78644C593273"; +const std::string kTestCertificate = a2bs_hex( + "0A98030802120D73657269616C5F6E756D62657218B4B2CDE00422E8024D49494243674B43" + "415145412B78475A2F77637A39756746705030374E73706F365531376C3059684669467078" + "78553470546B334C69667A3952337A734973754552777461372B66574966784F6F32303865" + "74742F6A68736B69566F645345743351424768345842697079576F704B775A393348486144" + "565A41414C692F32412B785442745764456F37584755756A4B447643322F615A4B756B666A" + "704F6955493841684C41666A6D6C63442F555A31515068306D4873676C524E436D7043776D" + "7753584139564E6D687A2B5069422B446D6C3457576E4B572F56486F32756A54587871372B" + "65664D55344832666E79335365334B594F73465046475A31544E5153596C46755368577248" + "5074694C6D5564506F50364356326D4D4C31746B2B6C3744494971587251684C554B444143" + "654D35726F4D78306B4C6855574238502B30756A31434E6C4E4E344A525A6C433778466671" + "694D62465255395A344E3659774944415141422899203A11746573742E7769646576696E65" + "2E636F6D128202307836353063396632653637303165336665373364333035343930346139" + "61346262646239363733336631633463373433656635373361643661633134633561336266" + "38613437333166366536323736666165613532343733303336373766623864626466323466" + "66373865353363323530353263646361383765656366656538353437366263623861303563" + "62396131656665663763623837646436383232336531313763653830306163343631373731" + "37323534343735376134383762653332663561623866653038373966613861646437386265" + "34363565613866386435616366393737653966316165333664346434373831366561366564" + "343133373262"); // A Wrapped Private Key // The data elements in this module are used to test the storage and @@ -76,42 +74,54 @@ const std::string kTestWrappedPrivateKey = // The test certificate in file storage format. // The data elements in this module are used to test the storage and // retrieval of certificates and licenses -const std::string kTestCertificateFileData = - "0ABD09080110011AB6090ABC05124B035F3D256A656F0E505A085E7A6C482B61035E0C4A" - "540F7803137F4C3B45206B7F33347F4D7A005E56400F0955011F4E07072D0D4678181746" - "0974326A516E3944385760280E4F166B380F033D045231201E6146041C3A6F01345C5930" - "0D32592732192C0F23105863067B31467B1477010D6F1D1944272509572A26217E1E6F7B" - "666F46153E7749106E4876046819467E164A731773155B3236537D5128682014174D1250" - "63380E48356A370B5015416A7F672F132E37364E154B41540F440E47092775531508495F" - "1E55576F363C0C190C3A332179415B343905563E37645E68007053315A1A20286E7C3B43" - "20424A5F7F36635558686C3565762122237D344A411C0F00342135776753461D105C2111" - "1E5024434E5E0F275D120616584435410F210E5228532D214F505D0F0B3C34032C7C597F" - "6159665E664C682C5A6C03212E71333C3A642D796A65642E151827086E2D671C130B172C" - "43192C792D294440630163526D0658537A073E0F32231E7426593230692A4468386D3511" - "542F1A6F71440128466E510445294F4465113D1B1A711D4D67691363093B680854322B04" - "1C2F72524A513E5F0E407C62331728520E6C0C09107C26737B7828723166195228361964" - "7A6241391940297D2067036D443C64766918236C51175A636F000A2E5A4C5B725D550065" - "2B1C39283037723F02550929766F2D204F0E616F1233206B75661B0F755E1E3807491079" - "663A191C0B2D5E363B3768663A4E222A1D32015D3D783E5148313F05713B140347231C59" - "243648313C23770F554E0127153350597775274A580306202E65265957291F490F642A2E" - "7C6700716400617C7E6A303266523B102906195E003C2D111A7D4740122C694100372660" - "2B59263B5C09473D4E025E3541701B122D340A3D145436137002687E4C470D2F6F4C357A" - "3245384D737B734E2274301179402473486311156E5A0C78644C59327312F4034F724B06" - "5326371A2F5F6F51467C2E26555C453B5C7C1B4F2738454B782E3E7B5340435A66374D06" - "12052C521A233D7A67194871751C78575E5177070130264C4F037633320E667B1A491929" - "24491338693D106E6113014A733A241A1A033E28352178146B4F543D38104A5919120325" - "502C31365506096D59585E08774B5B567A7B5D03451E6B11633E52672C226103104B3E4C" - "031A6403050F3A574D2C501711773802741F7F3A0D364757101D02181C7D4D3520716750" - "6A424C094E4A72316F791F162D76657D2B5D3C2D7B273A2869277175613165187E552824" - "30491467086425432347701C3116446D21645C756B2D3D0F797C3220322D622A254D0B7D" - "4F1D5D0C0A36755D1246741A34783C45157247091C78232B7D2E0E1F637A2A3739085D76" - "166747034350613969072F5B5C5B21657E470C7E513B3F091D74455A3A0737057B7E3B53" - "37191D4E7536087C334B6028530F3F5B23380B6A076031294501003D6D1F240F63053D5D" - "0B271B6A0F26185650731308660B0447566041684F584C22216E567D3B7755695F7F3D6B" - "64525E7227165948101540243C19495C4C702F37490F2661335379782562414326304302" - "0E1E6760123D51056F2F1E482F2E3D021B27677D3E7E3C0C11757C3448275E08382E1112" - "63644C6D224714706D760A054A586E17505C3429575A41043F1842091220F8D0A23D4B1B" - "C7B23A38B921BC1EA8938D1FD22FF9A389B58DA856A3E2625F27"; +const std::string kTestCertificateFileData = a2bs_hex( + "0A950D080110011A8E0D0AA0050A98030802120D73657269616C5F6E756D62657218B4B2CD" + "E00422E8024D49494243674B43415145412B78475A2F77637A39756746705030374E73706F" + "365531376C305968466946707878553470546B334C69667A3952337A734973754552777461" + "372B66574966784F6F3230386574742F6A68736B69566F6453457433514247683458426970" + "79576F704B775A393348486144565A41414C692F32412B785442745764456F37584755756A" + "4B447643322F615A4B756B666A704F6955493841684C41666A6D6C63442F555A3151506830" + "6D4873676C524E436D7043776D7753584139564E6D687A2B5069422B446D6C3457576E4B57" + "2F56486F32756A54587871372B65664D55344832666E79335365334B594F73465046475A31" + "544E5153596C467553685772485074694C6D5564506F50364356326D4D4C31746B2B6C3744" + "494971587251684C554B444143654D35726F4D78306B4C6855574238502B30756A31434E6C" + "4E4E344A525A6C433778466671694D62465255395A344E3659774944415141422899203A11" + "746573742E7769646576696E652E636F6D1282023078363530633966326536373031653366" + "65373364333035343930346139613462626462393637333366316334633734336566353733" + "61643661633134633561336266386134373331663665363237366661656135323437333033" + "36373766623864626466323466663738653533633235303532636463613837656563666565" + "38353437366263623861303563623961316566656637636238376464363832323365313137" + "63653830306163343631373731373235343437353761343837626533326635616238666530" + "38373966613861646437386265343635656138663864356163663937376539663161653336" + "6434643437383136656136656434313337326212E807344637323442303635333236333731" + "41324635463646353134363743324532363535354334353342354337433142344632373338" + "34353442373832453345374235333430343335413636333734443036313230353243353231" + "41323333443741363731393438373137353143373835373545353137373037303133303236" + "34433446303337363333333230453636374231413439313932393234343931333338363933" + "44313036453631313330313441373333413234314131413033334532383335323137383134" + "36423446353433443338313034413539313931323033323535303243333133363535303630" + "39364435393538354530383737344235423536374137423544303334353145364231313633" + "33453532363732433232363130333130344233453443303331413634303330353046334135" + "37344432433530313731313737333830323734314637463341304433363437353731303144" + "30323138314337443444333532303731363735303641343234433039344534413732333136" + "46373931463136324437363635374432423544334332443742323733413238363932373731" + "37353631333136353138374535353238323433303439313436373038363432353433323334" + "37373031433331313634343644323136343543373536423244334430463739374333323230" + "33323244363232413235344430423744344631443544304330413336373535443132343637" + "34314133343738334334353135373234373039314337383233324237443245304531463633" + "37413241333733393038354437363136363734373033343335303631333936393037324635" + "42354335423231363537453437304337453531334233463039314437343435354133413037" + "33373035374237453342353333373139314434453735333630383743333334423630323835" + "33304633463542323333383042364130373630333132393435303130303344364431463234" + "30463633303533443544304232373142364130463236313835363530373331333038363630" + "42303434373536363034313638344635383443323232313645353637443342373735353639" + "35463746334436423634353235453732323731363539343831303135343032343343313934" + "39354334433730324633373439304632363631333335333739373832353632343134333236" + "33303433303230453145363736303132334435313035364632463145343832463245334430" + "32314232373637374433453745334330433131373537433334343832373545303833383245" + "31313132363336343443364432323437313437303644373630413035344135383645313735" + "303543333432393537354134313034334631383432303912205C6993E9656F73A41739773A" + "0FCBA8AE232CD8856ACE585FF6BFB2A09C20061E"); struct LicenseInfo { std::string key_set_id; @@ -2014,6 +2024,7 @@ class MockFileSystem : public FileSystem { // gmock methods using ::testing::_; +using ::testing::AllArgs; using ::testing::AllOf; using ::testing::DoAll; using ::testing::Eq; @@ -2075,9 +2086,9 @@ class DeviceFilesTest : public ::testing::Test { class DeviceFilesStoreTest : public DeviceFilesTest, public ::testing::WithParamInterface {}; -class DeviceCertificateStoreTest : public DeviceFilesTest {}; - -class DeviceCertificateTest : public DeviceFilesTest {}; +class DeviceCertificateTest + : public DeviceFilesTest, + public ::testing::WithParamInterface {}; class DeviceFilesSecurityLevelTest : public DeviceFilesTest, @@ -2102,103 +2113,35 @@ class DeviceFilesDeleteMultipleUsageInfoTest public ::testing::WithParamInterface {}; MATCHER(IsCreateFileFlagSet, "") { return FileSystem::kCreate & arg; } -MATCHER_P(IsStrEq, str, "") { - // Estimating the length of data. We can have gmock provide length - // as well as pointer to data but that will introduce a dependency on tr1 - return memcmp(arg, str.c_str(), str.size()) == 0; +MATCHER_P(StrAndLenEq, str, "") { + const std::string data(std::get<0>(arg), std::get<1>(arg)); + return data == str; } -MATCHER_P(ContainsAllElementsInVector, str_vector, "") { - // Estimating the length of data. We can have gmock provide length - // as well as pointer to data but that will introduce a dependency on tr1 - size_t str_length = 0; - for (size_t i = 0; i < str_vector.size(); ++i) { - str_length += str_vector[i].size(); - } - std::string data(arg, str_length + kProtobufEstimatedOverhead); - bool all_entries_found = true; - for (size_t i = 0; i < str_vector.size(); ++i) { - if (data.find(str_vector[i]) == std::string::npos) { - all_entries_found = false; +MATCHER_P(StrAndLenContains, str_vector, "") { + const std::string data(std::get<0>(arg), std::get<1>(arg)); + for (const std::string& str : str_vector) { + if (data.find(str) == std::string::npos) { + return false; } } - return all_entries_found; -} -MATCHER_P2(Contains, str1, size, "") { - // Estimating the length of data. We can have gmock provide length - // as well as pointer to data but that will introduce a dependency on tr1 - std::string data(arg, size + str1.size() + kProtobufEstimatedOverhead); - return (data.find(str1) != std::string::npos); -} -MATCHER_P3(Contains, str1, str2, size, "") { - // Estimating the length of data. We can have gmock provide length - // as well as pointer to data but that will introduce a dependency on tr1 - std::string data( - arg, size + str1.size() + str2.size() + kProtobufEstimatedOverhead); - return (data.find(str1) != std::string::npos && - data.find(str2) != std::string::npos); -} -MATCHER_P4(Contains, str1, str2, str3, size, "") { - // Estimating the length of data. We can have gmock provide length - // as well as pointer to data but that will introduce a dependency on tr1 - std::string data(arg, size + str1.size() + str2.size() + str3.size() + - kProtobufEstimatedOverhead); - return (data.find(str1) != std::string::npos && - data.find(str2) != std::string::npos && - data.find(str3) != std::string::npos); -} -MATCHER_P6(Contains, str1, str2, str3, str4, str5, size, "") { - // Estimating the length of data. We can have gmock provide length - // as well as pointer to data but that will introduce a dependency on tr1 - std::string data(arg, size + str1.size() + str2.size() + str3.size() + - str4.size() + str5.size() + - kProtobufEstimatedOverhead); - return (data.find(str1) != std::string::npos && - data.find(str2) != std::string::npos && - data.find(str3) != std::string::npos && - data.find(str4) != std::string::npos && - data.find(str5) != std::string::npos); -} -MATCHER_P8(Contains, str1, str2, str3, str4, str5, str6, map7, str8, "") { - // Estimating the length of data. We can have gmock provide length - // as well as pointer to data but that will introduce a dependency on tr1 - size_t map7_len = 0; - CdmAppParameterMap::const_iterator itr = map7.begin(); - for (itr = map7.begin(); itr != map7.end(); ++itr) { - map7_len += itr->first.length(); - map7_len += itr->second.length(); - } - std::string data(arg, str1.size() + str2.size() + str3.size() + str4.size() + - str5.size() + str6.size() + map7_len + str8.size() + - kProtobufEstimatedOverhead); - bool map7_entries_present = true; - for (itr = map7.begin(); itr != map7.end(); ++itr) { - map7_entries_present = map7_entries_present && - data.find(itr->first) != std::string::npos && - data.find(itr->second) != std::string::npos; - } - return (data.find(str1) != std::string::npos && - data.find(str2) != std::string::npos && - data.find(str3) != std::string::npos && - data.find(str4) != std::string::npos && - data.find(str5) != std::string::npos && - data.find(str6) != std::string::npos && map7_entries_present && - data.find(str8) != std::string::npos); + return true; } -TEST_F(DeviceCertificateStoreTest, StoreCertificate) { +TEST_F(DeviceCertificateTest, StoreCertificate) { MockFileSystem file_system; std::string certificate(CdmRandom::RandomData(kCertificateLen)); std::string wrapped_private_key(CdmRandom::RandomData(kWrappedKeyLen)); std::string device_certificate_path = - device_base_path_ + DeviceFiles::GetCertificateFileName(); + device_base_path_ + DeviceFiles::GetCertificateFileName(false); // Call to Open will return a unique_ptr, freeing this object. MockFile* file = new MockFile(); EXPECT_CALL(file_system, DoOpen(StrEq(device_certificate_path), IsCreateFileFlagSet())) .WillOnce(Return(file)); - EXPECT_CALL(*file, Write(Contains(certificate, wrapped_private_key, 0), - Gt(certificate.size() + wrapped_private_key.size()))) + EXPECT_CALL(*file, Write(_, _)) + .With(AllArgs(StrAndLenContains( + std::vector{certificate, wrapped_private_key}))) .WillOnce(ReturnArg<1>()); EXPECT_CALL(*file, Read(_, _)).Times(0); @@ -2207,17 +2150,18 @@ TEST_F(DeviceCertificateStoreTest, StoreCertificate) { EXPECT_TRUE(device_files.StoreCertificate(certificate, wrapped_private_key)); } -// TODO(tinskip): Fix. kTestCertificateFileData appears to be incorect. -TEST_F(DeviceCertificateTest, DISABLED_ReadCertificate) { +TEST_P(DeviceCertificateTest, ReadCertificate) { MockFileSystem file_system; + const bool atsc_mode = GetParam(); std::string device_certificate_path = - device_base_path_ + DeviceFiles::GetCertificateFileName(); - std::string data = a2bs_hex(kTestCertificateFileData); + device_base_path_ + DeviceFiles::GetCertificateFileName(atsc_mode); + std::string data = kTestCertificateFileData; // Call to Open will return a unique_ptr, freeing this object. MockFile* file = new MockFile(); EXPECT_CALL(file_system, Exists(StrEq(device_certificate_path))) - .WillOnce(Return(true)); + .Times(2) + .WillRepeatedly(Return(true)); EXPECT_CALL(file_system, FileSize(StrEq(device_certificate_path))) .WillOnce(Return(data.size())); EXPECT_CALL(file_system, DoOpen(StrEq(device_certificate_path), _)) @@ -2233,16 +2177,18 @@ TEST_F(DeviceCertificateTest, DISABLED_ReadCertificate) { std::string certificate, wrapped_private_key; std::string serial_number; uint32_t system_id = 0; - ASSERT_TRUE(device_files.RetrieveCertificate( - &certificate, &wrapped_private_key, &serial_number, &system_id)); - EXPECT_EQ(kTestCertificate, b2a_hex(certificate)); - EXPECT_EQ(kTestWrappedPrivateKey, b2a_hex(wrapped_private_key)); + ASSERT_TRUE(device_files.RetrieveCertificate(atsc_mode, &certificate, + &wrapped_private_key, + &serial_number, &system_id)); + EXPECT_EQ(kTestCertificate, certificate); + EXPECT_EQ(kTestWrappedPrivateKey, wrapped_private_key); } -TEST_F(DeviceCertificateTest, HasCertificate) { +TEST_P(DeviceCertificateTest, HasCertificate) { MockFileSystem file_system; + bool atsc_mode = GetParam(); std::string device_certificate_path = - device_base_path_ + DeviceFiles::GetCertificateFileName(); + device_base_path_ + DeviceFiles::GetCertificateFileName(atsc_mode); EXPECT_CALL(file_system, Exists(StrEq(device_certificate_path))) .WillOnce(Return(false)) @@ -2253,11 +2199,14 @@ TEST_F(DeviceCertificateTest, HasCertificate) { ASSERT_TRUE(device_files.Init(kSecurityLevelL1)); // MockFile returns false. - EXPECT_FALSE(device_files.HasCertificate()); + EXPECT_FALSE(device_files.HasCertificate(atsc_mode)); // MockFile returns true. - EXPECT_TRUE(device_files.HasCertificate()); + EXPECT_TRUE(device_files.HasCertificate(atsc_mode)); } +INSTANTIATE_TEST_CASE_P(AtscMode, DeviceCertificateTest, + ::testing::Values(false, true)); + TEST_P(DeviceFilesSecurityLevelTest, SecurityLevel) { MockFileSystem file_system; std::string certificate(CdmRandom::RandomData(kCertificateLen)); @@ -2268,15 +2217,16 @@ TEST_P(DeviceFilesSecurityLevelTest, SecurityLevel) { ASSERT_TRUE( Properties::GetDeviceFilesBasePath(security_level, &device_base_path)); std::string device_certificate_path = - device_base_path + DeviceFiles::GetCertificateFileName(); + device_base_path + DeviceFiles::GetCertificateFileName(false); // Call to Open will return a unique_ptr, freeing this object. MockFile* file = new MockFile(); EXPECT_CALL(file_system, DoOpen(StrEq(device_certificate_path), IsCreateFileFlagSet())) .WillOnce(Return(file)); - EXPECT_CALL(*file, Write(Contains(certificate, wrapped_private_key, 0), - Gt(certificate.size() + wrapped_private_key.size()))) + EXPECT_CALL(*file, Write(_, _)) + .With(AllArgs(StrAndLenContains( + std::vector{certificate, wrapped_private_key}))) .WillOnce(ReturnArg<1>()); EXPECT_CALL(*file, Read(_, _)).Times(0); @@ -2298,20 +2248,26 @@ TEST_P(DeviceFilesStoreTest, StoreLicense) { CdmAppParameterMap app_parameters = GetAppParameters(kLicenseTestData[license_num].app_parameters); + std::vector expected_substrings{ + kLicenseTestData[license_num].pssh_data, + kLicenseTestData[license_num].key_request, + kLicenseTestData[license_num].key_response, + kLicenseTestData[license_num].key_renewal_request, + kLicenseTestData[license_num].key_renewal_response, + kLicenseTestData[license_num].key_release_url, + kLicenseTestData[license_num].usage_entry, + }; + for (const auto& iter : app_parameters) { + expected_substrings.push_back(iter.first); + expected_substrings.push_back(iter.second); + } + // Call to Open will return a unique_ptr, freeing this object. MockFile* file = new MockFile(); EXPECT_CALL(file_system, DoOpen(StrEq(license_path), IsCreateFileFlagSet())) .WillOnce(Return(file)); - EXPECT_CALL( - *file, - Write(Contains(kLicenseTestData[license_num].pssh_data, - kLicenseTestData[license_num].key_request, - kLicenseTestData[license_num].key_response, - kLicenseTestData[license_num].key_renewal_request, - kLicenseTestData[license_num].key_renewal_response, - kLicenseTestData[license_num].key_release_url, - app_parameters, kLicenseTestData[license_num].usage_entry), - Gt(GetLicenseDataSize(kLicenseTestData[license_num])))) + EXPECT_CALL(*file, Write(_, _)) + .With(AllArgs(StrAndLenContains(expected_substrings))) .WillOnce(ReturnArg<1>()); EXPECT_CALL(*file, Read(_, _)).Times(0); @@ -2350,20 +2306,27 @@ TEST_F(DeviceFilesTest, StoreLicenses) { CdmAppParameterMap app_parameters = GetAppParameters(kLicenseTestData[i].app_parameters); + std::vector expected_substrings{ + kLicenseTestData[i].pssh_data, + kLicenseTestData[i].key_request, + kLicenseTestData[i].key_response, + kLicenseTestData[i].key_renewal_request, + kLicenseTestData[i].key_renewal_response, + kLicenseTestData[i].key_release_url, + kLicenseTestData[i].usage_entry, + }; + for (const auto& iter : app_parameters) { + expected_substrings.push_back(iter.first); + expected_substrings.push_back(iter.second); + } + // Call to Open will return a unique_ptr, freeing this object. MockFile* file = new MockFile(); EXPECT_CALL(file_system, DoOpen(StrEq(license_path), IsCreateFileFlagSet())) .WillOnce(Return(file)); - EXPECT_CALL(*file, - Write(Contains(kLicenseTestData[i].pssh_data, - kLicenseTestData[i].key_request, - kLicenseTestData[i].key_response, - kLicenseTestData[i].key_renewal_request, - kLicenseTestData[i].key_renewal_response, - kLicenseTestData[i].key_release_url, - app_parameters, kLicenseTestData[i].usage_entry), - Gt(GetLicenseDataSize(kLicenseTestData[i])))) + EXPECT_CALL(*file, Write(_, _)) + .With(AllArgs(StrAndLenContains(expected_substrings))) .WillOnce(ReturnArg<1>()); EXPECT_CALL(*file, Read(_, _)).Times(0); } @@ -2523,8 +2486,8 @@ TEST_F(DeviceFilesTest, UpdateLicenseState) { MockFile* file = new MockFile(); EXPECT_CALL(file_system, DoOpen(StrEq(license_path), IsCreateFileFlagSet())) .WillOnce(Return(file)); - EXPECT_CALL(*file, Write(IsStrEq(kLicenseUpdateTestData[i].file_data), - Eq(kLicenseUpdateTestData[i].file_data.size()))) + EXPECT_CALL(*file, Write(_, _)) + .With(AllArgs(StrAndLenEq(kLicenseUpdateTestData[i].file_data))) .WillOnce(ReturnArg<1>()); EXPECT_CALL(*file, Read(_, _)).Times(0); DeviceFiles::CdmLicenseData license_data{ @@ -2955,7 +2918,6 @@ TEST_P(DeviceFilesUsageInfoTest, Store) { std::string file_name = DeviceFiles::GetUsageInfoFileName(app_id); std::string path = device_base_path_ + file_name; - size_t usage_data_fields_length = 0; std::vector usage_data_fields; std::vector usage_data_list; @@ -2969,18 +2931,12 @@ TEST_P(DeviceFilesUsageInfoTest, Store) { usage_data_fields.push_back(kUsageInfoTestData[i].usage_data.license); usage_data_fields.push_back(kUsageInfoTestData[i].usage_data.key_set_id); usage_data_fields.push_back(kUsageInfoTestData[i].usage_data.usage_entry); - usage_data_fields_length += - kUsageInfoTestData[i].usage_data.provider_session_token.size() + - kUsageInfoTestData[i].usage_data.license_request.size() + - kUsageInfoTestData[i].usage_data.license.size() + - kUsageInfoTestData[i].usage_data.key_set_id.size() + - kUsageInfoTestData[i].usage_data.usage_entry.size(); } } EXPECT_CALL(file_system, DoOpen(StrEq(path), _)).WillOnce(Return(file)); - EXPECT_CALL(*file, Write(ContainsAllElementsInVector(usage_data_fields), - Gt(usage_data_fields_length))) + EXPECT_CALL(*file, Write(_, _)) + .With(AllArgs(StrAndLenContains(usage_data_fields))) .WillOnce(ReturnArg<1>()); DeviceFiles device_files(&file_system); @@ -3222,7 +3178,6 @@ TEST_P(DeviceFilesUsageInfoTest, UpdateUsageInfo) { std::string file_name = DeviceFiles::GetUsageInfoFileName(app_id); std::string path = device_base_path_ + file_name; - size_t usage_data_fields_length = 0; std::vector usage_data_fields; size_t max_index_by_app_id = 0; @@ -3240,12 +3195,6 @@ TEST_P(DeviceFilesUsageInfoTest, UpdateUsageInfo) { kUsageInfoTestData[i].usage_data.key_set_id); usage_data_fields.push_back( kUsageInfoTestData[i].usage_data.usage_entry); - usage_data_fields_length += - kUsageInfoTestData[i].usage_data.provider_session_token.size() + - kUsageInfoTestData[i].usage_data.license_request.size() + - kUsageInfoTestData[i].usage_data.license.size() + - kUsageInfoTestData[i].usage_data.key_set_id.size() + - kUsageInfoTestData[i].usage_data.usage_entry.size(); } } } @@ -3257,12 +3206,6 @@ TEST_P(DeviceFilesUsageInfoTest, UpdateUsageInfo) { usage_data_fields.push_back(kUsageInfoUpdateTestData.license); usage_data_fields.push_back(kUsageInfoUpdateTestData.key_set_id); usage_data_fields.push_back(kUsageInfoUpdateTestData.usage_entry); - usage_data_fields_length += - kUsageInfoTestData[index].usage_data.provider_session_token.size() + - kUsageInfoUpdateTestData.license_request.size() + - kUsageInfoUpdateTestData.license.size() + - kUsageInfoUpdateTestData.key_set_id.size() + - kUsageInfoUpdateTestData.usage_entry.size(); } std::string file_data = @@ -3289,14 +3232,14 @@ TEST_P(DeviceFilesUsageInfoTest, UpdateUsageInfo) { .Times(2) .WillOnce(Return(file)) .WillOnce(Return(next_file)); - ON_CALL(*file, Write(ContainsAllElementsInVector(usage_data_fields), - Gt(usage_data_fields_length))) + ON_CALL(*file, Write(_, _)) + .With(AllArgs(StrAndLenContains(usage_data_fields))) .WillByDefault(DoAll(InvokeWithoutArgs([&write_called]() -> void { write_called = true; }), ReturnArg<1>())); - ON_CALL(*next_file, Write(ContainsAllElementsInVector(usage_data_fields), - Gt(usage_data_fields_length))) + ON_CALL(*next_file, Write(_, _)) + .With(AllArgs(StrAndLenContains(usage_data_fields))) .WillByDefault(DoAll(InvokeWithoutArgs([&write_called]() -> void { write_called = true; }), @@ -3358,8 +3301,9 @@ TEST_P(DeviceFilesHlsAttributesTest, Store) { EXPECT_CALL(file_system, Exists(StrEq(path))).WillRepeatedly(Return(true)); EXPECT_CALL(file_system, DoOpen(StrEq(path), _)).WillOnce(Return(file)); - EXPECT_CALL(*file, Write(Contains(param->media_segment_iv, 0), - Gt(param->media_segment_iv.size()))) + EXPECT_CALL(*file, Write(_, _)) + .With(AllArgs( + StrAndLenContains(std::vector{param->media_segment_iv}))) .WillOnce(ReturnArg<1>()); EXPECT_CALL(*file, Read(_, _)).Times(0); @@ -3394,7 +3338,6 @@ TEST_P(DeviceFilesUsageTableTest, Store) { MockFile* file = new MockFile(); int index = GetParam(); - size_t entry_data_length = 0; std::vector entry_data; std::vector usage_entry_info; usage_entry_info.resize(index + 1); @@ -3402,18 +3345,15 @@ TEST_P(DeviceFilesUsageTableTest, Store) { usage_entry_info[i] = kUsageEntriesTestData[i]; entry_data.push_back(kUsageEntriesTestData[i].key_set_id); entry_data.push_back(kUsageEntriesTestData[i].usage_info_file_name); - entry_data_length += kUsageEntriesTestData[i].key_set_id.size() + - kUsageEntriesTestData[i].usage_info_file_name.size(); } entry_data.push_back(kUsageTableInfoTestData[index].usage_table_header); - entry_data_length += kUsageTableInfoTestData[index].usage_table_header.size(); std::string path = device_base_path_ + DeviceFiles::GetUsageTableFileName(); EXPECT_CALL(file_system, Exists(StrEq(path))).WillRepeatedly(Return(true)); EXPECT_CALL(file_system, DoOpen(StrEq(path), _)).WillOnce(Return(file)); - EXPECT_CALL(*file, Write(ContainsAllElementsInVector(entry_data), - Gt(entry_data_length))) + EXPECT_CALL(*file, Write(_, _)) + .With(AllArgs(StrAndLenContains(entry_data))) .WillOnce(ReturnArg<1>()); EXPECT_CALL(*file, Read(_, _)).Times(0); diff --git a/core/test/http_socket.cpp b/core/test/http_socket.cpp index 38610791..6769ebb9 100644 --- a/core/test/http_socket.cpp +++ b/core/test/http_socket.cpp @@ -8,7 +8,8 @@ #include #include #include -#include + +#include #ifdef _WIN32 # include "winsock2.h" @@ -33,6 +34,11 @@ namespace wvcdm { namespace { +// Number of attempts to identify an Internet host and a service should the +// host's nameserver be temporarily unavailable. See getaddrinfo(3) for +// more info. +constexpr size_t kMaxNameserverAttempts = 2; + // Helper function to tokenize a string. This makes it easier to avoid silly // parsing bugs that creep in easily when each part of the string is parsed // with its own piece of code. @@ -71,6 +77,17 @@ bool IsRetryableSslError(int ssl_error) { ssl_error != SSL_ERROR_SSL; } +// Ensures that the SSL library is only initialized once. +void InitSslLibrary() { + static bool ssl_initialized = false; + static std::mutex ssl_init_mutex; + std::lock_guard guard(ssl_init_mutex); + if (!ssl_initialized) { + SSL_library_init(); + ssl_initialized = true; + } +} + #if 0 // unused, may be useful for debugging SSL-related issues. void ShowServerCertificate(const SSL* ssl) { @@ -211,7 +228,7 @@ HttpSocket::HttpSocket(const std::string& url) : socket_fd_(-1), ssl_(nullptr), ssl_ctx_(nullptr) { valid_url_ = ParseUrl(url, &scheme_, &secure_connect_, &domain_name_, &port_, &resource_path_); - SSL_library_init(); + InitSslLibrary(); } HttpSocket::~HttpSocket() { CloseSocket(); } @@ -237,6 +254,12 @@ void HttpSocket::CloseSocket() { bool HttpSocket::Connect(int timeout_in_ms) { if (!valid_url_) { + LOGE("URL is invalid"); + return false; + } + + if (socket_fd_ != -1) { + LOGE("Socket already connected"); return false; } @@ -259,24 +282,42 @@ bool HttpSocket::Connect(int timeout_in_ms) { hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_NUMERICSERV | AI_ADDRCONFIG; - struct addrinfo* addr_info = nullptr; - int ret = - getaddrinfo(domain_name_.c_str(), port_.c_str(), &hints, &addr_info); + + int ret = EAI_AGAIN; + for (size_t attempt = 1; + attempt <= kMaxNameserverAttempts && ret == EAI_AGAIN; ++attempt) { + if (attempt > 1) { + LOGW( + "Nameserver is temporarily unavailable, waiting to try again: " + "attempt = %zu", + attempt); + sleep(1); + } + ret = getaddrinfo(domain_name_.c_str(), port_.c_str(), &hints, &addr_info); + } + if (ret != 0) { - LOGE("getaddrinfo failed, errno = %d", ret); + if (ret == EAI_SYSTEM) { + // EAI_SYSTEM implies an underlying system issue. Error is + // specified by |errno|. + LOGE("getaddrinfo failed due to system error: errno = %d", GetError()); + } else { + // Error is specified by return value. + LOGE("getaddrinfo failed: ret = %d", ret); + } return false; } - // get a socket + // Open a socket. socket_fd_ = socket(addr_info->ai_family, addr_info->ai_socktype, addr_info->ai_protocol); if (socket_fd_ < 0) { - LOGE("cannot open socket, errno = %d", GetError()); + LOGE("Cannot open socket: errno = %d", GetError()); return false; } - // set the socket in non-blocking mode + // Set the socket in non-blocking mode. #ifdef _WIN32 u_long mode = 1; // Non-blocking mode. if (ioctlsocket(socket_fd_, FIONBIO, &mode) != 0) { @@ -285,7 +326,7 @@ bool HttpSocket::Connect(int timeout_in_ms) { return false; } #else - int original_flags = fcntl(socket_fd_, F_GETFL, 0); + const int original_flags = fcntl(socket_fd_, F_GETFL, 0); if (original_flags == -1) { LOGE("fcntl error, errno = %d", errno); CloseSocket(); @@ -301,9 +342,10 @@ bool HttpSocket::Connect(int timeout_in_ms) { // connect to the server ret = connect(socket_fd_, addr_info->ai_addr, addr_info->ai_addrlen); freeaddrinfo(addr_info); + addr_info = nullptr; if (ret == 0) { - // connected right away. + // Connected right away. } else { if (GetError() != ERROR_ASYNC_COMPLETE) { // failed right away. @@ -336,6 +378,8 @@ bool HttpSocket::Connect(int timeout_in_ms) { return false; } + // |BIO_NOCLOSE| prevents closing the socket from being closed when + // the BIO is freed. BIO* a_bio = BIO_new_socket(socket_fd_, BIO_NOCLOSE); if (!a_bio) { LOGE("BIO_new_socket error"); @@ -354,9 +398,9 @@ bool HttpSocket::Connect(int timeout_in_ms) { CloseSocket(); return false; } - bool for_read = ssl_err == SSL_ERROR_WANT_READ; + const bool for_read = (ssl_err == SSL_ERROR_WANT_READ); if (!SocketWait(socket_fd_, for_read, timeout_in_ms)) { - LOGE("cannot connect to %s", domain_name_.c_str()); + LOGE("Cannot connect securely to %s", domain_name_.c_str()); CloseSocket(); return false; } diff --git a/core/test/license_request.h b/core/test/license_request.h index 93254c1e..6188d8e4 100644 --- a/core/test/license_request.h +++ b/core/test/license_request.h @@ -15,8 +15,8 @@ namespace wvcdm { // Google license servers. class LicenseRequest { public: - LicenseRequest() {}; - ~LicenseRequest() {}; + LicenseRequest() {} + ~LicenseRequest() {} void GetDrmMessage(const std::string& response, std::string& drm_msg); diff --git a/core/test/parallel_operations_test.cpp b/core/test/parallel_operations_test.cpp index 458a17d3..c0632639 100644 --- a/core/test/parallel_operations_test.cpp +++ b/core/test/parallel_operations_test.cpp @@ -6,7 +6,6 @@ // would, but in parallel, attempting to create as many collisions in the CDM // code as possible. -#include #include #include #include @@ -41,6 +40,12 @@ constexpr const auto kMinimumWait = std::chrono::nanoseconds(1); constexpr const int kRepetitions = 10; constexpr const int kThreadCount = 24; +// Number of attempts to request a license key from the license server +// before failing. +constexpr size_t kMaxKeyRequestAttempts = 5; +// Wait time between failed key requests. +constexpr const auto kRequestRetryWait = std::chrono::milliseconds(10); + const std::vector kKeyId = a2b_hex("371ea35e1a985d75d198a7f41020dc23"); const std::vector kIv = a2b_hex("cedc47cccd6cb437af41325953c2e5e0"); const std::vector kEncryptedData = a2b_hex( @@ -110,17 +115,27 @@ class ParallelCdmTest : public WvCdmTestBase, ASSERT_EQ(kKeyRequestTypeInitial, key_request->type); } - void GetKeyResponse(const std::string& url, const CdmKeyRequest& key_request, + void GetKeyResponse(const CdmSessionId& session_id, const std::string& url, + const CdmKeyRequest& key_request, std::string* key_response) { - UrlRequest url_request(url); - ASSERT_TRUE(url_request.is_connected()); - + bool request_ok = false; std::string http_response; - url_request.PostRequest(key_request.message); - ASSERT_TRUE(url_request.GetResponse(&http_response)); - int status_code = url_request.GetStatusCode(http_response); - ASSERT_EQ(kHttpOk, status_code); - + for (size_t attempt = 1; attempt <= kMaxKeyRequestAttempts; ++attempt) { + UrlRequest url_request(url); + ASSERT_TRUE(url_request.is_connected()); + url_request.PostRequest(key_request.message); + if (url_request.GetResponse(&http_response)) { + int status_code = url_request.GetStatusCode(http_response); + ASSERT_EQ(kHttpOk, status_code); + request_ok = true; + break; + } else { + LOGW("License request failed: sid = %s, attempt = %zu", + session_id.c_str(), attempt); + std::this_thread::sleep_for(kRequestRetryWait); + } + } + ASSERT_TRUE(request_ok); LicenseRequest license_request; license_request.GetDrmMessage(http_response, *key_response); } @@ -184,8 +199,10 @@ class ParallelCdmTest : public WvCdmTestBase, template void RunSessionThreadsSimultaneously(Function do_work) { std::atomic threads_waiting(0); - - const int session_count = std::min(kThreadCount, GetMaxNumberOfSessions()); + // The OEMCrypto V16 adapter makes use of one of the sessions, + // reducing the maximum number of sessions available for the test. + const int session_count = + std::min(kThreadCount, GetMaxNumberOfSessions() - 1); LOGI("Running %d Threads", session_count); std::vector sessions(session_count); std::vector> threads; @@ -231,7 +248,10 @@ TEST_P(ParallelCdmTest, ParallelLicenseRequests) { GenerateKeyRequest(session_id, init_data, &key_request)); std::string key_response; - ASSERT_NO_FATAL_FAILURE(GetKeyResponse(url, key_request, &key_response)); + LOGD("Getting license request: sid = %s", session_id.c_str()); + ASSERT_NO_FATAL_FAILURE( + GetKeyResponse(session_id, url, key_request, &key_response)) + << "SID: " << session_id; ASSERT_NO_FATAL_FAILURE(AddKey(session_id, key_response)); }); @@ -248,7 +268,8 @@ TEST_P(ParallelCdmTest, ParallelDecryptSessions) { GenerateKeyRequest(session_id, init_data, &key_request)); std::string key_response; - ASSERT_NO_FATAL_FAILURE(GetKeyResponse(url, key_request, &key_response)); + ASSERT_NO_FATAL_FAILURE( + GetKeyResponse(session_id, url, key_request, &key_response)); ASSERT_NO_FATAL_FAILURE(AddKey(session_id, key_response)); @@ -273,7 +294,8 @@ TEST_P(ParallelCdmTest, ParallelDecryptsInSameSession) { GenerateKeyRequest(session_id, init_data, &key_request)); std::string key_response; - ASSERT_NO_FATAL_FAILURE(GetKeyResponse(url, key_request, &key_response)); + ASSERT_NO_FATAL_FAILURE( + GetKeyResponse(session_id, url, key_request, &key_response)); ASSERT_NO_FATAL_FAILURE(AddKey(session_id, key_response)); diff --git a/core/test/service_certificate_unittest.cpp b/core/test/service_certificate_unittest.cpp index aecbc92a..6958d307 100644 --- a/core/test/service_certificate_unittest.cpp +++ b/core/test/service_certificate_unittest.cpp @@ -79,6 +79,7 @@ class StubCdmClientPropertySet : public CdmClientPropertySet { } uint32_t session_sharing_id() const override { return session_sharing_id_; } + virtual bool use_atsc_mode() const { return false; } void set_session_sharing_id(uint32_t id) override { session_sharing_id_ = id; diff --git a/core/test/test_base.cpp b/core/test/test_base.cpp index 572cef79..b527645b 100644 --- a/core/test/test_base.cpp +++ b/core/test/test_base.cpp @@ -294,8 +294,8 @@ void WvCdmTestBase::Provision() { std::shared_ptr(new EngineMetrics)); FakeProvisioningServer server; CdmResponseType result = cdm_engine.GetProvisioningRequest( - cert_type, cert_authority, server.service_certificate(), &prov_request, - &provisioning_server_url); + cert_type, cert_authority, server.service_certificate(), kLevelDefault, + &prov_request, &provisioning_server_url); ASSERT_EQ(NO_ERROR, result); if (!binary_provisioning_) { std::vector prov_request_v = Base64SafeDecode(prov_request); @@ -304,8 +304,8 @@ void WvCdmTestBase::Provision() { std::string response; ASSERT_TRUE(server.MakeResponse(prov_request, &response)) << "Fake provisioning server could not provision"; - result = - cdm_engine.HandleProvisioningResponse(response, &cert, &wrapped_key); + result = cdm_engine.HandleProvisioningResponse(response, kLevelDefault, + &cert, &wrapped_key); EXPECT_EQ(NO_ERROR, result); } else { // TODO(fredgc): provision for different SPOIDs. @@ -314,7 +314,7 @@ void WvCdmTestBase::Provision() { CdmResponseType result = cdm_engine.GetProvisioningRequest( cert_type, cert_authority, config_.provisioning_service_certificate(), - &prov_request, &provisioning_server_url); + kLevelDefault, &prov_request, &provisioning_server_url); ASSERT_EQ(NO_ERROR, result); if (binary_provisioning_) { @@ -329,84 +329,54 @@ void WvCdmTestBase::Provision() { // for test vs. production server. provisioning_server_url.assign(config_.provisioning_server()); - // TODO(b/139361531): Remove loop once provisioning service is stable. - std::string http_message; - size_t attempt_num = 0; - bool provision_success = false; - do { - if (attempt_num > 0) { - // Sleep between attempts. - std::this_thread::sleep_for(std::chrono::seconds(1)); - } - ++attempt_num; - - // Make request. - UrlRequest url_request(provisioning_server_url); - if (!url_request.is_connected()) { - LOGE("Failed to connect to provisioning server: url = %s", - provisioning_server_url.c_str()); - continue; - } - url_request.PostCertRequestInQueryString(prov_request); - - // Receive and parse response. - if (!url_request.GetResponse(&http_message)) { - LOGE("Failed to get provisioning response"); - continue; - } - - LOGV("http_message: \n%s\n", http_message.c_str()); - - if (binary_provisioning_) { - // extract provisioning response from received message - // Extracts signed response from JSON string, result is serialized - // protobuf. - static const std::string kMessageStart = "\"signedResponse\": \""; - static const std::string kMessageEnd = "\""; - std::string protobuf_response; - if (!ExtractSignedMessage(http_message, kMessageStart, kMessageEnd, - &protobuf_response)) { - LOGE( - "Failed to extract signed serialized response from JSON " - "response"); - continue; - } - - LOGV("Extracted response message: \n%s\n", protobuf_response.c_str()); - - // base64 decode response to yield binary protobuf - std::vector response_vec(Base64SafeDecode(protobuf_response)); - if (response_vec.empty() && !protobuf_response.empty()) { - LOGE("Failed to decode base64 of response: response = %s", - protobuf_response.c_str()); - continue; - } - - std::string binary_protobuf_response(response_vec.begin(), - response_vec.end()); - - if (cdm_engine.HandleProvisioningResponse( - binary_protobuf_response, &cert, &wrapped_key) != NO_ERROR) { - LOGE("Failed to handle provisioning response"); - continue; - } - } else { - if (cdm_engine.HandleProvisioningResponse(http_message, &cert, - &wrapped_key) != NO_ERROR) { - LOGE("Failed to handle binary provisioning response"); - continue; - } - } - provision_success = true; - } while (attempt_num <= kDefaultMaxProvisioningAttempts && - !provision_success); - - if (attempt_num > 1) { - LOGW("Provisioning request failed at least once: attempts = %zu", - attempt_num); + // Make request. + UrlRequest url_request(provisioning_server_url); + if (!url_request.is_connected()) { + LOGE("Failed to connect to provisioning server: url = %s", + provisioning_server_url.c_str()); + } + url_request.PostCertRequestInQueryString(prov_request); + + // Receive and parse response. + std::string http_message; + ASSERT_TRUE(url_request.GetResponse(&http_message)) + << "Failed to get provisioning response"; + LOGV("http_message: \n%s\n", http_message.c_str()); + + if (binary_provisioning_) { + // extract provisioning response from received message + // Extracts signed response from JSON string, result is serialized + // protobuf. + static const std::string kMessageStart = "\"signedResponse\": \""; + static const std::string kMessageEnd = "\""; + std::string protobuf_response; + const bool extract_ok = ExtractSignedMessage( + http_message, kMessageStart, kMessageEnd, &protobuf_response); + ASSERT_TRUE(extract_ok) << "Failed to extract signed serialized " + "response from JSON response"; + LOGV("Extracted response message: \n%s\n", protobuf_response.c_str()); + + ASSERT_FALSE(protobuf_response.empty()) + << "Protobuf response is unexpectedly empty"; + + // base64 decode response to yield binary protobuf + const std::vector response_vec( + Base64SafeDecode(protobuf_response)); + ASSERT_FALSE(response_vec.empty()) + << "Failed to decode base64 of response: response = " + << protobuf_response; + + const std::string binary_protobuf_response(response_vec.begin(), + response_vec.end()); + + ASSERT_EQ(NO_ERROR, cdm_engine.HandleProvisioningResponse( + binary_protobuf_response, kLevelDefault, &cert, + &wrapped_key)); + } else { + ASSERT_EQ(NO_ERROR, + cdm_engine.HandleProvisioningResponse( + http_message, kLevelDefault, &cert, &wrapped_key)); } - ASSERT_TRUE(provision_success) - << "Failed to provision: message = " << http_message; } } @@ -561,6 +531,7 @@ void TestLicenseHolder::GenerateKeyRequest( CdmAppParameterMap app_parameters; CdmKeySetId key_set_id; InitializationData init_data(init_data_type_string, key_id); + if (g_cutoff >= LOG_DEBUG) init_data.DumpToLogs(); CdmKeyRequest key_request; CdmResponseType result = cdm_engine_->GenerateKeyRequest( session_id_, key_set_id, init_data, kLicenseTypeStreaming, app_parameters, diff --git a/core/test/test_base.h b/core/test/test_base.h index e4c75584..c6ba927f 100644 --- a/core/test/test_base.h +++ b/core/test/test_base.h @@ -22,9 +22,6 @@ namespace wvcdm { // to configure OEMCrypto to use a test keybox. class WvCdmTestBase : public ::testing::Test { public: - // Default number of provisioning try attempts. - constexpr static size_t kDefaultMaxProvisioningAttempts = 10; - WvCdmTestBase(); ~WvCdmTestBase() override {} void SetUp() override; diff --git a/core/test/test_printers.cpp b/core/test/test_printers.cpp index fb6b7fb0..b88b8910 100644 --- a/core/test/test_printers.cpp +++ b/core/test/test_printers.cpp @@ -284,21 +284,6 @@ void PrintTo(const enum CdmResponseType& value, ::std::ostream* os) { case INSUFFICIENT_CRYPTO_RESOURCES: *os << "INSUFFICIENT_CRYPTO_RESOURCES"; break; - case INSUFFICIENT_CRYPTO_RESOURCES_2: - *os << "INSUFFICIENT_CRYPTO_RESOURCES_2"; - break; - case INSUFFICIENT_CRYPTO_RESOURCES_3: - *os << "INSUFFICIENT_CRYPTO_RESOURCES_3"; - break; - case INSUFFICIENT_CRYPTO_RESOURCES_4: - *os << "INSUFFICIENT_CRYPTO_RESOURCES_4"; - break; - case INSUFFICIENT_CRYPTO_RESOURCES_5: - *os << "INSUFFICIENT_CRYPTO_RESOURCES_5"; - break; - case INSUFFICIENT_CRYPTO_RESOURCES_6: - *os << "INSUFFICIENT_CRYPTO_RESOURCES_6"; - break; case INSUFFICIENT_OUTPUT_PROTECTION: *os << "INSUFFICIENT_OUTPUT_PROTECTION"; break; @@ -521,6 +506,9 @@ void PrintTo(const enum CdmResponseType& value, ::std::ostream* os) { case LICENSE_RESPONSE_PARSE_ERROR_5: *os << "LICENSE_RESPONSE_PARSE_ERROR_5"; break; + case LICENSE_USAGE_ENTRY_MISSING: + *os << "LICENSE_USAGE_ENTRY_MISSING"; + break; case LIST_LICENSE_ERROR_1: *os << "LIST_LICENSE_ERROR_1"; break; @@ -560,6 +548,9 @@ void PrintTo(const enum CdmResponseType& value, ::std::ostream* os) { case LOAD_USAGE_ENTRY_GENERATION_SKEW: *os << "LOAD_USAGE_ENTRY_GENERATION_SKEW"; break; + case LOAD_USAGE_ENTRY_INVALID_SESSION: + *os << "LOAD_USAGE_ENTRY_INVALID_SESSION"; + break; case LOAD_USAGE_ENTRY_SIGNATURE_FAILURE: *os << "LOAD_USAGE_ENTRY_SIGNATURE_FAILURE"; break; @@ -578,6 +569,9 @@ void PrintTo(const enum CdmResponseType& value, ::std::ostream* os) { case LOAD_USAGE_HEADER_UNKNOWN_ERROR: *os << "LOAD_USAGE_HEADER_UNKNOWN_ERROR"; break; + case MOVE_USAGE_ENTRY_DESTINATION_IN_USE: + *os << "MOVE_USAGE_ENTRY_DESTINATION_IN_USE"; + break; case MOVE_USAGE_ENTRY_UNKNOWN_ERROR: *os << "MOVE_USAGE_ENTRY_UNKNOWN_ERROR"; break; @@ -818,8 +812,11 @@ void PrintTo(const enum CdmResponseType& value, ::std::ostream* os) { case SET_DECRYPT_HASH_ERROR: *os << "SET_DECRYPT_HASH_ERROR"; break; - case SHRINK_USAGE_TABLER_HEADER_UNKNOWN_ERROR: - *os << "SHRINK_USAGE_TABLER_HEADER_UNKNOWN_ERROR"; + case SHRINK_USAGE_TABLE_HEADER_ENTRY_IN_USE: + *os << "SHRINK_USAGE_TABLE_HEADER_ENTRY_IN_USE"; + break; + case SHRINK_USAGE_TABLE_HEADER_UNKNOWN_ERROR: + *os << "SHRINK_USAGE_TABLE_HEADER_UNKNOWN_ERROR"; break; case SIGNATURE_NOT_FOUND: *os << "SIGNATURE_NOT_FOUND"; @@ -923,6 +920,9 @@ void PrintTo(const enum CdmResponseType& value, ::std::ostream* os) { case WEBM_INIT_DATA_UNAVAILABLE: *os << "WEBM_INIT_DATA_UNAVAILABLE"; break; + case PROVISIONING_NOT_ALLOWED_FOR_ATSC: + *os << "PROVISIONING_NOT_ALLOWED_FOR_ATSC"; + break; default: *os << "Unknown CdmResponseType"; break; diff --git a/core/test/url_request.cpp b/core/test/url_request.cpp index 456eba49..2f89e07a 100644 --- a/core/test/url_request.cpp +++ b/core/test/url_request.cpp @@ -87,8 +87,8 @@ void UrlRequest::Reconnect() { if (socket_.Connect(kConnectTimeoutMs)) { is_connected_ = true; } else { - LOGE("failed to connect to %s, port=%d", socket_.domain_name().c_str(), - socket_.port()); + LOGE("Failed to connect: url = %s, port = %d, attempt = %u", + socket_.domain_name().c_str(), socket_.port(), i); } } } @@ -148,7 +148,7 @@ bool UrlRequest::PostRequestWithPath(const std::string& path, // buffer to store length of data as a string char data_size_buffer[32] = {0}; - snprintf(data_size_buffer, sizeof(data_size_buffer), "%zd", data.size()); + snprintf(data_size_buffer, sizeof(data_size_buffer), "%zu", data.size()); request.append("Content-Length: "); request.append(data_size_buffer); // appends size of data @@ -158,7 +158,8 @@ bool UrlRequest::PostRequestWithPath(const std::string& path, request.append(data); - int ret = socket_.Write(request.c_str(), request.size(), kWriteTimeoutMs); + const int ret = + socket_.Write(request.c_str(), request.size(), kWriteTimeoutMs); LOGV("HTTP request: (%d): %s", request.size(), b2a_hex(request).c_str()); return ret != -1; } diff --git a/core/test/usage_table_header_unittest.cpp b/core/test/usage_table_header_unittest.cpp index 8a5c6f32..a0ac01ee 100644 --- a/core/test/usage_table_header_unittest.cpp +++ b/core/test/usage_table_header_unittest.cpp @@ -21,12 +21,36 @@ #include "wv_cdm_constants.h" #include "wv_cdm_types.h" +// gmock methods +using ::testing::_; +using ::testing::AllOf; +using ::testing::AtMost; +using ::testing::ContainerEq; +using ::testing::Contains; +using ::testing::DoAll; +using ::testing::ElementsAre; +using ::testing::ElementsAreArray; +using ::testing::Ge; +using ::testing::Invoke; +using ::testing::InvokeWithoutArgs; +using ::testing::Lt; +using ::testing::NotNull; +using ::testing::Return; +using ::testing::SaveArg; +using ::testing::SetArgPointee; +using ::testing::SizeIs; +using ::testing::StrEq; +using ::testing::UnorderedElementsAre; +using ::testing::UnorderedElementsAreArray; + namespace wvcdm { namespace { const std::string kEmptyString; +constexpr size_t kDefaultTableCapacity = 300; + constexpr int64_t kDefaultExpireDuration = 33 * 24 * 60 * 60; // 33 Days const CdmUsageTableHeader kEmptyUsageTableHeader; @@ -172,7 +196,7 @@ const std::vector kEmptyUsageInfoUsageDataList; const std::vector kEmptyUsageEntryInfoVector; std::vector kUsageEntryInfoVector; std::vector k10UsageEntryInfoVector; -std::vector k201UsageEntryInfoVector; +std::vector kOverFullUsageEntryInfoVector; const DeviceFiles::LicenseState kActiveLicenseState = DeviceFiles::kLicenseStateActive; @@ -189,7 +213,8 @@ int64_t kPlaybackDuration = 300; int64_t kGracePeriodEndTime = 60; // ==== LRU Upgrade Data ==== -const CdmUsageTableHeader kUpgradableUsageTableHeader = {0}; +const CdmUsageTableHeader kUpgradableUsageTableHeader = + "Upgradable Table Header"; // Usage entries. const CdmUsageEntryInfo kUpgradableUsageEntryInfo1 = { @@ -251,7 +276,7 @@ const int64_t kUpgradedUsageEntryInfo2ExpireTime = 0; // Unset const int64_t kUpgradedUsageEntryInfo3LastUsedTime = kLruBaseTime; const int64_t kUpgradedUsageEntryInfo3ExpireTime = kLruBaseTime + 604800 + 86400; -const CdmUsageTableHeader kUpgradedUsageTableHeader = {0}; +const CdmUsageTableHeader kUpgradedUsageTableHeader = "Upgraded Table Header"; std::vector kUpgradedUsageEntryInfoList; namespace { @@ -274,23 +299,24 @@ void InitVectorConstants() { k10UsageEntryInfoVector.push_back(kUsageEntryInfoOfflineLicense5); k10UsageEntryInfoVector.push_back(kUsageEntryInfoSecureStop5); - k201UsageEntryInfoVector.clear(); - for (size_t i = 0; i < 201; ++i) { + kOverFullUsageEntryInfoVector.clear(); + for (size_t i = 0; i < (kDefaultTableCapacity + 1); ++i) { switch (i % 4) { case 0: - k201UsageEntryInfoVector.push_back(kUsageEntryInfoOfflineLicense1); + kOverFullUsageEntryInfoVector.push_back(kUsageEntryInfoOfflineLicense1); break; case 1: - k201UsageEntryInfoVector.push_back(kUsageEntryInfoSecureStop1); + kOverFullUsageEntryInfoVector.push_back(kUsageEntryInfoSecureStop1); break; case 2: - k201UsageEntryInfoVector.push_back(kUsageEntryInfoOfflineLicense2); + kOverFullUsageEntryInfoVector.push_back(kUsageEntryInfoOfflineLicense2); break; case 3: - k201UsageEntryInfoVector.push_back(kUsageEntryInfoSecureStop2); + kOverFullUsageEntryInfoVector.push_back(kUsageEntryInfoSecureStop2); break; default: - k201UsageEntryInfoVector.push_back(kUsageEntryInfoStorageTypeUnknown); + kOverFullUsageEntryInfoVector.push_back( + kUsageEntryInfoStorageTypeUnknown); break; } } @@ -349,28 +375,6 @@ void ToVector(std::vector& vec, } } -// Used to quickly populate a vector of CdmUsageEntryInfo structs with LRU -// information. This is intended to allow tests which are not concerned with -// the LRU replacement policy of the UsageTableHeader, but are affected by its -// presents. -void GenericLruUpgrade(std::vector* usage_entry_info_list, - int64_t last_use_time = kLruBaseTime, - int64_t offline_license_expiry_time = - kLruBaseTime + kDefaultExpireDuration) { - if (usage_entry_info_list == nullptr) { - return; - } - for (auto& usage_entry_info : *usage_entry_info_list) { - usage_entry_info.last_use_time = last_use_time; - if (usage_entry_info.storage_type == kStorageLicense) { - usage_entry_info.offline_license_expiry_time = - offline_license_expiry_time; - } else { - usage_entry_info.offline_license_expiry_time = 0; - } - } -} - }; // namespace class MockDeviceFiles : public DeviceFiles { @@ -400,6 +404,7 @@ class MockDeviceFiles : public DeviceFiles { bool(const std::string&, const std::string&, std::string*, CdmKeyMessage*, CdmKeyResponse*, CdmUsageEntry*, uint32_t*)); + MOCK_METHOD2(StoreLicense, bool(const CdmLicenseData&, ResponseType*)); MOCK_METHOD1(DeleteLicense, bool(const std::string&)); MOCK_METHOD0(DeleteAllLicenses, bool()); MOCK_METHOD0(DeleteAllUsageInfo, bool()); @@ -424,16 +429,19 @@ class MockCryptoSession : public TestCryptoSession { MockCryptoSession(metrics::CryptoMetrics* metrics) : TestCryptoSession(metrics) {} MOCK_METHOD1(Open, CdmResponseType(SecurityLevel)); - MOCK_METHOD1(LoadUsageTableHeader, - CdmResponseType(const CdmUsageTableHeader&)); - MOCK_METHOD1(CreateUsageTableHeader, CdmResponseType(CdmUsageTableHeader*)); + // Usage Table Header. + MOCK_METHOD2(CreateUsageTableHeader, + CdmResponseType(SecurityLevel, CdmUsageTableHeader*)); + MOCK_METHOD2(LoadUsageTableHeader, + CdmResponseType(SecurityLevel, const CdmUsageTableHeader&)); + MOCK_METHOD3(ShrinkUsageTableHeader, + CdmResponseType(SecurityLevel, uint32_t, CdmUsageTableHeader*)); + // Usage Entry. MOCK_METHOD1(CreateUsageEntry, CdmResponseType(uint32_t*)); MOCK_METHOD2(LoadUsageEntry, CdmResponseType(uint32_t, const CdmUsageEntry&)); MOCK_METHOD2(UpdateUsageEntry, CdmResponseType(CdmUsageTableHeader*, CdmUsageEntry*)); MOCK_METHOD1(MoveUsageEntry, CdmResponseType(uint32_t)); - MOCK_METHOD2(ShrinkUsageTableHeader, - CdmResponseType(uint32_t, CdmUsageTableHeader*)); // Fake method for testing. Having an EXPECT_CALL causes complexities // for getting table capacity during initialization. @@ -453,8 +461,8 @@ class MockCryptoSession : public TestCryptoSession { } private: - size_t maximum_usage_table_entries_ = 0; - bool maximum_usage_table_entries_set_ = false; + size_t maximum_usage_table_entries_ = kDefaultTableCapacity; + bool maximum_usage_table_entries_set_ = true; }; // Partial mock of the UsageTableHeader. This is to test when dependency @@ -462,32 +470,27 @@ class MockCryptoSession : public TestCryptoSession { class MockUsageTableHeader : public UsageTableHeader { public: MockUsageTableHeader() : UsageTableHeader() {} - MOCK_METHOD3(DeleteEntry, CdmResponseType(uint32_t, DeviceFiles*, - metrics::CryptoMetrics*)); + MOCK_METHOD4(InvalidateEntry, CdmResponseType(uint32_t, bool, DeviceFiles*, + metrics::CryptoMetrics*)); + MOCK_METHOD6(AddEntry, + CdmResponseType(CryptoSession*, bool, const CdmKeySetId&, + const std::string&, const CdmKeyResponse&, + uint32_t*)); + + CdmResponseType SuperAddEntry(CryptoSession* crypto_session, + bool persistent_license, + const CdmKeySetId& key_set_id, + const std::string& usage_info_filename, + const CdmKeyResponse& license_message, + uint32_t* usage_entry_number) { + return UsageTableHeader::AddEntry(crypto_session, persistent_license, + key_set_id, usage_info_filename, + license_message, usage_entry_number); + } }; } // namespace -// gmock methods -using ::testing::_; -using ::testing::AllOf; -using ::testing::AtMost; -using ::testing::ContainerEq; -using ::testing::Contains; -using ::testing::DoAll; -using ::testing::ElementsAreArray; -using ::testing::Ge; -using ::testing::Invoke; -using ::testing::Lt; -using ::testing::NotNull; -using ::testing::Return; -using ::testing::SaveArg; -using ::testing::SetArgPointee; -using ::testing::SizeIs; -using ::testing::StrEq; -using ::testing::UnorderedElementsAre; -using ::testing::UnorderedElementsAreArray; - class UsageTableHeaderTest : public WvCdmTestBase { public: static void SetUpTestCase() { @@ -495,9 +498,9 @@ class UsageTableHeaderTest : public WvCdmTestBase { } // Useful when UsageTableHeader is mocked - void DeleteEntry(uint32_t usage_entry_number, DeviceFiles*, - metrics::CryptoMetrics*) { - usage_table_header_->DeleteEntryForTest(usage_entry_number); + void InvalidateEntry(uint32_t usage_entry_number, bool, DeviceFiles*, + metrics::CryptoMetrics*) { + usage_table_header_->InvalidateEntryForTest(usage_entry_number); } protected: @@ -538,6 +541,7 @@ class UsageTableHeaderTest : public WvCdmTestBase { // Create new mock objects if using MockUsageTableHeader device_files_ = new MockDeviceFiles(); crypto_session_ = new MockCryptoSession(&crypto_metrics_); + MockUsageTableHeader* mock_usage_table_header = new MockUsageTableHeader(); // mock_usage_table_header_ object takes ownership of these objects @@ -561,7 +565,7 @@ class UsageTableHeaderTest : public WvCdmTestBase { .WillOnce(DoAll(SetArgPointee<0>(usage_table_header), SetArgPointee<1>(usage_entry_info_vector), SetArgPointee<2>(false), Return(true))); - EXPECT_CALL(*crypto_session_, LoadUsageTableHeader(usage_table_header)) + EXPECT_CALL(*crypto_session_, LoadUsageTableHeader(_, usage_table_header)) .WillOnce(Return(NO_ERROR)); EXPECT_TRUE(usage_table_header_->Init(security_level, crypto_session_)); } @@ -594,14 +598,17 @@ class UsageTableHeaderInitializationTest }; TEST_P(UsageTableHeaderInitializationTest, CreateUsageTableHeader) { + const SecurityLevel security_level = + (GetParam() == kSecurityLevelL3) ? kLevel3 : kLevelDefault; EXPECT_CALL(*device_files_, RetrieveUsageTableInfo(NotNull(), NotNull(), NotNull())) .WillOnce(DoAll(SetArgPointee<0>(kEmptyUsageTableHeader), SetArgPointee<1>(kEmptyUsageEntryInfoVector), SetArgPointee<2>(false), Return(false))); - EXPECT_CALL(*crypto_session_, CreateUsageTableHeader(NotNull())) + EXPECT_CALL(*crypto_session_, + CreateUsageTableHeader(security_level, NotNull())) .WillOnce( - DoAll(SetArgPointee<0>(kEmptyUsageTableHeader), Return(NO_ERROR))); + DoAll(SetArgPointee<1>(kEmptyUsageTableHeader), Return(NO_ERROR))); EXPECT_CALL(*device_files_, StoreUsageTableInfo(kEmptyUsageTableHeader, kEmptyUsageEntryInfoVector)) .WillOnce(Return(true)); @@ -610,14 +617,17 @@ TEST_P(UsageTableHeaderInitializationTest, CreateUsageTableHeader) { } TEST_P(UsageTableHeaderInitializationTest, Upgrade_UnableToRetrieveLicenses) { + const SecurityLevel security_level = + (GetParam() == kSecurityLevelL3) ? kLevel3 : kLevelDefault; EXPECT_CALL(*device_files_, RetrieveUsageTableInfo(NotNull(), NotNull(), NotNull())) .WillOnce(DoAll(SetArgPointee<0>(kEmptyUsageTableHeader), SetArgPointee<1>(kEmptyUsageEntryInfoVector), SetArgPointee<2>(false), Return(false))); - EXPECT_CALL(*crypto_session_, CreateUsageTableHeader(NotNull())) + EXPECT_CALL(*crypto_session_, + CreateUsageTableHeader(security_level, NotNull())) .WillOnce( - DoAll(SetArgPointee<0>(kEmptyUsageTableHeader), Return(NO_ERROR))); + DoAll(SetArgPointee<1>(kEmptyUsageTableHeader), Return(NO_ERROR))); // TODO: Why not being called? //EXPECT_CALL(*device_files_, DeleteAllLicenses()).WillOnce(Return(true)); EXPECT_CALL(*device_files_, StoreUsageTableInfo(kEmptyUsageTableHeader, @@ -628,14 +638,17 @@ TEST_P(UsageTableHeaderInitializationTest, Upgrade_UnableToRetrieveLicenses) { } TEST_P(UsageTableHeaderInitializationTest, Upgrade_UnableToRetrieveUsageInfo) { + const SecurityLevel security_level = + (GetParam() == kSecurityLevelL3) ? kLevel3 : kLevelDefault; EXPECT_CALL(*device_files_, RetrieveUsageTableInfo(NotNull(), NotNull(), NotNull())) .WillOnce(DoAll(SetArgPointee<0>(kEmptyUsageTableHeader), SetArgPointee<1>(kEmptyUsageEntryInfoVector), SetArgPointee<2>(false), Return(false))); - EXPECT_CALL(*crypto_session_, CreateUsageTableHeader(NotNull())) + EXPECT_CALL(*crypto_session_, + CreateUsageTableHeader(security_level, NotNull())) .WillOnce( - DoAll(SetArgPointee<0>(kEmptyUsageTableHeader), Return(NO_ERROR))); + DoAll(SetArgPointee<1>(kEmptyUsageTableHeader), Return(NO_ERROR))); EXPECT_CALL(*device_files_, StoreUsageTableInfo(kEmptyUsageTableHeader, kEmptyUsageEntryInfoVector)) .WillOnce(Return(true)); @@ -644,48 +657,100 @@ TEST_P(UsageTableHeaderInitializationTest, Upgrade_UnableToRetrieveUsageInfo) { } TEST_P(UsageTableHeaderInitializationTest, UsageTableHeaderExists) { + const SecurityLevel security_level = + (GetParam() == kSecurityLevelL3) ? kLevel3 : kLevelDefault; EXPECT_CALL(*device_files_, RetrieveUsageTableInfo(NotNull(), NotNull(), NotNull())) .WillOnce(DoAll(SetArgPointee<0>(kUsageTableHeader), SetArgPointee<1>(kUsageEntryInfoVector), SetArgPointee<2>(false), Return(true))); - EXPECT_CALL(*crypto_session_, LoadUsageTableHeader(kUsageTableHeader)) + EXPECT_CALL(*crypto_session_, + LoadUsageTableHeader(security_level, kUsageTableHeader)) .WillOnce(Return(NO_ERROR)); EXPECT_TRUE(usage_table_header_->Init(GetParam(), crypto_session_)); } -TEST_P(UsageTableHeaderInitializationTest, 200UsageEntries) { - std::vector usage_entries_200 = k201UsageEntryInfoVector; - usage_entries_200.resize(200); +TEST_P(UsageTableHeaderInitializationTest, UsageEntriesAtCapacity) { + std::vector usage_entries = kOverFullUsageEntryInfoVector; + usage_entries.resize(kDefaultTableCapacity); + const SecurityLevel security_level = + (GetParam() == kSecurityLevelL3) ? kLevel3 : kLevelDefault; EXPECT_CALL(*device_files_, RetrieveUsageTableInfo(NotNull(), NotNull(), NotNull())) .WillOnce(DoAll(SetArgPointee<0>(kUsageTableHeader), - SetArgPointee<1>(usage_entries_200), - SetArgPointee<2>(false), Return(true))); - EXPECT_CALL(*crypto_session_, LoadUsageTableHeader(kUsageTableHeader)) + SetArgPointee<1>(usage_entries), SetArgPointee<2>(false), + Return(true))); + EXPECT_CALL(*crypto_session_, + LoadUsageTableHeader(security_level, kUsageTableHeader)) .WillOnce(Return(NO_ERROR)); EXPECT_TRUE(usage_table_header_->Init(GetParam(), crypto_session_)); } +TEST_P(UsageTableHeaderInitializationTest, UsageEntries_NoCapacity) { + crypto_session_->SetMaximumUsageTableEntries(0); // Unlimited. + std::vector usage_entries = kOverFullUsageEntryInfoVector; + usage_entries.resize(kDefaultTableCapacity); + const SecurityLevel security_level = + (GetParam() == kSecurityLevelL3) ? kLevel3 : kLevelDefault; + EXPECT_CALL(*device_files_, + RetrieveUsageTableInfo(NotNull(), NotNull(), NotNull())) + .WillOnce(DoAll(SetArgPointee<0>(kUsageTableHeader), + SetArgPointee<1>(usage_entries), SetArgPointee<2>(false), + Return(true))); + EXPECT_CALL(*crypto_session_, + LoadUsageTableHeader(security_level, kUsageTableHeader)) + .WillOnce(Return(NO_ERROR)); + + // Expect an attempt to create a new entry. + EXPECT_CALL(*crypto_session_, Open(security_level)) + .WillOnce(Return(NO_ERROR)); + const uint32_t expect_usage_entry_number = kDefaultTableCapacity; + EXPECT_CALL(*crypto_session_, CreateUsageEntry(NotNull())) + .WillOnce( + DoAll(SetArgPointee<0>(expect_usage_entry_number), Return(NO_ERROR))); + EXPECT_CALL(*crypto_session_, UpdateUsageEntry(NotNull(), NotNull())) + .WillOnce( + DoAll(SetArgPointee<0>(kAnotherUsageTableHeader), Return(NO_ERROR))); + EXPECT_CALL(*device_files_, + StoreUsageTableInfo(kAnotherUsageTableHeader, + SizeIs(kDefaultTableCapacity + 1))) + .WillOnce(Return(true)); + + // Delete the entry after. + EXPECT_CALL( + *crypto_session_, + ShrinkUsageTableHeader(security_level, kDefaultTableCapacity, NotNull())) + .WillOnce(DoAll(SetArgPointee<2>(kYetAnotherUsageTableHeader), + Return(NO_ERROR))); + EXPECT_CALL(*device_files_, + StoreUsageTableInfo(kYetAnotherUsageTableHeader, + SizeIs(kDefaultTableCapacity))) + .WillOnce(Return(true)); + + EXPECT_TRUE(usage_table_header_->Init(GetParam(), crypto_session_)); +} + TEST_P(UsageTableHeaderInitializationTest, - 201UsageEntries_AddEntryFails_UsageTableHeaderRecreated) { + UsageEntriesOverCapacity_AddEntryFails_UsageTableHeaderRecreated) { EXPECT_CALL(*device_files_, RetrieveUsageTableInfo(NotNull(), NotNull(), NotNull())) .WillOnce(DoAll(SetArgPointee<0>(kUsageTableHeader), - SetArgPointee<1>(k201UsageEntryInfoVector), + SetArgPointee<1>(kOverFullUsageEntryInfoVector), SetArgPointee<2>(false), Return(true))); const SecurityLevel security_level = (GetParam() == kSecurityLevelL3) ? kLevel3 : kLevelDefault; EXPECT_CALL(*crypto_session_, Open(security_level)).WillOnce(Return(NO_ERROR)); - EXPECT_CALL(*crypto_session_, LoadUsageTableHeader(kUsageTableHeader)) + EXPECT_CALL(*crypto_session_, + LoadUsageTableHeader(security_level, kUsageTableHeader)) .WillOnce(Return(NO_ERROR)); - EXPECT_CALL(*crypto_session_, CreateUsageTableHeader(NotNull())) + EXPECT_CALL(*crypto_session_, + CreateUsageTableHeader(security_level, NotNull())) .WillOnce( - DoAll(SetArgPointee<0>(kEmptyUsageTableHeader), Return(NO_ERROR))); + DoAll(SetArgPointee<1>(kEmptyUsageTableHeader), Return(NO_ERROR))); EXPECT_CALL(*device_files_, DeleteAllLicenses()).WillOnce(Return(true)); EXPECT_CALL(*device_files_, DeleteAllUsageInfo()).WillOnce(Return(true)); EXPECT_CALL(*device_files_, DeleteUsageTableInfo()).WillOnce(Return(true)); @@ -694,7 +759,8 @@ TEST_P(UsageTableHeaderInitializationTest, .WillOnce(Return(true)); // Expectations for AddEntry - const uint32_t expect_usage_entry_number = k201UsageEntryInfoVector.size(); + const uint32_t expect_usage_entry_number = + kOverFullUsageEntryInfoVector.size(); EXPECT_CALL(*crypto_session_, CreateUsageEntry(NotNull())) .WillOnce(DoAll(SetArgPointee<0>(expect_usage_entry_number), Return(CREATE_USAGE_ENTRY_UNKNOWN_ERROR))); @@ -703,90 +769,83 @@ TEST_P(UsageTableHeaderInitializationTest, } TEST_P(UsageTableHeaderInitializationTest, - 201UsageEntries_DeleteEntryFails_UsageTableHeaderRecreated) { - std::vector usage_entries_202 = k201UsageEntryInfoVector; - usage_entries_202.push_back(kDummyUsageEntryInfo); - + UsageEntries_NoCapacity_AddEntryFails_UsageTableHeaderRecreated) { + crypto_session_->SetMaximumUsageTableEntries(0); // Unlimited. + std::vector usage_entries = kOverFullUsageEntryInfoVector; + usage_entries.resize(kDefaultTableCapacity); + const SecurityLevel security_level = + (GetParam() == kSecurityLevelL3) ? kLevel3 : kLevelDefault; EXPECT_CALL(*device_files_, RetrieveUsageTableInfo(NotNull(), NotNull(), NotNull())) .WillOnce(DoAll(SetArgPointee<0>(kUsageTableHeader), - SetArgPointee<1>(k201UsageEntryInfoVector), - SetArgPointee<2>(false), Return(true))); - EXPECT_CALL(*crypto_session_, LoadUsageTableHeader(kUsageTableHeader)) + SetArgPointee<1>(usage_entries), SetArgPointee<2>(false), + Return(true))); + EXPECT_CALL(*crypto_session_, + LoadUsageTableHeader(security_level, kUsageTableHeader)) .WillOnce(Return(NO_ERROR)); - EXPECT_CALL(*crypto_session_, CreateUsageTableHeader(NotNull())) - .WillOnce( - DoAll(SetArgPointee<0>(kEmptyUsageTableHeader), Return(NO_ERROR))); + // Try to create a new entry, and fail. + EXPECT_CALL(*crypto_session_, Open(security_level)) + .WillOnce(Return(NO_ERROR)); + EXPECT_CALL(*crypto_session_, CreateUsageEntry(NotNull())) + .WillOnce(Return(CREATE_USAGE_ENTRY_UNKNOWN_ERROR)); + // Expect clean up. EXPECT_CALL(*device_files_, DeleteAllLicenses()).WillOnce(Return(true)); EXPECT_CALL(*device_files_, DeleteAllUsageInfo()).WillOnce(Return(true)); EXPECT_CALL(*device_files_, DeleteUsageTableInfo()).WillOnce(Return(true)); + // Expect recreation of usage table. + EXPECT_CALL(*crypto_session_, + CreateUsageTableHeader(security_level, NotNull())) + .WillOnce( + DoAll(SetArgPointee<1>(kEmptyUsageTableHeader), Return(NO_ERROR))); EXPECT_CALL(*device_files_, StoreUsageTableInfo(kEmptyUsageTableHeader, kEmptyUsageEntryInfoVector)) .WillOnce(Return(true)); - // Expectations for AddEntry - const uint32_t expect_usage_entry_number = k201UsageEntryInfoVector.size(); - EXPECT_CALL(*crypto_session_, CreateUsageEntry(NotNull())) - .WillOnce(DoAll(SetArgPointee<0>(expect_usage_entry_number), - Return(NO_ERROR))); - EXPECT_CALL(*device_files_, StoreUsageTableInfo(kUsageTableHeader, - usage_entries_202)) - .WillOnce(Return(true)); - - // Expectations for DeleteEntry - const SecurityLevel security_level = - (GetParam() == kSecurityLevelL3) ? kLevel3 : kLevelDefault; - EXPECT_CALL(*crypto_session_, - Open(security_level)) - .Times(2) - .WillRepeatedly(Return(NO_ERROR)); - EXPECT_CALL( - *crypto_session_, - ShrinkUsageTableHeader(usage_entries_202.size() - 1, NotNull())) - .WillOnce(Return(SHRINK_USAGE_TABLER_HEADER_UNKNOWN_ERROR)); - EXPECT_TRUE(usage_table_header_->Init(GetParam(), crypto_session_)); } TEST_P(UsageTableHeaderInitializationTest, - 201UsageEntries_AddDeleteEntrySucceeds) { - std::vector usage_entries_202 = k201UsageEntryInfoVector; - usage_entries_202.push_back(kDummyUsageEntryInfo); + UsageEntriesOverCapacity_AddInvalidateEntrySucceeds) { + // Capacity +2. + std::vector usage_entries = kOverFullUsageEntryInfoVector; + usage_entries.push_back(kDummyUsageEntryInfo); + + const SecurityLevel security_level = + (GetParam() == kSecurityLevelL3) ? kLevel3 : kLevelDefault; EXPECT_CALL(*device_files_, RetrieveUsageTableInfo(NotNull(), NotNull(), NotNull())) .WillOnce(DoAll(SetArgPointee<0>(kUsageTableHeader), - SetArgPointee<1>(k201UsageEntryInfoVector), + SetArgPointee<1>(kOverFullUsageEntryInfoVector), SetArgPointee<2>(false), Return(true))); - EXPECT_CALL(*crypto_session_, LoadUsageTableHeader(kUsageTableHeader)) + EXPECT_CALL(*crypto_session_, + LoadUsageTableHeader(security_level, kUsageTableHeader)) .WillOnce(Return(NO_ERROR)); // Expectations for AddEntry - const uint32_t expect_usage_entry_number = k201UsageEntryInfoVector.size(); + const uint32_t expect_usage_entry_number = + kOverFullUsageEntryInfoVector.size(); EXPECT_CALL(*crypto_session_, CreateUsageEntry(NotNull())) .WillOnce(DoAll(SetArgPointee<0>(expect_usage_entry_number), Return(NO_ERROR))); - EXPECT_CALL(*device_files_, StoreUsageTableInfo(kUsageTableHeader, - usage_entries_202)) + EXPECT_CALL(*crypto_session_, UpdateUsageEntry(NotNull(), NotNull())) + .WillOnce( + DoAll(SetArgPointee<0>(kAnotherUsageTableHeader), Return(NO_ERROR))); + EXPECT_CALL(*device_files_, + StoreUsageTableInfo(kAnotherUsageTableHeader, usage_entries)) .WillOnce(Return(true)); - // Expectations for DeleteEntry - const SecurityLevel security_level = - (GetParam() == kSecurityLevelL3) ? kLevel3 : kLevelDefault; - + // Expectations for InvalidateEntry, assumes no entry other entry is invalid. + EXPECT_CALL(*crypto_session_, Open(security_level)) + .WillOnce(Return(NO_ERROR)); EXPECT_CALL(*crypto_session_, - Open(security_level)) - .Times(2) - .WillRepeatedly(Return(NO_ERROR)); - EXPECT_CALL( - *crypto_session_, - ShrinkUsageTableHeader(usage_entries_202.size() - 1, NotNull())) - .WillOnce( - DoAll(SetArgPointee<1>(kAnotherUsageTableHeader), Return(NO_ERROR))); + ShrinkUsageTableHeader(security_level, usage_entries.size() - 1, + NotNull())) + .WillOnce(DoAll(SetArgPointee<2>(kYetAnotherUsageTableHeader), + Return(NO_ERROR))); EXPECT_CALL(*device_files_, - StoreUsageTableInfo( - kAnotherUsageTableHeader, - SizeIs(k201UsageEntryInfoVector.size()))) + StoreUsageTableInfo(kYetAnotherUsageTableHeader, + SizeIs(kOverFullUsageEntryInfoVector.size()))) .WillOnce(Return(true)); EXPECT_TRUE(usage_table_header_->Init(GetParam(), crypto_session_)); @@ -845,10 +904,12 @@ TEST_F(UsageTableHeaderTest, AddEntry_NextConsecutiveOfflineUsageEntry) { EXPECT_CALL(*crypto_session_, CreateUsageEntry(NotNull())) .WillOnce( DoAll(SetArgPointee<0>(expect_usage_entry_number), Return(NO_ERROR))); - + EXPECT_CALL(*crypto_session_, UpdateUsageEntry(NotNull(), NotNull())) + .WillOnce( + DoAll(SetArgPointee<0>(kAnotherUsageTableHeader), Return(NO_ERROR))); EXPECT_CALL(*device_files_, StoreUsageTableInfo( - kUsageTableHeader, + kAnotherUsageTableHeader, UnorderedElementsAreArray(expect_usage_entry_info_vector))) .WillOnce(Return(true)); @@ -876,10 +937,12 @@ TEST_F(UsageTableHeaderTest, AddEntry_NextConsecutiveSecureStopUsageEntry) { EXPECT_CALL(*crypto_session_, CreateUsageEntry(NotNull())) .WillOnce( DoAll(SetArgPointee<0>(expect_usage_entry_number), Return(NO_ERROR))); - + EXPECT_CALL(*crypto_session_, UpdateUsageEntry(NotNull(), NotNull())) + .WillOnce( + DoAll(SetArgPointee<0>(kAnotherUsageTableHeader), Return(NO_ERROR))); EXPECT_CALL(*device_files_, StoreUsageTableInfo( - kUsageTableHeader, + kAnotherUsageTableHeader, UnorderedElementsAreArray(expect_usage_entry_info_vector))) .WillOnce(Return(true)); @@ -904,11 +967,13 @@ TEST_F(UsageTableHeaderTest, AddEntry_SkipUsageEntries) { EXPECT_CALL(*crypto_session_, CreateUsageEntry(NotNull())) .WillOnce( DoAll(SetArgPointee<0>(expect_usage_entry_number), Return(NO_ERROR))); - + EXPECT_CALL(*crypto_session_, UpdateUsageEntry(NotNull(), NotNull())) + .WillOnce( + DoAll(SetArgPointee<0>(kAnotherUsageTableHeader), Return(NO_ERROR))); EXPECT_CALL( *device_files_, StoreUsageTableInfo( - kUsageTableHeader, + kAnotherUsageTableHeader, UnorderedElementsAre( kUsageEntryInfoOfflineLicense1, kUsageEntryInfoSecureStop1, kUsageEntryInfoStorageTypeUnknown, @@ -938,28 +1003,31 @@ TEST_F(UsageTableHeaderTest, uint32_t usage_entry_number_first_to_be_deleted; // randomly chosen std::vector final_usage_entries; - uint32_t expected_usage_entry_number = k10UsageEntryInfoVector.size() - 1; + const uint32_t expected_usage_entry_number = + k10UsageEntryInfoVector.size() - 1; // Setup expectations EXPECT_CALL(*mock_usage_table_header, - DeleteEntry(_, device_files_, NotNull())) + InvalidateEntry(_, true, device_files_, NotNull())) .WillOnce(DoAll(SaveArg<0>(&usage_entry_number_first_to_be_deleted), - Invoke(this, &UsageTableHeaderTest::DeleteEntry), + Invoke(this, &UsageTableHeaderTest::InvalidateEntry), Return(NO_ERROR))); EXPECT_CALL(*crypto_session_, CreateUsageEntry(NotNull())) - .WillOnce(Return(INSUFFICIENT_CRYPTO_RESOURCES_3)) + .WillOnce(Return(INSUFFICIENT_CRYPTO_RESOURCES)) + .WillOnce(DoAll(SetArgPointee<0>(expected_usage_entry_number), + Return(NO_ERROR))); + EXPECT_CALL(*crypto_session_, UpdateUsageEntry(NotNull(), NotNull())) .WillOnce( - DoAll(SetArgPointee<0>(expected_usage_entry_number), - Return(NO_ERROR))); + DoAll(SetArgPointee<0>(kAnotherUsageTableHeader), Return(NO_ERROR))); - EXPECT_CALL(*device_files_, StoreUsageTableInfo(kUsageTableHeader, _)) + EXPECT_CALL(*device_files_, StoreUsageTableInfo(kAnotherUsageTableHeader, _)) .WillOnce(DoAll(SaveArg<1>(&final_usage_entries), Return(true))); // Now invoke the method under test uint32_t usage_entry_number; EXPECT_EQ(NO_ERROR, - mock_usage_table_header->AddEntry( + mock_usage_table_header->SuperAddEntry( crypto_session_, kUsageEntryInfoOfflineLicense6.storage_type == kStorageLicense, kUsageEntryInfoOfflineLicense6.key_set_id, @@ -983,123 +1051,50 @@ TEST_F(UsageTableHeaderTest, EXPECT_EQ(expected_usage_entries, final_usage_entries); } -TEST_F(UsageTableHeaderTest, - AddEntry_CreateUsageEntryFailsTwice_SucceedsThirdTime) { - // Initialize and setup - MockUsageTableHeader* mock_usage_table_header = SetUpMock(); - std::vector usage_entry_info_vector_at_start = - k10UsageEntryInfoVector; - GenericLruUpgrade(&usage_entry_info_vector_at_start); - Init(kSecurityLevelL1, kUsageTableHeader, usage_entry_info_vector_at_start); - - uint32_t usage_entry_number_first_to_be_deleted; // randomly chosen - uint32_t usage_entry_number_second_to_be_deleted; // randomly chosen - std::vector final_usage_entries; - - uint32_t expected_usage_entry_number = k10UsageEntryInfoVector.size() - 2; - - // Setup expectations - EXPECT_CALL(*mock_usage_table_header, - DeleteEntry(_, device_files_, NotNull())) - .WillOnce(DoAll(SaveArg<0>(&usage_entry_number_first_to_be_deleted), - Invoke(this, &UsageTableHeaderTest::DeleteEntry), - Return(NO_ERROR))) - .WillOnce(DoAll(SaveArg<0>(&usage_entry_number_second_to_be_deleted), - Invoke(this, &UsageTableHeaderTest::DeleteEntry), - Return(NO_ERROR))); - - EXPECT_CALL(*crypto_session_, CreateUsageEntry(NotNull())) - .WillOnce(Return(INSUFFICIENT_CRYPTO_RESOURCES_3)) - .WillOnce(Return(INSUFFICIENT_CRYPTO_RESOURCES_3)) - .WillOnce( - DoAll(SetArgPointee<0>(expected_usage_entry_number), - Return(NO_ERROR))); - - EXPECT_CALL(*device_files_, StoreUsageTableInfo(kUsageTableHeader, _)) - .WillOnce(DoAll(SaveArg<1>(&final_usage_entries), Return(true))); - - // Now invoke the method under test - uint32_t usage_entry_number; - EXPECT_EQ(NO_ERROR, - mock_usage_table_header->AddEntry( - crypto_session_, - kUsageEntryInfoOfflineLicense6.storage_type == kStorageLicense, - kUsageEntryInfoOfflineLicense6.key_set_id, - kUsageEntryInfoOfflineLicense6.usage_info_file_name, - kEmptyString /* license */, &usage_entry_number)); - - // Verify added/deleted usage entry number and entries - EXPECT_EQ(expected_usage_entry_number, usage_entry_number); - - EXPECT_LE(0u, usage_entry_number_first_to_be_deleted); - EXPECT_LE(usage_entry_number_first_to_be_deleted, - usage_entry_info_vector_at_start.size() - 1); - EXPECT_LE(0u, usage_entry_number_second_to_be_deleted); - EXPECT_LE(usage_entry_number_second_to_be_deleted, - usage_entry_info_vector_at_start.size() - 1); - - std::vector expected_usage_entries = - usage_entry_info_vector_at_start; - expected_usage_entries[usage_entry_number_first_to_be_deleted] = - expected_usage_entries[expected_usage_entries.size() - 1]; - expected_usage_entries.resize(expected_usage_entries.size() - 1); - expected_usage_entries[usage_entry_number_second_to_be_deleted] = - expected_usage_entries[expected_usage_entries.size() - 1]; - expected_usage_entries.resize(expected_usage_entries.size() - 1); - expected_usage_entries.push_back(kUsageEntryInfoOfflineLicense6); - - EXPECT_EQ(expected_usage_entries, final_usage_entries); -} - -TEST_F(UsageTableHeaderTest, AddEntry_CreateUsageEntryFailsThrice) { +// The usage table should only delete/invalidate a single entry. +// After which, it should fail. +TEST_F(UsageTableHeaderTest, AddEntry_CreateUsageEntryFailsEveryTime) { // Initialize and setup MockUsageTableHeader* mock_usage_table_header = SetUpMock(); Init(kSecurityLevelL1, kUsageTableHeader, k10UsageEntryInfoVector); - std::vector usage_entry_info_vector_at_start = - k10UsageEntryInfoVector; - - uint32_t usage_entry_number_first_to_be_deleted; // randomly chosen - uint32_t usage_entry_number_second_to_be_deleted; // randomly chosen - uint32_t usage_entry_number_third_to_be_deleted; // randomly chosen - std::vector final_usage_entries; // Setup expectations EXPECT_CALL(*mock_usage_table_header, - DeleteEntry(_, device_files_, NotNull())) - .WillOnce(DoAll(SaveArg<0>(&usage_entry_number_first_to_be_deleted), - Invoke(this, &UsageTableHeaderTest::DeleteEntry), - Return(NO_ERROR))) - .WillOnce(DoAll(SaveArg<0>(&usage_entry_number_second_to_be_deleted), - Invoke(this, &UsageTableHeaderTest::DeleteEntry), - Return(NO_ERROR))) - .WillOnce(DoAll(SaveArg<0>(&usage_entry_number_third_to_be_deleted), - Invoke(this, &UsageTableHeaderTest::DeleteEntry), + InvalidateEntry(_, true, device_files_, NotNull())) + .WillOnce(DoAll(Invoke(this, &UsageTableHeaderTest::InvalidateEntry), Return(NO_ERROR))); EXPECT_CALL(*crypto_session_, CreateUsageEntry(NotNull())) - .Times(4) - .WillRepeatedly(Return(INSUFFICIENT_CRYPTO_RESOURCES_3)); + .Times(2) + .WillRepeatedly(Return(INSUFFICIENT_CRYPTO_RESOURCES)); // Now invoke the method under test uint32_t usage_entry_number; - EXPECT_EQ(INSUFFICIENT_CRYPTO_RESOURCES_3, - mock_usage_table_header->AddEntry( - crypto_session_, - kUsageEntryInfoOfflineLicense6.storage_type == kStorageLicense, + EXPECT_EQ(INSUFFICIENT_CRYPTO_RESOURCES, + mock_usage_table_header->SuperAddEntry( + crypto_session_, true /* persistent */, kUsageEntryInfoOfflineLicense6.key_set_id, kUsageEntryInfoOfflineLicense6.usage_info_file_name, kEmptyString /* license */, &usage_entry_number)); - // Verify deleted usage entry number and entries - EXPECT_LE(0u, usage_entry_number_first_to_be_deleted); - EXPECT_LE(usage_entry_number_first_to_be_deleted, - usage_entry_info_vector_at_start.size() - 1); - EXPECT_LE(0u, usage_entry_number_second_to_be_deleted); - EXPECT_LE(usage_entry_number_second_to_be_deleted, - usage_entry_info_vector_at_start.size() - 1); - EXPECT_LE(0u, usage_entry_number_third_to_be_deleted); - EXPECT_LE(usage_entry_number_third_to_be_deleted, - usage_entry_info_vector_at_start.size() - 1); + // Verify the number of entries deleted. + constexpr uint32_t kExpectedEntriesDeleted = 1; + + const std::vector& final_usage_entries = + mock_usage_table_header->usage_entry_info(); + uint32_t invalid_entries = 0; + for (const CdmUsageEntryInfo& usage_entry_info : final_usage_entries) { + if (usage_entry_info.storage_type == kStorageTypeUnknown) { + ++invalid_entries; + } + } + // Number of entries deleted is equal to the number of entries + // marked as invalid plus the number of fewer entries in the table + // at the end of the call. + const uint32_t entries_deleted = + invalid_entries + + (k10UsageEntryInfoVector.size() - final_usage_entries.size()); + EXPECT_EQ(kExpectedEntriesDeleted, entries_deleted); } TEST_F(UsageTableHeaderTest, LoadEntry_InvalidEntryNumber) { @@ -1163,348 +1158,272 @@ TEST_F(UsageTableHeaderTest, UpdateEntry) { usage_table_header_->UpdateEntry(0, crypto_session_, &usage_entry)); } -TEST_F(UsageTableHeaderTest, - LoadEntry_LoadUsageEntryFailsOnce_SucceedsSecondTime) { - // Initialize and setup - MockUsageTableHeader* mock_usage_table_header = SetUpMock(); - Init(kSecurityLevelL1, kUsageTableHeader, k10UsageEntryInfoVector); - - // We try to load a usage entry from the first 9 entries, since DeleteEntry - // can't delete an entry if the last one is in use. - - uint32_t usage_entry_number_to_load = - CdmRandom::RandomInRange(k10UsageEntryInfoVector.size() - 2); - CdmUsageEntry usage_entry_to_load = kUsageEntry; - - // Setup expectations - EXPECT_CALL(*mock_usage_table_header, - DeleteEntry(_, device_files_, NotNull())) - .Times(1) - .WillRepeatedly( - DoAll(Invoke(this, &UsageTableHeaderTest::DeleteEntry), - Return(NO_ERROR))); - - EXPECT_CALL(*crypto_session_, - LoadUsageEntry(usage_entry_number_to_load, usage_entry_to_load)) - .WillOnce(Return(INSUFFICIENT_CRYPTO_RESOURCES_3)) - .WillOnce(Return(NO_ERROR)); - - // Now invoke the method under test - EXPECT_EQ(NO_ERROR, - mock_usage_table_header->LoadEntry( - crypto_session_, - usage_entry_to_load, - usage_entry_number_to_load)); -} - -TEST_F(UsageTableHeaderTest, - LoadEntry_LoadUsageEntryFailsTwice_SucceedsThirdTime) { - // Initialize and setup - MockUsageTableHeader* mock_usage_table_header = SetUpMock(); - Init(kSecurityLevelL1, kUsageTableHeader, k10UsageEntryInfoVector); - - // We try to load a usage entry from the first 8 entries, since DeleteEntry - // can't delete an entry if the last one is in use. - uint32_t usage_entry_number_to_load = - CdmRandom::RandomInRange(k10UsageEntryInfoVector.size() - 3); - CdmUsageEntry usage_entry_to_load = kUsageEntry; - - // Setup expectations - EXPECT_CALL(*mock_usage_table_header, - DeleteEntry(_, device_files_, NotNull())) - .Times(2) - .WillRepeatedly( - DoAll(Invoke(this, &UsageTableHeaderTest::DeleteEntry), - Return(NO_ERROR))); - - EXPECT_CALL(*crypto_session_, - LoadUsageEntry(usage_entry_number_to_load, usage_entry_to_load)) - .WillOnce(Return(INSUFFICIENT_CRYPTO_RESOURCES_3)) - .WillOnce(Return(INSUFFICIENT_CRYPTO_RESOURCES_3)) - .WillOnce(Return(NO_ERROR)); - - // Now invoke the method under test - EXPECT_EQ(NO_ERROR, - mock_usage_table_header->LoadEntry( - crypto_session_, - usage_entry_to_load, - usage_entry_number_to_load)); -} - -TEST_F(UsageTableHeaderTest, LoadEntry_LoadUsageEntryFailsThrice) { - // Initialize and setup - MockUsageTableHeader* mock_usage_table_header = SetUpMock(); - Init(kSecurityLevelL1, kUsageTableHeader, k10UsageEntryInfoVector); - - // We try to load a usage entry from the first 7 entries, since DeleteEntry - // can't delete an entry if the last one is in use. - uint32_t usage_entry_number_to_load = - CdmRandom::RandomInRange(k10UsageEntryInfoVector.size() - 4); - CdmUsageEntry usage_entry_to_load = kUsageEntry; - - // Setup expectations - EXPECT_CALL(*mock_usage_table_header, - DeleteEntry(_, device_files_, NotNull())) - .Times(3) - .WillRepeatedly( - DoAll(Invoke(this, &UsageTableHeaderTest::DeleteEntry), - Return(NO_ERROR))); - - EXPECT_CALL(*crypto_session_, - LoadUsageEntry(usage_entry_number_to_load, usage_entry_to_load)) - .Times(4) - .WillRepeatedly(Return(INSUFFICIENT_CRYPTO_RESOURCES_3)); - - // Now invoke the method under test - EXPECT_EQ(INSUFFICIENT_CRYPTO_RESOURCES_3, - mock_usage_table_header->LoadEntry( - crypto_session_, - usage_entry_to_load, - usage_entry_number_to_load)); -} - -TEST_F(UsageTableHeaderTest, DeleteEntry_InvalidUsageEntryNumber) { +TEST_F(UsageTableHeaderTest, InvalidateEntry_InvalidUsageEntryNumber) { Init(kSecurityLevelL1, kUsageTableHeader, kUsageEntryInfoVector); uint32_t usage_entry_number = kUsageEntryInfoVector.size(); metrics::CryptoMetrics metrics; - EXPECT_NE(NO_ERROR, usage_table_header_->DeleteEntry( - usage_entry_number, device_files_, &metrics)); + EXPECT_NE(NO_ERROR, usage_table_header_->InvalidateEntry( + usage_entry_number, true, device_files_, &metrics)); } // Initial Test state: -// 1. Entry to be delete is the last entry and is an Offline license. +// 1. Entry to be deleted is the last entry and is an Offline license. // When attempting to delete the entry a crypto session error // will occur. // // Attempting to delete the entry in (1) will result in: -// a. The usage entry requested to be deleted will not be deleted. +// a. The entry will be marked as kStorageTypeUnknown. +// b. The usage table will be requested to shrink both the last entry +// and the unknown entry before it. +// c. OEMCrypto error will cause the internal entries to remain the +// same. +// d. InvalidateEntry() will return NO_ERROR as the storage type is +// changed. // // Storage type Usage entries // at start at end // ============= ======== ====== // Offline License 1 0 0 // Secure Stop 1 1 1 -// Storage Type unknown 2 2 -// Offline License 2 3 3 +// Storage Type Unknown 2 2 +// Offline License 2 3 3 (Storage Type Unknown) // // # of usage entries 4 4 -TEST_F(UsageTableHeaderTest, DeleteEntry_CryptoSessionError) { - std::vector usage_entry_info_vector; - const CdmUsageEntryInfo usage_entry_info_array[] = { +TEST_F(UsageTableHeaderTest, InvalidateEntry_CryptoSessionError) { + const std::vector usage_entry_info_vector = { kUsageEntryInfoOfflineLicense1, kUsageEntryInfoSecureStop1, kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoOfflineLicense2}; - ToVector(usage_entry_info_vector, usage_entry_info_array, - sizeof(usage_entry_info_array)); Init(kSecurityLevelL1, kUsageTableHeader, usage_entry_info_vector); - uint32_t usage_entry_number_to_be_deleted = - usage_entry_info_vector.size() - 1; // kUsageEntryInfoOfflineLicense2 + const uint32_t usage_entry_number_to_be_deleted = + 3; // kUsageEntryInfoOfflineLicense2 metrics::CryptoMetrics metrics; - EXPECT_CALL(*crypto_session_, Open(kLevelDefault)).WillOnce(Return(NO_ERROR)); - EXPECT_CALL( - *crypto_session_, - ShrinkUsageTableHeader(usage_entry_info_vector.size() - 1, NotNull())) - .WillOnce(Return(SHRINK_USAGE_TABLER_HEADER_UNKNOWN_ERROR)); + EXPECT_CALL(*crypto_session_, + ShrinkUsageTableHeader(kLevelDefault, 2, NotNull())) + .WillOnce(Return(SHRINK_USAGE_TABLE_HEADER_UNKNOWN_ERROR)); - EXPECT_NE(NO_ERROR, - usage_table_header_->DeleteEntry(usage_entry_number_to_be_deleted, - device_files_, &metrics)); + // Regardless, the usage table should be updated to reflect the changes + // to the usage entry marked as storage type unknown. + EXPECT_CALL( + *device_files_, + StoreUsageTableInfo(kUsageTableHeader, + ElementsAre(kUsageEntryInfoOfflineLicense1, + kUsageEntryInfoSecureStop1, + kUsageEntryInfoStorageTypeUnknown, + kUsageEntryInfoStorageTypeUnknown))) + .WillOnce(Return(true)); + + EXPECT_EQ(NO_ERROR, usage_table_header_->InvalidateEntry( + usage_entry_number_to_be_deleted, true, device_files_, + &metrics)); + // Check that the list is unchanged. + constexpr size_t expected_size = 4; + EXPECT_EQ(expected_size, usage_table_header_->usage_entry_info().size()); } // Initial Test state: -// 1. Entry to be delete is the last entry and is an Offline license. +// 1. Entry to be deleted is the last entry and is an offline license. // // Attempting to delete the entry in (1) will result in: -// a. The usage entry requested to be deleted will be deleted. +// a. The entry will be marked as kStorageTypeUnknown. +// b. Usage table will be resized to remove the last two entries. +// c. Updated table will be saved. +// d. InvalidateEntry() will return NO_ERROR. // // Storage type Usage entries // at start at end // ============= ======== ====== // Offline License 1 0 0 // Secure Stop 1 1 1 -// Storage Type unknown 2 2 +// Storage Type Unknown 2 Deleted // Offline License 2 3 Deleted // -// # of usage entries 4 3 -TEST_F(UsageTableHeaderTest, DeleteEntry_LastOfflineEntry) { - std::vector usage_entry_info_vector; - const CdmUsageEntryInfo usage_entry_info_array[] = { +// # of usage entries 4 2 +TEST_F(UsageTableHeaderTest, InvalidateEntry_LastEntry_OfflineEntry) { + const std::vector usage_entry_info_vector = { kUsageEntryInfoOfflineLicense1, kUsageEntryInfoSecureStop1, kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoOfflineLicense2}; - ToVector(usage_entry_info_vector, usage_entry_info_array, - sizeof(usage_entry_info_array)); Init(kSecurityLevelL1, kUsageTableHeader, usage_entry_info_vector); - uint32_t usage_entry_number_to_be_deleted = - usage_entry_info_vector.size() - 1; // kUsageEntryInfoOfflineLicense2 + const uint32_t usage_entry_number_to_be_deleted = + 3; // kUsageEntryInfoOfflineLicense2 metrics::CryptoMetrics metrics; - EXPECT_CALL(*crypto_session_, Open(kLevelDefault)).WillOnce(Return(NO_ERROR)); - EXPECT_CALL( - *crypto_session_, - ShrinkUsageTableHeader(usage_entry_info_vector.size() - 1, NotNull())) + // Expectations for call to shrink. + EXPECT_CALL(*crypto_session_, + ShrinkUsageTableHeader(kLevelDefault, 2, NotNull())) .WillOnce( - DoAll(SetArgPointee<1>(kAnotherUsageTableHeader), Return(NO_ERROR))); + DoAll(SetArgPointee<2>(kAnotherUsageTableHeader), Return(NO_ERROR))); EXPECT_CALL(*device_files_, - StoreUsageTableInfo( - kAnotherUsageTableHeader, - UnorderedElementsAre(kUsageEntryInfoOfflineLicense1, - kUsageEntryInfoSecureStop1, - kUsageEntryInfoStorageTypeUnknown))) + StoreUsageTableInfo(kAnotherUsageTableHeader, + ElementsAre(kUsageEntryInfoOfflineLicense1, + kUsageEntryInfoSecureStop1))) .WillOnce(Return(true)); - EXPECT_EQ(NO_ERROR, - usage_table_header_->DeleteEntry(usage_entry_number_to_be_deleted, - device_files_, &metrics)); + EXPECT_EQ(NO_ERROR, usage_table_header_->InvalidateEntry( + usage_entry_number_to_be_deleted, true, device_files_, + &metrics)); + // Check the end state of the usage table. + constexpr size_t expected_size = 2; + EXPECT_EQ(expected_size, usage_table_header_->usage_entry_info().size()); } // Initial Test state: -// 1. Entry to be delete is the last entry and is a secure stop. +// 1. Entry to be deleted is the last entry and is a secure stop. // // Attempting to delete the entry in (1) will result in: -// a. The usage entry requested to be deleted will be deleted. +// a. The entry will be marked as kStorageTypeUnknown. +// b. Usage table will be resized to remove the last two entries. +// c. Updated table will be saved. +// d. InvalidateEntry() will return NO_ERROR. // // Storage type Usage entries // at start at end // ============= ======== ====== // Offline License 1 0 0 // Secure Stop 1 1 1 -// Storage Type unknown 2 2 +// Storage Type Unknown 2 Deleted // Secure Stop 2 3 Deleted // -// # of usage entries 4 3 -TEST_F(UsageTableHeaderTest, DeleteEntry_LastSecureStopEntry) { - std::vector usage_entry_info_vector; - const CdmUsageEntryInfo usage_entry_info_array[] = { +// # of usage entries 4 2 +TEST_F(UsageTableHeaderTest, InvalidateEntry_LastEntry_SecureStopEntry) { + const std::vector usage_entry_info_vector = { kUsageEntryInfoOfflineLicense1, kUsageEntryInfoSecureStop1, kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoSecureStop2}; - ToVector(usage_entry_info_vector, usage_entry_info_array, - sizeof(usage_entry_info_array)); Init(kSecurityLevelL1, kUsageTableHeader, usage_entry_info_vector); - uint32_t usage_entry_number_to_be_deleted = - usage_entry_info_vector.size() - 1; // kUsageEntryInfoSecureStop2 + const uint32_t usage_entry_number_to_be_deleted = + 3; // kUsageEntryInfoSecureStop2 metrics::CryptoMetrics metrics; - EXPECT_CALL(*crypto_session_, Open(kLevelDefault)).WillOnce(Return(NO_ERROR)); - EXPECT_CALL( - *crypto_session_, - ShrinkUsageTableHeader(usage_entry_info_vector.size() - 1, NotNull())) + // Expectation when shrinking table. + EXPECT_CALL(*crypto_session_, + ShrinkUsageTableHeader(kLevelDefault, 2, NotNull())) .WillOnce( - DoAll(SetArgPointee<1>(kAnotherUsageTableHeader), Return(NO_ERROR))); + DoAll(SetArgPointee<2>(kAnotherUsageTableHeader), Return(NO_ERROR))); EXPECT_CALL(*device_files_, - StoreUsageTableInfo( - kAnotherUsageTableHeader, - UnorderedElementsAre(kUsageEntryInfoOfflineLicense1, - kUsageEntryInfoSecureStop1, - kUsageEntryInfoStorageTypeUnknown))) + StoreUsageTableInfo(kAnotherUsageTableHeader, + ElementsAre(kUsageEntryInfoOfflineLicense1, + kUsageEntryInfoSecureStop1))) .WillOnce(Return(true)); - EXPECT_EQ(NO_ERROR, - usage_table_header_->DeleteEntry(usage_entry_number_to_be_deleted, - device_files_, &metrics)); + EXPECT_EQ(NO_ERROR, usage_table_header_->InvalidateEntry( + usage_entry_number_to_be_deleted, true, device_files_, + &metrics)); + // Check the end state of the usage table. + constexpr size_t expected_size = 2; + EXPECT_EQ(expected_size, usage_table_header_->usage_entry_info().size()); } // Initial Test state: // 1. Last few entries are offline licenses, but have license files // missing from persistent storage. -// 2. Usage entry to be deleted preceeds those in (1). +// 2. Usage entry to be deleted precedes those in (1). // // Attempting to delete the entry in (2) will result in: -// a. Offline entries in (1) will be deleted. -// b. The usage entry requested to be deleted will be deleted. +// a. The entry will be marked as kStorageTypeUnknown. +// b. While defragging, the last two entries will be selected to +// move. +// c. Getting the usage entry for the selected entries will fail and +// result in them being set as kStorageTypeUnknown. +// d. No entries will be moved due to (c). +// e. Usage table will be resized to have only one entry. +// f. Updated table will be saved. +// g. InvalidateEntry() will return NO_ERROR. // // Storage type Usage entries // at start at end // ============= ======== ====== // Secure Stop 1 0 0 -// Storage Type unknown 1 1 +// Storage Type Unknown 1 Deleted // Offline License 1 2 Deleted -// Offline License 2 3 Deleted -// Offline License 3 4 Deleted +// Offline License 2 3 Deleted (because missing) +// Offline License 3 4 Deleted (because missing) // -// # of usage entries 5 2 +// # of usage entries 5 1 TEST_F(UsageTableHeaderTest, - DeleteEntry_LastOfflineEntriesHaveMissingLicenses) { - std::vector usage_entry_info_vector = { + InvalidateEntry_LastOfflineEntriesHaveMissingLicenses) { + const std::vector usage_entry_info_vector = { kUsageEntryInfoSecureStop1, kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoOfflineLicense1, kUsageEntryInfoOfflineLicense2, kUsageEntryInfoOfflineLicense3}; Init(kSecurityLevelL1, kUsageTableHeader, usage_entry_info_vector); - uint32_t usage_entry_number_to_be_deleted = - usage_entry_info_vector.size() - 3; // kUsageEntryInfoOfflineLicense1 + const uint32_t usage_entry_number_to_be_deleted = + 2; // kUsageEntryInfoOfflineLicense1 metrics::CryptoMetrics metrics; - EXPECT_CALL(*crypto_session_, Open(kLevelDefault)).WillOnce(Return(NO_ERROR)); - EXPECT_CALL( - *crypto_session_, - ShrinkUsageTableHeader(usage_entry_number_to_be_deleted, NotNull())) - .WillOnce( - DoAll(SetArgPointee<1>(kAnotherUsageTableHeader), Return(NO_ERROR))); - - EXPECT_CALL(*device_files_, - StoreUsageTableInfo( - kAnotherUsageTableHeader, - UnorderedElementsAre(kUsageEntryInfoSecureStop1, - kUsageEntryInfoStorageTypeUnknown))) - .WillOnce(Return(true)); + // Offline license 2 and 3 cannot be retrieved. EXPECT_CALL(*device_files_, RetrieveLicense(kUsageEntryInfoOfflineLicense2.key_set_id, - NotNull(), NotNull())); + NotNull(), NotNull())) + .WillOnce(Return(false)); EXPECT_CALL(*device_files_, RetrieveLicense(kUsageEntryInfoOfflineLicense3.key_set_id, - NotNull(), NotNull())); + NotNull(), NotNull())) + .WillOnce(Return(false)); - EXPECT_EQ(NO_ERROR, - usage_table_header_->DeleteEntry(usage_entry_number_to_be_deleted, - device_files_, &metrics)); + // Shrink to contain only the one valid entry. + EXPECT_CALL(*crypto_session_, + ShrinkUsageTableHeader(kLevelDefault, 1, NotNull())) + .WillOnce( + DoAll(SetArgPointee<2>(kAnotherUsageTableHeader), Return(NO_ERROR))); + + EXPECT_CALL(*device_files_, + StoreUsageTableInfo(kAnotherUsageTableHeader, + ElementsAre(kUsageEntryInfoSecureStop1))) + .WillOnce(Return(true)); + + EXPECT_EQ(NO_ERROR, usage_table_header_->InvalidateEntry( + usage_entry_number_to_be_deleted, true, device_files_, + &metrics)); + // Check the end state of the usage table. + constexpr size_t expected_size = 1; + EXPECT_EQ(expected_size, usage_table_header_->usage_entry_info().size()); } // Initial Test state: // 1. Last few entries are secure stops, but have entries // missing from usage info file in persistent storage. -// 2. Usage entry to be deleted preceeds those in (1). +// 2. Usage entry to be deleted precedes those in (1). // // Attempting to delete the entry in (2) will result in: -// a. Secure stops in (1) will be deleted. -// b. The usage entry requested to be deleted will be deleted. +// a. The entry will be marked as kStorageTypeUnknown. +// b. While defragging, the last two entries will be selected to +// move. +// c. Getting the usage entry for the selected entries will fail and +// result in them being set as kStorageTypeUnknown. +// d. No entries will be moved due to (c). +// e. Usage table will be resized to have only one entry. +// f. Updated table will be saved. +// g. InvalidateEntry() will return NO_ERROR. // // Storage type Usage entries // at start at end // ============= ======== ====== // Offline License 1 0 0 -// Storage Type unknown 1 1 +// Storage Type Unknown 1 Deleted // Secure stop 1 2 Deleted -// Secure stop 2 3 Deleted -// Secure stop 3 4 Deleted +// Secure stop 2 3 Deleted (because missing) +// Secure stop 3 4 Deleted (because missing) // -// # of usage entries 5 2 -TEST_F(UsageTableHeaderTest, DeleteEntry_LastSecureStopEntriesAreMissing) { - std::vector usage_entry_info_vector; - const CdmUsageEntryInfo usage_entry_info_array[] = { +// # of usage entries 5 1 +TEST_F(UsageTableHeaderTest, InvalidateEntry_LastSecureStopEntriesAreMissing) { + const std::vector usage_entry_info_vector = { kUsageEntryInfoOfflineLicense1, kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoSecureStop1, kUsageEntryInfoSecureStop2, kUsageEntryInfoSecureStop3}; - ToVector(usage_entry_info_vector, usage_entry_info_array, - sizeof(usage_entry_info_array)); Init(kSecurityLevelL1, kUsageTableHeader, usage_entry_info_vector); - uint32_t usage_entry_number_to_be_deleted = - usage_entry_info_vector.size() - 3; // kUsageEntryInfoSecureStop1 + const uint32_t usage_entry_number_to_be_deleted = + 2; // kUsageEntryInfoSecureStop1 metrics::CryptoMetrics metrics; - EXPECT_CALL(*crypto_session_, Open(kLevelDefault)).WillOnce(Return(NO_ERROR)); - EXPECT_CALL( - *crypto_session_, - ShrinkUsageTableHeader(usage_entry_number_to_be_deleted, NotNull())) - .WillOnce( - DoAll(SetArgPointee<1>(kAnotherUsageTableHeader), Return(NO_ERROR))); - + // Streaming license 2 and 3 cannot be retrieved. EXPECT_CALL(*device_files_, RetrieveUsageInfoByKeySetId( kUsageEntryInfoSecureStop2.usage_info_file_name, @@ -1518,51 +1437,66 @@ TEST_F(UsageTableHeaderTest, DeleteEntry_LastSecureStopEntriesAreMissing) { NotNull(), NotNull(), NotNull())) .WillOnce(Return(false)); + // Shrink to contain only the one valid entry. + EXPECT_CALL(*crypto_session_, + ShrinkUsageTableHeader(kLevelDefault, 1, NotNull())) + .WillOnce( + DoAll(SetArgPointee<2>(kAnotherUsageTableHeader), Return(NO_ERROR))); + EXPECT_CALL(*device_files_, - StoreUsageTableInfo( - kAnotherUsageTableHeader, - UnorderedElementsAre(kUsageEntryInfoOfflineLicense1, - kUsageEntryInfoStorageTypeUnknown))) + StoreUsageTableInfo(kAnotherUsageTableHeader, + ElementsAre(kUsageEntryInfoOfflineLicense1))) .WillOnce(Return(true)); - EXPECT_EQ(NO_ERROR, - usage_table_header_->DeleteEntry(usage_entry_number_to_be_deleted, - device_files_, &metrics)); + EXPECT_EQ(NO_ERROR, usage_table_header_->InvalidateEntry( + usage_entry_number_to_be_deleted, true, device_files_, + &metrics)); + // Check the end state of the usage table. + constexpr size_t expected_size = 1; + EXPECT_EQ(expected_size, usage_table_header_->usage_entry_info().size()); } // Initial Test state: // 1. Last few entries are offline licenses, but have incorrect usage // entry number stored in persistent file store. -// 2. Usage entry to be deleted preceeds those in (1). +// 2. Usage entry to be deleted precedes those in (1). // // Attempting to delete the entry in (2) will result in: -// a. Offline entries in (1) will be deleted. -// b. The usage entry requested to be deleted will be deleted. +// a. The entry will be marked as kStorageTypeUnknown. +// b. While defragging, the last two entries will be selected to +// move. +// c. Getting the usage entry for the selected entries will fail due +// to a mismatch in usage entry number and result in them being set +// as kStorageTypeUnknown. +// d. No entries will be moved due to (c). +// e. Usage table will be resized to have only one entry. +// f. Updated table will be saved. +// g. InvalidateEntry() will return NO_ERROR. // // Storage type Usage entries // at start at end // ============= ======== ====== // Secure Stop 1 0 0 -// Storage Type unknown 1 1 +// Storage Type Unknown 1 Deleted // Offline License 1 2 Deleted -// Offline License 2 3 Deleted -// Offline License 3 4 Deleted +// Offline License 2 3 Deleted (because incorrect #) +// Offline License 3 4 Deleted (because incorrect #) // -// # of usage entries 5 2 +// # of usage entries 5 1 TEST_F(UsageTableHeaderTest, - DeleteEntry_LastOfflineEntriesHaveIncorrectUsageEntryNumber) { - std::vector usage_entry_info_vector = { + InvalidateEntry_LastOfflineEntriesHaveIncorrectUsageEntryNumber) { + const std::vector usage_entry_info_vector = { kUsageEntryInfoSecureStop1, kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoOfflineLicense1, kUsageEntryInfoOfflineLicense2, kUsageEntryInfoOfflineLicense3}; Init(kSecurityLevelL1, kUsageTableHeader, usage_entry_info_vector); const uint32_t usage_entry_number_to_be_deleted = - usage_entry_info_vector.size() - 3; // kUsageEntryInfoOfflineLicense1 + 2; // kUsageEntryInfoOfflineLicense1 metrics::CryptoMetrics metrics; - DeviceFiles::ResponseType sub_error_code; + // Set offline license file data with mismatched usage entry numbers. const DeviceFiles::CdmLicenseData offline_license_3_data{ - usage_entry_info_vector[usage_entry_info_vector.size() - 1].key_set_id, + kUsageEntryInfoOfflineLicense3.key_set_id, kActiveLicenseState, kPsshData, kKeyRequest, @@ -1575,13 +1509,14 @@ TEST_F(UsageTableHeaderTest, kGracePeriodEndTime, kEmptyAppParameters, kUsageEntry, - static_cast(usage_entry_info_vector.size() - 2)}; - EXPECT_TRUE( - device_files_->StoreLicense(offline_license_3_data, &sub_error_code)); - EXPECT_EQ(DeviceFiles::kNoError, sub_error_code); + static_cast(3) /* Mismatch */}; + EXPECT_CALL(*device_files_, + RetrieveLicense(kUsageEntryInfoOfflineLicense3.key_set_id, + NotNull(), NotNull())) + .WillOnce(DoAll(SetArgPointee<1>(offline_license_3_data), Return(true))); const DeviceFiles::CdmLicenseData offline_license_2_data{ - usage_entry_info_vector[usage_entry_info_vector.size() - 2].key_set_id, + kUsageEntryInfoOfflineLicense2.key_set_id, kActiveLicenseState, kPsshData, kKeyRequest, @@ -1594,86 +1529,80 @@ TEST_F(UsageTableHeaderTest, kGracePeriodEndTime, kEmptyAppParameters, kUsageEntry, - static_cast(usage_entry_info_vector.size() - 3)}; - EXPECT_TRUE( - device_files_->StoreLicense(offline_license_2_data, &sub_error_code)); - EXPECT_EQ(DeviceFiles::kNoError, sub_error_code); - - EXPECT_CALL(*crypto_session_, Open(kLevelDefault)).WillOnce(Return(NO_ERROR)); - EXPECT_CALL( - *crypto_session_, - ShrinkUsageTableHeader(usage_entry_number_to_be_deleted, NotNull())) - .WillOnce( - DoAll(SetArgPointee<1>(kAnotherUsageTableHeader), Return(NO_ERROR))); + static_cast(2) /* Mismatch */}; EXPECT_CALL(*device_files_, - StoreUsageTableInfo( - kAnotherUsageTableHeader, - UnorderedElementsAre(kUsageEntryInfoSecureStop1, - kUsageEntryInfoStorageTypeUnknown))) - .WillOnce(Return(true)); - EXPECT_CALL(*device_files_, RetrieveLicense(offline_license_3_data.key_set_id, - NotNull(), NotNull())); - EXPECT_CALL(*device_files_, RetrieveLicense(offline_license_2_data.key_set_id, - NotNull(), NotNull())); + RetrieveLicense(kUsageEntryInfoOfflineLicense2.key_set_id, + NotNull(), NotNull())) + .WillOnce(DoAll(SetArgPointee<1>(offline_license_2_data), Return(true))); - EXPECT_EQ(NO_ERROR, - usage_table_header_->DeleteEntry(usage_entry_number_to_be_deleted, - device_files_, &metrics)); + // Shrink to contain only the one valid entry. + EXPECT_CALL(*crypto_session_, + ShrinkUsageTableHeader(kLevelDefault, 1, NotNull())) + .WillOnce( + DoAll(SetArgPointee<2>(kAnotherUsageTableHeader), Return(NO_ERROR))); + EXPECT_CALL(*device_files_, + StoreUsageTableInfo(kAnotherUsageTableHeader, + ElementsAre(kUsageEntryInfoSecureStop1))) + .WillOnce(Return(true)); + + EXPECT_EQ(NO_ERROR, usage_table_header_->InvalidateEntry( + usage_entry_number_to_be_deleted, true, device_files_, + &metrics)); + // Check the end state of the usage table. + constexpr size_t expected_size = 1; + EXPECT_EQ(expected_size, usage_table_header_->usage_entry_info().size()); } // Initial Test state: // 1. Last few entries are secure stops, but have incorrect usage // entry number stored in persistent file store. -// 2. Usage entry to be deleted preceeds those in (1). +// 2. Usage entry to be deleted precedes those in (1). // // Attempting to delete the entry in (2) will result in: -// a. Secure stops entries in (1) will be deleted. -// b. The usage entry requested to be deleted will be deleted. +// a. The entry will be marked as kStorageTypeUnknown. +// b. While defragging, the last two entries will be selected to +// move. +// c. Getting the usage entry for the selected entries will fail due +// to a mismatch in usage entry number and result in them being set +// as kStorageTypeUnknown. +// d. No entries will be moved due to (c). +// e. Usage table will be resized to have only one entry. +// f. Updated table will be saved. +// g. InvalidateEntry() will return NO_ERROR. // // Storage type Usage entries // at start at end // ============= ======== ====== // Offline License 1 0 0 -// Storage Type unknown 1 1 +// Storage Type Unknown 1 Deleted // Secure stop 1 2 Deleted -// Secure stop 2 3 Deleted -// Secure stop 3 4 Deleted +// Secure stop 2 3 Deleted (because incorrect #) +// Secure stop 3 4 Deleted (because incorrect #) // -// # of usage entries 5 2 +// # of usage entries 5 1 TEST_F(UsageTableHeaderTest, - DeleteEntry_LastSecureStopEntriesHaveIncorrectUsageEntryNumber) { - std::vector usage_entry_info_vector; - const CdmUsageEntryInfo usage_entry_info_array[] = { + InvalidateEntry_LastSecureStopEntriesHaveIncorrectUsageEntryNumber) { + const std::vector usage_entry_info_vector = { kUsageEntryInfoOfflineLicense1, kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoSecureStop1, kUsageEntryInfoSecureStop2, kUsageEntryInfoSecureStop3}; - ToVector(usage_entry_info_vector, usage_entry_info_array, - sizeof(usage_entry_info_array)); Init(kSecurityLevelL1, kUsageTableHeader, usage_entry_info_vector); - uint32_t usage_entry_number_to_be_deleted = - usage_entry_info_vector.size() - 3; // kUsageEntryInfoSecureStop1 - uint32_t usage_entry_number_after_deleted_entry = - usage_entry_number_to_be_deleted + 1; + const uint32_t usage_entry_number_to_be_deleted = + 2; // kUsageEntryInfoSecureStop1 metrics::CryptoMetrics metrics; - EXPECT_CALL(*crypto_session_, Open(kLevelDefault)).WillOnce(Return(NO_ERROR)); - EXPECT_CALL( - *crypto_session_, - ShrinkUsageTableHeader(usage_entry_number_to_be_deleted, NotNull())) - .WillOnce( - DoAll(SetArgPointee<1>(kAnotherUsageTableHeader), Return(NO_ERROR))); - + // Set streaming license file data with mismatched usage entry numbers. EXPECT_CALL(*device_files_, RetrieveUsageInfoByKeySetId( kUsageEntryInfoSecureStop2.usage_info_file_name, kUsageEntryInfoSecureStop2.key_set_id, NotNull(), NotNull(), NotNull(), NotNull(), NotNull())) - .WillOnce(DoAll( - SetArgPointee<2>(kProviderSessionToken), - SetArgPointee<3>(kKeyRequest), SetArgPointee<4>(kKeyResponse), - SetArgPointee<5>(kUsageEntry), - SetArgPointee<6>(usage_entry_number_to_be_deleted), Return(true))); + .WillOnce(DoAll(SetArgPointee<2>(kProviderSessionToken), + SetArgPointee<3>(kKeyRequest), + SetArgPointee<4>(kKeyResponse), + SetArgPointee<5>(kUsageEntry), + SetArgPointee<6>(2) /* Mismatch */, Return(true))); EXPECT_CALL(*device_files_, RetrieveUsageInfoByKeySetId( @@ -1684,73 +1613,122 @@ TEST_F(UsageTableHeaderTest, SetArgPointee<3>(kKeyRequest), SetArgPointee<4>(kKeyResponse), SetArgPointee<5>(kUsageEntry), - SetArgPointee<6>(usage_entry_number_after_deleted_entry), - Return(true))); + SetArgPointee<6>(3) /* Mismatch */, Return(true))); + + // Shrink to contain only the one valid entry. + EXPECT_CALL(*crypto_session_, + ShrinkUsageTableHeader(kLevelDefault, 1, NotNull())) + .WillOnce( + DoAll(SetArgPointee<2>(kAnotherUsageTableHeader), Return(NO_ERROR))); EXPECT_CALL(*device_files_, - StoreUsageTableInfo( - kAnotherUsageTableHeader, - UnorderedElementsAre(kUsageEntryInfoOfflineLicense1, - kUsageEntryInfoStorageTypeUnknown))) + StoreUsageTableInfo(kAnotherUsageTableHeader, + ElementsAre(kUsageEntryInfoOfflineLicense1))) .WillOnce(Return(true)); - EXPECT_EQ(NO_ERROR, - usage_table_header_->DeleteEntry(usage_entry_number_to_be_deleted, - device_files_, &metrics)); + EXPECT_EQ(NO_ERROR, usage_table_header_->InvalidateEntry( + usage_entry_number_to_be_deleted, true, device_files_, + &metrics)); + // Check the end state of the usage table. + constexpr size_t expected_size = 1; + EXPECT_EQ(expected_size, usage_table_header_->usage_entry_info().size()); } // Initial Test state: // 1. Last few entries are of storage type unknown. -// 2. Usage entry to be deleted preceeds those in (1). +// 2. Usage entry to be deleted precedes those in (1). // // Attempting to delete the entry in (2) will result in: -// a. Entries of storage type unknown at the end will be deleted. -// b. The usage entry requested to be deleted will be deleted. +// a. The entry will be marked as kStorageTypeUnknown. +// b. While defragging, offline license 2 will be selected to be +// moved. +// c. The selected entry will have the usage entry loaded from device +// files. +// d. The selected license will be moved to the entry position near the +// front of the table. +// e. The moved entry will be updated in device files. +// f. Usage table will be resized to have only one entry. +// g. Updated table will be saved. +// h. InvalidateEntry() will return NO_ERROR. // // Storage type Usage entries // at start at end // ============= ======== ====== // Secure Stop 1 0 0 -// Storage Type unknown 1 1 +// Storage Type Unknown 1 Deleted // Offline License 1 2 2 -// Offline License 2 3 3 +// Offline License 2 3 1 (Moved) // Offline License 3 4 Deleted -// Storage Type unknown 5 Deleted -// Storage Type unknown 6 Deleted +// Storage Type Unknown 5 Deleted +// Storage Type Unknown 6 Deleted // -// # of usage entries 7 4 -TEST_F(UsageTableHeaderTest, DeleteEntry_LastEntriesAreStorageTypeUnknown) { - std::vector usage_entry_info_vector; - const CdmUsageEntryInfo usage_entry_info_array[] = { - kUsageEntryInfoSecureStop1, kUsageEntryInfoOfflineLicense1, - kUsageEntryInfoOfflineLicense2, kUsageEntryInfoOfflineLicense3, - kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoStorageTypeUnknown}; - ToVector(usage_entry_info_vector, usage_entry_info_array, - sizeof(usage_entry_info_array)); +// # of usage entries 7 3 +TEST_F(UsageTableHeaderTest, InvalidateEntry_LastEntriesAreStorageTypeUnknown) { + const std::vector usage_entry_info_vector = { + kUsageEntryInfoSecureStop1, kUsageEntryInfoStorageTypeUnknown, + kUsageEntryInfoOfflineLicense1, kUsageEntryInfoOfflineLicense2, + kUsageEntryInfoOfflineLicense3, kUsageEntryInfoStorageTypeUnknown, + kUsageEntryInfoStorageTypeUnknown}; Init(kSecurityLevelL1, kUsageTableHeader, usage_entry_info_vector); - uint32_t usage_entry_number_to_be_deleted = - usage_entry_info_vector.size() - 3; // kUsageEntryInfoOfflineLicense3 + const uint32_t usage_entry_number_to_be_deleted = + 4; // kUsageEntryInfoOfflineLicense3 metrics::CryptoMetrics metrics; + // Expect calls for moving offline license 2 (position 3) to position 1. + const DeviceFiles::CdmLicenseData offline_license_2_data{ + kUsageEntryInfoOfflineLicense2.key_set_id, + kActiveLicenseState, + kPsshData, + kKeyRequest, + kKeyResponse, + kKeyRenewalRequest, + kKeyRenewalResponse, + kReleaseServerUrl, + kPlaybackStartTime, + kPlaybackStartTime + kPlaybackDuration, + kGracePeriodEndTime, + kEmptyAppParameters, + kUsageEntry, + static_cast(3)}; + EXPECT_CALL(*device_files_, + RetrieveLicense(kUsageEntryInfoOfflineLicense2.key_set_id, + NotNull(), NotNull())) + .Times(2) // First to get entry, then again to update. + .WillRepeatedly( + DoAll(SetArgPointee<1>(offline_license_2_data), Return(true))); + // Calls during Move(). EXPECT_CALL(*crypto_session_, Open(kLevelDefault)).WillOnce(Return(NO_ERROR)); - EXPECT_CALL( - *crypto_session_, - ShrinkUsageTableHeader(usage_entry_number_to_be_deleted, NotNull())) - .WillOnce( - DoAll(SetArgPointee<1>(kAnotherUsageTableHeader), Return(NO_ERROR))); - - EXPECT_CALL( - *device_files_, - StoreUsageTableInfo(kAnotherUsageTableHeader, - UnorderedElementsAre(kUsageEntryInfoSecureStop1, - kUsageEntryInfoOfflineLicense1, - kUsageEntryInfoOfflineLicense2))) + EXPECT_CALL(*crypto_session_, LoadUsageEntry(3, kUsageEntry)) + .WillOnce(Return(NO_ERROR)); + EXPECT_CALL(*crypto_session_, MoveUsageEntry(1)).WillOnce(Return(NO_ERROR)); + EXPECT_CALL(*crypto_session_, UpdateUsageEntry(NotNull(), NotNull())) + .WillOnce(DoAll(SetArgPointee<0>(kYetAnotherUsageEntry), + SetArgPointee<1>(kUsageEntry), Return(NO_ERROR))); + EXPECT_CALL(*device_files_, StoreUsageTableInfo(kYetAnotherUsageEntry, _)) + .WillOnce(Return(true)); + EXPECT_CALL(*device_files_, StoreLicense(_, NotNull())) .WillOnce(Return(true)); - EXPECT_EQ(NO_ERROR, - usage_table_header_->DeleteEntry(usage_entry_number_to_be_deleted, - device_files_, &metrics)); + // Shrink to contain the remaining valid entry. + EXPECT_CALL(*crypto_session_, + ShrinkUsageTableHeader(kLevelDefault, 3, NotNull())) + .WillOnce( + DoAll(SetArgPointee<2>(kAnotherUsageTableHeader), Return(NO_ERROR))); + + EXPECT_CALL(*device_files_, + StoreUsageTableInfo(kAnotherUsageTableHeader, + ElementsAre(kUsageEntryInfoSecureStop1, + kUsageEntryInfoOfflineLicense2, + kUsageEntryInfoOfflineLicense1))) + .WillOnce(Return(true)); + + EXPECT_EQ(NO_ERROR, usage_table_header_->InvalidateEntry( + usage_entry_number_to_be_deleted, true, device_files_, + &metrics)); + // Check the end state of the usage table. + constexpr size_t expected_size = 3; + EXPECT_EQ(expected_size, usage_table_header_->usage_entry_info().size()); } // Initial Test state: @@ -1759,40 +1737,43 @@ TEST_F(UsageTableHeaderTest, DeleteEntry_LastEntriesAreStorageTypeUnknown) { // OEMCrypto_MoveUsageEntry on it will fail. // // Attempting to delete the entry in (1) will result in: -// b. The last offline usage entry will not be deleted/moved if the -// OEMCrypto_MoveUsageEntry operation fails. -// c. The usage entry requested to be deleted will be marked as -// storage type unknown. +// a. The entry will be marked as kStorageTypeUnknown. +// b. While defragging, offline license 3 will be selected to be +// moved. +// c. The selected entry will have the usage entry loaded from device +// files. +// d. The move process will fail due to the entry being busy, leaving +// the entry inplace. +// e. Usage table will not be resized. +// f. Updated table will be saved. +// g. InvalidateEntry() will return NO_ERROR. // // Storage type Usage entries // at start at end // ============= ======== ====== // Secure Stop 1 0 0 -// Storage Type unknown 1 1 -// Offline License 1 2 Deleted/storage type unknown +// Secure Stop 2 1 1 +// Offline License 1 2 2 (storage type unknown) // Offline License 2 3 3 // Offline License 3 4 4 // // # of usage entries 5 5 TEST_F(UsageTableHeaderTest, - DeleteEntry_LastEntryIsOffline_MoveOfflineEntryFailed) { - std::vector usage_entry_info_vector; - const CdmUsageEntryInfo usage_entry_info_array[] = { - kUsageEntryInfoSecureStop1, kUsageEntryInfoStorageTypeUnknown, + InvalidateEntry_LastEntryIsOffline_MoveOfflineEntryFailed) { + const std::vector usage_entry_info_vector = { + kUsageEntryInfoSecureStop1, kUsageEntryInfoSecureStop2, kUsageEntryInfoOfflineLicense1, kUsageEntryInfoOfflineLicense2, kUsageEntryInfoOfflineLicense3}; - ToVector(usage_entry_info_vector, usage_entry_info_array, - sizeof(usage_entry_info_array)); Init(kSecurityLevelL1, kUsageTableHeader, usage_entry_info_vector); - uint32_t usage_entry_number_to_be_deleted = - usage_entry_info_vector.size() - 3; // kUsageEntryInfoOfflineLicense1 - uint32_t last_usage_entry_number = - usage_entry_info_vector.size() - 1; // kUsageEntryInfoOfflineLicense3 + const uint32_t usage_entry_number_to_be_deleted = + 2; // kUsageEntryInfoOfflineLicense1 metrics::CryptoMetrics metrics; - const DeviceFiles::CdmLicenseData license_data{ - usage_entry_info_vector[last_usage_entry_number].key_set_id, + // Expect calls for moving offline license 3 (position 4), but + // failure to move will not result in any calls for updating. + const DeviceFiles::CdmLicenseData offline_license_3_data{ + kUsageEntryInfoOfflineLicense3.key_set_id, kActiveLicenseState, kPsshData, kKeyRequest, @@ -1805,34 +1786,36 @@ TEST_F(UsageTableHeaderTest, kGracePeriodEndTime, kEmptyAppParameters, kUsageEntry, - last_usage_entry_number}; - DeviceFiles::ResponseType sub_error_code; - EXPECT_TRUE(device_files_->StoreLicense(license_data, &sub_error_code)); - EXPECT_EQ(DeviceFiles::kNoError, sub_error_code); - - EXPECT_CALL(*crypto_session_, Open(kLevelDefault)).WillOnce(Return(NO_ERROR)); - EXPECT_CALL(*crypto_session_, - LoadUsageEntry(last_usage_entry_number, kUsageEntry)) - .WillOnce(Return(NO_ERROR)); - EXPECT_CALL(*crypto_session_, - MoveUsageEntry(usage_entry_number_to_be_deleted)) - .WillOnce(Return(MOVE_USAGE_ENTRY_UNKNOWN_ERROR)); - - EXPECT_CALL( - *device_files_, - StoreUsageTableInfo( - kUsageTableHeader, - UnorderedElementsAre( - kUsageEntryInfoSecureStop1, kUsageEntryInfoStorageTypeUnknown, - kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoOfflineLicense2, - kUsageEntryInfoOfflineLicense3))) - .WillOnce(Return(true)); + /* usage_entry_number = */ 4}; EXPECT_CALL(*device_files_, - RetrieveLicense(license_data.key_set_id, NotNull(), NotNull())); + RetrieveLicense(kUsageEntryInfoOfflineLicense3.key_set_id, + NotNull(), NotNull())) + .WillOnce(DoAll(SetArgPointee<1>(offline_license_3_data), Return(true))); - EXPECT_EQ(NO_ERROR, - usage_table_header_->DeleteEntry(usage_entry_number_to_be_deleted, - device_files_, &metrics)); + // Calls during Move(). + EXPECT_CALL(*crypto_session_, Open(kLevelDefault)).WillOnce(Return(NO_ERROR)); + EXPECT_CALL(*crypto_session_, LoadUsageEntry(4, kUsageEntry)) + .WillOnce(Return(LOAD_USAGE_ENTRY_INVALID_SESSION)); + + // No calls to shrink are expected. + + // Regardless, the usage table should be updated to reflect the changes + // to the usage entry marked as storage type unknown. + EXPECT_CALL(*device_files_, + StoreUsageTableInfo(kUsageTableHeader, + ElementsAre(kUsageEntryInfoSecureStop1, + kUsageEntryInfoSecureStop2, + kUsageEntryInfoStorageTypeUnknown, + kUsageEntryInfoOfflineLicense2, + kUsageEntryInfoOfflineLicense3))) + .WillOnce(Return(true)); + + EXPECT_EQ(NO_ERROR, usage_table_header_->InvalidateEntry( + usage_entry_number_to_be_deleted, true, device_files_, + &metrics)); + // Check that the table has been updated as expected. + constexpr size_t expected_size = 5; + EXPECT_EQ(expected_size, usage_table_header_->usage_entry_info().size()); } // Initial Test state: @@ -1841,117 +1824,126 @@ TEST_F(UsageTableHeaderTest, // OEMCrypto_MoveUsageEntry on it will fail. // // Attempting to delete the entry in (1) will result in: -// b. The last secure stop usage entry will not be deleted/moved if the -// OEMCrypto_MoveUsageEntry operation fails. -// c. The usage entry requested to be deleted will be marked as -// storage type unknown. +// a. The entry will be marked as kStorageTypeUnknown. +// b. While defragging, secure stop 3 will be selected to be moved. +// c. The selected entry will have the usage entry loaded from device +// files. +// d. The move process will fail due to the entry being busy, leaving +// the entry inplace. +// e. Usage table will not be resized. +// f. Updated table will be saved. +// g. InvalidateEntry() will return NO_ERROR. // // Storage type Usage entries // at start at end // ============= ======== ====== // Offline License 1 0 0 -// Storage Type unknown 1 1 -// Secure stop 1 2 Deleted/storage type unknown -// Secure stop 2 3 3 -// Secure stop 3 4 4 +// Offline License 2 1 1 +// Secure Stop 1 2 2 (storage type unknown) +// Secure Stop 2 3 3 +// Secure Stop 3 4 4 // // # of usage entries 5 5 TEST_F(UsageTableHeaderTest, - DeleteEntry_LastEntryIsSecureStop_MoveSecureStopEntryFailed) { - std::vector usage_entry_info_vector; - const CdmUsageEntryInfo usage_entry_info_array[] = { - kUsageEntryInfoOfflineLicense1, kUsageEntryInfoStorageTypeUnknown, + InvalidateEntry_LastEntryIsSecureStop_MoveSecureStopEntryFailed) { + const std::vector usage_entry_info_vector = { + kUsageEntryInfoOfflineLicense1, kUsageEntryInfoOfflineLicense2, kUsageEntryInfoSecureStop1, kUsageEntryInfoSecureStop2, kUsageEntryInfoSecureStop3}; - ToVector(usage_entry_info_vector, usage_entry_info_array, - sizeof(usage_entry_info_array)); Init(kSecurityLevelL1, kUsageTableHeader, usage_entry_info_vector); - uint32_t usage_entry_number_to_be_deleted = - usage_entry_info_vector.size() - 3; // kUsageEntryInfoSecureStop1 - uint32_t last_usage_entry_number = - usage_entry_info_vector.size() - 1; // kUsageEntryInfoSecureStop3 + const uint32_t usage_entry_number_to_be_deleted = + 2; // kUsageEntryInfoSecureStop1 metrics::CryptoMetrics metrics; - EXPECT_CALL(*crypto_session_, Open(kLevelDefault)).WillOnce(Return(NO_ERROR)); - EXPECT_CALL(*crypto_session_, - LoadUsageEntry(last_usage_entry_number, kUsageEntry)) - .WillOnce(Return(NO_ERROR)); - EXPECT_CALL(*crypto_session_, - MoveUsageEntry(usage_entry_number_to_be_deleted)) - .WillOnce(Return(MOVE_USAGE_ENTRY_UNKNOWN_ERROR)); - + // Expect calls for moving secure stop 3 (position 4), but + // failure to move will not result in any calls for updating. EXPECT_CALL(*device_files_, RetrieveUsageInfoByKeySetId( kUsageEntryInfoSecureStop3.usage_info_file_name, kUsageEntryInfoSecureStop3.key_set_id, NotNull(), NotNull(), NotNull(), NotNull(), NotNull())) - .WillOnce(DoAll(SetArgPointee<2>(kProviderSessionToken), - SetArgPointee<3>(kKeyRequest), - SetArgPointee<4>(kKeyResponse), - SetArgPointee<5>(kUsageEntry), - SetArgPointee<6>(last_usage_entry_number), Return(true))); + .WillOnce(DoAll( + SetArgPointee<2>(kProviderSessionToken), + SetArgPointee<3>(kKeyRequest), SetArgPointee<4>(kKeyResponse), + SetArgPointee<5>(kUsageEntry), SetArgPointee<6>(4), Return(true))); - EXPECT_CALL( - *device_files_, - StoreUsageTableInfo( - kUsageTableHeader, - UnorderedElementsAre( - kUsageEntryInfoOfflineLicense1, kUsageEntryInfoStorageTypeUnknown, - kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoSecureStop2, - kUsageEntryInfoSecureStop3))) + // Calls during Move(). + EXPECT_CALL(*crypto_session_, Open(kLevelDefault)).WillOnce(Return(NO_ERROR)); + EXPECT_CALL(*crypto_session_, LoadUsageEntry(4, kUsageEntry)) + .WillOnce(Return(LOAD_USAGE_ENTRY_INVALID_SESSION)); + + // No calls to shrink are expected. + + // Regardless, the usage table should be updated to reflect the changes + // to the usage entry marked as storage type unknown. + EXPECT_CALL(*device_files_, + StoreUsageTableInfo(kUsageTableHeader, + ElementsAre(kUsageEntryInfoOfflineLicense1, + kUsageEntryInfoOfflineLicense2, + kUsageEntryInfoStorageTypeUnknown, + kUsageEntryInfoSecureStop2, + kUsageEntryInfoSecureStop3))) .WillOnce(Return(true)); - EXPECT_EQ(NO_ERROR, - usage_table_header_->DeleteEntry(usage_entry_number_to_be_deleted, - device_files_, &metrics)); + EXPECT_EQ(NO_ERROR, usage_table_header_->InvalidateEntry( + usage_entry_number_to_be_deleted, true, device_files_, + &metrics)); + constexpr size_t expected_size = 5; + EXPECT_EQ(expected_size, usage_table_header_->usage_entry_info().size()); } // Initial Test state: -// 1. Usage entry to be deleted is not last +// 1. Usage entry to be deleted is not last (Offline license 1) // 2. Last few entries are of storage type unknown. -// 3. Entry that preceeds those in (2) is an offline license and calling -// OEMCrypto_MoveUsageEntry on it will fail. +// 3. Entry that precedes those in (2) are offline license and calling +// OEMCrypto_LoadUsageEntry on it will fail. // // Attempting to delete the entry in (1) will result in: -// a. Entries of storage type unknown at the end will be deleted. -// b. The offline usage entry that preceeds the entries in (a) will -// not be deleted/moved if the OEMCrypto_MoveUsageEntry operation fails. -// c. The usage entry requested to be deleted will be marked as -// storage type unknown. +// a. The entry will be marked as kStorageTypeUnknown. +// b. While defragging, offline licenses 2 and 3 will be selected to be +// moved. +// c. The selected entry will have the usage entry loaded from device +// files. +// d. The move processes will fail due to the entries being busy, leaving +// the entries inplace. +// e. Usage table will be resized to remove the last two entries. +// f. Updated table will be saved. +// g. InvalidateEntry() will return NO_ERROR. // // Storage type Usage entries // at start at end // ============= ======== ====== // Secure Stop 1 0 0 -// Storage Type unknown 1 1 -// Offline License 1 2 Deleted/storage type unknown +// Storage Type Unknown 1 1 +// Offline License 1 2 2 (storage type unknown) // Offline License 2 3 3 // Offline License 3 4 4 -// Storage Type unknown 5 Deleted -// Storage Type unknown 6 Deleted +// Storage Type Unknown 5 Deleted +// Storage Type Unknown 6 Deleted // // # of usage entries 7 5 TEST_F(UsageTableHeaderTest, - DeleteEntry_LastEntriesAreOfflineAndUnknown_MoveOfflineEntryFailed) { - std::vector usage_entry_info_vector; - const CdmUsageEntryInfo usage_entry_info_array[] = { + InvalidateEntry_LastEntriesAreOfflineAndUnknown_MoveOfflineEntryFailed) { + const std::vector usage_entry_info_vector = { kUsageEntryInfoSecureStop1, kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoOfflineLicense1, kUsageEntryInfoOfflineLicense2, kUsageEntryInfoOfflineLicense3, kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoStorageTypeUnknown}; - ToVector(usage_entry_info_vector, usage_entry_info_array, - sizeof(usage_entry_info_array)); Init(kSecurityLevelL1, kUsageTableHeader, usage_entry_info_vector); - uint32_t usage_entry_number_to_be_deleted = - usage_entry_info_vector.size() - 5; // kUsageEntryInfoOfflineLicense1 - uint32_t last_valid_usage_entry_number = - usage_entry_info_vector.size() - 3; // kUsageEntryInfoOfflineLicense3 + const uint32_t usage_entry_number_to_be_deleted = + 2; // kUsageEntryInfoOfflineLicense1 metrics::CryptoMetrics metrics; - const DeviceFiles::CdmLicenseData license_data{ - usage_entry_info_vector[last_valid_usage_entry_number].key_set_id, + EXPECT_CALL(*crypto_session_, Open(kLevelDefault)) + .Times(2) + .WillRepeatedly(Return(NO_ERROR)); + + // Expect calls for moving offline license 3 (position 4), but + // failure to move will not result in any calls for updating. + const DeviceFiles::CdmLicenseData offline_license_3_data{ + kUsageEntryInfoOfflineLicense3.key_set_id, kActiveLicenseState, kPsshData, kKeyRequest, @@ -1964,163 +1956,203 @@ TEST_F(UsageTableHeaderTest, kGracePeriodEndTime, kEmptyAppParameters, kUsageEntry, - last_valid_usage_entry_number}; - DeviceFiles::ResponseType sub_error_code; - EXPECT_TRUE(device_files_->StoreLicense(license_data, &sub_error_code)); - EXPECT_EQ(DeviceFiles::kNoError, sub_error_code); - - EXPECT_CALL(*crypto_session_, Open(kLevelDefault)) - .Times(2) - .WillRepeatedly(Return(NO_ERROR)); - EXPECT_CALL(*crypto_session_, - LoadUsageEntry(last_valid_usage_entry_number, kUsageEntry)) - .WillOnce(Return(NO_ERROR)); - EXPECT_CALL(*crypto_session_, - MoveUsageEntry(usage_entry_number_to_be_deleted)) - .WillOnce(Return(MOVE_USAGE_ENTRY_UNKNOWN_ERROR)); - EXPECT_CALL( - *crypto_session_, - ShrinkUsageTableHeader(last_valid_usage_entry_number + 1, NotNull())) - .WillOnce( - DoAll(SetArgPointee<1>(kAnotherUsageTableHeader), Return(NO_ERROR))); - - EXPECT_CALL( - *device_files_, - StoreUsageTableInfo( - kAnotherUsageTableHeader, - UnorderedElementsAre( - kUsageEntryInfoSecureStop1, kUsageEntryInfoStorageTypeUnknown, - kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoOfflineLicense2, - kUsageEntryInfoOfflineLicense3))) - .WillOnce(Return(true)); + 4}; EXPECT_CALL(*device_files_, - RetrieveLicense(license_data.key_set_id, NotNull(), NotNull())); + RetrieveLicense(kUsageEntryInfoOfflineLicense3.key_set_id, + NotNull(), NotNull())) + .WillOnce(DoAll(SetArgPointee<1>(offline_license_3_data), Return(true))); + EXPECT_CALL(*crypto_session_, LoadUsageEntry(4, kUsageEntry)) + .WillOnce(Return(LOAD_USAGE_ENTRY_INVALID_SESSION)); - EXPECT_EQ(NO_ERROR, - usage_table_header_->DeleteEntry(usage_entry_number_to_be_deleted, - device_files_, &metrics)); + // Expect calls for moving offline license 2 (position 3), but + // failure to move will not result in any calls for updating. + const DeviceFiles::CdmLicenseData offline_license_2_data{ + kUsageEntryInfoOfflineLicense2.key_set_id, + kActiveLicenseState, + kPsshData, + kKeyRequest, + kKeyResponse, + kKeyRenewalRequest, + kKeyRenewalResponse, + kReleaseServerUrl, + kPlaybackStartTime, + kPlaybackStartTime + kPlaybackDuration, + kGracePeriodEndTime, + kEmptyAppParameters, + kUsageEntry, + 3}; + EXPECT_CALL(*device_files_, + RetrieveLicense(kUsageEntryInfoOfflineLicense2.key_set_id, + NotNull(), NotNull())) + .WillOnce(DoAll(SetArgPointee<1>(offline_license_2_data), Return(true))); + EXPECT_CALL(*crypto_session_, LoadUsageEntry(3, kUsageEntry)) + .WillOnce(Return(LOAD_USAGE_ENTRY_INVALID_SESSION)); + + // Expect a call to shrink table to cut off only the unknown entries + // at the end of the table. + EXPECT_CALL(*crypto_session_, + ShrinkUsageTableHeader(kLevelDefault, 5, NotNull())) + .WillOnce( + DoAll(SetArgPointee<2>(kAnotherUsageTableHeader), Return(NO_ERROR))); + + // Update table for the entry now marked storage type unknown and + // the entries that were cut off. + EXPECT_CALL(*device_files_, + StoreUsageTableInfo(kAnotherUsageTableHeader, + ElementsAre(kUsageEntryInfoSecureStop1, + kUsageEntryInfoStorageTypeUnknown, + kUsageEntryInfoStorageTypeUnknown, + kUsageEntryInfoOfflineLicense2, + kUsageEntryInfoOfflineLicense3))) + .WillOnce(Return(true)); + + EXPECT_EQ(NO_ERROR, usage_table_header_->InvalidateEntry( + usage_entry_number_to_be_deleted, true, device_files_, + &metrics)); + // Check the end state of the usage table. + constexpr size_t expected_size = 5; + EXPECT_EQ(expected_size, usage_table_header_->usage_entry_info().size()); } // Initial Test state: -// 1. Usage entry to be deleted is not last +// 1. Usage entry to be deleted is not last (Secure stop 1) // 2. Last few entries are of storage type unknown. -// 3. Entry that preceeds those in (2) is an offline license and calling -// OEMCrypto_MoveUsageEntry on it will fail. +// 3. Entry that precedes those in (2) are secure stops and calling +// OEMCrypto_LoadUsageEntry on it will fail. // // Attempting to delete the entry in (1) will result in: -// a. Entries of storage type unknown at the end will be deleted. -// b. The offline usage entry that preceeds the entries in (a) will -// not be deleted/moved if the OEMCrypto_MoveUsageEntry operation fails. -// c. The usage entry requested to be deleted will be marked as -// storage type unknown. +// a. The entry will be marked as kStorageTypeUnknown. +// b. While defragging, secure stop 2 and 3 will be selected to be +// moved. +// c. The selected entry will have the usage entry loaded from device +// files. +// d. The move processes will fail due to the entries being busy, leaving +// the entries inplace. +// e. Usage table will be resized to remove the last two entries. +// f. Updated table will be saved. +// g. InvalidateEntry() will return NO_ERROR. // // Storage type Usage entries // at start at end // ============= ======== ====== // Offline License 1 0 0 // Storage Type unknown 1 1 -// Secure stop 1 2 Deleted/storage type unknown +// Secure stop 1 2 2 (storage type unknown) // Secure stop 2 3 3 // Secure stop 3 4 4 // Storage Type unknown 5 Deleted // Storage Type unknown 6 Deleted // // # of usage entries 7 5 -TEST_F(UsageTableHeaderTest, - DeleteEntry_LastEntriesAreSecureStopAndUnknown_MoveOfflineEntryFailed) { - std::vector usage_entry_info_vector; - const CdmUsageEntryInfo usage_entry_info_array[] = { +TEST_F( + UsageTableHeaderTest, + InvalidateEntry_LastEntriesAreSecureStopAndUnknown_MoveOfflineEntryFailed) { + const std::vector usage_entry_info_vector = { kUsageEntryInfoOfflineLicense1, kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoSecureStop1, kUsageEntryInfoSecureStop2, kUsageEntryInfoSecureStop3, kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoStorageTypeUnknown}; - ToVector(usage_entry_info_vector, usage_entry_info_array, - sizeof(usage_entry_info_array)); Init(kSecurityLevelL1, kUsageTableHeader, usage_entry_info_vector); - uint32_t usage_entry_number_to_be_deleted = - usage_entry_info_vector.size() - 5; // kUsageEntryInfoOfflineLicense1 - uint32_t last_valid_usage_entry_number = - usage_entry_info_vector.size() - 3; // kUsageEntryInfoOfflineLicense3 + const uint32_t usage_entry_number_to_be_deleted = + 2; // kUsageEntryInfoSecureStop1 metrics::CryptoMetrics metrics; EXPECT_CALL(*crypto_session_, Open(kLevelDefault)) .Times(2) .WillRepeatedly(Return(NO_ERROR)); - EXPECT_CALL(*crypto_session_, - LoadUsageEntry(last_valid_usage_entry_number, kUsageEntry)) - .WillOnce(Return(NO_ERROR)); - EXPECT_CALL(*crypto_session_, - MoveUsageEntry(usage_entry_number_to_be_deleted)) - .WillOnce(Return(MOVE_USAGE_ENTRY_UNKNOWN_ERROR)); + // Expect calls for moving streaming license 3 (position 4), but + // failure to move will not result in any calls for updating. EXPECT_CALL(*device_files_, RetrieveUsageInfoByKeySetId( kUsageEntryInfoSecureStop3.usage_info_file_name, kUsageEntryInfoSecureStop3.key_set_id, NotNull(), NotNull(), NotNull(), NotNull(), NotNull())) - .WillOnce( - DoAll(SetArgPointee<2>(kProviderSessionToken), - SetArgPointee<3>(kKeyRequest), SetArgPointee<4>(kKeyResponse), - SetArgPointee<5>(kUsageEntry), - SetArgPointee<6>(last_valid_usage_entry_number), Return(true))); - EXPECT_CALL( - *crypto_session_, - ShrinkUsageTableHeader(last_valid_usage_entry_number + 1, NotNull())) - .WillOnce( - DoAll(SetArgPointee<1>(kAnotherUsageTableHeader), Return(NO_ERROR))); + .WillOnce(DoAll( + SetArgPointee<2>(kProviderSessionToken), + SetArgPointee<3>(kKeyRequest), SetArgPointee<4>(kKeyResponse), + SetArgPointee<5>(kUsageEntry), SetArgPointee<6>(4), Return(true))); + EXPECT_CALL(*crypto_session_, LoadUsageEntry(4, kUsageEntry)) + .WillOnce(Return(LOAD_USAGE_ENTRY_INVALID_SESSION)); - EXPECT_CALL( - *device_files_, - StoreUsageTableInfo( - kAnotherUsageTableHeader, - UnorderedElementsAre( - kUsageEntryInfoOfflineLicense1, kUsageEntryInfoStorageTypeUnknown, - kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoSecureStop2, - kUsageEntryInfoSecureStop3))) + // Expect calls for moving streaming license 2 (position 3), but + // failure to move will not result in any calls for updating. + EXPECT_CALL(*device_files_, + RetrieveUsageInfoByKeySetId( + kUsageEntryInfoSecureStop2.usage_info_file_name, + kUsageEntryInfoSecureStop2.key_set_id, NotNull(), NotNull(), + NotNull(), NotNull(), NotNull())) + .WillOnce(DoAll( + SetArgPointee<2>(kProviderSessionToken), + SetArgPointee<3>(kKeyRequest), SetArgPointee<4>(kKeyResponse), + SetArgPointee<5>(kUsageEntry), SetArgPointee<6>(3), Return(true))); + EXPECT_CALL(*crypto_session_, LoadUsageEntry(3, kUsageEntry)) + .WillOnce(Return(LOAD_USAGE_ENTRY_INVALID_SESSION)); + + // Expect a call to shrink table to cut off only the unknown entries + // at the end of the table. + EXPECT_CALL(*crypto_session_, + ShrinkUsageTableHeader(kLevelDefault, 5, NotNull())) + .WillOnce( + DoAll(SetArgPointee<2>(kAnotherUsageTableHeader), Return(NO_ERROR))); + + // Update table for the entry now marked storage type unknown and + // the entries that were cut off. + EXPECT_CALL(*device_files_, + StoreUsageTableInfo(kAnotherUsageTableHeader, + ElementsAre(kUsageEntryInfoOfflineLicense1, + kUsageEntryInfoStorageTypeUnknown, + kUsageEntryInfoStorageTypeUnknown, + kUsageEntryInfoSecureStop2, + kUsageEntryInfoSecureStop3))) .WillOnce(Return(true)); - EXPECT_EQ(NO_ERROR, - usage_table_header_->DeleteEntry(usage_entry_number_to_be_deleted, - device_files_, &metrics)); + EXPECT_EQ(NO_ERROR, usage_table_header_->InvalidateEntry( + usage_entry_number_to_be_deleted, true, device_files_, + &metrics)); + // Check the end state of the usage table. + constexpr size_t expected_size = 5; + EXPECT_EQ(expected_size, usage_table_header_->usage_entry_info().size()); } // Initial Test state: // 1. Usage entry to be deleted is not last. -// 2. Last entry is an offline license. +// 2. Last entries are valid offline licenses. // // Attempting to delete the entry in (1) will result in: -// a. The usage entry requested to be deleted will be replaced with the last -// entry. +// a. The entry will be marked as kStorageTypeUnknown. +// b. While defragging, offline licenses 2 and 3 will be selected to be +// moved. +// c. The selected entries will be moved. +// d. Usage table will be resized. +// e. Updated table will be saved. +// f. InvalidateEntry() will return NO_ERROR. // // Storage type Usage entries // at start at end // ============= ======== ====== // Secure Stop 1 0 0 -// Storage Type unknown 1 1 +// Storage Type unknown 1 Deleted // Offline License 1 2 Deleted -// Offline License 2 3 3 -// Offline License 3 4 2 +// Offline License 2 3 2 (moved) +// Offline License 3 4 1 (moved) // -// # of usage entries 5 4 -TEST_F(UsageTableHeaderTest, DeleteEntry_LastEntryIsOffline) { - std::vector usage_entry_info_vector; - const CdmUsageEntryInfo usage_entry_info_array[] = { +// # of usage entries 5 3 +TEST_F(UsageTableHeaderTest, InvalidateEntry_LastEntryIsOffline) { + const std::vector usage_entry_info_vector = { kUsageEntryInfoSecureStop1, kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoOfflineLicense1, kUsageEntryInfoOfflineLicense2, kUsageEntryInfoOfflineLicense3}; - ToVector(usage_entry_info_vector, usage_entry_info_array, - sizeof(usage_entry_info_array)); Init(kSecurityLevelL1, kUsageTableHeader, usage_entry_info_vector); - uint32_t usage_entry_number_to_be_deleted = - usage_entry_info_vector.size() - 3; // kUsageEntryInfoOfflineLicense1 - uint32_t last_usage_entry_number = - usage_entry_info_vector.size() - 1; // kUsageEntryInfoOfflineLicense3 + const uint32_t usage_entry_number_to_be_deleted = + 2; // kUsageEntryInfoOfflineLicense1 metrics::CryptoMetrics metrics; - const DeviceFiles::CdmLicenseData stored_license_data{ - usage_entry_info_vector[last_usage_entry_number].key_set_id, + // Expect calls for moving offline license 3 (position 4) to position 1. + const DeviceFiles::CdmLicenseData offline_license_3_data{ + kUsageEntryInfoOfflineLicense3.key_set_id, kActiveLicenseState, kPsshData, kKeyRequest, @@ -2133,222 +2165,236 @@ TEST_F(UsageTableHeaderTest, DeleteEntry_LastEntryIsOffline) { kGracePeriodEndTime, kEmptyAppParameters, kUsageEntry, - last_usage_entry_number}; - DeviceFiles::ResponseType sub_error_code; - EXPECT_TRUE( - device_files_->StoreLicense(stored_license_data, &sub_error_code)); - EXPECT_EQ(DeviceFiles::kNoError, sub_error_code); - - EXPECT_CALL(*crypto_session_, Open(kLevelDefault)) - .Times(2) - .WillRepeatedly(Return(NO_ERROR)); - EXPECT_CALL(*crypto_session_, - LoadUsageEntry(last_usage_entry_number, kUsageEntry)) - .WillOnce(Return(NO_ERROR)); - EXPECT_CALL(*crypto_session_, - MoveUsageEntry(usage_entry_number_to_be_deleted)) - .WillOnce(Return(NO_ERROR)); - EXPECT_CALL(*crypto_session_, UpdateUsageEntry(NotNull(), NotNull())) - .WillOnce(DoAll(SetArgPointee<0>(kAnotherUsageTableHeader), - SetArgPointee<1>(kAnotherUsageEntry), Return(NO_ERROR))); - EXPECT_CALL(*crypto_session_, - ShrinkUsageTableHeader(last_usage_entry_number, NotNull())) - .WillOnce( - DoAll(SetArgPointee<1>(kAnotherUsageTableHeader), Return(NO_ERROR))); - - EXPECT_CALL( - *device_files_, - StoreUsageTableInfo( - kAnotherUsageTableHeader, - UnorderedElementsAre( - kUsageEntryInfoSecureStop1, kUsageEntryInfoStorageTypeUnknown, - kUsageEntryInfoOfflineLicense3, kUsageEntryInfoOfflineLicense2))) - .WillOnce(Return(true)); - - EXPECT_CALL( - *device_files_, - StoreUsageTableInfo( - kAnotherUsageTableHeader, - UnorderedElementsAre( - kUsageEntryInfoSecureStop1, kUsageEntryInfoStorageTypeUnknown, - kUsageEntryInfoOfflineLicense3, kUsageEntryInfoOfflineLicense2, - kUsageEntryInfoOfflineLicense3))) - .WillOnce(Return(true)); - // Expecting three calls to RetrieveLicense(), twice by usage table when - // swapping (probing for swap, and the actual swap_, then by test case - // to verify data is still there. + static_cast(4)}; EXPECT_CALL(*device_files_, RetrieveLicense(kUsageEntryInfoOfflineLicense3.key_set_id, NotNull(), NotNull())) - .Times(3); + .Times(2) // First to get entry, then again to update. + .WillRepeatedly( + DoAll(SetArgPointee<1>(offline_license_3_data), Return(true))); + EXPECT_CALL(*crypto_session_, LoadUsageEntry(4, kUsageEntry)) + .WillOnce(Return(NO_ERROR)); + EXPECT_CALL(*crypto_session_, MoveUsageEntry(1)).WillOnce(Return(NO_ERROR)); - EXPECT_EQ(NO_ERROR, - usage_table_header_->DeleteEntry(usage_entry_number_to_be_deleted, - device_files_, &metrics)); + // Expect calls for moving offline license 2 (position 3) to position 2. + const DeviceFiles::CdmLicenseData offline_license_2_data{ + kUsageEntryInfoOfflineLicense2.key_set_id, + kActiveLicenseState, + kPsshData, + kKeyRequest, + kKeyResponse, + kKeyRenewalRequest, + kKeyRenewalResponse, + kReleaseServerUrl, + kPlaybackStartTime, + kPlaybackStartTime + kPlaybackDuration, + kGracePeriodEndTime, + kEmptyAppParameters, + kUsageEntry, + static_cast(3)}; + EXPECT_CALL(*device_files_, + RetrieveLicense(kUsageEntryInfoOfflineLicense2.key_set_id, + NotNull(), NotNull())) + .Times(2) // First to get entry, then again to update. + .WillRepeatedly( + DoAll(SetArgPointee<1>(offline_license_2_data), Return(true))); + EXPECT_CALL(*crypto_session_, LoadUsageEntry(3, kUsageEntry)) + .WillOnce(Return(NO_ERROR)); + EXPECT_CALL(*crypto_session_, MoveUsageEntry(2)).WillOnce(Return(NO_ERROR)); - DeviceFiles::CdmLicenseData retrieved_license_data; - EXPECT_TRUE( - device_files_->RetrieveLicense(kUsageEntryInfoOfflineLicense3.key_set_id, - &retrieved_license_data, &sub_error_code)); + // Common to both moves. + EXPECT_CALL(*crypto_session_, Open(kLevelDefault)) + .Times(2) + .WillRepeatedly(Return(NO_ERROR)); + EXPECT_CALL(*device_files_, StoreLicense(_, NotNull())) + .Times(2) + .WillRepeatedly(Return(true)); + EXPECT_CALL(*crypto_session_, UpdateUsageEntry(NotNull(), NotNull())) + .Times(2) + .WillRepeatedly(DoAll(SetArgPointee<0>(kAnotherUsageTableHeader), + SetArgPointee<1>(kAnotherUsageEntry), + Return(NO_ERROR))); + EXPECT_CALL(*device_files_, StoreUsageTableInfo(kAnotherUsageTableHeader, _)) + .Times(2) + .WillRepeatedly(Return(true)); - EXPECT_EQ(kActiveLicenseState, retrieved_license_data.state); - EXPECT_EQ(kPsshData, retrieved_license_data.pssh_data); - EXPECT_EQ(kKeyRequest, retrieved_license_data.license_request); - EXPECT_EQ(kKeyResponse, retrieved_license_data.license); - EXPECT_EQ(kKeyRenewalRequest, retrieved_license_data.license_renewal_request); - EXPECT_EQ(kKeyRenewalResponse, retrieved_license_data.license_renewal); - EXPECT_EQ(kReleaseServerUrl, retrieved_license_data.release_server_url); - EXPECT_EQ(kPlaybackStartTime, retrieved_license_data.playback_start_time); - EXPECT_EQ(kPlaybackStartTime + kPlaybackDuration, - retrieved_license_data.last_playback_time); - EXPECT_EQ(kGracePeriodEndTime, retrieved_license_data.grace_period_end_time); - EXPECT_EQ(kEmptyAppParameters.size(), - retrieved_license_data.app_parameters.size()); - EXPECT_EQ(kAnotherUsageEntry, retrieved_license_data.usage_entry); - EXPECT_EQ(usage_entry_number_to_be_deleted, - retrieved_license_data.usage_entry_number); - EXPECT_EQ(DeviceFiles::kNoError, sub_error_code); + // Shrink to contain the remaining valid entry. + EXPECT_CALL(*crypto_session_, + ShrinkUsageTableHeader(kLevelDefault, 3, NotNull())) + .WillOnce( + DoAll(SetArgPointee<2>(kYetAnotherUsageEntry), Return(NO_ERROR))); + EXPECT_CALL(*device_files_, + StoreUsageTableInfo(kYetAnotherUsageEntry, + ElementsAre(kUsageEntryInfoSecureStop1, + kUsageEntryInfoOfflineLicense3, + kUsageEntryInfoOfflineLicense2))) + .WillOnce(Return(true)); + + EXPECT_EQ(NO_ERROR, usage_table_header_->InvalidateEntry( + usage_entry_number_to_be_deleted, true, device_files_, + &metrics)); + // Check the end state of the usage table. + constexpr size_t expected_size = 3; + EXPECT_EQ(expected_size, usage_table_header_->usage_entry_info().size()); } // Initial Test state: // 1. Usage entry to be deleted is not last. -// 2. Last entry is a secure stop. +// 2. Last entries are valid streaming licenses. // // Attempting to delete the entry in (1) will result in: -// a. The usage entry requested to be deleted will be replaced with the last -// entry. +// a. The entry will be marked as kStorageTypeUnknown. +// b. While defragging, secure stop 2 and 3 will be selected to be +// moved. +// c. The selected entries will be moved. +// d. Usage table will be resized. +// e. Updated table will be saved. +// f. InvalidateEntry() will return NO_ERROR. // // Storage type Usage entries // at start at end // ============= ======== ====== // Offline License 1 0 0 -// Storage Type unknown 1 1 +// Storage Type unknown 1 Deleted // Secure stop 1 2 Deleted -// Secure stop 2 3 3 -// Secure stop 3 4 2 +// Secure stop 2 3 2 (moved) +// Secure stop 3 4 1 (moved) // -// # of usage entries 5 4 -TEST_F(UsageTableHeaderTest, DeleteEntry_LastEntryIsSecureStop) { - std::vector usage_entry_info_vector; - const CdmUsageEntryInfo usage_entry_info_array[] = { +// # of usage entries 5 3 +TEST_F(UsageTableHeaderTest, InvalidateEntry_LastEntryIsSecureStop) { + const std::vector usage_entry_info_vector = { kUsageEntryInfoOfflineLicense1, kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoSecureStop1, kUsageEntryInfoSecureStop2, kUsageEntryInfoSecureStop3}; - ToVector(usage_entry_info_vector, usage_entry_info_array, - sizeof(usage_entry_info_array)); Init(kSecurityLevelL1, kUsageTableHeader, usage_entry_info_vector); - uint32_t usage_entry_number_to_be_deleted = - usage_entry_info_vector.size() - 3; // kUsageEntryInfoSecureStop1 - uint32_t last_usage_entry_number = - usage_entry_info_vector.size() - 1; // kUsageEntryInfoSecureStop3 + const uint32_t usage_entry_number_to_be_deleted = + 2; // kUsageEntryInfoSecureStop1 metrics::CryptoMetrics metrics; - EXPECT_CALL(*crypto_session_, Open(kLevelDefault)) - .Times(2) - .WillRepeatedly(Return(NO_ERROR)); - EXPECT_CALL(*crypto_session_, - LoadUsageEntry(last_usage_entry_number, kUsageEntry)) - .WillOnce(Return(NO_ERROR)); - EXPECT_CALL(*crypto_session_, - MoveUsageEntry(usage_entry_number_to_be_deleted)) - .WillOnce(Return(NO_ERROR)); - EXPECT_CALL(*crypto_session_, UpdateUsageEntry(NotNull(), NotNull())) - .WillOnce(DoAll(SetArgPointee<0>(kAnotherUsageTableHeader), - SetArgPointee<1>(kAnotherUsageEntry), Return(NO_ERROR))); - EXPECT_CALL(*crypto_session_, - ShrinkUsageTableHeader(last_usage_entry_number, NotNull())) - .WillOnce( - DoAll(SetArgPointee<1>(kAnotherUsageTableHeader), Return(NO_ERROR))); - + // Expect calls for moving streaming license 3 (position 4) to position 1. EXPECT_CALL(*device_files_, RetrieveUsageInfoByKeySetId( kUsageEntryInfoSecureStop3.usage_info_file_name, kUsageEntryInfoSecureStop3.key_set_id, NotNull(), NotNull(), NotNull(), NotNull(), NotNull())) - .WillRepeatedly( - DoAll(SetArgPointee<2>(kProviderSessionToken), - SetArgPointee<3>(kKeyRequest), SetArgPointee<4>(kKeyResponse), - SetArgPointee<5>(kUsageEntry), - SetArgPointee<6>(last_usage_entry_number), Return(true))); + .Times(2) // First to get entry, second to update. + .WillRepeatedly(DoAll( + SetArgPointee<2>(kProviderSessionToken), + SetArgPointee<3>(kKeyRequest), SetArgPointee<4>(kKeyResponse), + SetArgPointee<5>(kUsageEntry), SetArgPointee<6>(4), Return(true))); + EXPECT_CALL( + *device_files_, + DeleteUsageInfo(kUsageEntryInfoSecureStop3.usage_info_file_name, _)) + .WillOnce(Return(true)); + EXPECT_CALL( + *device_files_, + StoreUsageInfo(_, _, _, kUsageEntryInfoSecureStop3.usage_info_file_name, + kUsageEntryInfoSecureStop3.key_set_id, _, 1)) + .WillOnce(Return(true)); + EXPECT_CALL(*crypto_session_, LoadUsageEntry(4, kUsageEntry)) + .WillOnce(Return(NO_ERROR)); + EXPECT_CALL(*crypto_session_, MoveUsageEntry(1)); + // Expect calls for moving streaming license 2 (position 3) to position 2. EXPECT_CALL(*device_files_, - DeleteUsageInfo(kUsageEntryInfoSecureStop3.usage_info_file_name, - kProviderSessionToken)) - .WillOnce(Return(true)); - + RetrieveUsageInfoByKeySetId( + kUsageEntryInfoSecureStop2.usage_info_file_name, + kUsageEntryInfoSecureStop2.key_set_id, NotNull(), NotNull(), + NotNull(), NotNull(), NotNull())) + .Times(2) // First to get entry, second to update. + .WillRepeatedly(DoAll( + SetArgPointee<2>(kProviderSessionToken), + SetArgPointee<3>(kKeyRequest), SetArgPointee<4>(kKeyResponse), + SetArgPointee<5>(kUsageEntry), SetArgPointee<6>(3), Return(true))); EXPECT_CALL( *device_files_, - StoreUsageInfo(kProviderSessionToken, kKeyRequest, kKeyResponse, - kUsageEntryInfoSecureStop3.usage_info_file_name, - kUsageEntryInfoSecureStop3.key_set_id, kAnotherUsageEntry, - usage_entry_number_to_be_deleted)) + DeleteUsageInfo(kUsageEntryInfoSecureStop2.usage_info_file_name, _)) .WillOnce(Return(true)); - EXPECT_CALL( *device_files_, - StoreUsageTableInfo( - kAnotherUsageTableHeader, - UnorderedElementsAre( - kUsageEntryInfoOfflineLicense1, kUsageEntryInfoStorageTypeUnknown, - kUsageEntryInfoSecureStop3, kUsageEntryInfoSecureStop2))) + StoreUsageInfo(_, _, _, kUsageEntryInfoSecureStop2.usage_info_file_name, + kUsageEntryInfoSecureStop2.key_set_id, _, 2)) + .WillOnce(Return(true)); + EXPECT_CALL(*crypto_session_, LoadUsageEntry(3, kUsageEntry)) + .WillOnce(Return(NO_ERROR)); + EXPECT_CALL(*crypto_session_, MoveUsageEntry(2)); + + // Common to both moves. + EXPECT_CALL(*crypto_session_, Open(kLevelDefault)) + .Times(2) + .WillRepeatedly(Return(NO_ERROR)); + EXPECT_CALL(*crypto_session_, UpdateUsageEntry(NotNull(), NotNull())) + .Times(2) + .WillRepeatedly(DoAll(SetArgPointee<0>(kAnotherUsageTableHeader), + SetArgPointee<1>(kAnotherUsageEntry), + Return(NO_ERROR))); + EXPECT_CALL(*device_files_, StoreUsageTableInfo(kAnotherUsageTableHeader, _)) + .Times(2) + .WillRepeatedly(Return(true)); + + // Shrink to contain the remaining valid entry. + EXPECT_CALL(*crypto_session_, + ShrinkUsageTableHeader(kLevelDefault, 3, NotNull())) + .WillOnce( + DoAll(SetArgPointee<2>(kYetAnotherUsageEntry), Return(NO_ERROR))); + EXPECT_CALL(*device_files_, + StoreUsageTableInfo(kYetAnotherUsageEntry, + ElementsAre(kUsageEntryInfoOfflineLicense1, + kUsageEntryInfoSecureStop3, + kUsageEntryInfoSecureStop2))) .WillOnce(Return(true)); - EXPECT_CALL( - *device_files_, - StoreUsageTableInfo( - kAnotherUsageTableHeader, - UnorderedElementsAre( - kUsageEntryInfoOfflineLicense1, kUsageEntryInfoStorageTypeUnknown, - kUsageEntryInfoSecureStop3, kUsageEntryInfoSecureStop2, - kUsageEntryInfoSecureStop3))) - .WillOnce(Return(true)); - - EXPECT_EQ(NO_ERROR, - usage_table_header_->DeleteEntry(usage_entry_number_to_be_deleted, - device_files_, &metrics)); + EXPECT_EQ(NO_ERROR, usage_table_header_->InvalidateEntry( + usage_entry_number_to_be_deleted, true, device_files_, + &metrics)); + // Check the end state of the usage table. + constexpr size_t expected_size = 3; + EXPECT_EQ(expected_size, usage_table_header_->usage_entry_info().size()); } // Initial Test state: // 1. Usage entry to be deleted is not last. // 2. Last few entries are of storage type unknown. -// 3. Entry that preceeds those in (2) is an offline license. +// 3. Entry that precedes those in (2) is an offline license. // // Attempting to delete the entry in (1) will result in: -// a. The entry being deleted and replaced with the offline entry in (3). -// b. The entries with unknown storage type in (2) will be deleted. +// a. The entry will be marked as kStorageTypeUnknown. +// b. While defragging, offline licenses 2 and 3 will be selected to be +// moved. +// c. The selected entries will be moved. +// d. Usage table will be resized. +// e. Updated table will be saved. +// f. InvalidateEntry() will return NO_ERROR. // // Storage type Usage entries // at start at end // ============= ======== ====== // Secure Stop 1 0 0 -// Storage Type unknown 1 1 +// Storage Type Unknown 1 Deleted // Offline License 1 2 Deleted -// Offline License 2 3 3 -// Offline License 3 4 2 -// Storage Type unknown 5 Deleted -// Storage Type unknown 6 Deleted +// Offline License 2 3 2 (moved) +// Offline License 3 4 1 (moved) +// Storage Type Unknown 5 Deleted +// Storage Type Unknown 6 Deleted // // # of usage entries 7 4 -TEST_F(UsageTableHeaderTest, DeleteEntry_LastEntriesAreOfflineAndUnknknown) { - std::vector usage_entry_info_vector; - const CdmUsageEntryInfo usage_entry_info_array[] = { +TEST_F(UsageTableHeaderTest, + InvalidateEntry_LastEntriesAreOfflineAndUnknknown) { + const std::vector usage_entry_info_vector = { kUsageEntryInfoSecureStop1, kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoOfflineLicense1, kUsageEntryInfoOfflineLicense2, kUsageEntryInfoOfflineLicense3, kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoStorageTypeUnknown}; - ToVector(usage_entry_info_vector, usage_entry_info_array, - sizeof(usage_entry_info_array)); Init(kSecurityLevelL1, kUsageTableHeader, usage_entry_info_vector); - uint32_t usage_entry_number_to_be_deleted = - usage_entry_info_vector.size() - 5; // kUsageEntryInfoOfflineLicense1 - uint32_t last_valid_usage_entry_number = - usage_entry_info_vector.size() - 3; // kUsageEntryInfoOfflineLicense3 + const uint32_t usage_entry_number_to_be_deleted = + 2; // kUsageEntryInfoOfflineLicense1 metrics::CryptoMetrics metrics; - const DeviceFiles::CdmLicenseData stored_license_data{ - usage_entry_info_vector[last_valid_usage_entry_number].key_set_id, + // Expect calls for moving offline license 3 (position 4) to position 1. + const DeviceFiles::CdmLicenseData offline_license_3_data{ + kUsageEntryInfoOfflineLicense3.key_set_id, kActiveLicenseState, kPsshData, kKeyRequest, @@ -2361,186 +2407,719 @@ TEST_F(UsageTableHeaderTest, DeleteEntry_LastEntriesAreOfflineAndUnknknown) { kGracePeriodEndTime, kEmptyAppParameters, kUsageEntry, - last_valid_usage_entry_number}; - DeviceFiles::ResponseType sub_error_code; - EXPECT_TRUE( - device_files_->StoreLicense(stored_license_data, &sub_error_code)); - EXPECT_EQ(DeviceFiles::kNoError, sub_error_code); - - EXPECT_CALL(*crypto_session_, Open(kLevelDefault)) - .Times(2) - .WillRepeatedly(Return(NO_ERROR)); - EXPECT_CALL(*crypto_session_, - LoadUsageEntry(last_valid_usage_entry_number, kUsageEntry)) - .WillOnce(Return(NO_ERROR)); - EXPECT_CALL(*crypto_session_, - MoveUsageEntry(usage_entry_number_to_be_deleted)) - .WillOnce(Return(NO_ERROR)); - EXPECT_CALL(*crypto_session_, UpdateUsageEntry(NotNull(), NotNull())) - .WillOnce(DoAll(SetArgPointee<0>(kAnotherUsageTableHeader), - SetArgPointee<1>(kAnotherUsageEntry), Return(NO_ERROR))); - EXPECT_CALL(*crypto_session_, - ShrinkUsageTableHeader(last_valid_usage_entry_number, NotNull())) - .WillOnce( - DoAll(SetArgPointee<1>(kAnotherUsageTableHeader), Return(NO_ERROR))); - - EXPECT_CALL( - *device_files_, - StoreUsageTableInfo( - kAnotherUsageTableHeader, - UnorderedElementsAre( - kUsageEntryInfoSecureStop1, kUsageEntryInfoStorageTypeUnknown, - kUsageEntryInfoOfflineLicense3, kUsageEntryInfoOfflineLicense2))) - .WillOnce(Return(true)); - - EXPECT_CALL( - *device_files_, - StoreUsageTableInfo( - kAnotherUsageTableHeader, - UnorderedElementsAre( - kUsageEntryInfoSecureStop1, kUsageEntryInfoStorageTypeUnknown, - kUsageEntryInfoOfflineLicense3, kUsageEntryInfoOfflineLicense2, - kUsageEntryInfoOfflineLicense3, kUsageEntryInfoStorageTypeUnknown, - kUsageEntryInfoStorageTypeUnknown))) - .WillOnce(Return(true)); - // Expecting three calls to RetrieveLicense(), twice by usage table when - // swapping (probing for swap, and the actual swap_, then by test case - // to verify data is still there. + static_cast(4)}; EXPECT_CALL(*device_files_, RetrieveLicense(kUsageEntryInfoOfflineLicense3.key_set_id, NotNull(), NotNull())) - .Times(3); + .Times(2) // First to get entry, then again to update. + .WillRepeatedly( + DoAll(SetArgPointee<1>(offline_license_3_data), Return(true))); + EXPECT_CALL(*crypto_session_, LoadUsageEntry(4, kUsageEntry)) + .WillOnce(Return(NO_ERROR)); + EXPECT_CALL(*crypto_session_, MoveUsageEntry(1)).WillOnce(Return(NO_ERROR)); - EXPECT_EQ(NO_ERROR, - usage_table_header_->DeleteEntry(usage_entry_number_to_be_deleted, - device_files_, &metrics)); + // Expect calls for moving offline license 2 (position 3) to position 2. + const DeviceFiles::CdmLicenseData offline_license_2_data{ + kUsageEntryInfoOfflineLicense2.key_set_id, + kActiveLicenseState, + kPsshData, + kKeyRequest, + kKeyResponse, + kKeyRenewalRequest, + kKeyRenewalResponse, + kReleaseServerUrl, + kPlaybackStartTime, + kPlaybackStartTime + kPlaybackDuration, + kGracePeriodEndTime, + kEmptyAppParameters, + kUsageEntry, + static_cast(3)}; + EXPECT_CALL(*device_files_, + RetrieveLicense(kUsageEntryInfoOfflineLicense2.key_set_id, + NotNull(), NotNull())) + .Times(2) // First to get entry, then again to update. + .WillRepeatedly( + DoAll(SetArgPointee<1>(offline_license_2_data), Return(true))); + EXPECT_CALL(*crypto_session_, LoadUsageEntry(3, kUsageEntry)) + .WillOnce(Return(NO_ERROR)); + EXPECT_CALL(*crypto_session_, MoveUsageEntry(2)).WillOnce(Return(NO_ERROR)); - DeviceFiles::CdmLicenseData retrieved_license_data; - EXPECT_TRUE( - device_files_->RetrieveLicense(kUsageEntryInfoOfflineLicense3.key_set_id, - &retrieved_license_data, &sub_error_code)); + // Common to both moves. + EXPECT_CALL(*crypto_session_, Open(kLevelDefault)) + .Times(2) + .WillRepeatedly(Return(NO_ERROR)); + EXPECT_CALL(*device_files_, StoreLicense(_, NotNull())) + .Times(2) + .WillRepeatedly(Return(true)); + EXPECT_CALL(*crypto_session_, UpdateUsageEntry(NotNull(), NotNull())) + .Times(2) + .WillRepeatedly(DoAll(SetArgPointee<0>(kAnotherUsageTableHeader), + SetArgPointee<1>(kAnotherUsageEntry), + Return(NO_ERROR))); + EXPECT_CALL(*device_files_, StoreUsageTableInfo(kAnotherUsageTableHeader, _)) + .Times(2) + .WillRepeatedly(Return(true)); - EXPECT_EQ(kActiveLicenseState, retrieved_license_data.state); - EXPECT_EQ(kPsshData, retrieved_license_data.pssh_data); - EXPECT_EQ(kKeyRequest, retrieved_license_data.license_request); - EXPECT_EQ(kKeyResponse, retrieved_license_data.license); - EXPECT_EQ(kKeyRenewalRequest, retrieved_license_data.license_renewal_request); - EXPECT_EQ(kKeyRenewalResponse, retrieved_license_data.license_renewal); - EXPECT_EQ(kReleaseServerUrl, retrieved_license_data.release_server_url); - EXPECT_EQ(kPlaybackStartTime, retrieved_license_data.playback_start_time); - EXPECT_EQ(kPlaybackStartTime + kPlaybackDuration, - retrieved_license_data.last_playback_time); - EXPECT_EQ(kGracePeriodEndTime, retrieved_license_data.grace_period_end_time); - EXPECT_EQ(kEmptyAppParameters.size(), - retrieved_license_data.app_parameters.size()); - EXPECT_EQ(kAnotherUsageEntry, retrieved_license_data.usage_entry); - EXPECT_EQ(usage_entry_number_to_be_deleted, - retrieved_license_data.usage_entry_number); - EXPECT_EQ(DeviceFiles::kNoError, sub_error_code); + // Shrink to contain the remaining valid entry. + EXPECT_CALL(*crypto_session_, + ShrinkUsageTableHeader(kLevelDefault, 3, NotNull())) + .WillOnce( + DoAll(SetArgPointee<2>(kYetAnotherUsageEntry), Return(NO_ERROR))); + EXPECT_CALL(*device_files_, + StoreUsageTableInfo(kYetAnotherUsageEntry, + ElementsAre(kUsageEntryInfoSecureStop1, + kUsageEntryInfoOfflineLicense3, + kUsageEntryInfoOfflineLicense2))) + .WillOnce(Return(true)); + + EXPECT_EQ(NO_ERROR, usage_table_header_->InvalidateEntry( + usage_entry_number_to_be_deleted, true, device_files_, + &metrics)); + // Check the end state of the usage table. + constexpr size_t expected_size = 3; + EXPECT_EQ(expected_size, usage_table_header_->usage_entry_info().size()); } // Initial Test state: -// 1. Usage entry to be deleted is not last. +// 1. Usage entry to be deleted is not last (Secure stop 1) // 2. Last few entries are of storage type unknown. -// 3. Entry that preceeds those in (2) is a secure stop. // // Attempting to delete the entry in (1) will result in: -// a. The entry being deleted and replaced with the secure stop entry in (3). -// b. The entries with unknown storage type in (2) will be deleted. +// a. The entry will be marked as kStorageTypeUnknown. +// b. While defragging, secure stop 2 and 3 will be selected to be +// moved. +// c. The selected entries will be moved. +// d. Usage table will be resized. +// e. Updated table will be saved. +// f. InvalidateEntry() will return NO_ERROR. // // Storage type Usage entries // at start at end // ============= ======== ====== // Offline License 1 0 0 -// Storage Type unknown 1 1 +// Storage Type unknown 1 Deleted // Secure stop 1 2 Deleted -// Secure stop 2 3 3 -// Secure stop 3 4 2 +// Secure stop 2 3 2 (Moved) +// Secure stop 3 4 1 (Moved) // Storage Type unknown 5 Deleted // Storage Type unknown 6 Deleted // -// # of usage entries 7 4 -TEST_F(UsageTableHeaderTest, DeleteEntry_LastEntriesAreSecureStopAndUnknknown) { - std::vector usage_entry_info_vector; - const CdmUsageEntryInfo usage_entry_info_array[] = { +// # of usage entries 7 3 +TEST_F(UsageTableHeaderTest, + InvalidateEntry_LastEntriesAreSecureStopAndUnknknown) { + const std::vector usage_entry_info_vector = { kUsageEntryInfoOfflineLicense1, kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoSecureStop1, kUsageEntryInfoSecureStop2, kUsageEntryInfoSecureStop3, kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoStorageTypeUnknown}; - ToVector(usage_entry_info_vector, usage_entry_info_array, - sizeof(usage_entry_info_array)); Init(kSecurityLevelL1, kUsageTableHeader, usage_entry_info_vector); - uint32_t usage_entry_number_to_be_deleted = - usage_entry_info_vector.size() - 5; // kUsageEntryInfoSecureStop1 - uint32_t last_valid_usage_entry_number = - usage_entry_info_vector.size() - 3; // kUsageEntryInfoSecureStop3 + const uint32_t usage_entry_number_to_be_deleted = + 2; // kUsageEntryInfoSecureStop1 metrics::CryptoMetrics metrics; - EXPECT_CALL(*crypto_session_, Open(kLevelDefault)) - .Times(2) - .WillRepeatedly(Return(NO_ERROR)); - EXPECT_CALL(*crypto_session_, - LoadUsageEntry(last_valid_usage_entry_number, kUsageEntry)) - .WillOnce(Return(NO_ERROR)); - EXPECT_CALL(*crypto_session_, - MoveUsageEntry(usage_entry_number_to_be_deleted)) - .WillOnce(Return(NO_ERROR)); - EXPECT_CALL(*crypto_session_, UpdateUsageEntry(NotNull(), NotNull())) - .WillOnce(DoAll(SetArgPointee<0>(kAnotherUsageTableHeader), - SetArgPointee<1>(kAnotherUsageEntry), Return(NO_ERROR))); - EXPECT_CALL(*crypto_session_, - ShrinkUsageTableHeader(last_valid_usage_entry_number, NotNull())) - .WillOnce( - DoAll(SetArgPointee<1>(kAnotherUsageTableHeader), Return(NO_ERROR))); - + // Expect calls for moving streaming license 3 (position 4) to position 1. EXPECT_CALL(*device_files_, RetrieveUsageInfoByKeySetId( kUsageEntryInfoSecureStop3.usage_info_file_name, kUsageEntryInfoSecureStop3.key_set_id, NotNull(), NotNull(), NotNull(), NotNull(), NotNull())) - .WillRepeatedly( - DoAll(SetArgPointee<2>(kProviderSessionToken), - SetArgPointee<3>(kKeyRequest), SetArgPointee<4>(kKeyResponse), - SetArgPointee<5>(kUsageEntry), - SetArgPointee<6>(last_valid_usage_entry_number), Return(true))); + .Times(2) // First to get entry, second to update. + .WillRepeatedly(DoAll( + SetArgPointee<2>(kProviderSessionToken), + SetArgPointee<3>(kKeyRequest), SetArgPointee<4>(kKeyResponse), + SetArgPointee<5>(kUsageEntry), SetArgPointee<6>(4), Return(true))); + EXPECT_CALL( + *device_files_, + DeleteUsageInfo(kUsageEntryInfoSecureStop3.usage_info_file_name, _)) + .WillOnce(Return(true)); + EXPECT_CALL( + *device_files_, + StoreUsageInfo(_, _, _, kUsageEntryInfoSecureStop3.usage_info_file_name, + kUsageEntryInfoSecureStop3.key_set_id, _, 1)) + .WillOnce(Return(true)); + EXPECT_CALL(*crypto_session_, LoadUsageEntry(4, kUsageEntry)) + .WillOnce(Return(NO_ERROR)); + EXPECT_CALL(*crypto_session_, MoveUsageEntry(1)); + + // Expect calls for moving streaming license 2 (position 3) to position 2. + EXPECT_CALL(*device_files_, + RetrieveUsageInfoByKeySetId( + kUsageEntryInfoSecureStop2.usage_info_file_name, + kUsageEntryInfoSecureStop2.key_set_id, NotNull(), NotNull(), + NotNull(), NotNull(), NotNull())) + .Times(2) // First to get entry, second to update. + .WillRepeatedly(DoAll( + SetArgPointee<2>(kProviderSessionToken), + SetArgPointee<3>(kKeyRequest), SetArgPointee<4>(kKeyResponse), + SetArgPointee<5>(kUsageEntry), SetArgPointee<6>(3), Return(true))); + EXPECT_CALL( + *device_files_, + DeleteUsageInfo(kUsageEntryInfoSecureStop2.usage_info_file_name, _)) + .WillOnce(Return(true)); + EXPECT_CALL( + *device_files_, + StoreUsageInfo(_, _, _, kUsageEntryInfoSecureStop2.usage_info_file_name, + kUsageEntryInfoSecureStop2.key_set_id, _, 2)) + .WillOnce(Return(true)); + EXPECT_CALL(*crypto_session_, LoadUsageEntry(3, kUsageEntry)) + .WillOnce(Return(NO_ERROR)); + EXPECT_CALL(*crypto_session_, MoveUsageEntry(2)); + + // Common to both moves. + EXPECT_CALL(*crypto_session_, Open(kLevelDefault)) + .Times(2) + .WillRepeatedly(Return(NO_ERROR)); + EXPECT_CALL(*crypto_session_, UpdateUsageEntry(NotNull(), NotNull())) + .Times(2) + .WillRepeatedly(DoAll(SetArgPointee<0>(kAnotherUsageTableHeader), + SetArgPointee<1>(kAnotherUsageEntry), + Return(NO_ERROR))); + EXPECT_CALL(*device_files_, StoreUsageTableInfo(kAnotherUsageTableHeader, _)) + .Times(2) + .WillRepeatedly(Return(true)); + + // Shrink to contain the remaining valid entry. + EXPECT_CALL(*crypto_session_, + ShrinkUsageTableHeader(kLevelDefault, 3, NotNull())) + .WillOnce( + DoAll(SetArgPointee<2>(kYetAnotherUsageEntry), Return(NO_ERROR))); EXPECT_CALL(*device_files_, - DeleteUsageInfo(kUsageEntryInfoSecureStop3.usage_info_file_name, - kProviderSessionToken)) + StoreUsageTableInfo(kYetAnotherUsageEntry, + ElementsAre(kUsageEntryInfoOfflineLicense1, + kUsageEntryInfoSecureStop3, + kUsageEntryInfoSecureStop2))) .WillOnce(Return(true)); + EXPECT_EQ(NO_ERROR, usage_table_header_->InvalidateEntry( + usage_entry_number_to_be_deleted, true, device_files_, + &metrics)); + // Check the end state of the usage table. + constexpr size_t expected_size = 3; + EXPECT_EQ(expected_size, usage_table_header_->usage_entry_info().size()); +} + +// Initial Test state: +// 1. Usage entry to be deleted is not last (Secure stop 1) +// 2. All other entries are invalid. +// +// Attempting to delete the entry in (1) will result in: +// a. The entry will be marked as kStorageTypeUnknown. +// b. Defrag is skipped (no calls to Move()). +// c. Usage table will be resized. +// e. Updated table will be saved. +// f. InvalidateEntry() will return NO_ERROR. +// +// Storage type Usage entries +// at start at end +// ============= ======== ====== +// Storage Type Unknown 0 Deleted +// Storage Type Unknown 1 Deleted +// Storage Type Unknown 2 Deleted +// Secure stop 1 3 Deleted +// Storage Type Unknown 4 Deleted +// Storage Type Unknown 5 Deleted +// Storage Type Unknown 6 Deleted +// +// # of usage entries 7 0 +TEST_F(UsageTableHeaderTest, InvalidateEntry_NoValidSessionsAfter) { + const std::vector usage_entry_info_vector = { + kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoStorageTypeUnknown, + kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoSecureStop1, + kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoStorageTypeUnknown, + kUsageEntryInfoStorageTypeUnknown}; + + Init(kSecurityLevelL1, kUsageTableHeader, usage_entry_info_vector); + const uint32_t usage_entry_number_to_be_deleted = + 3; // kUsageEntryInfoSecureStop1 + metrics::CryptoMetrics metrics; + + // No calls related to defragging, just shrinking the table and save. + EXPECT_CALL(*crypto_session_, + ShrinkUsageTableHeader(kLevelDefault, 0, NotNull())) + .WillOnce( + DoAll(SetArgPointee<2>(kAnotherUsageTableHeader), Return(NO_ERROR))); + + EXPECT_CALL(*device_files_, StoreUsageTableInfo(kAnotherUsageTableHeader, + kEmptyUsageEntryInfoVector)) + .WillOnce(Return(true)); + + EXPECT_EQ(NO_ERROR, usage_table_header_->InvalidateEntry( + usage_entry_number_to_be_deleted, true, device_files_, + &metrics)); + EXPECT_TRUE(usage_table_header_->usage_entry_info().empty()); +} + +// 1. Usage entry to be deleted is last valid entry (Secure stop 1) +// 2. There exists an entry to be moved (Offline License 1) +// 3. OEMCrypto is at max sessions, and any attempt to open a new session +// will fail with INSUFFICIENT_CRYPTO_RESOURCES. +// +// Attempting to delete the entry in (1) will result in: +// a. The entry will be marked as kStorageTypeUnknown. +// b. Only remaining entry is selected for move (Offline license 1) +// c. Opening session for move will fail. +// d. Defrag is aborted; but shrink is still made up to last valid. +// e. Usage table will be resized. +// f. Updated table will be saved. +// g. InvalidateEntry() will return NO_ERROR. +// +// Storage type Usage entries +// at start at end +// ============= ======== ====== +// Storage Type Unknown 0 0 +// Offline License 1 1 1 +// Secure Stop 1 2 Deleted +// Storage Type Unknown 3 Deleted +// +// # of usage entries 4 2 +TEST_F(UsageTableHeaderTest, InvalidateEntry_MaxSessionReached) { + const std::vector usage_entry_info_vector = { + kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoOfflineLicense1, + kUsageEntryInfoSecureStop1, kUsageEntryInfoStorageTypeUnknown}; + + Init(kSecurityLevelL1, kUsageTableHeader, usage_entry_info_vector); + const uint32_t usage_entry_number_to_be_deleted = + 2; // kUsageEntryInfoSecureStop1 + metrics::CryptoMetrics metrics; + + // Expected calls for moving offline license 1 (position 1) to position 0. + // But will fail when opening a crypto session. + const DeviceFiles::CdmLicenseData offline_license_1_data{ + kUsageEntryInfoOfflineLicense1.key_set_id, + kActiveLicenseState, + kPsshData, + kKeyRequest, + kKeyResponse, + kKeyRenewalRequest, + kKeyRenewalResponse, + kReleaseServerUrl, + kPlaybackStartTime, + kPlaybackStartTime + kPlaybackDuration, + kGracePeriodEndTime, + kEmptyAppParameters, + kUsageEntry, + static_cast(1)}; + EXPECT_CALL(*device_files_, + RetrieveLicense(kUsageEntryInfoOfflineLicense1.key_set_id, + NotNull(), NotNull())) + .WillOnce(DoAll(SetArgPointee<1>(offline_license_1_data), Return(true))); + EXPECT_CALL(*crypto_session_, Open(kLevelDefault)) + .WillRepeatedly(Return(INSUFFICIENT_CRYPTO_RESOURCES)); + + // Despite being unable to open session, the table should be resized to + // exclude the trailing invalid entries. + EXPECT_CALL(*crypto_session_, + ShrinkUsageTableHeader(kLevelDefault, 2, NotNull())) + .WillOnce( + DoAll(SetArgPointee<2>(kAnotherUsageTableHeader), Return(NO_ERROR))); + EXPECT_CALL(*device_files_, + StoreUsageTableInfo(kAnotherUsageTableHeader, + ElementsAre(kUsageEntryInfoStorageTypeUnknown, + kUsageEntryInfoOfflineLicense1))) + .WillOnce(Return(true)); + + EXPECT_EQ(NO_ERROR, usage_table_header_->InvalidateEntry( + usage_entry_number_to_be_deleted, true, device_files_, + &metrics)); + // Check the end state of the usage table. + constexpr size_t expected_size = 2; + EXPECT_EQ(expected_size, usage_table_header_->usage_entry_info().size()); +} + +// 1. Usage entry to be deleted is first valid entry (Secure stop 1) +// 2. There exists an entry to be moved (Offline License 1) +// 3. OEMCrypto is at max sessions, and any attempt to open a new session +// will fail with INSUFFICIENT_CRYPTO_RESOURCES. +// +// Attempting to delete the entry in (1) will result in: +// a. The entry will be marked as kStorageTypeUnknown. +// b. Only remaining entry is selected for move (Offline license 1) +// c. Opening session for move will fail. +// d. Defrag is aborted; but shrink is still made up to last valid. +// e. Usage table will be resized. +// f. Updated table will be saved. +// g. InvalidateEntry() will return NO_ERROR. +// +// Storage type Usage entries +// at start at end +// ============= ======== ====== +// Secure Stop 1 0 0 (Storage type unknown) +// Offline License 1 1 1 (Failed to move) +// Storage Type Unknown 2 Deleted +// Storage Type Unknown 3 Deleted +// +// # of usage entries 4 2 +TEST_F(UsageTableHeaderTest, InvalidateEntry_FirstEntry_MaxSessionReached) { + const std::vector usage_entry_info_vector = { + kUsageEntryInfoSecureStop1, kUsageEntryInfoOfflineLicense1, + kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoStorageTypeUnknown}; + + Init(kSecurityLevelL1, kUsageTableHeader, usage_entry_info_vector); + const uint32_t usage_entry_number_to_be_deleted = + 0; // kUsageEntryInfoSecureStop1 + metrics::CryptoMetrics metrics; + + // Expected calls for moving offline license 1 (position 1) to position 0. + // But will fail when opening a crypto session. + const DeviceFiles::CdmLicenseData offline_license_1_data{ + kUsageEntryInfoOfflineLicense1.key_set_id, + kActiveLicenseState, + kPsshData, + kKeyRequest, + kKeyResponse, + kKeyRenewalRequest, + kKeyRenewalResponse, + kReleaseServerUrl, + kPlaybackStartTime, + kPlaybackStartTime + kPlaybackDuration, + kGracePeriodEndTime, + kEmptyAppParameters, + kUsageEntry, + static_cast(1)}; + EXPECT_CALL(*device_files_, + RetrieveLicense(kUsageEntryInfoOfflineLicense1.key_set_id, + NotNull(), NotNull())) + .WillOnce(DoAll(SetArgPointee<1>(offline_license_1_data), Return(true))); + EXPECT_CALL(*crypto_session_, Open(kLevelDefault)) + .WillRepeatedly(Return(INSUFFICIENT_CRYPTO_RESOURCES)); + + // Despite being unable to open session, the table should be resized to + // exclude the trailing invalid entries. + EXPECT_CALL(*crypto_session_, + ShrinkUsageTableHeader(kLevelDefault, 2, NotNull())) + .WillOnce( + DoAll(SetArgPointee<2>(kAnotherUsageTableHeader), Return(NO_ERROR))); + EXPECT_CALL(*device_files_, + StoreUsageTableInfo(kAnotherUsageTableHeader, + ElementsAre(kUsageEntryInfoStorageTypeUnknown, + kUsageEntryInfoOfflineLicense1))) + .WillOnce(Return(true)); + + EXPECT_EQ(NO_ERROR, usage_table_header_->InvalidateEntry( + usage_entry_number_to_be_deleted, true, device_files_, + &metrics)); + // Check the end state of the usage table. + constexpr size_t expected_size = 2; + EXPECT_EQ(expected_size, usage_table_header_->usage_entry_info().size()); +} + +// 1. Usage entry to be deleted is last valid entry (Secure stop 1) +// 2. There exists an entry to be moved (Offline License 1) +// 3. Moving entry will result in a system invalidation error. +// +// Attempting to delete the entry in (1) will result in: +// a. The entry will be marked as kStorageTypeUnknown. +// b. Only remaining entry is selected for move (Offline license 1) +// c. Moving entry will result in system invalidation. +// d. Defrag is aborted; no call to Shrink() +// f. Updated table will be saved. +// g. InvalidateEntry() will return SYSTEM_INVALIDATED_ERROR. +// +// Storage type Usage entries +// at start at end +// ============= ======== ====== +// Storage Type Unknown 0 0 +// Offline License 1 1 1 +// Secure Stop 1 2 2 (Storage Type Unknown) +// Storage Type Unknown 3 3 +// +// # of usage entries 4 4 +TEST_F(UsageTableHeaderTest, InvalidateEntry_SystemInvalidation_OnMove) { + const std::vector usage_entry_info_vector = { + kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoOfflineLicense1, + kUsageEntryInfoSecureStop1, kUsageEntryInfoStorageTypeUnknown}; + + Init(kSecurityLevelL1, kUsageTableHeader, usage_entry_info_vector); + const uint32_t usage_entry_number_to_be_deleted = + 2; // kUsageEntryInfoSecureStop1 + metrics::CryptoMetrics metrics; + + // Expected calls for moving offline license 1 (position 1) to position 0. + // But will fail when moving. + const DeviceFiles::CdmLicenseData offline_license_1_data{ + kUsageEntryInfoOfflineLicense1.key_set_id, + kActiveLicenseState, + kPsshData, + kKeyRequest, + kKeyResponse, + kKeyRenewalRequest, + kKeyRenewalResponse, + kReleaseServerUrl, + kPlaybackStartTime, + kPlaybackStartTime + kPlaybackDuration, + kGracePeriodEndTime, + kEmptyAppParameters, + kUsageEntry, + static_cast(1)}; + EXPECT_CALL(*device_files_, + RetrieveLicense(kUsageEntryInfoOfflineLicense1.key_set_id, + NotNull(), NotNull())) + .WillOnce(DoAll(SetArgPointee<1>(offline_license_1_data), Return(true))); + EXPECT_CALL(*crypto_session_, Open(kLevelDefault)).WillOnce(Return(NO_ERROR)); + EXPECT_CALL(*crypto_session_, LoadUsageEntry(1, kUsageEntry)); + EXPECT_CALL(*crypto_session_, MoveUsageEntry(0)) + .WillOnce(Return(SYSTEM_INVALIDATED_ERROR)); + + // Defrag is aborted, and table is saved, but no call to shrink(). EXPECT_CALL( *device_files_, - StoreUsageInfo(kProviderSessionToken, kKeyRequest, kKeyResponse, - kUsageEntryInfoSecureStop3.usage_info_file_name, - kUsageEntryInfoSecureStop3.key_set_id, kAnotherUsageEntry, - usage_entry_number_to_be_deleted)) + StoreUsageTableInfo(kUsageTableHeader, + ElementsAre(kUsageEntryInfoStorageTypeUnknown, + kUsageEntryInfoOfflineLicense1, + kUsageEntryInfoStorageTypeUnknown, + kUsageEntryInfoStorageTypeUnknown))) .WillOnce(Return(true)); + EXPECT_EQ(SYSTEM_INVALIDATED_ERROR, usage_table_header_->InvalidateEntry( + usage_entry_number_to_be_deleted, + true, device_files_, &metrics)); + // Check the end state of the usage table. + constexpr size_t expected_size = 4; + EXPECT_EQ(expected_size, usage_table_header_->usage_entry_info().size()); +} + +// 1. Usage entry to be deleted is last valid entry (Secure stop 1) +// 2. There exists an entry to be moved (Offline License 1) +// 3. Moving entry will result in a session invalidation error. +// +// Note: This is very similar to InvalidateEntry_SystemInvalidation_OnMove +// except that the error returned is not of importance to the calling +// session and is ignored (NO_ERROR returned). +// +// Attempting to delete the entry in (1) will result in: +// a. The entry will be marked as kStorageTypeUnknown. +// b. Only remaining entry is selected for move (Offline license 1) +// c. Moving entry will result in session invalidation. +// d. Defrag is aborted; no call to Shrink() +// f. Updated table will be saved. +// g. InvalidateEntry() will return NO_ERROR. +// +// Storage type Usage entries +// at start at end +// ============= ======== ====== +// Storage Type Unknown 0 0 +// Offline License 1 1 1 +// Secure Stop 1 2 2 (Storage Type Unknown) +// Storage Type Unknown 3 3 +// +// # of usage entries 4 4 +TEST_F(UsageTableHeaderTest, InvalidateEntry_SessionInvalidation_OnMove) { + const std::vector usage_entry_info_vector = { + kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoOfflineLicense1, + kUsageEntryInfoSecureStop1, kUsageEntryInfoStorageTypeUnknown}; + + Init(kSecurityLevelL1, kUsageTableHeader, usage_entry_info_vector); + const uint32_t usage_entry_number_to_be_deleted = + 2; // kUsageEntryInfoSecureStop1 + metrics::CryptoMetrics metrics; + + // Expected calls for moving offline license 1 (position 1) to position 0. + // But will fail when moving. + const DeviceFiles::CdmLicenseData offline_license_1_data{ + kUsageEntryInfoOfflineLicense1.key_set_id, + kActiveLicenseState, + kPsshData, + kKeyRequest, + kKeyResponse, + kKeyRenewalRequest, + kKeyRenewalResponse, + kReleaseServerUrl, + kPlaybackStartTime, + kPlaybackStartTime + kPlaybackDuration, + kGracePeriodEndTime, + kEmptyAppParameters, + kUsageEntry, + static_cast(1)}; + EXPECT_CALL(*device_files_, + RetrieveLicense(kUsageEntryInfoOfflineLicense1.key_set_id, + NotNull(), NotNull())) + .WillOnce(DoAll(SetArgPointee<1>(offline_license_1_data), Return(true))); + EXPECT_CALL(*crypto_session_, Open(kLevelDefault)).WillOnce(Return(NO_ERROR)); + EXPECT_CALL(*crypto_session_, LoadUsageEntry(1, kUsageEntry)); + EXPECT_CALL(*crypto_session_, MoveUsageEntry(0)) + .WillOnce(Return(SESSION_LOST_STATE_ERROR)); + + // Defrag is aborted, and table is saved, but no call to shrink(). EXPECT_CALL( *device_files_, - StoreUsageTableInfo( - kAnotherUsageTableHeader, - UnorderedElementsAre( - kUsageEntryInfoOfflineLicense1, kUsageEntryInfoStorageTypeUnknown, - kUsageEntryInfoSecureStop3, kUsageEntryInfoSecureStop2))) + StoreUsageTableInfo(kUsageTableHeader, + ElementsAre(kUsageEntryInfoStorageTypeUnknown, + kUsageEntryInfoOfflineLicense1, + kUsageEntryInfoStorageTypeUnknown, + kUsageEntryInfoStorageTypeUnknown))) .WillOnce(Return(true)); + // The underlying error should not be returned to the caller. + EXPECT_EQ(NO_ERROR, usage_table_header_->InvalidateEntry( + usage_entry_number_to_be_deleted, true, device_files_, + &metrics)); + // Check the end state of the usage table. + constexpr size_t expected_size = 4; + EXPECT_EQ(expected_size, usage_table_header_->usage_entry_info().size()); +} + +// 1. Usage entry to be deleted is last valid entry (Secure stop 1) +// 2. There exists an entry to be moved (Offline License 1) +// 3. Shrinking table will fail due to an unspecified entry being in use. +// +// Attempting to delete the entry in (1) will result in: +// a. The entry will be marked as kStorageTypeUnknown. +// b. Only remaining entry is selected for move (Offline license 1) +// c. Entry will be moved successfully. +// d. Shrinking table fill fail with SHRINK_USAGE_TABLE_HEADER_ENTRY_IN_USE. +// f. Updated table will be saved, with trailing invalid entries. +// g. InvalidateEntry() will return NO_ERROR. +// +// Storage type Usage entries +// at start at end +// ============= ======== ====== +// Storage Type Unknown 0 1 (swapped Offline License 1) +// Offline License 1 1 0 (moved) +// Secure Stop 1 2 2 (Storage Type Unknown) +// Storage Type Unknown 3 3 +// +// # of usage entries 4 4 +TEST_F(UsageTableHeaderTest, InvalidateEntry_ShrinkFails) { + const std::vector usage_entry_info_vector = { + kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoOfflineLicense1, + kUsageEntryInfoSecureStop1, kUsageEntryInfoStorageTypeUnknown}; + + Init(kSecurityLevelL1, kUsageTableHeader, usage_entry_info_vector); + const uint32_t usage_entry_number_to_be_deleted = + 2; // kUsageEntryInfoSecureStop1 + metrics::CryptoMetrics metrics; + + // Expected calls for moving offline license 1 (position 1) to position 0. + const DeviceFiles::CdmLicenseData offline_license_1_data{ + kUsageEntryInfoOfflineLicense1.key_set_id, + kActiveLicenseState, + kPsshData, + kKeyRequest, + kKeyResponse, + kKeyRenewalRequest, + kKeyRenewalResponse, + kReleaseServerUrl, + kPlaybackStartTime, + kPlaybackStartTime + kPlaybackDuration, + kGracePeriodEndTime, + kEmptyAppParameters, + kUsageEntry, + static_cast(1)}; + EXPECT_CALL(*device_files_, + RetrieveLicense(kUsageEntryInfoOfflineLicense1.key_set_id, + NotNull(), NotNull())) + .Times(2) // First to get entry, then again to update. + .WillRepeatedly( + DoAll(SetArgPointee<1>(offline_license_1_data), Return(true))); + EXPECT_CALL(*crypto_session_, Open(kLevelDefault)).WillOnce(Return(NO_ERROR)); + EXPECT_CALL(*crypto_session_, LoadUsageEntry(1, kUsageEntry)); + EXPECT_CALL(*crypto_session_, MoveUsageEntry(0)).WillOnce(Return(NO_ERROR)); + EXPECT_CALL(*crypto_session_, UpdateUsageEntry(NotNull(), NotNull())) + .WillOnce(DoAll(SetArgPointee<0>(kAnotherUsageTableHeader), + SetArgPointee<1>(kUsageEntry), Return(NO_ERROR))); EXPECT_CALL( *device_files_, - StoreUsageTableInfo( - kAnotherUsageTableHeader, - UnorderedElementsAre( - kUsageEntryInfoOfflineLicense1, kUsageEntryInfoStorageTypeUnknown, - kUsageEntryInfoSecureStop3, kUsageEntryInfoSecureStop2, - kUsageEntryInfoSecureStop3, kUsageEntryInfoStorageTypeUnknown, - kUsageEntryInfoStorageTypeUnknown))) + StoreUsageTableInfo(kAnotherUsageTableHeader, + ElementsAre(kUsageEntryInfoOfflineLicense1, + kUsageEntryInfoStorageTypeUnknown, + kUsageEntryInfoStorageTypeUnknown, + kUsageEntryInfoStorageTypeUnknown))) + .WillOnce(Return(true)); + EXPECT_CALL(*device_files_, StoreLicense(_, NotNull())) .WillOnce(Return(true)); - EXPECT_EQ(NO_ERROR, - usage_table_header_->DeleteEntry(usage_entry_number_to_be_deleted, - device_files_, &metrics)); + // Expect a call to shrink table, but will fail due to an unspecified + // entry being in use. + EXPECT_CALL(*crypto_session_, + ShrinkUsageTableHeader(kLevelDefault, 1, NotNull())) + .WillOnce(Return(SHRINK_USAGE_TABLE_HEADER_ENTRY_IN_USE)); + + EXPECT_EQ(NO_ERROR, usage_table_header_->InvalidateEntry( + usage_entry_number_to_be_deleted, true, device_files_, + &metrics)); + // Check the end state of the usage table. + constexpr size_t expected_size = 4; + EXPECT_EQ(expected_size, usage_table_header_->usage_entry_info().size()); +} + +// 1. Usage entry to be deleted is last valid entry (Secure stop 1) +// 2. There exists an entry to be moved (Offline License 1) +// 3. Moving entry cannot be done as the destination is in use. +// (this is unexpected but possible in normal operation) +// +// Attempting to delete the entry in (1) will result in: +// a. The entry will be marked as kStorageTypeUnknown. +// b. Only remaining entry is selected for move (Offline license 1) +// c. Call to move will fail with MOVE_USAGE_ENTRY_DESTINATION_IN_USE. +// d. Usage table will be resized. +// e. Updated table will be saved. +// f. InvalidateEntry() will return NO_ERROR. +// +// Storage type Usage entries +// at start at end +// ============= ======== ====== +// Storage Type Unknown 0 0 +// Offline License 1 1 1 (unable to move) +// Secure Stop 1 2 Deleted +// Storage Type Unknown 3 Deleted +// +// # of usage entries 4 2 +TEST_F(UsageTableHeaderTest, InvalidateEntry_DestinationInUse_OnMove) { + const std::vector usage_entry_info_vector = { + kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoOfflineLicense1, + kUsageEntryInfoSecureStop1, kUsageEntryInfoStorageTypeUnknown}; + + Init(kSecurityLevelL1, kUsageTableHeader, usage_entry_info_vector); + const uint32_t usage_entry_number_to_be_deleted = + 2; // kUsageEntryInfoSecureStop1 + metrics::CryptoMetrics metrics; + + // Expected calls for moving offline license 1 (position 1) to position 0. + // But will fail due to the destination being in use. + const DeviceFiles::CdmLicenseData offline_license_1_data{ + kUsageEntryInfoOfflineLicense1.key_set_id, + kActiveLicenseState, + kPsshData, + kKeyRequest, + kKeyResponse, + kKeyRenewalRequest, + kKeyRenewalResponse, + kReleaseServerUrl, + kPlaybackStartTime, + kPlaybackStartTime + kPlaybackDuration, + kGracePeriodEndTime, + kEmptyAppParameters, + kUsageEntry, + static_cast(1)}; + EXPECT_CALL(*device_files_, + RetrieveLicense(kUsageEntryInfoOfflineLicense1.key_set_id, + NotNull(), NotNull())) + .WillOnce(DoAll(SetArgPointee<1>(offline_license_1_data), Return(true))); + EXPECT_CALL(*crypto_session_, Open(kLevelDefault)).WillOnce(Return(NO_ERROR)); + EXPECT_CALL(*crypto_session_, LoadUsageEntry(1, kUsageEntry)); + EXPECT_CALL(*crypto_session_, MoveUsageEntry(0)) + .WillOnce(Return(MOVE_USAGE_ENTRY_DESTINATION_IN_USE)); + // No expectations for updating the entry or the header from the move + // operation. + + // Shrink table down to the last valid entry. + EXPECT_CALL(*crypto_session_, + ShrinkUsageTableHeader(kLevelDefault, 2, NotNull())) + .WillOnce(DoAll(SetArgPointee<2>(kAnotherUsageEntry), Return(NO_ERROR))); + EXPECT_CALL(*device_files_, + StoreUsageTableInfo(kAnotherUsageEntry, + ElementsAre(kUsageEntryInfoStorageTypeUnknown, + kUsageEntryInfoOfflineLicense1))) + .WillOnce(Return(true)); + + EXPECT_EQ(NO_ERROR, usage_table_header_->InvalidateEntry( + usage_entry_number_to_be_deleted, true, device_files_, + &metrics)); + // Check the end state of the usage table. + constexpr size_t expected_size = 2; + EXPECT_EQ(expected_size, usage_table_header_->usage_entry_info().size()); } // If the crypto session says the usage table header is stale, init should fail. @@ -2557,11 +3136,13 @@ TEST_F(UsageTableHeaderTest, StaleHeader) { .WillOnce(DoAll(SetArgPointee<0>(kUsageTableHeader), SetArgPointee<1>(usage_entry_info_vector), SetArgPointee<2>(false), Return(true))); - EXPECT_CALL(*crypto_session_, LoadUsageTableHeader(kUsageTableHeader)) + EXPECT_CALL(*crypto_session_, + LoadUsageTableHeader(kLevelDefault, kUsageTableHeader)) .WillOnce(Return(LOAD_USAGE_HEADER_GENERATION_SKEW)); - EXPECT_CALL(*crypto_session_, CreateUsageTableHeader(NotNull())) + EXPECT_CALL(*crypto_session_, + CreateUsageTableHeader(kLevelDefault, NotNull())) .WillOnce( - DoAll(SetArgPointee<0>(kEmptyUsageTableHeader), Return(NO_ERROR))); + DoAll(SetArgPointee<1>(kEmptyUsageTableHeader), Return(NO_ERROR))); EXPECT_CALL(*device_files_, DeleteAllLicenses()).WillOnce(Return(true)); EXPECT_CALL(*device_files_, DeleteAllUsageInfo()).WillOnce(Return(true)); EXPECT_CALL(*device_files_, DeleteUsageTableInfo()).WillOnce(Return(true)); @@ -2580,7 +3161,9 @@ TEST_F(UsageTableHeaderTest, Shrink_NoneOfTable) { // These calls are "expensive" and should be avoided if possible. EXPECT_CALL(*crypto_session_, Open(_)).Times(0); - EXPECT_CALL(*crypto_session_, ShrinkUsageTableHeader(_, NotNull())).Times(0); + EXPECT_CALL(*crypto_session_, + ShrinkUsageTableHeader(kLevelDefault, _, NotNull())) + .Times(0); EXPECT_CALL(*device_files_, StoreUsageTableInfo(_, _)).Times(0); EXPECT_EQ(usage_table_header_->Shrink(&metrics, 0), NO_ERROR); @@ -2595,9 +3178,9 @@ TEST_F(UsageTableHeaderTest, Shrink_PartOfTable) { k10UsageEntryInfoVector.cend() - to_shink); metrics::CryptoMetrics metrics; - EXPECT_CALL(*crypto_session_, Open(kLevelDefault)).WillOnce(Return(NO_ERROR)); - EXPECT_CALL(*crypto_session_, - ShrinkUsageTableHeader(shunken_entries.size(), NotNull())) + EXPECT_CALL( + *crypto_session_, + ShrinkUsageTableHeader(kLevelDefault, shunken_entries.size(), NotNull())) .WillOnce(Return(NO_ERROR)); EXPECT_CALL(*device_files_, StoreUsageTableInfo(kUsageTableHeader, shunken_entries)) @@ -2611,8 +3194,8 @@ TEST_F(UsageTableHeaderTest, Shrink_AllOfTable) { Init(kSecurityLevelL1, kUsageTableHeader, k10UsageEntryInfoVector); metrics::CryptoMetrics metrics; - EXPECT_CALL(*crypto_session_, Open(kLevelDefault)).WillOnce(Return(NO_ERROR)); - EXPECT_CALL(*crypto_session_, ShrinkUsageTableHeader(0, NotNull())) + EXPECT_CALL(*crypto_session_, + ShrinkUsageTableHeader(kLevelDefault, 0, NotNull())) .WillOnce(Return(NO_ERROR)); EXPECT_CALL(*device_files_, StoreUsageTableInfo(kUsageTableHeader, kEmptyUsageEntryInfoVector)) @@ -2628,8 +3211,8 @@ TEST_F(UsageTableHeaderTest, Shrink_AllOfTable) { TEST_F(UsageTableHeaderTest, Shrink_MoreThanTable) { Init(kSecurityLevelL1, kUsageTableHeader, k10UsageEntryInfoVector); metrics::CryptoMetrics metrics; - EXPECT_CALL(*crypto_session_, Open(kLevelDefault)).WillOnce(Return(NO_ERROR)); - EXPECT_CALL(*crypto_session_, ShrinkUsageTableHeader(0, NotNull())) + EXPECT_CALL(*crypto_session_, + ShrinkUsageTableHeader(kLevelDefault, 0, NotNull())) .WillOnce(Return(NO_ERROR)); EXPECT_CALL(*device_files_, StoreUsageTableInfo(kUsageTableHeader, kEmptyUsageEntryInfoVector)) @@ -2654,7 +3237,7 @@ TEST_F(UsageTableHeaderTest, LruUsageTableUpgrade_NoAction) { SetArgPointee<2>(/* lru_upgrade = */ false), Return(true))); EXPECT_CALL(*crypto_session_, - LoadUsageTableHeader(kUpgradableUsageTableHeader)) + LoadUsageTableHeader(kLevelDefault, kUpgradableUsageTableHeader)) .WillOnce(Return(NO_ERROR)); // These function are called specifically by the LRU upgrading system. @@ -2680,7 +3263,7 @@ TEST_F(UsageTableHeaderTest, LruUsageTableUpgrade_Succeed) { SetArgPointee<2>(/* lru_upgrade = */ true), Return(true))); EXPECT_CALL(*crypto_session_, - LoadUsageTableHeader(kUpgradableUsageTableHeader)) + LoadUsageTableHeader(kLevelDefault, kUpgradableUsageTableHeader)) .WillOnce(Return(NO_ERROR)); for (size_t i = 0; i < kUpgradableUsageEntryInfoList.size(); ++i) { @@ -2744,7 +3327,7 @@ TEST_F(UsageTableHeaderTest, SetArgPointee<2>(/* lru_upgrade = */ true), Return(true))); EXPECT_CALL(*crypto_session_, - LoadUsageTableHeader(kUpgradableUsageTableHeader)) + LoadUsageTableHeader(kLevelDefault, kUpgradableUsageTableHeader)) .WillOnce(Return(NO_ERROR)); // Expectations of the one successful license. @@ -2804,7 +3387,7 @@ TEST_F(UsageTableHeaderTest, SetArgPointee<2>(/* lru_upgrade = */ true), Return(true))); EXPECT_CALL(*crypto_session_, - LoadUsageTableHeader(kUpgradableUsageTableHeader)) + LoadUsageTableHeader(kLevelDefault, kUpgradableUsageTableHeader)) .WillOnce(Return(NO_ERROR)); for (size_t i = 0; i < kUpgradableUsageEntryInfoList.size(); ++i) { @@ -2857,7 +3440,7 @@ TEST_F(UsageTableHeaderTest, LruUsageTableUpgrade_AllFailure) { SetArgPointee<2>(/* lru_upgrade = */ true), Return(true))); EXPECT_CALL(*crypto_session_, - LoadUsageTableHeader(kUpgradableUsageTableHeader)) + LoadUsageTableHeader(kLevelDefault, kUpgradableUsageTableHeader)) .WillOnce(Return(NO_ERROR)); for (size_t i = 0; i < kUpgradableUsageEntryInfoList.size(); ++i) { @@ -2881,7 +3464,8 @@ TEST_F(UsageTableHeaderTest, LruUsageTableUpgrade_AllFailure) { EXPECT_CALL(*device_files_, DeleteAllLicenses()); EXPECT_CALL(*device_files_, DeleteAllUsageInfo()); EXPECT_CALL(*device_files_, DeleteUsageTableInfo()); - EXPECT_CALL(*crypto_session_, CreateUsageTableHeader(NotNull())) + EXPECT_CALL(*crypto_session_, + CreateUsageTableHeader(kLevelDefault, NotNull())) .WillOnce(Return(NO_ERROR)); EXPECT_CALL(*device_files_, StoreUsageTableInfo(_, _)); @@ -2896,7 +3480,8 @@ TEST_F(UsageTableHeaderTest, LruLastUsedTime_CreateLicenseEntry) { SetArgPointee<1>(kUpgradedUsageEntryInfoList), SetArgPointee<2>(/* lru_upgrade = */ false), Return(true))); - EXPECT_CALL(*crypto_session_, LoadUsageTableHeader(kUpgradedUsageTableHeader)) + EXPECT_CALL(*crypto_session_, + LoadUsageTableHeader(kLevelDefault, kUpgradedUsageTableHeader)) .WillOnce(Return(NO_ERROR)); EXPECT_TRUE(usage_table_header_->Init(kSecurityLevelL1, crypto_session_)); @@ -2917,8 +3502,10 @@ TEST_F(UsageTableHeaderTest, LruLastUsedTime_CreateLicenseEntry) { MockClock mock_clock; usage_table_header_->SetClock(&mock_clock); EXPECT_CALL(mock_clock, GetCurrentTime()).WillOnce(Return(kLruBaseTime)); - EXPECT_CALL(*device_files_, - StoreUsageTableInfo(kUpgradedUsageTableHeader, _)); + EXPECT_CALL(*crypto_session_, UpdateUsageEntry(NotNull(), NotNull())) + .WillOnce( + DoAll(SetArgPointee<0>(kAnotherUsageTableHeader), Return(NO_ERROR))); + EXPECT_CALL(*device_files_, StoreUsageTableInfo(kAnotherUsageTableHeader, _)); // The Call. uint32_t usage_entry_number = 0; @@ -2940,7 +3527,8 @@ TEST_F(UsageTableHeaderTest, LruLastUsedTime_CreateUsageInfoEntry) { SetArgPointee<1>(kUpgradedUsageEntryInfoList), SetArgPointee<2>(/* lru_upgrade = */ false), Return(true))); - EXPECT_CALL(*crypto_session_, LoadUsageTableHeader(kUpgradedUsageTableHeader)) + EXPECT_CALL(*crypto_session_, + LoadUsageTableHeader(kLevelDefault, kUpgradedUsageTableHeader)) .WillOnce(Return(NO_ERROR)); EXPECT_TRUE(usage_table_header_->Init(kSecurityLevelL1, crypto_session_)); @@ -2962,8 +3550,10 @@ TEST_F(UsageTableHeaderTest, LruLastUsedTime_CreateUsageInfoEntry) { MockClock mock_clock; usage_table_header_->SetClock(&mock_clock); EXPECT_CALL(mock_clock, GetCurrentTime()).WillOnce(Return(kLruBaseTime)); - EXPECT_CALL(*device_files_, - StoreUsageTableInfo(kUpgradedUsageTableHeader, _)); + EXPECT_CALL(*crypto_session_, UpdateUsageEntry(NotNull(), NotNull())) + .WillOnce( + DoAll(SetArgPointee<0>(kAnotherUsageTableHeader), Return(NO_ERROR))); + EXPECT_CALL(*device_files_, StoreUsageTableInfo(kAnotherUsageTableHeader, _)); // The Call. uint32_t usage_entry_number = 0; @@ -2985,7 +3575,8 @@ TEST_F(UsageTableHeaderTest, LruLastUsedTime_UpdateEntry) { SetArgPointee<1>(kUpgradedUsageEntryInfoList), SetArgPointee<2>(/* lru_upgrade = */ false), Return(true))); - EXPECT_CALL(*crypto_session_, LoadUsageTableHeader(kUpgradedUsageTableHeader)) + EXPECT_CALL(*crypto_session_, + LoadUsageTableHeader(kLevelDefault, kUpgradedUsageTableHeader)) .WillOnce(Return(NO_ERROR)); EXPECT_TRUE(usage_table_header_->Init(kSecurityLevelL1, crypto_session_)); @@ -3027,7 +3618,8 @@ TEST_F(UsageTableHeaderTest, LruLastUsedTime_LoadEntry) { SetArgPointee<1>(kUpgradedUsageEntryInfoList), SetArgPointee<2>(/* lru_upgrade = */ false), Return(true))); - EXPECT_CALL(*crypto_session_, LoadUsageTableHeader(kUpgradedUsageTableHeader)) + EXPECT_CALL(*crypto_session_, + LoadUsageTableHeader(kLevelDefault, kUpgradedUsageTableHeader)) .WillOnce(Return(NO_ERROR)); EXPECT_TRUE(usage_table_header_->Init(kSecurityLevelL1, crypto_session_)); @@ -3066,38 +3658,16 @@ TEST_F(UsageTableHeaderTest, LruLastUsedTime_LoadEntry) { // operations. TEST_F(UsageTableHeaderTest, DetermineLicenseToRemove_InvalidInput) { constexpr size_t kUnexpiredThreshold = 50; // Arbitrary - constexpr size_t kRemovalCount = 3; // Also artbirary std::vector usage_entry_info_list; - std::vector removal_candidates; + uint32_t entry_to_remove = 0; // Empty list. EXPECT_FALSE(UsageTableHeader::DetermineLicenseToRemoveForTesting( - usage_entry_info_list, kLruBaseTime, kUnexpiredThreshold, kRemovalCount, - &removal_candidates)); + usage_entry_info_list, kLruBaseTime, kUnexpiredThreshold, + &entry_to_remove)); // Output is null. usage_entry_info_list = kUpgradedUsageEntryInfoList; EXPECT_FALSE(UsageTableHeader::DetermineLicenseToRemoveForTesting( - usage_entry_info_list, kLruBaseTime, kUnexpiredThreshold, kRemovalCount, - nullptr)); - // Zero size requests. - EXPECT_FALSE(UsageTableHeader::DetermineLicenseToRemoveForTesting( - usage_entry_info_list, kLruBaseTime, kUnexpiredThreshold, 0, - &removal_candidates)); - // Request more than is available. Not invalid, but an unlikely use - // case. - EXPECT_TRUE(UsageTableHeader::DetermineLicenseToRemoveForTesting( - usage_entry_info_list, kLruBaseTime, kUnexpiredThreshold, - /* removal_count = */ usage_entry_info_list.size() * 2, - &removal_candidates)); - // Only unexpired offline license, but threshold is not met. - usage_entry_info_list.resize(kRemovalCount); - for (size_t i = 0; i < kRemovalCount; ++i) { - usage_entry_info_list[i].storage_type = kStorageLicense; - usage_entry_info_list[i].last_use_time = kLruBaseTime; - usage_entry_info_list[i].offline_license_expiry_time = kLruBaseTime + 1; - } - EXPECT_FALSE(UsageTableHeader::DetermineLicenseToRemoveForTesting( - usage_entry_info_list, kLruBaseTime, kUnexpiredThreshold, kRemovalCount, - &removal_candidates)); + usage_entry_info_list, kLruBaseTime, kUnexpiredThreshold, nullptr)); } // Check that the major priority buckets are respected. @@ -3107,6 +3677,7 @@ TEST_F(UsageTableHeaderTest, DetermineLicenseToRemove_InvalidInput) { TEST_F(UsageTableHeaderTest, DetermineLicenseToRemove_BasicPriorities) { constexpr int64_t kOneDay = 24 * 60 * 60; constexpr int64_t kCurrentTime = kLruBaseTime + kOneDay; + constexpr uint32_t kInvalidEntry = 9999; std::vector usage_entry_info_list; // Unexpired offline license. @@ -3140,56 +3711,98 @@ TEST_F(UsageTableHeaderTest, DetermineLicenseToRemove_BasicPriorities) { usage_entry_info_list.push_back(unknown_entry_info); constexpr uint32_t unknown_entry_number = 3; - std::vector removal_candidates; + // Case 1: If there is an entry with unknown storage type, it should + // be selected above any other entry. + uint32_t entry_to_remove = kInvalidEntry; // Expect the unknown entry. EXPECT_TRUE(UsageTableHeader::DetermineLicenseToRemoveForTesting( usage_entry_info_list, kCurrentTime, - /* unexpired_threshold = */ 3, - /* removal_count = */ 1, &removal_candidates)); - const std::vector unknown_entry_numbers = {unknown_entry_number}; - EXPECT_THAT(removal_candidates, ContainerEq(unknown_entry_numbers)); + /* unexpired_threshold = */ 3, &entry_to_remove)); + EXPECT_EQ(unknown_entry_number, entry_to_remove); usage_entry_info_list.pop_back(); // Removing unknown. - // Expect both expired offline and streaming license. + // Case 2a: Threshold not met, all entries are equally stale. + // The expired entry should be selected over the streaming license. + entry_to_remove = kInvalidEntry; EXPECT_TRUE(UsageTableHeader::DetermineLicenseToRemoveForTesting( usage_entry_info_list, kCurrentTime, - /* unexpired_threshold = */ 3, - /* removal_count = */ 3, &removal_candidates)); - const std::vector expired_and_streaming_entry_numbers = { - expired_entry_number, streaming_entry_number}; - EXPECT_THAT(removal_candidates, - ContainerEq(expired_and_streaming_entry_numbers)); + /* unexpired_threshold = */ 3, &entry_to_remove)); + EXPECT_EQ(expired_entry_number, entry_to_remove); - // With threshold met, expect all three. + // Case 2b: Threshold not met, streaming license is most stale. + usage_entry_info_list[streaming_entry_number].last_use_time--; + entry_to_remove = kInvalidEntry; EXPECT_TRUE(UsageTableHeader::DetermineLicenseToRemoveForTesting( usage_entry_info_list, kCurrentTime, - /* unexpired_threshold = */ 0, - /* removal_count = */ 3, &removal_candidates)); - const std::vector all_license_entry_numbers = { - expired_entry_number, streaming_entry_number, unexpired_entry_number}; - EXPECT_THAT(removal_candidates, ContainerEq(all_license_entry_numbers)); + /* unexpired_threshold = */ 3, &entry_to_remove)); + EXPECT_EQ(streaming_entry_number, entry_to_remove); usage_entry_info_list.pop_back(); // Removing streaming. - usage_entry_info_list.pop_back(); // Removing expired offline. + // |usage_entry_info_list| only contains 1 expired and 1 unexpired offline + // license. - // Sanity check: while below threshold there will not be any possible - // candidates, expect failure. This is an unexpected case in normal - // operation. - EXPECT_FALSE(UsageTableHeader::DetermineLicenseToRemoveForTesting( - usage_entry_info_list, kCurrentTime, - /* unexpired_threshold = */ 3, - /* removal_count = */ 1, &removal_candidates)); - - // Expect the unexpired license. + // Case 2c: Threshold met, equally stale entries. Expect the expired + // entry over the unexpired. + entry_to_remove = kInvalidEntry; EXPECT_TRUE(UsageTableHeader::DetermineLicenseToRemoveForTesting( usage_entry_info_list, kCurrentTime, - /* unexpired_threshold = */ 0, - /* removal_count = */ 1, &removal_candidates)); - const std::vector unexpired_entry_numbers = { - unexpired_entry_number}; - EXPECT_THAT(removal_candidates, ContainerEq(unexpired_entry_numbers)); + /* unexpired_threshold = */ 0, &entry_to_remove)); + EXPECT_EQ(expired_entry_number, entry_to_remove); + + // Case 3a: Threshold not met, expired entry is the most stale. + entry_to_remove = kInvalidEntry; + usage_entry_info_list[expired_entry_number].last_use_time--; + EXPECT_TRUE(UsageTableHeader::DetermineLicenseToRemoveForTesting( + usage_entry_info_list, kCurrentTime, + /* unexpired_threshold = */ 3, &entry_to_remove)); + EXPECT_EQ(expired_entry_number, entry_to_remove); + + // Case 3b: Threshold not met, unexpired entry is the most stale. + entry_to_remove = kInvalidEntry; + usage_entry_info_list[expired_entry_number].last_use_time++; + usage_entry_info_list[unexpired_entry_number].last_use_time--; + EXPECT_TRUE(UsageTableHeader::DetermineLicenseToRemoveForTesting( + usage_entry_info_list, kCurrentTime, + /* unexpired_threshold = */ 3, &entry_to_remove)); + EXPECT_EQ(expired_entry_number, entry_to_remove); + + // Case 3c: Threshold met, unexpired entry is the most stale. + entry_to_remove = kInvalidEntry; + EXPECT_TRUE(UsageTableHeader::DetermineLicenseToRemoveForTesting( + usage_entry_info_list, kCurrentTime, + /* unexpired_threshold = */ 0, &entry_to_remove)); + EXPECT_EQ(unexpired_entry_number, entry_to_remove); + + // Case 3d: Threshold met, expired entry is the most stale. + entry_to_remove = kInvalidEntry; + usage_entry_info_list[expired_entry_number].last_use_time--; + usage_entry_info_list[unexpired_entry_number].last_use_time++; + EXPECT_TRUE(UsageTableHeader::DetermineLicenseToRemoveForTesting( + usage_entry_info_list, kCurrentTime, + /* unexpired_threshold = */ 0, &entry_to_remove)); + EXPECT_EQ(expired_entry_number, entry_to_remove); + + usage_entry_info_list.pop_back(); // Removing expired offline. + + // Case 4a: Threshold met, and only an unexpired offline license + // is available. + entry_to_remove = kInvalidEntry; // Invalidate value. + EXPECT_TRUE(UsageTableHeader::DetermineLicenseToRemoveForTesting( + usage_entry_info_list, kCurrentTime, + /* unexpired_threshold = */ 0, &entry_to_remove)); + EXPECT_EQ(unexpired_entry_number, entry_to_remove); + + // Case 4b (stability check): Threshold not met, and only an + // unexpired offline license is available. This is an unexpected + // condition in normal operation, but the algorithm should still + // return an entry. + entry_to_remove = kInvalidEntry; + EXPECT_TRUE(UsageTableHeader::DetermineLicenseToRemoveForTesting( + usage_entry_info_list, kCurrentTime, + /* unexpired_threshold = */ 3, &entry_to_remove)); + EXPECT_EQ(unexpired_entry_number, entry_to_remove); } // Testing algorithm with unexpired offline and streaming license. The @@ -3198,6 +3811,7 @@ TEST_F(UsageTableHeaderTest, DetermineLicenseToRemove_BasicPriorities) { TEST_F(UsageTableHeaderTest, DetermineLicenseToRemove_NoExpiredAndBelowThreshold) { constexpr int64_t kOneDay = 24 * 60 * 60; + constexpr uint32_t kInvalidEntry = 9999; std::vector usage_entry_info_list = kUpgradedUsageEntryInfoList; const size_t offline_threshold = usage_entry_info_list.size() + 1; @@ -3218,17 +3832,13 @@ TEST_F(UsageTableHeaderTest, // Must exist at least one streaming license for test to work. ASSERT_LT(0ull, usage_info_count); - std::vector removal_candidates; + uint32_t entry_to_remove = kInvalidEntry; EXPECT_TRUE(UsageTableHeader::DetermineLicenseToRemoveForTesting( usage_entry_info_list, kLruBaseTime, offline_threshold, - /* removal_count = */ 3, &removal_candidates)); + &entry_to_remove)); - EXPECT_EQ(usage_info_count, removal_candidates.size()); - // Ensure that the proposed removal candidates are streaming. - for (uint32_t usage_entry_number : removal_candidates) { - EXPECT_EQ(kStorageUsageInfo, - usage_entry_info_list[usage_entry_number].storage_type); - } + EXPECT_EQ(kStorageUsageInfo, + usage_entry_info_list[entry_to_remove].storage_type); } // When the number of unexpired offline licenses are below the @@ -3243,6 +3853,7 @@ TEST_F(UsageTableHeaderTest, constexpr int64_t kCurrentTime = kLruBaseTime + kOneDay * 2; // A threshold larger than the possible number of offline entries. constexpr size_t kUnexpiredThreshold = kSetSize + 1; + constexpr uint32_t kInvalidEntry = 9999; std::vector usage_entry_info_list; usage_entry_info_list.resize(kSetSize); @@ -3271,126 +3882,16 @@ TEST_F(UsageTableHeaderTest, expired_license_numbers.push_back(i); } - std::vector removal_candidates; + uint32_t entry_to_remove = kInvalidEntry; EXPECT_TRUE(UsageTableHeader::DetermineLicenseToRemoveForTesting( - usage_entry_info_list, kCurrentTime, kUnexpiredThreshold, 3, - &removal_candidates)); + usage_entry_info_list, kCurrentTime, kUnexpiredThreshold, + &entry_to_remove)); - // Sorting to ensure equality will work. - std::sort(removal_candidates.begin(), removal_candidates.end()); - std::sort(expired_license_numbers.begin(), expired_license_numbers.end()); - - EXPECT_EQ(expired_license_numbers, removal_candidates); -} - -// Test that if all of the license are unknown. For unknown licenses, -// the last use time has no effect on their selection. -TEST_F(UsageTableHeaderTest, DetermineLicenseToRemove_AllUnknown) { - constexpr int64_t kOneDay = 24 * 60 * 60; - constexpr size_t kSetSize = 10; - constexpr int64_t kCurrentTime = kLruBaseTime + kOneDay * 2; - constexpr size_t kRemovalCount = 3; - - std::vector usage_entry_info_list; - usage_entry_info_list.resize(kSetSize); - - // Create a set of all unknown licenses. - for (uint32_t i = 0; i < kSetSize; ++i) { - CdmUsageEntryInfo& usage_entry_info = usage_entry_info_list[i]; - usage_entry_info.storage_type = kStorageTypeUnknown; - usage_entry_info.key_set_id = "unknown_key_set_id"; - usage_entry_info.last_use_time = - CdmRandom::RandomInRange(kLruBaseTime, kCurrentTime); - } - - std::vector removal_candidates; - EXPECT_TRUE(UsageTableHeader::DetermineLicenseToRemoveForTesting( - usage_entry_info_list, kCurrentTime, 0, kRemovalCount, - &removal_candidates)); - // There should always be 3 (assuming kSetSize >= 3). - EXPECT_EQ(kRemovalCount, removal_candidates.size()); -} - -// Should there be two or more license which have the same -// |last_use_time| but only 1 of them are to be selected (due to the -// number being requested), then the algorithm should randomly select -// between the ones available. -TEST_F(UsageTableHeaderTest, DetermineLicenseToRemove_RandomnessOfCutoff) { - constexpr int64_t kOneDay = 24 * 60 * 60; - constexpr size_t kSetSize = 10; - constexpr int64_t kCurrentTime = kLruBaseTime + kOneDay * 2; - constexpr size_t kTrials = 25; - std::vector usage_entry_info_list; - - // All will be streaming licenses. - usage_entry_info_list.resize(kSetSize); - for (size_t i = 0; i < kSetSize; ++i) { - CdmUsageEntryInfo& usage_entry_info = usage_entry_info_list[i]; - usage_entry_info.storage_type = kStorageUsageInfo; - usage_entry_info.last_use_time = kLruBaseTime + kOneDay; - usage_entry_info.key_set_id = "nothing_unusual"; - } - - std::vector expected_removals; - // Select two to be the most stale. - while (expected_removals.size() < 2) { - const uint32_t i = CdmRandom::RandomInRange(kSetSize - 1); - CdmUsageEntryInfo& usage_entry_info = usage_entry_info_list[i]; - if (usage_entry_info.key_set_id != "nothing_unusual") continue; - usage_entry_info_list[i].last_use_time = kLruBaseTime; - usage_entry_info.key_set_id = "most_stale"; - expected_removals.push_back(i); - } - - // Select another two to be slightly less stale. - std::vector random_removals; - while (random_removals.size() < 2) { - const uint32_t i = CdmRandom::RandomInRange(kSetSize - 1); - CdmUsageEntryInfo& usage_entry_info = usage_entry_info_list[i]; - if (usage_entry_info.key_set_id != "nothing_unusual") continue; - usage_entry_info_list[i].last_use_time = kLruBaseTime + 1; - usage_entry_info.key_set_id = "somewhat_stale"; - random_removals.push_back(i); - } - - // Create two sets for each possible outcome (one for each random - // removal). - std::vector possible_removal_1 = expected_removals; - possible_removal_1.push_back(random_removals[0]); - std::sort(possible_removal_1.begin(), possible_removal_1.end()); - std::vector possible_removal_2 = expected_removals; - possible_removal_2.push_back(random_removals[1]); - std::sort(possible_removal_2.begin(), possible_removal_2.end()); - std::vector possible_removal_3 = expected_removals; - - // Flags to check that both outcomes have occurred. - bool occurrence_1 = false; - bool occurrence_2 = false; - - // Each set should be equally likely. Possible false-negative every - // 2^kTrials time. - for (size_t i = 0; i < kTrials; ++i) { - // Remove 3 out of the 4 possible candidates. - std::vector removal_candidates; - EXPECT_TRUE(UsageTableHeader::DetermineLicenseToRemoveForTesting( - usage_entry_info_list, kCurrentTime, 0, 3, &removal_candidates)); - - std::sort(removal_candidates.begin(), removal_candidates.end()); - if (removal_candidates == possible_removal_1) { - occurrence_1 = true; - } else if (removal_candidates == possible_removal_2) { - occurrence_2 = true; - } else { - EXPECT_TRUE(false) << "Unexpected removal set"; - } - } - - EXPECT_TRUE(occurrence_1); - EXPECT_TRUE(occurrence_2); + EXPECT_THAT(expired_license_numbers, Contains(entry_to_remove)); } // This test primarily tests the robustness of the algorithm for a full -// set of entries (200). Creates 3 stale streaming license and 3 +// set of entries (200). Creates 1 stale streaming license and 1 // offline licenses which are more stale than the streaming. // // First, with the stale offline licenses unexpired, checks that the @@ -3403,6 +3904,7 @@ TEST_F(UsageTableHeaderTest, DetermineLicenseToRemove_LargeMixedSet) { constexpr int64_t kOneDay = 24 * 60 * 60; constexpr size_t kLargeSetSize = 200; constexpr int64_t kCurrentTime = kLruBaseTime + kOneDay * 2; + constexpr uint32_t kInvalidEntry = 9999; std::vector usage_entry_info_list; usage_entry_info_list.resize(kLargeSetSize); @@ -3421,72 +3923,57 @@ TEST_F(UsageTableHeaderTest, DetermineLicenseToRemove_LargeMixedSet) { } } - // Select 3 streaming license to be more stale than the rest. - std::vector modified_usage_info_numbers; - while (modified_usage_info_numbers.size() < 3) { + // Select a streaming license to be more stale than the rest. + uint32_t modified_usage_info_number = kInvalidEntry; + while (modified_usage_info_number == kInvalidEntry) { const uint32_t i = CdmRandom::RandomInRange(kLargeSetSize - 1); CdmUsageEntryInfo& usage_entry_info = usage_entry_info_list[i]; - // Skip streaming license that have already been modified and offline - // licenses. - if (usage_entry_info.storage_type != kStorageUsageInfo || - usage_entry_info.key_set_id != "nothing_unusual") - continue; - usage_entry_info.last_use_time = - kLruBaseTime + 10 + modified_usage_info_numbers.size(); + // Skip offline licenses. + if (usage_entry_info.storage_type != kStorageUsageInfo) continue; + usage_entry_info.last_use_time = kLruBaseTime + 10; usage_entry_info.key_set_id = "stale_streaming"; - modified_usage_info_numbers.push_back(i); + modified_usage_info_number = i; } - std::sort(modified_usage_info_numbers.begin(), - modified_usage_info_numbers.end()); - // Select 3 offline license to be even more stale, but unexpired. - std::vector modified_offline_license_numbers; - while (modified_offline_license_numbers.size() < 3) { + // Select a offline license to be even more stale, but unexpired. + uint32_t modified_offline_license_number = kInvalidEntry; + while (modified_offline_license_number == kInvalidEntry) { const uint32_t i = CdmRandom::RandomInRange(kLargeSetSize - 1); CdmUsageEntryInfo& usage_entry_info = usage_entry_info_list[i]; - // Skip offline license that have already been modified and streaming - // licenses. - if (usage_entry_info.storage_type != kStorageLicense || - usage_entry_info.key_set_id != "nothing_unusual") - continue; + // Skip streaming licenses. + if (usage_entry_info.storage_type != kStorageLicense) continue; usage_entry_info.last_use_time = kLruBaseTime; usage_entry_info.key_set_id = "stale_offline"; - modified_offline_license_numbers.push_back(i); + modified_offline_license_number = i; } - std::sort(modified_offline_license_numbers.begin(), - modified_offline_license_numbers.end()); // Test using only streaming and expired offline licenses // (which there are none). - std::vector removal_candidates; + uint32_t entry_to_remove = kInvalidEntry; EXPECT_TRUE(UsageTableHeader::DetermineLicenseToRemoveForTesting( usage_entry_info_list, kCurrentTime, - /* unexpired_threshold = */ kLargeSetSize, 3, &removal_candidates)); - EXPECT_THAT(removal_candidates, - UnorderedElementsAreArray(modified_usage_info_numbers)); + /* unexpired_threshold = */ kLargeSetSize, &entry_to_remove)); + EXPECT_EQ(modified_usage_info_number, entry_to_remove); - // Test where the equality threshold is met, now the 3 unexpired - // licenses should be selected. - removal_candidates.clear(); + // Test where the equality threshold is met, now the stale unexpired + // license should be selected. + entry_to_remove = kInvalidEntry; EXPECT_TRUE(UsageTableHeader::DetermineLicenseToRemoveForTesting( usage_entry_info_list, kCurrentTime, - /* unexpired_threshold = */ 0, 3, &removal_candidates)); - EXPECT_THAT(removal_candidates, - UnorderedElementsAreArray(modified_offline_license_numbers)); + /* unexpired_threshold = */ 0, &entry_to_remove)); + EXPECT_EQ(modified_offline_license_number, entry_to_remove); - // Make the 3 offline licenses expired. - for (uint32_t i : modified_offline_license_numbers) { - CdmUsageEntryInfo& usage_entry_info = usage_entry_info_list[i]; - usage_entry_info.offline_license_expiry_time = kLruBaseTime; - } + // Make the stale offline license expired. + CdmUsageEntryInfo& offline_usage_entry_info = + usage_entry_info_list[modified_offline_license_number]; + offline_usage_entry_info.offline_license_expiry_time = kLruBaseTime; // Test again, expecting that the expired license should be considered. - removal_candidates.clear(); + entry_to_remove = kInvalidEntry; EXPECT_TRUE(UsageTableHeader::DetermineLicenseToRemoveForTesting( usage_entry_info_list, kCurrentTime, - /* unexpired_threshold = */ kLargeSetSize, 3, &removal_candidates)); - EXPECT_THAT(removal_candidates, - UnorderedElementsAreArray(modified_offline_license_numbers)); + /* unexpired_threshold = */ kLargeSetSize, &entry_to_remove)); + EXPECT_EQ(modified_offline_license_number, entry_to_remove); } TEST_F(UsageTableHeaderTest, PotentialTableCapacity_Unavailable) { @@ -3494,15 +3981,7 @@ TEST_F(UsageTableHeaderTest, PotentialTableCapacity_Unavailable) { Init(kSecurityLevelL1, kUsageTableHeader, kUsageEntryInfoVector); EXPECT_EQ(usage_table_header_->potential_table_capacity(), kMinimumUsageTableEntriesSupported); -} - -TEST_F(UsageTableHeaderTest, PotentialTableCapacity_Zero) { - // This will issue a warning about the reported capacity is unexpected, - // and will default to the version's required minimum. - crypto_session_->SetMaximumUsageTableEntries(0u); - Init(kSecurityLevelL1, kUsageTableHeader, kUsageEntryInfoVector); - EXPECT_EQ(usage_table_header_->potential_table_capacity(), - kMinimumUsageTableEntriesSupported); + EXPECT_FALSE(usage_table_header_->HasUnlimitedTableCapacity()); } TEST_F(UsageTableHeaderTest, PotentialTableCapacity_TooSmall) { @@ -3513,6 +3992,25 @@ TEST_F(UsageTableHeaderTest, PotentialTableCapacity_TooSmall) { Init(kSecurityLevelL1, kUsageTableHeader, kUsageEntryInfoVector); EXPECT_EQ(usage_table_header_->potential_table_capacity(), kMinimumUsageTableEntriesSupported); + EXPECT_FALSE(usage_table_header_->HasUnlimitedTableCapacity()); +} + +TEST_F(UsageTableHeaderTest, PotentialTableCapacity_Unlimited) { + MockUsageTableHeader* mock_usage_table_header = SetUpMock(); + // Zero indicates that the table size is unlimited. + crypto_session_->SetMaximumUsageTableEntries(0u); + // Expect calls to make a delete entry. + EXPECT_CALL(*crypto_session_, Open(_)).WillOnce(Return(NO_ERROR)); + const size_t test_index = kUsageEntryInfoVector.size(); + EXPECT_CALL(*mock_usage_table_header, AddEntry(_, _, _, _, _, NotNull())) + .WillOnce(DoAll(SetArgPointee<5>(test_index), Return(NO_ERROR))); + EXPECT_CALL(*mock_usage_table_header, InvalidateEntry(test_index, _, _, _)) + .WillOnce(Return(NO_ERROR)); + + Init(kSecurityLevelL1, kUsageTableHeader, kUsageEntryInfoVector); + constexpr size_t kZero = 0u; + EXPECT_EQ(mock_usage_table_header->potential_table_capacity(), kZero); + EXPECT_TRUE(mock_usage_table_header->HasUnlimitedTableCapacity()); } TEST_F(UsageTableHeaderTest, PotentialTableCapacity_Available) { @@ -3520,6 +4018,7 @@ TEST_F(UsageTableHeaderTest, PotentialTableCapacity_Available) { crypto_session_->SetMaximumUsageTableEntries(kTableCapacity); Init(kSecurityLevelL1, kUsageTableHeader, kUsageEntryInfoVector); EXPECT_EQ(usage_table_header_->potential_table_capacity(), kTableCapacity); + EXPECT_FALSE(usage_table_header_->HasUnlimitedTableCapacity()); } } // namespace wvcdm diff --git a/oemcrypto/include/OEMCryptoCENC.h b/oemcrypto/include/OEMCryptoCENC.h index db54f9d8..6e71a894 100644 --- a/oemcrypto/include/OEMCryptoCENC.h +++ b/oemcrypto/include/OEMCryptoCENC.h @@ -8,7 +8,7 @@ * Reference APIs needed to support Widevine's crypto algorithms. * * See the document "WV Modular DRM Security Integration Guide for Common - * Encryption (CENC) -- version 16.2" for a description of this API. You + * Encryption (CENC) -- version 16.3" for a description of this API. You * can find this document in the widevine repository as * docs/WidevineModularDRMSecurityIntegrationGuideforCENC_v16.pdf * Changes between different versions of this API are documented in the files diff --git a/oemcrypto/include/OEMCryptoCENCCommon.h b/oemcrypto/include/OEMCryptoCENCCommon.h index e8f2ad90..03d7030a 100644 --- a/oemcrypto/include/OEMCryptoCENCCommon.h +++ b/oemcrypto/include/OEMCryptoCENCCommon.h @@ -112,7 +112,8 @@ typedef enum OEMCrypto_Usage_Entry_Status { */ typedef enum OEMCrypto_LicenseType { OEMCrypto_ContentLicense = 0, - OEMCrypto_EntitlementLicense = 1 + OEMCrypto_EntitlementLicense = 1, + OEMCrypto_LicenstType_MaxValue = OEMCrypto_EntitlementLicense, } OEMCrypto_LicenseType; /* Private key type used in the provisioning response. */ diff --git a/oemcrypto/test/fuzz_tests/README.md b/oemcrypto/test/fuzz_tests/README.md new file mode 100644 index 00000000..aafeda15 --- /dev/null +++ b/oemcrypto/test/fuzz_tests/README.md @@ -0,0 +1,187 @@ +# OEMCRYPTO Fuzzing + +## Objective + +* Run fuzzing on OEMCrypto public APIs on linux using google supported + clusterfuzz infrastructure to find security vulnerabilities. + + Design Document - https://docs.google.com/document/d/1mdSV2irJZz5Y9uYb5DmSIddBjrAIZU9q8G5Q_BGpA4I/edit?usp=sharing + + Fuzzing at google - + [go/fuzzing](https://g3doc.corp.google.com/security/fuzzing/g3doc/fuzzing_resources.md?cl=head) +## Monitoring +### Cluster fuzz statistics + +* Performance of OEMCrypto fuzz binaries running continuously using cluster + fuzz infrastructure can be monitored + [here](https://clusterfuzz.corp.google.com/fuzzer-stats). + + The options to select are `Job type: libfuzzer_asan_oemcrypto` and `Fuzzer: + fuzzer name you are looking for` + + Example: [load_license_fuzz](https://clusterfuzz.corp.google.com/fuzzer-stats?group_by=by-day&date_start=2020-07-11&date_end=2020-07-17&fuzzer=libFuzzer_oemcrypto_load_license_fuzz&job=libfuzzer_asan_oemcrypto) + +### Issues filed by clusterfuzz - Fixing those issues + +* Any issues found with the fuzz target under test are reported by clusterfuzz + [here](https://b.corp.google.com/hotlists/2442954). + +* The bug will have a link to the test case that generated the bug. Download + the test case and follow the steps from + [testing fuzzer locally](#testing-fuzzer-locally) section to run the fuzzer + locally using the test case that caused the crash. + +* Once the issue is fixed, consider adding the test case that caused the crash + to the seed corpus zip file. Details about seed corpus and their location + are mentioned in + [this section](#build-oemcrypto-unit-tests-to-generate-corpus). + +## Corpus + +* Once the fuzzer scripts are ready and running continuously using clusterfuzz + or android infrastructure, we can measure the efficiency of fuzzers by + looking at code coverage and number of new features that have been + discovered by fuzzer scripts here Fuzz script statistics. + + A fuzzer which tries to start from random inputs and figure out intelligent + inputs to crash the libraries can be time consuming and not effective. A way + to make fuzzers more effective is by providing a set of valid and invalid + inputs of the library so that fuzzer can use those as a starting point. + These sets of valid and invalid inputs are called corpus. + + The idea is to run OEMCrypto unit tests and read required data into binary + corpus files before calling into respective OEMCrypto APIs under test. + Writing corpus data to binary files is controlled by --generate_corpus flag. + +### Build OEMCrypto unit tests to generate corpus + +* Install Pre-requisites + + ```shell + $ sudo apt-get install gyp ninja-build + ``` + +* download cdm source code (including ODK & OEMCrypto unit tests): + + ```shell + $ git clone sso://widevine-internal/cdm + ``` + +* Build OEMCrypto unit tests and run with --generate_corpus flag to generate + corpus files: + + ```shell + $ cd /path/to/cdm/repo + $ export CDM_DIR=/path/to/cdm/repo + $ export PATH_TO_CDM_DIR=.. + $ gyp --format=ninja --depth=$(pwd) oemcrypto/oemcrypto_unittests.gyp + $ ninja -C out/Default/ + $ ./out/Default/oemcrypto_unittests --generate_corpus + ``` + +* To avoid uploading huge binary files to git repository, the corpus files + will be saved in fuzzername_seed_corpus.zip format in blockbuster project's + oemcrypto_fuzzing_corpus GCS bucket using gsutil. If you need permissions + for blockbuster project, contact widevine-engprod@google.com. + + ```shell + $ gsutil cp gs://oemcrypto_fuzzing_corpus/ \ + + ``` + +## Testing fuzzer locally + +* Corpus needed to run fuzz tests locally are available in blockbuster + project's oemcrypto_fuzzing_corpus GCS bucket. If you need permissions for + this project, contact widevine-engprod@google.com. Download corpus. + + ```shell + $ gsutil cp gs://oemcrypto_fuzzing_corpus/ \ + + ``` + +* Add flags to generate additional debugging information. Add '-g3' flag to + oemcrypto_fuzztests.gypi cflags_cc in order to generate additional debug + information locally. + +* Build and test fuzz scripts locally using: + + ```shell + $ export CXX=clang++ + $ export CC=clang + $ export GYP_DEFINES="clang=1" + $ cd /path/to/cdm/repo + $ export PATH_TO_CDM_DIR=. + $ gyp --format=ninja --depth=$(pwd) \ + oemcrypto/test/fuzz_tests/oemcrypto_fuzztests.gyp + $ ninja -C out/Default/ + $ mkdir /tmp/new_interesting_corpus + $ ./out/Default/fuzzer_binary /tmp/new_interesting_corpus \ + /path/to/fuzz/seed/corpus/folder + ``` + +* In order to run fuzz script against a crash input, follow the above steps + and run the fuzz binary against crash input rather than seed corpus. + + ```shell + $ ./out/Default/fuzzer_binary crash_input_file + ``` +## Adding a new OEMCrypto fuzz script +* In order to fuzz a new OEMCrypto API in future, a fuzz script can be added to + oemcrypto/test/fuzz_tests folder which ends with _fuzz.cc. + +* In the program, define the function LLVMFuzzerTestOneInput with the following signature: + ``` + extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + + return 0; + } + ``` + *Note*: Make sure LLVMFuzzerTestOneInput calls the function you want to fuzz. + +* Add a new target to oemcrypto_fuzztests.gyp file and follow instructions in + [testing fuzzer locally](#testing-fuzzer-locally) to build and test locally. + +## Generate code coverage reports locally + +* Code coverage is a means of measuring fuzzer performance. We want to make + sure that our fuzzer covers all the paths in our code and make any tweeks to + fuzzer logic so we can maximize coverage to get better results. + + Coverage reports for git on borg project is not automated and needs to be + generated manually. Future plan is to build a dashboard for git on borg + coverage reports. + +* In order to generate coverage reports, we need to compile fuzzer binary with + flags to enable coverage. We can remove + `-fsanitize=fuzzer,address,undefined` from oemcrypto_fuzztests.gypi file as + that is needed only while fuzzing. Add following flags to both cflags_cc and + ldflags of oemcrypto_fuzztests.gypi and build fuzz binaries as mentioned in + `Testing fuzzer locally` section. + + ``` + '-fprofile-instr-generate', + '-fcoverage-mapping', + ``` + +* We need to run fuzzer binary against the corpus downloaded from + [clusterfuzz](https://clusterfuzz.corp.google.com/fuzzer-stats). Clock on + download link from corpus_backup column. Use gsutil command to download the + entire corpus for the fuzz binary. + +* Use the following commands to generate raw profile data file with coverage + information and generate a html coverage report for a single fuzzer. More + information about clang source based coverage can be found + [here](https://clang.llvm.org/docs/SourceBasedCodeCoverage.html). Follow + [this](https://clang.llvm.org/docs/SourceBasedCodeCoverage.html) for steps + to combine code coverage reports of multiple fuzzers. + + ```shell + # Run fuzz binary against corpus backup to generate default.profraw file. + $ ./out/Default/fuzz_binary path/to/corpus/backup -runs=0 + # Index raw profile files to generate coverage reports. + $ llvm-profdata merge -sparse default.profraw -o default.profdata + # Generate html coverage file. + $ llvm-cov show ./out/Default/fuzz_binary -format=html \ + -instr-profile=default.profdata -o default.html + ``` \ No newline at end of file diff --git a/oemcrypto/test/fuzz_tests/oemcrypto_fuzz_helper.cc b/oemcrypto/test/fuzz_tests/oemcrypto_fuzz_helper.cc new file mode 100644 index 00000000..2984ff00 --- /dev/null +++ b/oemcrypto/test/fuzz_tests/oemcrypto_fuzz_helper.cc @@ -0,0 +1,8 @@ +// Copyright 2020 Google LLC. All Rights Reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. +#include "oemcrypto_fuzz_helper.h" + +namespace wvoec { +void RedirectStdoutToFile() { freopen("log.txt", "a", stdout); } +} // namespace wvoec diff --git a/oemcrypto/test/fuzz_tests/oemcrypto_fuzz_helper.h b/oemcrypto/test/fuzz_tests/oemcrypto_fuzz_helper.h new file mode 100644 index 00000000..d74c1a88 --- /dev/null +++ b/oemcrypto/test/fuzz_tests/oemcrypto_fuzz_helper.h @@ -0,0 +1,94 @@ +// Copyright 2020 Google LLC. All Rights Reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. +#ifndef OEMCRYPTO_FUZZ_HELPER_H_ +#define OEMCRYPTO_FUZZ_HELPER_H_ + +#include + +#include "FuzzedDataProvider.h" +#include "OEMCryptoCENC.h" +#include "oec_device_features.h" +#include "oec_session_util.h" +#include "oemcrypto_corpus_generator_helper.h" +#include "oemcrypto_session_tests_helper.h" +namespace wvoec { +// Initial setup to create a valid OEMCrypto state such as initializing crypto +// firmware/hardware, installing golden key box etc. in order to fuzz +// OEMCrypto APIs. +class InitializeFuzz : public SessionUtil { + public: + InitializeFuzz() { + wvoec::global_features.Initialize(); + OEMCrypto_SetSandbox(kTestSandbox, sizeof(kTestSandbox)); + OEMCrypto_Initialize(); + EnsureTestKeys(); + } + + ~InitializeFuzz() { OEMCrypto_Terminate(); } +}; + +class OEMCryptoLicenseAPIFuzz : public InitializeFuzz { + public: + OEMCryptoLicenseAPIFuzz() : license_messages_(&session_) { + session_.open(); + InstallTestRSAKey(&session_); + session_.GenerateNonce(); + } + + ~OEMCryptoLicenseAPIFuzz() { + session_.close(); + } + + LicenseRoundTrip& license_messages() { return license_messages_; } + + private: + Session session_; + LicenseRoundTrip license_messages_; +}; + +class OEMCryptoProvisioningAPIFuzz : public InitializeFuzz { + public: + OEMCryptoProvisioningAPIFuzz() + : provisioning_messages_(&session_, encoded_rsa_key_) { + // Opens a session and Generates Nonce. + provisioning_messages_.PrepareSession(keybox_); + } + + ~OEMCryptoProvisioningAPIFuzz() { session_.close(); } + + ProvisioningRoundTrip& provisioning_messages() { + return provisioning_messages_; + } + + private: + Session session_; + ProvisioningRoundTrip provisioning_messages_; +}; + +// Initial setup to create a valid state such as creating session, installing +// golden key box etc. in order to fuzz Load Renewal API. +class OEMCryptoRenewalAPIFuzz : public OEMCryptoLicenseAPIFuzz { + public: + OEMCryptoRenewalAPIFuzz() : renewal_messages_(&license_messages()) {} + + RenewalRoundTrip& renewal_messages() { return renewal_messages_; } + + private: + RenewalRoundTrip renewal_messages_; +}; + +// Convert data to valid enum value. +template +void ConvertDataToValidEnum(T max_enum_value, T* t) { + FuzzedDataProvider fuzzed_enum_data(reinterpret_cast(t), sizeof(T)); + *t = static_cast(fuzzed_enum_data.ConsumeIntegralInRange( + 0, static_cast(max_enum_value))); +} + +// Redirect printf and log statements from oemcrypto functions to a file to +// reduce noise +void RedirectStdoutToFile(); +} // namespace wvoec + +#endif // OEMCRYPTO_FUZZ_HELPER_H_ diff --git a/oemcrypto/test/fuzz_tests/oemcrypto_fuzz_structs.h b/oemcrypto/test/fuzz_tests/oemcrypto_fuzz_structs.h new file mode 100644 index 00000000..a12a1f0a --- /dev/null +++ b/oemcrypto/test/fuzz_tests/oemcrypto_fuzz_structs.h @@ -0,0 +1,31 @@ +// Copyright 2020 Google LLC. All Rights Reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. +#ifndef OEMCRYPTO_FUZZ_STRUCTS_H_ +#define OEMCRYPTO_FUZZ_STRUCTS_H_ + +namespace wvoec { +struct OEMCrypto_Renewal_Response_Fuzz { + // Timer limits in core license response needs to be fuzzed as load renewal + // depends on timer limits loaded from license response. + ODK_TimerLimits timer_limits; + // message(core_response + license_renewal_response) which mimics + // response from license renewal server needs to be fuzzed. core_request + // will be used to generate serialized core response. + oemcrypto_core_message::ODK_RenewalRequest core_request; + // Renewal duration seconds needs to be fuzzed which is part of serialized + // core message from license renewal server. + uint64_t renewal_duration_seconds; + // license_renewal_response is of variable length and not included in this + // structure. +}; + +struct OEMCrypto_Request_Fuzz { + // We would like to fuzz computed signature_length, input core_message_length + // that ODK parses and actual message buffer to the request APIs. + size_t signature_length; + size_t core_message_length; +}; +} // namespace wvoec + +#endif // OEMCRYPTO_FUZZ_STRUCTS_H_ \ No newline at end of file diff --git a/oemcrypto/test/fuzz_tests/oemcrypto_fuzztests.gyp b/oemcrypto/test/fuzz_tests/oemcrypto_fuzztests.gyp index 52d6c67c..a0cd6457 100644 --- a/oemcrypto/test/fuzz_tests/oemcrypto_fuzztests.gyp +++ b/oemcrypto/test/fuzz_tests/oemcrypto_fuzztests.gyp @@ -5,20 +5,48 @@ # Builds under the CDM ./build.py (target platform) build system # Refer to the distribution package's README for details. { - 'variables': { - 'oemcrypto_lib%': '', + 'target_defaults': { + 'type': 'executable', + 'includes': [ + 'oemcrypto_fuzztests.gypi', + ], }, 'targets': [ { - 'target_name': 'wv_ce_cdm_oemcrypto_generate_signature_fuzz_test', - 'type': 'executable', + 'target_name': 'oemcrypto_load_license_fuzz', 'sources': [ - # The test runner and the testing device certificate. - 'oemcrypto_generate_signature.cc', + 'oemcrypto_load_license_fuzz.cc', ], - 'includes': [ - 'oemcrypto_fuzztests.gypi', + }, + { + 'target_name': 'oemcrypto_load_provisioning_fuzz', + 'sources': [ + 'oemcrypto_load_provisioning_fuzz.cc', ], - }, + }, + { + 'target_name': 'oemcrypto_load_renewal_fuzz', + 'sources': [ + 'oemcrypto_load_renewal_fuzz.cc', + ], + }, + { + 'target_name': 'oemcrypto_license_request_fuzz', + 'sources': [ + 'oemcrypto_license_request_fuzz.cc', + ], + }, + { + 'target_name': 'oemcrypto_provisioning_request_fuzz', + 'sources': [ + 'oemcrypto_provisioning_request_fuzz.cc', + ], + }, + { + 'target_name': 'oemcrypto_renewal_request_fuzz', + 'sources': [ + 'oemcrypto_renewal_request_fuzz.cc', + ], + }, ], } diff --git a/oemcrypto/test/fuzz_tests/oemcrypto_fuzztests.gypi b/oemcrypto/test/fuzz_tests/oemcrypto_fuzztests.gypi index 03635156..f0d5d163 100644 --- a/oemcrypto/test/fuzz_tests/oemcrypto_fuzztests.gypi +++ b/oemcrypto/test/fuzz_tests/oemcrypto_fuzztests.gypi @@ -1,48 +1,82 @@ # 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. -# -# Include this in any custom unit test targets. -# Does not include the test runner main. + { + 'variables': { + 'boringssl_libcrypto_path%': '../../../third_party/boringssl/boringssl.gyp:crypto', + 'boringssl_libssl_path%': '../../../third_party/boringssl/boringssl.gyp:ssl', + 'oemcrypto_dir': '../..', + 'platform_specific_dir': '../../../linux/src', + 'privacy_crypto_impl%': 'boringssl', + # Flag used to generate source based code coverage reports. + 'generate_code_coverage_report%': 'false', + 'util_dir': '../../../util', + }, 'sources': [ + '../../odk/src/core_message_deserialize.cpp', + '../../odk/src/core_message_serialize.cpp', '../oec_device_features.cpp', '../oec_key_deriver.cpp', + '../oemcrypto_corpus_generator_helper.cpp', '../oec_session_util.cpp', + '../oemcrypto_corpus_generator_helper.cpp', + 'oemcrypto_fuzz_helper.cc', '../oemcrypto_session_tests_helper.cpp', - '../oemcrypto_session_tests_helper.h', - '../../../cdm/test/device_cert.cpp', - '../../../cdm/test/device_cert.h', + '<(platform_specific_dir)/file_store.cpp', + '<(platform_specific_dir)/log.cpp', + '<(util_dir)/src/platform.cpp', + '<(util_dir)/src/rw_lock.cpp', + '<(util_dir)/src/string_conversions.cpp', + '<(util_dir)/test/test_sleep.cpp', + '<(util_dir)/test/test_clock.cpp', ], 'include_dirs': [ - '../../../core/include', # log.h - '../../include', - '../../ref/src', # oemcrypto_key_ref.h - '../', - '../../../cdm/test', + '../../../third_party/fuzz', + '<(util_dir)/include', + '<(util_dir)/test', + '<(oemcrypto_dir)/include', + '<(oemcrypto_dir)/ref/src', + '<(oemcrypto_dir)/test', + '<(oemcrypto_dir)/test/fuzz_tests', + '<(oemcrypto_dir)/odk/include', + '<(oemcrypto_dir)/odk/src', ], - 'defines': [ - 'OEMCRYPTO_TESTS', - 'OEMCRYPTO_FUZZ_TESTS', - ], - 'libraries': [ - '../../../third_party/fuzz/platforms/x86-64/libFuzzer.a', + 'includes': [ + '../../../util/libssl_dependency.gypi', + '../../ref/oec_ref.gypi', ], 'dependencies': [ - '../../../cdm/cdm.gyp:widevine_ce_cdm_shared', - '../../../third_party/gmock.gyp:gmock', '../../../third_party/gmock.gyp:gtest', + '../../../third_party/gmock.gyp:gmock', + ], + 'defines': [ + 'OEMCRYPTO_FUZZ_TESTS', ], 'conditions': [ - ['oemcrypto_lib==""', { - 'includes': [ - '../../ref/oec_ref.gypi', - ], - }, { - 'libraries': [ - '../../../third_party/fuzz/platforms/x86-64/libFuzzer.a', - '<(oemcrypto_lib)', - ], + ['generate_code_coverage_report=="false"', { + # Include flags to build fuzzer binaries for cluster fuzz. + 'cflags_cc': [ + '-std=c++11', + '-fsanitize=fuzzer,address,undefined', + # Need -g flag to include source line numbers in error stack trace. + '-g', + ], }], + ['generate_code_coverage_report=="true"', { + # Include flags to build fuzzer binaries to generate source based code coverage reports. + 'cflags_cc': [ + '-std=c++11', + '-fprofile-instr-generate', + '-fcoverage-mapping', + ], + }], + ], # conditions + 'ldflags': [ + '-fPIC', + '-fsanitize=fuzzer,address,undefined', + ], + 'libraries': [ + '-lpthread', ], } diff --git a/oemcrypto/test/fuzz_tests/oemcrypto_license_request_fuzz.cc b/oemcrypto/test/fuzz_tests/oemcrypto_license_request_fuzz.cc new file mode 100644 index 00000000..d100ddea --- /dev/null +++ b/oemcrypto/test/fuzz_tests/oemcrypto_license_request_fuzz.cc @@ -0,0 +1,28 @@ +// Copyright 2020 Google LLC. All Rights Reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. + +#include "oemcrypto_fuzz_helper.h" +#include "oemcrypto_fuzz_structs.h" + +namespace wvoec { + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + // Redirect printf and log statements from oemcrypto functions to a file to + // reduce noise + RedirectStdoutToFile(); + // Reject the input if it is less than fuzz data structure size. + if (size < sizeof(OEMCrypto_Request_Fuzz)) { + return 0; + } + // Input for license request API will be modified by OEMCrypto, hence it + // cannot be a const. Fuzzer complains if const identifier is removed of data, + // hence copying data into a non const pointer. + uint8_t* input = new uint8_t[size]; + memcpy(input, data, size); + OEMCryptoLicenseAPIFuzz license_api_fuzz; + license_api_fuzz.license_messages().InjectFuzzedRequestData(input, size); + delete[] input; + return 0; +} +} // namespace wvoec \ No newline at end of file diff --git a/oemcrypto/test/fuzz_tests/oemcrypto_load_license_fuzz.cc b/oemcrypto/test/fuzz_tests/oemcrypto_load_license_fuzz.cc new file mode 100644 index 00000000..e125ae04 --- /dev/null +++ b/oemcrypto/test/fuzz_tests/oemcrypto_load_license_fuzz.cc @@ -0,0 +1,30 @@ +// Copyright 2020 Google LLC. All Rights Reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. + +#include "oemcrypto_fuzz_helper.h" + +namespace wvoec { +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + // Redirect printf and log statements from oemcrypto functions to a file to + // reduce noise + RedirectStdoutToFile(); + if (size < sizeof(ODK_ParsedLicense) + sizeof(MessageData)) { + return 0; + } + OEMCryptoLicenseAPIFuzz license_api_fuzz; + license_api_fuzz.license_messages().SignAndVerifyRequest(); + // Interpreting input fuzz data as unencrypted (core_response + license + // message data) from license server. + license_api_fuzz.license_messages().InjectFuzzedResponseData(data, size); + + // Convert OEMCrypto_LicenseType in core_response to a valid enum value. + ConvertDataToValidEnum( + OEMCrypto_LicenstType_MaxValue, + &license_api_fuzz.license_messages().core_response().license_type); + + license_api_fuzz.license_messages().EncryptAndSignResponse(); + license_api_fuzz.license_messages().LoadResponse(); + return 0; +} +} // namespace wvoec diff --git a/oemcrypto/test/fuzz_tests/oemcrypto_load_provisioning_fuzz.cc b/oemcrypto/test/fuzz_tests/oemcrypto_load_provisioning_fuzz.cc new file mode 100644 index 00000000..739f79f4 --- /dev/null +++ b/oemcrypto/test/fuzz_tests/oemcrypto_load_provisioning_fuzz.cc @@ -0,0 +1,27 @@ +// Copyright 2020 Google LLC. All Rights Reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. + +#include "oemcrypto_fuzz_helper.h" + +namespace wvoec { + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + // Redirect printf and log statements from oemcrypto functions to a file to + // reduce noise + RedirectStdoutToFile(); + if (size < sizeof(ODK_ParsedProvisioning) + sizeof(RSAPrivateKeyMessage)) { + return 0; + } + + OEMCryptoProvisioningAPIFuzz provisioning_api_fuzz; + provisioning_api_fuzz.provisioning_messages().SignAndVerifyRequest(); + // Interpreting input fuzz data as unencrypted(core_response + provisioning + // message data) from provisioning server. + provisioning_api_fuzz.provisioning_messages().InjectFuzzedResponseData(data, + size); + provisioning_api_fuzz.provisioning_messages().EncryptAndSignResponse(); + provisioning_api_fuzz.provisioning_messages().LoadResponse(); + return 0; +} +} // namespace wvoec diff --git a/oemcrypto/test/fuzz_tests/oemcrypto_load_renewal_fuzz.cc b/oemcrypto/test/fuzz_tests/oemcrypto_load_renewal_fuzz.cc new file mode 100644 index 00000000..93baee6d --- /dev/null +++ b/oemcrypto/test/fuzz_tests/oemcrypto_load_renewal_fuzz.cc @@ -0,0 +1,42 @@ +// Copyright 2020 Google LLC. All Rights Reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. + +#include "oemcrypto_fuzz_helper.h" +#include "oemcrypto_fuzz_structs.h" + +namespace wvoec { + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + // Redirect printf and log statements from oemcrypto functions to a file to + // reduce noise + RedirectStdoutToFile(); + if (size < sizeof(OEMCrypto_Renewal_Response_Fuzz)) { + return 0; + } + // Copy input data to OEMCrypto_Renewal_Response_Fuzz and rest of message + // into encrypted license_renewal_response. + OEMCrypto_Renewal_Response_Fuzz fuzzed_data; + memcpy(&fuzzed_data, data, sizeof(fuzzed_data)); + const uint8_t* renewal_response = + data + sizeof(OEMCrypto_Renewal_Response_Fuzz); + const size_t renewal_response_size = + size - sizeof(OEMCrypto_Renewal_Response_Fuzz); + + OEMCryptoLicenseAPIFuzz license_api_fuzz; + license_api_fuzz.license_messages().SignAndVerifyRequest(); + license_api_fuzz.license_messages().CreateDefaultResponse(); + // Inject timer limits from fuzzed input to timer_limits field from + // core license response. + license_api_fuzz.license_messages().InjectFuzzedTimerLimits(fuzzed_data); + license_api_fuzz.license_messages().EncryptAndSignResponse(); + license_api_fuzz.license_messages().LoadResponse(); + + OEMCryptoRenewalAPIFuzz renewal_response_fuzz; + renewal_response_fuzz.renewal_messages().SignAndVerifyRequest(); + renewal_response_fuzz.renewal_messages().InjectFuzzedResponseData( + fuzzed_data, renewal_response, renewal_response_size); + renewal_response_fuzz.renewal_messages().LoadResponse(); + return 0; +} +} // namespace wvoec \ No newline at end of file diff --git a/oemcrypto/test/fuzz_tests/oemcrypto_provisioning_request_fuzz.cc b/oemcrypto/test/fuzz_tests/oemcrypto_provisioning_request_fuzz.cc new file mode 100644 index 00000000..1cda2abd --- /dev/null +++ b/oemcrypto/test/fuzz_tests/oemcrypto_provisioning_request_fuzz.cc @@ -0,0 +1,28 @@ +// Copyright 2020 Google LLC. All Rights Reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. + +#include "oemcrypto_fuzz_helper.h" +#include "oemcrypto_fuzz_structs.h" + +namespace wvoec { +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + // Redirect printf and log statements from oemcrypto functions to a file to + // reduce noise + RedirectStdoutToFile(); + // If input size is less than fuzz data structure size, reject the input. + if (size < sizeof(OEMCrypto_Request_Fuzz)) { + return 0; + } + // Input for provisioning request API will be modified by OEMCrypto, hence it + // cannot be a const. Fuzzer complains if const identifier is removed of data, + // hence copying data into a non const pointer. + uint8_t* input = new uint8_t[size]; + memcpy(input, data, size); + OEMCryptoProvisioningAPIFuzz provisioning_api_fuzz; + provisioning_api_fuzz.provisioning_messages().InjectFuzzedRequestData(input, + size); + delete[] input; + return 0; +} +} // namespace wvoec \ No newline at end of file diff --git a/oemcrypto/test/fuzz_tests/oemcrypto_renewal_request_fuzz.cc b/oemcrypto/test/fuzz_tests/oemcrypto_renewal_request_fuzz.cc new file mode 100644 index 00000000..67ecb0a9 --- /dev/null +++ b/oemcrypto/test/fuzz_tests/oemcrypto_renewal_request_fuzz.cc @@ -0,0 +1,27 @@ +// Copyright 2020 Google LLC. All Rights Reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. + +#include "oemcrypto_fuzz_helper.h" +#include "oemcrypto_fuzz_structs.h" + +namespace wvoec { +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + // Redirect printf and log statements from oemcrypto functions to a file to + // reduce noise + RedirectStdoutToFile(); + // If input size is less than fuzz data structure, reject the input. + if (size < sizeof(OEMCrypto_Request_Fuzz)) { + return 0; + } + // Input for renewal request API will be modified by OEMCrypto, hence it + // cannot be a const. Fuzzer complains if const identifier is removed of data, + // hence copying data into a non const pointer. + uint8_t* input = new uint8_t[size]; + memcpy(input, data, size); + OEMCryptoRenewalAPIFuzz renewal_api_fuzz; + renewal_api_fuzz.renewal_messages().InjectFuzzedRequestData(input, size); + delete[] input; + return 0; +} +} // namespace wvoec \ No newline at end of file diff --git a/oemcrypto/test/oec_device_features.cpp b/oemcrypto/test/oec_device_features.cpp index 74f4d492..7c4fa267 100644 --- a/oemcrypto/test/oec_device_features.cpp +++ b/oemcrypto/test/oec_device_features.cpp @@ -8,58 +8,15 @@ #include -#ifdef _WIN32 -# include -#else -# include -# include -#endif - #include #include "oec_test_data.h" +#include "test_sleep.h" namespace wvoec { DeviceFeatures global_features; -bool CanChangeTime() { -#ifdef _WIN32 - LUID desired_id; - if (!LookupPrivilegeValue(nullptr, SE_SYSTEMTIME_NAME, &desired_id)) - return false; - HANDLE token; - if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &token)) - return false; - std::unique_ptr safe_token(token, &CloseHandle); - - // This queries all the permissions given to the token to determine if we can - // change the system time. Note this is subtly different from PrivilegeCheck - // as that only checks "enabled" privileges; even with admin rights, the - // privilege is default disabled, even when granted. - - DWORD size = 0; - // Determine how big we need to allocate first. - GetTokenInformation(token, TokenPrivileges, nullptr, 0, &size); - // Since TOKEN_PRIVILEGES uses a variable-length array, we need to use malloc - std::unique_ptr privileges( - (TOKEN_PRIVILEGES*)malloc(size), &free); - if (privileges && GetTokenInformation(token, TokenPrivileges, - privileges.get(), size, &size)) { - for (int i = 0; i < privileges->PrivilegeCount; i++) { - if (privileges->Privileges[i].Luid.HighPart == desired_id.HighPart && - privileges->Privileges[i].Luid.LowPart == desired_id.LowPart) { - return true; - } - } - } - - return false; -#else - return getuid() == 0; -#endif -} - void DeviceFeatures::Initialize() { if (initialized_) return; uses_keybox = false; @@ -188,8 +145,11 @@ std::string DeviceFeatures::RestrictFilter(const std::string& initial_filter) { // clang-format on // Some tests may require root access. If user is not root, filter these tests // out. - if (!CanChangeTime()) { + if (!wvcdm::TestSleep::CanChangeSystemTime()) { + printf("Filtering out TimeRollbackPrevention.\n"); FilterOut(&filter, "*TimeRollbackPrevention*"); + } else { + printf("Can change time. I will run TimeRollbackPrevention.\n"); } // Performance tests take a long time. Filter them out if they are not // specifically requested. diff --git a/oemcrypto/test/oec_key_deriver.cpp b/oemcrypto/test/oec_key_deriver.cpp index 34fb0fe5..d2bff254 100644 --- a/oemcrypto/test/oec_key_deriver.cpp +++ b/oemcrypto/test/oec_key_deriver.cpp @@ -61,6 +61,12 @@ void Encryptor::PadAndEncryptProvisioningMessage( EXPECT_EQ(1, GetRandBytes(data->rsa_key_iv, KEY_IV_SIZE)); ASSERT_EQ(enc_key_.size(), KEY_SIZE); *encrypted = *data; + if (data->rsa_key_length > sizeof(data->rsa_key)) { + // OEMCrypto Fuzzing: fuzzed |rsa_key_length| overflows the allocated + // buffer. Skip encryption in that case. + return; + } + size_t padding = AES_BLOCK_SIZE - (data->rsa_key_length % AES_BLOCK_SIZE); memset(data->rsa_key + data->rsa_key_length, static_cast(padding), padding); diff --git a/oemcrypto/test/oec_session_util.cpp b/oemcrypto/test/oec_session_util.cpp index 49ab4acf..7cc37f8b 100644 --- a/oemcrypto/test/oec_session_util.cpp +++ b/oemcrypto/test/oec_session_util.cpp @@ -31,8 +31,10 @@ #include "core_message_serialize.h" #include "disallow_copy_and_assign.h" #include "log.h" +#include "odk_structs.h" #include "oec_device_features.h" #include "oec_test_data.h" +#include "oemcrypto_corpus_generator_helper.h" #include "oemcrypto_types.h" #include "platform.h" #include "string_conversions.h" @@ -162,6 +164,10 @@ void RoundTrip(gen_signature_length, + core_message_length, data); + } vector gen_signature(gen_signature_length); ASSERT_EQ(PrepAndSignRequest(session()->session_id(), data.data(), @@ -177,6 +183,27 @@ void RoundTrip +void RoundTrip::InjectFuzzedRequestData(uint8_t* data, + size_t size) { + OEMCrypto_Request_Fuzz fuzz_structure; + // Copy data into fuzz structure, cap signature length at 1mb as it will be + // used to initialize signature vector. + memcpy(&fuzz_structure, data, sizeof(fuzz_structure)); + fuzz_structure.signature_length = + std::min(fuzz_structure.signature_length, MB); + vector signature(fuzz_structure.signature_length); + + // Interpret rest of data as actual message buffer to request APIs. + uint8_t* message_ptr = data + sizeof(fuzz_structure); + size_t message_size = size - sizeof(fuzz_structure); + PrepAndSignRequest(session()->session_id(), message_ptr, message_size, + &fuzz_structure.core_message_length, signature.data(), + &fuzz_structure.signature_length); +} + template OEMCrypto_Substring RoundTripnonce(); +} + OEMCryptoResult ProvisioningRoundTrip::LoadResponse(Session* session) { EXPECT_NE(session, nullptr); + // Write corpus for oemcrypto_load_provisioning_fuzz. Fuzz script expects + // unencrypted response from provisioning server as input corpus data. + // Data will be encrypted and signed again explicitly by fuzzer script after + // mutations. + if (ShouldGenerateCorpus()) { + const std::string file_name = + GetFileName("oemcrypto_load_provisioning_fuzz_seed_corpus"); + // Corpus for license response fuzzer should be in the format: + // unencrypted (core_response + response_data). + AppendToFile(file_name, reinterpret_cast(&core_response_), + sizeof(ODK_ParsedProvisioning)); + AppendToFile(file_name, reinterpret_cast(&response_data_), + sizeof(response_data_)); + } size_t wrapped_key_length = 0; const OEMCryptoResult sts = LoadResponseNoRetry(session, &wrapped_key_length); if (sts != OEMCrypto_ERROR_SHORT_BUFFER) return sts; @@ -472,6 +526,69 @@ void LicenseRoundTrip::CreateDefaultResponse() { FillCoreResponseSubstrings(); } +void LicenseRoundTrip::ConvertDataToValidBools(ODK_ParsedLicense* t) { + t->nonce_required = ConvertByteToValidBoolean(&t->nonce_required); + t->timer_limits.soft_enforce_playback_duration = ConvertByteToValidBoolean( + &t->timer_limits.soft_enforce_playback_duration); + t->timer_limits.soft_enforce_rental_duration = + ConvertByteToValidBoolean(&t->timer_limits.soft_enforce_rental_duration); +} + +void LicenseRoundTrip::InjectFuzzedTimerLimits( + OEMCrypto_Renewal_Response_Fuzz& fuzzed_data) { + // Interpreting fuzz data as timer limits. + // Copy timer limits from data. + memcpy(&core_response_.timer_limits, &fuzzed_data.timer_limits, + sizeof(fuzzed_data.timer_limits)); + ConvertDataToValidBools(&core_response_); +} + +void LicenseRoundTrip::InjectFuzzedResponseData(const uint8_t* data, + size_t size) { + // Interpreting fuzz data as unencrypted core_response + message_data + const size_t core_response_size = sizeof(ODK_ParsedLicense); + // Copy core_response from data. + memcpy(&core_response_, data, core_response_size); + // Maximum number of keys could be kMaxNumKeys(30). key_array_length can be + // any random value as it is read from fuzz data. + // Key data array(MessageKeyData keys[kMaxNumKeys]) will be looped over + // key_array_length number of times during LoadLicense. If key_array_length is + // more than kMaxNumKeys, setting it to max value of kMaxNumKeys as we should + // not go out of bounds of this array length. For corpus, this value is + // already hard coded to 4. + if (core_response_.key_array_length > kMaxNumKeys) { + core_response_.key_array_length = kMaxNumKeys; + } + // For corpus data, this value gets set to 4, but we need to test other + // scenarios too, hence reading key_array_length value. + set_num_keys(core_response_.key_array_length); + ConvertDataToValidBools(&core_response_); + // TODO(b/157520981): Once assertion bug is fixed, for loop can be removed. + // Workaround for the above bug: key_data.length and key_id.length are being + // used in AES decryption process and are expected to be a multiple of 16. An + // assertion in AES decryption fails if this condition is not met which will + // crash fuzzer. + for (uint32_t i = 0; i < num_keys_; ++i) { + size_t key_data_length = core_response_.key_array[i].key_data.length; + size_t key_id_length = core_response_.key_array[i].key_id.length; + if (key_data_length % 16 != 0) { + core_response_.key_array[i].key_data.length = + key_data_length - (key_data_length % 16); + } + if (key_id_length % 16 != 0) { + core_response_.key_array[i].key_id.length = + key_id_length - (key_id_length % 16); + } + } + + // Copy response_data from data and set nonce to match one in request to pass + // nonce validations. + memcpy(&response_data_, data + core_response_size, sizeof(response_data_)); + for (uint32_t i = 0; i < num_keys_; ++i) { + response_data_.keys[i].control.nonce = session()->nonce(); + } +} + void LicenseRoundTrip::CreateResponseWithGenericCryptoKeys() { CreateDefaultResponse(); response_data_.keys[0].control.control_bits |= @@ -540,17 +657,28 @@ void LicenseRoundTrip::EncryptAndSignResponse() { 2 * MAC_KEY_SIZE, response_data_.mac_key_iv); for (unsigned int i = 0; i < num_keys_; i++) { - memcpy(iv_buffer, &response_data_.keys[i].control_iv[0], KEY_IV_SIZE); - AES_KEY aes_key; - AES_set_encrypt_key(&response_data_.keys[i].key_data[0], 128, &aes_key); - AES_cbc_encrypt( - reinterpret_cast(&response_data_.keys[i].control), - reinterpret_cast(&encrypted_response_data_.keys[i].control), - KEY_SIZE, &aes_key, iv_buffer, AES_ENCRYPT); - session_->key_deriver().CBCEncrypt( - &response_data_.keys[i].key_data[0], - &encrypted_response_data_.keys[i].key_data[0], - response_data_.keys[i].key_data_length, response_data_.keys[i].key_iv); + // OEMCrypto Fuzzing skip encryption: key_data_length can be any number when + // called from fuzzer. We want to skip encryption if that happens and let + // LoadLicense be called with unencrypted data for that key. OEMCrypto + // Fuzzing skip encryption: key_data_length being a random value will + // encrypt data which is not expected to, there by leading to inefficient + // fuzzing. + if (response_data_.keys[i].key_data_length <= + sizeof(response_data_.keys[i].key_data) && + response_data_.keys[i].key_data_length % 16 == 0) { + memcpy(iv_buffer, &response_data_.keys[i].control_iv[0], KEY_IV_SIZE); + AES_KEY aes_key; + AES_set_encrypt_key(&response_data_.keys[i].key_data[0], 128, &aes_key); + AES_cbc_encrypt( + reinterpret_cast(&response_data_.keys[i].control), + reinterpret_cast(&encrypted_response_data_.keys[i].control), + KEY_SIZE, &aes_key, iv_buffer, AES_ENCRYPT); + session_->key_deriver().CBCEncrypt( + &response_data_.keys[i].key_data[0], + &encrypted_response_data_.keys[i].key_data[0], + response_data_.keys[i].key_data_length, + response_data_.keys[i].key_iv); + } } if (api_version_ < kCoreMessagesAPI) { serialized_core_message_.resize(0); @@ -588,6 +716,21 @@ void LicenseRoundTrip::EncryptAndSignResponse() { OEMCryptoResult LicenseRoundTrip::LoadResponse(Session* session) { EXPECT_NE(session, nullptr); + // Write corpus for oemcrypto_load_license_fuzz. Fuzz script expects + // unecnrypted response from license server as input corpus data. + // Data will be encrypted and signed again explicitly by fuzzer script + // after mutations. + if (ShouldGenerateCorpus()) { + const std::string file_name = + GetFileName("oemcrypto_load_license_fuzz_seed_corpus"); + // Corpus for license response fuzzer should be in the format: + // core_response + response_data. + AppendToFile(file_name, reinterpret_cast(&core_response_), + sizeof(ODK_ParsedLicense)); + AppendToFile(file_name, reinterpret_cast(&response_data_), + sizeof(response_data_)); + } + // Some tests adjust the offset to be beyond the length of the message. Here, // we create a duplicate of the main message buffer so that these offsets do // not point to garbage data. The goal is to make sure OEMCrypto is verifying @@ -630,7 +773,7 @@ OEMCryptoResult LicenseRoundTrip::LoadResponse(Session* session) { // Note: we verify content licenses here. For entitlement license, we verify // the key control blocks after loading entitled content keys. - if (license_type_ == OEMCrypto_ContentLicense) VerifyTestKeys(); + if (license_type_ == OEMCrypto_ContentLicense) VerifyTestKeys(session); } return result; } @@ -644,12 +787,12 @@ OEMCryptoResult LicenseRoundTrip::ReloadResponse(Session* session) { // with the truth key control block. Failures in this function probably // indicate the OEMCrypto_LoadLicense/LoadKeys did not correctly process the key // control block. -void LicenseRoundTrip::VerifyTestKeys() { +void LicenseRoundTrip::VerifyTestKeys(Session* session) { for (unsigned int i = 0; i < num_keys_; i++) { KeyControlBlock block; size_t size = sizeof(block); OEMCryptoResult sts = OEMCrypto_QueryKeyControl( - session_->session_id(), response_data_.keys[i].key_id, + session->session_id(), response_data_.keys[i].key_id, response_data_.keys[i].key_id_length, reinterpret_cast(&block), &size); if (sts != OEMCrypto_ERROR_NOT_IMPLEMENTED) { @@ -906,7 +1049,46 @@ void RenewalRoundTrip::EncryptAndSignResponse() { &response_signature_); } +void RenewalRoundTrip::InjectFuzzedResponseData( + OEMCrypto_Renewal_Response_Fuzz& fuzzed_data, + const uint8_t* renewal_response, const size_t renewal_response_size) { + // Serializing core message. + // This call also sets nonce in core response to match with session nonce. + oemcrypto_core_message::serialize::CreateCoreRenewalResponse( + fuzzed_data.core_request, fuzzed_data.renewal_duration_seconds, + &serialized_core_message_); + + // Copy serialized core message and encrypted response from data and + // calculate signature. Now we will have a valid signature for data generated + // by fuzzer. + encrypted_response_.assign(serialized_core_message_.begin(), + serialized_core_message_.end()); + encrypted_response_.insert(encrypted_response_.end(), renewal_response, + renewal_response + renewal_response_size); + session()->key_deriver().ServerSignBuffer(encrypted_response_.data(), + encrypted_response_.size(), + &response_signature_); +} + OEMCryptoResult RenewalRoundTrip::LoadResponse(Session* session) { + // Write corpus for oemcrypto_load_renewal_fuzz. Fuzz script expects + // encrypted response from Renewal server as input corpus data. + // Data will be signed again explicitly by fuzzer script after mutations. + if (ShouldGenerateCorpus()) { + const std::string file_name = + GetFileName("oemcrypto_load_renewal_fuzz_seed_corpus"); + // Corpus for renewal response fuzzer should be in the format: + // OEMCrypto_Renewal_Response_Fuzz + license_renewal_response. + OEMCrypto_Renewal_Response_Fuzz renewal_response_fuzz; + renewal_response_fuzz.core_request = core_request_; + renewal_response_fuzz.renewal_duration_seconds = renewal_duration_seconds_; + AppendToFile(file_name, + reinterpret_cast(&renewal_response_fuzz), + sizeof(renewal_response_fuzz)); + AppendToFile(file_name, + reinterpret_cast(&encrypted_response_data_), + sizeof(encrypted_response_data_)); + } if (license_messages_->api_version() < kCoreMessagesAPI) { return OEMCrypto_RefreshKeys( session->session_id(), encrypted_response_.data(), @@ -1009,7 +1191,7 @@ void Session::GenerateDerivedKeysFromSessionKey() { // Uses test certificate. vector session_key; vector enc_session_key; - if (public_rsa_ == nullptr) PreparePublicKey(); + ASSERT_NE(public_rsa_, nullptr) << "No public RSA key loaded in test code."; // A failure here probably indicates that there is something wrong with the // test program and its dependency on BoringSSL. ASSERT_TRUE(GenerateRSASessionKey(&session_key, &enc_session_key)); @@ -1297,12 +1479,11 @@ void Session::VerifyRSASignature(const vector& message, const uint8_t* signature, size_t signature_length, RSA_Padding_Scheme padding_scheme) { - EXPECT_TRUE(nullptr != public_rsa_) - << "No public RSA key loaded in test code.\n"; + ASSERT_NE(public_rsa_, nullptr) << "No public RSA key loaded in test code."; - EXPECT_EQ(static_cast(RSA_size(public_rsa_)), signature_length) + ASSERT_EQ(static_cast(RSA_size(public_rsa_)), signature_length) << "Signature size is wrong. " << signature_length << ", should be " - << RSA_size(public_rsa_) << "\n"; + << RSA_size(public_rsa_); if (padding_scheme == kSign_RSASSA_PSS) { boringssl_ptr pkey(EVP_PKEY_new()); @@ -1471,7 +1652,10 @@ void Session::VerifyReport(Test_PST_Report expected, int64_t time_first_decrypt, int64_t time_last_decrypt) { const int64_t now = wvcdm::Clock().GetCurrentTime(); - expected.seconds_since_license_received = now - time_license_received; + expected.seconds_since_license_received = + (time_license_received > 0 && time_license_received < now) + ? now - time_license_received + : 0; expected.seconds_since_first_decrypt = (time_first_decrypt > 0 && time_first_decrypt < now) ? now - time_first_decrypt @@ -1482,4 +1666,41 @@ void Session::VerifyReport(Test_PST_Report expected, : 0; ASSERT_NO_FATAL_FAILURE(VerifyPST(expected)); } + +bool ConvertByteToValidBoolean(const bool* in) { + const char* buf = reinterpret_cast(in); + for (size_t i = 0; i < sizeof(bool); i++) { + if (buf[i]) { + return true; + } + } + return false; +} + +template +void WriteRequestApiCorpus(size_t signature_length, size_t core_message_length, + vector& data) { + std::string file_name; + if (std::is_same::value) { + file_name = GetFileName("oemcrypto_license_request_fuzz_seed_corpus"); + } else if (std::is_same< + CoreRequest, + oemcrypto_core_message::ODK_ProvisioningRequest>::value) { + file_name = GetFileName("oemcrypto_provisioning_request_fuzz_seed_corpus"); + } else if (std::is_same::value) { + file_name = GetFileName("oemcrypto_renewal_request_fuzz_seed_corpus"); + } else { + LOGE("Invalid CoreRequest type while writing request api corups."); + } + // Corpus for request APIs should be signature_length + core_message_length + + // data pointer. + AppendToFile(file_name, reinterpret_cast(&signature_length), + sizeof(signature_length)); + AppendToFile(file_name, reinterpret_cast(&core_message_length), + sizeof(core_message_length)); + AppendToFile(file_name, reinterpret_cast(data.data()), + data.size()); +} } // namespace wvoec diff --git a/oemcrypto/test/oec_session_util.h b/oemcrypto/test/oec_session_util.h index 36848d52..ffde2994 100644 --- a/oemcrypto/test/oec_session_util.h +++ b/oemcrypto/test/oec_session_util.h @@ -18,6 +18,7 @@ #include "odk.h" #include "oec_device_features.h" #include "oec_key_deriver.h" +#include "oemcrypto_fuzz_structs.h" #include "oemcrypto_types.h" #include "pst_report.h" @@ -32,6 +33,8 @@ void PrintTo(const vector& value, ostream* os); } // namespace std namespace wvoec { +// OEMCrypto Fuzzing: Set max signture length to 1mb. +const size_t MB = 1024 * 1024; // Make sure this is larger than kMaxKeysPerSession, in oemcrypto_test.cpp constexpr size_t kMaxNumKeys = 30; @@ -158,6 +161,9 @@ class RoundTrip { // Have OEMCrypto sign a request message and then verify the signature and the // core message. virtual void SignAndVerifyRequest(); + // Used for OEMCrypto Fuzzing: Function to convert fuzzer data to valid + // License/Provisioning/Renwal request data that can be serialized. + virtual void InjectFuzzedRequestData(uint8_t* data, size_t size); // Create a default |response_data| and |core_response|. virtual void CreateDefaultResponse() = 0; // Copy fields from |response_data| to |padded_response_data|, encrypting @@ -241,6 +247,11 @@ class ProvisioningRoundTrip void set_allowed_schemes(uint32_t allowed_schemes) { allowed_schemes_ = allowed_schemes; } + // Used for OEMCrypto Fuzzing: Function to convert fuzzer data to valid + // provisioning response data that can be parsed. Calculates signature for + // data generated by fuzzer, so that signature validation passes when parsing + // provisioning response. + void InjectFuzzedResponseData(const uint8_t* data, size_t size); protected: void VerifyRequestSignature(const vector& data, @@ -286,6 +297,18 @@ class LicenseRoundTrip license_type_(OEMCrypto_ContentLicense), request_hash_() {} void CreateDefaultResponse() override; + // Used for OEMCrypto Fuzzing: Function to inject fuzzed timer limits + // into timer_limits field from core_response. We need to fuzz timer + // limits in order to efficiently fuzz load renewal response API. + void InjectFuzzedTimerLimits(OEMCrypto_Renewal_Response_Fuzz& fuzzed_data); + // Used for OEMCrypto Fuzzing: Function to convert fuzzer data to valid + // License response data that can be parsed. Calculates signature for data + // generated by fuzzer, so that signature validation passes when parsing + // license response. + void InjectFuzzedResponseData(const uint8_t* data, size_t size); + // Used for OEMCrypto Fuzzing: Convert boolean flags in parsed_license to + // valid bytes to avoid errors from msan. + void ConvertDataToValidBools(ODK_ParsedLicense* t); // Create a license with four keys. Each key is responsible for one of generic // encrypt (key 0), decrypt (key 1), sign (key 2) and verify (key 3). Each key // is allowed only one type of operation. @@ -298,7 +321,7 @@ class LicenseRoundTrip // Reload an offline license into a different session. This derives new mac // keys and then calls LoadResponse. OEMCryptoResult ReloadResponse(Session* session); - void VerifyTestKeys(); + void VerifyTestKeys(Session* session); // Set the default key control block for all keys. This is used in // CreateDefaultResponse. The key control block determines the restrictions // that OEMCrypto should place on a key's use. For example, it specifies the @@ -386,6 +409,9 @@ class RenewalRoundTrip is_release_(false) {} void CreateDefaultResponse() override; void EncryptAndSignResponse() override; + void InjectFuzzedResponseData(OEMCrypto_Renewal_Response_Fuzz& fuzzed_data, + const uint8_t* renewal_response, + const size_t renewal_response_size); OEMCryptoResult LoadResponse() override { return LoadResponse(session_); } OEMCryptoResult LoadResponse(Session* session) override; uint64_t renewal_duration_seconds() const { @@ -599,6 +625,13 @@ class Session { string pst_; }; +// Used for OEMCrypto Fuzzing: Convert byte to a valid boolean to avoid errors +// generated by msan. +bool ConvertByteToValidBoolean(const bool* in); +// Used for OEMCrypto Fuzzing: Generates corpus for request APIs. +template +void WriteRequestApiCorpus(size_t signature_length, size_t core_message_length, + vector& data); } // namespace wvoec #endif // CDM_OEC_SESSION_UTIL_H_ diff --git a/oemcrypto/test/oemcrypto_corpus_generator_helper.cpp b/oemcrypto/test/oemcrypto_corpus_generator_helper.cpp new file mode 100644 index 00000000..33c0f7ac --- /dev/null +++ b/oemcrypto/test/oemcrypto_corpus_generator_helper.cpp @@ -0,0 +1,34 @@ +/* Copyright 2020 Google LLC. All rights reserved. This file and proprietary */ +/* source code may only be used and distributed under the Widevine Master */ +/* License Agreement. */ +#include "oemcrypto_corpus_generator_helper.h" + +#include +#include + +namespace wvoec { +bool g_generate_corpus; + +void AppendToFile(const std::string& file_name, const char* message, + const size_t message_size) { + std::ofstream filebuf(file_name.c_str(), std::ios::app | std::ios::binary); + if (!filebuf) { + std::cout << "Cannot open file " << file_name.c_str() << std::endl; + } + filebuf.write(message, message_size); + filebuf.close(); +} + +std::string GetFileName(const char* directory) { + std::string file_name(PATH_TO_CORPUS); + file_name += directory; + file_name += "/"; + file_name += std::to_string(rand()); + return file_name; +} + +void SetGenerateCorpus(bool should_generate_corpus) { + g_generate_corpus = should_generate_corpus; +} +bool ShouldGenerateCorpus() { return g_generate_corpus; } +} // namespace wvoec diff --git a/oemcrypto/test/oemcrypto_corpus_generator_helper.h b/oemcrypto/test/oemcrypto_corpus_generator_helper.h new file mode 100644 index 00000000..e145fe63 --- /dev/null +++ b/oemcrypto/test/oemcrypto_corpus_generator_helper.h @@ -0,0 +1,25 @@ +/* Copyright 2020 Google LLC. All rights reserved. This file and proprietary */ +/* source code may only be used and distributed under the Widevine Master */ +/* License Agreement. */ +#ifndef CDM_OEMCRYPTO_CORPUS_GENERATOR_HELPER_H_ +#define CDM_OEMCRYPTO_CORPUS_GENERATOR_HELPER_H_ + +#define PATH_TO_CORPUS "./oemcrypto/test/fuzz_tests/corpus/" + +#include +#include +#include + +namespace wvoec { +void AppendToFile(const std::string& file_name, const char* message, + const size_t message_size); + +std::string GetFileName(const char* directory); + +void SetGenerateCorpus(bool should_generate_corpus); +// Output of this function decides if binary data needs to be written +// to corpus files or not. Controlled by --generate_corpus flag. +bool ShouldGenerateCorpus(); +} // namespace wvoec + +#endif // CDM_OEMCRYPTO_CORPUS_GENERATOR_HELPER_H_ diff --git a/oemcrypto/test/oemcrypto_session_tests_helper.cpp b/oemcrypto/test/oemcrypto_session_tests_helper.cpp index 9e2bc7e7..da8d5ec2 100644 --- a/oemcrypto/test/oemcrypto_session_tests_helper.cpp +++ b/oemcrypto/test/oemcrypto_session_tests_helper.cpp @@ -81,6 +81,6 @@ void SessionUtil::InstallTestRSAKey(Session* s) { ASSERT_NO_FATAL_FAILURE(s->InstallRSASessionTestKey(wrapped_rsa_key_)); } // Test RSA key should be loaded. - ASSERT_NO_FATAL_FAILURE(s->GenerateDerivedKeysFromSessionKey()); + ASSERT_NO_FATAL_FAILURE(s->PreparePublicKey()); } } // namespace wvoec diff --git a/oemcrypto/test/oemcrypto_test.cpp b/oemcrypto/test/oemcrypto_test.cpp index 06144d70..50e1027f 100644 --- a/oemcrypto/test/oemcrypto_test.cpp +++ b/oemcrypto/test/oemcrypto_test.cpp @@ -14,12 +14,6 @@ #include #include -#ifdef _WIN32 -# include -#else -# include -#endif - #include #include #include @@ -122,37 +116,6 @@ const size_t kLargeMessageSize[] = { 8*KiB, 8*KiB, 16*KiB, 32*KiB}; // const size_t kAV1NumberSubsamples[] = { 72, 144, 288, 576}; // clang-format on -/** @return The Unix time of the given time point. */ -template -uint64_t UnixTime( - const std::chrono::time_point& point) { - return point.time_since_epoch() / std::chrono::seconds(1); -} - -#ifdef _WIN32 -using NativeTime = SYSTEMTIME; -#else -using NativeTime = timeval; -#endif - -void AddNativeTime(int64_t delta_seconds, NativeTime* time) { -#ifdef _WIN32 - // See remarks from this for why this series is used. - // https://msdn.microsoft.com/en-us/f77cdf86-0f97-4a89-b565-95b46fa7d65b - FILETIME file_time; - ASSERT_TRUE(SystemTimeToFileTime(time, &file_time)); - uint64_t long_time = static_cast(file_time.dwLowDateTime) | - (static_cast(file_time.dwHighDateTime) << 32); - long_time += - delta_seconds * 1e7; // long_time is in 100-nanosecond intervals. - file_time.dwLowDateTime = long_time & ((1ull << 32) - 1); - file_time.dwHighDateTime = long_time >> 32; - ASSERT_TRUE(FileTimeToSystemTime(&file_time, time)); -#else - time->tv_sec += delta_seconds; -#endif -} - } // namespace class OEMCryptoClientTest : public ::testing::Test, public SessionUtil { @@ -191,13 +154,13 @@ class OEMCryptoClientTest : public ::testing::Test, public SessionUtil { // tests are failing when the device has the wrong keybox installed. TEST_F(OEMCryptoClientTest, VersionNumber) { const std::string log_message = - "OEMCrypto unit tests for API 16.2. Tests last updated 2020-03-27"; + "OEMCrypto unit tests for API 16.3. Tests last updated 2020-06-01"; cout << " " << log_message << "\n"; LOGI("%s", log_message.c_str()); // If any of the following fail, then it is time to update the log message // above. EXPECT_EQ(ODK_MAJOR_VERSION, 16); - EXPECT_EQ(ODK_MINOR_VERSION, 2); + EXPECT_EQ(ODK_MINOR_VERSION, 3); EXPECT_EQ(kCurrentAPI, 16u); const char* level = OEMCrypto_SecurityLevel(); ASSERT_NE(nullptr, level); @@ -725,6 +688,7 @@ TEST_F(OEMCryptoProv30Test, GetCertOnlyAPI16) { ASSERT_NO_FATAL_FAILURE(s.open()); // Install the DRM Cert's RSA key. ASSERT_NO_FATAL_FAILURE(s.InstallRSASessionTestKey(wrapped_rsa_key_)); + ASSERT_NO_FATAL_FAILURE(s.PreparePublicKey()); // Request the OEM Cert. -- This should NOT load the OEM Private key. vector public_cert; size_t public_cert_length = 0; @@ -890,7 +854,13 @@ TEST_P(OEMCryptoLicenseTest, LoadKeyWithNoRequest) { license_messages_.core_request().api_minor_version = ODK_MINOR_VERSION; ASSERT_NO_FATAL_FAILURE(license_messages_.CreateDefaultResponse()); ASSERT_NO_FATAL_FAILURE(license_messages_.EncryptAndSignResponse()); - ASSERT_EQ(OEMCrypto_SUCCESS, license_messages_.LoadResponse()); + + // Load license in a different session, which did not create the request. + Session session2; + ASSERT_NO_FATAL_FAILURE(session2.open()); + ASSERT_NO_FATAL_FAILURE(InstallTestRSAKey(&session2)); + ASSERT_NO_FATAL_FAILURE(session2.GenerateDerivedKeysFromSessionKey()); + ASSERT_EQ(OEMCrypto_SUCCESS, license_messages_.LoadResponse(&session2)); } // Verify that a license may be loaded with a nonce. @@ -1865,7 +1835,7 @@ TEST_P(OEMCryptoRefreshTest, RefreshLargeBuffer) { LoadLicense(); RenewalRoundTrip renewal_messages(&license_messages_); const size_t max_size = GetResourceValue(kLargeMessageSize); - license_messages_.set_message_size(max_size); + renewal_messages.set_message_size(max_size); MakeRenewalRequest(&renewal_messages); LoadRenewal(&renewal_messages, OEMCrypto_SUCCESS); } @@ -2233,10 +2203,11 @@ class OEMCryptoSessionTestsDecryptTests void TestDecryptCENC() { OEMCryptoResult sts; + // OEMCrypto only supports providing a decrypt hash for one sample. + if (samples_.size() > 1) verify_crc_ = false; + // If supported, check the decrypt hashes. if (verify_crc_) { - // OEMCrypto only supports providing a decrypt hash for the first sample - // in the sample array. const TestSample& sample = samples_[0]; uint32_t hash = @@ -2291,7 +2262,7 @@ class OEMCryptoSessionTestsDecryptTests } } } - if (global_features.supports_crc) { + if (verify_crc_) { uint32_t frame; ASSERT_EQ(OEMCrypto_GetHashErrorCode(session_.session_id(), &frame), OEMCrypto_SUCCESS); @@ -2611,18 +2582,6 @@ TEST_P(OEMCryptoLicenseTest, DecryptSecureToClear) { session_.TestDecryptCTR(true, OEMCrypto_ERROR_UNKNOWN_FAILURE)); } -// If analog is forbidden, then decrypt to a clear buffer should be forbidden. -TEST_P(OEMCryptoLicenseTest, DecryptNoAnalogToClearAPI13) { - ASSERT_NO_FATAL_FAILURE(session_.GenerateNonce()); - ASSERT_NO_FATAL_FAILURE(license_messages_.SignAndVerifyRequest()); - license_messages_.set_control(wvoec::kControlDisableAnalogOutput); - ASSERT_NO_FATAL_FAILURE(license_messages_.CreateDefaultResponse()); - ASSERT_NO_FATAL_FAILURE(license_messages_.EncryptAndSignResponse()); - ASSERT_EQ(OEMCrypto_SUCCESS, license_messages_.LoadResponse()); - ASSERT_NO_FATAL_FAILURE( - session_.TestDecryptCTR(true, OEMCrypto_ERROR_ANALOG_OUTPUT)); -} - // Test that key duration is honored. TEST_P(OEMCryptoLicenseTest, KeyDuration) { ASSERT_NO_FATAL_FAILURE(session_.GenerateNonce()); @@ -4201,6 +4160,11 @@ class OEMCryptoGenericCryptoTest : public OEMCryptoRefreshTest { } } + void ResizeBuffer(size_t new_size) { + buffer_size_ = new_size; + InitializeClearBuffer(); // Re-initialize the clear buffer. + } + void EncryptAndLoadKeys() { ASSERT_NO_FATAL_FAILURE(license_messages_.EncryptAndSignResponse()); ASSERT_EQ(OEMCrypto_SUCCESS, license_messages_.LoadResponse()); @@ -4536,7 +4500,7 @@ TEST_P(OEMCryptoGenericCryptoTest, GenericKeyBadVerify) { // Test Generic_Encrypt with the maximum buffer size. TEST_P(OEMCryptoGenericCryptoTest, GenericKeyEncryptLargeBuffer) { - buffer_size_ = GetResourceValue(kMaxGenericBuffer); + ResizeBuffer(GetResourceValue(kMaxGenericBuffer)); EncryptAndLoadKeys(); unsigned int key_index = 0; vector expected_encrypted; @@ -4559,7 +4523,7 @@ TEST_P(OEMCryptoGenericCryptoTest, GenericKeyEncryptLargeBuffer) { // Test Generic_Decrypt with the maximum buffer size. TEST_P(OEMCryptoGenericCryptoTest, GenericKeyDecryptLargeBuffer) { // Some applications are known to pass in a block that is almost 400k. - buffer_size_ = GetResourceValue(kMaxGenericBuffer); + ResizeBuffer(GetResourceValue(kMaxGenericBuffer)); EncryptAndLoadKeys(); unsigned int key_index = 1; vector encrypted; @@ -4580,7 +4544,7 @@ TEST_P(OEMCryptoGenericCryptoTest, GenericKeyDecryptLargeBuffer) { // Test Generic_Sign with the maximum buffer size. TEST_P(OEMCryptoGenericCryptoTest, GenericKeySignLargeBuffer) { - buffer_size_ = GetResourceValue(kMaxGenericBuffer); + ResizeBuffer(GetResourceValue(kMaxGenericBuffer)); EncryptAndLoadKeys(); unsigned int key_index = 2; vector expected_signature; @@ -4608,7 +4572,7 @@ TEST_P(OEMCryptoGenericCryptoTest, GenericKeySignLargeBuffer) { // Test Generic_Verify with the maximum buffer size. TEST_P(OEMCryptoGenericCryptoTest, GenericKeyVerifyLargeBuffer) { - buffer_size_ = GetResourceValue(kMaxGenericBuffer); + ResizeBuffer(GetResourceValue(kMaxGenericBuffer)); EncryptAndLoadKeys(); unsigned int key_index = 3; vector signature; @@ -4917,6 +4881,7 @@ class LicenseWithUsageEntry { ASSERT_NO_FATAL_FAILURE(session_.open()); ASSERT_NO_FATAL_FAILURE(session_.ReloadUsageEntry()); ASSERT_NO_FATAL_FAILURE(util->InstallTestRSAKey(&session_)); + ASSERT_NO_FATAL_FAILURE(session_.GenerateDerivedKeysFromSessionKey()); ASSERT_EQ(OEMCrypto_SUCCESS, license_messages_.LoadResponse()); } @@ -5719,19 +5684,29 @@ class OEMCryptoUsageTableDefragTest : public OEMCryptoUsageTableTest { void ShrinkHeader(uint32_t new_size, OEMCryptoResult expected_result = OEMCrypto_SUCCESS) { + // We call OEMCrypto_ShrinkUsageTableHeader once with a zero length buffer, + // so that OEMCrypto can tell us how big the buffer should be. size_t header_buffer_length = 0; OEMCryptoResult sts = OEMCrypto_ShrinkUsageTableHeader( new_size, nullptr, &header_buffer_length); + // If we are expecting success, then the first call shall return + // SHORT_BUFFER. However, if we are not expecting success, this first call + // may return either SHORT_BUFFER or the expect error. if (expected_result == OEMCrypto_SUCCESS) { ASSERT_EQ(OEMCrypto_ERROR_SHORT_BUFFER, sts); - } else { - ASSERT_NE(OEMCrypto_SUCCESS, sts); - if (sts != OEMCrypto_ERROR_SHORT_BUFFER) return; + } else if (sts != OEMCrypto_ERROR_SHORT_BUFFER) { + // If we got any thing from the first call, it should be the expected + // error, and we don't need to call a second time. + ASSERT_EQ(expected_result, sts); + return; } + // If the first call resulted in SHORT_BUFFER, we should resize the buffer + // and try again. ASSERT_LT(0u, header_buffer_length); encrypted_usage_header_.resize(header_buffer_length); sts = OEMCrypto_ShrinkUsageTableHeader( new_size, encrypted_usage_header_.data(), &header_buffer_length); + // For the second call, we always demand the expected result. ASSERT_EQ(expected_result, sts); } }; @@ -6030,6 +6005,7 @@ TEST_P(OEMCryptoUsageTableTest, ReloadUsageTableWithSkew) { old_usage_entry_1.data(), old_usage_entry_1.size())); ASSERT_NO_FATAL_FAILURE(InstallTestRSAKey(&s)); + ASSERT_NO_FATAL_FAILURE(s.GenerateDerivedKeysFromSessionKey()); ASSERT_EQ(OEMCrypto_SUCCESS, entry.license_messages().LoadResponse()); } @@ -6200,44 +6176,12 @@ class OEMCryptoUsageTableTestWallClock : public OEMCryptoUsageTableTest { public: void SetUp() override { OEMCryptoUsageTableTest::SetUp(); - did_change_system_time_ = false; - test_start_steady_ = steady_clock_.now(); -#ifdef _WIN32 - GetSystemTime(&test_start_wall_); -#else - ASSERT_EQ(0, gettimeofday(&test_start_wall_, nullptr)); -#endif } void TearDown() override { - if (did_change_system_time_) { - const auto delta = steady_clock_.now() - test_start_steady_; - const int64_t delta_sec = delta / std::chrono::seconds(1); - ASSERT_NO_FATAL_FAILURE(SetWallTimeDelta(delta_sec)); - } + wvcdm::TestSleep::ResetRollback(); OEMCryptoUsageTableTest::TearDown(); } - - protected: - /** - * Sets the current wall-clock time to a delta based on the start of the - * test. - */ - void SetWallTimeDelta(int64_t delta_seconds) { - did_change_system_time_ = true; - NativeTime time = test_start_wall_; - ASSERT_NO_FATAL_FAILURE(AddNativeTime(delta_seconds, &time)); -#ifdef _WIN32 - ASSERT_TRUE(SetSystemTime(&time)); -#else - ASSERT_EQ(0, settimeofday(&time, nullptr)); -#endif - } - - std::chrono::steady_clock steady_clock_; - bool did_change_system_time_; - NativeTime test_start_wall_; - std::chrono::time_point test_start_steady_; }; // NOTE: This test needs root access since clock_settime messes with the system @@ -6246,61 +6190,104 @@ class OEMCryptoUsageTableTestWallClock : public OEMCryptoUsageTableTest { // We don't test roll-forward protection or instances where the user rolls back // the time to the last decrypt call since this requires hardware-secure clocks // to guarantee. +// +// This test overlaps two tests in parallel because they each have several +// seconds of sleeping, then we roll the system clock back, and then we sleep +// some more. +// For the first test, we use entry1. The playback duration is 6 short +// intervals. We play for 3, roll the clock back 2, and then play for 3 more. +// We then sleep until after the allowed playback duration and try to play. If +// OEMCrypto allows the rollback, then there is only 5 intervals, which is +// legal. But if OEMCrypto forbids the rollback, then there is 8 intervals of +// playback, which is not legal. +// +// For the second test, we use entry2. The rental duration is 6 short +// intervals. The times are the same as for entry1, except we do not start +// playback for entry2 until the end. + +// clang-format off +// [--][--][--][--][--][--][--] -- playback or rental limit. +// +// Here's what the system clock sees with rollback: +// [--][--][--] 3 short intervals of playback or sleep +// <------> Rollback 2 short intervals. +// [--][--][--] 3 short intervals of playback or sleep +// [--] 1 short intervals of sleep. +// +// Here's what the system clock sees without rollback: +// [--][--][--] 3 short intervals of playback or sleep +// [--][--][--] 3 short intervals of playback or sleep +// [--][--]X 2 short intervals of sleep. +// +// |<---------------------------->| 8 short intervals from license received +// until pst reports generated. +// clang-format on + TEST_P(OEMCryptoUsageTableTestWallClock, TimeRollbackPrevention) { cout << "This test temporarily rolls back the system time in order to verify " - << "that the usage report accounts for the change. It then rolls " - << "the time back forward to the absolute time." << endl; - LicenseWithUsageEntry entry; - entry.MakeOfflineAndClose(this); - Session& s = entry.session(); - std::chrono::system_clock wall_clock; - std::chrono::steady_clock monotonic_clock; - const auto loaded = wall_clock.now(); + << "that the usage report accounts for the change. After the test, it " + << "rolls the clock back forward." << endl; + constexpr int kRollBackTime = kShortSleep * 2; + constexpr int kPlaybackCount = 3; + constexpr int kTotalTime = kShortSleep * 8; - ASSERT_NO_FATAL_FAILURE(entry.OpenAndReload(this)); - const auto first_decrypt = wall_clock.now(); - // Monotonic clock can't be changed. We use this since system clock will be - // unreliable. - const auto first_decrypt_monotonic = monotonic_clock.now(); - ASSERT_NO_FATAL_FAILURE(entry.TestDecryptCTR()); - ASSERT_NO_FATAL_FAILURE(s.UpdateUsageEntry(&encrypted_usage_header_)); - ASSERT_NO_FATAL_FAILURE(s.close()); + LicenseWithUsageEntry entry1; + entry1.license_messages() + .core_response() + .timer_limits.total_playback_duration_seconds = 7 * kShortSleep; + entry1.MakeOfflineAndClose(this); + Session& s1 = entry1.session(); + ASSERT_NO_FATAL_FAILURE(entry1.OpenAndReload(this)); - // Imitate playback. - wvcdm::TestSleep::Sleep(kLongDuration * 2); + LicenseWithUsageEntry entry2; + entry2.license_messages() + .core_response() + .timer_limits.rental_duration_seconds = 7 * kShortSleep; + entry2.MakeOfflineAndClose(this); + Session& s2 = entry2.session(); + ASSERT_NO_FATAL_FAILURE(entry2.OpenAndReload(this)); - ASSERT_NO_FATAL_FAILURE(entry.OpenAndReload(this)); - ASSERT_NO_FATAL_FAILURE(entry.TestDecryptCTR()); - ASSERT_NO_FATAL_FAILURE(s.UpdateUsageEntry(&encrypted_usage_header_)); - ASSERT_NO_FATAL_FAILURE(s.close()); + // Start with three short intervals of playback for entry1. + for (int i = 0; i < kPlaybackCount; i++) { + ASSERT_NO_FATAL_FAILURE(entry1.TestDecryptCTR()); + wvcdm::TestSleep::Sleep(kShortSleep); + ASSERT_NO_FATAL_FAILURE(entry1.TestDecryptCTR()); + } - // Rollback the wall clock time. cout << "Rolling the system time back..." << endl; + ASSERT_TRUE(wvcdm::TestSleep::RollbackSystemTime(kRollBackTime)); + + // Three more short intervals of playback after the rollback. + for (int i = 0; i < kPlaybackCount; i++) { + ASSERT_NO_FATAL_FAILURE(entry1.TestDecryptCTR()); + wvcdm::TestSleep::Sleep(kShortSleep); + ASSERT_NO_FATAL_FAILURE(entry1.TestDecryptCTR()); + } + + // One short interval of sleep to push us past the 6 interval duration. + wvcdm::TestSleep::Sleep(2 * kShortSleep); + + // Should not be able to continue playback in entry1. ASSERT_NO_FATAL_FAILURE( - SetWallTimeDelta(-static_cast(kLongDuration) * 10)); - - // Try to playback again. - ASSERT_NO_FATAL_FAILURE(entry.OpenAndReload(this)); - const auto third_decrypt_monotonic = monotonic_clock.now(); - ASSERT_NO_FATAL_FAILURE(entry.TestDecryptCTR()); - ASSERT_NO_FATAL_FAILURE(s.UpdateUsageEntry(&encrypted_usage_header_)); - ASSERT_NO_FATAL_FAILURE(s.close()); - Test_PST_Report expected(entry.pst(), kActive); - - // Restore wall clock to its original position to verify that OEMCrypto does - // not report negative times. - const auto test_duration = third_decrypt_monotonic - first_decrypt_monotonic; - cout << "Rolling the system time forward to the absolute time..." << endl; + entry1.TestDecryptCTR(false, OEMCrypto_ERROR_KEY_EXPIRED)); + // Should not be able to start playback in entry2. ASSERT_NO_FATAL_FAILURE( - SetWallTimeDelta(test_duration / std::chrono::seconds(1))); - // Need to update time created since the verification checks the time of PST - // report creation. - expected.time_created = UnixTime(wall_clock.now()); + entry2.TestDecryptCTR(true, OEMCrypto_ERROR_KEY_EXPIRED)); - const auto end_time = first_decrypt + test_duration; - ASSERT_NO_FATAL_FAILURE(s.VerifyReport( - expected, UnixTime(loaded), UnixTime(first_decrypt), UnixTime(end_time))); - ASSERT_NO_FATAL_FAILURE(s.close()); + // Now we look at the usage reports: + ASSERT_NO_FATAL_FAILURE(s1.UpdateUsageEntry(&encrypted_usage_header_)); + ASSERT_NO_FATAL_FAILURE(s2.UpdateUsageEntry(&encrypted_usage_header_)); + + ASSERT_NO_FATAL_FAILURE(s1.GenerateReport(entry1.pst())); + wvcdm::Unpacked_PST_Report report1 = s1.pst_report(); + EXPECT_EQ(report1.status(), kActive); + EXPECT_GE(report1.seconds_since_license_received(), kTotalTime); + EXPECT_GE(report1.seconds_since_first_decrypt(), kTotalTime); + + ASSERT_NO_FATAL_FAILURE(s2.GenerateReport(entry2.pst())); + wvcdm::Unpacked_PST_Report report2 = s2.pst_report(); + EXPECT_EQ(report2.status(), kUnused); + EXPECT_GE(report2.seconds_since_license_received(), kTotalTime); } // Verify that a large PST can be used with usage table entries. diff --git a/oemcrypto/test/oemcrypto_test_main.cpp b/oemcrypto/test/oemcrypto_test_main.cpp index 1cf4a598..5815fd4d 100644 --- a/oemcrypto/test/oemcrypto_test_main.cpp +++ b/oemcrypto/test/oemcrypto_test_main.cpp @@ -4,6 +4,7 @@ #include "OEMCryptoCENC.h" #include "log.h" #include "oec_device_features.h" +#include "oemcrypto_corpus_generator_helper.h" #include "test_sleep.h" static void acknowledge_cast() { @@ -23,6 +24,9 @@ int main(int argc, char** argv) { // Skip the first element, which is the program name. const std::vector args(argv + 1, argv + argc); for (const std::string& arg : args) { + if (arg == "--generate_corpus") { + wvoec::SetGenerateCorpus(true); + } if (arg == "--verbose" || arg == "-v") { ++verbosity; } else if (arg == "--cast") { diff --git a/oemcrypto/test/oemcrypto_unittests.gypi b/oemcrypto/test/oemcrypto_unittests.gypi index 6857fc7e..ac747be6 100644 --- a/oemcrypto/test/oemcrypto_unittests.gypi +++ b/oemcrypto/test/oemcrypto_unittests.gypi @@ -10,6 +10,7 @@ 'oec_decrypt_fallback_chain.cpp', 'oec_key_deriver.cpp', 'oec_session_util.cpp', + 'oemcrypto_corpus_generator_helper.cpp', 'oemcrypto_session_tests_helper.cpp', 'oemcrypto_test.cpp', 'wvcrc.cpp', @@ -20,6 +21,7 @@ '<(oemcrypto_dir)/include', '<(oemcrypto_dir)/ref/src', '<(oemcrypto_dir)/test', + '<(oemcrypto_dir)/test/fuzz_tests', '<(oemcrypto_dir)/odk/include', ], 'defines': [ diff --git a/platforms/x86-64/settings.gypi b/platforms/x86-64/settings.gypi index 610514e7..068888cb 100644 --- a/platforms/x86-64/settings.gypi +++ b/platforms/x86-64/settings.gypi @@ -14,8 +14,9 @@ 'cflags': [ '-fPIC', '-fvisibility=hidden', + '-fno-common', + '-Wno-unknown-warning-option', ], - # These are flags passed to the compiler for plain C only. 'cflags_c': [ # Compile using the C11 standard with POSIX extensions @@ -56,7 +57,6 @@ '-Wno-unused-parameter', # repeated in protobufs triggers this '-Wno-unused-local-typedefs', # metrics requires this #'-Wno-maybe-uninitialized', - '-Wno-unknown-warning-option', '-Wno-dangling-else', # Allowed by Google C++ Style ], diff --git a/third_party/boringssl/boringssl.gyp b/third_party/boringssl/boringssl.gyp index de2e8364..69b455e4 100644 --- a/third_party/boringssl/boringssl.gyp +++ b/third_party/boringssl/boringssl.gyp @@ -13,14 +13,17 @@ '-fvisibility=hidden', # BoringSSL violates these warnings '-Wno-cast-qual', + '-Wno-ignored-qualifiers', ], 'cflags!': [ '-Wbad-function-cast', '-Wcast-qual', + '-Wignored-qualifiers', ], 'cflags_cc!': [ '-Wbad-function-cast', '-Wcast-qual', + '-Wignored-qualifiers', ], 'cflags_c': [ # BoringSSL violates these warnings @@ -29,6 +32,7 @@ 'cflags_c!': [ '-Wbad-function-cast', '-Wcast-qual', + '-Wignored-qualifiers', ], 'msvs_settings': { @@ -41,15 +45,14 @@ 'ldflags': [ '-fPIC', ], - 'link_settings': { - 'ldflags': [ - '-lpthread', - ], - }, 'link_settings': { 'conditions': [ - ['OS=="win"', { + ['OS!="win"', { + 'libraries': [ + '-lpthread', + ], + }, { 'libraries': [ '-ladvapi32', ], @@ -62,10 +65,12 @@ 'cflags': [ '-Wno-unknown-warning-option', '-Wno-cast-qual', + '-Wno-ignored-qualifiers', ], 'cflags!': [ '-Wbad-function-cast', '-Wcast-qual', + '-Wignored-qualifiers', ], 'cflags_c': [ '-Wno-bad-function-cast', @@ -73,10 +78,12 @@ 'cflags_c!': [ '-Wbad-function-cast', '-Wcast-qual', + '-Wignored-qualifiers', ], 'cflags_cc!': [ '-Wbad-function-cast', '-Wcast-qual', + '-Wignored-qualifiers', ], }, diff --git a/third_party/gmock.gyp b/third_party/gmock.gyp index 0cc6cf60..d7c70a8b 100644 --- a/third_party/gmock.gyp +++ b/third_party/gmock.gyp @@ -12,6 +12,11 @@ ], 'cflags_cc': [ '-std=c++11', + # gMock violates these warnings + '-Wno-deprecated-copy', + ], + 'cflags_cc!': [ + '-Wdeprecated-copy', ], 'ldflags': [ '-fPIC', @@ -27,6 +32,13 @@ 'googletest/googlemock/include', 'googletest/googletest/include', ], + 'cflags_cc': [ + # gMock's exported headers violate these warnings + '-Wno-deprecated-copy', + ], + 'cflags_cc!': [ + '-Wdeprecated-copy', + ], 'xcode_settings': { 'OTHER_CFLAGS': [ '-Wno-inconsistent-missing-override', @@ -65,7 +77,7 @@ 'conditions': [ ['OS!="win"', { 'link_settings': { - 'ldflags': [ + 'libraries': [ '-lpthread', ], }, diff --git a/third_party/gyp/xcode_emulation.py b/third_party/gyp/xcode_emulation.py index 0bdf88db..ca76187b 100644 --- a/third_party/gyp/xcode_emulation.py +++ b/third_party/gyp/xcode_emulation.py @@ -726,6 +726,8 @@ class XcodeSettings(object): def _AddObjectiveCARCFlags(self, flags): if self._Test('CLANG_ENABLE_OBJC_ARC', 'YES', default='NO'): flags.append('-fobjc-arc') + if self._Test('CLANG_ENABLE_OBJC_WEAK', 'YES', default='NO'): + flags.append('-fobjc-weak') def _AddObjectiveCMissingPropertySynthesisFlags(self, flags): if self._Test('CLANG_WARN_OBJC_MISSING_PROPERTY_SYNTHESIS', diff --git a/util/test/test_sleep.cpp b/util/test/test_sleep.cpp index 2140c9ee..1656381a 100644 --- a/util/test/test_sleep.cpp +++ b/util/test/test_sleep.cpp @@ -4,16 +4,26 @@ #include "test_sleep.h" +#ifdef _WIN32 +# include +#else +# include +#endif + +#include +#include #include #include #include "clock.h" +#include "log.h" namespace wvcdm { bool TestSleep::real_sleep_ = true; TestSleep::CallBack* TestSleep::callback_ = nullptr; +int TestSleep::total_clock_rollback_ = 0; void TestSleep::Sleep(unsigned int seconds) { int64_t milliseconds = 1000 * seconds; @@ -23,10 +33,10 @@ void TestSleep::Sleep(unsigned int seconds) { // total since the start, and then compare to a running total of sleep // calls. We sleep for approximately x second, and then advance the clock by // the amount of time that has actually passed. - static auto start_real = std::chrono::steady_clock().now(); + static auto start_real = std::chrono::system_clock().now(); static int64_t fake_clock = 0; sleep(seconds); - auto now_real = std::chrono::steady_clock().now(); + auto now_real = std::chrono::system_clock().now(); int64_t total_real = (now_real - start_real) / std::chrono::milliseconds(1); // We want to advance the fake clock by the difference between the real // clock, and the previous value on the fake clock. @@ -41,4 +51,98 @@ void TestSleep::SyncFakeClock() { Sleep(0); } +bool TestSleep::RollbackSystemTime(int seconds) { + if (real_sleep_) { +#ifdef _WIN32 + // See remarks from this for why this series is used. + // https://msdn.microsoft.com/en-us/f77cdf86-0f97-4a89-b565-95b46fa7d65b + SYSTEMTIME time; + GetSystemTime(&time); + FILETIME file_time; + if (!SystemTimeToFileTime(time, &file_time)) return false; + uint64_t long_time = + static_cast(file_time.dwLowDateTime) | + (static_cast(file_time.dwHighDateTime) << 32); + long_time += static_cast(delta_seconds) * + 1e7; // long_time is in 100-nanosecond intervals. + file_time.dwLowDateTime = long_time & ((1ull << 32) - 1); + file_time.dwHighDateTime = long_time >> 32; + if (!FileTimeToSystemTime(&file_time, &time)) return false; + if (!SetSystemTime(&time)) return false; +#else + auto time = std::chrono::system_clock::now(); + auto modified_time = time - std::chrono::seconds(seconds); + ; + timespec time_spec; + time_spec.tv_sec = std::chrono::duration_cast( + modified_time.time_since_epoch()) + .count(); + time_spec.tv_nsec = std::chrono::duration_cast( + modified_time.time_since_epoch()) + .count() % + (1000 * 1000 * 1000); + if (clock_settime(CLOCK_REALTIME, &time_spec)) { + LOGE("Error setting clock: %s", strerror(errno)); + return false; + } +#endif + } // end if(real_sleep_)... + + // For both real and fake sleep we still update the callback and we still keep + // track of the total amount of time slept. + total_clock_rollback_ += seconds; + if (callback_ != nullptr) callback_->ElapseTime(-1000 * seconds); + return true; +} + +bool TestSleep::CanChangeSystemTime() { + // If we are using a fake clock, then we can move the clock backwards by + // just going backwards. + // ElapseTime. + if (!real_sleep_) { + return true; + } +#ifdef _WIN32 + LUID desired_id; + if (!LookupPrivilegeValue(nullptr, SE_SYSTEMTIME_NAME, &desired_id)) { + LOGE("Win32 time rollback: no SYSTEMTIME permission."); + return false; + } + HANDLE token; + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &token)) { + LOGE("Win32 time rollback: cannot access process token."); + return false; + } + std::unique_ptr safe_token(token, &CloseHandle); + + // This queries all the permissions given to the token to determine if we can + // change the system time. Note this is subtly different from PrivilegeCheck + // as that only checks "enabled" privileges; even with admin rights, the + // privilege is default disabled, even when granted. + + DWORD size = 0; + // Determine how big we need to allocate first. + GetTokenInformation(token, TokenPrivileges, nullptr, 0, &size); + // Since TOKEN_PRIVILEGES uses a variable-length array, we need to use malloc + std::unique_ptr privileges( + (TOKEN_PRIVILEGES*)malloc(size), &free); + if (privileges && GetTokenInformation(token, TokenPrivileges, + privileges.get(), size, &size)) { + for (int i = 0; i < privileges->PrivilegeCount; i++) { + if (privileges->Privileges[i].Luid.HighPart == desired_id.HighPart && + privileges->Privileges[i].Luid.LowPart == desired_id.LowPart) { + return true; + } + } + } + LOGE("Win32 time rollback: cannot set system time."); + return false; +#else + // Otherwise, the test needs to be run as root. + const uid_t uid = getuid(); + if (uid == 0) return true; + LOGE("Unix time rollback: not running as root (uid=%u.", uid); + return false; +#endif +} } // namespace wvcdm diff --git a/util/test/test_sleep.h b/util/test/test_sleep.h index 832f1ade..34c4b3f6 100644 --- a/util/test/test_sleep.h +++ b/util/test/test_sleep.h @@ -22,8 +22,9 @@ class TestSleep { virtual ~CallBack(){}; }; - // If real_sleep_ is true, then this sleeps for |seconds| of time. - // If the callback exists, this calls the callback. + // If real_sleep_ is true, then this sleeps for |seconds| of time. If + // real_sleep_ is false, then the fake clock is advanced by |seconds|. If the + // callback exists, this calls the callback. static void Sleep(unsigned int seconds); // If we are using a real clock and a fake clock, then the real clock advances @@ -33,8 +34,30 @@ class TestSleep { // failing due to this drift. static void SyncFakeClock(); - static void set_real_sleep(bool real_sleep) { real_sleep_ = real_sleep; } + // Roll the system clock back by |seconds|. Returns true on success. A well + // mannered test will call CanChangeSystemTime before attempting to call this + // function and then assert that this is true. This function should *NOT* roll + // back the clock used by OEMCrypto -- in fact, there are several tests that + // verify this function does not roll back the clock used by OEMCrypto. + static bool RollbackSystemTime(int seconds); + // Roll the system clock forward to undo all previous calls to + // RollBackSystemTime. Returns true on success. + static bool ResetRollback() { + return total_clock_rollback_ == 0 || + RollbackSystemTime(-total_clock_rollback_); + } + + // Returns true if the system time can be rolled back. This is true on some + // devices if the tests are run as root. It is also true when using a fake + // clock with the reference version of OEMCrypto. This function is about the + // system clock, *NOT* the clock used by OEMCrypto. + static bool CanChangeSystemTime(); + + static void set_real_sleep(bool real_sleep) { real_sleep_ = real_sleep; } + static bool real_sleep() { return real_sleep_; } + + // The callback is notified whenever sleep is called. static void set_callback(CallBack* callback) { callback_ = callback; } private: @@ -42,6 +65,9 @@ class TestSleep { static bool real_sleep_; // Called when the clock should advance. static CallBack* callback_; + // The sum of all calls to RollBackSystemTime. Kept so we can undo all changes + // at the end of a test. + static int total_clock_rollback_; }; } // namespace wvcdm