// Copyright 2023 Google LLC. All Rights Reserved. This file and proprietary // source code may only be used and distributed under the Widevine // License Agreement. // // Test data for OEMCrypto unit tests. // #ifndef CDM_OEMCRYPTO_DECRYPT_TEST_ #define CDM_OEMCRYPTO_DECRYPT_TEST_ #include #include "OEMCryptoCENC.h" #include "log.h" #include "oec_decrypt_fallback_chain.h" #include "oemcrypto_basic_test.h" #include "oemcrypto_license_test.h" namespace wvoec { // Used to test the different HDCP versions. This test is parameterized by the // required HDCP version in the key control block. class OEMCryptoSessionTestDecryptWithHDCP : public OEMCryptoSessionTests, public WithParamInterface { protected: void DecryptWithHDCP(OEMCrypto_HDCP_Capability version) { OEMCryptoResult sts; OEMCrypto_HDCP_Capability current, maximum; sts = OEMCrypto_GetHDCPCapability(¤t, &maximum); ASSERT_EQ(OEMCrypto_SUCCESS, sts); Session s; ASSERT_NO_FATAL_FAILURE(s.open()); ASSERT_NO_FATAL_FAILURE(InstallTestDrmKey(&s)); LicenseRoundTrip license_messages(&s); license_messages.set_control((version << wvoec::kControlHDCPVersionShift) | wvoec::kControlObserveHDCP | wvoec::kControlHDCPRequired); ASSERT_NO_FATAL_FAILURE(license_messages.SignAndVerifyRequest()); ASSERT_NO_FATAL_FAILURE(license_messages.CreateDefaultResponse()); ASSERT_NO_FATAL_FAILURE(license_messages.EncryptAndSignResponse()); ASSERT_EQ(OEMCrypto_SUCCESS, license_messages.LoadResponse()); if (((version <= HDCP_V2_3 || current >= HDCP_V1_0) && version > current) || (current == HDCP_V1 && version >= HDCP_V1_0)) { if (global_features.api_version >= 16) { // Can provide either OEMCrypto_WARNING_MIXED_OUTPUT_PROTECTION or // OEMCrypto_ERROR_INSUFFICIENT_HDCP. TestDecryptCTR allows either to be // reported if OEMCrypto_WARNING_MIXED_OUTPUT_PROTECTION is expected. ASSERT_NO_FATAL_FAILURE( s.TestDecryptCTR(true, OEMCrypto_WARNING_MIXED_OUTPUT_PROTECTION)) << "Failed when current HDCP = " << HDCPCapabilityAsString(current) << ", maximum HDCP = " << HDCPCapabilityAsString(maximum) << ", license HDCP = " << HDCPCapabilityAsString(version); } else { ASSERT_NO_FATAL_FAILURE( s.TestDecryptCTR(true, OEMCrypto_ERROR_INSUFFICIENT_HDCP)) << "Failed when current HDCP = " << HDCPCapabilityAsString(current) << ", maximum HDCP = " << HDCPCapabilityAsString(maximum) << ", license HDCP = " << HDCPCapabilityAsString(version); } } else { ASSERT_NO_FATAL_FAILURE(s.TestDecryptCTR(true, OEMCrypto_SUCCESS)) << "Failed when current HDCP = " << HDCPCapabilityAsString(current) << ", maximum HDCP = " << HDCPCapabilityAsString(maximum) << ", license HDCP = " << HDCPCapabilityAsString(version); } } }; struct SubsampleSize { size_t clear_size; size_t encrypted_size; SubsampleSize(size_t clear, size_t encrypted) : clear_size(clear), encrypted_size(encrypted) {} }; // Struct for holding the data for one test sample in the decrypt tests. struct TestSample { // Encrypted data -- this is input to OEMCrypto, and output from EncryptData. std::vector encrypted_buffer; std::vector clear_buffer; // OEMCrypto store clear output here. std::vector truth_buffer; // Truth data for clear text. OEMCrypto_SampleDescription description; std::vector subsamples; int secure_buffer_fid; }; // A class of tests that test decryption for a variety of patterns and modes. // This test is parameterized by three parameters: // 1. The pattern used for pattern decryption. // 2. The cipher mode for decryption: either CTR or CBC. // 3. A boolean that determines if decrypt in place should be done. When the // output buffer is clear, it should be possible for the input and output // buffers to be the same. class OEMCryptoSessionTestsDecryptTests : public OEMCryptoLicenseTestAPI16, public WithParamInterface> { protected: void SetUp() override { OEMCryptoLicenseTestAPI16::SetUp(); if (wvoec::global_features.derive_key_method == wvoec::DeviceFeatures::NO_METHOD) { GTEST_SKIP() << "Test for devices that can derive session keys only."; } pattern_ = ::testing::get<0>(GetParam()); cipher_mode_ = ::testing::get<1>(GetParam()); decrypt_inplace_ = ::testing::get<2>(GetParam()).decrypt_inplace; output_buffer_type_ = ::testing::get<2>(GetParam()).type; verify_crc_ = global_features.supports_crc; // Pick a random key. EXPECT_EQ(GetRandBytes(key_, sizeof(key_)), 1); // Pick a random starting iv. Some tests override this before using it. EXPECT_EQ(GetRandBytes(initial_iv_, sizeof(initial_iv_)), 1); } void TearDown() override { FreeSecureBuffers(); OEMCryptoLicenseTestAPI16::TearDown(); } void SetSubsampleSizes(std::vector subsample_sizes) { // This is just sugar for having one sample with the given subsamples in it. SetSampleSizes({std::move(subsample_sizes)}); } void SetSampleSizes(std::vector> sample_sizes) { ASSERT_GT(sample_sizes.size(), 0u); samples_.clear(); samples_.reserve(sample_sizes.size()); // Convert all the size arrays to TestSample structs for (const std::vector& subsample_sizes : sample_sizes) { // This could be one line if we had C++17 samples_.emplace_back(); TestSample& sample = samples_.back(); ASSERT_GT(subsample_sizes.size(), 0u); sample.subsamples.reserve(subsample_sizes.size()); // Convert all the sizes to subsample descriptions and tally the total // sample size size_t sample_size = 0; size_t current_block_offset = 0; for (const SubsampleSize& size : subsample_sizes) { sample.subsamples.push_back(OEMCrypto_SubSampleDescription{ size.clear_size, size.encrypted_size, 0, // Subsample Flags, to be filled in after the loop current_block_offset}); // Update the rolling variables sample_size += size.clear_size + size.encrypted_size; if (cipher_mode_ == OEMCrypto_CipherMode_CENC) { current_block_offset = (current_block_offset + size.encrypted_size) % AES_BLOCK_SIZE; } } // Set the subsample flags now that all the subsamples are processed sample.subsamples.front().subsample_flags |= OEMCrypto_FirstSubsample; sample.subsamples.back().subsample_flags |= OEMCrypto_LastSubsample; // Set related information on the sample description sample.description.subsamples = sample.subsamples.data(); sample.description.subsamples_length = sample.subsamples.size(); sample.description.buffers.input_data_length = sample_size; } } // Set up the input buffer and either a clear or secure output buffer for each // test sample. This should be called after SetSubsampleSizes(). void MakeBuffers() { for (TestSample& sample : samples_) { const size_t total_size = sample.description.buffers.input_data_length; ASSERT_GT(total_size, 0u); sample.encrypted_buffer.clear(); sample.truth_buffer.clear(); sample.clear_buffer.clear(); sample.encrypted_buffer.resize(total_size); sample.truth_buffer.resize(total_size); for (size_t i = 0; i < total_size; i++) sample.truth_buffer[i] = i % 256; OEMCrypto_DestBufferDesc& output_descriptor = sample.description.buffers.output_descriptor; output_descriptor.type = output_buffer_type_; switch (output_descriptor.type) { case OEMCrypto_BufferType_Clear: if (decrypt_inplace_) { // Add some padding to verify there is no overrun. sample.encrypted_buffer.resize(total_size + kBufferOverrunPadding, 0xaa); output_descriptor.buffer.clear.clear_buffer = sample.encrypted_buffer.data(); } else { // Add some padding to verify there is no overrun. sample.clear_buffer.resize(total_size + kBufferOverrunPadding, 0xaa); output_descriptor.buffer.clear.clear_buffer = sample.clear_buffer.data(); } output_descriptor.buffer.clear.clear_buffer_length = total_size; break; case OEMCrypto_BufferType_Secure: output_descriptor.buffer.secure.secure_buffer_length = total_size; ASSERT_EQ(OEMCrypto_AllocateSecureBuffer( session_.session_id(), total_size, &output_descriptor, &sample.secure_buffer_fid), OEMCrypto_SUCCESS); ASSERT_NE(output_descriptor.buffer.secure.secure_buffer, nullptr); // It is OK if OEMCrypto changes the maximum size, but there must // still be enough room for our data. ASSERT_GE(output_descriptor.buffer.secure.secure_buffer_length, total_size); output_descriptor.buffer.secure.offset = 0; break; case OEMCrypto_BufferType_Direct: output_descriptor.buffer.direct.is_video = false; break; } // switch (output_descriptor.type) } // sample loop } void FreeSecureBuffers() { for (TestSample& sample : samples_) { OEMCrypto_DestBufferDesc& output_descriptor = sample.description.buffers.output_descriptor; if (output_descriptor.type == OEMCrypto_BufferType_Secure) { ASSERT_EQ(OEMCrypto_FreeSecureBuffer(session_.session_id(), &output_descriptor, sample.secure_buffer_fid), OEMCrypto_SUCCESS); } } } void EncryptData() { AES_KEY aes_key; AES_set_encrypt_key(key_, AES_BLOCK_SIZE * 8, &aes_key); for (TestSample& sample : samples_) { uint8_t iv[KEY_IV_SIZE]; // Current IV memcpy(iv, initial_iv_, KEY_IV_SIZE); memcpy(sample.description.iv, initial_iv_, KEY_IV_SIZE); size_t buffer_index = 0; // byte index into in and out. size_t block_offset = 0; // byte index into current block. for (const OEMCrypto_SubSampleDescription& subsample : sample.subsamples) { // Copy clear content. if (subsample.num_bytes_clear > 0) { memcpy(&sample.encrypted_buffer[buffer_index], &sample.truth_buffer[buffer_index], subsample.num_bytes_clear); buffer_index += subsample.num_bytes_clear; } // The IV resets at the start of each subsample in the 'cbcs' schema. if (cipher_mode_ == OEMCrypto_CipherMode_CBCS) { memcpy(iv, initial_iv_, KEY_IV_SIZE); } size_t pattern_offset = 0; const size_t subsample_end = buffer_index + subsample.num_bytes_encrypted; while (buffer_index < subsample_end) { const size_t size = min(subsample_end - buffer_index, AES_BLOCK_SIZE - block_offset); const size_t pattern_length = pattern_.encrypt + pattern_.skip; const bool skip_block = (pattern_offset >= pattern_.encrypt) && (pattern_length > 0); if (pattern_length > 0) { pattern_offset = (pattern_offset + 1) % pattern_length; } // CBC mode should just copy a partial block at the end. If there // is a partial block at the beginning, an error is returned, so we // can put whatever we want in the output buffer. if (skip_block || ((cipher_mode_ == OEMCrypto_CipherMode_CBCS) && (size < AES_BLOCK_SIZE))) { memcpy(&sample.encrypted_buffer[buffer_index], &sample.truth_buffer[buffer_index], size); block_offset = 0; // Next block should be complete. } else { if (cipher_mode_ == OEMCrypto_CipherMode_CENC) { uint8_t aes_output[AES_BLOCK_SIZE]; AES_encrypt(iv, aes_output, &aes_key); for (size_t n = 0; n < size; n++) { sample.encrypted_buffer[buffer_index + n] = aes_output[n + block_offset] ^ sample.truth_buffer[buffer_index + n]; } if (size + block_offset < AES_BLOCK_SIZE) { // Partial block. Don't increment iv. Compute next block // offset. block_offset = block_offset + size; } else { EXPECT_EQ(block_offset + size, static_cast(AES_BLOCK_SIZE)); // Full block. Increment iv, and set offset to 0 for next // block. ctr128_inc64(1, iv); block_offset = 0; } } else { uint8_t aes_input[AES_BLOCK_SIZE]; for (size_t n = 0; n < size; n++) { aes_input[n] = sample.truth_buffer[buffer_index + n] ^ iv[n]; } AES_encrypt(aes_input, &sample.encrypted_buffer[buffer_index], &aes_key); memcpy(iv, &sample.encrypted_buffer[buffer_index], AES_BLOCK_SIZE); // CBC mode should always start on block boundary. block_offset = 0; } } buffer_index += size; } // encryption loop } // per-subsample loop } // per-sample loop } void LoadLicense() { uint32_t control = wvoec::kControlNonceEnabled; if (verify_crc_) control |= kControlAllowHashVerification; if (output_buffer_type_ == OEMCrypto_BufferType_Secure) control |= kControlObserveDataPath | kControlDataPathSecure; license_messages_.set_control(control); ASSERT_NO_FATAL_FAILURE(license_messages_.SignAndVerifyRequest()); ASSERT_NO_FATAL_FAILURE(license_messages_.CreateDefaultResponse()); license_messages_.core_response() .timer_limits.initial_renewal_duration_seconds = kDuration; memcpy(license_messages_.response_data().keys[0].key_data, key_, sizeof(key_)); license_messages_.response_data().keys[0].cipher_mode = cipher_mode_; ASSERT_NO_FATAL_FAILURE(license_messages_.EncryptAndSignResponse()); ASSERT_EQ(OEMCrypto_SUCCESS, license_messages_.LoadResponse()); ASSERT_EQ(GetKeyHandleIntoVector(session_.session_id(), session_.license().keys[0].key_id, session_.license().keys[0].key_id_length, cipher_mode_, key_handle_), OEMCrypto_SUCCESS); } void TestDecryptCENC() { ASSERT_EQ(DecryptCENC(), OEMCrypto_SUCCESS); } void ValidateDecryptedData() { for (TestSample& sample : samples_) { if (sample.description.buffers.output_descriptor.type == OEMCrypto_BufferType_Clear) { const size_t total_size = sample.description.buffers.input_data_length; // To verify there is no buffer overrun after decrypting, look at the // padded bytes just after the data buffer that was written. It // should not have changed from the original 0xaa that we set in // MakeBuffer function. if (decrypt_inplace_) { EXPECT_EQ(std::count(sample.encrypted_buffer.begin() + total_size, sample.encrypted_buffer.end(), 0xaa), static_cast(kBufferOverrunPadding)) << "Buffer overrun."; sample.encrypted_buffer.resize(total_size); // Remove padding. // We expect encrypted buffer to have been changed by OEMCrypto. EXPECT_EQ(sample.encrypted_buffer, sample.truth_buffer); } else { EXPECT_EQ(std::count(sample.clear_buffer.begin() + total_size, sample.clear_buffer.end(), 0xaa), static_cast(kBufferOverrunPadding)) << "Buffer overrun."; sample.clear_buffer.resize(total_size); // Remove padding. EXPECT_EQ(sample.clear_buffer, sample.truth_buffer); } } } if (verify_crc_) { uint32_t frame; ASSERT_EQ(OEMCrypto_GetHashErrorCode(session_.session_id(), &frame), OEMCrypto_SUCCESS); } } OEMCryptoResult DecryptCENC() { // OEMCrypto only supports providing a decrypt hash for one sample. if (samples_.size() > 1) verify_crc_ = false; // If supported, check the decrypt hashes. if (verify_crc_) { const TestSample& sample = samples_[0]; uint32_t hash = util::wvcrc32(sample.truth_buffer.data(), sample.truth_buffer.size()); OEMCrypto_SetDecryptHash(session_.session_id(), 1, reinterpret_cast(&hash), sizeof(hash)); } // Build an array of just the sample descriptions. std::vector sample_descriptions; sample_descriptions.reserve(samples_.size()); for (TestSample& sample : samples_) { // This must be deferred until this point in case the test modifies the // buffer before testing decrypt. sample.description.buffers.input_data = sample.encrypted_buffer.data(); // Append to the description array. sample_descriptions.push_back(sample.description); } // Perform decryption using the test data that was previously set up. OEMCryptoResult result = DecryptFallbackChain::Decrypt( key_handle_.data(), key_handle_.size(), sample_descriptions.data(), sample_descriptions.size(), cipher_mode_, &pattern_); if (result != OEMCrypto_SUCCESS) return result; ValidateDecryptedData(); return result; } // Parameters of test case OEMCrypto_CENCEncryptPatternDesc pattern_; OEMCryptoCipherMode cipher_mode_; bool decrypt_inplace_; // If true, input and output buffers are the same. OEMCryptoBufferType output_buffer_type_; bool verify_crc_; uint8_t key_[AES_BLOCK_SIZE]; // Encryption Key. uint8_t initial_iv_[KEY_IV_SIZE]; // Starting IV for every sample. std::vector samples_; std::vector key_handle_; }; } // namespace wvoec #endif // CDM_OEMCRYPTO_DECRYPT_TEST_