diff --git a/libwvdrmengine/cdm/core/include/cdm_session.h b/libwvdrmengine/cdm/core/include/cdm_session.h index 8859093a..65f96955 100644 --- a/libwvdrmengine/cdm/core/include/cdm_session.h +++ b/libwvdrmengine/cdm/core/include/cdm_session.h @@ -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/libwvdrmengine/cdm/core/include/wv_cdm_types.h b/libwvdrmengine/cdm/core/include/wv_cdm_types.h index f19248e0..c0205a70 100644 --- a/libwvdrmengine/cdm/core/include/wv_cdm_types.h +++ b/libwvdrmengine/cdm/core/include/wv_cdm_types.h @@ -411,6 +411,7 @@ enum CdmResponseType { LOAD_USAGE_ENTRY_INVALID_SESSION = 357, MOVE_USAGE_ENTRY_DESTINATION_IN_USE = 358, SHRINK_USAGE_TABLE_HEADER_ENTRY_IN_USE = 359, + LICENSE_USAGE_ENTRY_MISSING = 360, // Don't forget to add new values to // * core/test/test_printers.cpp. // * android/include/mapErrors-inl.h diff --git a/libwvdrmengine/cdm/core/src/cdm_engine.cpp b/libwvdrmengine/cdm/core/src/cdm_engine.cpp index ffaacbfd..66309966 100644 --- a/libwvdrmengine/cdm/core/src/cdm_engine.cpp +++ b/libwvdrmengine/cdm/core/src/cdm_engine.cpp @@ -169,10 +169,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); @@ -1114,14 +1117,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; @@ -1141,6 +1149,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/libwvdrmengine/cdm/core/src/cdm_session.cpp b/libwvdrmengine/cdm/core/src/cdm_session.cpp index 33b066b4..ab32c568 100644 --- a/libwvdrmengine/cdm/core/src/cdm_session.cpp +++ b/libwvdrmengine/cdm/core/src/cdm_session.cpp @@ -282,6 +282,9 @@ CdmResponseType CdmSession::RestoreOfflineSession(const CdmKeySetId& key_set_id, key_response_, &provider_session_token) || usage_table_header_ == nullptr) { provider_session_token.clear(); + } 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_); @@ -1131,6 +1134,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/libwvdrmengine/cdm/core/test/test_printers.cpp b/libwvdrmengine/cdm/core/test/test_printers.cpp index 392b9d59..25bb54b9 100644 --- a/libwvdrmengine/cdm/core/test/test_printers.cpp +++ b/libwvdrmengine/cdm/core/test/test_printers.cpp @@ -521,6 +521,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; diff --git a/libwvdrmengine/cdm/test/request_license_test.cpp b/libwvdrmengine/cdm/test/request_license_test.cpp index 8a5d64f1..ed36edee 100644 --- a/libwvdrmengine/cdm/test/request_license_test.cpp +++ b/libwvdrmengine/cdm/test/request_license_test.cpp @@ -5997,6 +5997,107 @@ TEST_F(WvCdmRequestLicenseTest, DISABLED_DecryptPathTest) { decryptor_->CloseSession(session_id_); } +// This tests checks that if a valid offline license file is found on +// the device but is missing the usage entry associated with it, that +// the CDM can still remove the license without issuing an error back +// to the calling app. +// This checks two cases: +// 1) The license's entry is outside the range of the table +// 2) The entry in the usage table that the license points to does +// not match the license's key set ID (possible for entry to have +// been overwritten). +TEST_F(WvCdmRequestLicenseTest, RemoveOfflineLicenseWithMissingUsageEntry) { + Unprovision(); + Provision(); + const CdmSecurityLevel security_level = GetDefaultSecurityLevel(); + + FileSystem file_system; + DeviceFiles handle(&file_system); + EXPECT_TRUE(handle.Init(security_level)); + + std::string key_id; + std::string client_auth; + GetOfflineConfiguration(&key_id, &client_auth); + + // Setup: Request an offline license to create a valid license file. + // This license will be used to test how the CDM handles request to + // remove it when its entry has been deleted. + + decryptor_->OpenSession(config_.key_system(), nullptr, kDefaultCdmIdentifier, + nullptr, &session_id_); + GenerateKeyRequest(key_id, kLicenseTypeOffline); + VerifyKeyRequestResponse(config_.license_server(), client_auth); + // Save the key set ID for check below. + const std::string original_key_set_id(key_set_id_); + decryptor_->CloseSession(session_id_); + + // Retrieve license from storage for later. + DeviceFiles::CdmLicenseData license_data; + DeviceFiles::ResponseType sub_result = DeviceFiles::kNoError; + EXPECT_TRUE( + handle.RetrieveLicense(original_key_set_id, &license_data, &sub_result)); + EXPECT_EQ(DeviceFiles::kNoError, sub_result); + + // Re-provision. + Unprovision(); + handle.DeleteAllFiles(); + Provision(); + + // Part 1: Test when usage entry is out of range of the table. + + // Store license from earlier, this will cause ListStoredLicenses() to + // return the key set ID of the setup license. + EXPECT_TRUE(handle.StoreLicense(license_data, &sub_result)); + EXPECT_EQ(DeviceFiles::kNoError, sub_result); + + std::vector key_set_ids; + EXPECT_EQ(NO_ERROR, decryptor_->ListStoredLicenses( + security_level, kDefaultCdmIdentifier, &key_set_ids)); + // Note: It is possible that future changes to the CDM will cause this + // check to fail (such by filtering results from ListStoreLicenses). + EXPECT_THAT(key_set_ids, Contains(original_key_set_id)); + + EXPECT_EQ(NO_ERROR, + decryptor_->RemoveOfflineLicense( + original_key_set_id, security_level, kDefaultCdmIdentifier)); + + // Verify license has been removed. + EXPECT_EQ(NO_ERROR, decryptor_->ListStoredLicenses( + security_level, kDefaultCdmIdentifier, &key_set_ids)); + EXPECT_THAT(key_set_ids, Not(Contains(original_key_set_id))); + + // Re-provision. + Unprovision(); + handle.DeleteAllFiles(); + Provision(); + + // Part 2: Test when the entry does not match the license's key set ID. + + EXPECT_TRUE(handle.StoreLicense(license_data, &sub_result)); + EXPECT_EQ(DeviceFiles::kNoError, sub_result); + + // Request another license so that the usage table is not empty. + decryptor_->OpenSession(config_.key_system(), nullptr, kDefaultCdmIdentifier, + nullptr, &session_id_); + GenerateKeyRequest(key_id, kLicenseTypeOffline); + VerifyKeyRequestResponse(config_.license_server(), client_auth); + decryptor_->CloseSession(session_id_); + + // Get list of existing offline licenses. + EXPECT_EQ(NO_ERROR, decryptor_->ListStoredLicenses( + security_level, kDefaultCdmIdentifier, &key_set_ids)); + EXPECT_THAT(key_set_ids, Contains(original_key_set_id)); + + EXPECT_EQ(NO_ERROR, + decryptor_->RemoveOfflineLicense( + original_key_set_id, security_level, kDefaultCdmIdentifier)); + + // Verify license has been removed. + EXPECT_EQ(NO_ERROR, decryptor_->ListStoredLicenses( + security_level, kDefaultCdmIdentifier, &key_set_ids)); + EXPECT_THAT(key_set_ids, Not(Contains(original_key_set_id))); +} + class WvCdmRequestLicenseRollbackTest : public WvCdmRequestLicenseTest, public ::testing::WithParamInterface { diff --git a/libwvdrmengine/include/WVErrors.h b/libwvdrmengine/include/WVErrors.h index 38ef22c8..d0bf031c 100644 --- a/libwvdrmengine/include/WVErrors.h +++ b/libwvdrmengine/include/WVErrors.h @@ -292,10 +292,11 @@ enum { kLoadUsageEntryInvalidSession = ERROR_DRM_VENDOR_MIN + 309, kMoveUsageEntryDestinationInUse = ERROR_DRM_VENDOR_MIN + 310, kShrinkUsageTableHeaderEntryInUse = ERROR_DRM_VENDOR_MIN + 311, + kLicenseUsageEntryMissing = ERROR_DRM_VENDOR_MIN + 312, // This should always follow the last error code. // The offset value should be updated each time a new error code is added. - kErrorWVDrmMaxErrorUsed = ERROR_DRM_VENDOR_MIN + 311, + kErrorWVDrmMaxErrorUsed = ERROR_DRM_VENDOR_MIN + 312, // Used by crypto test mode kErrorTestMode = ERROR_DRM_VENDOR_MAX, diff --git a/libwvdrmengine/include/mapErrors-inl.h b/libwvdrmengine/include/mapErrors-inl.h index bc98d9e5..c86726ed 100644 --- a/libwvdrmengine/include/mapErrors-inl.h +++ b/libwvdrmengine/include/mapErrors-inl.h @@ -356,6 +356,8 @@ static android::status_t mapCdmResponseType(wvcdm::CdmResponseType res) { return kLicenseResponseParseError4; case wvcdm::LICENSE_RESPONSE_PARSE_ERROR_5: return kLicenseResponseParseError5; + case wvcdm::LICENSE_USAGE_ENTRY_MISSING: + return kLicenseUsageEntryMissing; case wvcdm::LIST_LICENSE_ERROR_1: return kListLicenseError1; case wvcdm::LIST_LICENSE_ERROR_2: diff --git a/libwvdrmengine/include_hidl/mapErrors-inl.h b/libwvdrmengine/include_hidl/mapErrors-inl.h index 1f861c0d..dfd4daa4 100644 --- a/libwvdrmengine/include_hidl/mapErrors-inl.h +++ b/libwvdrmengine/include_hidl/mapErrors-inl.h @@ -358,6 +358,7 @@ static Status mapCdmResponseType(wvcdm::CdmResponseType res) { case wvcdm::LOAD_USAGE_ENTRY_INVALID_SESSION: case wvcdm::MOVE_USAGE_ENTRY_DESTINATION_IN_USE: case wvcdm::SHRINK_USAGE_TABLE_HEADER_ENTRY_IN_USE: + case wvcdm::LICENSE_USAGE_ENTRY_MISSING: ALOGW("Returns UNKNOWN error for legacy status: %d", res); return Status::ERROR_DRM_UNKNOWN;