// // Copyright 2018 Google LLC. All Rights Reserved. This file and proprietary // source code may only be used and distributed under the Widevine License // Agreement. // //#define LOG_NDEBUG 0 #define LOG_TAG "WVCryptoPluginTest" #include #include #include #include #include #include #include #include #include "gmock/gmock.h" #include "gtest/gtest.h" #include "wv_cdm_constants.h" #include "wv_cdm_types.h" #include "wv_content_decryption_module.h" #include "HidlTypes.h" #include "OEMCryptoCENC.h" #include "TypeConvert.h" #include "WVCryptoPlugin.h" namespace wvdrm { namespace hardware { namespace drm { namespace V1_4 { namespace widevine { using ::android::MemoryDealer; using ::testing::_; using ::testing::DefaultValue; using ::testing::DoAll; using ::testing::ElementsAreArray; using ::testing::Field; using ::testing::InSequence; using ::testing::Matcher; using ::testing::SetArgPointee; using ::testing::StrictMock; using ::testing::Test; using ::testing::Truly; using ::testing::Value; using ::testing::internal::ElementsAreArrayMatcher; using wvcdm::kCipherModeCtr; using wvcdm::kCipherModeCbc; using wvcdm::CdmCipherMode; using wvcdm::CdmDecryptionParametersV16; using wvcdm::CdmDecryptionSample; using wvcdm::CdmDecryptionSubsample; using wvcdm::CdmQueryMap; using wvcdm::CdmResponseType; using wvcdm::CdmSessionId; using wvcdm::KeyId; using wvcdm::KEY_ID_SIZE; using wvcdm::KEY_IV_SIZE; using wvcdm::QUERY_KEY_SECURITY_LEVEL; using wvcdm::QUERY_VALUE_SECURITY_LEVEL_L1; using wvcdm::QUERY_VALUE_SECURITY_LEVEL_L3; class MockCDM : public wvcdm::WvContentDecryptionModule { public: MOCK_METHOD(bool, IsOpenSession, (const CdmSessionId&), (override)); MOCK_METHOD(CdmResponseType, DecryptV16, (const CdmSessionId&, bool, const CdmDecryptionParametersV16&), (override)); MOCK_METHOD(CdmResponseType, QuerySessionStatus, (const CdmSessionId&, CdmQueryMap*), (override)); }; class WVCryptoPluginTest : public Test { protected: static const uint32_t kSessionIdSize = 16; uint8_t* pDest = nullptr; uint8_t* pSrc = nullptr; uint8_t sessionId[kSessionIdSize]; uint32_t nextBufferId = 0; std::map heapBases; virtual void SetUp() { FILE* fp = fopen("/dev/urandom", "r"); fread(sessionId, sizeof(uint8_t), kSessionIdSize, fp); fclose(fp); // Set default CdmResponseType value for gMock DefaultValue::Set(wvcdm::NO_ERROR); heapBases.clear(); } void setHeapBase(WVCryptoPlugin& plugin, const sp& heap) { ASSERT_NE(heap, nullptr); void* heapBase = heap->getBase(); ASSERT_NE(heapBase, nullptr); native_handle_t* nativeHandle = native_handle_create(1, 0); ASSERT_NE(nativeHandle, nullptr); nativeHandle->data[0] = heap->getHeapID(); auto hidlHandle = hidl_handle(nativeHandle); auto hidlMemory = hidl_memory("ashmem", hidlHandle, heap->getSize()); heapBases.insert( std::pair(heapBase, nextBufferId)); Return hResult = plugin.setSharedBufferBase(hidlMemory, nextBufferId++); ALOGE_IF(!hResult.isOk(), "setHeapBase failed setSharedBufferBase"); } void toSharedBuffer(WVCryptoPlugin& plugin, const sp& memory, SharedBuffer* buffer) { ssize_t offset; size_t size; ASSERT_NE(memory, nullptr); ASSERT_NE(buffer, nullptr); sp heap = memory->getMemory(&offset, &size); ASSERT_NE(heap, nullptr); setHeapBase(plugin, heap); buffer->bufferId = heapBases[heap->getBase()]; buffer->offset = offset >= 0 ? offset : 0; buffer->size = size; } }; TEST_F(WVCryptoPluginTest, CorrectlyReportsSecureBuffers) { android::sp> cdm = new StrictMock(); CdmQueryMap l1Map; l1Map[QUERY_KEY_SECURITY_LEVEL] = QUERY_VALUE_SECURITY_LEVEL_L1; CdmQueryMap l3Map; l3Map[QUERY_KEY_SECURITY_LEVEL] = QUERY_VALUE_SECURITY_LEVEL_L3; // Provide the expected behavior for IsOpenSession EXPECT_CALL(*cdm, IsOpenSession(_)) .WillRepeatedly(testing::Return(true)); // Specify the expected calls to QuerySessionStatus EXPECT_CALL(*cdm, QuerySessionStatus(_, _)) .WillOnce(DoAll(SetArgPointee<1>(l1Map), testing::Return(wvcdm::NO_ERROR))) .WillOnce(DoAll(SetArgPointee<1>(l3Map), testing::Return(wvcdm::NO_ERROR))); WVCryptoPlugin plugin(sessionId, kSessionIdSize, cdm.get()); EXPECT_TRUE(plugin.requiresSecureDecoderComponent("video/mp4")) << "WVCryptoPlugin incorrectly allows an insecure video decoder on L1"; EXPECT_FALSE(plugin.requiresSecureDecoderComponent("video/mp4")) << "WVCryptoPlugin incorrectly expects a secure video decoder on L3"; EXPECT_FALSE(plugin.requiresSecureDecoderComponent("audio/aac")) << "WVCryptoPlugin incorrectly expects a secure audio decoder"; } // TODO(b/28295739): // Add New MediaCrypto Unit Tests for CBC & Pattern Mode in cdmPatternDesc. // Predicate that validates that the fields of a passed-in // CdmDecryptionParametersV16 match the values it was given at construction // time. // // This could be done with a huge pile of gMock matchers, but it is ugly and // unmaintainable, particularly once you get into validating the subsamples. The // logic here is complex enough to warrant a custom matcher for this one test. class CDPMatcher { public: // TODO(b/35259313): Uncomment the removed parameters once the matcher can // convert them from HIDL accesses to physical addresses. CDPMatcher(const uint8_t* keyId, bool isSecure, Mode cipherMode, const Pattern& pattern, const uint8_t* /* input */, size_t inputLength, const uint8_t* /* output */, size_t outputLength, const uint8_t* iv, const SubSample* subsamples, size_t subsamplesLength) : mKeyId(keyId, keyId + KEY_ID_SIZE), mIsSecure(isSecure), mCipherMode(cipherMode), mPattern(pattern), /* mInput(input), */ mInputLength(inputLength), /* mOutput(output), */ mOutputLength(outputLength), mIv(iv, iv + KEY_IV_SIZE), mSubsamples(subsamples, subsamples + subsamplesLength) {} bool operator()(const CdmDecryptionParametersV16& params) const { if (mCipherMode == Mode::AES_CTR && params.cipher_mode != kCipherModeCtr) { return false; } else if (mCipherMode == Mode::AES_CBC && params.cipher_mode != kCipherModeCbc) { return false; } if (params.key_id != mKeyId || params.is_secure != mIsSecure || params.pattern.encrypt_blocks != mPattern.encryptBlocks || params.pattern.skip_blocks != mPattern.skipBlocks || params.samples.size() != 1) { return false; } const CdmDecryptionSample& sample = params.samples[0]; if (// TODO(b/35259313): Convert from a HIDL access to a physical address. // sample.encrypt_buffer != mInput || sample.encrypt_buffer_length != mInputLength || // TODO(b/35259313): Convert from a HIDL access to a physical address. // sample.decrypt_buffer != mOutput || sample.decrypt_buffer_size != mOutputLength || sample.decrypt_buffer_offset != 0 || sample.iv != mIv || sample.subsamples.size() != mSubsamples.size()) { return false; } for (size_t i = 0; i < mSubsamples.size(); ++i) { const SubSample& androidSubsample = mSubsamples[i]; const CdmDecryptionSubsample& cdmSubsample = sample.subsamples[i]; if (cdmSubsample.clear_bytes != androidSubsample.numBytesOfClearData || cdmSubsample.protected_bytes != androidSubsample.numBytesOfEncryptedData) { return false; } } return true; } private: const KeyId mKeyId; const bool mIsSecure; const Mode mCipherMode; const Pattern mPattern; // TODO(b/35259313): Uncomment this field once the matcher can convert this // from a HIDL access to a physical address. // const uint8_t* const mInput; const size_t mInputLength; // TODO(b/35259313): Uncomment this field once the matcher can convert this // from a HIDL access to a physical address. // const uint8_t* const mOutput; const size_t mOutputLength; const std::vector mIv; const std::vector mSubsamples; }; TEST_F(WVCryptoPluginTest, AttemptsToDecrypt) { android::sp> cdm = new StrictMock(); constexpr size_t kSubSampleCount = 6; SubSample subSamples[kSubSampleCount]; memset(subSamples, 0, sizeof(subSamples)); subSamples[0].numBytesOfEncryptedData = 16; subSamples[1].numBytesOfClearData = 16; subSamples[1].numBytesOfEncryptedData = 16; subSamples[2].numBytesOfEncryptedData = 8; subSamples[3].numBytesOfClearData = 29; subSamples[3].numBytesOfEncryptedData = 24; subSamples[4].numBytesOfEncryptedData = 60; subSamples[5].numBytesOfEncryptedData = 16; std::vector subSamplesVector( subSamples, subSamples + sizeof(subSamples) / sizeof(subSamples[0])); auto hSubSamples = hidl_vec(subSamplesVector); uint8_t keyId[KEY_ID_SIZE]; uint8_t iv[KEY_IV_SIZE]; static const size_t kDataSize = 185; uint8_t inputData[kDataSize]; FILE* fp = fopen("/dev/urandom", "r"); fread(keyId, sizeof(uint8_t), KEY_ID_SIZE, fp); fread(iv, sizeof(uint8_t), KEY_IV_SIZE, fp); fread(inputData, sizeof(uint8_t), kDataSize, fp); fclose(fp); sp memDealer = new MemoryDealer( kDataSize * 2, "WVCryptoPlugin_test"); sp source = memDealer->allocate(kDataSize); ASSERT_NE(source, nullptr); pSrc = static_cast( static_cast(source->unsecurePointer())); ASSERT_NE(pSrc, nullptr); memcpy(pSrc, inputData, source->size()); sp destination = memDealer->allocate(kDataSize); ASSERT_NE(destination, nullptr); pDest = static_cast( static_cast(destination->unsecurePointer())); ASSERT_NE(pDest, nullptr); Pattern noPattern = { 0, 0 }; // Provide the expected behavior for IsOpenSession EXPECT_CALL(*cdm, IsOpenSession(_)) .WillRepeatedly(testing::Return(true)); // Specify the expected calls to Decrypt CDPMatcher paramsMatcher(keyId, false, Mode::AES_CTR, noPattern, pSrc, kDataSize, pDest, kDataSize, iv, subSamples, kSubSampleCount); EXPECT_CALL(*cdm, DecryptV16(ElementsAreArray(sessionId, kSessionIdSize), true, Truly(paramsMatcher))) .Times(1); WVCryptoPlugin plugin(sessionId, kSessionIdSize, cdm.get()); uint32_t bytesWritten = 0; std::string errorDetailMessage; DestinationBuffer hDestination; hDestination.type = BufferType::SHARED_MEMORY; toSharedBuffer(plugin, destination, &hDestination.nonsecureMemory); SharedBuffer sourceBuffer; toSharedBuffer(plugin, source, &sourceBuffer); plugin.decrypt( false, hidl_array(keyId), hidl_array(iv), Mode::AES_CTR, noPattern, hSubSamples, sourceBuffer, 0, hDestination, [&](Status status, uint32_t hBytesWritten, hidl_string hDetailedError) { EXPECT_EQ(status, Status::OK); bytesWritten = hBytesWritten; errorDetailMessage.assign(hDetailedError.c_str()); }); EXPECT_EQ(kDataSize, bytesWritten) << "WVCryptoPlugin decrypted the wrong number of bytes"; EXPECT_EQ(0u, errorDetailMessage.size()) << "WVCryptoPlugin reported a detailed error message."; } TEST_F(WVCryptoPluginTest, RejectsCens) { android::sp> cdm = new StrictMock(); constexpr size_t kSubSampleCount = 2; SubSample subSamples[kSubSampleCount]; memset(subSamples, 0, sizeof(subSamples)); subSamples[0].numBytesOfEncryptedData = 16; subSamples[1].numBytesOfClearData = 16; subSamples[1].numBytesOfEncryptedData = 16; std::vector subSamplesVector( subSamples, subSamples + sizeof(subSamples) / sizeof(subSamples[0])); auto hSubSamples = hidl_vec(subSamplesVector); uint8_t keyId[KEY_ID_SIZE]; uint8_t iv[KEY_IV_SIZE]; static const size_t kDataSize = 48; uint8_t inputData[kDataSize]; FILE* fp = fopen("/dev/urandom", "r"); fread(keyId, sizeof(uint8_t), KEY_ID_SIZE, fp); fread(iv, sizeof(uint8_t), KEY_IV_SIZE, fp); fread(inputData, sizeof(uint8_t), kDataSize, fp); fclose(fp); sp memDealer = new MemoryDealer( kDataSize * 2, "WVCryptoPlugin_test"); sp source = memDealer->allocate(kDataSize); ASSERT_NE(source, nullptr); pSrc = static_cast( static_cast(source->unsecurePointer())); ASSERT_NE(pSrc, nullptr); memcpy(pSrc, inputData, source->size()); sp destination = memDealer->allocate(kDataSize); ASSERT_NE(destination, nullptr); pDest = static_cast( static_cast(destination->unsecurePointer())); ASSERT_NE(pDest, nullptr); Pattern recommendedPattern = { 1, 9 }; // Provide the expected behavior for IsOpenSession EXPECT_CALL(*cdm, IsOpenSession(_)) .WillRepeatedly(testing::Return(true)); // Refuse calls to Decrypt EXPECT_CALL(*cdm, DecryptV16(_, _, _)) .Times(0); WVCryptoPlugin plugin(sessionId, kSessionIdSize, cdm.get()); DestinationBuffer hDestination; hDestination.type = BufferType::SHARED_MEMORY; toSharedBuffer(plugin, destination, &hDestination.nonsecureMemory); SharedBuffer sourceBuffer; toSharedBuffer(plugin, source, &sourceBuffer); plugin.decrypt( false, hidl_array(keyId), hidl_array(iv), Mode::AES_CTR, recommendedPattern, hSubSamples, sourceBuffer, 0, hDestination, [&](Status status, uint32_t bytesWritten, hidl_string /* errorDetailMessage */) { EXPECT_EQ(status, Status::BAD_VALUE); EXPECT_EQ(bytesWritten, 0); }); } TEST_F(WVCryptoPluginTest, CommunicatesSecureBufferRequest) { android::sp> cdm = new StrictMock(); uint8_t keyId[KEY_ID_SIZE]; uint8_t iv[KEY_IV_SIZE]; static const size_t kDataSize = 32; uint8_t in[kDataSize]; FILE* fp = fopen("/dev/urandom", "r"); fread(keyId, sizeof(uint8_t), KEY_ID_SIZE, fp); fread(iv, sizeof(uint8_t), KEY_IV_SIZE, fp); fread(in, sizeof(uint8_t), kDataSize, fp); fclose(fp); SubSample subSample; subSample.numBytesOfClearData = 16; subSample.numBytesOfEncryptedData = 16; std::vector subSampleVector; subSampleVector.push_back(subSample); auto hSubSamples = hidl_vec(subSampleVector); // Provide the expected behavior for IsOpenSession EXPECT_CALL(*cdm, IsOpenSession(_)) .WillRepeatedly(testing::Return(true)); // Specify the expected calls to Decrypt { InSequence calls; typedef CdmDecryptionParametersV16 CDP; EXPECT_CALL(*cdm, DecryptV16(_, _, Field(&CDP::is_secure, false))) .Times(1); EXPECT_CALL(*cdm, DecryptV16(_, _, Field(&CDP::is_secure, true))) .Times(1); } sp memDealer = new MemoryDealer( kDataSize * 2, "WVCryptoPlugin_test"); sp source = memDealer->allocate(kDataSize); ASSERT_NE(source, nullptr); pSrc = static_cast( static_cast(source->unsecurePointer())); ASSERT_NE(pSrc, nullptr); memcpy(pSrc, in, source->size()); sp destination = memDealer->allocate(kDataSize); ASSERT_NE(destination, nullptr); pDest = static_cast( static_cast(destination->unsecurePointer())); ASSERT_NE(pDest, nullptr); WVCryptoPlugin plugin(sessionId, kSessionIdSize, cdm.get()); uint32_t bytesWritten = 0; std::string errorDetailMessage; DestinationBuffer hDestination; hDestination.type = BufferType::SHARED_MEMORY; toSharedBuffer(plugin, destination, &hDestination.nonsecureMemory); Pattern noPattern = { 0, 0 }; SharedBuffer sourceBuffer; toSharedBuffer(plugin, source, &sourceBuffer); plugin.decrypt( false, hidl_array(keyId), hidl_array(iv), Mode::AES_CTR, noPattern, hSubSamples, sourceBuffer, 0, hDestination, [&](Status status, uint32_t hBytesWritten, hidl_string hDetailedError) { EXPECT_EQ(status, Status::OK); bytesWritten = hBytesWritten; errorDetailMessage.assign(hDetailedError.c_str()); }); EXPECT_EQ(0u, errorDetailMessage.size()) << "WVCryptoPlugin reported a detailed error message."; plugin.decrypt( true, hidl_array(keyId), hidl_array(iv), Mode::AES_CTR, noPattern, hSubSamples, sourceBuffer, 0, hDestination, [&](Status status, uint32_t hBytesWritten, hidl_string hDetailedError) { EXPECT_EQ(status, Status::OK); bytesWritten = hBytesWritten; errorDetailMessage.assign(hDetailedError.c_str()); }); EXPECT_EQ(0u, errorDetailMessage.size()) << "WVCryptoPlugin reported a detailed error message."; } TEST_F(WVCryptoPluginTest, AllowsSessionIdChanges) { android::sp> cdm = new StrictMock(); uint8_t keyId[KEY_ID_SIZE]; uint8_t iv[KEY_IV_SIZE]; uint8_t sessionId2[kSessionIdSize]; static const size_t kDataSize = 32; uint8_t in[kDataSize]; FILE* fp = fopen("/dev/urandom", "r"); fread(keyId, sizeof(uint8_t), KEY_ID_SIZE, fp); fread(iv, sizeof(uint8_t), KEY_IV_SIZE, fp); fread(sessionId2, sizeof(uint8_t), kSessionIdSize, fp); fread(in, sizeof(uint8_t), kDataSize, fp); fclose(fp); SubSample subSample; subSample.numBytesOfClearData = 16; subSample.numBytesOfEncryptedData = 16; std::vector subSampleVector; subSampleVector.push_back(subSample); auto hSubSamples = hidl_vec(subSampleVector); std::vector sessionIdVector(sessionId, sessionId + kSessionIdSize); std::vector sessionId2Vector(sessionId2, sessionId2 + kSessionIdSize); // Provide the expected behavior for IsOpenSession EXPECT_CALL(*cdm, IsOpenSession(_)) .WillRepeatedly(testing::Return(true)); // Specify the expected calls to Decrypt { InSequence calls; EXPECT_CALL(*cdm, DecryptV16(ElementsAreArray(sessionId, kSessionIdSize), _, _)) .Times(1); EXPECT_CALL(*cdm, DecryptV16(ElementsAreArray(sessionId2, kSessionIdSize), _, _)) .Times(1); } sp memDealer = new MemoryDealer( kDataSize * 2, "WVCryptoPlugin_test"); sp source = memDealer->allocate(kDataSize); ASSERT_NE(source, nullptr); pSrc = static_cast( static_cast(source->unsecurePointer())); ASSERT_NE(pSrc, nullptr); memcpy(pSrc, in, source->size()); sp destination = memDealer->allocate(kDataSize); ASSERT_NE(destination, nullptr); pDest = static_cast( static_cast(destination->unsecurePointer())); ASSERT_NE(pDest, nullptr); uint8_t blank[1]; // Some compilers will not accept 0. WVCryptoPlugin plugin(blank, 0, cdm.get()); uint32_t bytesWritten = 0; std::string errorDetailMessage; DestinationBuffer hDestination; hDestination.type = BufferType::SHARED_MEMORY; toSharedBuffer(plugin, destination, &hDestination.nonsecureMemory); Pattern noPattern = { 0, 0 }; SharedBuffer sourceBuffer; toSharedBuffer(plugin, source, &sourceBuffer); Status status = plugin.setMediaDrmSession(sessionIdVector); EXPECT_EQ(status, Status::OK); plugin.decrypt( false, hidl_array(keyId), hidl_array(iv), Mode::AES_CTR, noPattern, hSubSamples, sourceBuffer, 0, hDestination, [&](Status status, uint32_t hBytesWritten, hidl_string hDetailedError) { EXPECT_EQ(status, Status::OK); bytesWritten = hBytesWritten; errorDetailMessage.assign(hDetailedError.c_str()); }); EXPECT_EQ(0u, errorDetailMessage.size()) << "WVCryptoPlugin reported a detailed error message."; status = plugin.setMediaDrmSession(sessionId2Vector); EXPECT_EQ(status, Status::OK); plugin.decrypt( false, hidl_array(keyId), hidl_array(iv), Mode::AES_CTR, noPattern, hSubSamples, sourceBuffer, 0, hDestination, [&](Status status, uint32_t hBytesWritten, hidl_string hDetailedError) { EXPECT_EQ(status, Status::OK); bytesWritten = hBytesWritten; errorDetailMessage.assign(hDetailedError.c_str()); }); EXPECT_EQ(0u, errorDetailMessage.size()) << "WVCryptoPlugin reported a detailed error message."; } TEST_F(WVCryptoPluginTest, DisallowsUnopenedSessionIdChanges) { android::sp> cdm = new StrictMock(); uint8_t blank[1]; // Some compilers will not accept 0. std::vector sessionIdVector(sessionId, sessionId + kSessionIdSize); // Specify the expected calls to IsOpenSession { InSequence calls; EXPECT_CALL(*cdm, IsOpenSession(ElementsAreArray(blank, 0))) .WillOnce(testing::Return(false)); EXPECT_CALL(*cdm, IsOpenSession(ElementsAreArray(sessionId, kSessionIdSize))) .WillOnce(testing::Return(false)) .WillOnce(testing::Return(true)); } WVCryptoPlugin plugin(blank, 0, cdm.get()); Status status = plugin.setMediaDrmSession(sessionIdVector); EXPECT_EQ(status, Status::ERROR_DRM_SESSION_NOT_OPENED); status = plugin.setMediaDrmSession(sessionIdVector); EXPECT_EQ(status, Status::OK); } } // namespace widevine } // namespace V1_4 } // namespace drm } // namespace hardware } // namespace wvdrm