Added Widevine and fixed key decryption.

Change-Id: Ia29ce678c0e64e886cdd92bbfda727768356fd21
This commit is contained in:
Thomas Inskip 2014-08-25 15:51:19 -07:00
parent 65e558b19c
commit 7b1640a8f7
25 changed files with 271 additions and 164 deletions

View File

@ -11,13 +11,22 @@
DEFINE_bool(enable_fixed_key_encryption, DEFINE_bool(enable_fixed_key_encryption,
false, false,
"Enable encryption with fixed key."); "Enable encryption with fixed key.");
DEFINE_bool(enable_fixed_key_decryption,
false,
"Enable decryption with fixed key.");
DEFINE_string(key_id, "", "Key id in hex string format."); DEFINE_string(key_id, "", "Key id in hex string format.");
DEFINE_string(key, "", "Key in hex string format."); DEFINE_string(key, "", "Key in hex string format.");
DEFINE_string(pssh, "", "PSSH in hex string format."); DEFINE_string(pssh, "", "PSSH in hex string format.");
static bool IsNotEmptyWithFixedKeyEncryption(const char* flag_name, static bool IsNotEmptyWithFixedKeyEncryption(const char* flag_name,
const std::string& flag_value) { const std::string& flag_value) {
return FLAGS_enable_fixed_key_encryption ? !flag_value.empty() : true; if (FLAGS_enable_fixed_key_encryption && flag_value.empty())
return false;
std::string flag_name_str(flag_name);
if (FLAGS_enable_fixed_key_decryption && (flag_name_str != "pssh") &&
flag_value.empty())
return false;
return true;
} }
static bool dummy_key_id_validator = static bool dummy_key_id_validator =

View File

@ -12,6 +12,7 @@
#include <gflags/gflags.h> #include <gflags/gflags.h>
DECLARE_bool(enable_fixed_key_encryption); DECLARE_bool(enable_fixed_key_encryption);
DECLARE_bool(enable_fixed_key_decryption);
DECLARE_string(key_id); DECLARE_string(key_id);
DECLARE_string(key); DECLARE_string(key);
DECLARE_string(pssh); DECLARE_string(pssh);

View File

@ -123,19 +123,20 @@ bool CreateRemuxJobs(const StreamDescriptorList& stream_descriptors,
if (stream_iter->input != previous_input) { if (stream_iter->input != previous_input) {
// New remux job needed. Create demux and job thread. // New remux job needed. Create demux and job thread.
scoped_ptr<Demuxer> demux(new Demuxer(stream_iter->input, NULL)); scoped_ptr<Demuxer> demuxer(new Demuxer(stream_iter->input));
Status status = demux->Initialize(); demuxer->SetKeySource(CreateDecryptionKeySource());
Status status = demuxer->Initialize();
if (!status.ok()) { if (!status.ok()) {
LOG(ERROR) << "Demuxer failed to initialize: " << status.ToString(); LOG(ERROR) << "Demuxer failed to initialize: " << status.ToString();
return false; return false;
} }
if (FLAGS_dump_stream_info) { if (FLAGS_dump_stream_info) {
printf("\nFile \"%s\":\n", stream_iter->input.c_str()); printf("\nFile \"%s\":\n", stream_iter->input.c_str());
DumpStreamInfo(demux->streams()); DumpStreamInfo(demuxer->streams());
if (stream_iter->output.empty()) if (stream_iter->output.empty())
continue; // just need stream info. continue; // just need stream info.
} }
remux_jobs->push_back(new RemuxJob(demux.Pass())); remux_jobs->push_back(new RemuxJob(demuxer.Pass()));
previous_input = stream_iter->input; previous_input = stream_iter->input;
} }
DCHECK(!remux_jobs->empty()); DCHECK(!remux_jobs->empty());

View File

@ -100,6 +100,9 @@ scoped_ptr<KeySource> CreateDecryptionKeySource() {
scoped_ptr<RequestSigner> signer(CreateSigner()); scoped_ptr<RequestSigner> signer(CreateSigner());
decryption_key_source.reset(new WidevineKeySource(FLAGS_key_server_url, decryption_key_source.reset(new WidevineKeySource(FLAGS_key_server_url,
signer.Pass())); signer.Pass()));
} else if (FLAGS_enable_fixed_key_decryption) {
decryption_key_source = KeySource::CreateFromHexStrings(
FLAGS_key_id, FLAGS_key, "", "");
} }
return decryption_key_source.Pass(); return decryption_key_source.Pass();
} }

View File

