| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "media/cdm/aes_decryptor.h" |
| |
| #include <stdint.h> |
| #include <memory> |
| #include <set> |
| #include <string> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/cxx17_backports.h" |
| #include "base/debug/leak_annotations.h" |
| #include "base/json/json_reader.h" |
| #include "base/run_loop.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/task_environment.h" |
| #include "base/values.h" |
| #include "media/base/cdm_callback_promise.h" |
| #include "media/base/cdm_config.h" |
| #include "media/base/cdm_key_information.h" |
| #include "media/base/decoder_buffer.h" |
| #include "media/base/decrypt_config.h" |
| #include "media/base/decryptor.h" |
| #include "media/base/media_switches.h" |
| #include "media/base/mock_filters.h" |
| #include "media/cdm/cdm_module.h" |
| #include "media/media_buildflags.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest-param-test.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| #if BUILDFLAG(ENABLE_LIBRARY_CDMS) |
| // GN check does not understand conditionals. Android & Fuchsia need nogncheck. |
| #include "media/cdm/api/content_decryption_module.h" // nogncheck |
| #include "media/cdm/cdm_adapter.h" |
| #include "media/cdm/cdm_auxiliary_helper.h" |
| #include "media/cdm/external_clear_key_test_helper.h" |
| #include "media/cdm/mock_helpers.h" |
| #include "media/cdm/simple_cdm_allocator.h" |
| #endif |
| |
| using ::testing::_; |
| using ::testing::AtMost; |
| using ::testing::Gt; |
| using ::testing::IsNull; |
| using ::testing::NotNull; |
| using ::testing::SaveArg; |
| using ::testing::StrictMock; |
| using ::testing::StrNe; |
| using ::testing::Unused; |
| |
| MATCHER(IsEmpty, "") { |
| return arg.empty(); |
| } |
| MATCHER(NotEmpty, "") { |
| return !arg.empty(); |
| } |
| MATCHER(IsJSONDictionary, "") { |
| std::string result(arg.begin(), arg.end()); |
| absl::optional<base::Value> root = base::JSONReader::Read(result); |
| return (root && root->type() == base::Value::Type::DICTIONARY); |
| } |
| MATCHER(IsNullTime, "") { |
| return arg.is_null(); |
| } |
| |
| namespace media { |
| |
| namespace { |
| |
| const uint8_t kOriginalData[] = "Original subsample data."; |
| const int kOriginalDataSize = 24; |
| |
| // In the examples below, 'k'(key) has to be 16 bytes, and will always require |
| // 2 bytes of padding. 'kid'(keyid) is variable length, and may require 0, 1, |
| // or 2 bytes of padding. |
| |
| const uint8_t kKeyId[] = { |
| // base64 equivalent is AAECAw |
| 0x00, 0x01, 0x02, 0x03}; |
| |
| // Key is 0x0405060708090a0b0c0d0e0f10111213, |
| // base64 equivalent is BAUGBwgJCgsMDQ4PEBESEw. |
| const char kKeyAsJWK[] = |
| "{" |
| " \"keys\": [" |
| " {" |
| " \"kty\": \"oct\"," |
| " \"alg\": \"A128KW\"," |
| " \"kid\": \"AAECAw\"," |
| " \"k\": \"BAUGBwgJCgsMDQ4PEBESEw\"" |
| " }" |
| " ]," |
| " \"type\": \"temporary\"" |
| "}"; |
| |
| // Same kid as kKeyAsJWK, key to decrypt kEncryptedData2 |
| const char kKeyAlternateAsJWK[] = |
| "{" |
| " \"keys\": [" |
| " {" |
| " \"kty\": \"oct\"," |
| " \"alg\": \"A128KW\"," |
| " \"kid\": \"AAECAw\"," |
| " \"k\": \"FBUWFxgZGhscHR4fICEiIw\"" |
| " }" |
| " ]" |
| "}"; |
| |
| const char kWrongKeyAsJWK[] = |
| "{" |
| " \"keys\": [" |
| " {" |
| " \"kty\": \"oct\"," |
| " \"alg\": \"A128KW\"," |
| " \"kid\": \"AAECAw\"," |
| " \"k\": \"7u7u7u7u7u7u7u7u7u7u7g\"" |
| " }" |
| " ]" |
| "}"; |
| |
| const char kWrongSizedKeyAsJWK[] = |
| "{" |
| " \"keys\": [" |
| " {" |
| " \"kty\": \"oct\"," |
| " \"alg\": \"A128KW\"," |
| " \"kid\": \"AAECAw\"," |
| " \"k\": \"AAECAw\"" |
| " }" |
| " ]" |
| "}"; |
| |
| const uint8_t kIv[] = {0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; |
| |
| // kOriginalData encrypted with kKey and kIv but without any subsamples (or |
| // equivalently using kSubsampleEntriesCypherOnly). |
| const uint8_t kEncryptedData[] = { |
| 0x2f, 0x03, 0x09, 0xef, 0x71, 0xaf, 0x31, 0x16, 0xfa, 0x9d, 0x18, 0x43, |
| 0x1e, 0x96, 0x71, 0xb5, 0xbf, 0xf5, 0x30, 0x53, 0x9a, 0x20, 0xdf, 0x95}; |
| |
| // kOriginalData encrypted with kSubsampleKey and kSubsampleIv using |
| // kSubsampleEntriesNormal. |
| const uint8_t kSubsampleEncryptedData[] = { |
| 0x4f, 0x72, 0x09, 0x16, 0x09, 0xe6, 0x79, 0xad, 0x70, 0x73, 0x75, 0x62, |
| 0x09, 0xbb, 0x83, 0x1d, 0x4d, 0x08, 0xd7, 0x78, 0xa4, 0xa7, 0xf1, 0x2e}; |
| |
| const uint8_t kOriginalData2[] = "Changed Original data."; |
| |
| const uint8_t kIv2[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; |
| |
| const uint8_t kKeyId2[] = { |
| // base64 equivalent is AAECAwQFBgcICQoLDA0ODxAREhM= |
| 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, |
| 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13}; |
| |
| const char kKey2AsJWK[] = |
| "{" |
| " \"keys\": [" |
| " {" |
| " \"kty\": \"oct\"," |
| " \"alg\": \"A128KW\"," |
| " \"kid\": \"AAECAwQFBgcICQoLDA0ODxAREhM\"," |
| " \"k\": \"FBUWFxgZGhscHR4fICEiIw\"" |
| " }" |
| " ]" |
| "}"; |
| |
| // 'k' in bytes is x14x15x16x17x18x19x1ax1bx1cx1dx1ex1fx20x21x22x23 |
| |
| const uint8_t kEncryptedData2[] = { |
| 0x57, 0x66, 0xf4, 0x12, 0x1a, 0xed, 0xb5, 0x79, 0x1c, 0x8e, 0x25, |
| 0xd7, 0x17, 0xe7, 0x5e, 0x16, 0xe3, 0x40, 0x08, 0x27, 0x11, 0xe9}; |
| |
| // Subsample entries for testing. The sum of |cypher_bytes| and |clear_bytes| of |
| // all entries must be equal to kOriginalDataSize to make the subsample entries |
| // valid. |
| |
| const SubsampleEntry kSubsampleEntriesNormal[] = {{2, 7}, {3, 11}, {1, 0}}; |
| |
| const SubsampleEntry kSubsampleEntriesWrongSize[] = { |
| {3, 6}, // This entry doesn't match the correct entry. |
| {3, 11}, |
| {1, 0}}; |
| |
| const SubsampleEntry kSubsampleEntriesInvalidTotalSize[] = { |
| {1, 1000}, // This entry is too large. |
| {3, 11}, |
| {1, 0}}; |
| |
| const SubsampleEntry kSubsampleEntriesClearOnly[] = {{7, 0}, {8, 0}, {9, 0}}; |
| |
| const SubsampleEntry kSubsampleEntriesCypherOnly[] = {{0, 6}, {0, 8}, {0, 10}}; |
| |
| scoped_refptr<DecoderBuffer> CreateEncryptedBuffer( |
| const std::vector<uint8_t>& data, |
| const std::vector<uint8_t>& key_id, |
| const std::vector<uint8_t>& iv, |
| const std::vector<SubsampleEntry>& subsample_entries) { |
| DCHECK(!data.empty()); |
| DCHECK(!iv.empty()); |
| scoped_refptr<DecoderBuffer> encrypted_buffer(new DecoderBuffer(data.size())); |
| memcpy(encrypted_buffer->writable_data(), data.data(), data.size()); |
| std::string key_id_string(key_id.begin(), key_id.end()); |
| std::string iv_string(iv.begin(), iv.end()); |
| encrypted_buffer->set_decrypt_config(DecryptConfig::CreateCencConfig( |
| key_id_string, iv_string, subsample_entries)); |
| return encrypted_buffer; |
| } |
| |
| scoped_refptr<DecoderBuffer> CreateClearBuffer( |
| const std::vector<uint8_t>& data) { |
| DCHECK(!data.empty()); |
| scoped_refptr<DecoderBuffer> encrypted_buffer(new DecoderBuffer(data.size())); |
| memcpy(encrypted_buffer->writable_data(), data.data(), data.size()); |
| return encrypted_buffer; |
| } |
| |
| enum ExpectedResult { RESOLVED, REJECTED }; |
| |
| enum class TestType { |
| kAesDecryptor, // Use AesDecryptor directly. |
| kCdmAdapter, // Load ExternalClearKey using CdmAdapter. |
| }; |
| |
| } // namespace |
| |
| // These tests only test decryption logic (no decoding). |
| class AesDecryptorTest : public testing::TestWithParam<TestType> { |
| public: |
| AesDecryptorTest() |
| : original_data_(kOriginalData, kOriginalData + kOriginalDataSize), |
| encrypted_data_(kEncryptedData, |
| kEncryptedData + base::size(kEncryptedData)), |
| subsample_encrypted_data_( |
| kSubsampleEncryptedData, |
| kSubsampleEncryptedData + base::size(kSubsampleEncryptedData)), |
| key_id_(kKeyId, kKeyId + base::size(kKeyId)), |
| iv_(kIv, kIv + base::size(kIv)), |
| normal_subsample_entries_( |
| kSubsampleEntriesNormal, |
| kSubsampleEntriesNormal + base::size(kSubsampleEntriesNormal)) {} |
| |
| MOCK_METHOD2(BufferDecrypted, |
| void(Decryptor::Status, scoped_refptr<DecoderBuffer>)); |
| |
| protected: |
| void SetUp() override { |
| if (GetParam() == TestType::kAesDecryptor) { |
| OnCdmCreated( |
| new AesDecryptor( |
| base::BindRepeating(&MockCdmClient::OnSessionMessage, |
| base::Unretained(&cdm_client_)), |
| base::BindRepeating(&MockCdmClient::OnSessionClosed, |
| base::Unretained(&cdm_client_)), |
| base::BindRepeating(&MockCdmClient::OnSessionKeysChange, |
| base::Unretained(&cdm_client_)), |
| base::BindRepeating(&MockCdmClient::OnSessionExpirationUpdate, |
| base::Unretained(&cdm_client_))), |
| std::string()); |
| } else if (GetParam() == TestType::kCdmAdapter) { |
| #if BUILDFLAG(ENABLE_LIBRARY_CDMS) |
| CdmConfig cdm_config; // default settings of false are sufficient. |
| |
| // Enable use of External Clear Key CDM. |
| scoped_feature_list_.InitWithFeatures( |
| {media::kExternalClearKeyForTesting}, {}); |
| |
| helper_ = std::make_unique<ExternalClearKeyTestHelper>(); |
| |
| #if BUILDFLAG(ENABLE_CDM_HOST_VERIFICATION) |
| CdmModule::GetInstance()->Initialize(helper_->LibraryPath(), {}); |
| #else |
| CdmModule::GetInstance()->Initialize(helper_->LibraryPath()); |
| #endif // BUILDFLAG(ENABLE_CDM_HOST_VERIFICATION) |
| |
| CdmAdapter::CreateCdmFunc create_cdm_func = |
| CdmModule::GetInstance()->GetCreateCdmFunc(); |
| |
| std::unique_ptr<CdmAllocator> allocator(new SimpleCdmAllocator()); |
| std::unique_ptr<CdmAuxiliaryHelper> cdm_helper( |
| new MockCdmAuxiliaryHelper(std::move(allocator))); |
| CdmAdapter::Create( |
| helper_->KeySystemName(), cdm_config, create_cdm_func, |
| std::move(cdm_helper), |
| base::BindRepeating(&MockCdmClient::OnSessionMessage, |
| base::Unretained(&cdm_client_)), |
| base::BindRepeating(&MockCdmClient::OnSessionClosed, |
| base::Unretained(&cdm_client_)), |
| base::BindRepeating(&MockCdmClient::OnSessionKeysChange, |
| base::Unretained(&cdm_client_)), |
| base::BindRepeating(&MockCdmClient::OnSessionExpirationUpdate, |
| base::Unretained(&cdm_client_)), |
| base::BindOnce(&AesDecryptorTest::OnCdmCreated, |
| base::Unretained(this))); |
| |
| base::RunLoop().RunUntilIdle(); |
| #else |
| NOTREACHED() |
| << "CdmAdapter tests only supported when library CDMs are supported."; |
| #endif |
| } else { |
| NOTREACHED() << "Unsupported test parameter."; |
| } |
| } |
| |
| void TearDown() override { |
| #if BUILDFLAG(ENABLE_LIBRARY_CDMS) |
| if (GetParam() == TestType::kCdmAdapter) { |
| // CDM must be destroyed before the module is reset. |
| cdm_ = nullptr; |
| CdmModule::ResetInstanceForTesting(); |
| } |
| #endif |
| } |
| |
| void OnCdmCreated(const scoped_refptr<ContentDecryptionModule>& cdm, |
| const std::string& error_message) { |
| EXPECT_EQ(error_message, ""); |
| cdm_ = cdm; |
| decryptor_ = cdm_->GetCdmContext()->GetDecryptor(); |
| } |
| |
| void OnResolveWithSession(ExpectedResult expected_result, |
| const std::string& session_id) { |
| EXPECT_EQ(expected_result, RESOLVED) << "Unexpectedly resolved."; |
| EXPECT_GT(session_id.length(), 0ul); |
| session_id_ = session_id; |
| } |
| |
| void OnResolve(ExpectedResult expected_result) { |
| EXPECT_EQ(expected_result, RESOLVED) << "Unexpectedly resolved."; |
| } |
| |
| void OnReject(ExpectedResult expected_result, |
| CdmPromise::Exception exception_code, |
| uint32_t system_code, |
| const std::string& error_message) { |
| EXPECT_EQ(expected_result, REJECTED) |
| << "Unexpectedly rejected with message: " << error_message; |
| } |
| |
| std::unique_ptr<SimpleCdmPromise> CreatePromise( |
| ExpectedResult expected_result) { |
| std::unique_ptr<SimpleCdmPromise> promise(new CdmCallbackPromise<>( |
| base::BindOnce(&AesDecryptorTest::OnResolve, base::Unretained(this), |
| expected_result), |
| base::BindOnce(&AesDecryptorTest::OnReject, base::Unretained(this), |
| expected_result))); |
| return promise; |
| } |
| |
| std::unique_ptr<NewSessionCdmPromise> CreateSessionPromise( |
| ExpectedResult expected_result) { |
| std::unique_ptr<NewSessionCdmPromise> promise( |
| new CdmCallbackPromise<std::string>( |
| base::BindOnce(&AesDecryptorTest::OnResolveWithSession, |
| base::Unretained(this), expected_result), |
| base::BindOnce(&AesDecryptorTest::OnReject, base::Unretained(this), |
| expected_result))); |
| return promise; |
| } |
| |
| // Creates a new session using |key_id|. Returns the session ID. |
| std::string CreateSession(const std::vector<uint8_t>& key_id) { |
| DCHECK(!key_id.empty()); |
| EXPECT_CALL(cdm_client_, |
| OnSessionMessage(NotEmpty(), _, IsJSONDictionary())); |
| cdm_->CreateSessionAndGenerateRequest(CdmSessionType::kTemporary, |
| EmeInitDataType::WEBM, key_id, |
| CreateSessionPromise(RESOLVED)); |
| // This expects the promise to be called synchronously, which is the case |
| // for AesDecryptor. |
| return session_id_; |
| } |
| |
| // Closes the session specified by |session_id|. |
| void CloseSession(const std::string& session_id) { |
| EXPECT_CALL(cdm_client_, |
| OnSessionClosed(session_id, CdmSessionClosedReason::kClose)); |
| cdm_->CloseSession(session_id, CreatePromise(RESOLVED)); |
| } |
| |
| // Removes the session specified by |session_id|. |
| void RemoveSession(const std::string& session_id) { |
| EXPECT_CALL(cdm_client_, OnSessionKeysChangeCalled(session_id, false)); |
| cdm_->RemoveSession(session_id, CreatePromise(RESOLVED)); |
| } |
| |
| // Updates the session specified by |session_id| with |key|. |result| |
| // tests that the update succeeds or generates an error. |
| void UpdateSessionAndExpect(std::string session_id, |
| const std::string& key, |
| ExpectedResult expected_result, |
| bool new_key_expected) { |
| DCHECK(!key.empty()); |
| |
| if (expected_result == RESOLVED) { |
| EXPECT_CALL(cdm_client_, |
| OnSessionKeysChangeCalled(session_id, new_key_expected)); |
| } else { |
| EXPECT_CALL(cdm_client_, OnSessionKeysChangeCalled(_, _)).Times(0); |
| } |
| |
| // AesDecryptor never calls OnSessionExpirationUpdate() since Clear Key key |
| // system doesn't need it. But ClearKeyCdm does call it for testing purpose. |
| EXPECT_CALL(cdm_client_, |
| OnSessionExpirationUpdate(session_id, IsNullTime())) |
| .Times(AtMost(1)); |
| |
| cdm_->UpdateSession(session_id, |
| std::vector<uint8_t>(key.begin(), key.end()), |
| CreatePromise(expected_result)); |
| } |
| |
| bool KeysInfoContains(const std::vector<uint8_t>& expected_key_id, |
| CdmKeyInformation::KeyStatus expected_status = |
| CdmKeyInformation::USABLE) { |
| for (auto& key_id : cdm_client_.keys_info()) { |
| if (key_id->key_id == expected_key_id && |
| key_id->status == expected_status) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| enum DecryptExpectation { |
| SUCCESS, |
| DATA_MISMATCH, |
| DATA_AND_SIZE_MISMATCH, |
| DECRYPT_ERROR, |
| NO_KEY |
| }; |
| |
| void DecryptAndExpect(scoped_refptr<DecoderBuffer> encrypted, |
| const std::vector<uint8_t>& plain_text, |
| DecryptExpectation result) { |
| scoped_refptr<DecoderBuffer> decrypted; |
| |
| switch (result) { |
| case SUCCESS: |
| case DATA_MISMATCH: |
| case DATA_AND_SIZE_MISMATCH: |
| EXPECT_CALL(*this, BufferDecrypted(Decryptor::kSuccess, NotNull())) |
| .WillOnce(SaveArg<1>(&decrypted)); |
| break; |
| case DECRYPT_ERROR: |
| EXPECT_CALL(*this, BufferDecrypted(Decryptor::kError, IsNull())) |
| .WillOnce(SaveArg<1>(&decrypted)); |
| break; |
| case NO_KEY: |
| EXPECT_CALL(*this, BufferDecrypted(Decryptor::kNoKey, IsNull())) |
| .WillOnce(SaveArg<1>(&decrypted)); |
| break; |
| } |
| |
| if (GetParam() == TestType::kCdmAdapter) { |
| ANNOTATE_SCOPED_MEMORY_LEAK; // http://crbug.com/569736 |
| decryptor_->Decrypt(Decryptor::kVideo, encrypted, |
| base::BindOnce(&AesDecryptorTest::BufferDecrypted, |
| base::Unretained(this))); |
| } else { |
| decryptor_->Decrypt(Decryptor::kVideo, encrypted, |
| base::BindOnce(&AesDecryptorTest::BufferDecrypted, |
| base::Unretained(this))); |
| } |
| |
| std::vector<uint8_t> decrypted_text; |
| if (decrypted.get() && decrypted->data_size()) { |
| decrypted_text.assign(decrypted->data(), |
| decrypted->data() + decrypted->data_size()); |
| } |
| |
| switch (result) { |
| case SUCCESS: |
| EXPECT_EQ(plain_text, decrypted_text); |
| break; |
| case DATA_MISMATCH: |
| EXPECT_EQ(plain_text.size(), decrypted_text.size()); |
| EXPECT_NE(plain_text, decrypted_text); |
| break; |
| case DATA_AND_SIZE_MISMATCH: |
| EXPECT_NE(plain_text.size(), decrypted_text.size()); |
| break; |
| case DECRYPT_ERROR: |
| case NO_KEY: |
| EXPECT_TRUE(decrypted_text.empty()); |
| break; |
| } |
| } |
| |
| // Must be the first member to be initialized first and destroyed last. |
| base::test::SingleThreadTaskEnvironment task_environment_; |
| |
| StrictMock<MockCdmClient> cdm_client_; |
| scoped_refptr<ContentDecryptionModule> cdm_; |
| Decryptor* decryptor_; |
| std::string session_id_; |
| |
| #if BUILDFLAG(ENABLE_LIBRARY_CDMS) |
| base::test::ScopedFeatureList scoped_feature_list_; |
| |
| // Helper class to load/unload External Clear Key Library, if necessary. |
| std::unique_ptr<ExternalClearKeyTestHelper> helper_; |
| #endif |
| |
| // Constants for testing. |
| const std::vector<uint8_t> original_data_; |
| const std::vector<uint8_t> encrypted_data_; |
| const std::vector<uint8_t> subsample_encrypted_data_; |
| const std::vector<uint8_t> key_id_; |
| const std::vector<uint8_t> iv_; |
| const std::vector<SubsampleEntry> normal_subsample_entries_; |
| const std::vector<SubsampleEntry> no_subsample_entries_; |
| }; |
| |
| TEST_P(AesDecryptorTest, CreateSessionWithEmptyInitData) { |
| cdm_->CreateSessionAndGenerateRequest( |
| CdmSessionType::kTemporary, EmeInitDataType::WEBM, std::vector<uint8_t>(), |
| CreateSessionPromise(REJECTED)); |
| cdm_->CreateSessionAndGenerateRequest( |
| CdmSessionType::kTemporary, EmeInitDataType::CENC, std::vector<uint8_t>(), |
| CreateSessionPromise(REJECTED)); |
| cdm_->CreateSessionAndGenerateRequest( |
| CdmSessionType::kTemporary, EmeInitDataType::KEYIDS, |
| std::vector<uint8_t>(), CreateSessionPromise(REJECTED)); |
| } |
| |
| TEST_P(AesDecryptorTest, CreateSessionWithVariousLengthInitData_WebM) { |
| std::vector<uint8_t> init_data; |
| init_data.resize(1); |
| EXPECT_CALL(cdm_client_, OnSessionMessage(NotEmpty(), _, IsJSONDictionary())); |
| cdm_->CreateSessionAndGenerateRequest( |
| CdmSessionType::kTemporary, EmeInitDataType::WEBM, |
| std::vector<uint8_t>(init_data), CreateSessionPromise(RESOLVED)); |
| |
| init_data.resize(16); // The expected size. |
| EXPECT_CALL(cdm_client_, OnSessionMessage(NotEmpty(), _, IsJSONDictionary())); |
| cdm_->CreateSessionAndGenerateRequest( |
| CdmSessionType::kTemporary, EmeInitDataType::WEBM, |
| std::vector<uint8_t>(init_data), CreateSessionPromise(RESOLVED)); |
| |
| init_data.resize(512); |
| EXPECT_CALL(cdm_client_, OnSessionMessage(NotEmpty(), _, IsJSONDictionary())); |
| cdm_->CreateSessionAndGenerateRequest( |
| CdmSessionType::kTemporary, EmeInitDataType::WEBM, |
| std::vector<uint8_t>(init_data), CreateSessionPromise(RESOLVED)); |
| |
| init_data.resize(513); |
| cdm_->CreateSessionAndGenerateRequest( |
| CdmSessionType::kTemporary, EmeInitDataType::WEBM, |
| std::vector<uint8_t>(init_data), CreateSessionPromise(REJECTED)); |
| } |
| |
| TEST_P(AesDecryptorTest, MultipleCreateSession) { |
| EXPECT_CALL(cdm_client_, OnSessionMessage(NotEmpty(), _, IsJSONDictionary())); |
| cdm_->CreateSessionAndGenerateRequest( |
| CdmSessionType::kTemporary, EmeInitDataType::WEBM, |
| std::vector<uint8_t>(1), CreateSessionPromise(RESOLVED)); |
| |
| EXPECT_CALL(cdm_client_, OnSessionMessage(NotEmpty(), _, IsJSONDictionary())); |
| cdm_->CreateSessionAndGenerateRequest( |
| CdmSessionType::kTemporary, EmeInitDataType::WEBM, |
| std::vector<uint8_t>(1), CreateSessionPromise(RESOLVED)); |
| |
| EXPECT_CALL(cdm_client_, OnSessionMessage(NotEmpty(), _, IsJSONDictionary())); |
| cdm_->CreateSessionAndGenerateRequest( |
| CdmSessionType::kTemporary, EmeInitDataType::WEBM, |
| std::vector<uint8_t>(1), CreateSessionPromise(RESOLVED)); |
| } |
| |
| TEST_P(AesDecryptorTest, CreateSessionWithCencInitData) { |
| const uint8_t init_data[] = { |
| 0x00, 0x00, 0x00, 0x44, // size = 68 |
| 0x70, 0x73, 0x73, 0x68, // 'pssh' |
| 0x01, // version |
| 0x00, 0x00, 0x00, // flags |
| 0x10, 0x77, 0xEF, 0xEC, 0xC0, 0xB2, 0x4D, 0x02, // SystemID |
| 0xAC, 0xE3, 0x3C, 0x1E, 0x52, 0xE2, 0xFB, 0x4B, |
| 0x00, 0x00, 0x00, 0x02, // key count |
| 0x7E, 0x57, 0x1D, 0x03, 0x7E, 0x57, 0x1D, 0x03, // key1 |
| 0x7E, 0x57, 0x1D, 0x03, 0x7E, 0x57, 0x1D, 0x03, |
| 0x7E, 0x57, 0x1D, 0x04, 0x7E, 0x57, 0x1D, 0x04, // key2 |
| 0x7E, 0x57, 0x1D, 0x04, 0x7E, 0x57, 0x1D, 0x04, |
| 0x00, 0x00, 0x00, 0x00 // datasize |
| }; |
| |
| EXPECT_CALL(cdm_client_, OnSessionMessage(NotEmpty(), _, IsJSONDictionary())); |
| cdm_->CreateSessionAndGenerateRequest( |
| CdmSessionType::kTemporary, EmeInitDataType::CENC, |
| std::vector<uint8_t>(init_data, init_data + base::size(init_data)), |
| CreateSessionPromise(RESOLVED)); |
| } |
| |
| TEST_P(AesDecryptorTest, CreateSessionWithKeyIdsInitData) { |
| const char init_data[] = |
| "{\"kids\":[\"AQI\",\"AQIDBA\",\"AQIDBAUGBwgJCgsMDQ4PEA\"]}"; |
| |
| EXPECT_CALL(cdm_client_, OnSessionMessage(NotEmpty(), _, IsJSONDictionary())); |
| cdm_->CreateSessionAndGenerateRequest( |
| CdmSessionType::kTemporary, EmeInitDataType::KEYIDS, |
| std::vector<uint8_t>(init_data, init_data + base::size(init_data) - 1), |
| CreateSessionPromise(RESOLVED)); |
| } |
| |
| TEST_P(AesDecryptorTest, NormalDecryption) { |
| std::string session_id = CreateSession(key_id_); |
| UpdateSessionAndExpect(session_id, kKeyAsJWK, RESOLVED, true); |
| scoped_refptr<DecoderBuffer> encrypted_buffer = CreateEncryptedBuffer( |
| encrypted_data_, key_id_, iv_, no_subsample_entries_); |
| DecryptAndExpect(encrypted_buffer, original_data_, SUCCESS); |
| } |
| |
| TEST_P(AesDecryptorTest, UnencryptedFrame) { |
| // An empty iv string signals that the frame is unencrypted. |
| scoped_refptr<DecoderBuffer> unencrypted_buffer = |
| CreateClearBuffer(original_data_); |
| DecryptAndExpect(unencrypted_buffer, original_data_, SUCCESS); |
| } |
| |
| TEST_P(AesDecryptorTest, WrongKey) { |
| std::string session_id = CreateSession(key_id_); |
| UpdateSessionAndExpect(session_id, kWrongKeyAsJWK, RESOLVED, true); |
| scoped_refptr<DecoderBuffer> encrypted_buffer = CreateEncryptedBuffer( |
| encrypted_data_, key_id_, iv_, no_subsample_entries_); |
| DecryptAndExpect(encrypted_buffer, original_data_, DATA_MISMATCH); |
| } |
| |
| TEST_P(AesDecryptorTest, NoKey) { |
| scoped_refptr<DecoderBuffer> encrypted_buffer = CreateEncryptedBuffer( |
| encrypted_data_, key_id_, iv_, no_subsample_entries_); |
| EXPECT_CALL(*this, BufferDecrypted(AesDecryptor::kNoKey, IsNull())); |
| decryptor_->Decrypt(Decryptor::kVideo, encrypted_buffer, |
| base::BindOnce(&AesDecryptorTest::BufferDecrypted, |
| base::Unretained(this))); |
| } |
| |
| TEST_P(AesDecryptorTest, KeyReplacement) { |
| std::string session_id = CreateSession(key_id_); |
| scoped_refptr<DecoderBuffer> encrypted_buffer = CreateEncryptedBuffer( |
| encrypted_data_, key_id_, iv_, no_subsample_entries_); |
| |
| UpdateSessionAndExpect(session_id, kWrongKeyAsJWK, RESOLVED, true); |
| ASSERT_NO_FATAL_FAILURE( |
| DecryptAndExpect(encrypted_buffer, original_data_, DATA_MISMATCH)); |
| |
| UpdateSessionAndExpect(session_id, kKeyAsJWK, RESOLVED, false); |
| ASSERT_NO_FATAL_FAILURE( |
| DecryptAndExpect(encrypted_buffer, original_data_, SUCCESS)); |
| } |
| |
| TEST_P(AesDecryptorTest, WrongSizedKey) { |
| std::string session_id = CreateSession(key_id_); |
| UpdateSessionAndExpect(session_id, kWrongSizedKeyAsJWK, REJECTED, true); |
| } |
| |
| TEST_P(AesDecryptorTest, MultipleKeysAndFrames) { |
| std::string session_id = CreateSession(key_id_); |
| UpdateSessionAndExpect(session_id, kKeyAsJWK, RESOLVED, true); |
| scoped_refptr<DecoderBuffer> encrypted_buffer = CreateEncryptedBuffer( |
| encrypted_data_, key_id_, iv_, no_subsample_entries_); |
| ASSERT_NO_FATAL_FAILURE( |
| DecryptAndExpect(encrypted_buffer, original_data_, SUCCESS)); |
| |
| UpdateSessionAndExpect(session_id, kKey2AsJWK, RESOLVED, true); |
| |
| // The first key is still available after we added a second key. |
| ASSERT_NO_FATAL_FAILURE( |
| DecryptAndExpect(encrypted_buffer, original_data_, SUCCESS)); |
| |
| // The second key is also available. |
| encrypted_buffer = CreateEncryptedBuffer( |
| std::vector<uint8_t>(kEncryptedData2, |
| kEncryptedData2 + base::size(kEncryptedData2)), |
| std::vector<uint8_t>(kKeyId2, kKeyId2 + base::size(kKeyId2)), |
| std::vector<uint8_t>(kIv2, kIv2 + base::size(kIv2)), |
| no_subsample_entries_); |
| ASSERT_NO_FATAL_FAILURE(DecryptAndExpect( |
| encrypted_buffer, |
| std::vector<uint8_t>(kOriginalData2, |
| kOriginalData2 + base::size(kOriginalData2) - 1), |
| SUCCESS)); |
| } |
| |
| TEST_P(AesDecryptorTest, CorruptedIv) { |
| std::string session_id = CreateSession(key_id_); |
| UpdateSessionAndExpect(session_id, kKeyAsJWK, RESOLVED, true); |
| |
| std::vector<uint8_t> bad_iv = iv_; |
| bad_iv[1]++; |
| |
| scoped_refptr<DecoderBuffer> encrypted_buffer = CreateEncryptedBuffer( |
| encrypted_data_, key_id_, bad_iv, no_subsample_entries_); |
| |
| DecryptAndExpect(encrypted_buffer, original_data_, DATA_MISMATCH); |
| } |
| |
| TEST_P(AesDecryptorTest, CorruptedData) { |
| std::string session_id = CreateSession(key_id_); |
| UpdateSessionAndExpect(session_id, kKeyAsJWK, RESOLVED, true); |
| |
| std::vector<uint8_t> bad_data = encrypted_data_; |
| bad_data[1]++; |
| |
| scoped_refptr<DecoderBuffer> encrypted_buffer = |
| CreateEncryptedBuffer(bad_data, key_id_, iv_, no_subsample_entries_); |
| DecryptAndExpect(encrypted_buffer, original_data_, DATA_MISMATCH); |
| } |
| |
| TEST_P(AesDecryptorTest, EncryptedAsUnencryptedFailure) { |
| std::string session_id = CreateSession(key_id_); |
| UpdateSessionAndExpect(session_id, kKeyAsJWK, RESOLVED, true); |
| scoped_refptr<DecoderBuffer> unencrypted_buffer = |
| CreateClearBuffer(encrypted_data_); |
| DecryptAndExpect(unencrypted_buffer, original_data_, DATA_MISMATCH); |
| } |
| |
| TEST_P(AesDecryptorTest, SubsampleDecryption) { |
| std::string session_id = CreateSession(key_id_); |
| UpdateSessionAndExpect(session_id, kKeyAsJWK, RESOLVED, true); |
| scoped_refptr<DecoderBuffer> encrypted_buffer = CreateEncryptedBuffer( |
| subsample_encrypted_data_, key_id_, iv_, normal_subsample_entries_); |
| DecryptAndExpect(encrypted_buffer, original_data_, SUCCESS); |
| } |
| |
| // Ensures noninterference of data offset and subsample mechanisms. We never |
| // expect to encounter this in the wild, but since the DecryptConfig doesn't |
| // disallow such a configuration, it should be covered. |
| TEST_P(AesDecryptorTest, SubsampleDecryptionWithOffset) { |
| std::string session_id = CreateSession(key_id_); |
| UpdateSessionAndExpect(session_id, kKeyAsJWK, RESOLVED, true); |
| scoped_refptr<DecoderBuffer> encrypted_buffer = CreateEncryptedBuffer( |
| subsample_encrypted_data_, key_id_, iv_, normal_subsample_entries_); |
| DecryptAndExpect(encrypted_buffer, original_data_, SUCCESS); |
| } |
| |
| TEST_P(AesDecryptorTest, SubsampleWrongSize) { |
| std::string session_id = CreateSession(key_id_); |
| UpdateSessionAndExpect(session_id, kKeyAsJWK, RESOLVED, true); |
| |
| std::vector<SubsampleEntry> subsample_entries_wrong_size( |
| kSubsampleEntriesWrongSize, |
| kSubsampleEntriesWrongSize + base::size(kSubsampleEntriesWrongSize)); |
| |
| scoped_refptr<DecoderBuffer> encrypted_buffer = CreateEncryptedBuffer( |
| subsample_encrypted_data_, key_id_, iv_, subsample_entries_wrong_size); |
| DecryptAndExpect(encrypted_buffer, original_data_, DATA_MISMATCH); |
| } |
| |
| TEST_P(AesDecryptorTest, SubsampleInvalidTotalSize) { |
| std::string session_id = CreateSession(key_id_); |
| UpdateSessionAndExpect(session_id, kKeyAsJWK, RESOLVED, true); |
| |
| std::vector<SubsampleEntry> subsample_entries_invalid_total_size( |
| kSubsampleEntriesInvalidTotalSize, |
| kSubsampleEntriesInvalidTotalSize + |
| base::size(kSubsampleEntriesInvalidTotalSize)); |
| |
| scoped_refptr<DecoderBuffer> encrypted_buffer = |
| CreateEncryptedBuffer(subsample_encrypted_data_, key_id_, iv_, |
| subsample_entries_invalid_total_size); |
| DecryptAndExpect(encrypted_buffer, original_data_, DECRYPT_ERROR); |
| } |
| |
| // No cypher bytes in any of the subsamples. |
| TEST_P(AesDecryptorTest, SubsampleClearBytesOnly) { |
| std::string session_id = CreateSession(key_id_); |
| UpdateSessionAndExpect(session_id, kKeyAsJWK, RESOLVED, true); |
| |
| std::vector<SubsampleEntry> clear_only_subsample_entries( |
| kSubsampleEntriesClearOnly, |
| kSubsampleEntriesClearOnly + base::size(kSubsampleEntriesClearOnly)); |
| |
| scoped_refptr<DecoderBuffer> encrypted_buffer = CreateEncryptedBuffer( |
| original_data_, key_id_, iv_, clear_only_subsample_entries); |
| DecryptAndExpect(encrypted_buffer, original_data_, SUCCESS); |
| } |
| |
| // No clear bytes in any of the subsamples. |
| TEST_P(AesDecryptorTest, SubsampleCypherBytesOnly) { |
| std::string session_id = CreateSession(key_id_); |
| UpdateSessionAndExpect(session_id, kKeyAsJWK, RESOLVED, true); |
| |
| std::vector<SubsampleEntry> cypher_only_subsample_entries( |
| kSubsampleEntriesCypherOnly, |
| kSubsampleEntriesCypherOnly + base::size(kSubsampleEntriesCypherOnly)); |
| |
| scoped_refptr<DecoderBuffer> encrypted_buffer = CreateEncryptedBuffer( |
| encrypted_data_, key_id_, iv_, cypher_only_subsample_entries); |
| DecryptAndExpect(encrypted_buffer, original_data_, SUCCESS); |
| } |
| |
| TEST_P(AesDecryptorTest, CloseSession) { |
| std::string session_id = CreateSession(key_id_); |
| scoped_refptr<DecoderBuffer> encrypted_buffer = CreateEncryptedBuffer( |
| encrypted_data_, key_id_, iv_, no_subsample_entries_); |
| |
| UpdateSessionAndExpect(session_id, kKeyAsJWK, RESOLVED, true); |
| ASSERT_NO_FATAL_FAILURE( |
| DecryptAndExpect(encrypted_buffer, original_data_, SUCCESS)); |
| |
| CloseSession(session_id); |
| } |
| |
| TEST_P(AesDecryptorTest, RemoveSession) { |
| std::string session_id = CreateSession(key_id_); |
| scoped_refptr<DecoderBuffer> encrypted_buffer = CreateEncryptedBuffer( |
| encrypted_data_, key_id_, iv_, no_subsample_entries_); |
| |
| UpdateSessionAndExpect(session_id, kKeyAsJWK, RESOLVED, true); |
| ASSERT_NO_FATAL_FAILURE( |
| DecryptAndExpect(encrypted_buffer, original_data_, SUCCESS)); |
| |
| RemoveSession(session_id); |
| ASSERT_NO_FATAL_FAILURE( |
| DecryptAndExpect(encrypted_buffer, original_data_, NO_KEY)); |
| } |
| |
| TEST_P(AesDecryptorTest, RemoveThenCloseSession) { |
| std::string session_id = CreateSession(key_id_); |
| scoped_refptr<DecoderBuffer> encrypted_buffer = CreateEncryptedBuffer( |
| encrypted_data_, key_id_, iv_, no_subsample_entries_); |
| |
| UpdateSessionAndExpect(session_id, kKeyAsJWK, RESOLVED, true); |
| EXPECT_TRUE(KeysInfoContains(key_id_, CdmKeyInformation::USABLE)); |
| ASSERT_NO_FATAL_FAILURE( |
| DecryptAndExpect(encrypted_buffer, original_data_, SUCCESS)); |
| |
| RemoveSession(session_id); |
| EXPECT_TRUE(KeysInfoContains(key_id_, CdmKeyInformation::RELEASED)); |
| ASSERT_NO_FATAL_FAILURE( |
| DecryptAndExpect(encrypted_buffer, original_data_, NO_KEY)); |
| |
| CloseSession(session_id); |
| } |
| |
| TEST_P(AesDecryptorTest, NoKeyAfterCloseSession) { |
| std::string session_id = CreateSession(key_id_); |
| scoped_refptr<DecoderBuffer> encrypted_buffer = CreateEncryptedBuffer( |
| encrypted_data_, key_id_, iv_, no_subsample_entries_); |
| |
| UpdateSessionAndExpect(session_id, kKeyAsJWK, RESOLVED, true); |
| ASSERT_NO_FATAL_FAILURE( |
| DecryptAndExpect(encrypted_buffer, original_data_, SUCCESS)); |
| |
| CloseSession(session_id); |
| ASSERT_NO_FATAL_FAILURE( |
| DecryptAndExpect(encrypted_buffer, original_data_, NO_KEY)); |
| } |
| |
| TEST_P(AesDecryptorTest, LatestKeyUsed) { |
| std::string session_id1 = CreateSession(key_id_); |
| scoped_refptr<DecoderBuffer> encrypted_buffer = CreateEncryptedBuffer( |
| encrypted_data_, key_id_, iv_, no_subsample_entries_); |
| |
| // Add alternate key, buffer should not be decoded properly. |
| UpdateSessionAndExpect(session_id1, kKeyAlternateAsJWK, RESOLVED, true); |
| ASSERT_NO_FATAL_FAILURE( |
| DecryptAndExpect(encrypted_buffer, original_data_, DATA_MISMATCH)); |
| |
| // Create a second session with a correct key value for key_id_. |
| std::string session_id2 = CreateSession(key_id_); |
| UpdateSessionAndExpect(session_id2, kKeyAsJWK, RESOLVED, true); |
| |
| // Should be able to decode with latest key. |
| ASSERT_NO_FATAL_FAILURE( |
| DecryptAndExpect(encrypted_buffer, original_data_, SUCCESS)); |
| } |
| |
| TEST_P(AesDecryptorTest, LatestKeyUsedAfterCloseSession) { |
| std::string session_id1 = CreateSession(key_id_); |
| scoped_refptr<DecoderBuffer> encrypted_buffer = CreateEncryptedBuffer( |
| encrypted_data_, key_id_, iv_, no_subsample_entries_); |
| UpdateSessionAndExpect(session_id1, kKeyAsJWK, RESOLVED, true); |
| ASSERT_NO_FATAL_FAILURE( |
| DecryptAndExpect(encrypted_buffer, original_data_, SUCCESS)); |
| |
| // Create a second session with a different key value for key_id_. |
| std::string session_id2 = CreateSession(key_id_); |
| UpdateSessionAndExpect(session_id2, kKeyAlternateAsJWK, RESOLVED, true); |
| |
| // Should not be able to decode with new key. |
| ASSERT_NO_FATAL_FAILURE( |
| DecryptAndExpect(encrypted_buffer, original_data_, DATA_MISMATCH)); |
| |
| // Close second session, should revert to original key. |
| CloseSession(session_id2); |
| ASSERT_NO_FATAL_FAILURE( |
| DecryptAndExpect(encrypted_buffer, original_data_, SUCCESS)); |
| } |
| |
| TEST_P(AesDecryptorTest, JWKKey) { |
| std::string session_id = CreateSession(key_id_); |
| |
| // Try a simple JWK key (i.e. not in a set) |
| const std::string kJwkSimple = |
| "{" |
| " \"kty\": \"oct\"," |
| " \"alg\": \"A128KW\"," |
| " \"kid\": \"AAECAwQFBgcICQoLDA0ODxAREhM\"," |
| " \"k\": \"FBUWFxgZGhscHR4fICEiIw\"" |
| "}"; |
| UpdateSessionAndExpect(session_id, kJwkSimple, REJECTED, true); |
| |
| // Try a key list with multiple entries. |
| const std::string kJwksMultipleEntries = |
| "{" |
| " \"keys\": [" |
| " {" |
| " \"kty\": \"oct\"," |
| " \"alg\": \"A128KW\"," |
| " \"kid\": \"AAECAwQFBgcICQoLDA0ODxAREhM\"," |
| " \"k\": \"FBUWFxgZGhscHR4fICEiIw\"" |
| " }," |
| " {" |
| " \"kty\": \"oct\"," |
| " \"alg\": \"A128KW\"," |
| " \"kid\": \"JCUmJygpKissLS4vMA\"," |
| " \"k\":\"MTIzNDU2Nzg5Ojs8PT4_QA\"" |
| " }" |
| " ]" |
| "}"; |
| UpdateSessionAndExpect(session_id, kJwksMultipleEntries, RESOLVED, true); |
| |
| // Try a key with no spaces and some \n plus additional fields. |
| const std::string kJwksNoSpaces = |
| "\n\n{\"something\":1,\"keys\":[{\n\n\"kty\":\"oct\",\"alg\":\"A128KW\"," |
| "\"kid\":\"AQIDBAUGBwgJCgsMCg4PAA\",\"k\":\"GawgguFyGrWKav7AX4VKUg" |
| "\",\"foo\":\"bar\"}]}\n\n"; |
| UpdateSessionAndExpect(session_id, kJwksNoSpaces, RESOLVED, true); |
| |
| // Try some non-ASCII characters. |
| UpdateSessionAndExpect(session_id, |
| "This is not ASCII due to \xff\xfe\xfd in it.", |
| REJECTED, true); |
| |
| // Try a badly formatted key. Assume that the JSON parser is fully tested, |
| // so we won't try a lot of combinations. However, need a test to ensure |
| // that the code doesn't crash if invalid JSON received. |
| UpdateSessionAndExpect(session_id, "This is not a JSON key.", REJECTED, true); |
| |
| // Try passing some valid JSON that is not a dictionary at the top level. |
| UpdateSessionAndExpect(session_id, "40", REJECTED, true); |
| |
| // Try an empty dictionary. |
| UpdateSessionAndExpect(session_id, "{ }", REJECTED, true); |
| |
| // Try an empty 'keys' dictionary. |
| UpdateSessionAndExpect(session_id, "{ \"keys\": [] }", REJECTED, true); |
| |
| // Try with 'keys' not a dictionary. |
| UpdateSessionAndExpect(session_id, "{ \"keys\":\"1\" }", REJECTED, true); |
| |
| // Try with 'keys' a list of integers. |
| UpdateSessionAndExpect(session_id, "{ \"keys\": [ 1, 2, 3 ] }", REJECTED, |
| true); |
| |
| // Try padding(=) at end of 'k' base64 string. |
| const std::string kJwksWithPaddedKey = |
| "{" |
| " \"keys\": [" |
| " {" |
| " \"kty\": \"oct\"," |
| " \"alg\": \"A128KW\"," |
| " \"kid\": \"AAECAw\"," |
| " \"k\": \"BAUGBwgJCgsMDQ4PEBESEw==\"" |
| " }" |
| " ]" |
| "}"; |
| UpdateSessionAndExpect(session_id, kJwksWithPaddedKey, REJECTED, true); |
| |
| // Try padding(=) at end of 'kid' base64 string. |
| const std::string kJwksWithPaddedKeyId = |
| "{" |
| " \"keys\": [" |
| " {" |
| " \"kty\": \"oct\"," |
| " \"alg\": \"A128KW\"," |
| " \"kid\": \"AAECAw==\"," |
| " \"k\": \"BAUGBwgJCgsMDQ4PEBESEw\"" |
| " }" |
| " ]" |
| "}"; |
| UpdateSessionAndExpect(session_id, kJwksWithPaddedKeyId, REJECTED, true); |
| |
| // Try a key with invalid base64 encoding. |
| const std::string kJwksWithInvalidBase64 = |
| "{" |
| " \"keys\": [" |
| " {" |
| " \"kty\": \"oct\"," |
| " \"alg\": \"A128KW\"," |
| " \"kid\": \"!@#$%^&*()\"," |
| " \"k\": \"BAUGBwgJCgsMDQ4PEBESEw\"" |
| " }" |
| " ]" |
| "}"; |
| UpdateSessionAndExpect(session_id, kJwksWithInvalidBase64, REJECTED, true); |
| |
| // Try a 3-byte 'kid' where no base64 padding is required. |
| // |kJwksMultipleEntries| above has 2 'kid's that require 1 and 2 padding |
| // bytes. Note that 'k' has to be 16 bytes, so it will always require padding. |
| const std::string kJwksWithNoPadding = |
| "{" |
| " \"keys\": [" |
| " {" |
| " \"kty\": \"oct\"," |
| " \"alg\": \"A128KW\"," |
| " \"kid\": \"Kiss\"," |
| " \"k\": \"BAUGBwgJCgsMDQ4PEBESEw\"" |
| " }" |
| " ]" |
| "}"; |
| UpdateSessionAndExpect(session_id, kJwksWithNoPadding, RESOLVED, true); |
| |
| // Empty key id. |
| const std::string kJwksWithEmptyKeyId = |
| "{" |
| " \"keys\": [" |
| " {" |
| " \"kty\": \"oct\"," |
| " \"alg\": \"A128KW\"," |
| " \"kid\": \"\"," |
| " \"k\": \"BAUGBwgJCgsMDQ4PEBESEw\"" |
| " }" |
| " ]" |
| "}"; |
| UpdateSessionAndExpect(session_id, kJwksWithEmptyKeyId, REJECTED, true); |
| CloseSession(session_id); |
| } |
| |
| TEST_P(AesDecryptorTest, GetKeyIds) { |
| std::vector<uint8_t> key_id1(kKeyId, kKeyId + base::size(kKeyId)); |
| std::vector<uint8_t> key_id2(kKeyId2, kKeyId2 + base::size(kKeyId2)); |
| |
| std::string session_id = CreateSession(key_id_); |
| EXPECT_FALSE(KeysInfoContains(key_id1)); |
| EXPECT_FALSE(KeysInfoContains(key_id2)); |
| |
| // Add 1 key, verify it is returned. |
| UpdateSessionAndExpect(session_id, kKeyAsJWK, RESOLVED, true); |
| EXPECT_TRUE(KeysInfoContains(key_id1)); |
| EXPECT_FALSE(KeysInfoContains(key_id2)); |
| |
| // Add second key, verify both IDs returned. |
| UpdateSessionAndExpect(session_id, kKey2AsJWK, RESOLVED, true); |
| EXPECT_TRUE(KeysInfoContains(key_id1)); |
| EXPECT_TRUE(KeysInfoContains(key_id2)); |
| } |
| |
| TEST_P(AesDecryptorTest, NoKeysChangeForSameKey) { |
| std::vector<uint8_t> key_id(kKeyId, kKeyId + base::size(kKeyId)); |
| |
| std::string session_id = CreateSession(key_id_); |
| EXPECT_FALSE(KeysInfoContains(key_id)); |
| |
| // Add key, verify it is returned. |
| UpdateSessionAndExpect(session_id, kKeyAsJWK, RESOLVED, true); |
| EXPECT_TRUE(KeysInfoContains(key_id)); |
| |
| // Add key a second time. |
| UpdateSessionAndExpect(session_id, kKeyAsJWK, RESOLVED, false); |
| EXPECT_TRUE(KeysInfoContains(key_id)); |
| |
| // Create a new session. Add key, should indicate key added for this session. |
| std::string session_id2 = CreateSession(key_id_); |
| UpdateSessionAndExpect(session_id2, kKeyAsJWK, RESOLVED, true); |
| } |
| |
| TEST_P(AesDecryptorTest, RandomSessionIDs) { |
| std::vector<uint8_t> key_id(kKeyId, kKeyId + base::size(kKeyId)); |
| const size_t kNumIterations = 25; |
| std::set<std::string> seen_sessions; |
| |
| for (size_t i = 0; i < kNumIterations; ++i) { |
| std::string session_id = CreateSession(key_id_); |
| EXPECT_TRUE(seen_sessions.find(session_id) == seen_sessions.end()); |
| EXPECT_EQ(16u, session_id.length()); |
| seen_sessions.insert(session_id); |
| } |
| |
| EXPECT_EQ(kNumIterations, seen_sessions.size()); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(AesDecryptor, |
| AesDecryptorTest, |
| testing::Values(TestType::kAesDecryptor)); |
| |
| #if BUILDFLAG(ENABLE_LIBRARY_CDMS) |
| INSTANTIATE_TEST_SUITE_P(CdmAdapter, |
| AesDecryptorTest, |
| testing::Values(TestType::kCdmAdapter)); |
| #endif |
| |
| // TODO(jrummell): Once MojoCdm/MojoCdmService/MojoDecryptor/ |
| // MojoDecryptorService are implemented, add a third version that tests the |
| // CDM via mojo. |
| |
| } // namespace media |