// Copyright 2018 Google LLC. All Rights Reserved. This file and proprietary // source code may only be used and distributed under the Widevine License // Agreement. #include "cdm_usage_table.h" #include #include #include "cdm_random.h" #include "crypto_session.h" #include "license.h" #include "log.h" #include "wv_cdm_constants.h" namespace wvcdm { namespace { using TableLock = std::unique_lock; const std::string kEmptyString; const wvcdm::CdmKeySetId kDummyKeySetId = "DummyKsid"; constexpr int64_t kDefaultExpireDuration = 33 * 24 * 60 * 60; // 33 Days // 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 // remove. This threshold is set to prevent thrashing in the case that // there are a very large number of unexpired offline licenses and few // expired / streaming licenses (ie, number of unexpired licenses // 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) { using video_widevine::SignedMessage; if (license == nullptr) { LOGE("Output parameter |license| is null"); return false; } SignedMessage signed_license_response; if (!signed_license_response.ParseFromString(license_message)) { LOGW("Unabled to parse signed license response"); return false; } if (signed_license_response.type() != SignedMessage::LICENSE) { LOGW("Unexpected signed message: type = %d, expected_type = %d", static_cast(signed_license_response.type()), static_cast(SignedMessage::LICENSE)); return false; } if (!signed_license_response.has_signature()) { LOGW("License response message is not signed"); return false; } if (!license->ParseFromString(signed_license_response.msg())) { LOGW("Failed to parse license"); return false; } return true; } bool RetrieveOfflineLicense(DeviceFiles* device_files, const std::string& key_set_id, CdmKeyResponse* license_message, UsageEntryIndex* entry_index) { if (device_files == nullptr) { LOGE("DeviceFiles handle is null"); return false; } if (license_message == nullptr) { LOGE("Output parameter |license_message| is null"); return false; } if (entry_index == nullptr) { LOGE("Output parameter |entry_index| is null"); return false; } DeviceFiles::CdmLicenseData license_data; DeviceFiles::ResponseType result = DeviceFiles::kNoError; if (!device_files->RetrieveLicense(key_set_id, &license_data, &result)) { LOGW("Failed to retrieve license: key_set_id = %s, result = %s", IdToString(key_set_id), DeviceFiles::ResponseTypeToString(result)); return false; } *license_message = std::move(license_data.license); *entry_index = license_data.usage_entry_index; return true; } bool RetrieveUsageInfoLicense(DeviceFiles* device_files, const std::string& usage_info_file_name, const std::string& key_set_id, CdmKeyResponse* license_message, UsageEntryIndex* entry_index) { if (device_files == nullptr) { LOGE("DeviceFiles handle is null"); return false; } if (license_message == nullptr) { LOGE("Output parameter |license_message| is null"); return false; } if (entry_index == nullptr) { LOGE("Output parameter |entry_index| is null"); return false; } UsageEntry entry; std::string provider_session_token; CdmKeyMessage license_request; std::string drm_certificate; CryptoWrappedKey wrapped_private_key; if (!device_files->RetrieveUsageInfoByKeySetId( usage_info_file_name, key_set_id, &provider_session_token, &license_request, license_message, &entry, entry_index, &drm_certificate, &wrapped_private_key)) { LOGW( "Failed to retrieve usage information: " "key_set_id = %s, usage_info_file_name = %s", IdToString(key_set_id), IdToString(usage_info_file_name)); return false; } 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; } bool IsValidCdmSecurityLevelForUsageInfo(CdmSecurityLevel security_level) { return security_level == kSecurityLevelL1 || security_level == kSecurityLevelL3; } RequestedSecurityLevel CdmSecurityLevelToRequestedLevel( CdmSecurityLevel security_level) { return security_level == kSecurityLevelL3 ? kLevel3 : kLevelDefault; } } // namespace CdmUsageTable::CdmUsageTable() : clock_ref_(&clock_) { file_system_.reset(new wvutil::FileSystem()); device_files_.reset(new DeviceFiles(file_system_.get())); } bool CdmUsageTable::Init(CdmSecurityLevel security_level, CryptoSession* crypto_session) { LOGD("security_level = %s", CdmSecurityLevelToString(security_level)); if (crypto_session == nullptr) { LOGE("No crypto session provided"); return false; } if (is_initialized_) { LOGE("Cannot reinitialize usage table: security_level = %s", CdmSecurityLevelToString(security_level)); return false; } if (!IsValidCdmSecurityLevelForUsageInfo(security_level)) { LOGE("Invalid security level provided: security_level = %s", CdmSecurityLevelToString(security_level)); return false; } security_level_ = security_level; requested_security_level_ = CdmSecurityLevelToRequestedLevel(security_level); if (!OpenSessionCheck(crypto_session)) { return false; } if (!DetermineTableCapacity(crypto_session)) { return false; } if (!device_files_->Init(security_level)) { LOGE("Failed to initialize device files"); return false; } // Attempt restoring first, if unable to restore, then create a new // table. return RestoreTable(crypto_session) || CreateNewTable(crypto_session); } bool CdmUsageTable::RestoreTable(CryptoSession* const crypto_session) { bool run_lru_upgrade = false; if (!device_files_->RetrieveUsageTableInfo(&header_, &entry_info_list_, &run_lru_upgrade)) { LOGW("Could not retrieve usage table"); return false; } LOGI("Found usage table to restore: entry_count = %zu", entry_info_list_.size()); const CdmResponseType status = crypto_session->LoadUsageTableHeader(requested_security_level_, header_); if (status != NO_ERROR) { LOGE("Failed to load usage table header: sts = %d", status.ToInt()); return false; } // If the saved usage entries/meta data is missing LRU information, // then the entries and their meta data must be updated. if (run_lru_upgrade && !LruUpgradeAllUsageEntries()) { LOGE("Failed to perform LRU upgrade to usage entry table"); return false; } if (!CapacityCheck(crypto_session)) { LOGE("Cannot restore table due to failing capacity check"); return false; } is_initialized_ = true; return true; } bool CdmUsageTable::CreateNewTable(CryptoSession* const crypto_session) { LOGD("Removing all usage table files"); // Existing files need to be deleted to avoid attempts to restore // licenses which no longer have a usage entry. device_files_->DeleteAllLicenses(); device_files_->DeleteAllUsageInfo(); device_files_->DeleteUsageTableInfo(); entry_info_list_.clear(); header_.clear(); const CdmResponseType status = crypto_session->CreateUsageTableHeader( requested_security_level_, &header_); if (status != NO_ERROR) { LOGE("Failed to create new usage table header"); return false; } if (!StoreTable()) { LOGE("Failed to store new usage table header"); return false; } is_initialized_ = true; return true; } CdmResponseType CdmUsageTable::AddEntry(CryptoSession* crypto_session, bool persistent_license, const CdmKeySetId& key_set_id, const std::string& usage_info_file_name, const CdmKeyResponse& license_message, UsageEntryIndex* entry_index) { LOGD("key_set_id = %s, type = %s, current_size = %zu", IdToString(key_set_id), persistent_license ? "OfflineLicense" : "Streaming", entry_info_list_.size()); metrics::CryptoMetrics* metrics = crypto_session->GetCryptoMetrics(); if (metrics == nullptr) metrics = &alternate_crypto_metrics_; TableLock auto_lock(lock_); CdmResponseType status = CreateEntry(crypto_session, entry_index); if (status == INSUFFICIENT_CRYPTO_RESOURCES) { LOGW("Usage table may be full, releasing oldest entry: size = %zu", entry_info_list_.size()); status = ReleaseOldestEntry(metrics); if (status == NO_ERROR) { status = CreateEntry(crypto_session, entry_index); } } if (status != NO_ERROR) return status; status = RelocateNewEntry(crypto_session, entry_index); if (status != NO_ERROR) return status; if (persistent_license) { SetOfflineEntryInfo(*entry_index, key_set_id, license_message); } else { SetUsageInfoEntryInfo(*entry_index, key_set_id, usage_info_file_name); } status = RefitTable(crypto_session); if (status != NO_ERROR) { entry_info_list_[*entry_index].Clear(); return status; } // 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. UsageEntry entry; status = crypto_session->UpdateUsageEntry(&header_, &entry); if (status != NO_ERROR) { LOGE("Failed to update new usage entry: entry_index = %u", *entry_index); entry_info_list_[*entry_index].Clear(); return status; } StoreTable(); return CdmResponseType(NO_ERROR); } CdmResponseType CdmUsageTable::LoadEntry(CryptoSession* crypto_session, const UsageEntry& entry, UsageEntryIndex entry_index) { { LOGD("entry_index = %u", entry_index); std::unique_lock auto_lock(lock_); if (entry_index >= entry_info_list_.size()) { LOGE( "Requested usage entry index is larger than table size: " "entry_index = %u, table_size = %zu", entry_index, entry_info_list_.size()); return CdmResponseType(USAGE_INVALID_LOAD_ENTRY); } } const CdmResponseType status = crypto_session->LoadUsageEntry(entry_index, entry); if (status == NO_ERROR) { entry_info_list_[entry_index].last_use_time = GetCurrentTime(); } return status; } CdmResponseType CdmUsageTable::UpdateEntry(UsageEntryIndex entry_index, CryptoSession* crypto_session, UsageEntry* entry) { LOGD("entry_index = %u", entry_index); std::unique_lock auto_lock(lock_); if (entry_index >= entry_info_list_.size()) { LOGE("Usage entry index %u is larger than usage entry size %zu", entry_index, entry_info_list_.size()); return CdmResponseType(USAGE_INVALID_PARAMETERS_2); } CdmResponseType status = crypto_session->UpdateUsageEntry(&header_, entry); if (status != NO_ERROR) return status; entry_info_list_[entry_index].last_use_time = GetCurrentTime(); StoreTable(); return CdmResponseType(NO_ERROR); } CdmResponseType CdmUsageTable::InvalidateEntry( UsageEntryIndex entry_index, bool defrag_table, DeviceFiles* device_files, metrics::CryptoMetrics* metrics) { LOGD("entry_index = %u", entry_index); TableLock auto_lock(lock_); return InvalidateEntryInternal(entry_index, defrag_table, device_files, metrics); } CdmResponseType CdmUsageTable::InvalidateEntryInternal( UsageEntryIndex entry_index, bool defrag_table, DeviceFiles* device_files, metrics::CryptoMetrics* metrics) { // 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 (entry_index >= entry_info_list_.size()) { LOGE( "Usage entry index is larger than table size: " "entry_index = %u, table_size = %zu", entry_index, entry_info_list_.size()); return CdmResponseType(USAGE_INVALID_PARAMETERS_1); } entry_info_list_[entry_index].Clear(); 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 (status != NO_ERROR) { LOGW("Failed to defrag usage table: sts = %s", status.ToString().c_str()); } 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(); } if (status == SYSTEM_INVALIDATED_ERROR) { LOGE("Invalidate entry failed due to system invalidation error"); return CdmResponseType(SYSTEM_INVALIDATED_ERROR); } } else { StoreTable(); } return CdmResponseType(NO_ERROR); } size_t CdmUsageTable::UsageInfoCount() const { LOGV("Locking to count usage info (streaming license) entries"); return std::count_if(entry_info_list_.cbegin(), entry_info_list_.cend(), EntryIsUsageInfo); } size_t CdmUsageTable::OfflineEntryCount() const { LOGV("Locking to count offline license entries"); return std::count_if(entry_info_list_.cbegin(), entry_info_list_.cend(), EntryIsOfflineLicense); } bool CdmUsageTable::OpenSessionCheck(CryptoSession* const crypto_session) { // The CdmUsageTable for the specified |requested_security_level_| // MUST be initialized before any sessions are opened. size_t session_count = 0; const CdmResponseType status = crypto_session->GetNumberOfOpenSessions( requested_security_level_, &session_count); if (status != NO_ERROR || session_count > 0) { LOGE( "Cannot initialize usage table header with open crypto session: " "status = %d, count = %zu", status.ToInt(), session_count); return false; } return true; } bool CdmUsageTable::CapacityCheck(CryptoSession* const crypto_session) { // If the table is around capacity or if unlimited and the table is // larger than the minimally required capacity, then a test must be // performed to ensure that the usage table is not in a state which // will prevent create operations. const size_t capacity_threshold = HasUnlimitedTableCapacity() ? kMinimumUsageTableEntriesSupported : potential_table_capacity(); if (entry_info_list_.size() <= capacity_threshold) { // No need to perform test if below capacity. return true; } metrics::CryptoMetrics* metrics = crypto_session->GetCryptoMetrics(); if (metrics == nullptr) metrics = &alternate_crypto_metrics_; // |local_crypto_session| points to an object whose scope is this // method or a test object whose scope is the lifetime of this class CryptoSession* local_crypto_session = test_crypto_session_.get(); std::unique_ptr scoped_crypto_session; if (local_crypto_session == nullptr) { scoped_crypto_session.reset(CryptoSession::MakeCryptoSession(metrics)); local_crypto_session = scoped_crypto_session.get(); } CdmResponseType status = local_crypto_session->Open(requested_security_level_); if (status != NO_ERROR) { LOGE("Failed to open crypto session for capacity test: sts = %d", status.ToInt()); return false; } UsageEntryIndex temporary_entry_index; status = AddEntry(local_crypto_session, true, kDummyKeySetId, kEmptyString, kEmptyString, &temporary_entry_index); if (status != NO_ERROR) { LOGE("Failed to add entry for capacity test: sts = %d", status.ToInt()); return false; } // Session must be closed before invalidating, otherwise Shrink() will // fail in call to InvalidateEntry(). local_crypto_session->Close(); status = InvalidateEntry(temporary_entry_index, /* defrag_table = */ true, device_files_.get(), metrics); if (status != NO_ERROR) { LOGE("Failed to invalidate entry for capacity test: sts = %d", status.ToInt()); return false; } if (entry_info_list_.size() > temporary_entry_index) { // 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( "Failed to shrink table for capacity test: " "post_check_size = %zu, check_usage_entry_number = %u", entry_info_list_.size(), temporary_entry_index); return false; } return true; } bool CdmUsageTable::DetermineTableCapacity(CryptoSession* crypto_session) { 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("capacity = unlimited, security_level = %s", CdmSecurityLevelToString(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("capacity = %zu, security_level = %s", potential_table_capacity_, CdmSecurityLevelToString(security_level_)); } return true; } CdmResponseType CdmUsageTable::CreateEntry(CryptoSession* const crypto_session, UsageEntryIndex* entry_index) { const CdmResponseType status = crypto_session->CreateUsageEntry(entry_index); if (status != NO_ERROR) return status; // If the new entry index is smaller than expected, then the usage // table may be out of sync or OEMCrypto has been rolled back. // Not safe to continue. if (*entry_index < entry_info_list_.size()) { LOGE( "New entry index is smaller than table size: " "entry_index = %u, table_size = %zu", *entry_index, entry_info_list_.size()); return CdmResponseType(USAGE_INVALID_NEW_ENTRY); } LOGI("entry_index = %u", *entry_index); const size_t previous_size = entry_info_list_.size(); entry_info_list_.resize(*entry_index + 1); if (*entry_index > previous_size) { LOGW( "New entry index is larger than table size, resizing: " "entry_index = %u, table_size = %zu", *entry_index, previous_size); for (size_t i = previous_size; i < entry_info_list_.size() - 1; ++i) { entry_info_list_[i].Clear(); } } entry_info_list_[*entry_index].Clear(); return CdmResponseType(NO_ERROR); } CdmResponseType CdmUsageTable::RelocateNewEntry( CryptoSession* const crypto_session, UsageEntryIndex* entry_index) { static constexpr UsageEntryIndex kMinimumEntryNumber = 0; const UsageEntryIndex initial_entry_index = *entry_index; if (initial_entry_index == kMinimumEntryNumber) { // First entry in the table. return CdmResponseType(NO_ERROR); } UsageEntryIndex unoccupied_entry_index = initial_entry_index; for (UsageEntryIndex i = kMinimumEntryNumber; i < initial_entry_index; i++) { if (IsEntryUnoccupied(i)) { unoccupied_entry_index = i; break; } } if (unoccupied_entry_index == initial_entry_index) { // No open position. return CdmResponseType(NO_ERROR); } const CdmResponseType status = crypto_session->MoveUsageEntry(unoccupied_entry_index); if (status == MOVE_USAGE_ENTRY_DESTINATION_IN_USE) { // Not unexpected, there is a window of time between releasing the // entry and closing the OEMCrypto session. LOGD("Released entry still in use: index = %u", unoccupied_entry_index); return CdmResponseType(NO_ERROR); } if (status != NO_ERROR) return status; LOGI("Entry moved: from_index = %u, to_index = %u", initial_entry_index, unoccupied_entry_index); *entry_index = unoccupied_entry_index; entry_info_list_[unoccupied_entry_index] = std::move(entry_info_list_[initial_entry_index]); entry_info_list_[initial_entry_index].Clear(); return CdmResponseType(NO_ERROR); } bool CdmUsageTable::IsEntryUnoccupied(const UsageEntryIndex entry_index) const { // TODO(sigquit): Check that entry is not in use by another session. // NOTE: The |storage_type| check will protect the integrity of the // entry. Attempting to use an entry index that is used by another // session is recoverable and will not affect any opened sessions. return entry_info_list_[entry_index].storage_type == kStorageTypeUnknown; } void CdmUsageTable::SetOfflineEntryInfo(const UsageEntryIndex entry_index, const std::string& key_set_id, const CdmKeyResponse& license_message) { CdmUsageEntryInfo& entry_info = entry_info_list_[entry_index]; entry_info.Clear(); entry_info.storage_type = kStorageLicense; entry_info.key_set_id = key_set_id; entry_info.last_use_time = GetCurrentTime(); // Need to determine the expire time for offline licenses. video_widevine::License license; if (!license_message.empty() && ParseLicenseFromLicenseMessage(license_message, &license)) { const video_widevine::License::Policy& policy = license.policy(); entry_info.offline_license_expiry_time = license.license_start_time() + policy.rental_duration_seconds() + policy.playback_duration_seconds(); } else { // If the license duration cannot be determined for any reason, it // is assumed to last at most 33 days. entry_info.offline_license_expiry_time = entry_info.last_use_time + kDefaultExpireDuration; } } void CdmUsageTable::SetUsageInfoEntryInfo( const UsageEntryIndex entry_index, const std::string& key_set_id, const std::string& usage_info_file_name) { CdmUsageEntryInfo& entry_info = entry_info_list_[entry_index]; entry_info.Clear(); entry_info.storage_type = kStorageUsageInfo; entry_info.key_set_id = key_set_id; entry_info.last_use_time = GetCurrentTime(); entry_info.usage_info_file_name = usage_info_file_name; } CdmResponseType CdmUsageTable::RefitTable(CryptoSession* const crypto_session) { // Remove all unoccupied entries at end of the table. uint32_t entries_to_remove = 0; const uint32_t old_size = static_cast(entry_info_list_.size()); for (uint32_t i = 0; i < old_size; i++) { const UsageEntryIndex entry_index = old_size - i - 1; if (!IsEntryUnoccupied(entry_index)) break; ++entries_to_remove; } if (entries_to_remove == 0) return CdmResponseType(NO_ERROR); const uint32_t new_size = old_size - entries_to_remove; const CdmResponseType status = crypto_session->ShrinkUsageTableHeader( requested_security_level_, new_size, &header_); if (status == SHRINK_USAGE_TABLE_HEADER_ENTRY_IN_USE) { // This error likely indicates that another session has released // its entry via a call to InvalidateEntry(), but has yet to close // its OEMCrypto session. // Safe to assume table state is not invalidated. LOGW("Unexpected entry in use: range = [%u, %zu]", new_size, entry_info_list_.size() - 1); return CdmResponseType(NO_ERROR); } if (status != NO_ERROR) return status; LOGD("Table shrunk: old_size = %zu, new_size = %u", entry_info_list_.size(), new_size); entry_info_list_.resize(new_size); return CdmResponseType(NO_ERROR); } CdmResponseType CdmUsageTable::MoveEntry(UsageEntryIndex from_entry_index, const UsageEntry& from_entry, UsageEntryIndex to_entry_index, DeviceFiles* device_files, metrics::CryptoMetrics* metrics) { LOGD("from_entry_index = %u, to_entry_index = %u", from_entry_index, to_entry_index); // 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; CryptoSession* crypto_session = test_crypto_session_.get(); if (crypto_session == nullptr) { scoped_crypto_session.reset(CryptoSession::MakeCryptoSession(metrics)); crypto_session = scoped_crypto_session.get(); } CdmResponseType status = crypto_session->Open(requested_security_level_); if (status != NO_ERROR) { LOGE("Cannot open session for move: entry_index = %u", from_entry_index); return status; } status = crypto_session->LoadUsageEntry(from_entry_index, from_entry); if (status != NO_ERROR) { LOGE("Failed to load usage entry: entry_index = %u", from_entry_index); return status; } status = crypto_session->MoveUsageEntry(to_entry_index); if (status != NO_ERROR) { LOGE( "Failed to move usage entry: " "from_entry_index = %u, to_entry_index = %u", from_entry_index, to_entry_index); return status; } entry_info_list_[to_entry_index] = entry_info_list_[from_entry_index]; entry_info_list_[from_entry_index].Clear(); UsageEntry entry; status = crypto_session->UpdateUsageEntry(&header_, &entry); if (status != NO_ERROR) { LOGE("Failed to update usage entry: entry_index = %u", to_entry_index); return status; } // Store the usage table and usage entry after successful move. StoreTable(); StoreEntry(to_entry_index, device_files, entry); return CdmResponseType(NO_ERROR); } CdmResponseType CdmUsageTable::GetEntry(UsageEntryIndex entry_index, DeviceFiles* device_files, UsageEntry* entry) { LOGD("Getting entry_index = %u, storage_type = %s", entry_index, CdmUsageEntryStorageTypeToString( entry_index < entry_info_list_.size() ? entry_info_list_[entry_index].storage_type : kStorageTypeUnknown)); UsageEntryIndex retrieved_entry_index; switch (entry_info_list_[entry_index].storage_type) { case kStorageLicense: { DeviceFiles::CdmLicenseData license_data; DeviceFiles::ResponseType sub_error_code = DeviceFiles::kNoError; if (!device_files->RetrieveLicense( entry_info_list_[entry_index].key_set_id, &license_data, &sub_error_code)) { LOGE("Failed to retrieve license: status = %d", static_cast(sub_error_code)); return CdmResponseType(USAGE_GET_ENTRY_RETRIEVE_LICENSE_FAILED); } retrieved_entry_index = license_data.usage_entry_index; *entry = std::move(license_data.usage_entry); break; } case kStorageUsageInfo: { std::string provider_session_token; CdmKeyMessage license_request; CdmKeyResponse license; std::string drm_certificate; CryptoWrappedKey wrapped_private_key; if (!device_files->RetrieveUsageInfoByKeySetId( entry_info_list_[entry_index].usage_info_file_name, entry_info_list_[entry_index].key_set_id, &provider_session_token, &license_request, &license, entry, &retrieved_entry_index, &drm_certificate, &wrapped_private_key)) { LOGE("Failed to retrieve usage information"); return CdmResponseType(USAGE_GET_ENTRY_RETRIEVE_USAGE_INFO_FAILED); } break; } case kStorageTypeUnknown: default: LOGE( "Cannot retrieve usage information with unknown storage type: " "storage_type = %d", static_cast(entry_info_list_[entry_index].storage_type)); return CdmResponseType(USAGE_GET_ENTRY_RETRIEVE_INVALID_STORAGE_TYPE); } if (entry_index != retrieved_entry_index) { LOGE( "Usage entry index mismatch: expected_entry_index = %u, " "retrieved_entry_index = %u", entry_index, retrieved_entry_index); return CdmResponseType(USAGE_ENTRY_NUMBER_MISMATCH); } return CdmResponseType(NO_ERROR); } CdmResponseType CdmUsageTable::StoreEntry(UsageEntryIndex entry_index, DeviceFiles* device_files, const UsageEntry& entry) { LOGD("entry_index = %u, storage_type = %s", entry_index, CdmUsageEntryStorageTypeToString( entry_index < entry_info_list_.size() ? entry_info_list_[entry_index].storage_type : kStorageTypeUnknown)); switch (entry_info_list_[entry_index].storage_type) { case kStorageLicense: { DeviceFiles::CdmLicenseData license_data; DeviceFiles::ResponseType sub_error_code = DeviceFiles::kNoError; if (!device_files->RetrieveLicense( entry_info_list_[entry_index].key_set_id, &license_data, &sub_error_code)) { LOGE("Failed to retrieve license: status = %s", DeviceFiles::ResponseTypeToString(sub_error_code)); return CdmResponseType(USAGE_STORE_ENTRY_RETRIEVE_LICENSE_FAILED); } // Update. license_data.usage_entry = entry; license_data.usage_entry_index = entry_index; if (!device_files->StoreLicense(license_data, &sub_error_code)) { LOGE("Failed to store license: status = %s", DeviceFiles::ResponseTypeToString(sub_error_code)); return CdmResponseType(USAGE_STORE_LICENSE_FAILED); } break; } case kStorageUsageInfo: { UsageEntry retrieved_entry; UsageEntryIndex retrieved_entry_index; std::string provider_session_token, init_data, key_request, key_response, key_renewal_request; std::string drm_certificate; CryptoWrappedKey wrapped_private_key; if (!device_files->RetrieveUsageInfoByKeySetId( entry_info_list_[entry_index].usage_info_file_name, entry_info_list_[entry_index].key_set_id, &provider_session_token, &key_request, &key_response, &retrieved_entry, &retrieved_entry_index, &drm_certificate, &wrapped_private_key)) { LOGE("Failed to retrieve usage information"); return CdmResponseType(USAGE_STORE_ENTRY_RETRIEVE_USAGE_INFO_FAILED); } device_files->DeleteUsageInfo( entry_info_list_[entry_index].usage_info_file_name, entry_info_list_[entry_index].key_set_id); if (!device_files->StoreUsageInfo( provider_session_token, key_request, key_response, entry_info_list_[entry_index].usage_info_file_name, entry_info_list_[entry_index].key_set_id, entry, entry_index, drm_certificate, wrapped_private_key)) { LOGE("Failed to store usage information"); return CdmResponseType(USAGE_STORE_USAGE_INFO_FAILED); } break; } case kStorageTypeUnknown: default: LOGE( "Cannot retrieve usage information with unknown storage type: " "storage_type = %d", static_cast(entry_info_list_[entry_index].storage_type)); return CdmResponseType(USAGE_STORE_ENTRY_RETRIEVE_INVALID_STORAGE_TYPE); } return CdmResponseType(NO_ERROR); } bool CdmUsageTable::StoreTable() { LOGV("Storing usage table information"); const bool result = device_files_->StoreUsageTableInfo(header_, entry_info_list_); if (result) { ++store_table_counter_; } else { LOGW("Failed to store usage table info"); } return result; } CdmResponseType CdmUsageTable::Shrink( metrics::CryptoMetrics* metrics, uint32_t number_of_usage_entries_to_delete) { LOGD("table_size = %zu, number_to_delete = %u", entry_info_list_.size(), number_of_usage_entries_to_delete); if (entry_info_list_.empty()) { LOGE("Usage entry info table unexpectedly empty"); return CdmResponseType(NO_USAGE_ENTRIES); } if (entry_info_list_.size() < number_of_usage_entries_to_delete) { LOGW( "Cannot delete more entries than the table size, reducing to current " "table size: table_size = %zu, number_to_delete = %u", entry_info_list_.size(), number_of_usage_entries_to_delete); number_of_usage_entries_to_delete = static_cast(entry_info_list_.size()); } if (number_of_usage_entries_to_delete == 0) return CdmResponseType(NO_ERROR); // 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; CryptoSession* crypto_session = test_crypto_session_.get(); if (crypto_session == nullptr) { scoped_crypto_session.reset(CryptoSession::MakeCryptoSession(metrics)); crypto_session = scoped_crypto_session.get(); } const uint32_t new_size = static_cast(entry_info_list_.size()) - number_of_usage_entries_to_delete; const CdmResponseType status = crypto_session->ShrinkUsageTableHeader( requested_security_level_, new_size, &header_); if (status == NO_ERROR) { entry_info_list_.resize(new_size); StoreTable(); } return status; } CdmResponseType CdmUsageTable::DefragTable(DeviceFiles* device_files, metrics::CryptoMetrics* metrics) { LOGV("current_size = %zu", entry_info_list_.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 (entry_info_list_.empty()) { LOGD("Table empty, nothing to defrag"); return CdmResponseType(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 (UsageEntryIndex i = 0; i < entry_info_list_.size() && entries_to_remove.size() < kMaxDefragEntryMoves; ++i) { if (entry_info_list_[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 CdmResponseType(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 < entry_info_list_.size() && entries_to_move.size() < entries_to_remove.size(); ++i) { // Search from the end of the table. const UsageEntryIndex entry_index = static_cast(entry_info_list_.size()) - i - 1; if (entry_info_list_[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", entry_info_list_.size()); const CdmResponseType status = Shrink(metrics, static_cast(entry_info_list_.size())); if (status != NO_ERROR) { LOGE("Failed to shrink table: sts = %s", status.ToString().c_str()); } return status; } // 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 UsageEntryIndex 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 UsageEntryIndex to_remove = static_cast(entry_info_list_.size()) - last_valid_entry - 1; LOGD("Removing all entries after the last valid entry: count = %u", to_remove); const CdmResponseType status = Shrink(metrics, to_remove); if (status != NO_ERROR) { LOGE("Failed to shrink table: sts = %s", status.ToString().c_str()); } return status; } // Step 4: Move the valid entries to overwrite the invalid entries. // Moving the highest indexed valid entry to the lowest indexed // invalid entry. // Reversing vectors to make accessing and popping easier. This // will put the lowest index invalid entry and the highest index // 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 UsageEntryIndex to_entry_index = entries_to_remove.back(); const UsageEntryIndex from_entry_index = 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_index > from_entry_index) { LOGD("Entries will not be moved further down the table"); break; } UsageEntry from_entry; CdmResponseType status = GetEntry(from_entry_index, device_files, &from_entry); if (status != NO_ERROR) { LOGW("Could not get entry: entry_index = %u", from_entry_index); // It is unlikely that an unretrievable entry will suddenly // become retrievable later on when it is needed. entry_info_list_[from_entry_index].Clear(); entries_to_move.pop_back(); continue; } status = MoveEntry(from_entry_index, from_entry, to_entry_index, device_files, metrics); switch (status.code()) { 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_index = %u", from_entry_index); 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. entry_info_list_[from_entry_index].Clear(); LOGW("From entry was corrupted: from_entry_index = %u", from_entry_index); 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_index| 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_index = %u", to_entry_index); 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_index = %u, from_entry_index = %u", to_entry_index, from_entry_index); 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", status.ToInt()); return status; } } // End switch case. } // End while loop. // Step 5: Find the new last valid entry. UsageEntryIndex new_last_valid_entry = static_cast(entry_info_list_.size()); for (size_t i = 0; i < entry_info_list_.size(); ++i) { const UsageEntryIndex entry_index = static_cast(entry_info_list_.size() - i) - 1; if (entry_info_list_[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 == entry_info_list_.size()) { LOGD( "All entries have been invalidated, shrinking entire table: size = %zu", entry_info_list_.size()); const CdmResponseType status = Shrink(metrics, static_cast(entry_info_list_.size())); if (status != NO_ERROR) { LOGE("Failed to shrink table: sts = %s", status.ToString().c_str()); } return status; } const UsageEntryIndex to_remove = static_cast(entry_info_list_.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(); return CdmResponseType(NO_ERROR); } // Step 6: Shrink table to the new size. LOGD("Clean up complete, shrinking table: count = %u", to_remove); const CdmResponseType status = Shrink(metrics, to_remove); if (status != NO_ERROR) { LOGE("Failed to shrink table: sts = %s", status.ToString().c_str()); } return status; } // End Defrag(). CdmResponseType CdmUsageTable::ReleaseOldestEntry( metrics::CryptoMetrics* metrics) { LOGV("Releasing oldest entry"); UsageEntryIndex entry_index_to_delete; if (!GetRemovalCandidate(&entry_index_to_delete)) { LOGE("Could not determine which license to remove"); return CdmResponseType(UNKNOWN_ERROR); } const CdmUsageEntryInfo& entry_info = entry_info_list_[entry_index_to_delete]; const int64_t current_time = GetCurrentTime(); // Capture metric values now, as the |entry_info| reference will // change after the call to invalidate. const int64_t staleness = current_time - entry_info.last_use_time; const CdmUsageEntryStorageType storage_type = entry_info.storage_type; const CdmResponseType status = InvalidateEntryInternal(entry_index_to_delete, /* defrag_table = */ true, device_files_.get(), metrics); if (status != NO_ERROR) { LOGE("Failed to invalidate oldest entry: status = %d", status.ToInt()); return status; } // Record metrics on success. RecordLruEventMetrics(metrics, staleness, storage_type); return CdmResponseType(NO_ERROR); } // Test only method. void CdmUsageTable::InvalidateEntryForTest(UsageEntryIndex entry_index) { LOGD("entry_index = %u", entry_index); if (entry_index >= entry_info_list_.size()) { LOGE( "Requested usage entry index is larger than table size: " "entry_index = %u, table_size = %zu", entry_index, entry_info_list_.size()); return; } // Move last entry into invalidated entry location and shrink usage // entries. entry_info_list_[entry_index] = entry_info_list_[entry_info_list_.size() - 1]; entry_info_list_.resize(entry_info_list_.size() - 1); } bool CdmUsageTable::LruUpgradeAllUsageEntries() { LOGV("Upgrading all usage entries with LRU information"); if (entry_info_list_.size() == 0) return true; // Nothing to upgrade. // For each entry, the status upgrading that entry is stored. At the // end, all problematic licenses will be marked as invalid. std::vector bad_license_file_entries; for (UsageEntryIndex entry_index = 0; entry_index < entry_info_list_.size(); ++entry_index) { CdmUsageEntryInfo& entry_info = entry_info_list_[entry_index]; UsageEntryIndex retrieved_entry_index; CdmKeyResponse license_message; bool retrieve_response = false; switch (entry_info.storage_type) { case kStorageLicense: { retrieve_response = RetrieveOfflineLicense(device_files_.get(), entry_info.key_set_id, &license_message, &retrieved_entry_index); break; } case kStorageUsageInfo: { retrieve_response = RetrieveUsageInfoLicense( device_files_.get(), entry_info.usage_info_file_name, entry_info.key_set_id, &license_message, &retrieved_entry_index); break; } case kStorageTypeUnknown: bad_license_file_entries.push_back(entry_index); continue; default: { LOGW("Unknown usage entry storage type: %d, entry_index = %u", static_cast(entry_info.storage_type), entry_index); bad_license_file_entries.push_back(entry_index); continue; } } if (!retrieve_response) { LOGW("Could not retrieve license message: entry_index = %u", entry_index); bad_license_file_entries.push_back(entry_index); continue; } if (retrieved_entry_index != entry_index) { LOGW( "Usage entry index mismatched: entry_index = %u, " "retrieved_entry_index = %u", entry_index, retrieved_entry_index); bad_license_file_entries.push_back(entry_index); continue; } video_widevine::License license; if (!ParseLicenseFromLicenseMessage(license_message, &license)) { LOGW("Could not parse license: entry_index = %u", entry_index); bad_license_file_entries.push_back(entry_index); continue; } // If |license_start_time| is 0, then this entry will be considered // for replacement above all others. entry_info.last_use_time = license.license_start_time(); if (entry_info.storage_type == kStorageLicense) { // Only offline licenses need |offline_license_expiry_time| set. const video_widevine::License::Policy& policy = license.policy(); // TODO(b/139372190): Change how these fields are set once feature is // implemented. if (policy.license_duration_seconds() == 0) { // Zero implies unlimited license duration. entry_info.offline_license_expiry_time = license.license_start_time() + policy.rental_duration_seconds() + policy.playback_duration_seconds(); } else { entry_info.offline_license_expiry_time = license.license_start_time() + policy.license_duration_seconds(); } } else { entry_info.offline_license_expiry_time = 0; } } // End for loop. if (bad_license_file_entries.size() == entry_info_list_.size()) { LOGE("Failed to perform LRU upgrade for every entry: count = %zu", entry_info_list_.size()); return false; } // Maps -> []. std::map> usage_info_clean_up; for (UsageEntryIndex entry_index : bad_license_file_entries) { CdmUsageEntryInfo& entry_info = entry_info_list_[entry_index]; if (entry_info.storage_type == kStorageLicense) { device_files_->DeleteLicense(entry_info.key_set_id); } else if (entry_info.storage_type == kStorageUsageInfo) { // To reduce write cycles, the deletion of usage info will be done // in bulk. auto it = usage_info_clean_up.find(entry_info.usage_info_file_name); if (it == usage_info_clean_up.end()) { it = usage_info_clean_up .emplace(entry_info.usage_info_file_name, std::vector()) .first; } it->second.push_back(entry_info.key_set_id); } // else kStorageUnknown { Nothing special }. entry_info.Clear(); } for (const auto& p : usage_info_clean_up) { device_files_->DeleteMultipleUsageInfoByKeySetIds(p.first, p.second); } return true; } bool CdmUsageTable::GetRemovalCandidate(UsageEntryIndex* entry_to_remove) { const size_t lru_unexpired_threshold = HasUnlimitedTableCapacity() ? kLruUnexpiredThresholdFraction * size() : kLruUnexpiredThresholdFraction * potential_table_capacity(); return DetermineLicenseToRemove(entry_info_list_, GetCurrentTime(), lru_unexpired_threshold, entry_to_remove); } void CdmUsageTable::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 CdmUsageTable::DetermineLicenseToRemove( const std::vector& entry_info_list, int64_t current_time, size_t unexpired_threshold, UsageEntryIndex* entry_to_remove) { if (entry_to_remove == nullptr) { LOGE("Output parameter |entry_to_remove| is null"); return false; } if (entry_info_list.empty()) { return false; } // Returns true if entry of first index is more stale than the // entry of the second index. const auto is_more_stale = [&](UsageEntryIndex i, UsageEntryIndex j) -> bool { return entry_info_list[i].last_use_time < entry_info_list[j].last_use_time; }; // 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 UsageEntryIndex kNoEntry = std::numeric_limits::max(); UsageEntryIndex stalest_expired_offline_license = kNoEntry; UsageEntryIndex stalest_unexpired_offline_license = kNoEntry; UsageEntryIndex stalest_streaming_license = kNoEntry; size_t unexpired_license_count = 0; for (UsageEntryIndex entry_index = 0; entry_index < entry_info_list.size(); ++entry_index) { const CdmUsageEntryInfo& entry_info = entry_info_list[entry_index]; if (entry_info.storage_type != kStorageLicense && entry_info.storage_type != kStorageUsageInfo) { // Unknown storage type entries. Remove this entry. *entry_to_remove = entry_index; return true; } if (entry_info.storage_type == kStorageLicense && entry_info.offline_license_expiry_time > current_time) { // Unexpired offline. ++unexpired_license_count; if (stalest_unexpired_offline_license == kNoEntry || is_more_stale(entry_index, stalest_unexpired_offline_license)) { stalest_unexpired_offline_license = entry_index; } } else if (entry_info.storage_type == kStorageLicense) { // Expired offline. if (stalest_expired_offline_license == kNoEntry || is_more_stale(entry_index, stalest_expired_offline_license)) { stalest_expired_offline_license = entry_index; } } else { // Streaming. if (stalest_streaming_license == kNoEntry || is_more_stale(entry_index, stalest_streaming_license)) { stalest_streaming_license = entry_index; } } } 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", entry_info_list.size(), unexpired_license_count, unexpired_threshold); *entry_to_remove = stalest_unexpired_offline_license; return true; } const auto select_most_stale = [&](UsageEntryIndex a, UsageEntryIndex b) -> UsageEntryIndex { if (a == kNoEntry) return b; if (b == kNoEntry) return a; return is_more_stale(a, b) ? a : b; }; // Only consider an unexpired entry if the threshold is reached. if (unexpired_license_count > unexpired_threshold) { const UsageEntryIndex 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); } if (*entry_to_remove == kNoEntry) { // Illegal state check. The loop above should have found at least // one entry given that |entry_info_list| is not empty. LOGE("No entry could be used for removal: size = %zu", entry_info_list.size()); return false; } return true; } } // namespace wvcdm