Fix SAMPLE-AES EC3 encryption

The IV was incorrectly updated across samples.

Created a new cryptor SampleAesEc3Cryptor specially for SAMPLE-AES
EC3 encryption / decryption. The new cryptor uses constant-iv, and
makes sure the IV is reset to the initial value at the beginning
of each audio frame and chained across syncframes within the the
audio frame.

Also added E-AC3 end to end test.

Fixes #279

Change-Id: I0aa60c17836daeef5ba433a05e5ff0906191d9ac
This commit is contained in:
KongQun Yang 2018-01-21 12:21:01 -08:00
parent 1455f43c02
commit 9ecee46658
14 changed files with 356 additions and 94 deletions

View File

@ -1051,6 +1051,22 @@ class PackagerFunctionalTest(PackagerAppTest):
os.path.join(self.tmp_dir, 'video.m3u8'),
'bear-640x360-v-mp4-cenc-golden.m3u8')
def testPackageWithEc3AndHlsSingleSegmentMp4Encrypted(self):
self.assertPackageSuccess(
self._GetStreams(
['audio', 'video'], hls=True, test_files=['bear-640x360-ec3.mp4']),
self._GetFlags(encryption=True, output_hls=True))
self._DiffGold(self.output[0], 'bear-640x360-ec3-a-cenc-golden.mp4')
self._DiffGold(self.output[1], 'bear-640x360-ec3-v-cenc-golden.mp4')
self._DiffGold(self.hls_master_playlist_output,
'bear-640x360-ec3-av-mp4-master-cenc-golden.m3u8')
self._DiffGold(
os.path.join(self.tmp_dir, 'audio.m3u8'),
'bear-640x360-ec3-a-mp4-cenc-golden.m3u8')
self._DiffGold(
os.path.join(self.tmp_dir, 'video.m3u8'),
'bear-640x360-ec3-v-mp4-cenc-golden.m3u8')
def testPackageWithHlsSingleSegmentMp4EncryptedAndAdCues(self):
self.assertPackageSuccess(
self._GetStreams(['audio', 'video'], hls=True),

Binary file not shown.

View File

@ -0,0 +1,17 @@
#EXTM3U
#EXT-X-VERSION:6
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
#EXT-X-TARGETDURATION:2
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MAP:URI="output_audio.mp4",BYTERANGE="909@0"
#EXT-X-KEY:METHOD=SAMPLE-AES-CENC,URI="data:text/plain;base64,MTIzNDU2Nzg5MDEyMzQ1Ng==",KEYFORMAT="identity"
#EXTINF:1.010,
#EXT-X-BYTERANGE:24460@977
output_audio.mp4
#EXTINF:0.975,
#EXT-X-BYTERANGE:23899
output_audio.mp4
#EXTINF:0.766,
#EXT-X-BYTERANGE:18811
output_audio.mp4
#EXT-X-ENDLIST

View File

@ -0,0 +1,5 @@
#EXTM3U
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
#EXT-X-MEDIA:TYPE=AUDIO,URI="audio.m3u8",GROUP-ID="audio",NAME="stream_0",AUTOSELECT=YES,CHANNELS="2"
#EXT-X-STREAM-INF:BANDWIDTH=1174135,CODECS="avc1.64001e,ec-3",RESOLUTION=640x360,AUDIO="audio"
video.m3u8

Binary file not shown.

View File

@ -0,0 +1,17 @@
#EXTM3U
#EXT-X-VERSION:6
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
#EXT-X-TARGETDURATION:2
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MAP:URI="output_video.mp4",BYTERANGE="1091@0"
#EXT-X-KEY:METHOD=SAMPLE-AES-CENC,URI="data:text/plain;base64,MTIzNDU2Nzg5MDEyMzQ1Ng==",KEYFORMAT="identity"
#EXTINF:1.001,
#EXT-X-BYTERANGE:93805@1159
output_video.mp4
#EXTINF:1.001,
#EXT-X-BYTERANGE:122340
output_video.mp4
#EXTINF:0.734,
#EXT-X-BYTERANGE:80067
output_video.mp4
#EXT-X-ENDLIST

View File

@ -68,7 +68,8 @@ class AesCryptor {
}
/// @}
/// Set IV.
/// Set IV. SetIv() implementation guarantees that the iv passed to SetIv()
/// is set to iv() and then calls SetIvInternal().
/// @return true if successful, false if the input is invalid.
bool SetIv(const std::vector<uint8_t>& iv);
@ -138,5 +139,3 @@ class AesCryptor {
} // namespace shaka
#endif // PACKAGER_MEDIA_BASE_AES_CRYPTOR_H_

View File

@ -15,6 +15,8 @@
'sources': [
'encryption_handler.cc',
'encryption_handler.h',
'sample_aes_ec3_cryptor.cc',
'sample_aes_ec3_cryptor.h',
],
'dependencies': [
'../base/media_base.gyp:media_base',
@ -26,6 +28,7 @@
'type': '<(gtest_target_type)',
'sources': [
'encryption_handler_unittest.cc',
'sample_aes_ec3_cryptor_unittest.cc',
],
'dependencies': [
'../../testing/gtest.gyp:gtest',

View File

@ -14,13 +14,14 @@
#include "packager/media/base/aes_encryptor.h"
#include "packager/media/base/aes_pattern_cryptor.h"
#include "packager/media/base/audio_stream_info.h"
#include "packager/media/base/key_source.h"
#include "packager/media/base/media_sample.h"
#include "packager/media/base/audio_stream_info.h"
#include "packager/media/base/video_stream_info.h"
#include "packager/media/codecs/video_slice_header_parser.h"
#include "packager/media/codecs/vp8_parser.h"
#include "packager/media/codecs/vp9_parser.h"
#include "packager/media/crypto/sample_aes_ec3_cryptor.h"
namespace shaka {
namespace media {
@ -280,14 +281,6 @@ Status EncryptionHandler::ProcessMediaSample(
}
DCHECK_EQ(decrypt_config->GetTotalSizeOfSubsamples(),
clear_sample->data_size());
} else if (codec_ == kCodecEAC3 &&
protection_scheme_ == kAppleSampleAesProtectionScheme) {
if (!SampleAesEncryptEac3Frame(clear_sample->data(),
clear_sample->data_size(),
&cipher_sample_data.get()[0])) {
return Status(error::ENCRYPTION_FAILURE,
"Failed to encrypt E-AC3 frame.");
}
} else {
memcpy(&cipher_sample_data.get()[0], clear_sample->data(),
std::min(clear_sample->data_size(), leading_clear_bytes_size_));
@ -330,6 +323,7 @@ Status EncryptionHandler::SetupProtectionPattern(StreamType stream_type) {
case kCodecAAC:
FALLTHROUGH_INTENDED;
case kCodecAC3:
FALLTHROUGH_INTENDED;
case kCodecEAC3:
// Audio is whole sample encrypted. We could not use a
// crypto_byte_block_ of 1 here as if there is one crypto block
@ -337,7 +331,10 @@ Status EncryptionHandler::SetupProtectionPattern(StreamType stream_type) {
// encrypted for audio.
crypt_byte_block_ = 0u;
skip_byte_block_ = 0u;
leading_clear_bytes_size_ = kAudioLeadingClearBytesSize;
// E-AC3 encryption is handled by SampleAesEc3Cryptor, which also
// manages leading clear bytes.
leading_clear_bytes_size_ =
codec_ == kCodecEAC3 ? 0 : kAudioLeadingClearBytesSize;
min_protected_data_size_ = leading_clear_bytes_size_ + 15u;
break;
default:
@ -371,6 +368,7 @@ Status EncryptionHandler::SetupProtectionPattern(StreamType stream_type) {
// Not using pattern encryption.
crypt_byte_block_ = 0u;
skip_byte_block_ = 0u;
break;
}
return Status::OK;
}
@ -400,17 +398,13 @@ bool EncryptionHandler::CreateEncryptor(const EncryptionKey& encryption_key) {
break;
case kAppleSampleAesProtectionScheme:
if (crypt_byte_block_ == 0 && skip_byte_block_ == 0) {
auto constant_iv_flag = AesCryptor::kUseConstantIv;
if (codec_ == kCodecEAC3) {
// MPEG-2 Stream Encryption Format for HTTP Live Streaming 2.3.1.3
// Enhanced AC-3: Within an Enhanced AC-3 audio frame, the AES-128
// cipher block chaining (CBC) initialization vector (IV) is not reset
// at syncframe boundaries. The IV is reset at the beginning of each
// audio frame.
// We'll manage the reset of IV outside of AesCryptor.
constant_iv_flag = AesCryptor::kDontUseConstantIv;
encryptor.reset(new SampleAesEc3Cryptor(
std::unique_ptr<AesCryptor>(new AesCbcEncryptor(kNoPadding))));
} else {
encryptor.reset(
new AesCbcEncryptor(kNoPadding, AesCryptor::kUseConstantIv));
}
encryptor.reset(new AesCbcEncryptor(kNoPadding, constant_iv_flag));
} else {
encryptor.reset(new AesPatternCryptor(
crypt_byte_block_, skip_byte_block_,
@ -568,35 +562,6 @@ bool EncryptionHandler::EncryptNalFrame(const uint8_t* source,
return true;
}
bool EncryptionHandler::SampleAesEncryptEac3Frame(const uint8_t* source,
size_t source_size,
uint8_t* dest) {
DCHECK(source);
DCHECK(dest);
std::vector<size_t> syncframe_sizes;
if (!ExtractEac3SyncframeSizes(source, source_size, &syncframe_sizes))
return false;
// MPEG-2 Stream Encryption Format for HTTP Live Streaming 2.3.1.3 Enhanced
// AC-3: The IV is reset at the beginning of each audio frame.
encryptor_->SetIv(encryptor_->iv());
for (size_t syncframe_size : syncframe_sizes) {
memcpy(dest, source, std::min(syncframe_size, leading_clear_bytes_size_));
if (syncframe_size > leading_clear_bytes_size_) {
// The residual block is left unecrypted (copied without encryption). No
// need to do special handling here.
EncryptBytes(source + leading_clear_bytes_size_,
syncframe_size - leading_clear_bytes_size_,
dest + leading_clear_bytes_size_);
}
source += syncframe_size;
dest += syncframe_size;
}
return true;
}
void EncryptionHandler::EncryptBytes(const uint8_t* source,
size_t source_size,
uint8_t* dest) {
@ -606,48 +571,6 @@ void EncryptionHandler::EncryptBytes(const uint8_t* source,
CHECK(encryptor_->Crypt(source, source_size, dest));
}
bool EncryptionHandler::ExtractEac3SyncframeSizes(
const uint8_t* source,
size_t source_size,
std::vector<size_t>* syncframe_sizes) {
DCHECK(source);
DCHECK(syncframe_sizes);
syncframe_sizes->clear();
BufferReader frame(source, source_size);
// ASTC Standard A/52:2012 Annex E: Enhanced AC-3.
while (frame.HasBytes(1)) {
uint16_t syncword;
if (!frame.Read2(&syncword)) {
LOG(ERROR) << "Not enough bytes for syncword.";
return false;
}
if (syncword != 0x0B77) {
LOG(ERROR) << "Invalid E-AC3 frame. Seeing 0x" << std::hex << syncword
<< std::dec
<< ". The sync frame does not start with "
"the valid syncword 0x0B77.";
return false;
}
uint16_t stream_type_and_syncframe_size;
if (!frame.Read2(&stream_type_and_syncframe_size)) {
LOG(ERROR) << "Not enough bytes for syncframe size.";
return false;
}
// frmsiz = least significant 11 bits. syncframe_size is (frmsiz + 1) * 2.
const size_t syncframe_size =
((stream_type_and_syncframe_size & 0x7FF) + 1) * 2;
if (!frame.SkipBytes(syncframe_size - sizeof(syncword) -
sizeof(stream_type_and_syncframe_size))) {
LOG(ERROR) << "Not enough bytes for syncframe. Expecting "
<< syncframe_size << " bytes.";
return false;
}
syncframe_sizes->push_back(syncframe_size);
}
return true;
}
void EncryptionHandler::InjectVpxParserForTesting(
std::unique_ptr<VPxParser> vpx_parser) {
vpx_parser_ = std::move(vpx_parser);

View File

@ -0,0 +1,115 @@
// Copyright 2018 Google Inc. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
#include "packager/media/crypto/sample_aes_ec3_cryptor.h"
#include <algorithm>
#include "packager/base/logging.h"
#include "packager/media/base/buffer_reader.h"
namespace shaka {
namespace media {
namespace {
bool ExtractEac3SyncframeSizes(const uint8_t* source,
size_t source_size,
std::vector<size_t>* syncframe_sizes) {
DCHECK(source);
DCHECK(syncframe_sizes);
syncframe_sizes->clear();
BufferReader frame(source, source_size);
// ASTC Standard A/52:2012 Annex E: Enhanced AC-3.
while (frame.HasBytes(1)) {
uint16_t syncword;
if (!frame.Read2(&syncword)) {
LOG(ERROR) << "Not enough bytes for syncword.";
return false;
}
if (syncword != 0x0B77) {
LOG(ERROR) << "Invalid E-AC3 frame. Seeing 0x" << std::hex << syncword
<< std::dec
<< ". The sync frame does not start with "
"the valid syncword 0x0B77.";
return false;
}
uint16_t stream_type_and_syncframe_size;
if (!frame.Read2(&stream_type_and_syncframe_size)) {
LOG(ERROR) << "Not enough bytes for syncframe size.";
return false;
}
// frmsiz = least significant 11 bits. syncframe_size is (frmsiz + 1) * 2.
const size_t syncframe_size =
((stream_type_and_syncframe_size & 0x7FF) + 1) * 2;
if (!frame.SkipBytes(syncframe_size - sizeof(syncword) -
sizeof(stream_type_and_syncframe_size))) {
LOG(ERROR) << "Not enough bytes for syncframe. Expecting "
<< syncframe_size << " bytes.";
return false;
}
syncframe_sizes->push_back(syncframe_size);
}
return true;
}
} // namespace
SampleAesEc3Cryptor::SampleAesEc3Cryptor(std::unique_ptr<AesCryptor> cryptor)
: AesCryptor(AesCryptor::kUseConstantIv), cryptor_(std::move(cryptor)) {
DCHECK(cryptor_);
DCHECK(!cryptor_->use_constant_iv());
}
bool SampleAesEc3Cryptor::InitializeWithIv(const std::vector<uint8_t>& key,
const std::vector<uint8_t>& iv) {
return SetIv(iv) && cryptor_->InitializeWithIv(key, iv);
}
bool SampleAesEc3Cryptor::CryptInternal(const uint8_t* text,
size_t text_size,
uint8_t* crypt_text,
size_t* crypt_text_size) {
// |crypt_text_size| is the same as |text_size|.
if (*crypt_text_size < text_size) {
LOG(ERROR) << "Expecting output size of at least " << text_size
<< " bytes.";
return false;
}
*crypt_text_size = text_size;
std::vector<size_t> syncframe_sizes;
if (!ExtractEac3SyncframeSizes(text, text_size, &syncframe_sizes))
return false;
// MPEG-2 Stream Encryption Format for HTTP Live Streaming 2.3.1.3 Enhanced
// AC-3: The first 16 bytes, starting with the syncframe() header, are not
// encrypted.
const size_t kLeadingClearBytesSize = 16u;
for (size_t syncframe_size : syncframe_sizes) {
memcpy(crypt_text, text, std::min(syncframe_size, kLeadingClearBytesSize));
if (syncframe_size > kLeadingClearBytesSize) {
// The residual block is left untouched (copied without
// encryption/decryption). No need to do special handling here.
if (!cryptor_->Crypt(text + kLeadingClearBytesSize,
syncframe_size - kLeadingClearBytesSize,
crypt_text + kLeadingClearBytesSize)) {
return false;
}
}
text += syncframe_size;
crypt_text += syncframe_size;
}
return true;
}
void SampleAesEc3Cryptor::SetIvInternal() {
CHECK(cryptor_->SetIv(iv()));
}
} // namespace media
} // namespace shaka

View File

@ -0,0 +1,47 @@
// Copyright 2018 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/aes_cryptor.h"
#ifndef PACKAGER_MEDIA_CRYPTO_SAMPLE_AES_EC3_CRYPTOR_H_
#define PACKAGER_MEDIA_CRYPTO_SAMPLE_AES_EC3_CRYPTOR_H_
namespace shaka {
namespace media {
/// Implements SAMPLE-AES E-AC3 encryption / decryption per specification at:
/// https://goo.gl/1sgcwY.
class SampleAesEc3Cryptor : public AesCryptor {
public:
/// @param cryptor points to an AesCryptor instance which performs the actual
/// encryption/decryption. Note that @a cryptor shall not use constant
/// iv.
explicit SampleAesEc3Cryptor(std::unique_ptr<AesCryptor> cryptor);
/// @name AesCryptor implementation overrides.
/// @{
bool InitializeWithIv(const std::vector<uint8_t>& key,
const std::vector<uint8_t>& iv) override;
/// @}
private:
SampleAesEc3Cryptor(const SampleAesEc3Cryptor&) = delete;
SampleAesEc3Cryptor& operator=(const SampleAesEc3Cryptor&) = delete;
// AesCryptor implementation overrides.
bool CryptInternal(const uint8_t* text,
size_t text_size,
uint8_t* crypt_text,
size_t* crypt_text_size) override;
void SetIvInternal() override;
std::unique_ptr<AesCryptor> cryptor_;
};
} // namespace media
} // namespace shaka
#endif // PACKAGER_MEDIA_CRYPTO_SAMPLE_AES_EC3_CRYPTOR_H_

View File

@ -0,0 +1,119 @@
// Copyright 2018 Google Inc. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
#include "packager/media/crypto/sample_aes_ec3_cryptor.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
using ::testing::_;
using ::testing::Invoke;
using ::testing::Return;
namespace shaka {
namespace media {
class MockAesCryptor : public AesCryptor {
public:
MockAesCryptor() : AesCryptor(kDontUseConstantIv) {}
MOCK_METHOD2(InitializeWithIv,
bool(const std::vector<uint8_t>& key,
const std::vector<uint8_t>& iv));
MOCK_METHOD4(CryptInternal,
bool(const uint8_t* text,
size_t text_size,
uint8_t* crypt_text,
size_t* crypt_text_size));
MOCK_METHOD0(SetIvInternal, void());
};
class SampleAesEc3CryptorTest : public ::testing::Test {
public:
SampleAesEc3CryptorTest()
: mock_cryptor_(new MockAesCryptor),
ec3_cryptor_(std::unique_ptr<MockAesCryptor>(mock_cryptor_)) {}
void SetUp() {
std::vector<uint8_t> key(16, 'k');
std::vector<uint8_t> iv(8, 'i');
EXPECT_CALL(*mock_cryptor_, InitializeWithIv(key, iv))
.WillOnce(Return(true));
EXPECT_TRUE(ec3_cryptor_.InitializeWithIv(key, iv));
EXPECT_EQ(iv, ec3_cryptor_.iv());
}
protected:
MockAesCryptor* mock_cryptor_; // Owned by |ec3_cryptor_|.
SampleAesEc3Cryptor ec3_cryptor_;
};
TEST_F(SampleAesEc3CryptorTest, Crypt) {
const std::vector<uint8_t> text = {
// First syncframe with 20 bytes.
0x0B, 0x77, 0x00, 0x09, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, 0x11, 0x12,
0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x20,
// Second syncframe with 26 bytes.
0x0B, 0x77, 0x00, 0x0C, 0x15, 0x16, 0x17, 0x18, 0x19, 0x20, 0x21, 0x22,
0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x30, 0x31, 0x32, 0x33, 0x34,
0x35, 0x36,
// Third syncframe with 16 bytes.
0x0B, 0x77, 0x00, 0x07, 0x15, 0x26, 0x27, 0x28, 0x29, 0x30, 0x31, 0x32,
0x33, 0x34, 0x35, 0x36};
EXPECT_CALL(*mock_cryptor_, CryptInternal(_, _, _, _))
.WillRepeatedly(Invoke([](const uint8_t* text, size_t text_size,
uint8_t* crypt_text, size_t* crypt_text_size) {
*crypt_text_size = text_size;
for (size_t i = 0; i < text_size; ++i) {
*crypt_text++ = *text++ + 0x40;
}
return true;
}));
const std::vector<uint8_t> expected_crypt_text = {
// First syncframe with 20 bytes.
0x0B, 0x77, 0x00, 0x09, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, 0x11, 0x12,
0x13, 0x14, 0x15, 0x16, 0x57, 0x58, 0x59, 0x60,
// Second syncframe with 26 bytes.
0x0B, 0x77, 0x00, 0x0C, 0x15, 0x16, 0x17, 0x18, 0x19, 0x20, 0x21, 0x22,
0x23, 0x24, 0x25, 0x26, 0x67, 0x68, 0x69, 0x70, 0x71, 0x72, 0x73, 0x74,
0x75, 0x76,
// Third syncframe with 16 bytes.
0x0B, 0x77, 0x00, 0x07, 0x15, 0x26, 0x27, 0x28, 0x29, 0x30, 0x31, 0x32,
0x33, 0x34, 0x35, 0x36};
std::vector<uint8_t> crypt_text;
ASSERT_TRUE(ec3_cryptor_.Crypt(text, &crypt_text));
EXPECT_EQ(expected_crypt_text, crypt_text);
}
TEST_F(SampleAesEc3CryptorTest, InvalidEc3Syncword) {
const std::vector<uint8_t> text = {0x0C, 0x77, 0x00, 0x09, 0x05, 0x06, 0x07,
0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14,
0x15, 0x16, 0x17, 0x18, 0x19, 0x20};
EXPECT_CALL(*mock_cryptor_, CryptInternal(_, _, _, _))
.WillRepeatedly(Return(true));
std::vector<uint8_t> crypt_text;
ASSERT_FALSE(ec3_cryptor_.Crypt(text, &crypt_text));
}
TEST_F(SampleAesEc3CryptorTest, InvalidEc3SyncframeSize) {
const std::vector<uint8_t> text = {0x0B, 0x77, 0x00, 0x0A, 0x05, 0x06, 0x07,
0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14,
0x15, 0x16, 0x17, 0x18, 0x19, 0x20};
EXPECT_CALL(*mock_cryptor_, CryptInternal(_, _, _, _))
.WillRepeatedly(Return(true));
std::vector<uint8_t> crypt_text;
ASSERT_FALSE(ec3_cryptor_.Crypt(text, &crypt_text));
}
} // namespace media
} // namespace shaka

View File

@ -33,7 +33,8 @@ bear-640x360-hevc.ts - HEVC + AAC encode, multiplexed into an MPEG2-TS container
// ISO-BMFF streams.
bear-1280x720.mp4 - AVC + AAC encode, mulitplexed into an ISOBMFF container.
bear-640x360.mp4 - Same as above, but in a different resolution.
bear-640x360-hevc.mp4 - Same content, but encoded with HEVC.
bear-640x360-ec3.mp4 - Same content, but audio encoded with E-AC3.
bear-640x360-hevc.mp4 - Same content, but video encoded with HEVC.
bear-320x180.mp4 - Same as above, but in a different resolution.
bear-640x360-trailing-moov.mp4 - Same content, but with moov box in the end.
bear-640x360-trailing-moov-additional-mdat.mp4 - Same content, but with moov box in the end and an additional unused mdat, which should be ignored.

Binary file not shown.