diff --git a/libwvdrmengine/cdm/core/include/usage_table_header.h b/libwvdrmengine/cdm/core/include/usage_table_header.h index 9f7dc34f..c0637e95 100644 --- a/libwvdrmengine/cdm/core/include/usage_table_header.h +++ b/libwvdrmengine/cdm/core/include/usage_table_header.h @@ -154,6 +154,46 @@ class UsageTableHeader { bool DetermineTableCapacity(CryptoSession* crypto_session); // == Table operation methods == + // NOTE: The following "Table operation methods" require + // |usage_table_header_lock_| to be taken before calling. + + // Creates a new entry for the provided crypto session. If the + // entry is created successfully in OEMCrypto, then a new entry + // info is added to the table's vector of entry info. + CdmResponseType CreateEntry(CryptoSession* const crypto_session, + uint32_t* usage_entry_number); + + // Attempts to relocate a newly created usage entry associated with + // the provided |crypto_session| to the lowest unoccupied position in + // the table. + // |usage_entry_number| is treated as both an input and output. + // Returns NO_ERROR so long as no internal operation fails, + // regardless of whether the entry was moved or not. + CdmResponseType RelocateNewEntry(CryptoSession* const crypto_session, + uint32_t* usage_entry_number); + + // Checks if the specified |usage_entry_number| is known to be + // unoccupied (released). + bool IsEntryUnoccupied(const uint32_t usage_entry_number) const; + + // SetOfflineEntryInfo() and SetUsageInfoEntryInfo() populate the + // entry meta-data with the required information based on the type + // of entry. + void SetOfflineEntryInfo(const uint32_t usage_entry_number, + const std::string& key_set_id, + const CdmKeyResponse& license_message); + void SetUsageInfoEntryInfo(const uint32_t usage_entry_number, + const std::string& key_set_id, + const std::string& usage_info_file_name); + + // Shrinks the table, removing all trailing unoccupied entries. + // |usage_entry_info_| will be resized appropriately. + // Caller must store the table after a successful call. + CdmResponseType RefitTable(CryptoSession* const crypto_session); + + virtual CdmResponseType InvalidateEntryInternal( + uint32_t usage_entry_number, bool defrag_table, DeviceFiles* device_files, + metrics::CryptoMetrics* metrics); CdmResponseType MoveEntry(uint32_t from /* usage entry number */, const CdmUsageEntry& from_usage_entry, @@ -279,6 +319,7 @@ class UsageTableHeader { #if defined(UNIT_TEST) // Test related declarations friend class UsageTableHeaderTest; + FRIEND_TEST(UsageTableHeaderTest, Shrink_NoneOfTable); FRIEND_TEST(UsageTableHeaderTest, Shrink_PartOfTable); FRIEND_TEST(UsageTableHeaderTest, Shrink_AllOfTable); diff --git a/libwvdrmengine/cdm/core/src/usage_table_header.cpp b/libwvdrmengine/cdm/core/src/usage_table_header.cpp index 7cd7fb95..ee1f58db 100644 --- a/libwvdrmengine/cdm/core/src/usage_table_header.cpp +++ b/libwvdrmengine/cdm/core/src/usage_table_header.cpp @@ -15,6 +15,8 @@ namespace wvcdm { namespace { +using TableLock = std::unique_lock; + const std::string kEmptyString; const wvcdm::CdmKeySetId kDummyKeySetId = "DummyKsid"; @@ -252,75 +254,39 @@ CdmResponseType UsageTableHeader::AddEntry( CryptoSession* crypto_session, bool persistent_license, const CdmKeySetId& key_set_id, const std::string& usage_info_file_name, const CdmKeyResponse& license_message, uint32_t* usage_entry_number) { - LOGD("oec_session_id = %u, type = %s, current_size = %zu", - crypto_session->oec_session_id(), + LOGD("key_set_id = %s, type = %s, current_size = %zu", IdToString(key_set_id), persistent_license ? "OfflineLicense" : "Streaming", usage_entry_info_.size()); metrics::CryptoMetrics* metrics = crypto_session->GetCryptoMetrics(); if (metrics == nullptr) metrics = &alternate_crypto_metrics_; + TableLock auto_lock(usage_table_header_lock_); - CdmResponseType status = crypto_session->CreateUsageEntry(usage_entry_number); - + CdmResponseType status = CreateEntry(crypto_session, usage_entry_number); if (status == INSUFFICIENT_CRYPTO_RESOURCES) { LOGW("Usage table may be full, releasing oldest entry: size = %zu", usage_entry_info_.size()); status = ReleaseOldestEntry(metrics); if (status == NO_ERROR) { - status = crypto_session->CreateUsageEntry(usage_entry_number); + status = CreateEntry(crypto_session, usage_entry_number); } } - if (status != NO_ERROR) return status; - LOGV("Locking to add entry"); - std::unique_lock auto_lock(usage_table_header_lock_); - if (*usage_entry_number < usage_entry_info_.size()) { - LOGE( - "New entry number is smaller than table size: " - "entry_info_number = %u, table_size = %zu", - *usage_entry_number, usage_entry_info_.size()); - return USAGE_INVALID_NEW_ENTRY; - } + status = RelocateNewEntry(crypto_session, usage_entry_number); + if (status != NO_ERROR) return status; - if (*usage_entry_number > usage_entry_info_.size()) { - LOGW( - "New entry number is larger than table size, resizing: " - "entry_info_number = %u, table_size = %zu", - *usage_entry_number, usage_entry_info_.size()); - const size_t number_of_entries = usage_entry_info_.size(); - usage_entry_info_.resize(*usage_entry_number + 1); - for (size_t i = number_of_entries; i < usage_entry_info_.size() - 1; ++i) { - usage_entry_info_[i].Clear(); - } - } else /* *usage_entry_number == usage_entry_info_.size() */ { - usage_entry_info_.resize(*usage_entry_number + 1); - } - - usage_entry_info_[*usage_entry_number].storage_type = - persistent_license ? kStorageLicense : kStorageUsageInfo; - usage_entry_info_[*usage_entry_number].key_set_id = key_set_id; - usage_entry_info_[*usage_entry_number].last_use_time = GetCurrentTime(); - if (!persistent_license) { - usage_entry_info_[*usage_entry_number].usage_info_file_name = - usage_info_file_name; - usage_entry_info_[*usage_entry_number].offline_license_expiry_time = 0; + if (persistent_license) { + SetOfflineEntryInfo(*usage_entry_number, key_set_id, license_message); } else { - // Need to determine the expire time for offline licenses. - video_widevine::License license; - if (license_message.size() > 0 && - ParseLicenseFromLicenseMessage(license_message, &license)) { - const video_widevine::License::Policy& policy = license.policy(); - usage_entry_info_[*usage_entry_number].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. - usage_entry_info_[*usage_entry_number].offline_license_expiry_time = - usage_entry_info_[*usage_entry_number].last_use_time + - kDefaultExpireDuration; - } + SetUsageInfoEntryInfo(*usage_entry_number, key_set_id, + usage_info_file_name); + } + + status = RefitTable(crypto_session); + if (status != NO_ERROR) { + usage_entry_info_[*usage_entry_number].Clear(); + return status; } // Call to update the usage table header, but don't store the usage @@ -334,8 +300,6 @@ CdmResponseType UsageTableHeader::AddEntry( usage_entry_info_[*usage_entry_number].Clear(); return status; } - - LOGI("usage_entry_number = %u", *usage_entry_number); StoreTable(device_files_.get()); return NO_ERROR; } @@ -393,7 +357,14 @@ CdmResponseType UsageTableHeader::InvalidateEntry( uint32_t usage_entry_number, bool defrag_table, DeviceFiles* device_files, metrics::CryptoMetrics* metrics) { LOGD("usage_entry_number = %u", usage_entry_number); - std::unique_lock auto_lock(usage_table_header_lock_); + TableLock auto_lock(usage_table_header_lock_); + return InvalidateEntryInternal(usage_entry_number, defrag_table, device_files, + metrics); +} + +CdmResponseType UsageTableHeader::InvalidateEntryInternal( + uint32_t usage_entry_number, 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. @@ -436,14 +407,12 @@ CdmResponseType UsageTableHeader::InvalidateEntry( size_t UsageTableHeader::UsageInfoCount() const { LOGV("Locking to count usage info (streaming license) entries"); - std::unique_lock auto_lock(usage_table_header_lock_); return std::count_if(usage_entry_info_.cbegin(), usage_entry_info_.cend(), EntryIsUsageInfo); } size_t UsageTableHeader::OfflineEntryCount() const { LOGV("Locking to count offline license entries"); - std::unique_lock auto_lock(usage_table_header_lock_); return std::count_if(usage_entry_info_.cbegin(), usage_entry_info_.cend(), EntryIsOfflineLicense); } @@ -547,6 +516,151 @@ bool UsageTableHeader::DetermineTableCapacity(CryptoSession* crypto_session) { return true; } +CdmResponseType UsageTableHeader::CreateEntry( + CryptoSession* const crypto_session, uint32_t* usage_entry_number) { + const CdmResponseType status = + crypto_session->CreateUsageEntry(usage_entry_number); + if (status != NO_ERROR) return status; + // If the new entry number is smaller than expected, then the usage + // table may be out of sync or OEMCrypto has been rolled back. + // Not safe to continue. + if (*usage_entry_number < usage_entry_info_.size()) { + LOGE( + "New entry number is smaller than table size: " + "entry_info_number = %u, table_size = %zu", + *usage_entry_number, usage_entry_info_.size()); + return USAGE_INVALID_NEW_ENTRY; + } + LOGI("usage_entry_number = %u", *usage_entry_number); + const size_t previous_size = usage_entry_info_.size(); + usage_entry_info_.resize(*usage_entry_number + 1); + if (*usage_entry_number > previous_size) { + LOGW( + "New entry number is larger than table size, resizing: " + "entry_info_number = %u, table_size = %zu", + *usage_entry_number, previous_size); + for (size_t i = previous_size; i < usage_entry_info_.size() - 1; ++i) { + usage_entry_info_[i].Clear(); + } + } + usage_entry_info_[*usage_entry_number].Clear(); + return NO_ERROR; +} + +CdmResponseType UsageTableHeader::RelocateNewEntry( + CryptoSession* const crypto_session, uint32_t* usage_entry_number) { + static constexpr uint32_t kMinimumEntryNumber = 0; + const uint32_t initial_entry_number = *usage_entry_number; + if (initial_entry_number == kMinimumEntryNumber) { + // First entry in the table. + return NO_ERROR; + } + uint32_t unoccupied_entry_number = initial_entry_number; + for (uint32_t i = kMinimumEntryNumber; i < initial_entry_number; i++) { + if (IsEntryUnoccupied(i)) { + unoccupied_entry_number = i; + break; + } + } + if (unoccupied_entry_number == initial_entry_number) { + // No open position. + return NO_ERROR; + } + const CdmResponseType status = + crypto_session->MoveUsageEntry(unoccupied_entry_number); + 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_number); + return NO_ERROR; + } + if (status != NO_ERROR) return status; + LOGI("Entry moved: from_index = %u, to_index = %u", initial_entry_number, + unoccupied_entry_number); + *usage_entry_number = unoccupied_entry_number; + usage_entry_info_[unoccupied_entry_number] = + std::move(usage_entry_info_[initial_entry_number]); + usage_entry_info_[initial_entry_number].Clear(); + return NO_ERROR; +} + +bool UsageTableHeader::IsEntryUnoccupied( + const uint32_t usage_entry_number) const { + if (usage_entry_info_[usage_entry_number].storage_type != + kStorageTypeUnknown) { + return false; + } + // 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 true; +} + +void UsageTableHeader::SetOfflineEntryInfo( + const uint32_t usage_entry_number, const std::string& key_set_id, + const CdmKeyResponse& license_message) { + CdmUsageEntryInfo& entry_info = usage_entry_info_[usage_entry_number]; + 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 UsageTableHeader::SetUsageInfoEntryInfo( + const uint32_t usage_entry_number, const std::string& key_set_id, + const std::string& usage_info_file_name) { + CdmUsageEntryInfo& entry_info = usage_entry_info_[usage_entry_number]; + 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 UsageTableHeader::RefitTable( + CryptoSession* const crypto_session) { + // Remove all unoccupied entries at end of the table. + uint32_t entries_to_remove = 0; + for (uint32_t i = 0; i < usage_entry_info_.size(); i++) { + const uint32_t usage_entry_number = usage_entry_info_.size() - i - 1; + if (!IsEntryUnoccupied(usage_entry_number)) break; + ++entries_to_remove; + } + if (entries_to_remove == 0) return NO_ERROR; + const uint32_t new_size = usage_entry_info_.size() - entries_to_remove; + const CdmResponseType status = crypto_session->ShrinkUsageTableHeader( + requested_security_level_, new_size, &usage_table_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, + usage_entry_info_.size() - 1); + return NO_ERROR; + } + if (status != NO_ERROR) return status; + LOGD("Table shrunk: old_size = %zu, new_size = %u", usage_entry_info_.size(), + new_size); + usage_entry_info_.resize(new_size); + return NO_ERROR; +} + CdmResponseType UsageTableHeader::MoveEntry( uint32_t from_usage_entry_number, const CdmUsageEntry& from_usage_entry, uint32_t to_usage_entry_number, DeviceFiles* device_files, @@ -1035,8 +1149,8 @@ CdmResponseType UsageTableHeader::ReleaseOldestEntry( const CdmUsageEntryStorageType storage_type = usage_entry_info.storage_type; const CdmResponseType status = - InvalidateEntry(entry_number_to_delete, /* defrag_table = */ true, - device_files_.get(), metrics); + InvalidateEntryInternal(entry_number_to_delete, /* defrag_table = */ true, + device_files_.get(), metrics); if (status != NO_ERROR) { LOGE("Failed to invalidate oldest entry: status = %d", @@ -1051,7 +1165,7 @@ CdmResponseType UsageTableHeader::ReleaseOldestEntry( // Test only method. void UsageTableHeader::InvalidateEntryForTest(uint32_t usage_entry_number) { - LOGV("usage_entry_number = %u", usage_entry_number); + LOGD("usage_entry_number = %u", usage_entry_number); if (usage_entry_number >= usage_entry_info_.size()) { LOGE( "Requested usage entry number is larger than table size: " @@ -1188,8 +1302,6 @@ bool UsageTableHeader::LruUpgradeAllUsageEntries() { } bool UsageTableHeader::GetRemovalCandidate(uint32_t* entry_to_remove) { - LOGV("Locking to determine removal candidates"); - std::unique_lock auto_lock(usage_table_header_lock_); const size_t lru_unexpired_threshold = HasUnlimitedTableCapacity() ? kLruUnexpiredThresholdFraction * size() diff --git a/libwvdrmengine/cdm/core/test/usage_table_header_unittest.cpp b/libwvdrmengine/cdm/core/test/usage_table_header_unittest.cpp index fb0b145b..36fd2ea9 100644 --- a/libwvdrmengine/cdm/core/test/usage_table_header_unittest.cpp +++ b/libwvdrmengine/cdm/core/test/usage_table_header_unittest.cpp @@ -202,8 +202,16 @@ const DeviceFiles::CdmUsageData kCdmUsageData3 = { const std::vector kEmptyUsageInfoUsageDataList; const std::vector kEmptyUsageEntryInfoVector; -std::vector kUsageEntryInfoVector; -std::vector k10UsageEntryInfoVector; +const std::vector kUsageEntryInfoVector = { + kUsageEntryInfoOfflineLicense1, kUsageEntryInfoSecureStop1, + kUsageEntryInfoStorageTypeUnknown}; +const std::vector k10UsageEntryInfoVector = { + kUsageEntryInfoOfflineLicense1, kUsageEntryInfoSecureStop1, + kUsageEntryInfoOfflineLicense2, kUsageEntryInfoSecureStop2, + kUsageEntryInfoOfflineLicense3, kUsageEntryInfoSecureStop3, + kUsageEntryInfoOfflineLicense4, kUsageEntryInfoSecureStop4, + kUsageEntryInfoOfflineLicense5, kUsageEntryInfoSecureStop5, +}; std::vector kOverFullUsageEntryInfoVector; const CdmOfflineLicenseState kActiveLicenseState = kLicenseStateActive; @@ -289,23 +297,6 @@ std::vector kUpgradedUsageEntryInfoList; namespace { void InitVectorConstants() { - kUsageEntryInfoVector.clear(); - kUsageEntryInfoVector.push_back(kUsageEntryInfoOfflineLicense1); - kUsageEntryInfoVector.push_back(kUsageEntryInfoSecureStop1); - kUsageEntryInfoVector.push_back(kUsageEntryInfoStorageTypeUnknown); - - k10UsageEntryInfoVector.clear(); - k10UsageEntryInfoVector.push_back(kUsageEntryInfoOfflineLicense1); - k10UsageEntryInfoVector.push_back(kUsageEntryInfoSecureStop1); - k10UsageEntryInfoVector.push_back(kUsageEntryInfoOfflineLicense2); - k10UsageEntryInfoVector.push_back(kUsageEntryInfoSecureStop2); - k10UsageEntryInfoVector.push_back(kUsageEntryInfoOfflineLicense3); - k10UsageEntryInfoVector.push_back(kUsageEntryInfoSecureStop3); - k10UsageEntryInfoVector.push_back(kUsageEntryInfoOfflineLicense4); - k10UsageEntryInfoVector.push_back(kUsageEntryInfoSecureStop4); - k10UsageEntryInfoVector.push_back(kUsageEntryInfoOfflineLicense5); - k10UsageEntryInfoVector.push_back(kUsageEntryInfoSecureStop5); - kOverFullUsageEntryInfoVector.clear(); for (size_t i = 0; i < (kDefaultTableCapacity + 1); ++i) { switch (i % 4) { @@ -479,8 +470,9 @@ class MockCryptoSession : public TestCryptoSession { class MockUsageTableHeader : public UsageTableHeader { public: MockUsageTableHeader() : UsageTableHeader() {} - MOCK_METHOD4(InvalidateEntry, CdmResponseType(uint32_t, bool, DeviceFiles*, - metrics::CryptoMetrics*)); + MOCK_METHOD4(InvalidateEntryInternal, + CdmResponseType(uint32_t, bool, DeviceFiles*, + metrics::CryptoMetrics*)); MOCK_METHOD6(AddEntry, CdmResponseType(CryptoSession*, bool, const CdmKeySetId&, const std::string&, const CdmKeyResponse&, uint32_t*)); @@ -1041,25 +1033,53 @@ TEST_F(UsageTableHeaderTest, AddEntry_UsageEntryTooSmall) { kEmptyString /* license */, &usage_entry_number)); } +// Initial Test state: +// 1. Table has a few entries, one of which is unoccupied. +// 2. An entry-less session requires a new entry. +// +// Attempting to add an entry will result in: +// a. A successful call to OEMCrypto to create an entry +// at the end of the current table +// b. Moving the new entry to the unoccupied entry index +// c. Shrink table to remove the now empty entry slot created in (a) +// d. Storing the new updated usage table +// +// Storage type Usage entries +// at start at end +// ============= ======== ====== +// Offline License 1 0 0 +// Secure Stop 1 1 1 +// Storage Type Unknown 2 Replaced +// Offline License 2 DNE 2 +// +// DNE = Does Not Exist +// +// # of usage entries 3 3 TEST_F(UsageTableHeaderTest, AddEntry_NextConsecutiveOfflineUsageEntry) { Init(kSecurityLevelL1, kUsageTableHeader, kUsageEntryInfoVector); - const uint32_t expect_usage_entry_number = kUsageEntryInfoVector.size(); + const uint32_t initial_usage_entry_number = kUsageEntryInfoVector.size(); + const uint32_t final_usage_entry_number = kUsageEntryInfoVector.size() - 1; std::vector expect_usage_entry_info_vector = kUsageEntryInfoVector; - expect_usage_entry_info_vector.resize(expect_usage_entry_number + 1); - expect_usage_entry_info_vector[expect_usage_entry_number] = + expect_usage_entry_info_vector[final_usage_entry_number] = kUsageEntryInfoOfflineLicense2; - EXPECT_CALL(*crypto_session_, CreateUsageEntry(NotNull())) + .WillOnce(DoAll(SetArgPointee<0>(initial_usage_entry_number), + Return(NO_ERROR))); + EXPECT_CALL(*crypto_session_, MoveUsageEntry(final_usage_entry_number)) + .WillOnce(Return(NO_ERROR)); + EXPECT_CALL( + *crypto_session_, + ShrinkUsageTableHeader(kLevelDefault, + expect_usage_entry_info_vector.size(), NotNull())) .WillOnce( - DoAll(SetArgPointee<0>(expect_usage_entry_number), Return(NO_ERROR))); + DoAll(SetArgPointee<2>(kAnotherUsageTableHeader), Return(NO_ERROR))); EXPECT_CALL(*crypto_session_, UpdateUsageEntry(NotNull(), NotNull())) - .WillOnce( - DoAll(SetArgPointee<0>(kAnotherUsageTableHeader), Return(NO_ERROR))); + .WillOnce(DoAll(SetArgPointee<0>(kYetAnotherUsageTableHeader), + Return(NO_ERROR))); EXPECT_CALL(*device_files_, - StoreUsageTableInfo( - kAnotherUsageTableHeader, - UnorderedElementsAreArray(expect_usage_entry_info_vector))) + StoreUsageTableInfo(kYetAnotherUsageTableHeader, + expect_usage_entry_info_vector)) .WillOnce(Return(true)); uint32_t usage_entry_number = 0; @@ -1070,29 +1090,182 @@ TEST_F(UsageTableHeaderTest, AddEntry_NextConsecutiveOfflineUsageEntry) { kUsageEntryInfoOfflineLicense2.key_set_id, kUsageEntryInfoOfflineLicense2.usage_info_file_name, kEmptyString /* license */, &usage_entry_number)); - EXPECT_EQ(expect_usage_entry_number, usage_entry_number); + EXPECT_EQ(final_usage_entry_number, usage_entry_number); } +// Initial Test state: +// 1. Table has a few entries, one of which is unoccupied. +// 2. An entry-less session requires a new entry. +// +// Attempting to add an entry will result in: +// a. A successful call to OEMCrypto to create an entry +// at the end of the current table +// b. Moving the new entry to the unoccupied entry index +// c. Shrink table to remove the now empty entry slot created in (a) +// d. Storing the new updated usage table +// +// Storage type Usage entries +// at start at end +// ============= ======== ====== +// Offline License 1 0 0 +// Secure Stop 1 1 1 +// Storage Type Unknown 2 Replaced +// Secure Stop 2 DNE 2 +// +// DNE = Does Not Exist +// +// # of usage entries 3 3 TEST_F(UsageTableHeaderTest, AddEntry_NextConsecutiveSecureStopUsageEntry) { Init(kSecurityLevelL1, kUsageTableHeader, kUsageEntryInfoVector); - const uint32_t expect_usage_entry_number = kUsageEntryInfoVector.size(); + const uint32_t initial_usage_entry_number = kUsageEntryInfoVector.size(); + const uint32_t final_usage_entry_number = kUsageEntryInfoVector.size() - 1; std::vector expect_usage_entry_info_vector = kUsageEntryInfoVector; - - expect_usage_entry_info_vector.resize(expect_usage_entry_number + 1); - expect_usage_entry_info_vector[expect_usage_entry_number] = + expect_usage_entry_info_vector[final_usage_entry_number] = kUsageEntryInfoSecureStop2; EXPECT_CALL(*crypto_session_, CreateUsageEntry(NotNull())) + .WillOnce(DoAll(SetArgPointee<0>(initial_usage_entry_number), + Return(NO_ERROR))); + EXPECT_CALL(*crypto_session_, MoveUsageEntry(final_usage_entry_number)) + .WillOnce(Return(NO_ERROR)); + EXPECT_CALL( + *crypto_session_, + ShrinkUsageTableHeader(kLevelDefault, + expect_usage_entry_info_vector.size(), NotNull())) .WillOnce( - DoAll(SetArgPointee<0>(expect_usage_entry_number), Return(NO_ERROR))); + DoAll(SetArgPointee<2>(kAnotherUsageTableHeader), Return(NO_ERROR))); + EXPECT_CALL(*crypto_session_, UpdateUsageEntry(NotNull(), NotNull())) + .WillOnce(DoAll(SetArgPointee<0>(kYetAnotherUsageTableHeader), + Return(NO_ERROR))); + EXPECT_CALL(*device_files_, + StoreUsageTableInfo(kYetAnotherUsageTableHeader, + expect_usage_entry_info_vector)) + .WillOnce(Return(true)); + + uint32_t usage_entry_number = 0; + EXPECT_EQ(NO_ERROR, + usage_table_header_->AddEntry( + crypto_session_, + kUsageEntryInfoSecureStop2.storage_type == kStorageLicense, + kUsageEntryInfoSecureStop2.key_set_id, + kUsageEntryInfoSecureStop2.usage_info_file_name, + kEmptyString /* license */, &usage_entry_number)); + EXPECT_EQ(final_usage_entry_number, usage_entry_number); +} + +// Initial Test state: +// 1. Table has a few entries, one of which is unoccupied. +// 2. An entry-less session requires a new entry. +// +// Attempting to add an entry will result in: +// a. An odd but successful call to OEMCrypto to create an entry +// beyond the end of the current table +// b. Empty entries will fill the gap between the original table +// and the new entry +// c. Move the new entry to the *lowest* unoccupied entry index +// d. Shrink table to remove the now empty entry slot created in (a) +// and the filler gap entries created in (b) +// e. Storing the new updated usage table +// +// Storage type Usage entries +// at start at end +// ============= ======== ====== +// Offline License 1 0 0 +// Secure Stop 1 1 1 +// Storage Type Unknown 2 Replaced +// Storage Type Unknown DNE Removed (never stored) +// Storage Type Unknown DNE Removed (never stored) +// Storage Type Unknown DNE Removed (never stored) +// Secure Stop 2 DNE 2 +// +// DNE = Does Not Exist +// +// # of usage entries 3 3 +TEST_F(UsageTableHeaderTest, AddEntry_SkipUsageEntries) { + Init(kSecurityLevelL1, kUsageTableHeader, kUsageEntryInfoVector); + const uint32_t next_usage_entry_number = kUsageEntryInfoVector.size(); + const size_t skip_usage_entries = 3; + const uint32_t initial_usage_entry_number = + next_usage_entry_number + skip_usage_entries; + const uint32_t final_usage_entry_number = kUsageEntryInfoVector.size() - 1; + std::vector expect_usage_entry_info_vector = + kUsageEntryInfoVector; + expect_usage_entry_info_vector[final_usage_entry_number] = + kUsageEntryInfoSecureStop2; + + EXPECT_CALL(*crypto_session_, CreateUsageEntry(NotNull())) + .WillOnce(DoAll(SetArgPointee<0>(initial_usage_entry_number), + Return(NO_ERROR))); + EXPECT_CALL(*crypto_session_, MoveUsageEntry(final_usage_entry_number)) + .WillOnce(Return(NO_ERROR)); + EXPECT_CALL( + *crypto_session_, + ShrinkUsageTableHeader(kLevelDefault, + expect_usage_entry_info_vector.size(), NotNull())) + .WillOnce( + DoAll(SetArgPointee<2>(kAnotherUsageTableHeader), Return(NO_ERROR))); + EXPECT_CALL(*crypto_session_, UpdateUsageEntry(NotNull(), NotNull())) + .WillOnce(DoAll(SetArgPointee<0>(kYetAnotherUsageTableHeader), + Return(NO_ERROR))); + EXPECT_CALL(*device_files_, + StoreUsageTableInfo(kYetAnotherUsageTableHeader, + expect_usage_entry_info_vector)) + .WillOnce(Return(true)); + + uint32_t usage_entry_number = 0; + EXPECT_EQ(NO_ERROR, + usage_table_header_->AddEntry( + crypto_session_, + kUsageEntryInfoSecureStop2.storage_type == kStorageLicense, + kUsageEntryInfoSecureStop2.key_set_id, + kUsageEntryInfoSecureStop2.usage_info_file_name, + kEmptyString /* license */, &usage_entry_number)); + EXPECT_EQ(final_usage_entry_number, usage_entry_number); +} + +// Initial Test state: +// 1. Table has a few entries, one of which is unoccupied. +// 2. An entry-less session requires a new entry. +// +// Attempting to add an entry will result in: +// a. A successful call to OEMCrypto to create an entry +// at the end of the current table +// b. Cannot move the new entry to the unoccupied entry index +// due to entry being in use (according to OEMCrypto) +// c. Storing the new updated usage table +// +// Storage type Usage entries +// at start at end +// ============= ======== ====== +// Offline License 1 0 0 +// Secure Stop 1 1 1 +// Storage Type Unknown 2 2 +// Secure Stop 2 DNE 3 +// +// DNE = Does Not Exist +// +// # of usage entries 3 4 +TEST_F(UsageTableHeaderTest, AddEntry_CannotMoveNewEntry) { + Init(kSecurityLevelL1, kUsageTableHeader, kUsageEntryInfoVector); + const uint32_t final_usage_entry_number = kUsageEntryInfoVector.size(); + const uint32_t attempted_usage_entry_number = + kUsageEntryInfoVector.size() - 1; + std::vector expect_usage_entry_info_vector = + kUsageEntryInfoVector; + expect_usage_entry_info_vector.push_back(kUsageEntryInfoSecureStop2); + + EXPECT_CALL(*crypto_session_, CreateUsageEntry(NotNull())) + .WillOnce( + DoAll(SetArgPointee<0>(final_usage_entry_number), Return(NO_ERROR))); + EXPECT_CALL(*crypto_session_, MoveUsageEntry(attempted_usage_entry_number)) + .WillOnce(Return(MOVE_USAGE_ENTRY_DESTINATION_IN_USE)); EXPECT_CALL(*crypto_session_, UpdateUsageEntry(NotNull(), NotNull())) .WillOnce( DoAll(SetArgPointee<0>(kAnotherUsageTableHeader), Return(NO_ERROR))); EXPECT_CALL(*device_files_, - StoreUsageTableInfo( - kAnotherUsageTableHeader, - UnorderedElementsAreArray(expect_usage_entry_info_vector))) + StoreUsageTableInfo(kAnotherUsageTableHeader, + expect_usage_entry_info_vector)) .WillOnce(Return(true)); uint32_t usage_entry_number = 0; @@ -1103,32 +1276,58 @@ TEST_F(UsageTableHeaderTest, AddEntry_NextConsecutiveSecureStopUsageEntry) { kUsageEntryInfoSecureStop2.key_set_id, kUsageEntryInfoSecureStop2.usage_info_file_name, kEmptyString /* license */, &usage_entry_number)); - EXPECT_EQ(expect_usage_entry_number, usage_entry_number); + EXPECT_EQ(final_usage_entry_number, usage_entry_number); } -TEST_F(UsageTableHeaderTest, AddEntry_SkipUsageEntries) { +// Initial Test state: +// 1. Table has a few entries, one of which is unoccupied. +// 2. An entry-less session requires a new entry. +// +// Attempting to add an entry will result in: +// a. A successful call to OEMCrypto to create an entry +// at the end of the current table +// b. Moving the new entry to the unoccupied entry index +// c. Fail to shrink table due to occupied entry +// d. Storing the new updated usage table +// +// Storage type Usage entries +// at start at end +// ============= ======== ====== +// Offline License 1 0 0 +// Secure Stop 1 1 1 +// Storage Type Unknown 2 Replaced +// Secure Stop 2 DNE 2 +// Storage Type Unknown DNE 3 (created when new entry moved) +// +// DNE = Does Not Exist +// +// # of usage entries 3 4 +TEST_F(UsageTableHeaderTest, AddEntry_CannotShinkAfterMove) { Init(kSecurityLevelL1, kUsageTableHeader, kUsageEntryInfoVector); - const uint32_t next_usage_entry_number = kUsageEntryInfoVector.size(); - size_t skip_usage_entries = 3; - uint32_t expect_usage_entry_number = - next_usage_entry_number + skip_usage_entries; + const uint32_t initial_usage_entry_number = kUsageEntryInfoVector.size(); + const uint32_t final_usage_entry_number = kUsageEntryInfoVector.size() - 1; + std::vector expect_usage_entry_info_vector = + kUsageEntryInfoVector; + expect_usage_entry_info_vector[final_usage_entry_number] = + kUsageEntryInfoSecureStop2; + expect_usage_entry_info_vector.push_back(kUsageEntryInfoStorageTypeUnknown); EXPECT_CALL(*crypto_session_, CreateUsageEntry(NotNull())) - .WillOnce( - DoAll(SetArgPointee<0>(expect_usage_entry_number), Return(NO_ERROR))); + .WillOnce(DoAll(SetArgPointee<0>(initial_usage_entry_number), + Return(NO_ERROR))); + EXPECT_CALL(*crypto_session_, MoveUsageEntry(final_usage_entry_number)) + .WillOnce(Return(NO_ERROR)); + EXPECT_CALL( + *crypto_session_, + ShrinkUsageTableHeader( + kLevelDefault, expect_usage_entry_info_vector.size() - 1, NotNull())) + .WillOnce(Return(SHRINK_USAGE_TABLE_HEADER_ENTRY_IN_USE)); EXPECT_CALL(*crypto_session_, UpdateUsageEntry(NotNull(), NotNull())) .WillOnce( DoAll(SetArgPointee<0>(kAnotherUsageTableHeader), Return(NO_ERROR))); - EXPECT_CALL( - *device_files_, - StoreUsageTableInfo( - kAnotherUsageTableHeader, - UnorderedElementsAre( - kUsageEntryInfoOfflineLicense1, kUsageEntryInfoSecureStop1, - kUsageEntryInfoStorageTypeUnknown, - kUsageEntryInfoStorageTypeUnknown, - kUsageEntryInfoStorageTypeUnknown, - kUsageEntryInfoStorageTypeUnknown, kUsageEntryInfoSecureStop2))) + EXPECT_CALL(*device_files_, + StoreUsageTableInfo(kAnotherUsageTableHeader, + expect_usage_entry_info_vector)) .WillOnce(Return(true)); uint32_t usage_entry_number = 0; @@ -1139,9 +1338,21 @@ TEST_F(UsageTableHeaderTest, AddEntry_SkipUsageEntries) { kUsageEntryInfoSecureStop2.key_set_id, kUsageEntryInfoSecureStop2.usage_info_file_name, kEmptyString /* license */, &usage_entry_number)); - EXPECT_EQ(expect_usage_entry_number, usage_entry_number); + EXPECT_EQ(final_usage_entry_number, usage_entry_number); } +// Initial Test state: +// 1. Table is full with entries +// 2. An entry-less session requires a new entry. +// +// Attempting to add an entry will result in: +// a. First call to OEMCrypto to create an entry fails due to +// table being full +// b. One of the existing entries will be removed, shrinking table +// by one +// c. Table will be stored +// d. Second call to OEMCrypto to create an entry will succeed +// e. Storing the new updated usage table TEST_F(UsageTableHeaderTest, AddEntry_CreateUsageEntryFailsOnce_SucceedsSecondTime) { // Initialize and setup @@ -1150,32 +1361,33 @@ TEST_F(UsageTableHeaderTest, std::vector usage_entry_info_vector_at_start = k10UsageEntryInfoVector; - uint32_t usage_entry_number_first_to_be_deleted; // randomly chosen - std::vector final_usage_entries; + uint32_t invalidated_entry = 0; // Randomly chosen by UsageTableHeader const uint32_t expected_usage_entry_number = k10UsageEntryInfoVector.size() - 1; - // Setup expectations + EXPECT_CALL(*crypto_session_, CreateUsageEntry(NotNull())) + // First call fails + .WillOnce(Return(INSUFFICIENT_CRYPTO_RESOURCES)) + // Second call succeeds + .WillOnce(DoAll(SetArgPointee<0>(expected_usage_entry_number), + Return(NO_ERROR))); + // Covers all other expectations. EXPECT_CALL(*mock_usage_table_header, - InvalidateEntry(_, true, device_files_, NotNull())) - .WillOnce(DoAll(SaveArg<0>(&usage_entry_number_first_to_be_deleted), + InvalidateEntryInternal(_, true, device_files_, NotNull())) + .WillOnce(DoAll(SaveArg<0>(&invalidated_entry), Invoke(this, &UsageTableHeaderTest::InvalidateEntry), Return(NO_ERROR))); - EXPECT_CALL(*crypto_session_, CreateUsageEntry(NotNull())) - .WillOnce(Return(INSUFFICIENT_CRYPTO_RESOURCES)) - .WillOnce(DoAll(SetArgPointee<0>(expected_usage_entry_number), - Return(NO_ERROR))); EXPECT_CALL(*crypto_session_, UpdateUsageEntry(NotNull(), NotNull())) .WillOnce( DoAll(SetArgPointee<0>(kAnotherUsageTableHeader), Return(NO_ERROR))); - + std::vector final_usage_entries; EXPECT_CALL(*device_files_, StoreUsageTableInfo(kAnotherUsageTableHeader, _)) .WillOnce(DoAll(SaveArg<1>(&final_usage_entries), Return(true))); // Now invoke the method under test - uint32_t usage_entry_number; + uint32_t usage_entry_number = 0; EXPECT_EQ(NO_ERROR, mock_usage_table_header->SuperAddEntry( crypto_session_, @@ -1187,17 +1399,15 @@ TEST_F(UsageTableHeaderTest, // Verify added/deleted usage entry number and entries EXPECT_EQ(expected_usage_entry_number, usage_entry_number); - EXPECT_LE(0u, usage_entry_number_first_to_be_deleted); - EXPECT_LE(usage_entry_number_first_to_be_deleted, - usage_entry_info_vector_at_start.size() - 1); + EXPECT_LE(0u, invalidated_entry); + EXPECT_LE(invalidated_entry, k10UsageEntryInfoVector.size() - 1); std::vector expected_usage_entries = - usage_entry_info_vector_at_start; - expected_usage_entries[usage_entry_number_first_to_be_deleted] = - expected_usage_entries[expected_usage_entries.size() - 1]; - expected_usage_entries.resize(expected_usage_entries.size() - 1); - expected_usage_entries.push_back(kUsageEntryInfoOfflineLicense6); + k10UsageEntryInfoVector; + expected_usage_entries[invalidated_entry] = expected_usage_entries.back(); + expected_usage_entries.pop_back(); + expected_usage_entries.push_back(kUsageEntryInfoOfflineLicense6); EXPECT_EQ(expected_usage_entries, final_usage_entries); } @@ -1210,7 +1420,7 @@ TEST_F(UsageTableHeaderTest, AddEntry_CreateUsageEntryFailsEveryTime) { // Setup expectations EXPECT_CALL(*mock_usage_table_header, - InvalidateEntry(_, true, device_files_, NotNull())) + InvalidateEntryInternal(_, true, device_files_, NotNull())) .WillOnce(DoAll(Invoke(this, &UsageTableHeaderTest::InvalidateEntry), Return(NO_ERROR)));