@ -55,6 +55,9 @@ static bool VerifyEncryptionAndDecryptionParams(const char* flag_name,
const std::string& flag_value) { const std::string& flag_value) {
DCHECK(flag_name); DCHECK(flag_name);
const std::string flag_name_str = flag_name;
bool is_common_param = (flag_name_str == "key_server_url") ||
(flag_name_str == "signer");
if (FLAGS_enable_widevine_encryption) { if (FLAGS_enable_widevine_encryption) {
if (flag_value.empty()) { if (flag_value.empty()) {
fprintf(stderr, fprintf(stderr,
@ -63,12 +66,12 @@ static bool VerifyEncryptionAndDecryptionParams(const char* flag_name,
return false; return false;
} }
} else if (FLAGS_enable_widevine_decryption) { } else if (FLAGS_enable_widevine_decryption) {
const std::string flag_name_str = flag_name; if (is_common_param) {
if (flag_name_str == "key_server_url") {
if (flag_value.empty()) { if (flag_value.empty()) {
fprintf(stderr, fprintf(stderr,
"ERROR: %s required if --enable_widevine_decryption is true\n", "ERROR: %s required if --enable_widevine_encryption or "
flag_name); "--enable_widevine_decryption is true\n",
flag_name);
return false; return false;
} }
} else { } else {
@ -80,9 +83,8 @@ static bool VerifyEncryptionAndDecryptionParams(const char* flag_name,
} }
} else { } else {
if (!flag_value.empty()) { if (!flag_value.empty()) {
const std::string flag_name_str = flag_name;
fprintf(stderr, "ERROR: %s should only be specified if %s" fprintf(stderr, "ERROR: %s should only be specified if %s"
" is true\n", flag_name, flag_name_str == "key_server_url" ? " is true\n", flag_name, is_common_param ?
"--enable_widevine_encryption or --enable_widevine_decryption" : "--enable_widevine_encryption or --enable_widevine_decryption" :
"--enable_widevine_encryption"); "--enable_widevine_encryption");
return false; return false;

View File

@ -8,8 +8,8 @@
namespace media { namespace media {
DecryptConfig::DecryptConfig(const std::string& key_id, DecryptConfig::DecryptConfig(const std::vector<uint8>& key_id,
const std::string& iv, const std::vector<uint8>& iv,
const int data_offset, const int data_offset,
const std::vector<SubsampleEntry>& subsamples) const std::vector<SubsampleEntry>& subsamples)
: key_id_(key_id), : key_id_(key_id),

View File

@ -19,12 +19,12 @@ namespace media {
/// encrypted bytes in a sample should be considered a single logical stream, /// encrypted bytes in a sample should be considered a single logical stream,
/// regardless of how they are divided into subsamples, and the clear bytes /// regardless of how they are divided into subsamples, and the clear bytes
/// should not be considered as part of decryption. This is logically equivalent /// should not be considered as part of decryption. This is logically equivalent
/// to concatenating all @a cypher_bytes portions of subsamples, decrypting that /// to concatenating all @a cipher_bytes portions of subsamples, decrypting that
/// result, and then copying each byte from the decrypted block over the /// result, and then copying each byte from the decrypted block over the
/// corresponding encrypted byte. /// corresponding encrypted byte.
struct SubsampleEntry { struct SubsampleEntry {
uint16 clear_bytes; uint16 clear_bytes;
uint32 cypher_bytes; uint32 cipher_bytes;
}; };
/// Contains all the information that a decryptor needs to decrypt a media /// Contains all the information that a decryptor needs to decrypt a media
@ -43,22 +43,22 @@ class DecryptConfig {
/// @param subsamples defines the clear and encrypted portions of the sample /// @param subsamples defines the clear and encrypted portions of the sample
/// as described in SubsampleEntry. A decrypted buffer will be equal /// as described in SubsampleEntry. A decrypted buffer will be equal
/// in size to the sum of the subsample sizes. /// in size to the sum of the subsample sizes.
DecryptConfig(const std::string& key_id, DecryptConfig(const std::vector<uint8>& key_id,
const std::string& iv, const std::vector<uint8>& iv,
const int data_offset, const int data_offset,
const std::vector<SubsampleEntry>& subsamples); const std::vector<SubsampleEntry>& subsamples);
~DecryptConfig(); ~DecryptConfig();
const std::string& key_id() const { return key_id_; } const std::vector<uint8>& key_id() const { return key_id_; }
const std::string& iv() const { return iv_; } const std::vector<uint8>& iv() const { return iv_; }
int data_offset() const { return data_offset_; } int data_offset() const { return data_offset_; }
const std::vector<SubsampleEntry>& subsamples() const { return subsamples_; } const std::vector<SubsampleEntry>& subsamples() const { return subsamples_; }
private: private:
const std::string key_id_; const std::vector<uint8> key_id_;
// Initialization vector. // Initialization vector.
const std::string iv_; const std::vector<uint8> iv_;
// Amount of data to be discarded before applying subsample information. // Amount of data to be discarded before applying subsample information.
const int data_offset_; const int data_offset_;

View File

@ -11,6 +11,7 @@
#include "base/stl_util.h" #include "base/stl_util.h"
#include "media/base/container_names.h" #include "media/base/container_names.h"
#include "media/base/decryptor_source.h" #include "media/base/decryptor_source.h"
#include "media/base/key_source.h"
#include "media/base/media_sample.h" #include "media/base/media_sample.h"
#include "media/base/media_stream.h" #include "media/base/media_stream.h"
#include "media/base/stream_info.h" #include "media/base/stream_info.h"
@ -24,10 +25,8 @@ const size_t kBufSize = 0x40000; // 256KB.
namespace media { namespace media {
Demuxer::Demuxer(const std::string& file_name, Demuxer::Demuxer(const std::string& file_name)
DecryptorSource* decryptor_source) : file_name_(file_name),
: decryptor_source_(decryptor_source),
file_name_(file_name),
media_file_(NULL), media_file_(NULL),
init_event_received_(false), init_event_received_(false),
buffer_(new uint8[kBufSize]) {} buffer_(new uint8[kBufSize]) {}
@ -38,6 +37,10 @@ Demuxer::~Demuxer() {
STLDeleteElements(&streams_); STLDeleteElements(&streams_);
} }
void Demuxer::SetKeySource(scoped_ptr<KeySource> key_source) {
key_source_ = key_source.Pass();
}
Status Demuxer::Initialize() { Status Demuxer::Initialize() {
DCHECK(!media_file_); DCHECK(!media_file_);
DCHECK(!init_event_received_); DCHECK(!init_event_received_);
@ -69,7 +72,7 @@ Status Demuxer::Initialize() {
parser_->Init(base::Bind(&Demuxer::ParserInitEvent, base::Unretained(this)), parser_->Init(base::Bind(&Demuxer::ParserInitEvent, base::Unretained(this)),
base::Bind(&Demuxer::NewSampleEvent, base::Unretained(this)), base::Bind(&Demuxer::NewSampleEvent, base::Unretained(this)),
base::Bind(&Demuxer::KeyNeededEvent, base::Unretained(this))); key_source_.get());
if (!parser_->Parse(buffer_.get(), bytes_read)) if (!parser_->Parse(buffer_.get(), bytes_read))
return Status(error::PARSER_FAILURE, return Status(error::PARSER_FAILURE,
@ -104,12 +107,6 @@ bool Demuxer::NewSampleEvent(uint32 track_id,
return false; return false;
} }
void Demuxer::KeyNeededEvent(MediaContainerName container,
scoped_ptr<uint8[]> init_data,
int init_data_size) {
NOTIMPLEMENTED();
}
Status Demuxer::Run() { Status Demuxer::Run() {
Status status; Status status;

View File

@ -17,8 +17,8 @@
namespace media { namespace media {
class Decryptor; class Decryptor;
class DecryptorSource;
class File; class File;
class KeySource;
class MediaParser; class MediaParser;
class MediaSample; class MediaSample;
class MediaStream; class MediaStream;
@ -31,11 +31,15 @@ class Demuxer {
/// @param file_name specifies the input source. It uses prefix matching to /// @param file_name specifies the input source. It uses prefix matching to
/// create a proper File object. The user can extend File to support /// create a proper File object. The user can extend File to support
/// a custom File object with its own prefix. /// a custom File object with its own prefix.
/// @param decryptor_source generates decryptor(s) from decryption explicit Demuxer(const std::string& file_name);
/// initialization data. It can be NULL if the media is not encrypted.
Demuxer(const std::string& file_name, DecryptorSource* decryptor_source);
~Demuxer(); ~Demuxer();
/// Set the KeySource for media decryption.
/// @param key_source points to the source of decryption keys. The key
/// source must support fetching of keys for the type of media being
/// demuxed.
void SetKeySource(scoped_ptr<KeySource> key_source);
/// Initialize the Demuxer. Calling other public methods of this class /// Initialize the Demuxer. Calling other public methods of this class
/// without this method returning OK, results in an undefined behavior. /// without this method returning OK, results in an undefined behavior.
/// This method primes the demuxer by parsing portions of the media file to /// This method primes the demuxer by parsing portions of the media file to
@ -61,17 +65,14 @@ class Demuxer {
void ParserInitEvent(const std::vector<scoped_refptr<StreamInfo> >& streams); void ParserInitEvent(const std::vector<scoped_refptr<StreamInfo> >& streams);
bool NewSampleEvent(uint32 track_id, bool NewSampleEvent(uint32 track_id,
const scoped_refptr<MediaSample>& sample); const scoped_refptr<MediaSample>& sample);
void KeyNeededEvent(MediaContainerName container,
scoped_ptr<uint8[]> init_data,
int init_data_size);
DecryptorSource* decryptor_source_;
std::string file_name_; std::string file_name_;
File* media_file_; File* media_file_;
bool init_event_received_; bool init_event_received_;
scoped_ptr<MediaParser> parser_; scoped_ptr<MediaParser> parser_;
std::vector<MediaStream*> streams_; std::vector<MediaStream*> streams_;
scoped_ptr<uint8[]> buffer_; scoped_ptr<uint8[]> buffer_;
scoped_ptr<KeySource> key_source_;
DISALLOW_COPY_AND_ASSIGN(Demuxer); DISALLOW_COPY_AND_ASSIGN(Demuxer);
}; };

View File

@ -25,12 +25,12 @@ KeySource::~KeySource() {}
Status KeySource::FetchKeys(const std::vector<uint8>& content_id, Status KeySource::FetchKeys(const std::vector<uint8>& content_id,
const std::string& policy) { const std::string& policy) {
NOTREACHED(); // Do nothing for fixed key decryption.
return Status::OK; return Status::OK;
} }
Status KeySource::FetchKeys(const std::vector<uint8>& pssh_data) { Status KeySource::FetchKeys(const std::vector<uint8>& pssh_data) {
NOTREACHED(); // Do nothing for fixed key decryption.
return Status::OK; return Status::OK;
} }
@ -45,6 +45,11 @@ Status KeySource::GetKey(const std::vector<uint8>& key_id,
EncryptionKey* key) { EncryptionKey* key) {
DCHECK(key); DCHECK(key);
DCHECK(encryption_key_); DCHECK(encryption_key_);
if (key_id != encryption_key_->key_id) {
return Status(error::NOT_FOUND, std::string("Key for key ID ") +
base::HexEncode(&key_id[0], key_id.size()) +
" was not found.");
}
*key = *encryption_key_; *key = *encryption_key_;
return Status::OK; return Status::OK;
} }

View File

@ -17,6 +17,7 @@
namespace media { namespace media {
class KeySource;
class MediaSample; class MediaSample;
class StreamInfo; class StreamInfo;
@ -40,20 +41,16 @@ class MediaParser {
bool(uint32 track_id, const scoped_refptr<MediaSample>& media_sample)> bool(uint32 track_id, const scoped_refptr<MediaSample>& media_sample)>
NewSampleCB; NewSampleCB;
/// Called when a new potentially encrypted stream has been parsed.
/// @param init_data is the initialization data associated with the stream.
/// @param init_data_size is the number of bytes of the initialization data.
typedef base::Callback<void(MediaContainerName container_name,
scoped_ptr<uint8[]> init_data,
int init_data_size)> NeedKeyCB;
/// Initialize the parser with necessary callbacks. Must be called before any /// Initialize the parser with necessary callbacks. Must be called before any
/// data is passed to Parse(). /// data is passed to Parse().
/// @param init_cb will be called once enough data has been parsed to /// @param init_cb will be called once enough data has been parsed to
/// determine the initial stream configurations. /// determine the initial stream configurations.
/// @param new_sample_cb will be called each time a new media sample is
/// available from the parser. May be NULL, and caller retains
/// ownership.
virtual void Init(const InitCB& init_cb, virtual void Init(const InitCB& init_cb,
const NewSampleCB& new_sample_cb, const NewSampleCB& new_sample_cb,
const NeedKeyCB& need_key_cb) = 0; KeySource* decryption_key_source) = 0;
/// Flush data currently in the parser and put the parser in a state where it /// Flush data currently in the parser and put the parser in a state where it
/// can receive data for a new seek point. /// can receive data for a new seek point.

View File

@ -10,7 +10,6 @@
#include "base/logging.h" #include "base/logging.h"
#include "base/strings/stringprintf.h" #include "base/strings/stringprintf.h"
#include "media/base/decrypt_config.h"
namespace media { namespace media {
@ -65,15 +64,13 @@ std::string MediaSample::ToString() const {
return "End of stream sample\n"; return "End of stream sample\n";
return base::StringPrintf( return base::StringPrintf(
"dts: %" PRId64 "\n pts: %" PRId64 "\n duration: %" PRId64 "\n " "dts: %" PRId64 "\n pts: %" PRId64 "\n duration: %" PRId64 "\n "
"is_key_frame: %s\n size: %zu\n side_data_size: %zu\n " "is_key_frame: %s\n size: %zu\n side_data_size: %zu\n",
"is_encrypted: %s\n",
dts_, dts_,
pts_, pts_,
duration_, duration_,
is_key_frame_ ? "true" : "false", is_key_frame_ ? "true" : "false",
data_.size(), data_.size(),
side_data_.size(), side_data_.size());
decrypt_config_ ? "true" : "false");
} }
} // namespace media } // namespace media

View File

@ -14,12 +14,9 @@
#include "base/logging.h" #include "base/logging.h"
#include "base/memory/ref_counted.h" #include "base/memory/ref_counted.h"
#include "base/memory/scoped_ptr.h" #include "base/memory/scoped_ptr.h"
#include "media/base/decrypt_config.h"
namespace media { namespace media {
class DecryptConfig;
/// Class to hold a media sample. /// Class to hold a media sample.
class MediaSample : public base::RefCountedThreadSafe<MediaSample> { class MediaSample : public base::RefCountedThreadSafe<MediaSample> {
public: public:
@ -113,16 +110,6 @@ class MediaSample : public base::RefCountedThreadSafe<MediaSample> {
return side_data_.size(); return side_data_.size();
} }
const DecryptConfig* decrypt_config() const {
DCHECK(!end_of_stream());
return decrypt_config_.get();
}
void set_decrypt_config(scoped_ptr<DecryptConfig> decrypt_config) {
DCHECK(!end_of_stream());
decrypt_config_ = decrypt_config.Pass();
}
// If there's no data in this buffer, it represents end of stream. // If there's no data in this buffer, it represents end of stream.
bool end_of_stream() const { return data_.size() == 0; } bool end_of_stream() const { return data_.size() == 0; }
@ -155,7 +142,6 @@ class MediaSample : public base::RefCountedThreadSafe<MediaSample> {
// http://www.matroska.org/technical/specs/index.html BlockAdditional[A5]. // http://www.matroska.org/technical/specs/index.html BlockAdditional[A5].
// Not used by mp4 and other containers. // Not used by mp4 and other containers.
std::vector<uint8> side_data_; std::vector<uint8> side_data_;
scoped_ptr<DecryptConfig> decrypt_config_;
DISALLOW_COPY_AND_ASSIGN(MediaSample); DISALLOW_COPY_AND_ASSIGN(MediaSample);
}; };

View File

@ -65,6 +65,9 @@ enum Code {
// The operation timed out. // The operation timed out.
TIME_OUT, TIME_OUT,
// Value was not found.
NOT_FOUND,
}; };
} // namespace error } // namespace error

View File

@ -155,16 +155,14 @@ Mp2tMediaParser::~Mp2tMediaParser() {
void Mp2tMediaParser::Init( void Mp2tMediaParser::Init(
const InitCB& init_cb, const InitCB& init_cb,
const NewSampleCB& new_sample_cb, const NewSampleCB& new_sample_cb,
const NeedKeyCB& need_key_cb) { KeySource* decryption_key_source) {
DCHECK(!is_initialized_); DCHECK(!is_initialized_);
DCHECK(init_cb_.is_null()); DCHECK(init_cb_.is_null());
DCHECK(!init_cb.is_null()); DCHECK(!init_cb.is_null());
DCHECK(!new_sample_cb.is_null()); DCHECK(!new_sample_cb.is_null());
DCHECK(!need_key_cb.is_null());
init_cb_ = init_cb; init_cb_ = init_cb;
new_sample_cb_ = new_sample_cb; new_sample_cb_ = new_sample_cb;
need_key_cb_ = need_key_cb;
} }
void Mp2tMediaParser::Flush() { void Mp2tMediaParser::Flush() {

View File

@ -34,7 +34,7 @@ class Mp2tMediaParser : public MediaParser {
// MediaParser implementation overrides. // MediaParser implementation overrides.
virtual void Init(const InitCB& init_cb, virtual void Init(const InitCB& init_cb,
const NewSampleCB& new_sample_cb, const NewSampleCB& new_sample_cb,
const NeedKeyCB& need_key_cb) OVERRIDE; KeySource* decryption_key_source) OVERRIDE;
virtual void Flush() OVERRIDE; virtual void Flush() OVERRIDE;
@ -74,7 +74,6 @@ class Mp2tMediaParser : public MediaParser {
// List of callbacks. // List of callbacks.
InitCB init_cb_; InitCB init_cb_;
NewSampleCB new_sample_cb_; NewSampleCB new_sample_cb_;
NeedKeyCB need_key_cb_;
bool sbr_in_mimetype_; bool sbr_in_mimetype_;

View File

@ -97,20 +97,13 @@ class Mp2tMediaParserTest : public testing::Test {
return true; return true;
} }
void OnKeyNeeded(MediaContainerName container_name,
scoped_ptr<uint8[]> init_data,
int init_data_size) {
DVLOG(1) << "OnKeyNeeded: " << init_data_size;
}
void InitializeParser() { void InitializeParser() {
parser_->Init( parser_->Init(
base::Bind(&Mp2tMediaParserTest::OnInit, base::Bind(&Mp2tMediaParserTest::OnInit,
base::Unretained(this)), base::Unretained(this)),
base::Bind(&Mp2tMediaParserTest::OnNewSample, base::Bind(&Mp2tMediaParserTest::OnNewSample,
base::Unretained(this)), base::Unretained(this)),
base::Bind(&Mp2tMediaParserTest::OnKeyNeeded, NULL);
base::Unretained(this)));
} }
bool ParseMpeg2TsFile(const std::string& filename, int append_bytes) { bool ParseMpeg2TsFile(const std::string& filename, int append_bytes) {

View File

@ -15,7 +15,7 @@ namespace {
// 64-bit (8-byte) or 128-bit (16-byte). // 64-bit (8-byte) or 128-bit (16-byte).
bool IsIvSizeValid(size_t iv_size) { return iv_size == 8 || iv_size == 16; } bool IsIvSizeValid(size_t iv_size) { return iv_size == 8 || iv_size == 16; }
// 16-bit |clear_bytes| and 32-bit |cypher_bytes|. // 16-bit |clear_bytes| and 32-bit |cipher_bytes|.
const size_t kSubsampleEntrySize = sizeof(uint16) + sizeof(uint32); const size_t kSubsampleEntrySize = sizeof(uint16) + sizeof(uint32);
} // namespace } // namespace
@ -44,11 +44,11 @@ bool FrameCENCInfo::Parse(uint8 iv_size, BufferReader* reader) {
subsamples_.resize(subsample_count); subsamples_.resize(subsample_count);
for (uint16 i = 0; i < subsample_count; ++i) { for (uint16 i = 0; i < subsample_count; ++i) {
uint16 clear_bytes; uint16 clear_bytes;
uint32 cypher_bytes; uint32 cipher_bytes;
RCHECK(reader->Read2(&clear_bytes) && RCHECK(reader->Read2(&clear_bytes) &&
reader->Read4(&cypher_bytes)); reader->Read4(&cipher_bytes));
subsamples_[i].clear_bytes = clear_bytes; subsamples_[i].clear_bytes = clear_bytes;
subsamples_[i].cypher_bytes = cypher_bytes; subsamples_[i].cipher_bytes = cipher_bytes;
} }
return true; return true;
} }
@ -65,7 +65,7 @@ void FrameCENCInfo::Write(BufferWriter* writer) const {
for (uint16 i = 0; i < subsample_count; ++i) { for (uint16 i = 0; i < subsample_count; ++i) {
writer->AppendInt(subsamples_[i].clear_bytes); writer->AppendInt(subsamples_[i].clear_bytes);
writer->AppendInt(subsamples_[i].cypher_bytes); writer->AppendInt(subsamples_[i].cipher_bytes);
} }
} }
@ -81,7 +81,7 @@ size_t FrameCENCInfo::ComputeSize() const {
size_t FrameCENCInfo::GetTotalSizeOfSubsamples() const { size_t FrameCENCInfo::GetTotalSizeOfSubsamples() const {
size_t size = 0; size_t size = 0;
for (size_t i = 0; i < subsamples_.size(); ++i) { for (size_t i = 0; i < subsamples_.size(); ++i) {
size += subsamples_[i].clear_bytes + subsamples_[i].cypher_bytes; size += subsamples_[i].clear_bytes + subsamples_[i].cipher_bytes;
} }
return size; return size;
} }

View File

@ -144,13 +144,13 @@ Status EncryptingFragmenter::EncryptSample(scoped_refptr<MediaSample> sample) {
SubsampleEntry subsample; SubsampleEntry subsample;
subsample.clear_bytes = nalu_length_size_ + 1; subsample.clear_bytes = nalu_length_size_ + 1;
subsample.cypher_bytes = nalu_length - 1; subsample.cipher_bytes = nalu_length - 1;
if (!reader.SkipBytes(nalu_length)) { if (!reader.SkipBytes(nalu_length)) {
return Status(error::MUXER_FAILURE, return Status(error::MUXER_FAILURE,
"Sample size does not match nalu_length."); "Sample size does not match nalu_length.");
} }
EncryptBytes(data + subsample.clear_bytes, subsample.cypher_bytes); EncryptBytes(data + subsample.clear_bytes, subsample.cipher_bytes);
cenc_info.AddSubsample(subsample); cenc_info.AddSubsample(subsample);
data += nalu_length_size_ + nalu_length; data += nalu_length_size_ + nalu_length;
} }

View File

@ -8,7 +8,11 @@
#include "base/callback_helpers.h" #include "base/callback_helpers.h"
#include "base/logging.h" #include "base/logging.h"
#include "base/memory/ref_counted.h" #include "base/memory/ref_counted.h"
#include "base/strings/string_number_conversions.h"
#include "media/base/aes_encryptor.h"
#include "media/base/audio_stream_info.h" #include "media/base/audio_stream_info.h"
#include "media/base/decrypt_config.h"
#include "media/base/key_source.h"
#include "media/base/media_sample.h" #include "media/base/media_sample.h"
#include "media/base/video_stream_info.h" #include "media/base/video_stream_info.h"
#include "media/formats/mp4/box_definitions.h" #include "media/formats/mp4/box_definitions.h"
@ -23,6 +27,9 @@ uint64 Rescale(uint64 time_in_old_scale, uint32 old_scale, uint32 new_scale) {
return (static_cast<double>(time_in_old_scale) / old_scale) * new_scale; return (static_cast<double>(time_in_old_scale) / old_scale) * new_scale;
} }
const char kWidevineKeySystemId[] = "edef8ba979d64acea3c827dcd51d21ed";
} // namespace } // namespace
namespace media { namespace media {
@ -31,21 +38,22 @@ namespace mp4 {
MP4MediaParser::MP4MediaParser() MP4MediaParser::MP4MediaParser()
: state_(kWaitingForInit), moof_head_(0), mdat_tail_(0) {} : state_(kWaitingForInit), moof_head_(0), mdat_tail_(0) {}
MP4MediaParser::~MP4MediaParser() {} MP4MediaParser::~MP4MediaParser() {
STLDeleteValues(&decryptor_map_);
}
void MP4MediaParser::Init(const InitCB& init_cb, void MP4MediaParser::Init(const InitCB& init_cb,
const NewSampleCB& new_sample_cb, const NewSampleCB& new_sample_cb,
const NeedKeyCB& need_key_cb) { KeySource* decryption_key_source) {
DCHECK_EQ(state_, kWaitingForInit); DCHECK_EQ(state_, kWaitingForInit);
DCHECK(init_cb_.is_null()); DCHECK(init_cb_.is_null());
DCHECK(!init_cb.is_null()); DCHECK(!init_cb.is_null());
DCHECK(!new_sample_cb.is_null()); DCHECK(!new_sample_cb.is_null());
DCHECK(!need_key_cb.is_null());
ChangeState(kParsingBoxes); ChangeState(kParsingBoxes);
init_cb_ = init_cb; init_cb_ = init_cb;
new_sample_cb_ = new_sample_cb; new_sample_cb_ = new_sample_cb;
need_key_cb_ = need_key_cb; decryption_key_source_ = decryption_key_source;
} }
void MP4MediaParser::Reset() { void MP4MediaParser::Reset() {
@ -292,7 +300,8 @@ bool MP4MediaParser::ParseMoov(BoxReader* reader) {
} }
init_cb_.Run(streams); init_cb_.Run(streams);
EmitNeedKeyIfNecessary(moov_->pssh); if (!FetchKeysIfNecessary(moov_->pssh))
return false;
runs_.reset(new TrackRunIterator(moov_.get())); runs_.reset(new TrackRunIterator(moov_.get()));
RCHECK(runs_->Init()); RCHECK(runs_->Init());
ChangeState(kEmittingSamples); ChangeState(kEmittingSamples);
@ -307,29 +316,40 @@ bool MP4MediaParser::ParseMoof(BoxReader* reader) {
if (!runs_) if (!runs_)
runs_.reset(new TrackRunIterator(moov_.get())); runs_.reset(new TrackRunIterator(moov_.get()));
RCHECK(runs_->Init(moof)); RCHECK(runs_->Init(moof));
EmitNeedKeyIfNecessary(moof.pssh); if (!FetchKeysIfNecessary(moof.pssh))
return false;
ChangeState(kEmittingSamples); ChangeState(kEmittingSamples);
return true; return true;
} }
void MP4MediaParser::EmitNeedKeyIfNecessary( bool MP4MediaParser::FetchKeysIfNecessary(
const std::vector<ProtectionSystemSpecificHeader>& headers) { const std::vector<ProtectionSystemSpecificHeader>& headers) {
if (headers.empty()) if (headers.empty())
return; return true;
size_t total_size = 0; if (!decryption_key_source_) {
for (size_t i = 0; i < headers.size(); i++) LOG(ERROR) << "Content is encrypted, but content decryption not enabled.";
total_size += headers[i].raw_box.size(); return false;
scoped_ptr<uint8[]> init_data(new uint8[total_size]);
size_t pos = 0;
for (size_t i = 0; i < headers.size(); i++) {
memcpy(&init_data.get()[pos],
&headers[i].raw_box[0],
headers[i].raw_box.size());
pos += headers[i].raw_box.size();
} }
need_key_cb_.Run(CONTAINER_MOV, init_data.Pass(), total_size);
// TODO(tinskip): Pass in raw 'pssh' boxes to FetchKeys. This will allow
// supporting multiple keysystems. Move this to KeySource.
std::vector<uint8> widevine_system_id;
base::HexStringToBytes(kWidevineKeySystemId, &widevine_system_id);
for (std::vector<ProtectionSystemSpecificHeader>::const_iterator iter =
headers.begin(); iter != headers.end(); ++iter) {
if (iter->system_id == widevine_system_id) {
Status status = decryption_key_source_->FetchKeys(iter->data);
if (!status.ok()) {
LOG(ERROR) << "Error fetching decryption keys: " << status;
return false;
}
return true;
}
}
LOG(ERROR) << "No viable 'pssh' box found for content decryption.";
return false;
} }
bool MP4MediaParser::EnqueueSample(bool* err) { bool MP4MediaParser::EnqueueSample(bool* err) {
@ -379,35 +399,20 @@ bool MP4MediaParser::EnqueueSample(bool* err) {
if (buf_size < runs_->sample_size()) if (buf_size < runs_->sample_size())
return false; return false;
scoped_ptr<DecryptConfig> decrypt_config; scoped_refptr<MediaSample> stream_sample(MediaSample::CopyFrom(
std::vector<SubsampleEntry> subsamples; buf, runs_->sample_size(), runs_->is_keyframe()));
if (runs_->is_encrypted()) { if (runs_->is_encrypted()) {
decrypt_config = runs_->GetDecryptConfig(); scoped_ptr<DecryptConfig> decrypt_config = runs_->GetDecryptConfig();
if (!decrypt_config) { if (!decrypt_config) {
*err = true; *err = true;
return false; return false;
} }
subsamples = decrypt_config->subsamples(); if (!DecryptSampleBuffer(decrypt_config.get(),
stream_sample->writable_data(),
stream_sample->data_size()))
return false;
} }
if (decrypt_config) {
if (!subsamples.empty()) {
// Create a new config with the updated subsamples.
decrypt_config.reset(new DecryptConfig(decrypt_config->key_id(),
decrypt_config->iv(),
decrypt_config->data_offset(),
subsamples));
}
// else, use the existing config.
}
std::vector<uint8> frame_buf(buf, buf + runs_->sample_size());
scoped_refptr<MediaSample> stream_sample = MediaSample::CopyFrom(
&frame_buf[0], frame_buf.size(), runs_->is_keyframe());
if (decrypt_config)
stream_sample->set_decrypt_config(decrypt_config.Pass());
stream_sample->set_dts(runs_->dts()); stream_sample->set_dts(runs_->dts());
stream_sample->set_pts(runs_->cts()); stream_sample->set_pts(runs_->cts());
stream_sample->set_duration(runs_->duration()); stream_sample->set_duration(runs_->duration());
@ -425,6 +430,80 @@ bool MP4MediaParser::EnqueueSample(bool* err) {
return true; return true;
} }
bool MP4MediaParser::DecryptSampleBuffer(const DecryptConfig* decrypt_config,
uint8* 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<AesCtrEncryptor> 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<SubsampleEntry>& subsamples = decrypt_config->subsamples();
uint8* current_ptr = buffer;
const uint8* 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<SubsampleEntry>::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 offset) { bool MP4MediaParser::ReadAndDiscardMDATsUntil(const int64 offset) {
bool err = false; bool err = false;
while (mdat_tail_ < offset) { while (mdat_tail_ < offset) {

View File

@ -7,17 +7,23 @@
#ifndef MEDIA_FORMATS_MP4_MP4_MEDIA_PARSER_H_ #ifndef MEDIA_FORMATS_MP4_MP4_MEDIA_PARSER_H_
#define MEDIA_FORMATS_MP4_MP4_MEDIA_PARSER_H_ #define MEDIA_FORMATS_MP4_MP4_MEDIA_PARSER_H_
#include <map>
#include <vector> #include <vector>
#include "base/basictypes.h" #include "base/basictypes.h"
#include "base/callback_forward.h" #include "base/callback_forward.h"
#include "base/compiler_specific.h" #include "base/compiler_specific.h"
#include "base/memory/scoped_ptr.h" #include "base/memory/scoped_ptr.h"
#include "base/memory/ref_counted.h"
#include "base/memory/scoped_ptr.h"
#include "media/base/media_parser.h" #include "media/base/media_parser.h"
#include "media/base/offset_byte_queue.h" #include "media/base/offset_byte_queue.h"
namespace media { namespace media {
class AesCtrEncryptor;
class DecryptConfig;
namespace mp4 { namespace mp4 {
class BoxReader; class BoxReader;
@ -34,7 +40,7 @@ class MP4MediaParser : public MediaParser {
/// @{ /// @{
virtual void Init(const InitCB& init_cb, virtual void Init(const InitCB& init_cb,
const NewSampleCB& new_sample_cb, const NewSampleCB& new_sample_cb,
const NeedKeyCB& need_key_cb) OVERRIDE; KeySource* decryption_key_source) OVERRIDE;
virtual void Flush() OVERRIDE; virtual void Flush() OVERRIDE;
virtual bool Parse(const uint8* buf, int size) OVERRIDE; virtual bool Parse(const uint8* buf, int size) OVERRIDE;
/// @} /// @}
@ -51,9 +57,13 @@ class MP4MediaParser : public MediaParser {
bool ParseMoov(mp4::BoxReader* reader); bool ParseMoov(mp4::BoxReader* reader);
bool ParseMoof(mp4::BoxReader* reader); bool ParseMoof(mp4::BoxReader* reader);
void EmitNeedKeyIfNecessary( bool FetchKeysIfNecessary(
const std::vector<ProtectionSystemSpecificHeader>& headers); const std::vector<ProtectionSystemSpecificHeader>& headers);
bool DecryptSampleBuffer(const DecryptConfig* decrypt_config,
uint8* buffer,
size_t buffer_size);
// To retain proper framing, each 'mdat' atom must be read; to limit memory // To retain proper framing, each 'mdat' atom must be read; to limit memory
// usage, the atom's data needs to be discarded incrementally as frames are // usage, the atom's data needs to be discarded incrementally as frames are
// extracted from the stream. This function discards data from the stream up // extracted from the stream. This function discards data from the stream up
@ -73,7 +83,7 @@ class MP4MediaParser : public MediaParser {
State state_; State state_;
InitCB init_cb_; InitCB init_cb_;
NewSampleCB new_sample_cb_; NewSampleCB new_sample_cb_;
NeedKeyCB need_key_cb_; KeySource* decryption_key_source_;
OffsetByteQueue queue_; OffsetByteQueue queue_;
@ -90,6 +100,9 @@ class MP4MediaParser : public MediaParser {
scoped_ptr<Movie> moov_; scoped_ptr<Movie> moov_;
scoped_ptr<TrackRunIterator> runs_; scoped_ptr<TrackRunIterator> runs_;
typedef std::map<std::vector<uint8>, AesCtrEncryptor*> DecryptorMap;
DecryptorMap decryptor_map_;
DISALLOW_COPY_AND_ASSIGN(MP4MediaParser); DISALLOW_COPY_AND_ASSIGN(MP4MediaParser);
}; };

View File

@ -53,19 +53,11 @@ class MP4MediaParserTest : public testing::Test {
return true; return true;
} }
void KeyNeededF(MediaContainerName container,
scoped_ptr<uint8[]> init_data,
int init_data_size) {
DVLOG(1) << "KeyNeededF: " << init_data_size;
EXPECT_TRUE(init_data.get());
EXPECT_GT(init_data_size, 0);
}
void InitializeParser() { void InitializeParser() {
parser_->Init( parser_->Init(
base::Bind(&MP4MediaParserTest::InitF, base::Unretained(this)), base::Bind(&MP4MediaParserTest::InitF, base::Unretained(this)),
base::Bind(&MP4MediaParserTest::NewSampleF, base::Unretained(this)), base::Bind(&MP4MediaParserTest::NewSampleF, base::Unretained(this)),
base::Bind(&MP4MediaParserTest::KeyNeededF, base::Unretained(this))); NULL);
} }
bool ParseMP4File(const std::string& filename, int append_bytes) { bool ParseMP4File(const std::string& filename, int append_bytes) {

View File

@ -541,10 +541,9 @@ scoped_ptr<DecryptConfig> TrackRunIterator::GetDecryptConfig() {
return scoped_ptr<DecryptConfig>(); return scoped_ptr<DecryptConfig>();
} }
const std::vector<uint8>& kid = track_encryption().default_kid;
return scoped_ptr<DecryptConfig>(new DecryptConfig( return scoped_ptr<DecryptConfig>(new DecryptConfig(
std::string(reinterpret_cast<const char*>(&kid[0]), kid.size()), track_encryption().default_kid,
std::string(cenc_info.iv().begin(), cenc_info.iv().end()), cenc_info.iv(),
0, // No offset to start of media data in MP4 using CENC. 0, // No offset to start of media data in MP4 using CENC.
cenc_info.subsamples())); cenc_info.subsamples()));
} }

View File

@ -336,7 +336,7 @@ TEST_F(TrackRunIteratorTest, DecryptConfigTest) {
config = iter_->GetDecryptConfig(); config = iter_->GetDecryptConfig();
EXPECT_EQ(config->subsamples().size(), 2u); EXPECT_EQ(config->subsamples().size(), 2u);
EXPECT_EQ(config->subsamples()[0].clear_bytes, 1u); EXPECT_EQ(config->subsamples()[0].clear_bytes, 1u);
EXPECT_EQ(config->subsamples()[1].cypher_bytes, 4u); EXPECT_EQ(config->subsamples()[1].cipher_bytes, 4u);
} }
// It is legal for aux info blocks to be shared among multiple formats. // It is legal for aux info blocks to be shared among multiple formats.

View File

@ -84,7 +84,7 @@ class FakeClock : public base::Clock {
class PackagerTestBasic : public ::testing::TestWithParam<const char*> { class PackagerTestBasic : public ::testing::TestWithParam<const char*> {
public: public:
PackagerTestBasic() : decryptor_source_(NULL) {} PackagerTestBasic() {}
virtual void SetUp() OVERRIDE { virtual void SetUp() OVERRIDE {
// Create a test directory for testing, will be deleted after test. // Create a test directory for testing, will be deleted after test.
@ -108,9 +108,12 @@ class PackagerTestBasic : public ::testing::TestWithParam<const char*> {
bool single_segment, bool single_segment,
bool enable_encryption); bool enable_encryption);
void Decrypt(const std::string& input,
const std::string& video_output,
const std::string& audio_output);
protected: protected:
base::FilePath test_directory_; base::FilePath test_directory_;
DecryptorSource* decryptor_source_;
FakeClock fake_clock_; FakeClock fake_clock_;
}; };
@ -148,7 +151,7 @@ void PackagerTestBasic::Remux(const std::string& input,
bool enable_encryption) { bool enable_encryption) {
CHECK(!video_output.empty() || !audio_output.empty()); CHECK(!video_output.empty() || !audio_output.empty());
Demuxer demuxer(GetFullPath(input), decryptor_source_); Demuxer demuxer(GetFullPath(input));
ASSERT_OK(demuxer.Initialize()); ASSERT_OK(demuxer.Initialize());
scoped_ptr<KeySource> encryption_key_source( scoped_ptr<KeySource> encryption_key_source(
@ -182,9 +185,9 @@ void PackagerTestBasic::Remux(const std::string& input,
if (enable_encryption) { if (enable_encryption) {
muxer_audio->SetKeySource(encryption_key_source.get(), muxer_audio->SetKeySource(encryption_key_source.get(),
KeySource::TRACK_TYPE_SD, KeySource::TRACK_TYPE_SD,
kClearLeadInSeconds, kClearLeadInSeconds,
kCryptoDurationInSeconds); kCryptoDurationInSeconds);
} }
} }
@ -192,6 +195,39 @@ void PackagerTestBasic::Remux(const std::string& input,
ASSERT_OK(demuxer.Run()); ASSERT_OK(demuxer.Run());
} }
void PackagerTestBasic::Decrypt(const std::string& input,
const std::string& video_output,
const std::string& audio_output) {
CHECK(!video_output.empty() || !audio_output.empty());
Demuxer demuxer(GetFullPath(input));
scoped_ptr<KeySource> decryption_key_source(
KeySource::CreateFromHexStrings(kKeyIdHex, kKeyHex, "", ""));
DCHECK(decryption_key_source);
demuxer.SetKeySource(decryption_key_source.Pass());
ASSERT_OK(demuxer.Initialize());
scoped_ptr<Muxer> muxer;
MediaStream* stream(NULL);
if (!video_output.empty()) {
muxer.reset(
new mp4::MP4Muxer(SetupOptions(video_output, true)));
stream = FindFirstVideoStream(demuxer.streams());
}
if (!audio_output.empty()) {
muxer.reset(
new mp4::MP4Muxer(SetupOptions(audio_output, true)));
stream = FindFirstAudioStream(demuxer.streams());
}
ASSERT_TRUE(muxer);
ASSERT_TRUE(stream != NULL);
ASSERT_TRUE(stream->info()->is_encrypted());
muxer->set_clock(&fake_clock_);
muxer->AddStream(stream);
ASSERT_OK(demuxer.Run());
}
TEST_P(PackagerTestBasic, MP4MuxerSingleSegmentUnencryptedVideo) { TEST_P(PackagerTestBasic, MP4MuxerSingleSegmentUnencryptedVideo) {
ASSERT_NO_FATAL_FAILURE(Remux(GetParam(), ASSERT_NO_FATAL_FAILURE(Remux(GetParam(),
kOutputVideo, kOutputVideo,
@ -215,11 +251,9 @@ TEST_P(PackagerTestBasic, MP4MuxerSingleSegmentEncryptedVideo) {
kSingleSegment, kSingleSegment,
kEnableEncryption)); kEnableEncryption));
// Expect the output to be encrypted. ASSERT_NO_FATAL_FAILURE(Decrypt(kOutputVideo,
Demuxer demuxer(GetFullPath(kOutputVideo), decryptor_source_); kOutputVideo2,
ASSERT_OK(demuxer.Initialize()); kOutputNone));
ASSERT_EQ(1u, demuxer.streams().size());
EXPECT_TRUE(demuxer.streams()[0]->info()->is_encrypted());
} }
TEST_P(PackagerTestBasic, MP4MuxerSingleSegmentEncryptedAudio) { TEST_P(PackagerTestBasic, MP4MuxerSingleSegmentEncryptedAudio) {
@ -229,11 +263,9 @@ TEST_P(PackagerTestBasic, MP4MuxerSingleSegmentEncryptedAudio) {
kSingleSegment, kSingleSegment,
kEnableEncryption)); kEnableEncryption));
// Expect the output to be encrypted. ASSERT_NO_FATAL_FAILURE(Decrypt(kOutputAudio,
Demuxer demuxer(GetFullPath(kOutputAudio), decryptor_source_); kOutputNone,
ASSERT_OK(demuxer.Initialize()); kOutputAudio2));
ASSERT_EQ(1u, demuxer.streams().size());
EXPECT_TRUE(demuxer.streams()[0]->info()->is_encrypted());
} }
class PackagerTest : public PackagerTestBasic { class PackagerTest : public PackagerTestBasic {