blob: f00dbec95e7c757aa03c32f56fc9388152243bdf [file] [log] [blame]
// Copyright 2018 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/gpu/windows/d3d11_decryptor.h"
#include <initguid.h>
#include <array>
#include "base/bind.h"
#include "base/containers/span.h"
#include "base/stl_util.h"
#include "media/base/decoder_buffer.h"
#include "media/base/subsample_entry.h"
#include "media/base/win/d3d11_mocks.h"
#include "media/cdm/cdm_proxy_context.h"
using ::testing::_;
using ::testing::AtLeast;
using ::testing::DoAll;
using ::testing::ElementsAreArray;
using ::testing::Invoke;
using ::testing::IsNull;
using ::testing::Mock;
using ::testing::Pointee;
using ::testing::Return;
using ::testing::SetArgPointee;
template <class T>
using ComPtr = Microsoft::WRL::ComPtr<T>;
namespace media {
namespace {
// clang-format off
// The value doesn't matter this is just a GUID.
DEFINE_GUID(TEST_GUID,
0x01020304, 0xffee, 0xefba,
0x93, 0xaa, 0x47, 0x77, 0x43, 0xb1, 0x22, 0x98);
// clang-format on
// Should be non-0 so that it's different from the default TimeDelta.
constexpr base::TimeDelta kTestTimestamp =
base::TimeDelta::FromMilliseconds(33);
const uint8_t kAnyKeyBlob[] = {3, 5, 38, 19};
const char kKeyId[] = "some 16 byte id.";
const char kIv[] = "some 16 byte iv.";
// For tests where the input doesn't matter.
const uint8_t kAnyInput[] = {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
};
const SubsampleEntry kAnyInputSubsample(0, base::size(kAnyInput));
scoped_refptr<DecoderBuffer> TestDecoderBuffer(
const uint8_t* input,
size_t data_size,
const std::string& key_id,
const std::string& iv,
const std::vector<SubsampleEntry>& subsamples) {
scoped_refptr<DecoderBuffer> encrypted_buffer =
DecoderBuffer::CopyFrom(input, data_size);
encrypted_buffer->set_decrypt_config(
DecryptConfig::CreateCencConfig(key_id, iv, subsamples));
encrypted_buffer->set_timestamp(kTestTimestamp);
return encrypted_buffer;
}
CdmProxyContext::D3D11DecryptContext TestDecryptContext(
ComPtr<D3D11CryptoSessionMock> crypto_session_mock) {
CdmProxyContext::D3D11DecryptContext decrypt_context = {};
decrypt_context.crypto_session = crypto_session_mock.Get();
decrypt_context.key_blob = kAnyKeyBlob;
decrypt_context.key_blob_size = base::size(kAnyKeyBlob);
decrypt_context.key_info_guid = TEST_GUID;
return decrypt_context;
}
class CallbackMock {
public:
MOCK_METHOD2(DecryptCallback, Decryptor::DecryptCB::RunType);
};
class CdmProxyContextMock : public CdmProxyContext {
public:
MOCK_METHOD2(GetD3D11DecryptContext,
base::Optional<D3D11DecryptContext>(CdmProxy::KeyType key_type,
const std::string& key_id));
};
// Checks that BUFFER_DESC has these fields match.
// Flags are ORed values, so this only checks that the expected flags are set.
// The other fields are ignored.
MATCHER_P3(BufferDescHas, usage, bind_flags, cpu_access, "") {
const D3D11_BUFFER_DESC& buffer_desc = *arg;
if (buffer_desc.Usage != usage)
return false;
// Because the flags are enums the compiler infers that the input flags are
// signed ints. And the compiler rejects comparing signed int and unsigned
// int, so they are cast here.
const UINT unsigned_bind_flags = bind_flags;
const UINT unsigned_cpu_access_flags = cpu_access;
if ((buffer_desc.BindFlags & unsigned_bind_flags) != unsigned_bind_flags)
return false;
return (buffer_desc.CPUAccessFlags & unsigned_cpu_access_flags) ==
unsigned_cpu_access_flags;
}
// NumEncryptedBytesAtBeginning must be greater than or equal to the size of the
// encrypted data, and also a multiple of 16.
MATCHER_P(NumEncryptedBytesAtBeginningGreaterOrEq, value, "") {
const D3D11_ENCRYPTED_BLOCK_INFO& block_info = *arg;
if (block_info.NumEncryptedBytesAtBeginning < value) {
*result_listener << block_info.NumEncryptedBytesAtBeginning
<< " is not less than " << value;
return false;
}
return block_info.NumEncryptedBytesAtBeginning % 16 == 0;
}
ACTION_P(SetBufferDescSize, size) {
arg0->ByteWidth = size;
}
MATCHER_P2(OutputDataEquals, data, size, "") {
scoped_refptr<DecoderBuffer> buffer = arg;
if (size != buffer->data_size()) {
return false;
}
if (buffer->timestamp() != kTestTimestamp) {
return false;
}
std::vector<uint8_t> expected(data, data + size);
std::vector<uint8_t> actual(buffer->data(),
buffer->data() + buffer->data_size());
return actual == expected;
}
} // namespace
class D3D11DecryptorTest : public ::testing::Test {
protected:
void SetUp() override {
decryptor_ = std::make_unique<D3D11Decryptor>(&mock_proxy_);
device_mock_ = CreateD3D11Mock<D3D11DeviceMock>();
device_context_mock_ = CreateD3D11Mock<D3D11DeviceContextMock>();
video_context_mock_ = CreateD3D11Mock<D3D11VideoContextMock>();
staging_buffer1_ = CreateD3D11Mock<D3D11BufferMock>();
staging_buffer2_ = CreateD3D11Mock<D3D11BufferMock>();
gpu_buffer_ = CreateD3D11Mock<D3D11BufferMock>();
}
// Only sets mock expectations to the objects. Use this for the case where the
// buffers are expected to be created from |device_mock_|, that's accessible
// through |crypto_session_mock|'s GetDevice() function.
void SetExpectationsForSuccessfulBufferInitialization(
D3D11CryptoSessionMock* crypto_session_mock,
CdmProxyContext::D3D11DecryptContext* decrypt_context) {
// As noted in the function comment, the device is accessible from the
// crypto session.
EXPECT_CALL(*crypto_session_mock, GetDevice(_))
.Times(AtLeast(1))
.WillRepeatedly(SetComPointee<0>(device_mock_.Get()));
// The other components accessible (directly or indirectly) from the device.
COM_EXPECT_CALL(device_mock_, GetImmediateContext(_))
.Times(AtLeast(1))
.WillRepeatedly(SetComPointee<0>(device_context_mock_.Get()));
COM_EXPECT_CALL(device_context_mock_,
QueryInterface(IID_ID3D11VideoContext, _))
.Times(AtLeast(1))
.WillRepeatedly(SetComPointeeAndReturnOk<1>(video_context_mock_.Get()));
EXPECT_CALL(mock_proxy_,
GetD3D11DecryptContext(CdmProxy::KeyType::kDecryptOnly, kKeyId))
.WillOnce(Return(*decrypt_context));
// These return big enough size.
COM_ON_CALL(staging_buffer1_, GetDesc(_))
.WillByDefault(SetBufferDescSize(20000));
COM_ON_CALL(staging_buffer2_, GetDesc(_))
.WillByDefault(SetBufferDescSize(20000));
COM_ON_CALL(gpu_buffer_, GetDesc(_))
.WillByDefault(SetBufferDescSize(20000));
// It should be requesting for 2 staging buffers one for writing the data to
// a GPU buffer and one for reading from the a GPU buffer.
COM_EXPECT_CALL(device_mock_,
CreateBuffer(BufferDescHas(D3D11_USAGE_STAGING, 0u,
D3D11_CPU_ACCESS_READ |
D3D11_CPU_ACCESS_WRITE),
nullptr, _))
.WillOnce(SetComPointeeAndReturnOk<2>(staging_buffer1_.Get()))
.WillOnce(SetComPointeeAndReturnOk<2>(staging_buffer2_.Get()));
// It should be requesting a GPU only accessible buffer to the decrypted
// output.
COM_EXPECT_CALL(device_mock_,
CreateBuffer(BufferDescHas(D3D11_USAGE_DEFAULT,
D3D11_BIND_RENDER_TARGET, 0u),
nullptr, _))
.WillOnce(SetComPointeeAndReturnOk<2>(gpu_buffer_.Get()));
}
// |input| is the input to the Decrypt() function, the subsample information
// is in |subsamples| therefore |input| may not be entirely encrypted. The
// data that is encrypted in |input| should be |encrypted_input|.
// |whole_output| is the expected output from the Decrypt() call, reported by
// the callback. The decrypted result of |encrypted_input| should be
// |decrypted_output|.
void ExpectSuccessfulDecryption(D3D11CryptoSessionMock* crypto_session_mock,
base::span<const uint8_t> input,
base::span<const uint8_t> encrypted_input,
base::span<const uint8_t> whole_output,
base::span<const uint8_t> decrypted_output,
const std::vector<SubsampleEntry>& subsamples,
D3D11Decryptor* decryptor) {
D3D11_MAPPED_SUBRESOURCE staging_buffer1_subresource = {};
auto staging_buffer1_subresource_buffer =
std::make_unique<uint8_t[]>(20000);
staging_buffer1_subresource.pData =
staging_buffer1_subresource_buffer.get();
// It should be requesting for a memory mapped buffer, from the staging
// buffer, to pass the encrypted data to the GPU.
COM_EXPECT_CALL(device_context_mock_,
Map(staging_buffer1_.Get(), 0, D3D11_MAP_WRITE, _, _))
.WillOnce(
DoAll(SetArgPointee<4>(staging_buffer1_subresource), Return(S_OK)));
COM_EXPECT_CALL(device_context_mock_, Unmap(staging_buffer1_.Get(), 0));
COM_EXPECT_CALL(
video_context_mock_,
DecryptionBlt(
crypto_session_mock,
reinterpret_cast<ID3D11Texture2D*>(staging_buffer1_.Get()),
reinterpret_cast<ID3D11Texture2D*>(gpu_buffer_.Get()),
NumEncryptedBytesAtBeginningGreaterOrEq(encrypted_input.size()),
sizeof(kAnyKeyBlob), kAnyKeyBlob, _, _));
COM_EXPECT_CALL(device_context_mock_,
CopyResource(staging_buffer2_.Get(), gpu_buffer_.Get()));
D3D11_MAPPED_SUBRESOURCE staging_buffer2_subresource = {};
// pData field is non-const void* so make a copy of kFakeDecryptedData that
// can be cast to void*.
std::unique_ptr<uint8_t[]> decrypted_data =
std::make_unique<uint8_t[]>(decrypted_output.size());
memcpy(decrypted_data.get(), decrypted_output.data(),
decrypted_output.size());
staging_buffer2_subresource.pData = decrypted_data.get();
// Tt should be requesting for a memory mapped buffer, from the staging
// buffer, to read the decrypted data out from the GPU buffer.
COM_EXPECT_CALL(device_context_mock_,
Map(staging_buffer2_.Get(), 0, D3D11_MAP_READ, _, _))
.WillOnce(
DoAll(SetArgPointee<4>(staging_buffer2_subresource), Return(S_OK)));
COM_EXPECT_CALL(device_context_mock_, Unmap(staging_buffer2_.Get(), 0));
CallbackMock callbacks;
EXPECT_CALL(callbacks,
DecryptCallback(Decryptor::kSuccess,
OutputDataEquals(whole_output.data(),
whole_output.size())));
scoped_refptr<DecoderBuffer> encrypted_buffer =
TestDecoderBuffer(input.data(), input.size(), kKeyId, kIv, subsamples);
decryptor->Decrypt(Decryptor::kAudio, encrypted_buffer,
base::BindRepeating(&CallbackMock::DecryptCallback,
base::Unretained(&callbacks)));
// Verify that the data copied to the GPU buffer is the encrypted portion.
EXPECT_TRUE(std::equal(encrypted_input.begin(), encrypted_input.end(),
staging_buffer1_subresource_buffer.get()));
}
std::unique_ptr<D3D11Decryptor> decryptor_;
CdmProxyContextMock mock_proxy_;
ComPtr<D3D11DeviceMock> device_mock_;
ComPtr<D3D11DeviceContextMock> device_context_mock_;
ComPtr<D3D11VideoContextMock> video_context_mock_;
private:
ComPtr<D3D11BufferMock> staging_buffer1_;
ComPtr<D3D11BufferMock> staging_buffer2_;
ComPtr<D3D11BufferMock> gpu_buffer_;
};
// Verify that full sample encrypted sample works.
TEST_F(D3D11DecryptorTest, FullSampleCtrDecrypt) {
const uint8_t kInput[] = {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
};
const SubsampleEntry kSubsample(0, base::size(kInput));
// This is arbitrary. Just used to check that this value is output from the
// method.
const uint8_t kFakeDecryptedData[] = {
15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0,
};
static_assert(base::size(kFakeDecryptedData) == base::size(kInput),
"Fake input and output data size must match.");
ComPtr<D3D11CryptoSessionMock> crypto_session_mock =
CreateD3D11Mock<D3D11CryptoSessionMock>();
CdmProxyContext::D3D11DecryptContext decrypt_context =
TestDecryptContext(crypto_session_mock);
SetExpectationsForSuccessfulBufferInitialization(crypto_session_mock.Get(),
&decrypt_context);
// The entire sample is encrypted so the encrypted/decrypted portions are the
// input/output.
ExpectSuccessfulDecryption(crypto_session_mock.Get(), kInput, kInput,
kFakeDecryptedData, kFakeDecryptedData,
{kSubsample}, decryptor_.get());
}
// Verify that it works for encrypted input that's not a multiple of 16.
TEST_F(D3D11DecryptorTest, InputSizeNotMultipleOf16) {
const uint8_t kInput[] = {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
};
const SubsampleEntry kSubsample(0, base::size(kInput));
// This is arbitrary. Just used to check that this value is output from the
// method.
const uint8_t kFakeDecryptedData[] = {
20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0,
};
static_assert(base::size(kFakeDecryptedData) == base::size(kInput),
"Fake input and output data size must match.");
ComPtr<D3D11CryptoSessionMock> crypto_session_mock =
CreateD3D11Mock<D3D11CryptoSessionMock>();
CdmProxyContext::D3D11DecryptContext decrypt_context =
TestDecryptContext(crypto_session_mock);
SetExpectationsForSuccessfulBufferInitialization(crypto_session_mock.Get(),
&decrypt_context);
// The entire sample is encrypted so the encrypted/decrypted portions are the
// input/output.
ExpectSuccessfulDecryption(crypto_session_mock.Get(), kInput, kInput,
kFakeDecryptedData, kFakeDecryptedData,
{kSubsample}, decryptor_.get());
}
// Verify subsample decryption works.
TEST_F(D3D11DecryptorTest, SubsampleCtrDecrypt) {
// clang-format off
const uint8_t kInput[] = {
// clear 16 bytes.
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
// encrypted 16 bytes.
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
// clear 5 bytes.
0, 1, 2, 3, 4,
// encrypted 16 bytes.
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
};
// Encrypted parts of the input
const uint8_t kInputEncrypted[] = {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
};
// This is arbitrary. Just used to check that this value is output from the
// method.
const uint8_t kFakeOutputData[] = {
// clear 16 bytes.
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
// decrypted 16 bytes.
15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0,
// clear 5 bytes.
0, 1, 2, 3, 4,
// decrypted 16 bytes.
15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0,
};
const uint8_t kFakeDecryptedData[] = {
15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0,
15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0,
};
// clang-format on
static_assert(base::size(kFakeOutputData) == base::size(kInput),
"Fake input and output data size must match.");
const std::vector<SubsampleEntry> subsamples = {SubsampleEntry(16, 16),
SubsampleEntry(5, 16)};
ComPtr<D3D11CryptoSessionMock> crypto_session_mock =
CreateD3D11Mock<D3D11CryptoSessionMock>();
CdmProxyContext::D3D11DecryptContext decrypt_context =
TestDecryptContext(crypto_session_mock);
SetExpectationsForSuccessfulBufferInitialization(crypto_session_mock.Get(),
&decrypt_context);
ExpectSuccessfulDecryption(crypto_session_mock.Get(), kInput, kInputEncrypted,
kFakeOutputData, kFakeDecryptedData, subsamples,
decryptor_.get());
}
// Verify that if the input is too big, it fails. This may be removed if the
// implementation supports big input.
TEST_F(D3D11DecryptorTest, DecryptInputTooBig) {
// Something pretty big to be an audio frame. The actual data size doesn't
// matter.
std::array<uint8_t, 1000000> kInput;
const SubsampleEntry kSubsample(0, base::size(kInput));
ComPtr<D3D11CryptoSessionMock> crypto_session_mock =
CreateD3D11Mock<D3D11CryptoSessionMock>();
CdmProxyContext::D3D11DecryptContext decrypt_context =
TestDecryptContext(crypto_session_mock);
SetExpectationsForSuccessfulBufferInitialization(crypto_session_mock.Get(),
&decrypt_context);
CallbackMock callbacks;
EXPECT_CALL(callbacks, DecryptCallback(Decryptor::kError, IsNull()));
scoped_refptr<DecoderBuffer> encrypted_buffer = TestDecoderBuffer(
kInput.data(), base::size(kInput), kKeyId, kIv, {kSubsample});
decryptor_->Decrypt(Decryptor::kAudio, encrypted_buffer,
base::BindRepeating(&CallbackMock::DecryptCallback,
base::Unretained(&callbacks)));
}
// If there is no decrypt config, it must be in the clear, so it shouldn't
// change the output.
TEST_F(D3D11DecryptorTest, NoDecryptConfig) {
scoped_refptr<DecoderBuffer> clear_buffer =
DecoderBuffer::CopyFrom(kAnyInput, base::size(kAnyInput));
clear_buffer->set_timestamp(kTestTimestamp);
CallbackMock callbacks;
EXPECT_CALL(
callbacks,
DecryptCallback(Decryptor::kSuccess,
OutputDataEquals(kAnyInput, base::size(kAnyInput))));
decryptor_->Decrypt(Decryptor::kAudio, clear_buffer,
base::BindRepeating(&CallbackMock::DecryptCallback,
base::Unretained(&callbacks)));
}
// The current decryptor cannot deal with pattern encryption.
TEST_F(D3D11DecryptorTest, PatternDecryption) {
scoped_refptr<DecoderBuffer> encrypted_buffer =
DecoderBuffer::CopyFrom(kAnyInput, base::size(kAnyInput));
encrypted_buffer->set_decrypt_config(DecryptConfig::CreateCbcsConfig(
kKeyId, kIv, {kAnyInputSubsample}, EncryptionPattern(1, 9)));
CallbackMock callbacks;
EXPECT_CALL(callbacks, DecryptCallback(Decryptor::kError, IsNull()));
decryptor_->Decrypt(Decryptor::kAudio, encrypted_buffer,
base::BindRepeating(&CallbackMock::DecryptCallback,
base::Unretained(&callbacks)));
}
// If there is no decrypt context, it's missing a key.
TEST_F(D3D11DecryptorTest, NoDecryptContext) {
scoped_refptr<DecoderBuffer> encrypted_buffer = TestDecoderBuffer(
kAnyInput, base::size(kAnyInput), kKeyId, kIv, {kAnyInputSubsample});
EXPECT_CALL(mock_proxy_,
GetD3D11DecryptContext(CdmProxy::KeyType::kDecryptOnly, kKeyId))
.WillOnce(Return(base::nullopt));
CallbackMock callbacks;
EXPECT_CALL(callbacks, DecryptCallback(Decryptor::kNoKey, IsNull()));
decryptor_->Decrypt(Decryptor::kAudio, encrypted_buffer,
base::BindRepeating(&CallbackMock::DecryptCallback,
base::Unretained(&callbacks)));
}
// Verify that if the crypto session's device is the same as the previous call,
// the buffers aren't recreated.
TEST_F(D3D11DecryptorTest, ReuseBuffers) {
ComPtr<D3D11CryptoSessionMock> crypto_session_mock =
CreateD3D11Mock<D3D11CryptoSessionMock>();
CdmProxyContext::D3D11DecryptContext decrypt_context =
TestDecryptContext(crypto_session_mock);
SetExpectationsForSuccessfulBufferInitialization(crypto_session_mock.Get(),
&decrypt_context);
// This test doesn't require checking the output or the correctness of the
// decyrption, so just pass the input buffer for output.
ExpectSuccessfulDecryption(crypto_session_mock.Get(), kAnyInput, kAnyInput,
kAnyInput, kAnyInput, {kAnyInputSubsample},
decryptor_.get());
Mock::VerifyAndClearExpectations(crypto_session_mock.Get());
Mock::VerifyAndClearExpectations(device_mock_.Get());
Mock::VerifyAndClearExpectations(video_context_mock_.Get());
Mock::VerifyAndClearExpectations(device_context_mock_.Get());
Mock::VerifyAndClearExpectations(&mock_proxy_);
COM_EXPECT_CALL(crypto_session_mock, GetDevice(_))
.Times(AtLeast(1))
.WillRepeatedly(SetComPointee<0>(device_mock_.Get()));
EXPECT_CALL(mock_proxy_,
GetD3D11DecryptContext(CdmProxy::KeyType::kDecryptOnly, kKeyId))
.WillOnce(Return(decrypt_context));
// Buffers should not be (re)initialized on the next call to decrypt because
// it's the same device as the previous call.
COM_EXPECT_CALL(device_mock_, CreateBuffer(_, _, _)).Times(0);
// This calls Decrypt() so that the expectations above are triggered.
ExpectSuccessfulDecryption(crypto_session_mock.Get(), kAnyInput, kAnyInput,
kAnyInput, kAnyInput, {kAnyInputSubsample},
decryptor_.get());
}
} // namespace media