From 7a90ee70ab1926ef476759a97217b4711a72ad1d Mon Sep 17 00:00:00 2001 From: Kongqun Yang Date: Thu, 2 Feb 2017 10:28:29 -0800 Subject: [PATCH] Implement EncryptionHandler Change-Id: Iabedf3b02057d6124d5393ae9618490e5595ad89 --- packager/media/base/decrypt_config.cc | 7 + packager/media/base/decrypt_config.h | 13 +- packager/media/base/media_handler.cc | 15 + packager/media/base/media_handler.h | 78 +-- packager/media/base/status.h | 3 + packager/media/base/stream_info.h | 1 + packager/media/crypto/crypto.gyp | 39 ++ packager/media/crypto/encryption_handler.cc | 418 ++++++++++++++++ packager/media/crypto/encryption_handler.h | 116 +++++ .../crypto/encryption_handler_unittest.cc | 456 ++++++++++++++++++ packager/packager.gyp | 1 + 11 files changed, 1116 insertions(+), 31 deletions(-) create mode 100644 packager/media/crypto/crypto.gyp create mode 100644 packager/media/crypto/encryption_handler.cc create mode 100644 packager/media/crypto/encryption_handler.h create mode 100644 packager/media/crypto/encryption_handler_unittest.cc diff --git a/packager/media/base/decrypt_config.cc b/packager/media/base/decrypt_config.cc index e56febfc5a..7b1c47aa89 100644 --- a/packager/media/base/decrypt_config.cc +++ b/packager/media/base/decrypt_config.cc @@ -31,5 +31,12 @@ DecryptConfig::DecryptConfig(const std::vector& key_id, 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 shaka diff --git a/packager/media/base/decrypt_config.h b/packager/media/base/decrypt_config.h index 29019f49dc..7adb7d8fdd 100644 --- a/packager/media/base/decrypt_config.h +++ b/packager/media/base/decrypt_config.h @@ -73,6 +73,17 @@ class 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& key_id() const { return key_id_; } const std::vector& iv() const { return iv_; } const std::vector& subsamples() const { return subsamples_; } @@ -88,7 +99,7 @@ class DecryptConfig { // Subsample information. May be empty for some formats, meaning entire frame // (less data ignored by data_offset_) is encrypted. - const std::vector subsamples_; + std::vector subsamples_; const FourCC protection_scheme_; // For pattern-based protection schemes, like CENS and CBCS. diff --git a/packager/media/base/media_handler.cc b/packager/media/base/media_handler.cc index 2c4fecaa0d..0ab130cb9b 100644 --- a/packager/media/base/media_handler.cc +++ b/packager/media/base/media_handler.cc @@ -23,6 +23,21 @@ Status MediaHandler::SetHandler(int output_stream_index, 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) { // The default implementation treats the output stream index to be identical // to the input stream index, which is true for most handlers. diff --git a/packager/media/base/media_handler.h b/packager/media/base/media_handler.h index fc461ec857..799a227e18 100644 --- a/packager/media/base/media_handler.h +++ b/packager/media/base/media_handler.h @@ -11,11 +11,47 @@ #include #include +#include "packager/media/base/media_sample.h" #include "packager/media/base/status.h" +#include "packager/media/base/stream_info.h" namespace shaka { 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 period_info; + std::unique_ptr stream_info; + std::unique_ptr encryption_config; + std::unique_ptr media_sample; + std::unique_ptr media_event; + std::unique_ptr segment_info; +}; + /// MediaHandler is the base media processing unit. Media handlers transform /// the input streams and propagate the outputs to downstream media handlers. /// There are three different types of media handlers: @@ -45,37 +81,14 @@ class MediaHandler { 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: - enum class StreamDataType { - kUnknown, - kPeriodInfo, - 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 period_info; - std::unique_ptr stream_info; - std::unique_ptr encryption_config; - std::unique_ptr media_sample; - std::unique_ptr media_event; - std::unique_ptr segment_info; - }; + /// Internal implementation of initialize. Note that it should only initialize + /// the MediaHandler itself. Downstream handlers are handled in Initialize(). + virtual Status InitializeInternal() = 0; /// Process the incoming stream data. Note that (1) stream_data.stream_index /// 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. 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 /// stream_data.stream_index should be the output stream index. Status Dispatch(std::unique_ptr stream_data); @@ -149,11 +165,13 @@ class MediaHandler { } int num_input_streams() const { return num_input_streams_; } + int next_output_stream_index() const { return next_output_stream_index_; } private: MediaHandler(const MediaHandler&) = delete; MediaHandler& operator=(const MediaHandler&) = delete; + bool initialized_ = false; // Number of input streams. int num_input_streams_ = 0; // The next available output stream index, used by AddHandler. diff --git a/packager/media/base/status.h b/packager/media/base/status.h index e1bfbbb133..6316ea1f29 100644 --- a/packager/media/base/status.h +++ b/packager/media/base/status.h @@ -48,6 +48,9 @@ enum Code { // Unable to parse the media file. PARSER_FAILURE, + // Failed to do the encryption. + ENCRYPTION_FAILURE, + // Fail to mux the media file. MUXER_FAILURE, diff --git a/packager/media/base/stream_info.h b/packager/media/base/stream_info.h index b98183239b..cd7a8e1039 100644 --- a/packager/media/base/stream_info.h +++ b/packager/media/base/stream_info.h @@ -81,6 +81,7 @@ class StreamInfo { codec_string_ = codec_string; } void set_language(const std::string& language) { language_ = language; } + void set_is_encrypted(bool is_encrypted) { is_encrypted_ = is_encrypted; } private: // Whether the stream is Audio or Video. diff --git a/packager/media/crypto/crypto.gyp b/packager/media/crypto/crypto.gyp new file mode 100644 index 0000000000..12a004ae3c --- /dev/null +++ b/packager/media/crypto/crypto.gyp @@ -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', + ] + }, + ], +} + diff --git a/packager/media/crypto/encryption_handler.cc b/packager/media/crypto/encryption_handler.cc new file mode 100644 index 0000000000..7e36ae2c1f --- /dev/null +++ b/packager/media/crypto/encryption_handler.cc @@ -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 +#include + +#include + +#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::max()); + const uint64_t kUInt16Max = std::numeric_limits::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(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(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(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 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(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 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 decrypt_config(new DecryptConfig( + key_id_, encryptor_->iv(), std::vector(), + 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 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(new AesCtrEncryptor()))); + break; + case FOURCC_cbcs: + encryptor.reset(new AesPatternCryptor( + crypt_byte_block_, skip_byte_block_, + AesPatternCryptor::kEncryptIfCryptByteBlockRemaining, + AesCryptor::kUseConstantIv, + std::unique_ptr(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& 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(frame.uncompressed_header_size); + uint32_t cipher_bytes = static_cast( + 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(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(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 vpx_parser) { + vpx_parser_ = std::move(vpx_parser); +} + +void EncryptionHandler::InjectVideoSliceHeaderParserForTesting( + std::unique_ptr header_parser) { + header_parser_ = std::move(header_parser); +} + +} // namespace media +} // namespace shaka diff --git a/packager/media/crypto/encryption_handler.h b/packager/media/crypto/encryption_handler.h new file mode 100644 index 0000000000..fb1341ea25 --- /dev/null +++ b/packager/media/crypto/encryption_handler.h @@ -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 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& 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 vpx_parser); + void InjectVideoSliceHeaderParserForTesting( + std::unique_ptr header_parser); + + EncryptionOptions encryption_options_; + KeySource* key_source_ = nullptr; + KeySource::TrackType track_type_ = KeySource::TRACK_TYPE_UNKNOWN; + std::unique_ptr 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 key_id_; + // VPx parser for VPx streams. + std::unique_ptr vpx_parser_; + // Video slice header parser for NAL strucutred streams. + std::unique_ptr header_parser_; +}; + +} // namespace media +} // namespace shaka + +#endif // PACKAGER_MEDIA_CRYPTO_ENCRYPTION_HANDLER_H_ diff --git a/packager/media/crypto/encryption_handler_unittest.cc b/packager/media/crypto/encryption_handler_unittest.cc new file mode 100644 index 0000000000..2c1f729282 --- /dev/null +++ b/packager/media/crypto/encryption_handler_unittest.cc @@ -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 +#include + +#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>& 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 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> stream_data_vector_; +}; + +class MockVpxParser : public VPxParser { + public: + MOCK_METHOD3(Parse, + bool(const uint8_t* data, + size_t data_size, + std::vector* vpx_frames)); +}; + +class MockVideoSliceHeaderParser : public VideoSliceHeaderParser { + public: + MOCK_METHOD1(Initialize, + bool(const std::vector& 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(); + ASSERT_OK(input_handler->AddHandler(encryption_handler_)); + ASSERT_OK(encryption_handler_->AddHandler(next_handler_)); + } + + Status Process(std::unique_ptr stream_data) { + return encryption_handler_->Process(std::move(stream_data)); + } + + void InjectVpxParserForTesting(std::unique_ptr vpx_parser) { + encryption_handler_->InjectVpxParserForTesting(std::move(vpx_parser)); + } + + void InjectVideoSliceHeaderParserForTesting( + std::unique_ptr header_parser) { + encryption_handler_->InjectVideoSliceHeaderParserForTesting( + std::move(header_parser)); + } + + protected: + std::shared_ptr encryption_handler_; + std::shared_ptr 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(); + // 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(); + 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> { + 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 GetMockStreamInfo() { + if (codec_ == kCodecAAC) { + return std::unique_ptr(new AudioStreamInfo( + kTrackId, kTimeScale, kDuration, codec_, kCodecString, kCodecConfig, + sizeof(kCodecConfig), kSampleBits, kNumChannels, kSamplingFrequency, + kSeekPrerollNs, kCodecDelayNs, kMaxBitrate, kAvgBitrate, kLanguage, + !kEncrypted)); + + } else { + return std::unique_ptr(new VideoStreamInfo( + kTrackId, kTimeScale, kDuration, codec_, kCodecString, kCodecConfig, + sizeof(kCodecConfig), kWidth, kHeight, kPixelWidth, kPixelHeight, + kTrickPlayRate, kNaluLengthSize, kLanguage, !kEncrypted)); + } + } + + std::vector GetMockVpxFrameInfo() { + std::vector 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 GetExpectedSubsamples() { + std::vector 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 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(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(new AesCbcDecryptor(kNoPadding)))); + break; + default: + LOG(FATAL) << "Not supposed to happen."; + } + + if (!aes_decryptor->InitializeWithIv( + std::vector(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& 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 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 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 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(kKeyId, kKeyId + sizeof(kKeyId)), + decrypt_config->key_id()); + EXPECT_EQ(std::vector(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(kData, kData + sizeof(kData)), + std::vector(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 diff --git a/packager/packager.gyp b/packager/packager.gyp index 6d8db451e0..b4ca5b745c 100644 --- a/packager/packager.gyp +++ b/packager/packager.gyp @@ -116,6 +116,7 @@ 'hls/hls.gyp:hls_unittest', 'media/base/media_base.gyp:media_base_unittest', 'media/codecs/codecs.gyp:codecs_unittest', + 'media/crypto/crypto.gyp:crypto_unittest', 'media/event/media_event.gyp:media_event_unittest', 'media/file/file.gyp:file_unittest', 'media/formats/mp2t/mp2t.gyp:mp2t_unittest',