diff options
author | xhwang@chromium.org <xhwang@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-03-08 05:09:27 +0000 |
---|---|---|
committer | xhwang@chromium.org <xhwang@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-03-08 05:09:27 +0000 |
commit | 08819ae89715143453b592abe0115ac1d3076003 (patch) | |
tree | 2f271a0ad88893d89a49703a5f6873ede8b5c073 | |
parent | a8076b782e9d36f69651c84d5aee2a5ae321dca7 (diff) | |
download | chromium_src-08819ae89715143453b592abe0115ac1d3076003.zip chromium_src-08819ae89715143453b592abe0115ac1d3076003.tar.gz chromium_src-08819ae89715143453b592abe0115ac1d3076003.tar.bz2 |
Add AES decryptor and tests.
For now we support decryption in video only. The first encryption key ID in ContentEncodings element will be used as the decryption key ID. Also we assume decryption key is the same as key ID.
BUG=117060
TEST=test page with encrypted content plays; added media_unittest
Review URL: http://codereview.chromium.org/9298021
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@125560 0039d316-1c4b-4281-b951-d872f2087c98
24 files changed, 459 insertions, 34 deletions
diff --git a/media/base/buffers.cc b/media/base/buffers.cc index 9f59c18..bfc1189 100644 --- a/media/base/buffers.cc +++ b/media/base/buffers.cc @@ -14,4 +14,8 @@ bool Buffer::IsEndOfStream() const { return GetData() == NULL; } +const DecryptConfig* Buffer::GetDecryptConfig() const { + return NULL; +} + } // namespace media diff --git a/media/base/buffers.h b/media/base/buffers.h index cbe89e1..379b175 100644 --- a/media/base/buffers.h +++ b/media/base/buffers.h @@ -34,6 +34,8 @@ namespace media { +class DecryptConfig; + // Indicates an invalid or missing timestamp. MEDIA_EXPORT extern inline base::TimeDelta kNoTimestamp() { return base::TimeDelta::FromMicroseconds(kint64min); @@ -84,7 +86,6 @@ class MEDIA_EXPORT StreamSample DISALLOW_COPY_AND_ASSIGN(StreamSample); }; - class MEDIA_EXPORT Buffer : public StreamSample { public: // Returns a read only pointer to the buffer data. @@ -96,6 +97,9 @@ class MEDIA_EXPORT Buffer : public StreamSample { // If there's no data in this buffer, it represents end of stream. virtual bool IsEndOfStream() const OVERRIDE; + // Return DecryptConfig if buffer is encrypted, or NULL otherwise. + virtual const DecryptConfig* GetDecryptConfig() const; + protected: virtual ~Buffer() {} }; diff --git a/media/base/data_buffer.cc b/media/base/data_buffer.cc index b82de51..3cfb254 100644 --- a/media/base/data_buffer.cc +++ b/media/base/data_buffer.cc @@ -4,6 +4,10 @@ #include "base/logging.h" #include "media/base/data_buffer.h" +#include "media/base/decrypt_config.h" +#if !defined(OS_ANDROID) +#include "media/ffmpeg/ffmpeg_common.h" +#endif namespace media { @@ -27,6 +31,23 @@ DataBuffer::DataBuffer(size_t buffer_size) DataBuffer::~DataBuffer() { } +scoped_refptr<DataBuffer> DataBuffer::CopyFrom(const uint8* data, + size_t data_size) { + size_t padding_size = 0; +#if !defined(OS_ANDROID) + // Why FF_INPUT_BUFFER_PADDING_SIZE? FFmpeg assumes all input buffers are + // padded with this value. + padding_size = FF_INPUT_BUFFER_PADDING_SIZE; +#endif + + scoped_refptr<DataBuffer> data_buffer( + new DataBuffer(data_size + padding_size)); + memcpy(data_buffer->data_.get(), data, data_size); + memset(data_buffer->data_.get() + data_size, 0, padding_size); + data_buffer->SetDataSize(data_size); + return data_buffer; +} + const uint8* DataBuffer::GetData() const { return data_.get(); } @@ -35,6 +56,10 @@ size_t DataBuffer::GetDataSize() const { return data_size_; } +const DecryptConfig* DataBuffer::GetDecryptConfig() const { + return decrypt_config_.get(); +} + uint8* DataBuffer::GetWritableData() { return data_.get(); } @@ -49,4 +74,8 @@ size_t DataBuffer::GetBufferSize() const { return buffer_size_; } +void DataBuffer::SetDecryptConfig(scoped_ptr<DecryptConfig> decrypt_config) { + decrypt_config_ = decrypt_config.Pass(); +} + } // namespace media diff --git a/media/base/data_buffer.h b/media/base/data_buffer.h index 6c5c8a9..651088f 100644 --- a/media/base/data_buffer.h +++ b/media/base/data_buffer.h @@ -24,9 +24,13 @@ class MEDIA_EXPORT DataBuffer : public Buffer { // set to a NULL ptr. explicit DataBuffer(size_t buffer_size); + // Create a DataBuffer whose |data_| is copied from |data|. + static scoped_refptr<DataBuffer> CopyFrom(const uint8* data, size_t size); + // Buffer implementation. virtual const uint8* GetData() const OVERRIDE; virtual size_t GetDataSize() const OVERRIDE; + virtual const DecryptConfig* GetDecryptConfig() const OVERRIDE; // Returns a read-write pointer to the buffer data. virtual uint8* GetWritableData(); @@ -38,6 +42,8 @@ class MEDIA_EXPORT DataBuffer : public Buffer { // Returns the size of the underlying buffer. virtual size_t GetBufferSize() const; + virtual void SetDecryptConfig(scoped_ptr<DecryptConfig> decrypt_config); + protected: virtual ~DataBuffer(); @@ -45,6 +51,7 @@ class MEDIA_EXPORT DataBuffer : public Buffer { scoped_array<uint8> data_; size_t buffer_size_; size_t data_size_; + scoped_ptr<DecryptConfig> decrypt_config_; DISALLOW_COPY_AND_ASSIGN(DataBuffer); }; diff --git a/media/base/decrypt_config.cc b/media/base/decrypt_config.cc new file mode 100644 index 0000000..9ae5f19 --- /dev/null +++ b/media/base/decrypt_config.cc @@ -0,0 +1,20 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "media/base/decrypt_config.h" + +#include "base/logging.h" + +namespace media { + +DecryptConfig::DecryptConfig(const uint8* key_id, int key_id_size) + : key_id_size_(key_id_size) { + CHECK_GT(key_id_size, 0); + key_id_.reset(new uint8[key_id_size]); + memcpy(key_id_.get(), key_id, key_id_size); +} + +DecryptConfig::~DecryptConfig() {} + +} // namespace media diff --git a/media/base/decrypt_config.h b/media/base/decrypt_config.h new file mode 100644 index 0000000..5fca787 --- /dev/null +++ b/media/base/decrypt_config.h @@ -0,0 +1,32 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef MEDIA_BASE_DECRYPT_CONFIG_H_ +#define MEDIA_BASE_DECRYPT_CONFIG_H_ + +#include "base/basictypes.h" +#include "base/memory/scoped_ptr.h" +#include "media/base/media_export.h" + +namespace media { + +// Contains all information that a decryptor needs to decrypt. +class MEDIA_EXPORT DecryptConfig { + public: + explicit DecryptConfig(const uint8* key_id, int key_id_size); + ~DecryptConfig(); + + const uint8* key_id() const { return key_id_.get(); } + int key_id_size() const { return key_id_size_; } + + private: + scoped_array<uint8> key_id_; + int key_id_size_; + + DISALLOW_COPY_AND_ASSIGN(DecryptConfig); +}; + +} // namespace media + +#endif // MEDIA_BASE_DECRYPT_CONFIG_H_ diff --git a/media/base/test_data_util.cc b/media/base/test_data_util.cc index 15284da..bb08175 100644 --- a/media/base/test_data_util.cc +++ b/media/base/test_data_util.cc @@ -7,6 +7,8 @@ #include "base/file_util.h" #include "base/logging.h" #include "base/path_service.h" +#include "media/base/buffers.h" +#include "media/base/data_buffer.h" #include "media/ffmpeg/ffmpeg_common.h" namespace media { @@ -51,11 +53,19 @@ void ReadTestDataFile(const std::string& name, scoped_array<uint8>* buffer, *size = file_size; } -void ReadTestDataFile(const std::string& name, scoped_refptr<Buffer>* buffer) { +void ReadTestDataFile(const std::string& name, + scoped_refptr<DataBuffer>* buffer) { scoped_array<uint8> buf; int buf_size; ReadTestDataFile(name, &buf, &buf_size); *buffer = new DataBuffer(buf.Pass(), buf_size); } +void ReadTestDataFile(const std::string& name, + scoped_refptr<Buffer>* buffer) { + scoped_refptr<DataBuffer> data_buffer; + ReadTestDataFile(name, &data_buffer); + *buffer = data_buffer; +} + } // namespace media diff --git a/media/base/test_data_util.h b/media/base/test_data_util.h index 142780b..a132875 100644 --- a/media/base/test_data_util.h +++ b/media/base/test_data_util.h @@ -8,11 +8,14 @@ #include <string> #include "base/basictypes.h" +#include "base/memory/ref_counted.h" #include "base/memory/scoped_ptr.h" -#include "media/base/data_buffer.h" namespace media { +class Buffer; +class DataBuffer; + // Returns a URL path for a file in the media/test/data directory. std::string GetTestDataURL(const std::string& name); @@ -27,6 +30,14 @@ void ReadTestDataFile(const std::string& name, int* size); // Reads a test file from media/test/data directory and stores it in +// a DataBuffer. +// +// |name| - The name of the file. +// |buffer| - The contents of the file. +void ReadTestDataFile(const std::string& name, + scoped_refptr<DataBuffer>* buffer); + +// Reads a test file from media/test/data directory and stores it in // a Buffer. // // |name| - The name of the file. diff --git a/media/crypto/DEPS b/media/crypto/DEPS new file mode 100644 index 0000000..4ef4138 --- /dev/null +++ b/media/crypto/DEPS @@ -0,0 +1,3 @@ +include_rules = [ + "+crypto", +] diff --git a/media/crypto/aes_decryptor.cc b/media/crypto/aes_decryptor.cc new file mode 100644 index 0000000..8e83daf --- /dev/null +++ b/media/crypto/aes_decryptor.cc @@ -0,0 +1,78 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "media/crypto/aes_decryptor.h" + +#include <string> + +#include "base/logging.h" +#include "crypto/encryptor.h" +#include "crypto/symmetric_key.h" +#include "media/base/buffers.h" +#include "media/base/data_buffer.h" +#include "media/base/decrypt_config.h" + +namespace media { + +static const char* kInitialCounter = "0000000000000000"; + +// Decrypts |input| using |raw_key|, which is the binary data for the decryption +// key. +// Return a scoped_refptr to a Buffer object with the decrypted data on success. +// Return a scoped_refptr to NULL if the data could not be decrypted. +// TODO(xhwang): Both the input and output are copied! Any performance concern? +static scoped_refptr<Buffer> DecryptData(const Buffer& input, + const uint8* raw_key, + int raw_key_size) { + CHECK(raw_key && raw_key_size > 0); + CHECK(input.GetDataSize()); + + scoped_ptr<crypto::SymmetricKey> key(crypto::SymmetricKey::Import( + crypto::SymmetricKey::AES, + std::string(reinterpret_cast<const char*>(raw_key), raw_key_size))); + if (!key.get()) { + DVLOG(1) << "Could not import key."; + return NULL; + } + + // Initialize encryption data. + // The IV must be exactly as long as the cipher block size. + crypto::Encryptor encryptor; + if (!encryptor.Init(key.get(), crypto::Encryptor::CBC, kInitialCounter)) { + DVLOG(1) << "Could not initialize encryptor."; + return NULL; + } + + std::string decrypt_text; + std::string encrypted_text(reinterpret_cast<const char*>(input.GetData()), + input.GetDataSize()); + if (!encryptor.Decrypt(encrypted_text, &decrypt_text)) { + DVLOG(1) << "Could not decrypt data."; + return NULL; + } + + return DataBuffer::CopyFrom( + reinterpret_cast<const uint8*>(decrypt_text.data()), + decrypt_text.size()); +} + +AesDecryptor::AesDecryptor() {} + +scoped_refptr<Buffer> AesDecryptor::Decrypt( + const scoped_refptr<Buffer>& encrypted) { + CHECK(encrypted->GetDecryptConfig()); + + // For now, the key is the key ID. + const uint8* key = encrypted->GetDecryptConfig()->key_id(); + int key_size = encrypted->GetDecryptConfig()->key_id_size(); + scoped_refptr<Buffer> decrypted = DecryptData(*encrypted, key, key_size); + if (decrypted) { + decrypted->SetTimestamp(encrypted->GetTimestamp()); + decrypted->SetDuration(encrypted->GetDuration()); + } + + return decrypted; +} + +} // namespace media diff --git a/media/crypto/aes_decryptor.h b/media/crypto/aes_decryptor.h new file mode 100644 index 0000000..32d6802 --- /dev/null +++ b/media/crypto/aes_decryptor.h @@ -0,0 +1,32 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef MEDIA_CRYPTO_AES_DECRYPTOR_H_ +#define MEDIA_CRYPTO_AES_DECRYPTOR_H_ + +#include "base/basictypes.h" +#include "base/memory/ref_counted.h" +#include "media/base/media_export.h" + +namespace media { + +class Buffer; + +// Decrypts AES encrypted buffer into unencrypted buffer. +class MEDIA_EXPORT AesDecryptor { + public: + AesDecryptor(); + + // Decrypt |input| buffer. The |input| should not be NULL. + // Return a Buffer that contains decrypted data if decryption succeeded. + // Return NULL if decryption failed. + scoped_refptr<Buffer> Decrypt(const scoped_refptr<Buffer>& input); + + private: + DISALLOW_COPY_AND_ASSIGN(AesDecryptor); +}; + +} // namespace media + +#endif // MEDIA_CRYPTO_AES_DECRYPTOR_H_ diff --git a/media/crypto/aes_decryptor_unittest.cc b/media/crypto/aes_decryptor_unittest.cc new file mode 100644 index 0000000..0143bd0 --- /dev/null +++ b/media/crypto/aes_decryptor_unittest.cc @@ -0,0 +1,56 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <string> + +#include "media/base/data_buffer.h" +#include "media/base/decrypt_config.h" +#include "media/crypto/aes_decryptor.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace media { + +// |kEncryptedDataHex| is encrypted from |kOriginalData| using |kRawKey|, whose +// length is |kKeySize|. Modifying any of these independently would fail the +// test. +static const char kOriginalData[] = "Original data."; +static const int kEncryptedDataSize = 16; +static const unsigned char kEncryptedData[] = + "\x82\x3A\x76\x92\xEC\x7F\xF8\x85\xEC\x23\x52\xFB\x19\xB1\xB9\x09"; +static const int kKeySize = 16; +static const unsigned char kRawKey[] = "A wonderful key!"; +static const unsigned char kWrongKey[] = "I'm a wrong key."; + +class AesDecryptorTest : public testing::Test { + public: + AesDecryptorTest() { + encrypted_data_ = DataBuffer::CopyFrom(kEncryptedData, kEncryptedDataSize); + } + + protected: + void SetKey(const uint8* key, int key_size) { + encrypted_data_->SetDecryptConfig( + scoped_ptr<DecryptConfig>(new DecryptConfig(key, key_size))); + } + + scoped_refptr<DataBuffer> encrypted_data_; + AesDecryptor decryptor_; +}; + +TEST_F(AesDecryptorTest, NormalDecryption) { + SetKey(kRawKey, kKeySize); + scoped_refptr<Buffer> decrypted_data = decryptor_.Decrypt(encrypted_data_); + ASSERT_TRUE(decrypted_data.get()); + size_t data_length = sizeof(kOriginalData) - 1; + ASSERT_EQ(data_length, decrypted_data->GetDataSize()); + ASSERT_EQ(0, memcmp(kOriginalData, decrypted_data->GetData(), data_length)); +} + +TEST_F(AesDecryptorTest, WrongKey) { + SetKey(kWrongKey, kKeySize); + scoped_refptr<Buffer> decrypted_data = decryptor_.Decrypt(encrypted_data_); + EXPECT_FALSE(decrypted_data.get()); +} + +} // media diff --git a/media/filters/chunk_demuxer.cc b/media/filters/chunk_demuxer.cc index 70b0f46..d8041b1 100644 --- a/media/filters/chunk_demuxer.cc +++ b/media/filters/chunk_demuxer.cc @@ -711,7 +711,6 @@ bool ChunkDemuxer::OnNewVideoConfig(const VideoDecoderConfig& config) { return true; } - bool ChunkDemuxer::OnAudioBuffers(const BufferQueue& buffers) { if (!audio_.get()) return false; diff --git a/media/filters/ffmpeg_audio_decoder_unittest.cc b/media/filters/ffmpeg_audio_decoder_unittest.cc index f580e79..eb634c7 100644 --- a/media/filters/ffmpeg_audio_decoder_unittest.cc +++ b/media/filters/ffmpeg_audio_decoder_unittest.cc @@ -7,6 +7,7 @@ #include "base/bind.h" #include "base/message_loop.h" #include "base/stringprintf.h" +#include "media/base/data_buffer.h" #include "media/base/mock_callback.h" #include "media/base/mock_filters.h" #include "media/base/test_data_util.h" diff --git a/media/filters/ffmpeg_video_decoder.cc b/media/filters/ffmpeg_video_decoder.cc index 61932ff..fe53f35 100644 --- a/media/filters/ffmpeg_video_decoder.cc +++ b/media/filters/ffmpeg_video_decoder.cc @@ -256,8 +256,19 @@ void FFmpegVideoDecoder::DoDecodeBuffer(const scoped_refptr<Buffer>& buffer) { state_ = kFlushCodec; } + scoped_refptr<Buffer> unencrypted_buffer = buffer; + if (buffer->GetDecryptConfig() && buffer->GetDataSize()) { + unencrypted_buffer = decryptor_.Decrypt(buffer); + if (!unencrypted_buffer || !unencrypted_buffer->GetDataSize()) { + state_ = kDecodeFinished; + DeliverFrame(VideoFrame::CreateEmptyFrame()); + host()->SetError(PIPELINE_ERROR_DECODE); + return; + } + } + scoped_refptr<VideoFrame> video_frame; - if (!Decode(buffer, &video_frame)) { + if (!Decode(unencrypted_buffer, &video_frame)) { state_ = kDecodeFinished; DeliverFrame(VideoFrame::CreateEmptyFrame()); host()->SetError(PIPELINE_ERROR_DECODE); diff --git a/media/filters/ffmpeg_video_decoder.h b/media/filters/ffmpeg_video_decoder.h index 19c939c..1e9f74d 100644 --- a/media/filters/ffmpeg_video_decoder.h +++ b/media/filters/ffmpeg_video_decoder.h @@ -9,6 +9,7 @@ #include "base/memory/scoped_ptr.h" #include "media/base/filters.h" +#include "media/crypto/aes_decryptor.h" #include "ui/gfx/size.h" class MessageLoop; @@ -90,6 +91,8 @@ class MEDIA_EXPORT FFmpegVideoDecoder : public VideoDecoder { // Pointer to the demuxer stream that will feed us compressed buffers. scoped_refptr<DemuxerStream> demuxer_stream_; + AesDecryptor decryptor_; + DISALLOW_COPY_AND_ASSIGN(FFmpegVideoDecoder); }; diff --git a/media/filters/ffmpeg_video_decoder_unittest.cc b/media/filters/ffmpeg_video_decoder_unittest.cc index 939e689..ec583ab 100644 --- a/media/filters/ffmpeg_video_decoder_unittest.cc +++ b/media/filters/ffmpeg_video_decoder_unittest.cc @@ -9,6 +9,7 @@ #include "base/memory/singleton.h" #include "base/string_util.h" #include "media/base/data_buffer.h" +#include "media/base/decrypt_config.h" #include "media/base/filters.h" #include "media/base/limits.h" #include "media/base/mock_callback.h" @@ -36,6 +37,9 @@ static const gfx::Rect kVisibleRect(320, 240); static const gfx::Size kNaturalSize(522, 288); static const AVRational kFrameRate = { 100, 1 }; static const AVRational kAspectRatio = { 1, 1 }; +static const int kKeySize = 16; +static const unsigned char kRawKey[] = "A wonderful key!"; +static const unsigned char kWrongKey[] = "I'm a wrong key."; ACTION_P(ReturnBuffer, buffer) { arg0.Run(buffer); @@ -57,6 +61,8 @@ class FFmpegVideoDecoderTest : public testing::Test { end_of_stream_buffer_ = new DataBuffer(0); ReadTestDataFile("vp8-I-frame-320x240", &i_frame_buffer_); ReadTestDataFile("vp8-corrupt-I-frame", &corrupt_i_frame_buffer_); + ReadTestDataFile("vp8-encrypted-I-frame-320x240", + &encrypted_i_frame_buffer_); config_.Initialize(kCodecVP8, VIDEO_CODEC_PROFILE_UNKNOWN, kVideoFormat, kCodedSize, kVisibleRect, @@ -203,6 +209,7 @@ class FFmpegVideoDecoderTest : public testing::Test { scoped_refptr<Buffer> end_of_stream_buffer_; scoped_refptr<Buffer> i_frame_buffer_; scoped_refptr<Buffer> corrupt_i_frame_buffer_; + scoped_refptr<DataBuffer> encrypted_i_frame_buffer_; // Used for generating timestamped buffers. std::deque<int64> timestamps_; @@ -285,7 +292,7 @@ TEST_F(FFmpegVideoDecoderTest, DecodeFrame_0ByteFrame) { ASSERT_TRUE(video_frame_a); ASSERT_TRUE(video_frame_b); - ASSERT_TRUE(video_frame_a); + ASSERT_TRUE(video_frame_c); EXPECT_FALSE(video_frame_a->IsEndOfStream()); EXPECT_FALSE(video_frame_b->IsEndOfStream()); @@ -354,6 +361,66 @@ TEST_F(FFmpegVideoDecoderTest, DecodeFrame_SmallerHeight) { DecodeIFrameThenTestFile("vp8-I-frame-320x120", 320, 120); } +TEST_F(FFmpegVideoDecoderTest, DecodeEncryptedFrame_Normal) { + Initialize(); + + // Simulate decoding a single encrypted frame. + encrypted_i_frame_buffer_->SetDecryptConfig( + scoped_ptr<DecryptConfig>(new DecryptConfig(kRawKey, kKeySize))); + scoped_refptr<VideoFrame> video_frame; + DecodeSingleFrame(encrypted_i_frame_buffer_, &video_frame); + + ASSERT_TRUE(video_frame); + EXPECT_FALSE(video_frame->IsEndOfStream()); +} + +// No key was provided with the encrypted frame. The decoder will mistakenly +// assume the frame is not encrypted. The behavior should be the same as +// decoding a corrupted frame. +TEST_F(FFmpegVideoDecoderTest, DecodeEncryptedFrame_NoKey) { + Initialize(); + + EXPECT_CALL(*demuxer_, Read(_)) + .WillRepeatedly(ReturnBuffer(encrypted_i_frame_buffer_)); + + // The error is only raised on the second decode attempt, so we expect at + // least one successful decode but we don't expect FrameReady() to be + // executed as an error is raised instead. + EXPECT_CALL(statistics_callback_, OnStatistics(_)); + EXPECT_CALL(host_, SetError(PIPELINE_ERROR_DECODE)); + + // Our read should still get satisfied with end of stream frame during an + // error. + scoped_refptr<VideoFrame> video_frame; + Read(&video_frame); + ASSERT_TRUE(video_frame); + EXPECT_TRUE(video_frame->IsEndOfStream()); + + message_loop_.RunAllPending(); +} + +// Test decrypting a encrypted frame with a wrong key. +TEST_F(FFmpegVideoDecoderTest, DecodeEncryptedFrame_WrongKey) { + Initialize(); + + encrypted_i_frame_buffer_->SetDecryptConfig( + scoped_ptr<DecryptConfig>(new DecryptConfig(kWrongKey, kKeySize))); + + EXPECT_CALL(*demuxer_, Read(_)) + .WillRepeatedly(ReturnBuffer(encrypted_i_frame_buffer_)); + + EXPECT_CALL(host_, SetError(PIPELINE_ERROR_DECODE)); + + // Our read should still get satisfied with end of stream frame during an + // error. + scoped_refptr<VideoFrame> video_frame; + Read(&video_frame); + ASSERT_TRUE(video_frame); + EXPECT_TRUE(video_frame->IsEndOfStream()); + + message_loop_.RunAllPending(); +} + // Test pausing when decoder has initialized but not decoded. TEST_F(FFmpegVideoDecoderTest, Pause_Initialized) { Initialize(); diff --git a/media/filters/pipeline_integration_test.cc b/media/filters/pipeline_integration_test.cc index bfcb4f3..d52009d 100644 --- a/media/filters/pipeline_integration_test.cc +++ b/media/filters/pipeline_integration_test.cc @@ -79,6 +79,16 @@ class PipelineIntegrationTest : public testing::Test, public PipelineIntegrationTestBase { public: + void StartPipelineWithMediaSource(MockMediaSource& source) { + pipeline_->Start( + CreateFilterCollection(&source), source.url(), + base::Bind(&PipelineIntegrationTest::OnEnded, base::Unretained(this)), + base::Bind(&PipelineIntegrationTest::OnError, base::Unretained(this)), + NetworkEventCB(), QuitOnStatusCB(PIPELINE_OK)); + + message_loop_.Run(); + } + // Verifies that seeking works properly for ChunkDemuxer when the // seek happens while there is a pending read on the ChunkDemuxer // and no data is available. @@ -89,15 +99,7 @@ class PipelineIntegrationTest int seek_file_position, int seek_append_size) { MockMediaSource source(filename, initial_append_size); - - pipeline_->Start(CreateFilterCollection(&source), source.url(), - base::Bind(&PipelineIntegrationTest::OnEnded, - base::Unretained(this)), - base::Bind(&PipelineIntegrationTest::OnError, - base::Unretained(this)), - NetworkEventCB(), - QuitOnStatusCB(PIPELINE_OK)); - message_loop_.Run(); + StartPipelineWithMediaSource(source); if (pipeline_status_ != PIPELINE_OK) return false; @@ -127,6 +129,20 @@ TEST_F(PipelineIntegrationTest, BasicPlayback) { ASSERT_TRUE(WaitUntilOnEnded()); } +TEST_F(PipelineIntegrationTest, EncryptedPlayback) { + MockMediaSource source("bear-320x240-encrypted.webm", 219726); + StartPipelineWithMediaSource(source); + + source.EndOfStream(); + ASSERT_EQ(PIPELINE_OK, pipeline_status_); + + Play(); + + ASSERT_TRUE(WaitUntilOnEnded()); + source.Abort(); + Stop(); +} + // TODO(acolwell): Fix flakiness http://crbug.com/109875 TEST_F(PipelineIntegrationTest, DISABLED_SeekWhilePaused) { ASSERT_TRUE(Start(GetTestDataURL("bear-320x240.webm"), PIPELINE_OK)); diff --git a/media/media.gyp b/media/media.gyp index d229ed3..30d85ed 100644 --- a/media/media.gyp +++ b/media/media.gyp @@ -17,6 +17,7 @@ '../base/base.gyp:base', '../base/third_party/dynamic_annotations/dynamic_annotations.gyp:dynamic_annotations', '../build/temp_gyp/googleurl.gyp:googleurl', + '../crypto/crypto.gyp:crypto', '../third_party/openmax/openmax.gyp:il', '../ui/ui.gyp:ui', ], @@ -115,6 +116,8 @@ 'base/data_buffer.h', 'base/data_source.cc', 'base/data_source.h', + 'base/decrypt_config.cc', + 'base/decrypt_config.h', 'base/demuxer.cc', 'base/demuxer.h', 'base/demuxer_factory.cc', @@ -160,6 +163,8 @@ 'base/video_frame.h', 'base/video_util.cc', 'base/video_util.h', + 'crypto/aes_decryptor.cc', + 'crypto/aes_decryptor.h', 'ffmpeg/ffmpeg_common.cc', 'ffmpeg/ffmpeg_common.h', 'ffmpeg/file_protocol.cc', @@ -601,6 +606,7 @@ 'base/video_frame_unittest.cc', 'base/video_util_unittest.cc', 'base/yuv_convert_unittest.cc', + 'crypto/aes_decryptor_unittest.cc', 'ffmpeg/ffmpeg_common_unittest.cc', 'filters/audio_renderer_algorithm_base_unittest.cc', 'filters/audio_renderer_base_unittest.cc', diff --git a/media/webm/webm_cluster_parser.cc b/media/webm/webm_cluster_parser.cc index 92f1dc7..c34cd8d 100644 --- a/media/webm/webm_cluster_parser.cc +++ b/media/webm/webm_cluster_parser.cc @@ -6,33 +6,33 @@ #include "base/logging.h" #include "media/base/data_buffer.h" -#include "media/ffmpeg/ffmpeg_common.h" +#include "media/base/decrypt_config.h" #include "media/webm/webm_constants.h" namespace media { -static Buffer* CreateBuffer(const uint8* data, size_t size) { - // Why FF_INPUT_BUFFER_PADDING_SIZE? FFmpeg assumes all input buffers are - // padded with this value. - scoped_array<uint8> buf(new uint8[size + FF_INPUT_BUFFER_PADDING_SIZE]); - memcpy(buf.get(), data, size); - memset(buf.get() + size, 0, FF_INPUT_BUFFER_PADDING_SIZE); - return new DataBuffer(buf.Pass(), size); -} - WebMClusterParser::WebMClusterParser(int64 timecode_scale, int audio_track_num, base::TimeDelta audio_default_duration, int video_track_num, - base::TimeDelta video_default_duration) + base::TimeDelta video_default_duration, + const uint8* video_encryption_key_id, + int video_encryption_key_id_size) : timecode_multiplier_(timecode_scale / 1000.0), audio_track_num_(audio_track_num), audio_default_duration_(audio_default_duration), video_track_num_(video_track_num), video_default_duration_(video_default_duration), + video_encryption_key_id_size_(video_encryption_key_id_size), parser_(kWebMIdCluster, this), last_block_timecode_(-1), cluster_timecode_(-1) { + CHECK_GE(video_encryption_key_id_size, 0); + if (video_encryption_key_id_size > 0) { + video_encryption_key_id_.reset(new uint8[video_encryption_key_id_size]); + memcpy(video_encryption_key_id_.get(), video_encryption_key_id, + video_encryption_key_id_size); + } } WebMClusterParser::~WebMClusterParser() {} @@ -115,7 +115,13 @@ bool WebMClusterParser::OnSimpleBlock(int track_num, int timecode, base::TimeDelta timestamp = base::TimeDelta::FromMicroseconds( (cluster_timecode_ + timecode) * timecode_multiplier_); - scoped_refptr<Buffer> buffer(CreateBuffer(data, size)); + scoped_refptr<DataBuffer> buffer = DataBuffer::CopyFrom(data, size); + + if (track_num == video_track_num_ && video_encryption_key_id_.get()) { + buffer->SetDecryptConfig(scoped_ptr<DecryptConfig>(new DecryptConfig( + video_encryption_key_id_.get(), video_encryption_key_id_size_))); + } + buffer->SetTimestamp(timestamp); BufferQueue* queue = NULL; diff --git a/media/webm/webm_cluster_parser.h b/media/webm/webm_cluster_parser.h index 2db3755..8646289 100644 --- a/media/webm/webm_cluster_parser.h +++ b/media/webm/webm_cluster_parser.h @@ -1,4 +1,4 @@ -// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -22,7 +22,9 @@ class WebMClusterParser : public WebMParserClient { int audio_track_num, base::TimeDelta audio_default_duration, int video_track_num, - base::TimeDelta video_default_duration); + base::TimeDelta video_default_duration, + const uint8* video_encryption_key_id, + int video_encryption_key_id_size); virtual ~WebMClusterParser(); // Resets the parser state so it can accept a new cluster. @@ -52,6 +54,8 @@ class WebMClusterParser : public WebMParserClient { base::TimeDelta audio_default_duration_; int video_track_num_; base::TimeDelta video_default_duration_; + scoped_array<uint8> video_encryption_key_id_; + int video_encryption_key_id_size_; WebMListParser parser_; diff --git a/media/webm/webm_stream_parser.cc b/media/webm/webm_stream_parser.cc index 036f7e7b0..d986deb 100644 --- a/media/webm/webm_stream_parser.cc +++ b/media/webm/webm_stream_parser.cc @@ -11,6 +11,7 @@ #include "media/filters/in_memory_url_protocol.h" #include "media/webm/webm_cluster_parser.h" #include "media/webm/webm_constants.h" +#include "media/webm/webm_content_encodings.h" #include "media/webm/webm_info_parser.h" #include "media/webm/webm_tracks_parser.h" @@ -33,6 +34,10 @@ class FFmpegConfigHelper { const VideoDecoderConfig& video_config() const; private: + static const uint8 kWebMHeader[]; + static const int kSegmentSizeOffset; + static const uint8 kEmptyCluster[]; + AVFormatContext* CreateFormatContext(const uint8* data, int size); bool SetupStreamConfigs(); @@ -50,10 +55,6 @@ class FFmpegConfigHelper { // DestroyAVFormatContext() in the destructor. AVFormatContext* format_context_; - static const uint8 kWebMHeader[]; - static const int kSegmentSizeOffset; - static const uint8 kEmptyCluster[]; - DISALLOW_COPY_AND_ASSIGN(FFmpegConfigHelper); }; @@ -305,7 +306,9 @@ int WebMStreamParser::ParseInfoAndTracks(const uint8* data, int size) { tracks_parser.audio_track_num(), tracks_parser.audio_default_duration(), tracks_parser.video_track_num(), - tracks_parser.video_default_duration())); + tracks_parser.video_default_duration(), + tracks_parser.video_encryption_key_id(), + tracks_parser.video_encryption_key_id_size())); ChangeState(PARSING_CLUSTERS); init_cb_.Run(true, duration); diff --git a/media/webm/webm_tracks_parser.cc b/media/webm/webm_tracks_parser.cc index 84550b7..7aa85d5 100644 --- a/media/webm/webm_tracks_parser.cc +++ b/media/webm/webm_tracks_parser.cc @@ -5,7 +5,9 @@ #include "media/webm/webm_tracks_parser.h" #include "base/logging.h" +#include "base/string_util.h" #include "media/webm/webm_constants.h" +#include "media/webm/webm_content_encodings.h" namespace media { @@ -24,6 +26,24 @@ WebMTracksParser::WebMTracksParser(int64 timecode_scale) WebMTracksParser::~WebMTracksParser() {} +const uint8* WebMTracksParser::video_encryption_key_id() const { + if (!video_content_encodings_client_.get()) + return NULL; + + DCHECK(!video_content_encodings_client_->content_encodings().empty()); + return video_content_encodings_client_->content_encodings()[0]-> + encryption_key_id(); +} + +int WebMTracksParser::video_encryption_key_id_size() const { + if (!video_content_encodings_client_.get()) + return 0; + + DCHECK(!video_content_encodings_client_->content_encodings().empty()); + return video_content_encodings_client_->content_encodings()[0]-> + encryption_key_id_size(); +} + int WebMTracksParser::Parse(const uint8* buf, int size) { track_type_ =-1; track_num_ = -1; diff --git a/media/webm/webm_tracks_parser.h b/media/webm/webm_tracks_parser.h index efe45d9..db2c928 100644 --- a/media/webm/webm_tracks_parser.h +++ b/media/webm/webm_tracks_parser.h @@ -36,6 +36,9 @@ class WebMTracksParser : public WebMParserClient { return video_default_duration_; } + const uint8* video_encryption_key_id() const; + int video_encryption_key_id_size() const; + private: // WebMParserClient methods virtual WebMParserClient* OnListStart(int id) OVERRIDE; |