Implement EncryptionHandler
Change-Id: Iabedf3b02057d6124d5393ae9618490e5595ad89
This commit is contained in:
parent
7b639f8e56
commit
7a90ee70ab
|
@ -31,5 +31,12 @@ DecryptConfig::DecryptConfig(const std::vector<uint8_t>& key_id,
|
||||||
|
|
||||||
DecryptConfig::~DecryptConfig() {}
|
DecryptConfig::~DecryptConfig() {}
|
||||||
|
|
||||||
|
size_t DecryptConfig::GetTotalSizeOfSubsamples() const {
|
||||||
|
size_t size = 0;
|
||||||
|
for (const SubsampleEntry& subsample : subsamples_)
|
||||||
|
size += subsample.clear_bytes + subsample.cipher_bytes;
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace media
|
} // namespace media
|
||||||
} // namespace shaka
|
} // namespace shaka
|
||||||
|
|
|
@ -73,6 +73,17 @@ class DecryptConfig {
|
||||||
|
|
||||||
~DecryptConfig();
|
~DecryptConfig();
|
||||||
|
|
||||||
|
/// @param clear_bytes is the size of clear bytes in the subsample to be
|
||||||
|
/// added.
|
||||||
|
/// @param cipher_bytes is the size of cipher bytes in the subsample to be
|
||||||
|
/// added.
|
||||||
|
void AddSubsample(uint16_t clear_bytes, uint32_t cipher_bytes) {
|
||||||
|
subsamples_.emplace_back(clear_bytes, cipher_bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @return The total size of subsamples.
|
||||||
|
size_t GetTotalSizeOfSubsamples() const;
|
||||||
|
|
||||||
const std::vector<uint8_t>& key_id() const { return key_id_; }
|
const std::vector<uint8_t>& key_id() const { return key_id_; }
|
||||||
const std::vector<uint8_t>& iv() const { return iv_; }
|
const std::vector<uint8_t>& iv() const { return iv_; }
|
||||||
const std::vector<SubsampleEntry>& subsamples() const { return subsamples_; }
|
const std::vector<SubsampleEntry>& subsamples() const { return subsamples_; }
|
||||||
|
@ -88,7 +99,7 @@ class DecryptConfig {
|
||||||
|
|
||||||
// Subsample information. May be empty for some formats, meaning entire frame
|
// Subsample information. May be empty for some formats, meaning entire frame
|
||||||
// (less data ignored by data_offset_) is encrypted.
|
// (less data ignored by data_offset_) is encrypted.
|
||||||
const std::vector<SubsampleEntry> subsamples_;
|
std::vector<SubsampleEntry> subsamples_;
|
||||||
|
|
||||||
const FourCC protection_scheme_;
|
const FourCC protection_scheme_;
|
||||||
// For pattern-based protection schemes, like CENS and CBCS.
|
// For pattern-based protection schemes, like CENS and CBCS.
|
||||||
|
|
|
@ -23,6 +23,21 @@ Status MediaHandler::SetHandler(int output_stream_index,
|
||||||
return Status::OK;
|
return Status::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Status MediaHandler::Initialize() {
|
||||||
|
if (initialized_)
|
||||||
|
return Status::OK;
|
||||||
|
Status status = InitializeInternal();
|
||||||
|
if (!status.ok())
|
||||||
|
return status;
|
||||||
|
for (auto& pair : output_handlers_) {
|
||||||
|
status = pair.second.first->Initialize();
|
||||||
|
if (!status.ok())
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
initialized_ = true;
|
||||||
|
return Status::OK;
|
||||||
|
}
|
||||||
|
|
||||||
Status MediaHandler::FlushStream(int input_stream_index) {
|
Status MediaHandler::FlushStream(int input_stream_index) {
|
||||||
// The default implementation treats the output stream index to be identical
|
// The default implementation treats the output stream index to be identical
|
||||||
// to the input stream index, which is true for most handlers.
|
// to the input stream index, which is true for most handlers.
|
||||||
|
|
|
@ -11,11 +11,47 @@
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
#include "packager/media/base/media_sample.h"
|
||||||
#include "packager/media/base/status.h"
|
#include "packager/media/base/status.h"
|
||||||
|
#include "packager/media/base/stream_info.h"
|
||||||
|
|
||||||
namespace shaka {
|
namespace shaka {
|
||||||
namespace media {
|
namespace media {
|
||||||
|
|
||||||
|
enum class StreamDataType {
|
||||||
|
kUnknown,
|
||||||
|
kPeriodInfo,
|
||||||
|
kStreamInfo,
|
||||||
|
kEncryptionConfig,
|
||||||
|
kMediaSample,
|
||||||
|
kMediaEvent,
|
||||||
|
kSegmentInfo,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO(kqyang): Define these structures.
|
||||||
|
struct PeriodInfo {};
|
||||||
|
struct EncryptionConfig {};
|
||||||
|
struct MediaEvent {};
|
||||||
|
struct SegmentInfo {
|
||||||
|
bool is_subsegment = false;
|
||||||
|
bool is_encrypted = false;
|
||||||
|
uint64_t start_timestamp = 0;
|
||||||
|
uint64_t duration = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO(kqyang): Should we use protobuf?
|
||||||
|
struct StreamData {
|
||||||
|
int stream_index;
|
||||||
|
StreamDataType stream_data_type;
|
||||||
|
|
||||||
|
std::unique_ptr<PeriodInfo> period_info;
|
||||||
|
std::unique_ptr<StreamInfo> stream_info;
|
||||||
|
std::unique_ptr<EncryptionConfig> encryption_config;
|
||||||
|
std::unique_ptr<MediaSample> media_sample;
|
||||||
|
std::unique_ptr<MediaEvent> media_event;
|
||||||
|
std::unique_ptr<SegmentInfo> segment_info;
|
||||||
|
};
|
||||||
|
|
||||||
/// MediaHandler is the base media processing unit. Media handlers transform
|
/// MediaHandler is the base media processing unit. Media handlers transform
|
||||||
/// the input streams and propagate the outputs to downstream media handlers.
|
/// the input streams and propagate the outputs to downstream media handlers.
|
||||||
/// There are three different types of media handlers:
|
/// There are three different types of media handlers:
|
||||||
|
@ -45,37 +81,14 @@ class MediaHandler {
|
||||||
return SetHandler(next_output_stream_index_, handler);
|
return SetHandler(next_output_stream_index_, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Initialize the handler and downstream handlers. Note that it should be
|
||||||
|
/// called after setting up the graph before running the graph.
|
||||||
|
Status Initialize();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
enum class StreamDataType {
|
/// Internal implementation of initialize. Note that it should only initialize
|
||||||
kUnknown,
|
/// the MediaHandler itself. Downstream handlers are handled in Initialize().
|
||||||
kPeriodInfo,
|
virtual Status InitializeInternal() = 0;
|
||||||
kStreamInfo,
|
|
||||||
kEncryptionConfig,
|
|
||||||
kMediaSample,
|
|
||||||
kMediaEvent,
|
|
||||||
kSegmentInfo,
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO(kqyang): Define these structures.
|
|
||||||
struct PeriodInfo {};
|
|
||||||
struct StreamInfo {};
|
|
||||||
struct EncryptionConfig {};
|
|
||||||
struct MediaSample {};
|
|
||||||
struct MediaEvent {};
|
|
||||||
struct SegmentInfo {};
|
|
||||||
|
|
||||||
// TODO(kqyang): Should we use protobuf?
|
|
||||||
struct StreamData {
|
|
||||||
int stream_index;
|
|
||||||
StreamDataType stream_data_type;
|
|
||||||
|
|
||||||
std::unique_ptr<PeriodInfo> period_info;
|
|
||||||
std::unique_ptr<StreamInfo> stream_info;
|
|
||||||
std::unique_ptr<EncryptionConfig> encryption_config;
|
|
||||||
std::unique_ptr<MediaSample> media_sample;
|
|
||||||
std::unique_ptr<MediaEvent> media_event;
|
|
||||||
std::unique_ptr<SegmentInfo> segment_info;
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Process the incoming stream data. Note that (1) stream_data.stream_index
|
/// Process the incoming stream data. Note that (1) stream_data.stream_index
|
||||||
/// should be the input stream index; (2) The implementation needs to call
|
/// should be the input stream index; (2) The implementation needs to call
|
||||||
|
@ -89,6 +102,9 @@ class MediaHandler {
|
||||||
/// Validate if the stream at the specified index actually exists.
|
/// Validate if the stream at the specified index actually exists.
|
||||||
virtual bool ValidateOutputStreamIndex(int stream_index) const;
|
virtual bool ValidateOutputStreamIndex(int stream_index) const;
|
||||||
|
|
||||||
|
bool initialized() { return initialized_; }
|
||||||
|
int num_input_streams() { return num_input_streams_; }
|
||||||
|
|
||||||
/// Dispatch the stream data to downstream handlers. Note that
|
/// Dispatch the stream data to downstream handlers. Note that
|
||||||
/// stream_data.stream_index should be the output stream index.
|
/// stream_data.stream_index should be the output stream index.
|
||||||
Status Dispatch(std::unique_ptr<StreamData> stream_data);
|
Status Dispatch(std::unique_ptr<StreamData> stream_data);
|
||||||
|
@ -149,11 +165,13 @@ class MediaHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
int num_input_streams() const { return num_input_streams_; }
|
int num_input_streams() const { return num_input_streams_; }
|
||||||
|
int next_output_stream_index() const { return next_output_stream_index_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
MediaHandler(const MediaHandler&) = delete;
|
MediaHandler(const MediaHandler&) = delete;
|
||||||
MediaHandler& operator=(const MediaHandler&) = delete;
|
MediaHandler& operator=(const MediaHandler&) = delete;
|
||||||
|
|
||||||
|
bool initialized_ = false;
|
||||||
// Number of input streams.
|
// Number of input streams.
|
||||||
int num_input_streams_ = 0;
|
int num_input_streams_ = 0;
|
||||||
// The next available output stream index, used by AddHandler.
|
// The next available output stream index, used by AddHandler.
|
||||||
|
|
|
@ -48,6 +48,9 @@ enum Code {
|
||||||
// Unable to parse the media file.
|
// Unable to parse the media file.
|
||||||
PARSER_FAILURE,
|
PARSER_FAILURE,
|
||||||
|
|
||||||
|
// Failed to do the encryption.
|
||||||
|
ENCRYPTION_FAILURE,
|
||||||
|
|
||||||
// Fail to mux the media file.
|
// Fail to mux the media file.
|
||||||
MUXER_FAILURE,
|
MUXER_FAILURE,
|
||||||
|
|
||||||
|
|
|
@ -81,6 +81,7 @@ class StreamInfo {
|
||||||
codec_string_ = codec_string;
|
codec_string_ = codec_string;
|
||||||
}
|
}
|
||||||
void set_language(const std::string& language) { language_ = language; }
|
void set_language(const std::string& language) { language_ = language; }
|
||||||
|
void set_is_encrypted(bool is_encrypted) { is_encrypted_ = is_encrypted; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Whether the stream is Audio or Video.
|
// Whether the stream is Audio or Video.
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|
{
|
||||||
|
'includes': [
|
||||||
|
'../../common.gypi',
|
||||||
|
],
|
||||||
|
'targets': [
|
||||||
|
{
|
||||||
|
'target_name': 'crypto',
|
||||||
|
'type': '<(component)',
|
||||||
|
'sources': [
|
||||||
|
'encryption_handler.cc',
|
||||||
|
'encryption_handler.h',
|
||||||
|
],
|
||||||
|
'dependencies': [
|
||||||
|
'../base/media_base.gyp:media_base',
|
||||||
|
'../codecs/codecs.gyp:codecs',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'target_name': 'crypto_unittest',
|
||||||
|
'type': '<(gtest_target_type)',
|
||||||
|
'sources': [
|
||||||
|
'encryption_handler_unittest.cc',
|
||||||
|
],
|
||||||
|
'dependencies': [
|
||||||
|
'../../testing/gtest.gyp:gtest',
|
||||||
|
'../../testing/gmock.gyp:gmock',
|
||||||
|
'../test/media_test.gyp:media_test_support',
|
||||||
|
'crypto',
|
||||||
|
]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,418 @@
|
||||||
|
// 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 <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
#include "packager/media/base/aes_encryptor.h"
|
||||||
|
#include "packager/media/base/aes_pattern_cryptor.h"
|
||||||
|
#include "packager/media/base/key_source.h"
|
||||||
|
#include "packager/media/base/video_stream_info.h"
|
||||||
|
#include "packager/media/codecs/video_slice_header_parser.h"
|
||||||
|
#include "packager/media/codecs/vp8_parser.h"
|
||||||
|
#include "packager/media/codecs/vp9_parser.h"
|
||||||
|
|
||||||
|
namespace shaka {
|
||||||
|
namespace media {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
const size_t kCencBlockSize = 16u;
|
||||||
|
|
||||||
|
// Adds one or more subsamples to |*subsamples|. This may add more than one
|
||||||
|
// if one of the values overflows the integer in the subsample.
|
||||||
|
void AddSubsample(uint64_t clear_bytes,
|
||||||
|
uint64_t cipher_bytes,
|
||||||
|
DecryptConfig* decrypt_config) {
|
||||||
|
CHECK_LT(cipher_bytes, std::numeric_limits<uint32_t>::max());
|
||||||
|
const uint64_t kUInt16Max = std::numeric_limits<uint16_t>::max();
|
||||||
|
while (clear_bytes > kUInt16Max) {
|
||||||
|
decrypt_config->AddSubsample(kUInt16Max, 0);
|
||||||
|
clear_bytes -= kUInt16Max;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clear_bytes > 0 || cipher_bytes > 0)
|
||||||
|
decrypt_config->AddSubsample(clear_bytes, cipher_bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
Codec GetVideoCodec(const StreamInfo& stream_info) {
|
||||||
|
if (stream_info.stream_type() != kStreamVideo) return kUnknownCodec;
|
||||||
|
const VideoStreamInfo& video_stream_info =
|
||||||
|
static_cast<const VideoStreamInfo&>(stream_info);
|
||||||
|
return video_stream_info.codec();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t GetNaluLengthSize(const StreamInfo& stream_info) {
|
||||||
|
if (stream_info.stream_type() != kStreamVideo)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
const VideoStreamInfo& video_stream_info =
|
||||||
|
static_cast<const VideoStreamInfo&>(stream_info);
|
||||||
|
return video_stream_info.nalu_length_size();
|
||||||
|
}
|
||||||
|
|
||||||
|
KeySource::TrackType GetTrackTypeForEncryption(const StreamInfo& stream_info,
|
||||||
|
uint32_t max_sd_pixels,
|
||||||
|
uint32_t max_hd_pixels,
|
||||||
|
uint32_t max_uhd1_pixels) {
|
||||||
|
if (stream_info.stream_type() == kStreamAudio)
|
||||||
|
return KeySource::TRACK_TYPE_AUDIO;
|
||||||
|
|
||||||
|
if (stream_info.stream_type() != kStreamVideo)
|
||||||
|
return KeySource::TRACK_TYPE_UNKNOWN;
|
||||||
|
|
||||||
|
DCHECK_EQ(kStreamVideo, stream_info.stream_type());
|
||||||
|
const VideoStreamInfo& video_stream_info =
|
||||||
|
static_cast<const VideoStreamInfo&>(stream_info);
|
||||||
|
uint32_t pixels = video_stream_info.width() * video_stream_info.height();
|
||||||
|
if (pixels <= max_sd_pixels) {
|
||||||
|
return KeySource::TRACK_TYPE_SD;
|
||||||
|
} else if (pixels <= max_hd_pixels) {
|
||||||
|
return KeySource::TRACK_TYPE_HD;
|
||||||
|
} else if (pixels <= max_uhd1_pixels) {
|
||||||
|
return KeySource::TRACK_TYPE_UHD1;
|
||||||
|
}
|
||||||
|
return KeySource::TRACK_TYPE_UHD2;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
EncryptionHandler::EncryptionHandler(
|
||||||
|
const EncryptionOptions& encryption_options,
|
||||||
|
KeySource* key_source)
|
||||||
|
: encryption_options_(encryption_options), key_source_(key_source) {}
|
||||||
|
|
||||||
|
EncryptionHandler::~EncryptionHandler() {}
|
||||||
|
|
||||||
|
Status EncryptionHandler::InitializeInternal() {
|
||||||
|
if (num_input_streams() != 1 || next_output_stream_index() != 1) {
|
||||||
|
return Status(error::INVALID_ARGUMENT,
|
||||||
|
"Expects exactly one input and output.");
|
||||||
|
}
|
||||||
|
return Status::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
Status EncryptionHandler::Process(std::unique_ptr<StreamData> stream_data) {
|
||||||
|
Status status;
|
||||||
|
switch (stream_data->stream_data_type) {
|
||||||
|
case StreamDataType::kStreamInfo:
|
||||||
|
status = ProcessStreamInfo(stream_data->stream_info.get());
|
||||||
|
break;
|
||||||
|
case StreamDataType::kSegmentInfo:
|
||||||
|
new_segment_ = true;
|
||||||
|
if (remaining_clear_lead_ > 0)
|
||||||
|
remaining_clear_lead_ -= stream_data->segment_info->duration;
|
||||||
|
else
|
||||||
|
stream_data->segment_info->is_encrypted = true;
|
||||||
|
break;
|
||||||
|
case StreamDataType::kMediaSample:
|
||||||
|
status = ProcessMediaSample(stream_data->media_sample.get());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
VLOG(3) << "Stream data type "
|
||||||
|
<< static_cast<int>(stream_data->stream_data_type) << " ignored.";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return status.ok() ? Dispatch(std::move(stream_data)) : status;
|
||||||
|
}
|
||||||
|
|
||||||
|
Status EncryptionHandler::ProcessStreamInfo(StreamInfo* stream_info) {
|
||||||
|
if (stream_info->is_encrypted()) {
|
||||||
|
return Status(error::INVALID_ARGUMENT,
|
||||||
|
"Input stream is already encrypted.");
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining_clear_lead_ =
|
||||||
|
encryption_options_.clear_lead_in_seconds * stream_info->time_scale();
|
||||||
|
crypto_period_duration_ =
|
||||||
|
encryption_options_.crypto_period_duration_in_seconds *
|
||||||
|
stream_info->time_scale();
|
||||||
|
nalu_length_size_ = GetNaluLengthSize(*stream_info);
|
||||||
|
video_codec_ = GetVideoCodec(*stream_info);
|
||||||
|
track_type_ = GetTrackTypeForEncryption(
|
||||||
|
*stream_info, encryption_options_.max_sd_pixels,
|
||||||
|
encryption_options_.max_hd_pixels, encryption_options_.max_uhd1_pixels);
|
||||||
|
switch (video_codec_) {
|
||||||
|
case kCodecVP8:
|
||||||
|
vpx_parser_.reset(new VP8Parser);
|
||||||
|
break;
|
||||||
|
case kCodecVP9:
|
||||||
|
vpx_parser_.reset(new VP9Parser);
|
||||||
|
break;
|
||||||
|
case kCodecH264:
|
||||||
|
header_parser_.reset(new H264VideoSliceHeaderParser);
|
||||||
|
break;
|
||||||
|
case kCodecHVC1:
|
||||||
|
FALLTHROUGH_INTENDED;
|
||||||
|
case kCodecHEV1:
|
||||||
|
header_parser_.reset(new H265VideoSliceHeaderParser);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Expect an audio codec with nalu length size == 0.
|
||||||
|
if (nalu_length_size_ > 0) {
|
||||||
|
LOG(WARNING) << "Unknown video codec '" << video_codec_ << "'";
|
||||||
|
return Status(error::ENCRYPTION_FAILURE, "Unknown video codec.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (header_parser_ &&
|
||||||
|
!header_parser_->Initialize(stream_info->codec_config())) {
|
||||||
|
return Status(error::ENCRYPTION_FAILURE, "Fail to read SPS and PPS data.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up protection pattern.
|
||||||
|
if (encryption_options_.protection_scheme == FOURCC_cbcs ||
|
||||||
|
encryption_options_.protection_scheme == FOURCC_cens) {
|
||||||
|
if (stream_info->stream_type() == kStreamVideo) {
|
||||||
|
// Use 1:9 pattern for video.
|
||||||
|
crypt_byte_block_ = 1u;
|
||||||
|
skip_byte_block_ = 9u;
|
||||||
|
} else {
|
||||||
|
// Tracks other than video are protected using whole-block full-sample
|
||||||
|
// encryption, which is essentially a pattern of 1:0. Note that this may
|
||||||
|
// not be the same as the non-pattern based encryption counterparts, e.g.
|
||||||
|
// in 'cens' for full sample encryption, the whole sample is encrypted up
|
||||||
|
// to the last 16-byte boundary, see 23001-7:2016(E) 9.7; while in 'cenc'
|
||||||
|
// for full sample encryption, the last partial 16-byte block is also
|
||||||
|
// encrypted, see 23001-7:2016(E) 9.4.2. Another difference is the use of
|
||||||
|
// constant iv.
|
||||||
|
crypt_byte_block_ = 1u;
|
||||||
|
skip_byte_block_ = 0u;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not using pattern encryption.
|
||||||
|
crypt_byte_block_ = 0u;
|
||||||
|
skip_byte_block_ = 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream_info->set_is_encrypted(true);
|
||||||
|
return Status::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
Status EncryptionHandler::ProcessMediaSample(MediaSample* sample) {
|
||||||
|
// We need to parse the frame (which also updates the vpx parser) even if the
|
||||||
|
// frame is not encrypted as the next (encrypted) frame may be dependent on
|
||||||
|
// this clear frame.
|
||||||
|
std::vector<VPxFrameInfo> vpx_frames;
|
||||||
|
if (vpx_parser_ &&
|
||||||
|
!vpx_parser_->Parse(sample->data(), sample->data_size(), &vpx_frames)) {
|
||||||
|
return Status(error::ENCRYPTION_FAILURE, "Failed to parse vpx frame.");
|
||||||
|
}
|
||||||
|
if (remaining_clear_lead_ > 0)
|
||||||
|
return Status::OK;
|
||||||
|
|
||||||
|
Status status;
|
||||||
|
if (new_segment_) {
|
||||||
|
EncryptionKey encryption_key;
|
||||||
|
bool create_encryptor = false;
|
||||||
|
if (crypto_period_duration_ != 0) {
|
||||||
|
const int64_t current_crypto_period_index =
|
||||||
|
sample->dts() / crypto_period_duration_;
|
||||||
|
if (current_crypto_period_index != prev_crypto_period_index_) {
|
||||||
|
status = key_source_->GetCryptoPeriodKey(current_crypto_period_index,
|
||||||
|
track_type_, &encryption_key);
|
||||||
|
if (!status.ok())
|
||||||
|
return status;
|
||||||
|
create_encryptor = true;
|
||||||
|
}
|
||||||
|
} else if (!encryptor_) {
|
||||||
|
status = key_source_->GetKey(track_type_, &encryption_key);
|
||||||
|
if (!status.ok())
|
||||||
|
return status;
|
||||||
|
create_encryptor = true;
|
||||||
|
}
|
||||||
|
if (create_encryptor && !CreateEncryptor(&encryption_key))
|
||||||
|
return Status(error::ENCRYPTION_FAILURE, "Failed to create encryptor");
|
||||||
|
new_segment_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<DecryptConfig> decrypt_config(new DecryptConfig(
|
||||||
|
key_id_, encryptor_->iv(), std::vector<SubsampleEntry>(),
|
||||||
|
encryption_options_.protection_scheme, crypt_byte_block_,
|
||||||
|
skip_byte_block_));
|
||||||
|
if (vpx_parser_) {
|
||||||
|
if (!EncryptVpxFrame(vpx_frames, sample, decrypt_config.get()))
|
||||||
|
return Status(error::ENCRYPTION_FAILURE, "Failed to encrypt VPx frames.");
|
||||||
|
DCHECK_EQ(decrypt_config->GetTotalSizeOfSubsamples(), sample->data_size());
|
||||||
|
} else if (nalu_length_size_ > 0) {
|
||||||
|
if (!EncryptNalFrame(sample, decrypt_config.get())) {
|
||||||
|
return Status(error::ENCRYPTION_FAILURE,
|
||||||
|
"Failed to encrypt video frames.");
|
||||||
|
}
|
||||||
|
DCHECK_EQ(decrypt_config->GetTotalSizeOfSubsamples(), sample->data_size());
|
||||||
|
} else {
|
||||||
|
DCHECK_LE(crypt_byte_block_, 1u);
|
||||||
|
DCHECK_EQ(skip_byte_block_, 0u);
|
||||||
|
EncryptBytes(sample->writable_data(), sample->data_size());
|
||||||
|
}
|
||||||
|
sample->set_decrypt_config(std::move(decrypt_config));
|
||||||
|
encryptor_->UpdateIv();
|
||||||
|
return Status::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EncryptionHandler::CreateEncryptor(EncryptionKey* encryption_key) {
|
||||||
|
std::unique_ptr<AesCryptor> encryptor;
|
||||||
|
switch (encryption_options_.protection_scheme) {
|
||||||
|
case FOURCC_cenc:
|
||||||
|
encryptor.reset(new AesCtrEncryptor);
|
||||||
|
break;
|
||||||
|
case FOURCC_cbc1:
|
||||||
|
encryptor.reset(new AesCbcEncryptor(kNoPadding));
|
||||||
|
break;
|
||||||
|
case FOURCC_cens:
|
||||||
|
encryptor.reset(new AesPatternCryptor(
|
||||||
|
crypt_byte_block_, skip_byte_block_,
|
||||||
|
AesPatternCryptor::kEncryptIfCryptByteBlockRemaining,
|
||||||
|
AesCryptor::kDontUseConstantIv,
|
||||||
|
std::unique_ptr<AesCryptor>(new AesCtrEncryptor())));
|
||||||
|
break;
|
||||||
|
case FOURCC_cbcs:
|
||||||
|
encryptor.reset(new AesPatternCryptor(
|
||||||
|
crypt_byte_block_, skip_byte_block_,
|
||||||
|
AesPatternCryptor::kEncryptIfCryptByteBlockRemaining,
|
||||||
|
AesCryptor::kUseConstantIv,
|
||||||
|
std::unique_ptr<AesCryptor>(new AesCbcEncryptor(kNoPadding))));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LOG(ERROR) << "Unsupported protection scheme.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encryption_key->iv.empty()) {
|
||||||
|
if (!AesCryptor::GenerateRandomIv(encryption_options_.protection_scheme,
|
||||||
|
&encryption_key->iv)) {
|
||||||
|
LOG(ERROR) << "Failed to generate random iv.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const bool initialized =
|
||||||
|
encryptor->InitializeWithIv(encryption_key->key, encryption_key->iv);
|
||||||
|
encryptor_ = std::move(encryptor);
|
||||||
|
key_id_ = encryption_key->key_id;
|
||||||
|
return initialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EncryptionHandler::EncryptVpxFrame(const std::vector<VPxFrameInfo>& vpx_frames,
|
||||||
|
MediaSample* sample,
|
||||||
|
DecryptConfig* decrypt_config) {
|
||||||
|
uint8_t* data = sample->writable_data();
|
||||||
|
const bool is_superframe = vpx_frames.size() > 1;
|
||||||
|
for (const VPxFrameInfo& frame : vpx_frames) {
|
||||||
|
uint16_t clear_bytes =
|
||||||
|
static_cast<uint16_t>(frame.uncompressed_header_size);
|
||||||
|
uint32_t cipher_bytes = static_cast<uint32_t>(
|
||||||
|
frame.frame_size - frame.uncompressed_header_size);
|
||||||
|
|
||||||
|
// "VP Codec ISO Media File Format Binding" document requires that the
|
||||||
|
// encrypted bytes of each frame within the superframe must be block
|
||||||
|
// aligned so that the counter state can be computed for each frame
|
||||||
|
// within the superframe.
|
||||||
|
// ISO/IEC 23001-7:2016 10.2 'cbc1' 10.3 'cens'
|
||||||
|
// The BytesOfProtectedData size SHALL be a multiple of 16 bytes to
|
||||||
|
// avoid partial blocks in Subsamples.
|
||||||
|
if (is_superframe || encryption_options_.protection_scheme == FOURCC_cbc1 ||
|
||||||
|
encryption_options_.protection_scheme == FOURCC_cens) {
|
||||||
|
const uint16_t misalign_bytes = cipher_bytes % kCencBlockSize;
|
||||||
|
clear_bytes += misalign_bytes;
|
||||||
|
cipher_bytes -= misalign_bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
decrypt_config->AddSubsample(clear_bytes, cipher_bytes);
|
||||||
|
if (cipher_bytes > 0)
|
||||||
|
EncryptBytes(data + clear_bytes, cipher_bytes);
|
||||||
|
data += frame.frame_size;
|
||||||
|
}
|
||||||
|
// Add subsample for the superframe index if exists.
|
||||||
|
if (is_superframe) {
|
||||||
|
size_t index_size = sample->data() + sample->data_size() - data;
|
||||||
|
DCHECK_LE(index_size, 2 + vpx_frames.size() * 4);
|
||||||
|
DCHECK_GE(index_size, 2 + vpx_frames.size() * 1);
|
||||||
|
uint16_t clear_bytes = static_cast<uint16_t>(index_size);
|
||||||
|
uint32_t cipher_bytes = 0;
|
||||||
|
decrypt_config->AddSubsample(clear_bytes, cipher_bytes);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EncryptionHandler::EncryptNalFrame(MediaSample* sample,
|
||||||
|
DecryptConfig* decrypt_config) {
|
||||||
|
const Nalu::CodecType nalu_type =
|
||||||
|
(video_codec_ == kCodecHVC1 || video_codec_ == kCodecHEV1) ? Nalu::kH265
|
||||||
|
: Nalu::kH264;
|
||||||
|
NaluReader reader(nalu_type, nalu_length_size_, sample->writable_data(),
|
||||||
|
sample->data_size());
|
||||||
|
|
||||||
|
// Store the current length of clear data. This is used to squash
|
||||||
|
// multiple unencrypted NAL units into fewer subsample entries.
|
||||||
|
uint64_t accumulated_clear_bytes = 0;
|
||||||
|
|
||||||
|
Nalu nalu;
|
||||||
|
NaluReader::Result result;
|
||||||
|
while ((result = reader.Advance(&nalu)) == NaluReader::kOk) {
|
||||||
|
if (nalu.is_video_slice()) {
|
||||||
|
// For video-slice NAL units, encrypt the video slice. This skips
|
||||||
|
// the frame header. If this is an unrecognized codec, the whole NAL unit
|
||||||
|
// will be encrypted.
|
||||||
|
const int64_t video_slice_header_size =
|
||||||
|
header_parser_ ? header_parser_->GetHeaderSize(nalu) : 0;
|
||||||
|
if (video_slice_header_size < 0) {
|
||||||
|
LOG(ERROR) << "Failed to read slice header.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t current_clear_bytes =
|
||||||
|
nalu.header_size() + video_slice_header_size;
|
||||||
|
uint64_t cipher_bytes = nalu.payload_size() - video_slice_header_size;
|
||||||
|
|
||||||
|
// ISO/IEC 23001-7:2016 10.2 'cbc1' 10.3 'cens'
|
||||||
|
// The BytesOfProtectedData size SHALL be a multiple of 16 bytes to
|
||||||
|
// avoid partial blocks in Subsamples.
|
||||||
|
if (encryption_options_.protection_scheme == FOURCC_cbc1 ||
|
||||||
|
encryption_options_.protection_scheme == FOURCC_cens) {
|
||||||
|
const uint16_t misalign_bytes = cipher_bytes % kCencBlockSize;
|
||||||
|
current_clear_bytes += misalign_bytes;
|
||||||
|
cipher_bytes -= misalign_bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t* nalu_data = nalu.data() + current_clear_bytes;
|
||||||
|
EncryptBytes(const_cast<uint8_t*>(nalu_data), cipher_bytes);
|
||||||
|
|
||||||
|
AddSubsample(
|
||||||
|
accumulated_clear_bytes + nalu_length_size_ + current_clear_bytes,
|
||||||
|
cipher_bytes, decrypt_config);
|
||||||
|
accumulated_clear_bytes = 0;
|
||||||
|
} else {
|
||||||
|
// For non-video-slice NAL units, don't encrypt.
|
||||||
|
accumulated_clear_bytes +=
|
||||||
|
nalu_length_size_ + nalu.header_size() + nalu.payload_size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result != NaluReader::kEOStream) {
|
||||||
|
LOG(ERROR) << "Failed to parse NAL units.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
AddSubsample(accumulated_clear_bytes, 0, decrypt_config);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EncryptionHandler::EncryptBytes(uint8_t* data, size_t size) {
|
||||||
|
DCHECK(encryptor_);
|
||||||
|
CHECK(encryptor_->Crypt(data, size, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
void EncryptionHandler::InjectVpxParserForTesting(
|
||||||
|
std::unique_ptr<VPxParser> vpx_parser) {
|
||||||
|
vpx_parser_ = std::move(vpx_parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EncryptionHandler::InjectVideoSliceHeaderParserForTesting(
|
||||||
|
std::unique_ptr<VideoSliceHeaderParser> header_parser) {
|
||||||
|
header_parser_ = std::move(header_parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace media
|
||||||
|
} // namespace shaka
|
|
@ -0,0 +1,116 @@
|
||||||
|
// 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
|
||||||
|
|
||||||
|
#ifndef PACKAGER_MEDIA_CRYPTO_ENCRYPTION_HANDLER_H_
|
||||||
|
#define PACKAGER_MEDIA_CRYPTO_ENCRYPTION_HANDLER_H_
|
||||||
|
|
||||||
|
#include "packager/media/base/key_source.h"
|
||||||
|
#include "packager/media/base/media_handler.h"
|
||||||
|
#include "packager/media/base/stream_info.h"
|
||||||
|
|
||||||
|
namespace shaka {
|
||||||
|
namespace media {
|
||||||
|
|
||||||
|
class AesCryptor;
|
||||||
|
class VideoSliceHeaderParser;
|
||||||
|
class VPxParser;
|
||||||
|
struct EncryptionKey;
|
||||||
|
struct VPxFrameInfo;
|
||||||
|
|
||||||
|
/// This structure defines encryption options.
|
||||||
|
struct EncryptionOptions {
|
||||||
|
/// Clear lead duration in seconds.
|
||||||
|
double clear_lead_in_seconds = 0;
|
||||||
|
/// The protection scheme: 'cenc', 'cens', 'cbc1', 'cbcs'.
|
||||||
|
FourCC protection_scheme = FOURCC_cenc;
|
||||||
|
/// The threshold to determine whether a video track should be considered as
|
||||||
|
/// SD. If the max pixels per frame is no higher than max_sd_pixels, i.e.
|
||||||
|
/// [0, max_sd_pixels], it is SD.
|
||||||
|
uint32_t max_sd_pixels = 0;
|
||||||
|
/// The threshold to determine whether a video track should be considered as
|
||||||
|
/// HD. If the max pixels per frame is higher than max_sd_pixels, but no
|
||||||
|
/// higher than max_hd_pixels, i.e. (max_sd_pixels, max_hd_pixels], it is HD.
|
||||||
|
uint32_t max_hd_pixels = 0;
|
||||||
|
/// The threshold to determine whether a video track should be considered as
|
||||||
|
/// UHD1. If the max pixels per frame is higher than max_hd_pixels, but no
|
||||||
|
/// higher than max_uhd1_pixels, i.e. (max_hd_pixels, max_uhd1_pixels], it is
|
||||||
|
/// UHD1. Otherwise it is UHD2.
|
||||||
|
uint32_t max_uhd1_pixels = 0;
|
||||||
|
/// Crypto period duration in seconds. A positive value means key rotation is
|
||||||
|
/// enabled, the key source must support key rotation in this case.
|
||||||
|
double crypto_period_duration_in_seconds = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class EncryptionHandler : public MediaHandler {
|
||||||
|
public:
|
||||||
|
EncryptionHandler(const EncryptionOptions& encryption_options,
|
||||||
|
KeySource* key_source);
|
||||||
|
|
||||||
|
~EncryptionHandler() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
/// @name MediaHandler implementation overrides.
|
||||||
|
/// @{
|
||||||
|
Status InitializeInternal() override;
|
||||||
|
Status Process(std::unique_ptr<StreamData> stream_data) override;
|
||||||
|
/// @}
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class EncryptionHandlerTest;
|
||||||
|
|
||||||
|
EncryptionHandler(const EncryptionHandler&) = delete;
|
||||||
|
EncryptionHandler& operator=(const EncryptionHandler&) = delete;
|
||||||
|
|
||||||
|
// Processes |stream_info| and sets up stream specific variables.
|
||||||
|
Status ProcessStreamInfo(StreamInfo* stream_info);
|
||||||
|
// Processes media sample end encrypts it if needed.
|
||||||
|
Status ProcessMediaSample(MediaSample* sample);
|
||||||
|
|
||||||
|
bool CreateEncryptor(EncryptionKey* encryption_key);
|
||||||
|
bool EncryptVpxFrame(const std::vector<VPxFrameInfo>& vpx_frames,
|
||||||
|
MediaSample* sample,
|
||||||
|
DecryptConfig* decrypt_config);
|
||||||
|
bool EncryptNalFrame(MediaSample* sample, DecryptConfig* decrypt_config);
|
||||||
|
void EncryptBytes(uint8_t* data, size_t size);
|
||||||
|
|
||||||
|
// Testing injections.
|
||||||
|
void InjectVpxParserForTesting(std::unique_ptr<VPxParser> vpx_parser);
|
||||||
|
void InjectVideoSliceHeaderParserForTesting(
|
||||||
|
std::unique_ptr<VideoSliceHeaderParser> header_parser);
|
||||||
|
|
||||||
|
EncryptionOptions encryption_options_;
|
||||||
|
KeySource* key_source_ = nullptr;
|
||||||
|
KeySource::TrackType track_type_ = KeySource::TRACK_TYPE_UNKNOWN;
|
||||||
|
std::unique_ptr<AesCryptor> encryptor_;
|
||||||
|
// Specifies the size of NAL unit length in bytes. Can be 1, 2 or 4 bytes. 0
|
||||||
|
// if it is not a NAL structured video.
|
||||||
|
uint8_t nalu_length_size_ = 0;
|
||||||
|
Codec video_codec_ = kUnknownCodec;
|
||||||
|
// Remaining clear lead in the stream's time scale.
|
||||||
|
int64_t remaining_clear_lead_ = 0;
|
||||||
|
// Crypto period duration in the stream's time scale.
|
||||||
|
uint64_t crypto_period_duration_ = 0;
|
||||||
|
// Previous crypto period index if key rotation is enabled.
|
||||||
|
int64_t prev_crypto_period_index_ = -1;
|
||||||
|
bool new_segment_ = true;
|
||||||
|
|
||||||
|
// Number of encrypted blocks (16-byte-block) in pattern based encryption.
|
||||||
|
uint8_t crypt_byte_block_ = 0;
|
||||||
|
/// Number of unencrypted blocks (16-byte-block) in pattern based encryption.
|
||||||
|
uint8_t skip_byte_block_ = 0;
|
||||||
|
|
||||||
|
// Current key id.
|
||||||
|
std::vector<uint8_t> key_id_;
|
||||||
|
// VPx parser for VPx streams.
|
||||||
|
std::unique_ptr<VPxParser> vpx_parser_;
|
||||||
|
// Video slice header parser for NAL strucutred streams.
|
||||||
|
std::unique_ptr<VideoSliceHeaderParser> header_parser_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace media
|
||||||
|
} // namespace shaka
|
||||||
|
|
||||||
|
#endif // PACKAGER_MEDIA_CRYPTO_ENCRYPTION_HANDLER_H_
|
|
@ -0,0 +1,456 @@
|
||||||
|
// 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>
|
||||||
|
|
||||||
|
#include "packager/media/base/aes_decryptor.h"
|
||||||
|
#include "packager/media/base/aes_pattern_cryptor.h"
|
||||||
|
#include "packager/media/base/audio_stream_info.h"
|
||||||
|
#include "packager/media/base/fixed_key_source.h"
|
||||||
|
#include "packager/media/base/test/status_test_util.h"
|
||||||
|
#include "packager/media/base/video_stream_info.h"
|
||||||
|
#include "packager/media/codecs/video_slice_header_parser.h"
|
||||||
|
#include "packager/media/codecs/vpx_parser.h"
|
||||||
|
|
||||||
|
namespace shaka {
|
||||||
|
namespace media {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using ::testing::_;
|
||||||
|
using ::testing::Combine;
|
||||||
|
using ::testing::DoAll;
|
||||||
|
using ::testing::ElementsAre;
|
||||||
|
using ::testing::Return;
|
||||||
|
using ::testing::SetArgPointee;
|
||||||
|
using ::testing::Values;
|
||||||
|
using ::testing::WithParamInterface;
|
||||||
|
|
||||||
|
class MockKeySource : public FixedKeySource {
|
||||||
|
public:
|
||||||
|
MOCK_METHOD2(GetKey, Status(TrackType track_type, EncryptionKey* key));
|
||||||
|
MOCK_METHOD3(GetCryptoPeriodKey,
|
||||||
|
Status(uint32_t crypto_period_index,
|
||||||
|
TrackType track_type,
|
||||||
|
EncryptionKey* key));
|
||||||
|
};
|
||||||
|
|
||||||
|
class FakeMediaHandler : public MediaHandler {
|
||||||
|
public:
|
||||||
|
const std::vector<std::unique_ptr<StreamData>>& stream_data_vector() const {
|
||||||
|
return stream_data_vector_;
|
||||||
|
}
|
||||||
|
void clear_stream_data_vector() { stream_data_vector_.clear(); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
Status InitializeInternal() override { return Status::OK; }
|
||||||
|
Status Process(std::unique_ptr<StreamData> stream_data) override {
|
||||||
|
stream_data_vector_.push_back(std::move(stream_data));
|
||||||
|
return Status::OK;
|
||||||
|
}
|
||||||
|
bool ValidateOutputStreamIndex(int stream_index) const override {
|
||||||
|
return stream_index == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::unique_ptr<StreamData>> stream_data_vector_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class MockVpxParser : public VPxParser {
|
||||||
|
public:
|
||||||
|
MOCK_METHOD3(Parse,
|
||||||
|
bool(const uint8_t* data,
|
||||||
|
size_t data_size,
|
||||||
|
std::vector<VPxFrameInfo>* vpx_frames));
|
||||||
|
};
|
||||||
|
|
||||||
|
class MockVideoSliceHeaderParser : public VideoSliceHeaderParser {
|
||||||
|
public:
|
||||||
|
MOCK_METHOD1(Initialize,
|
||||||
|
bool(const std::vector<uint8_t>& decoder_configuration));
|
||||||
|
MOCK_METHOD1(GetHeaderSize, int64_t(const Nalu& nalu));
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
class EncryptionHandlerTest : public ::testing::Test {
|
||||||
|
public:
|
||||||
|
void SetUp() override { SetUpEncryptionHandler(EncryptionOptions()); }
|
||||||
|
|
||||||
|
void SetUpEncryptionHandler(const EncryptionOptions& encryption_options) {
|
||||||
|
encryption_handler_.reset(
|
||||||
|
new EncryptionHandler(encryption_options, &mock_key_source_));
|
||||||
|
next_handler_.reset(new FakeMediaHandler);
|
||||||
|
|
||||||
|
// Input handler is not really used anywhere but just to satisfy one input
|
||||||
|
// one output restriction for the encryption handler.
|
||||||
|
auto input_handler = std::make_shared<FakeMediaHandler>();
|
||||||
|
ASSERT_OK(input_handler->AddHandler(encryption_handler_));
|
||||||
|
ASSERT_OK(encryption_handler_->AddHandler(next_handler_));
|
||||||
|
}
|
||||||
|
|
||||||
|
Status Process(std::unique_ptr<StreamData> stream_data) {
|
||||||
|
return encryption_handler_->Process(std::move(stream_data));
|
||||||
|
}
|
||||||
|
|
||||||
|
void InjectVpxParserForTesting(std::unique_ptr<VPxParser> vpx_parser) {
|
||||||
|
encryption_handler_->InjectVpxParserForTesting(std::move(vpx_parser));
|
||||||
|
}
|
||||||
|
|
||||||
|
void InjectVideoSliceHeaderParserForTesting(
|
||||||
|
std::unique_ptr<VideoSliceHeaderParser> header_parser) {
|
||||||
|
encryption_handler_->InjectVideoSliceHeaderParserForTesting(
|
||||||
|
std::move(header_parser));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::shared_ptr<EncryptionHandler> encryption_handler_;
|
||||||
|
std::shared_ptr<FakeMediaHandler> next_handler_;
|
||||||
|
MockKeySource mock_key_source_;
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(EncryptionHandlerTest, Initialize) {
|
||||||
|
ASSERT_OK(encryption_handler_->Initialize());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EncryptionHandlerTest, OnlyOneOutput) {
|
||||||
|
auto another_handler = std::make_shared<FakeMediaHandler>();
|
||||||
|
// Connecting another handler will fail.
|
||||||
|
ASSERT_EQ(error::INVALID_ARGUMENT,
|
||||||
|
encryption_handler_->AddHandler(another_handler).error_code());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EncryptionHandlerTest, OnlyOneInput) {
|
||||||
|
auto another_handler = std::make_shared<FakeMediaHandler>();
|
||||||
|
ASSERT_OK(another_handler->AddHandler(encryption_handler_));
|
||||||
|
ASSERT_EQ(error::INVALID_ARGUMENT,
|
||||||
|
encryption_handler_->Initialize().error_code());
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
const int kTrackId = 1;
|
||||||
|
const uint32_t kTimeScale = 1000;
|
||||||
|
const uint64_t kDuration = 10000;
|
||||||
|
const char kCodecString[] = "codec string";
|
||||||
|
const uint8_t kSampleBits = 1;
|
||||||
|
const uint8_t kNumChannels = 2;
|
||||||
|
const uint32_t kSamplingFrequency = 48000;
|
||||||
|
const uint64_t kSeekPrerollNs = 12345;
|
||||||
|
const uint64_t kCodecDelayNs = 56789;
|
||||||
|
const uint32_t kMaxBitrate = 13579;
|
||||||
|
const uint32_t kAvgBitrate = 13000;
|
||||||
|
const char kLanguage[] = "eng";
|
||||||
|
const uint16_t kWidth = 10u;
|
||||||
|
const uint16_t kHeight = 20u;
|
||||||
|
const uint32_t kPixelWidth = 2u;
|
||||||
|
const uint32_t kPixelHeight = 3u;
|
||||||
|
const int16_t kTrickPlayRate = 4;
|
||||||
|
const uint8_t kNaluLengthSize = 1u;
|
||||||
|
const bool kEncrypted = true;
|
||||||
|
const uint32_t kMaxSdPixels = 100u;
|
||||||
|
const uint32_t kMaxHdPixels = 200u;
|
||||||
|
const uint32_t kMaxUhd1Pixels = 300u;
|
||||||
|
|
||||||
|
// Use H264 code config.
|
||||||
|
const uint8_t kCodecConfig[]{
|
||||||
|
// Header
|
||||||
|
0x01, 0x64, 0x00, 0x1e, 0xff,
|
||||||
|
// SPS count (ignore top three bits)
|
||||||
|
0xe1,
|
||||||
|
// SPS
|
||||||
|
0x00, 0x19, // Size
|
||||||
|
0x67, 0x64, 0x00, 0x1e, 0xac, 0xd9, 0x40, 0xa0, 0x2f, 0xf9, 0x70, 0x11,
|
||||||
|
0x00, 0x00, 0x03, 0x03, 0xe9, 0x00, 0x00, 0xea, 0x60, 0x0f, 0x16, 0x2d,
|
||||||
|
0x96,
|
||||||
|
// PPS count
|
||||||
|
0x01,
|
||||||
|
// PPS
|
||||||
|
0x00, 0x06, // Size
|
||||||
|
0x68, 0xeb, 0xe3, 0xcb, 0x22, 0xc0,
|
||||||
|
};
|
||||||
|
// The data is based on H264. The same data is also used to test audio, which
|
||||||
|
// does not care the underlying data, and VP9, for which we will mock the
|
||||||
|
// parser.
|
||||||
|
const uint8_t kData[]{
|
||||||
|
// First NALU
|
||||||
|
0x15, 0x01, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
|
||||||
|
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19,
|
||||||
|
// Second NALU
|
||||||
|
0x13, 0x25, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
|
||||||
|
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
|
||||||
|
// Third NALU
|
||||||
|
0x06, 0x67, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
|
||||||
|
};
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
inline bool operator==(const SubsampleEntry& lhs, const SubsampleEntry& rhs) {
|
||||||
|
return lhs.clear_bytes == rhs.clear_bytes &&
|
||||||
|
lhs.cipher_bytes == rhs.cipher_bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
class EncryptionHandlerEncryptionTest
|
||||||
|
: public EncryptionHandlerTest,
|
||||||
|
public WithParamInterface<std::tr1::tuple<FourCC, Codec>> {
|
||||||
|
public:
|
||||||
|
void SetUp() override {
|
||||||
|
protection_scheme_ = std::tr1::get<0>(GetParam());
|
||||||
|
codec_ = std::tr1::get<1>(GetParam());
|
||||||
|
|
||||||
|
EncryptionOptions encryption_options;
|
||||||
|
encryption_options.protection_scheme = protection_scheme_;;
|
||||||
|
encryption_options.max_sd_pixels = kMaxSdPixels;
|
||||||
|
encryption_options.max_hd_pixels = kMaxHdPixels;
|
||||||
|
encryption_options.max_uhd1_pixels = kMaxUhd1Pixels;
|
||||||
|
SetUpEncryptionHandler(encryption_options);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<StreamInfo> GetMockStreamInfo() {
|
||||||
|
if (codec_ == kCodecAAC) {
|
||||||
|
return std::unique_ptr<StreamInfo>(new AudioStreamInfo(
|
||||||
|
kTrackId, kTimeScale, kDuration, codec_, kCodecString, kCodecConfig,
|
||||||
|
sizeof(kCodecConfig), kSampleBits, kNumChannels, kSamplingFrequency,
|
||||||
|
kSeekPrerollNs, kCodecDelayNs, kMaxBitrate, kAvgBitrate, kLanguage,
|
||||||
|
!kEncrypted));
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return std::unique_ptr<StreamInfo>(new VideoStreamInfo(
|
||||||
|
kTrackId, kTimeScale, kDuration, codec_, kCodecString, kCodecConfig,
|
||||||
|
sizeof(kCodecConfig), kWidth, kHeight, kPixelWidth, kPixelHeight,
|
||||||
|
kTrickPlayRate, kNaluLengthSize, kLanguage, !kEncrypted));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<VPxFrameInfo> GetMockVpxFrameInfo() {
|
||||||
|
std::vector<VPxFrameInfo> vpx_frames;
|
||||||
|
vpx_frames.resize(2);
|
||||||
|
vpx_frames[0].frame_size = 22;
|
||||||
|
vpx_frames[0].uncompressed_header_size = 3;
|
||||||
|
vpx_frames[1].frame_size = 20;
|
||||||
|
vpx_frames[1].uncompressed_header_size = 4;
|
||||||
|
return vpx_frames;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The subsamples values should match |GetMockVpxFrameInfo| above.
|
||||||
|
std::vector<SubsampleEntry> GetExpectedSubsamples() {
|
||||||
|
std::vector<SubsampleEntry> subsamples;
|
||||||
|
if (codec_ == kCodecAAC)
|
||||||
|
return subsamples;
|
||||||
|
if (codec_ == kCodecVP9 || protection_scheme_ == FOURCC_cbc1 ||
|
||||||
|
protection_scheme_ == FOURCC_cens) {
|
||||||
|
// Align the encrypted bytes to multiple of 16 bytes.
|
||||||
|
subsamples.emplace_back(6, 16);
|
||||||
|
} else {
|
||||||
|
subsamples.emplace_back(3, 19);
|
||||||
|
}
|
||||||
|
subsamples.emplace_back(4, 16);
|
||||||
|
subsamples.emplace_back(7, 0);
|
||||||
|
return subsamples;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Decrypt(const DecryptConfig& decrypt_config,
|
||||||
|
uint8_t* data,
|
||||||
|
size_t data_size) {
|
||||||
|
std::unique_ptr<AesCryptor> aes_decryptor;
|
||||||
|
switch (decrypt_config.protection_scheme()) {
|
||||||
|
case FOURCC_cenc:
|
||||||
|
aes_decryptor.reset(new AesCtrDecryptor);
|
||||||
|
break;
|
||||||
|
case FOURCC_cbc1:
|
||||||
|
aes_decryptor.reset(new AesCbcDecryptor(kNoPadding));
|
||||||
|
break;
|
||||||
|
case FOURCC_cens:
|
||||||
|
aes_decryptor.reset(new AesPatternCryptor(
|
||||||
|
decrypt_config.crypt_byte_block(), decrypt_config.skip_byte_block(),
|
||||||
|
AesPatternCryptor::kEncryptIfCryptByteBlockRemaining,
|
||||||
|
AesCryptor::kDontUseConstantIv,
|
||||||
|
std::unique_ptr<AesCryptor>(new AesCtrDecryptor())));
|
||||||
|
break;
|
||||||
|
case FOURCC_cbcs:
|
||||||
|
aes_decryptor.reset(new AesPatternCryptor(
|
||||||
|
decrypt_config.crypt_byte_block(), decrypt_config.skip_byte_block(),
|
||||||
|
AesPatternCryptor::kEncryptIfCryptByteBlockRemaining,
|
||||||
|
AesCryptor::kUseConstantIv,
|
||||||
|
std::unique_ptr<AesCryptor>(new AesCbcDecryptor(kNoPadding))));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LOG(FATAL) << "Not supposed to happen.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!aes_decryptor->InitializeWithIv(
|
||||||
|
std::vector<uint8_t>(kKey, kKey + sizeof(kKey)),
|
||||||
|
decrypt_config.iv())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decrypt_config.subsamples().empty()) {
|
||||||
|
// Sample not encrypted using subsample encryption. Decrypt whole.
|
||||||
|
if (!aes_decryptor->Crypt(data, data_size, data)) {
|
||||||
|
LOG(ERROR) << "Error during bulk sample decryption.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subsample decryption.
|
||||||
|
const std::vector<SubsampleEntry>& subsamples = decrypt_config.subsamples();
|
||||||
|
uint8_t* current_ptr = data;
|
||||||
|
const uint8_t* const buffer_end = data + data_size;
|
||||||
|
for (const auto& subsample : subsamples) {
|
||||||
|
if (current_ptr + subsample.clear_bytes + subsample.cipher_bytes >
|
||||||
|
buffer_end) {
|
||||||
|
LOG(ERROR) << "Subsamples overflow sample buffer.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
current_ptr += subsample.clear_bytes;
|
||||||
|
if (!aes_decryptor->Crypt(current_ptr, subsample.cipher_bytes,
|
||||||
|
current_ptr)) {
|
||||||
|
LOG(ERROR) << "Error decrypting subsample buffer.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
current_ptr += subsample.cipher_bytes;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t GetExpectedCryptByteBlock() {
|
||||||
|
switch (protection_scheme_) {
|
||||||
|
case FOURCC_cenc:
|
||||||
|
case FOURCC_cbc1:
|
||||||
|
return 0;
|
||||||
|
case FOURCC_cens:
|
||||||
|
case FOURCC_cbcs:
|
||||||
|
return 1;
|
||||||
|
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:
|
||||||
|
return 9;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
FourCC protection_scheme_;
|
||||||
|
Codec codec_;
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_P(EncryptionHandlerEncryptionTest, Encrypt) {
|
||||||
|
std::unique_ptr<StreamData> stream_data(new StreamData);
|
||||||
|
stream_data->stream_index = 0;
|
||||||
|
stream_data->stream_data_type = StreamDataType::kStreamInfo;
|
||||||
|
stream_data->stream_info = GetMockStreamInfo();
|
||||||
|
ASSERT_OK(Process(std::move(stream_data)));
|
||||||
|
ASSERT_EQ(1u, next_handler_->stream_data_vector().size());
|
||||||
|
ASSERT_EQ(0, next_handler_->stream_data_vector().back()->stream_index);
|
||||||
|
ASSERT_EQ(StreamDataType::kStreamInfo,
|
||||||
|
next_handler_->stream_data_vector().back()->stream_data_type);
|
||||||
|
ASSERT_TRUE(
|
||||||
|
next_handler_->stream_data_vector().back()->stream_info->is_encrypted());
|
||||||
|
|
||||||
|
// Inject vpx parser / video slice header parser if needed.
|
||||||
|
switch (codec_) {
|
||||||
|
case kCodecVP9:{
|
||||||
|
std::unique_ptr<MockVpxParser> mock_vpx_parser(new MockVpxParser);
|
||||||
|
EXPECT_CALL(*mock_vpx_parser, Parse(_, sizeof(kData), _))
|
||||||
|
.WillOnce(
|
||||||
|
DoAll(SetArgPointee<2>(GetMockVpxFrameInfo()), Return(true)));
|
||||||
|
InjectVpxParserForTesting(std::move(mock_vpx_parser));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case kCodecH264: {
|
||||||
|
std::unique_ptr<MockVideoSliceHeaderParser> mock_header_parser(
|
||||||
|
new MockVideoSliceHeaderParser);
|
||||||
|
// We want to return the same subsamples for VP9 and H264, so the return
|
||||||
|
// values here should match |GetMockVpxFrameInfo|.
|
||||||
|
EXPECT_CALL(*mock_header_parser, GetHeaderSize(_))
|
||||||
|
.WillOnce(Return(1))
|
||||||
|
.WillOnce(Return(2));
|
||||||
|
InjectVideoSliceHeaderParserForTesting(std::move(mock_header_parser));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream_data.reset(new StreamData);
|
||||||
|
stream_data->stream_index = 0;
|
||||||
|
stream_data->stream_data_type = StreamDataType::kMediaSample;
|
||||||
|
stream_data->media_sample.reset(
|
||||||
|
new MediaSample(kData, sizeof(kData), nullptr, 0, true));
|
||||||
|
|
||||||
|
EXPECT_CALL(mock_key_source_, GetKey(_, _))
|
||||||
|
.WillOnce(
|
||||||
|
DoAll(SetArgPointee<1>(GetMockEncryptionKey()), Return(Status::OK)));
|
||||||
|
ASSERT_OK(Process(std::move(stream_data)));
|
||||||
|
ASSERT_EQ(2u, next_handler_->stream_data_vector().size());
|
||||||
|
ASSERT_EQ(0, next_handler_->stream_data_vector().back()->stream_index);
|
||||||
|
ASSERT_EQ(StreamDataType::kMediaSample,
|
||||||
|
next_handler_->stream_data_vector().back()->stream_data_type);
|
||||||
|
|
||||||
|
auto* media_sample =
|
||||||
|
next_handler_->stream_data_vector().back()->media_sample.get();
|
||||||
|
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());
|
||||||
|
EXPECT_EQ(GetExpectedSubsamples(), decrypt_config->subsamples());
|
||||||
|
EXPECT_EQ(protection_scheme_, decrypt_config->protection_scheme());
|
||||||
|
EXPECT_EQ(GetExpectedCryptByteBlock(), decrypt_config->crypt_byte_block());
|
||||||
|
EXPECT_EQ(GetExpectedSkipByteBlock(), decrypt_config->skip_byte_block());
|
||||||
|
|
||||||
|
ASSERT_TRUE(Decrypt(*decrypt_config, media_sample->writable_data(),
|
||||||
|
media_sample->data_size()));
|
||||||
|
EXPECT_EQ(
|
||||||
|
std::vector<uint8_t>(kData, kData + sizeof(kData)),
|
||||||
|
std::vector<uint8_t>(media_sample->data(),
|
||||||
|
media_sample->data() + media_sample->data_size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
INSTANTIATE_TEST_CASE_P(
|
||||||
|
InstantiationName,
|
||||||
|
EncryptionHandlerEncryptionTest,
|
||||||
|
Combine(Values(FOURCC_cenc, FOURCC_cens, FOURCC_cbc1, FOURCC_cbcs),
|
||||||
|
Values(kCodecAAC, kCodecH264, kCodecVP9)));
|
||||||
|
|
||||||
|
// TODO(kqyang): Add more unit tests.
|
||||||
|
|
||||||
|
} // namespace media
|
||||||
|
} // namespace shaka
|
|
@ -116,6 +116,7 @@
|
||||||
'hls/hls.gyp:hls_unittest',
|
'hls/hls.gyp:hls_unittest',
|
||||||
'media/base/media_base.gyp:media_base_unittest',
|
'media/base/media_base.gyp:media_base_unittest',
|
||||||
'media/codecs/codecs.gyp:codecs_unittest',
|
'media/codecs/codecs.gyp:codecs_unittest',
|
||||||
|
'media/crypto/crypto.gyp:crypto_unittest',
|
||||||
'media/event/media_event.gyp:media_event_unittest',
|
'media/event/media_event.gyp:media_event_unittest',
|
||||||
'media/file/file.gyp:file_unittest',
|
'media/file/file.gyp:file_unittest',
|
||||||
'media/formats/mp2t/mp2t.gyp:mp2t_unittest',
|
'media/formats/mp2t/mp2t.gyp:mp2t_unittest',
|
||||||
|
|
Loading…
Reference in New Issue