diff --git a/packager/app/test/packager_test.py b/packager/app/test/packager_test.py index 08e4754c90..1f1cae2546 100755 --- a/packager/app/test/packager_test.py +++ b/packager/app/test/packager_test.py @@ -171,7 +171,7 @@ class PackagerAppTest(unittest.TestCase): self._GetStreams(['audio', 'video']), self._GetFlags(encryption=True, protection_scheme='cens')) - self._DiffGold(self.output[0], 'bear-640x360-a-cenc-golden.mp4') + self._DiffGold(self.output[0], 'bear-640x360-a-cens-golden.mp4') self._DiffGold(self.output[1], 'bear-640x360-v-cens-golden.mp4') self._DiffGold(self.mpd_output, 'bear-640x360-av-cens-golden.mpd') self._VerifyDecryption(self.output[0], 'bear-640x360-a-golden.mp4') @@ -182,7 +182,7 @@ class PackagerAppTest(unittest.TestCase): self._GetStreams(['audio', 'video']), self._GetFlags(encryption=True, protection_scheme='cbcs')) - self._DiffGold(self.output[0], 'bear-640x360-a-cbc1-golden.mp4') + self._DiffGold(self.output[0], 'bear-640x360-a-cbcs-golden.mp4') self._DiffGold(self.output[1], 'bear-640x360-v-cbcs-golden.mp4') self._DiffGold(self.mpd_output, 'bear-640x360-av-cbcs-golden.mpd') self._VerifyDecryption(self.output[0], 'bear-640x360-a-golden.mp4') diff --git a/packager/app/test/testdata/bear-640x360-a-cbcs-golden.mp4 b/packager/app/test/testdata/bear-640x360-a-cbcs-golden.mp4 new file mode 100644 index 0000000000..313271ea58 Binary files /dev/null and b/packager/app/test/testdata/bear-640x360-a-cbcs-golden.mp4 differ diff --git a/packager/app/test/testdata/bear-640x360-a-cens-golden.mp4 b/packager/app/test/testdata/bear-640x360-a-cens-golden.mp4 new file mode 100644 index 0000000000..fe06b5ac9d Binary files /dev/null and b/packager/app/test/testdata/bear-640x360-a-cens-golden.mp4 differ diff --git a/packager/app/test/testdata/bear-640x360-av-cbcs-golden.mpd b/packager/app/test/testdata/bear-640x360-av-cbcs-golden.mpd index 972d28a625..119988cfc5 100644 --- a/packager/app/test/testdata/bear-640x360-av-cbcs-golden.mpd +++ b/packager/app/test/testdata/bear-640x360-av-cbcs-golden.mpd @@ -15,15 +15,15 @@ - + - + AAAAMHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAABAxMjM0NTY3ODkwMTIzNDU2 output_audio.mp4 - - + + diff --git a/packager/app/test/testdata/bear-640x360-av-cens-golden.mpd b/packager/app/test/testdata/bear-640x360-av-cens-golden.mpd index 627ef222cb..cbd0e46067 100644 --- a/packager/app/test/testdata/bear-640x360-av-cens-golden.mpd +++ b/packager/app/test/testdata/bear-640x360-av-cens-golden.mpd @@ -17,7 +17,7 @@ - + AAAAMHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAABAxMjM0NTY3ODkwMTIzNDU2 diff --git a/packager/app/widevine_encryption_flags.cc b/packager/app/widevine_encryption_flags.cc index 7c34a62baf..09ef9f0955 100644 --- a/packager/app/widevine_encryption_flags.cc +++ b/packager/app/widevine_encryption_flags.cc @@ -58,11 +58,7 @@ DEFINE_int32(crypto_period_duration, DEFINE_string(protection_scheme, "cenc", "Choose protection scheme, 'cenc' or 'cbc1' or pattern-based " - "protection schemes 'cens' or 'cbcs'. Note that if a " - "pattern-based protection scheme only applies to video stream; " - "audio stream will be encrypted using the corresponding " - "non-pattern-based protection schemes, i.e. 'cenc' for 'cens', " - "'cbc1' for 'cbcs'."); + "protection schemes 'cens' or 'cbcs'."); namespace shaka { diff --git a/packager/media/base/aes_cryptor.h b/packager/media/base/aes_cryptor.h index 2c4997f232..1ca85c1369 100644 --- a/packager/media/base/aes_cryptor.h +++ b/packager/media/base/aes_cryptor.h @@ -80,6 +80,9 @@ class AesCryptor { /// @return The current iv. const std::vector& iv() const { return iv_; } + /// @return true if constant iv is used, false otherwise. + bool use_constant_iv() const { return constant_iv_flag_ == kUseConstantIv; } + /// @param protection_scheme specifies the protection scheme: 'cenc', 'cens', /// 'cbc1', 'cbcs', which is useful to determine the random iv size. /// @param iv points to generated initialization vector. diff --git a/packager/media/base/aes_pattern_cryptor.cc b/packager/media/base/aes_pattern_cryptor.cc index 8de98ede90..55a3be6b81 100644 --- a/packager/media/base/aes_pattern_cryptor.cc +++ b/packager/media/base/aes_pattern_cryptor.cc @@ -23,7 +23,13 @@ AesPatternCryptor::AesPatternCryptor(uint8_t crypt_byte_block, skip_byte_block_(skip_byte_block), encryption_mode_(encryption_mode), cryptor_(cryptor.Pass()) { + // |crypt_byte_block_| should never be 0. |skip_byte_block_| can be 0 to allow + // a special pattern of 1:0, which is the pattern for the case of pattern + // encryption when applied to non video tracks. + DCHECK_NE(crypt_byte_block_, 0u); + DCHECK(skip_byte_block_ != 0 || crypt_byte_block_ == 1); DCHECK(cryptor_); + DCHECK(!cryptor_->use_constant_iv()); } AesPatternCryptor::~AesPatternCryptor() {} @@ -45,6 +51,17 @@ bool AesPatternCryptor::CryptInternal(const uint8_t* text, } *crypt_text_size = text_size; + // Handle the special pattern 1:0. + if (skip_byte_block_ == 0) { + DCHECK_EQ(crypt_byte_block_, 1u); + const size_t crypt_byte_size = text_size / AES_BLOCK_SIZE * AES_BLOCK_SIZE; + if (!cryptor_->Crypt(text, crypt_byte_size, crypt_text)) + return false; + memcpy(crypt_text + crypt_byte_size, text + crypt_byte_size, + text_size - crypt_byte_size); + return true; + } + while (text_size > 0) { const size_t crypt_byte_size = crypt_byte_block_ * AES_BLOCK_SIZE; if (NeedEncrypt(text_size, crypt_byte_size)) { diff --git a/packager/media/base/aes_pattern_cryptor.h b/packager/media/base/aes_pattern_cryptor.h index 7c4d364a38..ba351304a6 100644 --- a/packager/media/base/aes_pattern_cryptor.h +++ b/packager/media/base/aes_pattern_cryptor.h @@ -47,7 +47,8 @@ class AesPatternCryptor : public AesCryptor { /// internally inside Crypt call, i.e. iv will be updated across Crypt /// calls. /// @param cryptor points to an AesCryptor instance which performs the actual - /// encryption/decryption. + /// encryption/decryption. Note that @a cryptor shall not use constant + /// iv. AesPatternCryptor(uint8_t crypt_byte_block, uint8_t skip_byte_block, PatternEncryptionMode encryption_mode, diff --git a/packager/media/base/aes_pattern_cryptor_unittest.cc b/packager/media/base/aes_pattern_cryptor_unittest.cc index dc6dbfb423..f7f311be04 100644 --- a/packager/media/base/aes_pattern_cryptor_unittest.cc +++ b/packager/media/base/aes_pattern_cryptor_unittest.cc @@ -219,5 +219,35 @@ TEST(SampleAesPatternCryptor, MoreThan16Bytes) { ASSERT_TRUE(pattern_cryptor.Crypt("0123456789abcdef012", &crypt_text)); } +TEST(FullSampleSpecialPatternTest, Test) { + MockAesCryptor* mock_cryptor = new MockAesCryptor(); + EXPECT_CALL(*mock_cryptor, CryptInternal(_, 96u, _, _)) + .WillOnce(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++ = 'e'; + return true; + })); + + const uint8_t kFulLSampleCryptBlock = 1; + const uint8_t kFullSampleSkipBlock = 0; + AesPatternCryptor pattern_cryptor( + kFulLSampleCryptBlock, kFullSampleSkipBlock, + AesPatternCryptor::kSkipIfCryptByteBlockRemaining, + AesPatternCryptor::kUseConstantIv, + scoped_ptr(mock_cryptor)); + + std::vector iv(8, 'i'); + // SetIv will be called only once by AesPatternCryptor::SetIv. + EXPECT_TRUE(pattern_cryptor.SetIv(iv)); + + std::string input_text(100, 'c'); + std::string crypt_text; + // More than 16 bytes so mock's CryptInternal should be called. + ASSERT_TRUE(pattern_cryptor.Crypt(input_text, &crypt_text)); + EXPECT_EQ(std::string(96, 'e') + std::string(4, 'c'), crypt_text); +} + } // namespace media } // namespace shaka diff --git a/packager/media/formats/mp4/box_definitions.cc b/packager/media/formats/mp4/box_definitions.cc index 253820c922..55b45cc226 100644 --- a/packager/media/formats/mp4/box_definitions.cc +++ b/packager/media/formats/mp4/box_definitions.cc @@ -126,6 +126,11 @@ FourCC TrackTypeToFourCC(TrackType track_type) { } } +bool IsProtectionSchemeSupported(FourCC scheme) { + return scheme == FOURCC_cenc || scheme == FOURCC_cens || + scheme == FOURCC_cbc1 || scheme == FOURCC_cbcs; +} + } // namespace FileType::FileType() : major_brand(FOURCC_NULL), minor_version(0) {} @@ -483,9 +488,12 @@ bool ProtectionSchemeInfo::ReadWriteInternal(BoxBuffer* buffer) { buffer->PrepareChildren() && buffer->ReadWriteChild(&format) && buffer->ReadWriteChild(&type)); - RCHECK(type.type == FOURCC_cenc || type.type == FOURCC_cbc1 || - type.type == FOURCC_cens || type.type == FOURCC_cbcs); - RCHECK(buffer->ReadWriteChild(&info)); + if (IsProtectionSchemeSupported(type.type)) { + RCHECK(buffer->ReadWriteChild(&info)); + } else { + DLOG(WARNING) << "Ignore unsupported protection scheme: " + << FourCCToString(type.type); + } // Other protection schemes are silently ignored. Since the protection scheme // type can't be determined until this box is opened, we return 'true' for // non-CENC protection scheme types. It is the parent box's responsibility to @@ -1001,8 +1009,8 @@ bool SampleGroupDescription::ReadWriteInternal(BoxBuffer* buffer) { return ReadWriteEntries(buffer, &audio_roll_recovery_entries); default: DCHECK(buffer->Reading()); - DLOG(WARNING) << "Sample group '" << grouping_type - << "' is not supported."; + DLOG(WARNING) << "Ignore unsupported sample group: " + << FourCCToString(static_cast(grouping_type)); return true; } } @@ -1077,9 +1085,8 @@ bool SampleToGroup::ReadWriteInternal(BoxBuffer* buffer) { if (grouping_type != FOURCC_seig && grouping_type != FOURCC_roll) { DCHECK(buffer->Reading()); - DLOG(WARNING) << "Sample group " - << FourCCToString(static_cast(grouping_type)) - << " is not supported."; + DLOG(WARNING) << "Ignore unsupported sample group: " + << FourCCToString(static_cast(grouping_type)); return true; } @@ -1538,8 +1545,17 @@ bool VideoSampleEntry::ReadWriteInternal(BoxBuffer* buffer) { RCHECK(buffer->PrepareChildren()); - if (format == FOURCC_encv) - RCHECK(buffer->ReadWriteChild(&sinf)); + if (format == FOURCC_encv) { + if (buffer->Reading()) { + // Continue scanning until a supported protection scheme is found, or + // until we run out of protection schemes. + while (!IsProtectionSchemeSupported(sinf.type.type)) + RCHECK(buffer->ReadWriteChild(&sinf)); + } else { + DCHECK(IsProtectionSchemeSupported(sinf.type.type)); + RCHECK(buffer->ReadWriteChild(&sinf)); + } + } const FourCC actual_format = GetActualFormat(); if (buffer->Reading()) { @@ -1777,8 +1793,17 @@ bool AudioSampleEntry::ReadWriteInternal(BoxBuffer* buffer) { samplerate >>= 16; RCHECK(buffer->PrepareChildren()); - if (format == FOURCC_enca) - RCHECK(buffer->ReadWriteChild(&sinf)); + if (format == FOURCC_enca) { + if (buffer->Reading()) { + // Continue scanning until a supported protection scheme is found, or + // until we run out of protection schemes. + while (!IsProtectionSchemeSupported(sinf.type.type)) + RCHECK(buffer->ReadWriteChild(&sinf)); + } else { + DCHECK(IsProtectionSchemeSupported(sinf.type.type)); + RCHECK(buffer->ReadWriteChild(&sinf)); + } + } RCHECK(buffer->TryReadWriteChild(&esds)); RCHECK(buffer->TryReadWriteChild(&ddts)); diff --git a/packager/media/formats/mp4/encrypting_fragmenter.cc b/packager/media/formats/mp4/encrypting_fragmenter.cc index 8e69aac10c..d54dcced06 100644 --- a/packager/media/formats/mp4/encrypting_fragmenter.cc +++ b/packager/media/formats/mp4/encrypting_fragmenter.cc @@ -186,9 +186,18 @@ void EncryptingFragmenter::FinalizeFragmentForEncryption() { DCHECK(!IsSubsampleEncryptionRequired()); saiz.default_sample_info_size = per_sample_iv_size; } - // It should only happen with full sample encryption + constant iv, which is - // not a legal combination. - CHECK(!saiz.sample_info_sizes.empty() || saiz.default_sample_info_size != 0); + + // It should only happen with full sample encryption + constant iv, i.e. + // 'cbcs' applying to audio. + if (saiz.default_sample_info_size == 0 && saiz.sample_info_sizes.empty()) { + DCHECK_EQ(protection_scheme_, FOURCC_cbcs); + DCHECK(!IsSubsampleEncryptionRequired()); + // ISO/IEC 23001-7:2016(E) The sample auxiliary information would then be + // empty and should be emitted. Clear saiz and saio boxes so they are not + // written. + saiz.sample_count = 0; + traf()->auxiliary_offset.offsets.clear(); + } } Status EncryptingFragmenter::CreateEncryptor() { @@ -337,6 +346,8 @@ Status EncryptingFragmenter::EncryptSample(scoped_refptr sample) { traf()->auxiliary_size.sample_info_sizes.push_back( sample_encryption_entry.ComputeSize()); } else { + DCHECK_LE(crypt_byte_block(), 1u); + DCHECK_EQ(skip_byte_block(), 0u); EncryptBytes(data, sample->data_size()); } diff --git a/packager/media/formats/mp4/segmenter.cc b/packager/media/formats/mp4/segmenter.cc index fbb7a32516..5b35794e00 100644 --- a/packager/media/formats/mp4/segmenter.cc +++ b/packager/media/formats/mp4/segmenter.cc @@ -28,10 +28,6 @@ namespace media { namespace mp4 { namespace { -// For pattern-based encryption. -const uint8_t kCryptByteBlock = 1u; -const uint8_t kSkipByteBlock = 9u; - const size_t kCencKeyIdSize = 16u; // The version of cenc implemented here. CENC 4. @@ -43,6 +39,12 @@ const uint8_t kKeyRotationDefaultKeyId[] = { 0, 0, 0, 0, 0, 0, 0, 0 }; +// Defines protection pattern for pattern-based encryption. +struct ProtectionPattern { + uint8_t crypt_byte_block; + uint8_t skip_byte_block; +}; + COMPILE_ASSERT(arraysize(kKeyRotationDefaultKeyId) == kCencKeyIdSize, cenc_key_id_must_be_size_16); @@ -52,21 +54,37 @@ uint64_t Rescale(uint64_t time_in_old_scale, return static_cast(time_in_old_scale) / old_scale * new_scale; } -uint8_t GetCryptByteBlock(FourCC protection_scheme) { - return (protection_scheme == FOURCC_cbcs || protection_scheme == FOURCC_cens) - ? kCryptByteBlock - : 0; -} - -uint8_t GetSkipByteBlock(FourCC protection_scheme) { - return (protection_scheme == FOURCC_cbcs || protection_scheme == FOURCC_cens) - ? kSkipByteBlock - : 0; +ProtectionPattern GetProtectionPattern(FourCC protection_scheme, + TrackType track_type) { + ProtectionPattern pattern; + if (protection_scheme != FOURCC_cbcs && protection_scheme != FOURCC_cens) { + // Not using pattern encryption. + pattern.crypt_byte_block = 0u; + pattern.skip_byte_block = 0u; + } else if (track_type != kVideo) { + // Tracks other than video are protected using whole-block full-sample + // encryption, which is essentially a pattern of 1:0. Note that this may not + // be the same as the non-pattern based encryption counterparts, e.g. in + // 'cens' for full sample encryption, the whole sample is encrypted up to + // the last 16-byte boundary, see 23001-7:2016(E) 9.7; while in 'cenc' for + // full sample encryption, the last partial 16-byte block is also encrypted, + // see 23001-7:2016(E) 9.4.2. Another difference is the use of constant iv. + pattern.crypt_byte_block = 1u; + pattern.skip_byte_block = 0u; + } else { + // Use 1:9 pattern for video. + const uint8_t kCryptByteBlock = 1u; + const uint8_t kSkipByteBlock = 9u; + pattern.crypt_byte_block = kCryptByteBlock; + pattern.skip_byte_block = kSkipByteBlock; + } + return pattern; } void GenerateSinf(const EncryptionKey& encryption_key, FourCC old_type, FourCC protection_scheme, + ProtectionPattern pattern, ProtectionSchemeInfo* sinf) { sinf->format.format = old_type; @@ -85,16 +103,15 @@ void GenerateSinf(const EncryptionKey& encryption_key, } else { track_encryption.default_per_sample_iv_size = encryption_key.iv.size(); } - track_encryption.default_crypt_byte_block = - GetCryptByteBlock(protection_scheme); - track_encryption.default_skip_byte_block = - GetSkipByteBlock(protection_scheme); + track_encryption.default_crypt_byte_block = pattern.crypt_byte_block; + track_encryption.default_skip_byte_block = pattern.skip_byte_block; track_encryption.default_kid = encryption_key.key_id; } void GenerateEncryptedSampleEntry(const EncryptionKey& encryption_key, double clear_lead_in_seconds, FourCC protection_scheme, + ProtectionPattern pattern, SampleDescription* description) { DCHECK(description); if (description->type == kVideo) { @@ -106,7 +123,8 @@ void GenerateEncryptedSampleEntry(const EncryptionKey& encryption_key, // Convert the first entry to an encrypted entry. VideoSampleEntry& entry = description->video_entries[0]; - GenerateSinf(encryption_key, entry.format, protection_scheme, &entry.sinf); + GenerateSinf(encryption_key, entry.format, protection_scheme, pattern, + &entry.sinf); entry.format = FOURCC_encv; } else { DCHECK_EQ(kAudio, description->type); @@ -118,7 +136,8 @@ void GenerateEncryptedSampleEntry(const EncryptionKey& encryption_key, // Convert the first entry to an encrypted entry. AudioSampleEntry& entry = description->audio_entries[0]; - GenerateSinf(encryption_key, entry.format, protection_scheme, &entry.sinf); + GenerateSinf(encryption_key, entry.format, protection_scheme, pattern, + &entry.sinf); entry.format = FOURCC_enca; } } @@ -174,20 +193,12 @@ Status Segmenter::Initialize(const std::vector& streams, continue; } - FourCC local_protection_scheme = protection_scheme; - if (streams[i]->info()->stream_type() != kStreamVideo) { - // Pattern encryption should only be used with video. Replaces with the - // corresponding non-pattern encryption scheme. - if (protection_scheme == FOURCC_cbcs) - local_protection_scheme = FOURCC_cbc1; - else if (protection_scheme == FOURCC_cens) - local_protection_scheme = FOURCC_cenc; - } - KeySource::TrackType track_type = GetTrackTypeForEncryption(*streams[i]->info(), max_sd_pixels); SampleDescription& description = moov_->tracks[i].media.information.sample_table.description; + ProtectionPattern pattern = + GetProtectionPattern(protection_scheme, description.type); if (key_rotation_enabled) { // Fill encrypted sample entry with default key. @@ -195,17 +206,16 @@ Status Segmenter::Initialize(const std::vector& streams, encryption_key.key_id.assign( kKeyRotationDefaultKeyId, kKeyRotationDefaultKeyId + arraysize(kKeyRotationDefaultKeyId)); - if (!AesCryptor::GenerateRandomIv(local_protection_scheme, + if (!AesCryptor::GenerateRandomIv(protection_scheme, &encryption_key.iv)) { return Status(error::INTERNAL_ERROR, "Failed to generate random iv."); } GenerateEncryptedSampleEntry(encryption_key, clear_lead_in_seconds, - local_protection_scheme, &description); + protection_scheme, pattern, &description); if (muxer_listener_) { muxer_listener_->OnEncryptionInfoReady( - kInitialEncryptionInfo, local_protection_scheme, - encryption_key.key_id, encryption_key.iv, - encryption_key.key_system_info); + kInitialEncryptionInfo, protection_scheme, encryption_key.key_id, + encryption_key.iv, encryption_key.key_system_info); } fragmenters_[i] = new KeyRotationFragmenter( @@ -213,8 +223,8 @@ Status Segmenter::Initialize(const std::vector& streams, encryption_key_source, track_type, crypto_period_duration_in_seconds * streams[i]->info()->time_scale(), clear_lead_in_seconds * streams[i]->info()->time_scale(), - local_protection_scheme, GetCryptByteBlock(local_protection_scheme), - GetSkipByteBlock(local_protection_scheme), muxer_listener_); + protection_scheme, pattern.crypt_byte_block, pattern.skip_byte_block, + muxer_listener_); continue; } @@ -224,14 +234,14 @@ Status Segmenter::Initialize(const std::vector& streams, if (!status.ok()) return status; if (encryption_key->iv.empty()) { - if (!AesCryptor::GenerateRandomIv(local_protection_scheme, + if (!AesCryptor::GenerateRandomIv(protection_scheme, &encryption_key->iv)) { return Status(error::INTERNAL_ERROR, "Failed to generate random iv."); } } GenerateEncryptedSampleEntry(*encryption_key, clear_lead_in_seconds, - local_protection_scheme, &description); + protection_scheme, pattern, &description); if (moov_->pssh.empty()) { moov_->pssh.resize(encryption_key->key_system_info.size()); @@ -241,17 +251,15 @@ Status Segmenter::Initialize(const std::vector& streams, if (muxer_listener_) { muxer_listener_->OnEncryptionInfoReady( - kInitialEncryptionInfo, local_protection_scheme, - encryption_key->key_id, encryption_key->iv, - encryption_key->key_system_info); + kInitialEncryptionInfo, protection_scheme, encryption_key->key_id, + encryption_key->iv, encryption_key->key_system_info); } } fragmenters_[i] = new EncryptingFragmenter( streams[i]->info(), &moof_->tracks[i], encryption_key.Pass(), clear_lead_in_seconds * streams[i]->info()->time_scale(), - local_protection_scheme, GetCryptByteBlock(local_protection_scheme), - GetSkipByteBlock(local_protection_scheme)); + protection_scheme, pattern.crypt_byte_block, pattern.skip_byte_block); } // Choose the first stream if there is no VIDEO.