From 22498e125ae27c8f59b9ed1aab191a878bb7bd39 Mon Sep 17 00:00:00 2001 From: KongQun Yang Date: Tue, 12 Jan 2016 17:32:48 -0800 Subject: [PATCH] Add WebM decryption support - Also refactor decryptor management code out of mp4_media_parser.cc - Move decryptor managment logic to DecryptorSource class to make it available for webm as well - Remove data_offset member from DecryptConfig which is not useful - Add widevine_pssh_data.proto file Closes #72 Change-Id: I1d32baf4013ebd3382b5372c7433fae5033a260e --- packager/media/base/decrypt_config.cc | 7 +- packager/media/base/decrypt_config.h | 9 - packager/media/base/decryptor_source.cc | 85 +++++++++ packager/media/base/decryptor_source.h | 30 +-- .../media/base/decryptor_source_unittest.cc | 172 ++++++++++++++++++ packager/media/base/media_base.gyp | 15 ++ packager/media/base/widevine_pssh_data.proto | 36 ++++ .../media/formats/mp4/mp4_media_parser.cc | 99 ++-------- packager/media/formats/mp4/mp4_media_parser.h | 13 +- .../media/formats/mp4/track_run_iterator.cc | 1 - packager/media/formats/webm/webm.gyp | 1 + .../media/formats/webm/webm_cluster_parser.cc | 25 ++- .../media/formats/webm/webm_cluster_parser.h | 28 ++- .../webm/webm_cluster_parser_unittest.cc | 86 ++++++++- .../media/formats/webm/webm_crypto_helpers.cc | 2 +- .../media/formats/webm/webm_media_parser.cc | 40 +++- .../media/formats/webm/webm_media_parser.h | 5 +- 17 files changed, 505 insertions(+), 149 deletions(-) create mode 100644 packager/media/base/decryptor_source.cc create mode 100644 packager/media/base/decryptor_source_unittest.cc create mode 100644 packager/media/base/widevine_pssh_data.proto diff --git a/packager/media/base/decrypt_config.cc b/packager/media/base/decrypt_config.cc index 65587e52bc..bd2dc56933 100644 --- a/packager/media/base/decrypt_config.cc +++ b/packager/media/base/decrypt_config.cc @@ -11,14 +11,9 @@ namespace media { DecryptConfig::DecryptConfig(const std::vector& key_id, const std::vector& iv, - const int data_offset, const std::vector& subsamples) - : key_id_(key_id), - iv_(iv), - data_offset_(data_offset), - subsamples_(subsamples) { + : key_id_(key_id), iv_(iv), subsamples_(subsamples) { CHECK_GT(key_id.size(), 0u); - CHECK_GE(data_offset, 0); } DecryptConfig::~DecryptConfig() {} diff --git a/packager/media/base/decrypt_config.h b/packager/media/base/decrypt_config.h index 90a3d471ba..3dd5ebcca4 100644 --- a/packager/media/base/decrypt_config.h +++ b/packager/media/base/decrypt_config.h @@ -38,22 +38,16 @@ class DecryptConfig { /// @param key_id is the ID that references the decryption key. /// @param iv is the initialization vector defined by the encryptor. - /// @param data_offset is the amount of data that should be discarded from - /// the head of the sample buffer before applying subsample - /// information. A decrypted buffer will be shorter than an encrypted - /// buffer by this amount. /// @param subsamples defines the clear and encrypted portions of the sample /// as described in SubsampleEntry. A decrypted buffer will be equal /// in size to the sum of the subsample sizes. DecryptConfig(const std::vector& key_id, const std::vector& iv, - const int data_offset, const std::vector& subsamples); ~DecryptConfig(); const std::vector& key_id() const { return key_id_; } const std::vector& iv() const { return iv_; } - int data_offset() const { return data_offset_; } const std::vector& subsamples() const { return subsamples_; } private: @@ -62,9 +56,6 @@ class DecryptConfig { // Initialization vector. const std::vector iv_; - // Amount of data to be discarded before applying subsample information. - const int data_offset_; - // Subsample information. May be empty for some formats, meaning entire frame // (less data ignored by data_offset_) is encrypted. const std::vector subsamples_; diff --git a/packager/media/base/decryptor_source.cc b/packager/media/base/decryptor_source.cc new file mode 100644 index 0000000000..a464fafe7a --- /dev/null +++ b/packager/media/base/decryptor_source.cc @@ -0,0 +1,85 @@ +// Copyright 2016 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/base/decryptor_source.h" + +#include "packager/base/logging.h" +#include "packager/base/stl_util.h" + +namespace edash_packager { +namespace media { + +DecryptorSource::DecryptorSource(KeySource* key_source) + : key_source_(key_source) { + CHECK(key_source); +} +DecryptorSource::~DecryptorSource() { + STLDeleteValues(&decryptor_map_); +} + +bool DecryptorSource::DecryptSampleBuffer(const DecryptConfig* decrypt_config, + uint8_t* buffer, + size_t buffer_size) { + DCHECK(decrypt_config); + DCHECK(buffer); + + // Get the decryptor object. + AesCtrEncryptor* decryptor; + auto found = decryptor_map_.find(decrypt_config->key_id()); + if (found == decryptor_map_.end()) { + // Create new AesCtrEncryptor + EncryptionKey key; + Status status(key_source_->GetKey(decrypt_config->key_id(), &key)); + if (!status.ok()) { + LOG(ERROR) << "Error retrieving decryption key: " << status; + return false; + } + scoped_ptr aes_ctr_encryptor(new AesCtrEncryptor); + if (!aes_ctr_encryptor->InitializeWithIv(key.key, decrypt_config->iv())) { + LOG(ERROR) << "Failed to initialize AesCtrEncryptor for decryption."; + return false; + } + decryptor = aes_ctr_encryptor.release(); + decryptor_map_[decrypt_config->key_id()] = decryptor; + } else { + decryptor = found->second; + } + if (!decryptor->SetIv(decrypt_config->iv())) { + LOG(ERROR) << "Invalid initialization vector."; + return false; + } + + if (decrypt_config->subsamples().empty()) { + // Sample not encrypted using subsample encryption. Decrypt whole. + if (!decryptor->Decrypt(buffer, buffer_size, buffer)) { + LOG(ERROR) << "Error during bulk sample decryption."; + return false; + } + return true; + } + + // Subsample decryption. + const std::vector& subsamples = decrypt_config->subsamples(); + uint8_t* current_ptr = buffer; + const uint8_t* const buffer_end = buffer + buffer_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 (!decryptor->Decrypt(current_ptr, subsample.cipher_bytes, current_ptr)) { + LOG(ERROR) << "Error decrypting subsample buffer."; + return false; + } + current_ptr += subsample.cipher_bytes; + } + return true; +} + +} // namespace media +} // namespace edash_packager diff --git a/packager/media/base/decryptor_source.h b/packager/media/base/decryptor_source.h index 3a04556adb..bf1091b6fc 100644 --- a/packager/media/base/decryptor_source.h +++ b/packager/media/base/decryptor_source.h @@ -1,4 +1,4 @@ -// Copyright 2014 Google Inc. All rights reserved. +// Copyright 2016 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 @@ -7,28 +7,30 @@ #ifndef MEDIA_BASE_DECRYPTOR_SOURCE_H_ #define MEDIA_BASE_DECRYPTOR_SOURCE_H_ -#include "packager/base/memory/scoped_ptr.h" -#include "packager/media/base/container_names.h" -#include "packager/media/base/status.h" +#include +#include + +#include "packager/media/base/aes_encryptor.h" +#include "packager/media/base/decrypt_config.h" +#include "packager/media/base/key_source.h" namespace edash_packager { namespace media { -/// DecryptorSource is responsible for decryption key acquisition. +/// DecryptorSource wraps KeySource and is responsible for decryptor management. class DecryptorSource { public: - DecryptorSource() {} - virtual ~DecryptorSource() {} + explicit DecryptorSource(KeySource* key_source); + ~DecryptorSource(); - /// NeedKey event handler. - /// @param container indicates the container format. - /// @param init_data specifies container dependent initialization data that - /// is used to initialize the decryption key. - /// @return OK on success, an adequate status on error. - virtual Status OnNeedKey(MediaContainerName container, - const std::string& init_data) = 0; + bool DecryptSampleBuffer(const DecryptConfig* decrypt_config, + uint8_t* buffer, + size_t buffer_size); private: + KeySource* key_source_; + std::map, AesCtrEncryptor*> decryptor_map_; + DISALLOW_COPY_AND_ASSIGN(DecryptorSource); }; diff --git a/packager/media/base/decryptor_source_unittest.cc b/packager/media/base/decryptor_source_unittest.cc new file mode 100644 index 0000000000..ae4350c6a4 --- /dev/null +++ b/packager/media/base/decryptor_source_unittest.cc @@ -0,0 +1,172 @@ +// Copyright 2016 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/base/decryptor_source.h" + +#include +#include + +#include "packager/base/macros.h" + +using ::testing::Return; +using ::testing::SetArgPointee; +using ::testing::StrictMock; +using ::testing::Mock; +using ::testing::_; + +namespace edash_packager { +namespace media { +namespace { + +const uint8_t kKeyId[] = { + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, +}; + +const uint8_t kMockKey[] = { + 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, +}; + +const uint8_t kIv[] = { + 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, +}; +const uint8_t kBuffer[] = { + 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x05, 0x06, 0x07, 0x08, 0x09, +}; +// Expected decrypted buffer with the above kMockKey and kIv. +const uint8_t kExpectedDecryptedBuffer[] = { + 0xfd, 0xf9, 0x8b, 0xb2, 0x1d, 0xd3, 0x07, 0x72, 0x51, 0xf4, 0xdf, + 0xf9, 0x16, 0x6a, 0x14, 0xcb, 0xde, 0xaa, 0x6a, 0x04, 0x85, +}; + +const uint8_t kIv2[] = { + 0x14, 0x25, 0x36, 0x47, 0x58, 0x69, 0x7a, 0x8b, +}; +const uint8_t kBuffer2[] = {0x05, 0x02}; +// Expected decrypted buffer with the above kMockKey and kIv2. +const uint8_t kExpectedDecryptedBuffer2[] = {0x20, 0x62}; + +class MockKeySource : public KeySource { + public: + MOCK_METHOD2(GetKey, + Status(const std::vector& key_id, EncryptionKey* key)); +}; + +} // namespace + +class DecryptorSourceTest : public ::testing::Test { + public: + DecryptorSourceTest() + : decryptor_source_(&mock_key_source_), + key_id_(std::vector(kKeyId, kKeyId + arraysize(kKeyId))), + buffer_(std::vector(kBuffer, kBuffer + arraysize(kBuffer))) {} + + protected: + StrictMock mock_key_source_; + DecryptorSource decryptor_source_; + std::vector key_id_; + std::vector buffer_; +}; + +TEST_F(DecryptorSourceTest, FullSampleDecryption) { + EncryptionKey encryption_key; + encryption_key.key.assign(kMockKey, kMockKey + arraysize(kMockKey)); + EXPECT_CALL(mock_key_source_, GetKey(key_id_, _)) + .WillOnce(DoAll(SetArgPointee<1>(encryption_key), Return(Status::OK))); + + DecryptConfig decrypt_config(key_id_, + std::vector(kIv, kIv + arraysize(kIv)), + std::vector()); + ASSERT_TRUE(decryptor_source_.DecryptSampleBuffer(&decrypt_config, &buffer_[0], + buffer_.size())); + EXPECT_EQ(std::vector( + kExpectedDecryptedBuffer, + kExpectedDecryptedBuffer + arraysize(kExpectedDecryptedBuffer)), + buffer_); + + // DecryptSampleBuffer can be called repetitively. No GetKey call again with + // the same key id. + buffer_.assign(kBuffer2, kBuffer2 + arraysize(kBuffer2)); + DecryptConfig decrypt_config2( + key_id_, std::vector(kIv2, kIv2 + arraysize(kIv2)), + std::vector()); + ASSERT_TRUE(decryptor_source_.DecryptSampleBuffer( + &decrypt_config2, &buffer_[0], buffer_.size())); + EXPECT_EQ(std::vector(kExpectedDecryptedBuffer2, + kExpectedDecryptedBuffer2 + + arraysize(kExpectedDecryptedBuffer2)), + buffer_); +} + +TEST_F(DecryptorSourceTest, SubsampleDecryption) { + EncryptionKey encryption_key; + encryption_key.key.assign(kMockKey, kMockKey + arraysize(kMockKey)); + EXPECT_CALL(mock_key_source_, GetKey(key_id_, _)) + .WillOnce(DoAll(SetArgPointee<1>(encryption_key), Return(Status::OK))); + + const SubsampleEntry kSubsamples[] = { + {2, 3}, + {3, 13}, + }; + // Expected decrypted buffer with the above subsamples. + const uint8_t kExpectedDecryptedBuffer[] = { + // Subsample[0].clear + 0x03, 0x04, + // Subsample[0].cipher + 0xfb, 0xfb, 0x89, + // Subsample[1].clear + 0x08, 0x09, 0x0a, + // Subsample[1].cipher + 0xb0, 0x1f, 0xdd, 0x09, 0x70, 0x5c, 0xfb, 0xd2, + 0xfb, 0x18, 0x64, 0x16, 0xc9, + }; + + DecryptConfig decrypt_config( + key_id_, std::vector(kIv, kIv + arraysize(kIv)), + std::vector(kSubsamples, + kSubsamples + arraysize(kSubsamples))); + ASSERT_TRUE(decryptor_source_.DecryptSampleBuffer( + &decrypt_config, &buffer_[0], buffer_.size())); + EXPECT_EQ(std::vector( + kExpectedDecryptedBuffer, + kExpectedDecryptedBuffer + arraysize(kExpectedDecryptedBuffer)), + buffer_); +} + +TEST_F(DecryptorSourceTest, SubsampleDecryptionSizeValidation) { + EncryptionKey encryption_key; + encryption_key.key.assign(kMockKey, kMockKey + arraysize(kMockKey)); + EXPECT_CALL(mock_key_source_, GetKey(key_id_, _)) + .WillOnce(DoAll(SetArgPointee<1>(encryption_key), Return(Status::OK))); + + // Total size exceeds buffer size. + const SubsampleEntry kSubsamples[] = { + {2, 3}, + {3, 14}, + }; + + DecryptConfig decrypt_config( + key_id_, std::vector(kIv, kIv + arraysize(kIv)), + std::vector(kSubsamples, + kSubsamples + arraysize(kSubsamples))); + ASSERT_FALSE(decryptor_source_.DecryptSampleBuffer( + &decrypt_config, &buffer_[0], buffer_.size())); +} + +TEST_F(DecryptorSourceTest, DecryptFailedIfGetKeyFailed) { + EXPECT_CALL(mock_key_source_, GetKey(key_id_, _)) + .WillOnce(Return(Status::UNKNOWN)); + + DecryptConfig decrypt_config(key_id_, + std::vector(kIv, kIv + arraysize(kIv)), + std::vector()); + ASSERT_FALSE(decryptor_source_.DecryptSampleBuffer( + &decrypt_config, &buffer_[0], buffer_.size())); +} + +} // namespace media +} // namespace edash_packager diff --git a/packager/media/base/media_base.gyp b/packager/media/base/media_base.gyp index 20a58a6200..9b28871d95 100644 --- a/packager/media/base/media_base.gyp +++ b/packager/media/base/media_base.gyp @@ -35,6 +35,7 @@ 'demuxer.h', 'decrypt_config.cc', 'decrypt_config.h', + 'decryptor_source.cc', 'decryptor_source.h', 'http_key_fetcher.cc', 'http_key_fetcher.h', @@ -87,6 +88,19 @@ '../../version/version.gyp:version', ], }, + { + 'target_name': 'widevine_pssh_data_proto', + 'type': '<(component)', + 'sources': ['widevine_pssh_data.proto'], + 'variables': { + 'proto_in_dir': '.', + 'proto_out_dir': 'packager/media/base', + }, + 'includes': ['../../build/protoc.gypi'], + 'dependencies': [ + '../../third_party/protobuf/protobuf.gyp:protobuf_lite', + ], + }, { 'target_name': 'media_base_unittest', 'type': '<(gtest_target_type)', @@ -97,6 +111,7 @@ 'buffer_writer_unittest.cc', 'closure_thread_unittest.cc', 'container_names_unittest.cc', + 'decryptor_source_unittest.cc', 'http_key_fetcher_unittest.cc', 'muxer_util_unittest.cc', 'offset_byte_queue_unittest.cc', diff --git a/packager/media/base/widevine_pssh_data.proto b/packager/media/base/widevine_pssh_data.proto new file mode 100644 index 0000000000..62b57f6009 --- /dev/null +++ b/packager/media/base/widevine_pssh_data.proto @@ -0,0 +1,36 @@ +// Copyright 2016 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 +// +// This file defines Widevine Pssh Data proto format. + +syntax = "proto2"; + +package edash_packager.media; + +message WidevinePsshData { + enum Algorithm { + UNENCRYPTED = 0; + AESCTR = 1; + }; + optional Algorithm algorithm = 1; + repeated bytes key_id = 2; + + // Content provider name. + optional string provider = 3; + + // A content identifier, specified by content provider. + optional bytes content_id = 4; + + // The name of a registered policy to be used for this asset. + optional string policy = 6; + + // Crypto period index, for media using key rotation. + optional uint32 crypto_period_index = 7; + + // Optional protected context for group content. The grouped_license is a + // serialized SignedMessage. + optional bytes grouped_license = 8; +} diff --git a/packager/media/formats/mp4/mp4_media_parser.cc b/packager/media/formats/mp4/mp4_media_parser.cc index 38c5022dce..b97a3d4eb6 100644 --- a/packager/media/formats/mp4/mp4_media_parser.cc +++ b/packager/media/formats/mp4/mp4_media_parser.cc @@ -11,7 +11,6 @@ #include "packager/base/logging.h" #include "packager/base/memory/ref_counted.h" #include "packager/base/strings/string_number_conversions.h" -#include "packager/media/base/aes_encryptor.h" #include "packager/media/base/audio_stream_info.h" #include "packager/media/base/buffer_reader.h" #include "packager/media/base/decrypt_config.h" @@ -90,11 +89,12 @@ const uint8_t kDtsAudioNumChannels = 6; } // namespace MP4MediaParser::MP4MediaParser() - : state_(kWaitingForInit), moof_head_(0), mdat_tail_(0) {} + : state_(kWaitingForInit), + decryption_key_source_(NULL), + moof_head_(0), + mdat_tail_(0) {} -MP4MediaParser::~MP4MediaParser() { - STLDeleteValues(&decryptor_map_); -} +MP4MediaParser::~MP4MediaParser() {} void MP4MediaParser::Init(const InitCB& init_cb, const NewSampleCB& new_sample_cb, @@ -108,6 +108,8 @@ void MP4MediaParser::Init(const InitCB& init_cb, init_cb_ = init_cb; new_sample_cb_ = new_sample_cb; decryption_key_source_ = decryption_key_source; + if (decryption_key_source) + decryptor_source_.reset(new DecryptorSource(decryption_key_source)); } void MP4MediaParser::Reset() { @@ -652,11 +654,18 @@ bool MP4MediaParser::EnqueueSample(bool* err) { scoped_refptr stream_sample(MediaSample::CopyFrom( buf, runs_->sample_size(), runs_->is_keyframe())); if (runs_->is_encrypted()) { + if (!decryptor_source_) { + *err = true; + LOG(ERROR) << "Encrypted media sample encountered, but decryption is not " + "enabled"; + return false; + } + scoped_ptr decrypt_config = runs_->GetDecryptConfig(); if (!decrypt_config || - !DecryptSampleBuffer(decrypt_config.get(), - stream_sample->writable_data(), - stream_sample->data_size())) { + !decryptor_source_->DecryptSampleBuffer(decrypt_config.get(), + stream_sample->writable_data(), + stream_sample->data_size())) { *err = true; LOG(ERROR) << "Cannot decrypt samples."; return false; @@ -684,80 +693,6 @@ bool MP4MediaParser::EnqueueSample(bool* err) { return true; } -bool MP4MediaParser::DecryptSampleBuffer(const DecryptConfig* decrypt_config, - uint8_t* buffer, - size_t buffer_size) { - DCHECK(decrypt_config); - DCHECK(buffer); - - if (!decryption_key_source_) { - LOG(ERROR) << "Encrypted media sample encountered, but decryption is not " - "enabled"; - return false; - } - - // Get the encryptor object. - AesCtrEncryptor* encryptor; - DecryptorMap::iterator found = decryptor_map_.find(decrypt_config->key_id()); - if (found == decryptor_map_.end()) { - // Create new AesCtrEncryptor - EncryptionKey key; - Status status(decryption_key_source_->GetKey(decrypt_config->key_id(), - &key)); - if (!status.ok()) { - LOG(ERROR) << "Error retrieving decryption key: " << status; - return false; - } - scoped_ptr new_encryptor(new AesCtrEncryptor); - if (!new_encryptor->InitializeWithIv(key.key, decrypt_config->iv())) { - LOG(ERROR) << "Failed to initialize AesCtrEncryptor for decryption."; - return false; - } - encryptor = new_encryptor.release(); - decryptor_map_[decrypt_config->key_id()] = encryptor; - } else { - encryptor = found->second; - } - if (!encryptor->SetIv(decrypt_config->iv())) { - LOG(ERROR) << "Invalid initialization vector."; - return false; - } - - if (decrypt_config->subsamples().empty()) { - // Sample not encrypted using subsample encryption. Decrypt whole. - if (!encryptor->Decrypt(buffer, buffer_size, buffer)) { - LOG(ERROR) << "Error during bulk sample decryption."; - return false; - } - return true; - } - - // Subsample decryption. - const std::vector& subsamples = decrypt_config->subsamples(); - uint8_t* current_ptr = buffer; - const uint8_t* buffer_end = buffer + buffer_size; - current_ptr += decrypt_config->data_offset(); - if (current_ptr > buffer_end) { - LOG(ERROR) << "Subsample data_offset too large."; - return false; - } - for (std::vector::const_iterator iter = subsamples.begin(); - iter != subsamples.end(); - ++iter) { - if ((current_ptr + iter->clear_bytes + iter->cipher_bytes) > buffer_end) { - LOG(ERROR) << "Subsamples overflow sample buffer."; - return false; - } - current_ptr += iter->clear_bytes; - if (!encryptor->Decrypt(current_ptr, iter->cipher_bytes, current_ptr)) { - LOG(ERROR) << "Error decrypting subsample buffer."; - return false; - } - current_ptr += iter->cipher_bytes; - } - return true; -} - bool MP4MediaParser::ReadAndDiscardMDATsUntil(const int64_t offset) { bool err = false; while (mdat_tail_ < offset) { diff --git a/packager/media/formats/mp4/mp4_media_parser.h b/packager/media/formats/mp4/mp4_media_parser.h index 59824e73df..8333132d32 100644 --- a/packager/media/formats/mp4/mp4_media_parser.h +++ b/packager/media/formats/mp4/mp4_media_parser.h @@ -17,15 +17,12 @@ #include "packager/base/memory/scoped_ptr.h" #include "packager/base/memory/ref_counted.h" #include "packager/base/memory/scoped_ptr.h" +#include "packager/media/base/decryptor_source.h" #include "packager/media/base/media_parser.h" #include "packager/media/base/offset_byte_queue.h" namespace edash_packager { namespace media { - -class AesCtrEncryptor; -class DecryptConfig; - namespace mp4 { class BoxReader; @@ -70,10 +67,6 @@ class MP4MediaParser : public MediaParser { bool FetchKeysIfNecessary( const std::vector& headers); - bool DecryptSampleBuffer(const DecryptConfig* decrypt_config, - uint8_t* buffer, - size_t buffer_size); - // To retain proper framing, each 'mdat' box must be read; to limit memory // usage, the box's data needs to be discarded incrementally as frames are // extracted from the stream. This function discards data from the stream up @@ -94,6 +87,7 @@ class MP4MediaParser : public MediaParser { InitCB init_cb_; NewSampleCB new_sample_cb_; KeySource* decryption_key_source_; + scoped_ptr decryptor_source_; OffsetByteQueue queue_; @@ -110,9 +104,6 @@ class MP4MediaParser : public MediaParser { scoped_ptr moov_; scoped_ptr runs_; - typedef std::map, AesCtrEncryptor*> DecryptorMap; - DecryptorMap decryptor_map_; - DISALLOW_COPY_AND_ASSIGN(MP4MediaParser); }; diff --git a/packager/media/formats/mp4/track_run_iterator.cc b/packager/media/formats/mp4/track_run_iterator.cc index f31f5a83fe..389a6ace33 100644 --- a/packager/media/formats/mp4/track_run_iterator.cc +++ b/packager/media/formats/mp4/track_run_iterator.cc @@ -600,7 +600,6 @@ scoped_ptr TrackRunIterator::GetDecryptConfig() { return scoped_ptr(new DecryptConfig( track_encryption().default_kid, sample_encryption_entry.initialization_vector, - 0, // No offset to start of media data in MP4 using CENC. sample_encryption_entry.subsamples)); } diff --git a/packager/media/formats/webm/webm.gyp b/packager/media/formats/webm/webm.gyp index 0508b31fbc..53bc0660d3 100644 --- a/packager/media/formats/webm/webm.gyp +++ b/packager/media/formats/webm/webm.gyp @@ -58,6 +58,7 @@ '../../../third_party/boringssl/boringssl.gyp:boringssl', '../../../third_party/libwebm/libwebm.gyp:libwebm', '../../base/media_base.gyp:base', + '../../base/media_base.gyp:widevine_pssh_data_proto', '../../filters/filters.gyp:filters' ], }, diff --git a/packager/media/formats/webm/webm_cluster_parser.cc b/packager/media/formats/webm/webm_cluster_parser.cc index bcfb77a1ac..f52f5ee696 100644 --- a/packager/media/formats/webm/webm_cluster_parser.cc +++ b/packager/media/formats/webm/webm_cluster_parser.cc @@ -60,7 +60,8 @@ WebMClusterParser::WebMClusterParser( const std::string& audio_encryption_key_id, const std::string& video_encryption_key_id, const MediaParser::NewSampleCB& new_sample_cb, - const MediaParser::InitCB& init_cb) + const MediaParser::InitCB& init_cb, + KeySource* decryption_key_source) : timecode_multiplier_(timecode_scale / 1000.0), audio_stream_info_(audio_stream_info), video_stream_info_(video_stream_info), @@ -79,6 +80,8 @@ WebMClusterParser::WebMClusterParser( true, video_default_duration, new_sample_cb) { + if (decryption_key_source) + decryptor_source_.reset(new DecryptorSource(decryption_key_source)); for (WebMTracksParser::TextTracks::const_iterator it = text_tracks.begin(); it != text_tracks.end(); ++it) { @@ -326,11 +329,12 @@ bool WebMClusterParser::OnBlock(bool is_simple_block, } Track* track = NULL; - StreamType stream_type = kStreamAudio; + StreamType stream_type = kStreamUnknown; std::string encryption_key_id; if (track_num == audio_.track_num()) { track = &audio_; encryption_key_id = audio_encryption_key_id_; + stream_type = kStreamAudio; } else if (track_num == video_.track_num()) { track = &video_; encryption_key_id = video_encryption_key_id_; @@ -348,6 +352,7 @@ bool WebMClusterParser::OnBlock(bool is_simple_block, LOG(ERROR) << "Unexpected track number " << track_num; return false; } + DCHECK_NE(stream_type, kStreamUnknown); last_block_timecode_ = timecode; @@ -384,9 +389,19 @@ bool WebMClusterParser::OnBlock(bool is_simple_block, buffer = MediaSample::CopyFrom(data + data_offset, size - data_offset, additional, additional_size, is_keyframe); - if (decrypt_config) { - // TODO(kqyang): Decrypt it if it is encrypted. - buffer->set_is_encrypted(true); + // An empty iv indicates that this sample is not encrypted. + if (decrypt_config && !decrypt_config->iv().empty()) { + if (!decryptor_source_) { + LOG(ERROR) << "Encrypted media sample encountered, but decryption is " + "not enabled"; + return false; + } + if (!decryptor_source_->DecryptSampleBuffer(decrypt_config.get(), + buffer->writable_data(), + buffer->data_size())) { + LOG(ERROR) << "Cannot decrypt samples"; + return false; + } } } else { std::string id, settings, content; diff --git a/packager/media/formats/webm/webm_cluster_parser.h b/packager/media/formats/webm/webm_cluster_parser.h index ee27bb928a..586b656385 100644 --- a/packager/media/formats/webm/webm_cluster_parser.h +++ b/packager/media/formats/webm/webm_cluster_parser.h @@ -11,6 +11,7 @@ #include #include "packager/base/memory/scoped_ptr.h" +#include "packager/media/base/decryptor_source.h" #include "packager/media/base/media_parser.h" #include "packager/media/base/media_sample.h" #include "packager/media/formats/webm/webm_parser.h" @@ -93,6 +94,28 @@ class WebMClusterParser : public WebMParserClient { typedef std::map TextTrackMap; public: + /// Create a WebMClusterParser from given parameters. + /// @param timecode_scale indicates timecode scale for the clusters. + /// @param audio_stream_info references audio stream information. It will + /// be NULL if there are no audio tracks available. + /// @param video_stream_info references video stream information. It will + /// be NULL if there are no video tracks available. + /// @param audio_default_duration indicates default duration for audio + /// samples. + /// @param video_default_duration indicates default duration for video + /// samples. + /// @param text_tracks contains text track information. + /// @param ignored_tracks contains a list of ignored track ids. + /// @param audio_encryption_key_id indicates the encryption key id for audio + /// samples if there is an audio stream and the audio stream is + /// encrypted. It is empty otherwise. + /// @param video_encryption_key_id indicates the encryption key id for video + /// samples if there is a video stream and the video stream is + /// encrypted. It is empty otherwise. + /// @param new_sample_cb is the callback to emit new samples. + /// @param init_cb is the callback to initialize streams. + /// @param decryption_key_source points to a decryption key source to fetch + /// decryption keys. Should not be NULL if the tracks are encrypted. WebMClusterParser(int64_t timecode_scale, scoped_refptr audio_stream_info, scoped_refptr video_stream_info, @@ -103,7 +126,8 @@ class WebMClusterParser : public WebMParserClient { const std::string& audio_encryption_key_id, const std::string& video_encryption_key_id, const MediaParser::NewSampleCB& new_sample_cb, - const MediaParser::InitCB& init_cb); + const MediaParser::InitCB& init_cb, + KeySource* decryption_key_source); ~WebMClusterParser() override; /// Resets the parser state so it can accept a new cluster. @@ -162,6 +186,8 @@ class WebMClusterParser : public WebMParserClient { scoped_refptr audio_stream_info_; scoped_refptr video_stream_info_; std::set ignored_tracks_; + + scoped_ptr decryptor_source_; std::string audio_encryption_key_id_; std::string video_encryption_key_id_; diff --git a/packager/media/formats/webm/webm_cluster_parser_unittest.cc b/packager/media/formats/webm/webm_cluster_parser_unittest.cc index a167e8d7cf..3c21a173ca 100644 --- a/packager/media/formats/webm/webm_cluster_parser_unittest.cc +++ b/packager/media/formats/webm/webm_cluster_parser_unittest.cc @@ -23,6 +23,7 @@ using ::testing::HasSubstr; using ::testing::InSequence; using ::testing::Return; +using ::testing::SetArgPointee; using ::testing::StrictMock; using ::testing::Mock; using ::testing::_; @@ -137,6 +138,23 @@ const uint8_t kEncryptedFrame[] = { // Some dummy encrypted data 0x01, }; +const uint8_t kMockKey[] = { + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x00, +}; +const uint8_t kExpectedDecryptedFrame[] = { + 0x41, +}; + +const uint8_t kClearFrameInEncryptedTrack[] = { + // Block is not encrypted + 0x00, + // Some dummy frame data + 0x01, 0x02, 0x03, +}; +const uint8_t kExpectedClearFrame[] = { + 0x01, 0x02, 0x03, +}; const uint8_t kVP8Frame[] = { 0x52, 0x04, 0x00, 0x9d, 0x01, 0x2a, 0x40, 0x01, 0xf0, 0x00, 0x00, 0x47, @@ -153,6 +171,12 @@ const uint8_t kVP9Frame[] = { 0xe1, 0xe6, 0xef, 0xff, 0xfd, 0xf7, 0x4f, 0x0f, }; +class MockKeySource : public KeySource { + public: + MOCK_METHOD2(GetKey, + Status(const std::vector& key_id, EncryptionKey* key)); +}; + scoped_ptr CreateCluster(int timecode, const BlockInfo* block_info, int block_count) { @@ -284,10 +308,6 @@ bool VerifyTextBuffers(const BlockInfo* block_info_ptr, return true; } -void VerifyEncryptedBuffer(scoped_refptr buffer) { - EXPECT_TRUE(buffer->is_encrypted()); -} - } // namespace class WebMClusterParserTest : public testing::Test { @@ -382,7 +402,7 @@ class WebMClusterParserTest : public testing::Test { ignored_tracks, audio_encryption_key_id, video_encryption_key_id, base::Bind(&WebMClusterParserTest::NewSampleEvent, base::Unretained(this)), - init_cb); + init_cb, &mock_key_source_); } // Create a default version of the parser for test. @@ -450,6 +470,7 @@ class WebMClusterParserTest : public testing::Test { BufferQueue audio_buffers_; BufferQueue video_buffers_; TextBufferQueueMap text_buffers_map_; + StrictMock mock_key_source_; private: DISALLOW_COPY_AND_ASSIGN(WebMClusterParserTest); @@ -806,6 +827,36 @@ TEST_F(WebMClusterParserTest, ParseVP9) { } TEST_F(WebMClusterParserTest, ParseEncryptedBlock) { + const std::string video_key_id("video_key_id"); + + EncryptionKey encryption_key; + encryption_key.key.assign(kMockKey, kMockKey + arraysize(kMockKey)); + EXPECT_CALL( + mock_key_source_, + GetKey(std::vector(video_key_id.begin(), video_key_id.end()), _)) + .WillOnce(DoAll(SetArgPointee<1>(encryption_key), Return(Status::OK))); + + scoped_ptr cluster( + CreateCluster(kEncryptedFrame, arraysize(kEncryptedFrame))); + + parser_.reset(CreateParserWithKeyIdsAndAudioCodec(std::string(), video_key_id, + kUnknownAudioCodec)); + + int result = parser_->Parse(cluster->data(), cluster->size()); + EXPECT_EQ(cluster->size(), result); + parser_->Flush(); + ASSERT_EQ(1UL, video_buffers_.size()); + scoped_refptr buffer = video_buffers_[0]; + EXPECT_EQ(std::vector( + kExpectedDecryptedFrame, + kExpectedDecryptedFrame + arraysize(kExpectedDecryptedFrame)), + std::vector(buffer->data(), + buffer->data() + buffer->data_size())); +} + +TEST_F(WebMClusterParserTest, ParseEncryptedBlockGetKeyFailed) { + EXPECT_CALL(mock_key_source_, GetKey(_, _)).WillOnce(Return(Status::UNKNOWN)); + scoped_ptr cluster( CreateCluster(kEncryptedFrame, arraysize(kEncryptedFrame))); @@ -813,11 +864,7 @@ TEST_F(WebMClusterParserTest, ParseEncryptedBlock) { std::string(), "video_key_id", kUnknownAudioCodec)); int result = parser_->Parse(cluster->data(), cluster->size()); - EXPECT_EQ(cluster->size(), result); - parser_->Flush(); - ASSERT_EQ(1UL, video_buffers_.size()); - scoped_refptr buffer = video_buffers_[0]; - VerifyEncryptedBuffer(buffer); + EXPECT_EQ(-1, result); } TEST_F(WebMClusterParserTest, ParseBadEncryptedBlock) { @@ -830,6 +877,25 @@ TEST_F(WebMClusterParserTest, ParseBadEncryptedBlock) { EXPECT_EQ(-1, result); } +TEST_F(WebMClusterParserTest, ParseClearFrameInEncryptedTrack) { + scoped_ptr cluster(CreateCluster( + kClearFrameInEncryptedTrack, arraysize(kClearFrameInEncryptedTrack))); + + parser_.reset(CreateParserWithKeyIdsAndAudioCodec( + std::string(), "video_key_id", kUnknownAudioCodec)); + + int result = parser_->Parse(cluster->data(), cluster->size()); + EXPECT_EQ(cluster->size(), result); + parser_->Flush(); + ASSERT_EQ(1UL, video_buffers_.size()); + scoped_refptr buffer = video_buffers_[0]; + EXPECT_EQ(std::vector( + kExpectedClearFrame, + kExpectedClearFrame + arraysize(kExpectedClearFrame)), + std::vector(buffer->data(), + buffer->data() + buffer->data_size())); +} + TEST_F(WebMClusterParserTest, ParseInvalidZeroSizedCluster) { const uint8_t kBuffer[] = { 0x1F, 0x43, 0xB6, 0x75, 0x80, // CLUSTER (size = 0) diff --git a/packager/media/formats/webm/webm_crypto_helpers.cc b/packager/media/formats/webm/webm_crypto_helpers.cc index 0dc0ee7f06..e22a4b6423 100644 --- a/packager/media/formats/webm/webm_crypto_helpers.cc +++ b/packager/media/formats/webm/webm_crypto_helpers.cc @@ -56,7 +56,7 @@ bool WebMCreateDecryptConfig(const uint8_t* data, decrypt_config->reset(new DecryptConfig( std::vector(key_id, key_id + key_id_size), std::vector(counter_block.begin(), counter_block.end()), - frame_offset, std::vector())); + std::vector())); *data_offset = frame_offset; return true; diff --git a/packager/media/formats/webm/webm_media_parser.cc b/packager/media/formats/webm/webm_media_parser.cc index 14b2823cd8..107ee69664 100644 --- a/packager/media/formats/webm/webm_media_parser.cc +++ b/packager/media/formats/webm/webm_media_parser.cc @@ -9,7 +9,9 @@ #include "packager/base/callback.h" #include "packager/base/callback_helpers.h" #include "packager/base/logging.h" +#include "packager/media/base/buffer_writer.h" #include "packager/media/base/timestamp.h" +#include "packager/media/base/widevine_pssh_data.pb.h" #include "packager/media/formats/webm/webm_cluster_parser.h" #include "packager/media/formats/webm/webm_constants.h" #include "packager/media/formats/webm/webm_content_encodings.h" @@ -185,8 +187,6 @@ int WebMMediaParser::ParseInfoAndTracks(const uint8_t* data, int size) { tracks_parser.audio_stream_info(); if (audio_stream_info) { audio_stream_info->set_duration(duration_in_us); - if (audio_stream_info->is_encrypted()) - OnEncryptedMediaInitData(tracks_parser.audio_encryption_key_id()); } else { VLOG(1) << "No audio track info found."; } @@ -195,19 +195,23 @@ int WebMMediaParser::ParseInfoAndTracks(const uint8_t* data, int size) { tracks_parser.video_stream_info(); if (video_stream_info) { video_stream_info->set_duration(duration_in_us); - if (video_stream_info->is_encrypted()) - OnEncryptedMediaInitData(tracks_parser.video_encryption_key_id()); } else { VLOG(1) << "No video track info found."; } + if (!FetchKeysIfNecessary(tracks_parser.audio_encryption_key_id(), + tracks_parser.video_encryption_key_id())) { + return -1; + } + cluster_parser_.reset(new WebMClusterParser( info_parser.timecode_scale(), audio_stream_info, video_stream_info, tracks_parser.GetAudioDefaultDuration(timecode_scale_in_us), tracks_parser.GetVideoDefaultDuration(timecode_scale_in_us), tracks_parser.text_tracks(), tracks_parser.ignored_tracks(), tracks_parser.audio_encryption_key_id(), - tracks_parser.video_encryption_key_id(), new_sample_cb_, init_cb_)); + tracks_parser.video_encryption_key_id(), new_sample_cb_, init_cb_, + decryption_key_source_)); return bytes_parsed; } @@ -228,8 +232,30 @@ int WebMMediaParser::ParseCluster(const uint8_t* data, int size) { return bytes_parsed; } -void WebMMediaParser::OnEncryptedMediaInitData(const std::string& key_id) { - NOTIMPLEMENTED() << "WebM decryption is not implemented yet."; +bool WebMMediaParser::FetchKeysIfNecessary( + const std::string& audio_encryption_key_id, + const std::string& video_encryption_key_id) { + if (audio_encryption_key_id.empty() && video_encryption_key_id.empty()) + return true; + // An error will be returned later if the samples need to be derypted. + if (!decryption_key_source_) + return true; + + // Generate WidevinePsshData from key_id. + WidevinePsshData widevine_pssh_data; + if (!audio_encryption_key_id.empty()) + widevine_pssh_data.add_key_id(audio_encryption_key_id); + if (!video_encryption_key_id.empty()) + widevine_pssh_data.add_key_id(video_encryption_key_id); + + const std::string serialized_string = widevine_pssh_data.SerializeAsString(); + Status status = decryption_key_source_->FetchKeys( + std::vector(serialized_string.begin(), serialized_string.end())); + if (!status.ok()) { + LOG(ERROR) << "Error fetching decryption keys: " << status; + return false; + } + return true; } } // namespace media diff --git a/packager/media/formats/webm/webm_media_parser.h b/packager/media/formats/webm/webm_media_parser.h index 80634c5dc9..5e90d0a598 100644 --- a/packager/media/formats/webm/webm_media_parser.h +++ b/packager/media/formats/webm/webm_media_parser.h @@ -56,8 +56,9 @@ class WebMMediaParser : public MediaParser { // Returning > 0 indicates success & the number of bytes parsed. int ParseCluster(const uint8_t* data, int size); - // Fire needkey event through the |encrypted_media_init_data_cb_|. - void OnEncryptedMediaInitData(const std::string& key_id); + // Fetch keys for the input key ids. Returns true on success, false otherwise. + bool FetchKeysIfNecessary(const std::string& audio_encryption_key_id, + const std::string& video_encryption_key_id); State state_; InitCB init_cb_;