1440 lines
55 KiB
C++
1440 lines
55 KiB
C++
// 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 <algorithm>
|
|
#include <limits>
|
|
|
|
#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<std::mutex>;
|
|
|
|
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<int>(signed_license_response.type()),
|
|
static_cast<int>(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<std::mutex> 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<std::mutex> 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<CryptoSession> 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<uint32_t>(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<CryptoSession> 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<int>(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<int>(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<int>(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<uint32_t>(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<CryptoSession> 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<uint32_t>(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<UsageEntryIndex> 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<UsageEntryIndex> 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<UsageEntryIndex>(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<uint32_t>(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<UsageEntryIndex>(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<UsageEntryIndex>(entry_info_list_.size());
|
|
for (size_t i = 0; i < entry_info_list_.size(); ++i) {
|
|
const UsageEntryIndex entry_index =
|
|
static_cast<UsageEntryIndex>(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<uint32_t>(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<UsageEntryIndex>(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<UsageEntryIndex> 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<int>(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 <usage_info_file_name> -> [<key_set_id>].
|
|
std::map<std::string, std::vector<std::string>> 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<std::string>())
|
|
.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<int>(storage_type));
|
|
}
|
|
|
|
// Static.
|
|
bool CdmUsageTable::DetermineLicenseToRemove(
|
|
const std::vector<CdmUsageEntryInfo>& 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<UsageEntryIndex>::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
|