// // Copyright 2022 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 #include "OEMCryptoCENC.h" #include "WVCryptoPlugin.h" #include "wv_cdm_constants.h" #include "wv_cdm_types.h" #include "wv_content_decryption_module.h" #include "gmock/gmock.h" #include "gtest/gtest.h" namespace wvdrm { namespace hardware { namespace drm { namespace widevine { using ::aidl::android::hardware::common::Ashmem; using ::aidl::android::hardware::drm::DecryptArgs; using ::aidl::android::hardware::drm::DestinationBuffer; using ::aidl::android::hardware::drm::Mode; using ::aidl::android::hardware::drm::Pattern; using ::aidl::android::hardware::drm::SharedBuffer; using ::aidl::android::hardware::drm::Status; using ::aidl::android::hardware::drm::SubSample; using ::android::sp; using ::testing::_; using ::testing::DefaultValue; using ::testing::DoAll; using ::testing::ElementsAreArray; using ::testing::Field; using ::testing::InSequence; using ::testing::Matcher; using ::testing::NiceMock; using ::testing::SetArgPointee; using ::testing::Test; using ::testing::Truly; using ::testing::Value; using ::testing::internal::ElementsAreArrayMatcher; using wvcdm::CdmCipherMode; using wvcdm::CdmDecryptionParametersV16; using wvcdm::CdmDecryptionSample; using wvcdm::CdmDecryptionSubsample; using wvcdm::CdmQueryMap; using wvcdm::CdmResponseType; using wvcdm::CdmSessionId; using wvcdm::kCipherModeCbc; using wvcdm::kCipherModeCtr; using wvcdm::KEY_ID_SIZE; using wvcdm::KEY_IV_SIZE; using wvcdm::KeyId; 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 constexpr uint32_t kSessionIdSize = 16; uint8_t *pDest = nullptr; uint8_t *pSrc = nullptr; uint8_t blank[1] = {}; // Some compilers will not accept 0. uint8_t sessionId[kSessionIdSize]; uint32_t nextBufferId = 0; std::map heapBases; android::sp> mCdm = nullptr; std::shared_ptr mPlugin = nullptr; std::shared_ptr mPluginNoSid = nullptr; public: void SetUp() override { FILE *fp = fopen("/dev/urandom", "r"); ASSERT_NE(fp, nullptr) << "Failed to open /dev/urandom"; fread(sessionId, sizeof(uint8_t), kSessionIdSize, fp); fclose(fp); // Set default CdmResponseType value for gMock DefaultValue::Set(CdmResponseType(wvcdm::NO_ERROR)); heapBases.clear(); mCdm = new NiceMock(); ASSERT_TRUE(mCdm) << "Failed to create mocked CDM"; mPlugin = ::ndk::SharedRefBase::make( sessionId, kSessionIdSize, mCdm.get()); ASSERT_TRUE(mPlugin) << "Failed to create crypto plugin"; mPluginNoSid = ::ndk::SharedRefBase::make(blank, 0, mCdm.get()); ASSERT_TRUE(mPluginNoSid) << "Failed to create crypto plugin w/o sid"; } void TearDown() override { if (mCdm) mCdm.clear(); if (mPlugin) mPlugin.reset(); if (mPluginNoSid) mPluginNoSid.reset(); } void getDecryptMemory(std::shared_ptr plugin, size_t size, size_t index, SharedBuffer &out) { out.bufferId = static_cast(index); out.offset = 0; out.size = static_cast(size); int fd = ashmem_create_region("mediacryptoTestSharedMemory", size); EXPECT_GE(fd, 0); EXPECT_EQ(size, ashmem_get_size_region(fd)); auto handle = native_handle_create(1, 0); handle->data[0] = fd; out.handle = ::android::makeToAidl(handle); auto ret = plugin->setSharedBufferBase(out); EXPECT_TRUE(ret.isOk()) << "WVCryptoPlugin failed to setSharedBufferBase"; native_handle_delete(handle); } uint8_t *fillRandom(const ::aidl::android::hardware::drm::SharedBuffer &buf) { std::random_device rd; std::mt19937 rand(rd()); auto fd = buf.handle.fds[0].get(); size_t size = buf.size; uint8_t *base = static_cast( mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)); EXPECT_NE(MAP_FAILED, base); auto p = reinterpret_cast(base); for (size_t i = 0; i < size / sizeof(uint32_t); i++) { p[i] = rand(); } return base; } }; TEST_F(WVCryptoPluginTest, CorrectlyReportsSecureBuffers) { 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(*mCdm, IsOpenSession(_)).WillRepeatedly(testing::Return(true)); // Specify the expected calls to QuerySessionStatus EXPECT_CALL(*mCdm, QuerySessionStatus(_, _)) .WillOnce(DoAll(SetArgPointee<1>(l1Map), testing::Return(CdmResponseType(wvcdm::NO_ERROR)))) .WillOnce(DoAll(SetArgPointee<1>(l3Map), testing::Return(CdmResponseType(wvcdm::NO_ERROR)))); bool isSecure = false; auto ret = mPlugin->requiresSecureDecoderComponent("video/mp4", &isSecure); EXPECT_TRUE(isSecure) << "WVCryptoPlugin incorrectly allows an insecure video decoder on L1"; ret = mPlugin->requiresSecureDecoderComponent("video/mp4", &isSecure); EXPECT_FALSE(isSecure) << "WVCryptoPlugin incorrectly expects a secure video decoder on L3"; ret = mPlugin->requiresSecureDecoderComponent("audio/aac", &isSecure); EXPECT_FALSE(isSecure) << "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 AIDL 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 ¶ms) 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 (sample.encrypt_buffer_length != mInputLength || 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; const size_t mInputLength; const size_t mOutputLength; const std::vector mIv; const std::vector mSubsamples; }; TEST_F(WVCryptoPluginTest, AttemptsToDecrypt) { 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 subSamplesVec( subSamples, subSamples + sizeof(subSamples) / sizeof(subSamples[0])); int64_t totalSize = 0; for (size_t i = 0; i < subSamplesVec.size(); ++i) { totalSize += subSamples[i].numBytesOfClearData; totalSize += subSamples[i].numBytesOfEncryptedData; } uint8_t keyId[KEY_ID_SIZE]; uint8_t iv[KEY_IV_SIZE]; uint8_t sessionId[kSessionIdSize]; static const size_t kDataSize = 185; FILE *fp = fopen("/dev/urandom", "r"); ASSERT_NE(fp, nullptr) << "Failed to open /dev/urandom"; fread(keyId, sizeof(uint8_t), KEY_ID_SIZE, fp); fread(iv, sizeof(uint8_t), KEY_IV_SIZE, fp); fclose(fp); SharedBuffer sourceBuffer; getDecryptMemory(mPlugin, kDataSize, 0, sourceBuffer); auto sourceBase = fillRandom(sourceBuffer); SharedBuffer destBuffer; getDecryptMemory(mPlugin, kDataSize, 0, destBuffer); auto destBase = fillRandom(destBuffer); SharedBuffer sourceRange; sourceRange.bufferId = 0; sourceRange.offset = 0; sourceRange.size = totalSize; SharedBuffer destRange; destRange.bufferId = 0; destRange.offset = 0; destRange.size = totalSize; Pattern noPattern = {0, 0}; // Provide the expected behavior for IsOpenSession EXPECT_CALL(*mCdm, IsOpenSession(_)).WillRepeatedly(testing::Return(true)); // TODO, initialize pSrc and pDest // Specify the expected calls to Decrypt CDPMatcher paramsMatcher(keyId, false, Mode::AES_CTR, noPattern, pSrc, kDataSize, pDest, kDataSize, iv, subSamples, kSubSampleCount); EXPECT_CALL(*mCdm, DecryptV16(ElementsAreArray(sessionId, kSessionIdSize), true, Truly(paramsMatcher))) .Times(1); DecryptArgs args; args.secure = false; args.keyId = std::vector(keyId, keyId + sizeof(keyId) / sizeof(keyId[0])); args.iv = std::vector(iv, iv + sizeof(iv) / sizeof(iv[0])); args.mode = Mode::AES_CTR; args.pattern = noPattern; args.subSamples = subSamplesVec; args.source = std::move(sourceRange); args.offset = 0; args.destination = std::move(destRange); std::vector sessionIdVector(sessionId, sessionId + kSessionIdSize); auto ret = mPlugin->setMediaDrmSession(sessionIdVector); EXPECT_TRUE(ret.isOk()); int32_t bytesWritten = 0; ret = mPlugin->decrypt(args, &bytesWritten); EXPECT_TRUE(ret.isOk()) << ret.getMessage(); std::string errorMessage(ret.getMessage()); EXPECT_EQ(kDataSize, bytesWritten) << "WVCryptoPlugin decrypted the wrong number of bytes"; EXPECT_EQ(0u, errorMessage.size()) << "WVCryptoPlugin reported a detailed error message."; munmap(sourceBase, totalSize); munmap(destBase, totalSize); } TEST_F(WVCryptoPluginTest, RejectsCens) { 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 subSamplesVec( subSamples, subSamples + sizeof(subSamples) / sizeof(subSamples[0])); int64_t totalSize = 0; for (size_t i = 0; i < subSamplesVec.size(); ++i) { totalSize += subSamples[i].numBytesOfClearData; totalSize += subSamples[i].numBytesOfEncryptedData; } uint8_t keyId[KEY_ID_SIZE]; uint8_t iv[KEY_IV_SIZE]; static const size_t kDataSize = 48; FILE *fp = fopen("/dev/urandom", "r"); ASSERT_NE(fp, nullptr) << "Failed to open /dev/urandom"; fread(keyId, sizeof(uint8_t), KEY_ID_SIZE, fp); fread(iv, sizeof(uint8_t), KEY_IV_SIZE, fp); fread(sessionId, sizeof(uint8_t), kSessionIdSize, fp); fclose(fp); // Provide the expected behavior for IsOpenSession EXPECT_CALL(*mCdm, IsOpenSession(_)).WillRepeatedly(testing::Return(true)); // Refuse calls to Decrypt EXPECT_CALL(*mCdm, DecryptV16(_, _, _)).Times(0); SharedBuffer sourceBuffer; getDecryptMemory(mPlugin, kDataSize, 0, sourceBuffer); auto sourceBase = fillRandom(sourceBuffer); SharedBuffer destBuffer; getDecryptMemory(mPlugin, kDataSize, 0, destBuffer); auto destBase = fillRandom(destBuffer); SharedBuffer sourceRange; sourceRange.bufferId = 0; sourceRange.offset = 0; sourceRange.size = totalSize; SharedBuffer destRange; destRange.bufferId = 0; destRange.offset = 0; destRange.size = totalSize; Pattern recommendedPattern = {1, 9}; std::vector keyIdVec(keyId, keyId + KEY_ID_SIZE); std::vector ivVec(iv, iv + KEY_IV_SIZE); DecryptArgs args; args.secure = false; args.keyId = keyIdVec; args.iv = ivVec; args.mode = Mode::AES_CTR; args.pattern = recommendedPattern; args.subSamples = subSamplesVec; args.source = std::move(sourceRange); args.offset = 0; args.destination = std::move(destRange); int32_t bytesWritten = -1; auto ret = mPlugin->decrypt(args, &bytesWritten); EXPECT_EQ(static_cast(Status::BAD_VALUE), ret.getServiceSpecificError()); EXPECT_EQ(bytesWritten, 0); munmap(sourceBase, totalSize); munmap(destBase, totalSize); } TEST_F(WVCryptoPluginTest, CommunicatesSecureBufferRequest) { uint8_t keyId[KEY_ID_SIZE]; uint8_t iv[KEY_IV_SIZE]; static const size_t kDataSize = 32; FILE *fp = fopen("/dev/urandom", "r"); ASSERT_NE(fp, nullptr) << "Failed to open /dev/urandom"; fread(keyId, sizeof(uint8_t), KEY_ID_SIZE, fp); fread(iv, sizeof(uint8_t), KEY_IV_SIZE, fp); fclose(fp); SubSample subSample; subSample.numBytesOfClearData = 16; subSample.numBytesOfEncryptedData = 16; std::vector subSamples; subSamples.push_back(subSample); int64_t totalSize = 0; for (size_t i = 0; i < subSamples.size(); ++i) { totalSize += subSamples[i].numBytesOfClearData; totalSize += subSamples[i].numBytesOfEncryptedData; } // Provide the expected behavior for IsOpenSession EXPECT_CALL(*mCdm, IsOpenSession(_)).WillRepeatedly(testing::Return(true)); // Specify the expected calls to Decrypt { InSequence calls; typedef CdmDecryptionParametersV16 CDP; EXPECT_CALL(*mCdm, DecryptV16(_, _, Field(&CDP::is_secure, false))) .Times(1); EXPECT_CALL(*mCdm, DecryptV16(_, _, Field(&CDP::is_secure, true))).Times(1); } SharedBuffer sourceBuffer; getDecryptMemory(mPlugin, kDataSize, 0, sourceBuffer); auto sourceBase = fillRandom(sourceBuffer); SharedBuffer destBuffer; getDecryptMemory(mPlugin, kDataSize, 0, destBuffer); auto destBase = fillRandom(destBuffer); SharedBuffer sourceRange; sourceRange.bufferId = 0; sourceRange.offset = 0; sourceRange.size = totalSize; SharedBuffer destRange; destRange.bufferId = 0; destRange.offset = 0; destRange.size = totalSize; Pattern noPattern = {0, 0}; std::vector keyIdVec(keyId, keyId + KEY_ID_SIZE); std::vector ivVec(iv, iv + KEY_IV_SIZE); DecryptArgs args; args.secure = false; args.keyId = keyIdVec; args.iv = ivVec; args.mode = Mode::AES_CTR; args.pattern = noPattern; args.subSamples = subSamples; args.source = std::move(sourceRange); args.offset = 0; args.destination = std::move(destRange); int32_t bytesWritten = 0; auto ret = mPlugin->decrypt(args, &bytesWritten); EXPECT_TRUE(ret.isOk()); std::string errorMessage(ret.getMessage()); EXPECT_EQ(0u, errorMessage.size()) << "WVCryptoPlugin reported a detailed error message."; args.secure = true; bytesWritten = 0; ret = mPlugin->decrypt(args, &bytesWritten); EXPECT_TRUE(ret.isOk()); std::string errorMessage2(ret.getMessage()); EXPECT_EQ(0u, errorMessage2.size()) << "WVCryptoPlugin reported a detailed error message."; munmap(sourceBase, totalSize); munmap(destBase, totalSize); } TEST_F(WVCryptoPluginTest, AllowsSessionIdChanges) { uint8_t keyId[KEY_ID_SIZE]; uint8_t iv[KEY_IV_SIZE]; uint8_t sessionId2[kSessionIdSize]; static constexpr size_t kDataSize = 32; FILE *fp = fopen("/dev/urandom", "r"); ASSERT_NE(fp, nullptr) << "Failed to open /dev/urandom"; 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); fclose(fp); SubSample subSample; subSample.numBytesOfClearData = 16; subSample.numBytesOfEncryptedData = 16; std::vector subSamples; subSamples.push_back(subSample); int64_t totalSize = 0; for (size_t i = 0; i < subSamples.size(); ++i) { totalSize += subSamples[i].numBytesOfClearData; totalSize += subSamples[i].numBytesOfEncryptedData; } std::vector sessionIdVector(sessionId, sessionId + kSessionIdSize); std::vector sessionId2Vector(sessionId2, sessionId2 + kSessionIdSize); // Provide the expected behavior for IsOpenSession EXPECT_CALL(*mCdm, IsOpenSession(_)).WillRepeatedly(testing::Return(true)); // Specify the expected calls to Decrypt { InSequence calls; EXPECT_CALL(*mCdm, DecryptV16(ElementsAreArray(sessionId, kSessionIdSize), _, _)) .Times(1); EXPECT_CALL(*mCdm, DecryptV16(ElementsAreArray(sessionId2, kSessionIdSize), _, _)) .Times(1); } SharedBuffer sourceBuffer; getDecryptMemory(mPluginNoSid, kDataSize, 0, sourceBuffer); auto sourceBase = fillRandom(sourceBuffer); SharedBuffer destBuffer; getDecryptMemory(mPluginNoSid, kDataSize, 0, destBuffer); auto destBase = fillRandom(destBuffer); SharedBuffer sourceRange; sourceRange.bufferId = 0; sourceRange.offset = 0; sourceRange.size = totalSize; SharedBuffer destRange; destRange.bufferId = 0; destRange.offset = 0; destRange.size = totalSize; Pattern noPattern = {0, 0}; std::vector keyIdVec(keyId, keyId + KEY_ID_SIZE); std::vector ivVec(iv, iv + KEY_IV_SIZE); DecryptArgs args; args.secure = false; args.keyId = keyIdVec; args.iv = ivVec; args.mode = Mode::AES_CTR; args.pattern = noPattern; args.subSamples = subSamples; args.source = std::move(sourceRange); args.offset = 0; args.destination = std::move(destRange); auto ret = mPluginNoSid->setMediaDrmSession(sessionIdVector); EXPECT_TRUE(ret.isOk()); int32_t bytesWritten = 0; ret = mPluginNoSid->decrypt(args, &bytesWritten); std::string errorMessage(ret.getMessage()); EXPECT_TRUE(ret.isOk()); EXPECT_EQ(0u, errorMessage.size()) << "WVCryptoPlugin reported a detailed error message."; ret = mPluginNoSid->setMediaDrmSession(sessionId2Vector); EXPECT_TRUE(ret.isOk()); bytesWritten = 0; ret = mPluginNoSid->decrypt(args, &bytesWritten); EXPECT_TRUE(ret.isOk()); std::string errorMessage2(ret.getMessage()); EXPECT_EQ(0u, errorMessage2.size()) << "WVCryptoPlugin reported a detailed error message."; munmap(sourceBase, totalSize); munmap(destBase, totalSize); } TEST_F(WVCryptoPluginTest, DisallowsUnopenedSessionIdChanges) { std::vector sessionIdVector(sessionId, sessionId + kSessionIdSize); // Specify the expected calls to IsOpenSession { InSequence calls; EXPECT_CALL(*mCdm, IsOpenSession(ElementsAreArray(blank, 0))) .WillRepeatedly(testing::Return(false)); EXPECT_CALL(*mCdm, IsOpenSession(ElementsAreArray(sessionId, kSessionIdSize))) .WillOnce(testing::Return(false)) .WillOnce(testing::Return(true)); } auto ret = mPluginNoSid->setMediaDrmSession(sessionIdVector); EXPECT_EQ(static_cast(Status::ERROR_DRM_SESSION_NOT_OPENED), ret.getServiceSpecificError()); ret = mPluginNoSid->setMediaDrmSession(sessionIdVector); EXPECT_TRUE(ret.isOk()); } } // namespace widevine } // namespace drm } // namespace hardware } // namespace wvdrm