2017-02-02 18:28:29 +00:00
|
|
|
// Copyright 2017 Google Inc. All rights reserved.
|
|
|
|
//
|
|
|
|
// Use of this source code is governed by a BSD-style
|
|
|
|
// license that can be found in the LICENSE file or at
|
|
|
|
// https://developers.google.com/open-source/licenses/bsd
|
|
|
|
|
|
|
|
#include "packager/media/crypto/encryption_handler.h"
|
|
|
|
|
|
|
|
#include <gmock/gmock.h>
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
|
2018-10-04 20:24:21 +00:00
|
|
|
#include "packager/media/base/aes_cryptor.h"
|
Implement ChunkingHandler
This handler is a multi-in multi-out handler. If more than one input is
provided, there should be one and only one video stream; also, all inputs
should come from the same thread and are synchronized.
There can be multiple chunking handler running in different threads or even
different processes, we use the "consistent chunking algorithm" to make sure
the chunks in different streams are aligned without explicit communcating
with each other - which is not efficient and often difficult.
Consistent Chunking Algorithm:
1. Find the consistent chunkable boundary
Let the timestamps for video frames be (t1, t2, t3, ...). Then a
consistent chunkable boundary is simply the first chunkable boundary after
(tk / N) != (tk-1 / N), where '/' denotes integer division, and N is the
intended chunk duration.
2. Chunk only at the consistent chunkable boundary
This algorithm will make sure the chunks from different video streams are
aligned if they have aligned GoPs. However, this algorithm will only work
for video streams. To be able to chunk non video streams at similar
positions as video streams, ChunkingHandler is designed to accept one video
input and multiple non video inputs, the non video inputs are chunked when
the video input is chunked. If the inputs are synchronized - which is true
if the inputs come from the same demuxer, the video and non video chunks
are aligned.
Change-Id: Id3bad51ab14f311efdb8713b6cd36d36cf9e4639
2017-02-07 18:58:47 +00:00
|
|
|
#include "packager/media/base/media_handler_test_base.h"
|
2018-10-04 20:24:21 +00:00
|
|
|
#include "packager/media/base/mock_aes_cryptor.h"
|
2017-10-17 23:03:08 +00:00
|
|
|
#include "packager/media/base/raw_key_source.h"
|
2018-10-04 20:24:21 +00:00
|
|
|
#include "packager/media/crypto/aes_encryptor_factory.h"
|
2018-10-02 23:08:32 +00:00
|
|
|
#include "packager/media/crypto/subsample_generator.h"
|
2017-06-29 22:23:53 +00:00
|
|
|
#include "packager/status_test_util.h"
|
2017-02-02 18:28:29 +00:00
|
|
|
|
|
|
|
namespace shaka {
|
|
|
|
namespace media {
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
using ::testing::_;
|
2018-10-04 20:24:21 +00:00
|
|
|
using ::testing::ByMove;
|
2017-02-02 18:28:29 +00:00
|
|
|
using ::testing::Combine;
|
|
|
|
using ::testing::DoAll;
|
|
|
|
using ::testing::ElementsAre;
|
2018-10-04 20:24:21 +00:00
|
|
|
using ::testing::Invoke;
|
2017-03-11 02:48:04 +00:00
|
|
|
using ::testing::Mock;
|
2017-02-02 18:28:29 +00:00
|
|
|
using ::testing::Return;
|
|
|
|
using ::testing::SetArgPointee;
|
2017-03-11 02:48:04 +00:00
|
|
|
using ::testing::StrictMock;
|
2017-02-02 18:28:29 +00:00
|
|
|
using ::testing::Values;
|
2017-03-11 02:48:04 +00:00
|
|
|
using ::testing::ValuesIn;
|
2017-02-02 18:28:29 +00:00
|
|
|
using ::testing::WithParamInterface;
|
|
|
|
|
2018-10-04 20:24:21 +00:00
|
|
|
const size_t kStreamIndex = 0;
|
|
|
|
const uint32_t kTimeScale = 1000;
|
2017-05-22 20:31:41 +00:00
|
|
|
const char kAudioStreamLabel[] = "AUDIO";
|
|
|
|
const char kSdVideoStreamLabel[] = "SD";
|
|
|
|
|
2017-03-11 02:48:04 +00:00
|
|
|
const uint8_t kKeyId[]{
|
|
|
|
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
|
|
|
0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15,
|
|
|
|
};
|
|
|
|
const uint8_t kKey[]{
|
|
|
|
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
|
|
|
0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15,
|
|
|
|
};
|
|
|
|
const uint8_t kIv[]{
|
|
|
|
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
|
|
|
0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15,
|
|
|
|
};
|
|
|
|
|
|
|
|
// The default KID for key rotation is all 0s.
|
|
|
|
const uint8_t kKeyRotationDefaultKeyId[] = {
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
};
|
|
|
|
|
2017-10-17 23:03:08 +00:00
|
|
|
class MockKeySource : public RawKeySource {
|
2017-02-02 18:28:29 +00:00
|
|
|
public:
|
2017-06-13 21:54:12 +00:00
|
|
|
MOCK_METHOD2(GetKey,
|
|
|
|
Status(const std::string& stream_label, EncryptionKey* key));
|
2019-01-24 18:39:54 +00:00
|
|
|
MOCK_METHOD4(GetCryptoPeriodKey,
|
2017-02-02 18:28:29 +00:00
|
|
|
Status(uint32_t crypto_period_index,
|
2019-01-24 18:39:54 +00:00
|
|
|
uint32_t crypto_period_duration_in_seconds,
|
2017-06-13 21:54:12 +00:00
|
|
|
const std::string& stream_label,
|
2017-02-02 18:28:29 +00:00
|
|
|
EncryptionKey* key));
|
|
|
|
};
|
|
|
|
|
2018-10-02 23:08:32 +00:00
|
|
|
class MockSubsampleGenerator : public SubsampleGenerator {
|
2017-02-02 18:28:29 +00:00
|
|
|
public:
|
2018-10-02 23:08:32 +00:00
|
|
|
MockSubsampleGenerator() : SubsampleGenerator(true) {}
|
|
|
|
|
|
|
|
MOCK_METHOD2(Initialize,
|
|
|
|
Status(FourCC protection_scheme, const StreamInfo& stream_info));
|
|
|
|
MOCK_METHOD3(GenerateSubsamples,
|
|
|
|
Status(const uint8_t* frame,
|
|
|
|
size_t frame_size,
|
|
|
|
std::vector<SubsampleEntry>* subsamples));
|
2017-02-02 18:28:29 +00:00
|
|
|
};
|
|
|
|
|
2018-10-04 20:24:21 +00:00
|
|
|
class MockAesEncryptorFactory : public AesEncryptorFactory {
|
|
|
|
public:
|
|
|
|
MOCK_METHOD6(CreateEncryptor,
|
|
|
|
std::unique_ptr<AesCryptor>(FourCC protection_scheme,
|
|
|
|
uint8_t crypt_byte_block,
|
|
|
|
uint8_t skip_byte_block,
|
|
|
|
Codec codec,
|
|
|
|
const std::vector<uint8_t>& key,
|
|
|
|
const std::vector<uint8_t>& iv));
|
|
|
|
};
|
|
|
|
|
2017-02-02 18:28:29 +00:00
|
|
|
} // namespace
|
|
|
|
|
2017-09-14 16:23:29 +00:00
|
|
|
class EncryptionHandlerTest : public MediaHandlerGraphTestBase {
|
2017-02-02 18:28:29 +00:00
|
|
|
public:
|
2017-07-05 23:47:55 +00:00
|
|
|
void SetUp() override { SetUpEncryptionHandler(EncryptionParams()); }
|
2017-02-02 18:28:29 +00:00
|
|
|
|
2017-07-05 23:47:55 +00:00
|
|
|
void SetUpEncryptionHandler(const EncryptionParams& encryption_params) {
|
|
|
|
EncryptionParams new_encryption_params = encryption_params;
|
|
|
|
if (!encryption_params.stream_label_func) {
|
2017-05-22 20:31:41 +00:00
|
|
|
// Setup default stream label function.
|
2017-07-05 23:47:55 +00:00
|
|
|
new_encryption_params.stream_label_func =
|
2017-05-22 20:31:41 +00:00
|
|
|
[](const EncryptionParams::EncryptedStreamAttributes&
|
|
|
|
stream_attributes) {
|
|
|
|
if (stream_attributes.stream_type ==
|
|
|
|
EncryptionParams::EncryptedStreamAttributes::kAudio) {
|
|
|
|
return kAudioStreamLabel;
|
|
|
|
}
|
|
|
|
return kSdVideoStreamLabel;
|
|
|
|
};
|
|
|
|
}
|
2017-02-02 18:28:29 +00:00
|
|
|
encryption_handler_.reset(
|
2017-07-05 23:47:55 +00:00
|
|
|
new EncryptionHandler(new_encryption_params, &mock_key_source_));
|
Implement ChunkingHandler
This handler is a multi-in multi-out handler. If more than one input is
provided, there should be one and only one video stream; also, all inputs
should come from the same thread and are synchronized.
There can be multiple chunking handler running in different threads or even
different processes, we use the "consistent chunking algorithm" to make sure
the chunks in different streams are aligned without explicit communcating
with each other - which is not efficient and often difficult.
Consistent Chunking Algorithm:
1. Find the consistent chunkable boundary
Let the timestamps for video frames be (t1, t2, t3, ...). Then a
consistent chunkable boundary is simply the first chunkable boundary after
(tk / N) != (tk-1 / N), where '/' denotes integer division, and N is the
intended chunk duration.
2. Chunk only at the consistent chunkable boundary
This algorithm will make sure the chunks from different video streams are
aligned if they have aligned GoPs. However, this algorithm will only work
for video streams. To be able to chunk non video streams at similar
positions as video streams, ChunkingHandler is designed to accept one video
input and multiple non video inputs, the non video inputs are chunked when
the video input is chunked. If the inputs are synchronized - which is true
if the inputs come from the same demuxer, the video and non video chunks
are aligned.
Change-Id: Id3bad51ab14f311efdb8713b6cd36d36cf9e4639
2017-02-07 18:58:47 +00:00
|
|
|
SetUpGraph(1 /* one input */, 1 /* one output */, encryption_handler_);
|
2018-10-02 23:08:32 +00:00
|
|
|
// Inject default subsamples to avoid parsing problems.
|
|
|
|
const std::vector<SubsampleEntry> empty_subsamples;
|
|
|
|
InjectSubsamples(empty_subsamples);
|
2017-02-02 18:28:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Status Process(std::unique_ptr<StreamData> stream_data) {
|
|
|
|
return encryption_handler_->Process(std::move(stream_data));
|
|
|
|
}
|
|
|
|
|
2017-03-11 02:48:04 +00:00
|
|
|
EncryptionKey GetMockEncryptionKey() {
|
|
|
|
EncryptionKey encryption_key;
|
|
|
|
encryption_key.key_id.assign(kKeyId, kKeyId + sizeof(kKeyId));
|
|
|
|
encryption_key.key.assign(kKey, kKey + sizeof(kKey));
|
|
|
|
encryption_key.iv.assign(kIv, kIv + sizeof(kIv));
|
|
|
|
return encryption_key;
|
|
|
|
}
|
|
|
|
|
2018-10-02 23:08:32 +00:00
|
|
|
void InjectSubsamples(const std::vector<SubsampleEntry>& subsamples) {
|
|
|
|
std::unique_ptr<MockSubsampleGenerator> mock_generator(
|
|
|
|
new MockSubsampleGenerator);
|
|
|
|
EXPECT_CALL(*mock_generator, GenerateSubsamples(_, _, _))
|
|
|
|
.WillRepeatedly(
|
|
|
|
DoAll(SetArgPointee<2>(subsamples), Return(Status::OK)));
|
2017-02-02 18:28:29 +00:00
|
|
|
|
2018-10-02 23:08:32 +00:00
|
|
|
encryption_handler_->InjectSubsampleGeneratorForTesting(
|
|
|
|
std::move(mock_generator));
|
2017-02-02 18:28:29 +00:00
|
|
|
}
|
|
|
|
|
2018-10-04 20:24:21 +00:00
|
|
|
void InjectEncryptorFactoryForTesting(
|
|
|
|
std::unique_ptr<AesEncryptorFactory> encryptor_factory) {
|
|
|
|
encryption_handler_->InjectEncryptorFactoryForTesting(
|
|
|
|
std::move(encryptor_factory));
|
|
|
|
}
|
|
|
|
|
2017-02-02 18:28:29 +00:00
|
|
|
protected:
|
|
|
|
std::shared_ptr<EncryptionHandler> encryption_handler_;
|
2017-03-11 02:48:04 +00:00
|
|
|
StrictMock<MockKeySource> mock_key_source_;
|
2017-02-02 18:28:29 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
TEST_F(EncryptionHandlerTest, Initialize) {
|
|
|
|
ASSERT_OK(encryption_handler_->Initialize());
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_F(EncryptionHandlerTest, OnlyOneOutput) {
|
|
|
|
// Connecting another handler will fail.
|
2017-02-24 01:17:47 +00:00
|
|
|
ASSERT_OK(encryption_handler_->AddHandler(some_handler()));
|
2017-02-02 18:28:29 +00:00
|
|
|
ASSERT_EQ(error::INVALID_ARGUMENT,
|
2017-02-24 01:17:47 +00:00
|
|
|
encryption_handler_->Initialize().error_code());
|
2017-02-02 18:28:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
TEST_F(EncryptionHandlerTest, OnlyOneInput) {
|
Implement ChunkingHandler
This handler is a multi-in multi-out handler. If more than one input is
provided, there should be one and only one video stream; also, all inputs
should come from the same thread and are synchronized.
There can be multiple chunking handler running in different threads or even
different processes, we use the "consistent chunking algorithm" to make sure
the chunks in different streams are aligned without explicit communcating
with each other - which is not efficient and often difficult.
Consistent Chunking Algorithm:
1. Find the consistent chunkable boundary
Let the timestamps for video frames be (t1, t2, t3, ...). Then a
consistent chunkable boundary is simply the first chunkable boundary after
(tk / N) != (tk-1 / N), where '/' denotes integer division, and N is the
intended chunk duration.
2. Chunk only at the consistent chunkable boundary
This algorithm will make sure the chunks from different video streams are
aligned if they have aligned GoPs. However, this algorithm will only work
for video streams. To be able to chunk non video streams at similar
positions as video streams, ChunkingHandler is designed to accept one video
input and multiple non video inputs, the non video inputs are chunked when
the video input is chunked. If the inputs are synchronized - which is true
if the inputs come from the same demuxer, the video and non video chunks
are aligned.
Change-Id: Id3bad51ab14f311efdb8713b6cd36d36cf9e4639
2017-02-07 18:58:47 +00:00
|
|
|
ASSERT_OK(some_handler()->AddHandler(encryption_handler_));
|
2017-02-02 18:28:29 +00:00
|
|
|
ASSERT_EQ(error::INVALID_ARGUMENT,
|
|
|
|
encryption_handler_->Initialize().error_code());
|
|
|
|
}
|
|
|
|
|
2018-10-04 20:24:21 +00:00
|
|
|
TEST_F(EncryptionHandlerTest, GetKeyFailed) {
|
|
|
|
const EncryptionKey mock_encryption_key = GetMockEncryptionKey();
|
|
|
|
EXPECT_CALL(mock_key_source_, GetKey(_, _))
|
|
|
|
.WillOnce(Return(Status(error::INVALID_ARGUMENT, "")));
|
|
|
|
|
|
|
|
ASSERT_NOT_OK(Process(StreamData::FromStreamInfo(
|
|
|
|
kStreamIndex, GetVideoStreamInfo(kTimeScale, kCodecH264))));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_F(EncryptionHandlerTest, CreateEncryptorFailed) {
|
|
|
|
const EncryptionKey mock_encryption_key = GetMockEncryptionKey();
|
|
|
|
EXPECT_CALL(mock_key_source_, GetKey(_, _))
|
|
|
|
.WillOnce(
|
|
|
|
DoAll(SetArgPointee<1>(mock_encryption_key), Return(Status::OK)));
|
|
|
|
|
|
|
|
std::unique_ptr<MockAesEncryptorFactory> mock_encryptor_factory(
|
|
|
|
new MockAesEncryptorFactory);
|
|
|
|
EXPECT_CALL(*mock_encryptor_factory,
|
|
|
|
CreateEncryptor(_, _, _, _, mock_encryption_key.key,
|
|
|
|
mock_encryption_key.iv))
|
|
|
|
.WillOnce(Return(ByMove(nullptr)));
|
|
|
|
InjectEncryptorFactoryForTesting(std::move(mock_encryptor_factory));
|
|
|
|
|
|
|
|
ASSERT_NOT_OK(Process(StreamData::FromStreamInfo(
|
|
|
|
kStreamIndex, GetVideoStreamInfo(kTimeScale, kCodecH264))));
|
|
|
|
}
|
|
|
|
|
2017-02-02 18:28:29 +00:00
|
|
|
namespace {
|
|
|
|
|
2017-03-11 02:48:04 +00:00
|
|
|
const bool kIsKeyFrame = true;
|
|
|
|
const bool kIsSubsegment = true;
|
2017-02-02 18:28:29 +00:00
|
|
|
const bool kEncrypted = true;
|
2017-03-11 02:48:04 +00:00
|
|
|
const int64_t kSegmentDuration = 1000;
|
2017-02-02 18:28:29 +00:00
|
|
|
|
2018-10-02 23:08:32 +00:00
|
|
|
// The contents of the data does not matter.
|
|
|
|
const uint8_t kData[] = {0x00, 0x01, 0x02, 0x03, 0x04,
|
|
|
|
0x05, 0x06, 0x07, 0x08, 0x09};
|
2017-09-18 23:31:00 +00:00
|
|
|
const size_t kDataSize = sizeof(kData);
|
2017-03-11 02:48:04 +00:00
|
|
|
|
2017-02-02 18:28:29 +00:00
|
|
|
} // namespace
|
|
|
|
|
|
|
|
class EncryptionHandlerEncryptionTest
|
|
|
|
: public EncryptionHandlerTest,
|
2018-10-02 23:08:32 +00:00
|
|
|
public WithParamInterface<std::tr1::tuple<FourCC, Codec>> {
|
2017-02-02 18:28:29 +00:00
|
|
|
public:
|
|
|
|
void SetUp() override {
|
|
|
|
protection_scheme_ = std::tr1::get<0>(GetParam());
|
|
|
|
codec_ = std::tr1::get<1>(GetParam());
|
|
|
|
}
|
|
|
|
|
|
|
|
uint8_t GetExpectedCryptByteBlock() {
|
|
|
|
switch (protection_scheme_) {
|
|
|
|
case FOURCC_cenc:
|
|
|
|
case FOURCC_cbc1:
|
|
|
|
return 0;
|
|
|
|
case FOURCC_cens:
|
|
|
|
case FOURCC_cbcs:
|
2018-10-04 20:24:21 +00:00
|
|
|
case kAppleSampleAesProtectionScheme:
|
2018-08-20 22:45:02 +00:00
|
|
|
return codec_ == kCodecAAC ? 0 : 1;
|
2017-02-02 18:28:29 +00:00
|
|
|
default:
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
uint8_t GetExpectedSkipByteBlock() {
|
|
|
|
// Always use full sample encryption for audio.
|
|
|
|
if (codec_ == kCodecAAC)
|
|
|
|
return 0;
|
|
|
|
switch (protection_scheme_) {
|
|
|
|
case FOURCC_cenc:
|
|
|
|
case FOURCC_cbc1:
|
|
|
|
return 0;
|
|
|
|
case FOURCC_cens:
|
|
|
|
case FOURCC_cbcs:
|
2017-03-11 02:48:04 +00:00
|
|
|
case kAppleSampleAesProtectionScheme:
|
2017-02-02 18:28:29 +00:00
|
|
|
return 9;
|
|
|
|
default:
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-11 02:48:04 +00:00
|
|
|
uint8_t GetExpectedPerSampleIvSize() {
|
|
|
|
switch (protection_scheme_) {
|
|
|
|
case FOURCC_cenc:
|
|
|
|
case FOURCC_cens:
|
|
|
|
case FOURCC_cbc1:
|
|
|
|
return sizeof(kIv);
|
|
|
|
case FOURCC_cbcs:
|
|
|
|
case kAppleSampleAesProtectionScheme:
|
|
|
|
return 0;
|
|
|
|
default:
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
std::vector<uint8_t> GetExpectedConstantIv() {
|
|
|
|
switch (protection_scheme_) {
|
|
|
|
case FOURCC_cbcs:
|
|
|
|
case kAppleSampleAesProtectionScheme:
|
|
|
|
return std::vector<uint8_t>(std::begin(kIv), std::end(kIv));
|
|
|
|
default:
|
|
|
|
return std::vector<uint8_t>();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-02 18:28:29 +00:00
|
|
|
protected:
|
|
|
|
FourCC protection_scheme_;
|
|
|
|
Codec codec_;
|
|
|
|
};
|
|
|
|
|
2018-10-04 20:24:21 +00:00
|
|
|
TEST_P(EncryptionHandlerEncryptionTest, VerifyEncryptorFactoryParams) {
|
|
|
|
EncryptionParams encryption_params;
|
|
|
|
encryption_params.protection_scheme = protection_scheme_;
|
|
|
|
SetUpEncryptionHandler(encryption_params);
|
|
|
|
|
|
|
|
const EncryptionKey mock_encryption_key = GetMockEncryptionKey();
|
|
|
|
EXPECT_CALL(mock_key_source_, GetKey(_, _))
|
|
|
|
.WillOnce(
|
|
|
|
DoAll(SetArgPointee<1>(mock_encryption_key), Return(Status::OK)));
|
|
|
|
|
|
|
|
std::unique_ptr<MockAesCryptor> mock_encryptor(new MockAesCryptor);
|
|
|
|
std::unique_ptr<MockAesEncryptorFactory> mock_encryptor_factory(
|
|
|
|
new MockAesEncryptorFactory);
|
|
|
|
EXPECT_CALL(*mock_encryptor_factory,
|
|
|
|
CreateEncryptor(protection_scheme_, GetExpectedCryptByteBlock(),
|
|
|
|
GetExpectedSkipByteBlock(), codec_,
|
|
|
|
mock_encryption_key.key, mock_encryption_key.iv))
|
|
|
|
.WillOnce(Return(ByMove(std::move(mock_encryptor))));
|
|
|
|
InjectEncryptorFactoryForTesting(std::move(mock_encryptor_factory));
|
|
|
|
|
|
|
|
if (IsVideoCodec(codec_)) {
|
|
|
|
ASSERT_OK(Process(StreamData::FromStreamInfo(
|
|
|
|
kStreamIndex, GetVideoStreamInfo(kTimeScale, codec_))));
|
|
|
|
} else {
|
|
|
|
ASSERT_OK(Process(StreamData::FromStreamInfo(
|
|
|
|
kStreamIndex, GetAudioStreamInfo(kTimeScale, codec_))));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-11 02:48:04 +00:00
|
|
|
TEST_P(EncryptionHandlerEncryptionTest, ClearLeadWithNoKeyRotation) {
|
|
|
|
const double kClearLeadInSeconds = 1.5 * kSegmentDuration / kTimeScale;
|
2017-07-05 23:47:55 +00:00
|
|
|
EncryptionParams encryption_params;
|
|
|
|
encryption_params.protection_scheme = protection_scheme_;
|
|
|
|
encryption_params.clear_lead_in_seconds = kClearLeadInSeconds;
|
|
|
|
SetUpEncryptionHandler(encryption_params);
|
2017-03-11 02:48:04 +00:00
|
|
|
|
|
|
|
const EncryptionKey mock_encryption_key = GetMockEncryptionKey();
|
|
|
|
EXPECT_CALL(mock_key_source_, GetKey(_, _))
|
|
|
|
.WillOnce(
|
|
|
|
DoAll(SetArgPointee<1>(mock_encryption_key), Return(Status::OK)));
|
2017-09-12 17:24:24 +00:00
|
|
|
|
|
|
|
if (IsVideoCodec(codec_)) {
|
|
|
|
ASSERT_OK(Process(StreamData::FromStreamInfo(
|
|
|
|
kStreamIndex, GetVideoStreamInfo(kTimeScale, codec_))));
|
|
|
|
} else {
|
|
|
|
ASSERT_OK(Process(StreamData::FromStreamInfo(
|
|
|
|
kStreamIndex, GetAudioStreamInfo(kTimeScale, codec_))));
|
|
|
|
}
|
|
|
|
|
2018-05-25 17:41:02 +00:00
|
|
|
EXPECT_THAT(
|
|
|
|
GetOutputStreamDataVector(),
|
|
|
|
ElementsAre(IsStreamInfo(kStreamIndex, kTimeScale, kEncrypted, _)));
|
2017-05-09 22:49:01 +00:00
|
|
|
const StreamInfo* stream_info =
|
|
|
|
GetOutputStreamDataVector().back()->stream_info.get();
|
|
|
|
ASSERT_TRUE(stream_info);
|
|
|
|
EXPECT_TRUE(stream_info->has_clear_lead());
|
|
|
|
EXPECT_THAT(stream_info->encryption_config(),
|
2017-03-11 02:48:04 +00:00
|
|
|
MatchEncryptionConfig(
|
|
|
|
protection_scheme_, GetExpectedCryptByteBlock(),
|
|
|
|
GetExpectedSkipByteBlock(), GetExpectedPerSampleIvSize(),
|
|
|
|
GetExpectedConstantIv(), mock_encryption_key.key_id));
|
|
|
|
ClearOutputStreamDataVector();
|
|
|
|
Mock::VerifyAndClearExpectations(&mock_key_source_);
|
|
|
|
|
|
|
|
// There are three segments. Only the third segment is encrypted.
|
|
|
|
for (int i = 0; i < 3; ++i) {
|
|
|
|
// Use single-frame segment for testing.
|
2017-09-12 17:24:24 +00:00
|
|
|
ASSERT_OK(Process(StreamData::FromMediaSample(
|
2017-09-18 23:31:00 +00:00
|
|
|
kStreamIndex, GetMediaSample(i * kSegmentDuration, kSegmentDuration,
|
|
|
|
kIsKeyFrame, kData, kDataSize))));
|
2017-09-12 17:24:24 +00:00
|
|
|
ASSERT_OK(Process(StreamData::FromSegmentInfo(
|
2018-05-25 17:41:02 +00:00
|
|
|
kStreamIndex, GetSegmentInfo(i * kSegmentDuration, kSegmentDuration,
|
|
|
|
!kIsSubsegment))));
|
2017-03-11 02:48:04 +00:00
|
|
|
const bool is_encrypted = i == 2;
|
|
|
|
const auto& output_stream_data = GetOutputStreamDataVector();
|
|
|
|
EXPECT_THAT(output_stream_data,
|
|
|
|
ElementsAre(IsMediaSample(kStreamIndex, i * kSegmentDuration,
|
2018-06-14 23:39:59 +00:00
|
|
|
kSegmentDuration, is_encrypted, _),
|
2017-03-11 02:48:04 +00:00
|
|
|
IsSegmentInfo(kStreamIndex, i * kSegmentDuration,
|
|
|
|
kSegmentDuration, !kIsSubsegment,
|
|
|
|
is_encrypted)));
|
2018-10-04 20:24:21 +00:00
|
|
|
if (is_encrypted) {
|
|
|
|
const auto* media_sample = output_stream_data.front()->media_sample.get();
|
|
|
|
const auto* decrypt_config = media_sample->decrypt_config();
|
|
|
|
EXPECT_EQ(std::vector<uint8_t>(kKeyId, kKeyId + sizeof(kKeyId)),
|
|
|
|
decrypt_config->key_id());
|
|
|
|
EXPECT_EQ(std::vector<uint8_t>(kIv, kIv + sizeof(kIv)),
|
|
|
|
decrypt_config->iv());
|
2018-10-02 23:08:32 +00:00
|
|
|
EXPECT_TRUE(decrypt_config->subsamples().empty());
|
2018-10-04 20:24:21 +00:00
|
|
|
EXPECT_EQ(protection_scheme_, decrypt_config->protection_scheme());
|
|
|
|
EXPECT_EQ(GetExpectedCryptByteBlock(),
|
|
|
|
decrypt_config->crypt_byte_block());
|
|
|
|
EXPECT_EQ(GetExpectedSkipByteBlock(), decrypt_config->skip_byte_block());
|
|
|
|
}
|
2017-03-11 02:48:04 +00:00
|
|
|
EXPECT_FALSE(output_stream_data.back()
|
|
|
|
->segment_info->key_rotation_encryption_config);
|
|
|
|
ClearOutputStreamDataVector();
|
|
|
|
}
|
|
|
|
}
|
2017-02-02 18:28:29 +00:00
|
|
|
|
2017-03-11 02:48:04 +00:00
|
|
|
TEST_P(EncryptionHandlerEncryptionTest, ClearLeadWithKeyRotation) {
|
|
|
|
const double kClearLeadInSeconds = 1.5 * kSegmentDuration / kTimeScale;
|
2017-06-20 23:30:03 +00:00
|
|
|
const int kSegmentsPerCryptoPeriod = 2; // 2 segments.
|
|
|
|
const double kCryptoPeriodDurationInSeconds =
|
|
|
|
kSegmentsPerCryptoPeriod * kSegmentDuration / kTimeScale;
|
2017-07-05 23:47:55 +00:00
|
|
|
EncryptionParams encryption_params;
|
|
|
|
encryption_params.protection_scheme = protection_scheme_;
|
|
|
|
encryption_params.clear_lead_in_seconds = kClearLeadInSeconds;
|
|
|
|
encryption_params.crypto_period_duration_in_seconds =
|
2017-03-11 02:48:04 +00:00
|
|
|
kCryptoPeriodDurationInSeconds;
|
2017-07-05 23:47:55 +00:00
|
|
|
SetUpEncryptionHandler(encryption_params);
|
2017-03-11 02:48:04 +00:00
|
|
|
|
2017-09-12 17:24:24 +00:00
|
|
|
if (IsVideoCodec(codec_)) {
|
|
|
|
ASSERT_OK(Process(StreamData::FromStreamInfo(
|
|
|
|
kStreamIndex, GetVideoStreamInfo(kTimeScale, codec_))));
|
|
|
|
} else {
|
|
|
|
ASSERT_OK(Process(StreamData::FromStreamInfo(
|
|
|
|
kStreamIndex, GetAudioStreamInfo(kTimeScale, codec_))));
|
|
|
|
}
|
|
|
|
|
2018-05-25 17:41:02 +00:00
|
|
|
EXPECT_THAT(
|
|
|
|
GetOutputStreamDataVector(),
|
|
|
|
ElementsAre(IsStreamInfo(kStreamIndex, kTimeScale, kEncrypted, _)));
|
2017-05-09 22:49:01 +00:00
|
|
|
const StreamInfo* stream_info =
|
|
|
|
GetOutputStreamDataVector().back()->stream_info.get();
|
|
|
|
ASSERT_TRUE(stream_info);
|
|
|
|
EXPECT_TRUE(stream_info->has_clear_lead());
|
|
|
|
const EncryptionConfig& encryption_config = stream_info->encryption_config();
|
2017-03-11 02:48:04 +00:00
|
|
|
EXPECT_EQ(protection_scheme_, encryption_config.protection_scheme);
|
|
|
|
EXPECT_EQ(GetExpectedCryptByteBlock(), encryption_config.crypt_byte_block);
|
|
|
|
EXPECT_EQ(GetExpectedSkipByteBlock(), encryption_config.skip_byte_block);
|
|
|
|
EXPECT_EQ(std::vector<uint8_t>(std::begin(kKeyRotationDefaultKeyId),
|
|
|
|
std::end(kKeyRotationDefaultKeyId)),
|
|
|
|
encryption_config.key_id);
|
|
|
|
ClearOutputStreamDataVector();
|
|
|
|
|
2017-06-20 23:30:03 +00:00
|
|
|
// There are five segments with the first two not encrypted.
|
|
|
|
for (int i = 0; i < 5; ++i) {
|
|
|
|
if ((i % kSegmentsPerCryptoPeriod) == 0) {
|
|
|
|
EXPECT_CALL(mock_key_source_,
|
2019-01-24 18:39:54 +00:00
|
|
|
GetCryptoPeriodKey(i / kSegmentsPerCryptoPeriod,
|
|
|
|
kCryptoPeriodDurationInSeconds, _, _))
|
|
|
|
.WillOnce(DoAll(SetArgPointee<3>(GetMockEncryptionKey()),
|
2017-06-20 23:30:03 +00:00
|
|
|
Return(Status::OK)));
|
|
|
|
}
|
2017-03-11 02:48:04 +00:00
|
|
|
// Use single-frame segment for testing.
|
2017-09-12 17:24:24 +00:00
|
|
|
ASSERT_OK(Process(StreamData::FromMediaSample(
|
2017-09-18 23:31:00 +00:00
|
|
|
kStreamIndex, GetMediaSample(i * kSegmentDuration, kSegmentDuration,
|
|
|
|
kIsKeyFrame, kData, kDataSize))));
|
2017-09-12 17:24:24 +00:00
|
|
|
ASSERT_OK(Process(StreamData::FromSegmentInfo(
|
2018-05-25 17:41:02 +00:00
|
|
|
kStreamIndex, GetSegmentInfo(i * kSegmentDuration, kSegmentDuration,
|
|
|
|
!kIsSubsegment))));
|
2017-06-20 23:30:03 +00:00
|
|
|
const bool is_encrypted = i >= 2;
|
2017-03-11 02:48:04 +00:00
|
|
|
const auto& output_stream_data = GetOutputStreamDataVector();
|
|
|
|
EXPECT_THAT(output_stream_data,
|
|
|
|
ElementsAre(IsMediaSample(kStreamIndex, i * kSegmentDuration,
|
2018-06-14 23:39:59 +00:00
|
|
|
kSegmentDuration, is_encrypted, _),
|
2017-03-11 02:48:04 +00:00
|
|
|
IsSegmentInfo(kStreamIndex, i * kSegmentDuration,
|
|
|
|
kSegmentDuration, !kIsSubsegment,
|
|
|
|
is_encrypted)));
|
|
|
|
EXPECT_THAT(*output_stream_data.back()
|
|
|
|
->segment_info->key_rotation_encryption_config,
|
|
|
|
MatchEncryptionConfig(
|
|
|
|
protection_scheme_, GetExpectedCryptByteBlock(),
|
|
|
|
GetExpectedSkipByteBlock(), GetExpectedPerSampleIvSize(),
|
|
|
|
GetExpectedConstantIv(), GetMockEncryptionKey().key_id));
|
|
|
|
Mock::VerifyAndClearExpectations(&mock_key_source_);
|
|
|
|
ClearOutputStreamDataVector();
|
2017-02-02 18:28:29 +00:00
|
|
|
}
|
2017-03-11 02:48:04 +00:00
|
|
|
}
|
|
|
|
|
2018-10-02 23:08:32 +00:00
|
|
|
INSTANTIATE_TEST_CASE_P(ProtectionSchemes,
|
2017-03-11 02:48:04 +00:00
|
|
|
EncryptionHandlerEncryptionTest,
|
2018-10-02 23:08:32 +00:00
|
|
|
Combine(Values(kAppleSampleAesProtectionScheme,
|
|
|
|
FOURCC_cenc,
|
|
|
|
FOURCC_cens,
|
|
|
|
FOURCC_cbc1,
|
|
|
|
FOURCC_cbcs),
|
|
|
|
Values(kCodecAAC, kCodecH264)));
|
|
|
|
|
|
|
|
struct SubsampleTestCase {
|
|
|
|
std::vector<SubsampleEntry> subsamples;
|
|
|
|
std::vector<uint8_t> expected_output;
|
|
|
|
};
|
|
|
|
|
|
|
|
inline bool operator==(const SubsampleEntry& lhs, const SubsampleEntry& rhs) {
|
|
|
|
return lhs.clear_bytes == rhs.clear_bytes &&
|
|
|
|
lhs.cipher_bytes == rhs.cipher_bytes;
|
|
|
|
}
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
const int64_t kSampleDuration = 1000;
|
|
|
|
|
|
|
|
// This mock encryption increases every byte by 0x10. See the function below.
|
|
|
|
const SubsampleTestCase kSubsampleTestCases[] = {
|
|
|
|
{
|
|
|
|
std::vector<SubsampleEntry>(), // No subsamples, i.e. full sample
|
|
|
|
// encrypted.
|
|
|
|
{0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{{8, 2}}, // One subsample.
|
|
|
|
{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x18, 0x19},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{{6, 2}, {2, 0}}, // Two subsamples.
|
|
|
|
{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x16, 0x17, 0x08, 0x09},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{{6, 2}, {0, 2}}, // Two subsamples.
|
|
|
|
{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x16, 0x17, 0x18, 0x19},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
bool MockEncrypt(const uint8_t* text,
|
|
|
|
size_t text_size,
|
|
|
|
uint8_t* crypt_text,
|
|
|
|
size_t* crypt_text_size) {
|
|
|
|
*crypt_text_size = text_size;
|
|
|
|
for (size_t i = 0; i < text_size; i++)
|
|
|
|
crypt_text[i] = text[i] + 0x10;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
class EncryptionHandlerSubsampleTest
|
|
|
|
: public EncryptionHandlerTest,
|
|
|
|
public WithParamInterface<SubsampleTestCase> {};
|
|
|
|
|
|
|
|
INSTANTIATE_TEST_CASE_P(SubsampleTestCases,
|
|
|
|
EncryptionHandlerSubsampleTest,
|
|
|
|
ValuesIn(kSubsampleTestCases));
|
|
|
|
|
|
|
|
TEST_P(EncryptionHandlerSubsampleTest, SubsampleTest) {
|
|
|
|
std::unique_ptr<MockAesCryptor> mock_encryptor(new MockAesCryptor);
|
|
|
|
EXPECT_CALL(*mock_encryptor, CryptInternal(_, _, _, _))
|
|
|
|
.WillRepeatedly(Invoke(MockEncrypt));
|
|
|
|
ASSERT_TRUE(mock_encryptor->SetIv(
|
|
|
|
std::vector<uint8_t>(std::begin(kIv), std::end(kIv))));
|
|
|
|
|
|
|
|
std::unique_ptr<MockAesEncryptorFactory> mock_encryptor_factory(
|
|
|
|
new MockAesEncryptorFactory);
|
|
|
|
EXPECT_CALL(*mock_encryptor_factory, CreateEncryptor(_, _, _, _, _, _))
|
|
|
|
.WillOnce(Return(ByMove(std::move(mock_encryptor))));
|
|
|
|
InjectEncryptorFactoryForTesting(std::move(mock_encryptor_factory));
|
|
|
|
|
|
|
|
InjectSubsamples(GetParam().subsamples);
|
|
|
|
|
|
|
|
EXPECT_CALL(mock_key_source_, GetKey(_, _))
|
|
|
|
.WillOnce(
|
|
|
|
DoAll(SetArgPointee<1>(GetMockEncryptionKey()), Return(Status::OK)));
|
|
|
|
|
|
|
|
ASSERT_OK(Process(StreamData::FromStreamInfo(
|
|
|
|
kStreamIndex, GetVideoStreamInfo(kTimeScale, kCodecH264))));
|
|
|
|
ASSERT_OK(Process(StreamData::FromMediaSample(
|
|
|
|
kStreamIndex,
|
|
|
|
GetMediaSample(0, kSampleDuration, kIsKeyFrame, kData, kDataSize))));
|
|
|
|
|
|
|
|
const auto& output_stream_data = GetOutputStreamDataVector();
|
|
|
|
EXPECT_THAT(output_stream_data,
|
|
|
|
ElementsAre(IsStreamInfo(kStreamIndex, kTimeScale, kEncrypted, _),
|
|
|
|
IsMediaSample(kStreamIndex, 0, kSampleDuration,
|
|
|
|
kEncrypted, _)));
|
|
|
|
|
|
|
|
const MediaSample& sample = *output_stream_data.back()->media_sample;
|
|
|
|
EXPECT_EQ(
|
|
|
|
GetParam().expected_output,
|
|
|
|
std::vector<uint8_t>(sample.data(), sample.data() + sample.data_size()));
|
|
|
|
|
|
|
|
const DecryptConfig& decrypt_config = *sample.decrypt_config();
|
|
|
|
EXPECT_EQ(GetParam().subsamples, decrypt_config.subsamples());
|
|
|
|
}
|
2017-03-11 02:48:04 +00:00
|
|
|
|
2018-10-04 20:24:21 +00:00
|
|
|
class EncryptionHandlerTrackTypeTest : public EncryptionHandlerTest {};
|
2017-03-11 02:48:04 +00:00
|
|
|
|
|
|
|
TEST_F(EncryptionHandlerTrackTypeTest, AudioTrackType) {
|
2017-05-22 20:31:41 +00:00
|
|
|
EncryptionParams::EncryptedStreamAttributes captured_stream_attributes;
|
2017-07-05 23:47:55 +00:00
|
|
|
EncryptionParams encryption_params;
|
|
|
|
encryption_params.stream_label_func =
|
2017-05-22 20:31:41 +00:00
|
|
|
[&captured_stream_attributes](
|
|
|
|
const EncryptionParams::EncryptedStreamAttributes&
|
|
|
|
stream_attributes) {
|
|
|
|
captured_stream_attributes = stream_attributes;
|
|
|
|
return kAudioStreamLabel;
|
|
|
|
};
|
2017-07-05 23:47:55 +00:00
|
|
|
SetUpEncryptionHandler(encryption_params);
|
2017-06-13 21:54:12 +00:00
|
|
|
EXPECT_CALL(mock_key_source_, GetKey(kAudioStreamLabel, _))
|
2017-03-11 02:48:04 +00:00
|
|
|
.WillOnce(
|
|
|
|
DoAll(SetArgPointee<1>(GetMockEncryptionKey()), Return(Status::OK)));
|
2017-09-12 17:24:24 +00:00
|
|
|
ASSERT_OK(Process(StreamData::FromStreamInfo(
|
2018-05-25 17:41:02 +00:00
|
|
|
kStreamIndex, GetAudioStreamInfo(kTimeScale))));
|
2017-05-22 20:31:41 +00:00
|
|
|
EXPECT_EQ(EncryptionParams::EncryptedStreamAttributes::kAudio,
|
|
|
|
captured_stream_attributes.stream_type);
|
2017-03-11 02:48:04 +00:00
|
|
|
}
|
|
|
|
|
2017-05-22 20:31:41 +00:00
|
|
|
TEST_F(EncryptionHandlerTrackTypeTest, VideoTrackType) {
|
2017-09-12 17:24:24 +00:00
|
|
|
const int32_t kWidth = 12;
|
|
|
|
const int32_t kHeight = 34;
|
2017-05-22 20:31:41 +00:00
|
|
|
EncryptionParams::EncryptedStreamAttributes captured_stream_attributes;
|
2017-07-05 23:47:55 +00:00
|
|
|
EncryptionParams encryption_params;
|
|
|
|
encryption_params.stream_label_func =
|
2017-05-22 20:31:41 +00:00
|
|
|
[&captured_stream_attributes](
|
|
|
|
const EncryptionParams::EncryptedStreamAttributes&
|
|
|
|
stream_attributes) {
|
|
|
|
captured_stream_attributes = stream_attributes;
|
|
|
|
return kSdVideoStreamLabel;
|
|
|
|
};
|
2017-07-05 23:47:55 +00:00
|
|
|
SetUpEncryptionHandler(encryption_params);
|
2017-06-13 21:54:12 +00:00
|
|
|
EXPECT_CALL(mock_key_source_, GetKey(kSdVideoStreamLabel, _))
|
2017-03-11 02:48:04 +00:00
|
|
|
.WillOnce(
|
|
|
|
DoAll(SetArgPointee<1>(GetMockEncryptionKey()), Return(Status::OK)));
|
2017-09-12 17:24:24 +00:00
|
|
|
ASSERT_OK(Process(StreamData::FromStreamInfo(
|
2018-05-25 17:41:02 +00:00
|
|
|
kStreamIndex, GetVideoStreamInfo(kTimeScale, kWidth, kHeight))));
|
2017-05-22 20:31:41 +00:00
|
|
|
EXPECT_EQ(EncryptionParams::EncryptedStreamAttributes::kVideo,
|
|
|
|
captured_stream_attributes.stream_type);
|
2017-09-12 17:24:24 +00:00
|
|
|
EXPECT_EQ(captured_stream_attributes.oneof.video.width, kWidth);
|
|
|
|
EXPECT_EQ(captured_stream_attributes.oneof.video.height, kHeight);
|
2017-03-11 02:48:04 +00:00
|
|
|
}
|
2017-02-02 18:28:29 +00:00
|
|
|
|
|
|
|
} // namespace media
|
|
|
|
} // namespace shaka
|