From 7a84443597e92a03f90806712f8629df40bf408e Mon Sep 17 00:00:00 2001 From: jrummell Date: Tue, 9 Jun 2015 16:51:00 -0700 Subject: Combine CENC 'pssh' box parsing routines. Also update the routines to ignore 'pssh' boxes with version 2 or later. BUG=460359, 460360 TEST=new unittests pass Committed: https://crrev.com/b666d7874efac44b359a95329f0cb890e97671df Cr-Commit-Position: refs/heads/master@{#333556} Review URL: https://codereview.chromium.org/1149023002 Cr-Commit-Position: refs/heads/master@{#333611} --- media/cdm/cenc_utils.cc | 215 ++++++++++++++------------------------- media/cdm/cenc_utils.h | 11 ++ media/cdm/cenc_utils_unittest.cc | 215 ++++++++++++++++++++++++++++++++------- 3 files changed, 267 insertions(+), 174 deletions(-) (limited to 'media/cdm') diff --git a/media/cdm/cenc_utils.cc b/media/cdm/cenc_utils.cc index 86779b2..495a6f2 100644 --- a/media/cdm/cenc_utils.cc +++ b/media/cdm/cenc_utils.cc @@ -4,7 +4,9 @@ #include "media/cdm/cenc_utils.h" -#include "media/base/bit_reader.h" +#include "base/stl_util.h" +#include "media/formats/mp4/box_definitions.h" +#include "media/formats/mp4/box_reader.h" namespace media { @@ -12,31 +14,6 @@ namespace media { // Encryption ('cenc') protection scheme may contain one or more protection // system specific header ('pssh') boxes. // ref: https://w3c.github.io/encrypted-media/cenc-format.html -// -// The format of a 'pssh' box is as follows: -// unsigned int(32) size; -// unsigned int(32) type = "pssh"; -// if (size==1) { -// unsigned int(64) largesize; -// } else if (size==0) { -// -- box extends to end of file -// } -// unsigned int(8) version; -// bit(24) flags; -// unsigned int(8)[16] SystemID; -// if (version > 0) -// { -// unsigned int(32) KID_count; -// { -// unsigned int(8)[16] KID; -// } [KID_count] -// } -// unsigned int(32) DataSize; -// unsigned int(8)[DataSize] Data; - -// Minimum size of a 'pssh' box includes all the required fields (size, type, -// version, flags, SystemID, DataSize). -const int kMinimumBoxSizeInBytes = 32; // SystemID for the Common System. // https://w3c.github.io/encrypted-media/cenc-format.html#common-system @@ -45,136 +22,102 @@ const uint8_t kCommonSystemId[] = { 0x10, 0x77, 0xef, 0xec, 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb, 0x4b }; -#define RCHECK(x) \ - do { \ - if (!(x)) \ - return false; \ - } while (0) - -// Helper function to read up to 32 bits from a bit stream. -static uint32_t ReadBits(BitReader* reader, int num_bits) { - DCHECK_GE(reader->bits_available(), num_bits); - DCHECK((num_bits > 0) && (num_bits <= 32)); - uint32_t value; - reader->ReadBits(num_bits, &value); - return value; -} - -// Checks whether the next 16 bytes matches the Common SystemID. -// Assumes |reader| has enough data. -static bool IsCommonSystemID(BitReader* reader) { - for (uint32_t i = 0; i < arraysize(kCommonSystemId); ++i) { - if (ReadBits(reader, 8) != kCommonSystemId[i]) - return false; - } - return true; -} - -// Checks that |reader| contains a valid 'ppsh' box header. |reader| is updated -// to point to the content immediately following the box header. Returns true -// if the header looks valid and |reader| contains enough data for the size of -// header. |size| is updated as the computed size of the box header. Otherwise -// false is returned. -static bool ValidBoxHeader(BitReader* reader, uint32* size) { - // Enough data for a miniumum size 'pssh' box? - uint32 available_bytes = reader->bits_available() / 8; - RCHECK(available_bytes >= kMinimumBoxSizeInBytes); - - *size = ReadBits(reader, 32); - - // Must be a 'pssh' box or else fail. - RCHECK(ReadBits(reader, 8) == 'p'); - RCHECK(ReadBits(reader, 8) == 's'); - RCHECK(ReadBits(reader, 8) == 's'); - RCHECK(ReadBits(reader, 8) == 'h'); - - if (*size == 1) { - // If largesize > 2**32 it is too big. - RCHECK(ReadBits(reader, 32) == 0); - *size = ReadBits(reader, 32); - } else if (*size == 0) { - *size = available_bytes; +static bool ReadAllPsshBoxes( + const std::vector& input, + std::vector* pssh_boxes) { + DCHECK(!input.empty()); + + // Verify that |input| contains only 'pssh' boxes. ReadAllChildren() is + // templated, so it checks that each box in |input| matches the box type of + // the parameter (in this case mp4::ProtectionSystemSpecificHeader is a + // 'pssh' box). mp4::ProtectionSystemSpecificHeader doesn't validate the + // 'pssh' contents, so this simply verifies that |input| only contains + // 'pssh' boxes and nothing else. + scoped_ptr input_reader( + mp4::BoxReader::ReadConcatentatedBoxes( + vector_as_array(&input), input.size())); + std::vector raw_pssh_boxes; + if (!input_reader->ReadAllChildren(&raw_pssh_boxes)) + return false; + + // Now that we have |input| parsed into |raw_pssh_boxes|, reparse each one + // into a mp4::FullProtectionSystemSpecificHeader, which extracts all the + // relevant fields from the box. Since there may be unparseable 'pssh' boxes + // (due to unsupported version, for example), this is done one by one, + // ignoring any boxes that can't be parsed. + for (const auto& raw_pssh_box : raw_pssh_boxes) { + scoped_ptr raw_pssh_reader( + mp4::BoxReader::ReadConcatentatedBoxes( + vector_as_array(&raw_pssh_box.raw_box), + raw_pssh_box.raw_box.size())); + // ReadAllChildren() appends any successfully parsed box onto it's + // parameter, so |pssh_boxes| will contain the collection of successfully + // parsed 'pssh' boxes. If an error occurs, try the next box. + if (!raw_pssh_reader->ReadAllChildren(pssh_boxes)) + continue; } - // Check that the buffer contains at least size bytes. - return available_bytes >= *size; + // Must have successfully parsed at least one 'pssh' box. + return pssh_boxes->size() > 0; } bool ValidatePsshInput(const std::vector& input) { - size_t offset = 0; - while (offset < input.size()) { - // Create a BitReader over the remaining part of the buffer. - BitReader reader(&input[offset], input.size() - offset); - uint32 size; - RCHECK(ValidBoxHeader(&reader, &size)); - - // Update offset to point at the next 'pssh' box (may not be one). - offset += size; - } + // No 'pssh' boxes is considered valid. + if (input.empty()) + return true; - // Only valid if this contains 0 or more 'pssh' boxes. - return offset == input.size(); + std::vector children; + return ReadAllPsshBoxes(input, &children); } bool GetKeyIdsForCommonSystemId(const std::vector& input, KeyIdList* key_ids) { - size_t offset = 0; KeyIdList result; + std::vector common_system_id( + kCommonSystemId, kCommonSystemId + arraysize(kCommonSystemId)); - while (offset < input.size()) { - BitReader reader(&input[offset], input.size() - offset); - uint32 size; - RCHECK(ValidBoxHeader(&reader, &size)); - - // Update offset to point at the next 'pssh' box (may not be one). - offset += size; - - // Check the version, as KIDs only available if version > 0. - uint8_t version = ReadBits(&reader, 8); - if (version == 0) - continue; - - // flags must be 0. If not, assume incorrect 'pssh' box and move to the - // next one. - if (ReadBits(&reader, 24) != 0) - continue; - - // Validate SystemID - RCHECK(static_cast(reader.bits_available()) >= - arraysize(kCommonSystemId) * 8); - if (!IsCommonSystemID(&reader)) - continue; // Not Common System, so try the next pssh box. - - // Since version > 0, next field is the KID_count. - RCHECK(static_cast(reader.bits_available()) >= - sizeof(uint32_t) * 8); - uint32_t count = ReadBits(&reader, 32); - - if (count == 0) - continue; + if (!input.empty()) { + std::vector children; + if (!ReadAllPsshBoxes(input, &children)) + return false; - // Make sure there is enough data for all the KIDs specified, and then - // extract them. - RCHECK(static_cast(reader.bits_available()) > count * 16 * 8); - while (count > 0) { - std::vector key; - key.reserve(16); - for (int i = 0; i < 16; ++i) { - key.push_back(ReadBits(&reader, 8)); - } - result.push_back(key); - --count; + // Check all children for an appropriate 'pssh' box, concatenating any + // key IDs found. + for (const auto& child : children) { + if (child.system_id == common_system_id && child.key_ids.size() > 0) + result.insert(result.end(), child.key_ids.begin(), child.key_ids.end()); } - - // Don't bother checking DataSize and Data. } - key_ids->swap(result); - + // No matching 'pssh' box found. // TODO(jrummell): This should return true only if there was at least one // key ID present. However, numerous test files don't contain the 'pssh' box // for Common Format, so no keys are found. http://crbug.com/460308 + key_ids->swap(result); return true; } +bool GetPsshData(const std::vector& input, + const std::vector& system_id, + std::vector* pssh_data) { + if (input.empty()) + return false; + + std::vector children; + if (!ReadAllPsshBoxes(input, &children)) + return false; + + // Check all children for an appropriate 'pssh' box, returning |data| from + // the first one found. + for (const auto& child : children) { + if (child.system_id == system_id) { + pssh_data->assign(child.data.begin(), child.data.end()); + return true; + } + } + + // No matching 'pssh' box found. + return false; +} + } // namespace media diff --git a/media/cdm/cenc_utils.h b/media/cdm/cenc_utils.h index ec85fb3..7dd82a1 100644 --- a/media/cdm/cenc_utils.h +++ b/media/cdm/cenc_utils.h @@ -28,6 +28,17 @@ MEDIA_EXPORT bool ValidatePsshInput(const std::vector& input); MEDIA_EXPORT bool GetKeyIdsForCommonSystemId(const std::vector& input, KeyIdList* key_ids); +// Gets the data field from the first 'pssh' box containing |system_id| UUID. +// Returns true if such a box is found and successfully parsed. Returns false +// otherwise. +// Notes: +// 1. If multiple PSSH boxes are found, the "Data" of the first matching 'pssh' +// box will be set in |pssh_data|. +// 2. Only PSSH boxes are allowed in |input|. +MEDIA_EXPORT bool GetPsshData(const std::vector& input, + const std::vector& system_id, + std::vector* pssh_data); + } // namespace media #endif // MEDIA_CDM_CENC_UTILS_H_ diff --git a/media/cdm/cenc_utils_unittest.cc b/media/cdm/cenc_utils_unittest.cc index 9923619..2e7ead2 100644 --- a/media/cdm/cenc_utils_unittest.cc +++ b/media/cdm/cenc_utils_unittest.cc @@ -25,6 +25,10 @@ const uint8_t kKey4Data[] = { 0x7E, 0x57, 0x1D, 0x04, 0x7E, 0x57, 0x1D, 0x06, 0x7E, 0x57, 0x1D, 0x04, 0x7E, 0x57, 0x1D, 0x06, }; +const uint8_t kCommonSystemSystemId[] = { + 0x10, 0x77, 0xEF, 0xEC, 0xC0, 0xB2, 0x4D, 0x02, + 0xAC, 0xE3, 0x3C, 0x1E, 0x52, 0xE2, 0xFB, 0x4B +}; class CencUtilsTest : public testing::Test { public: @@ -32,7 +36,10 @@ class CencUtilsTest : public testing::Test { : key1_(kKey1Data, kKey1Data + arraysize(kKey1Data)), key2_(kKey2Data, kKey2Data + arraysize(kKey2Data)), key3_(kKey3Data, kKey3Data + arraysize(kKey3Data)), - key4_(kKey4Data, kKey4Data + arraysize(kKey4Data)) {} + key4_(kKey4Data, kKey4Data + arraysize(kKey4Data)), + common_system_system_id_( + kCommonSystemSystemId, + kCommonSystemSystemId + arraysize(kCommonSystemSystemId)) {} protected: // Initialize the start of the 'pssh' box (up to key_count) @@ -58,23 +65,9 @@ class CencUtilsTest : public testing::Test { box->push_back(0); box->push_back(0); box->push_back(0); - // Add Clear Key SystemID. - box->push_back(0x10); - box->push_back(0x77); - box->push_back(0xEF); - box->push_back(0xEC); - box->push_back(0xC0); - box->push_back(0xB2); - box->push_back(0x4D); - box->push_back(0x02); - box->push_back(0xAC); - box->push_back(0xE3); - box->push_back(0x3C); - box->push_back(0x1E); - box->push_back(0x52); - box->push_back(0xE2); - box->push_back(0xFB); - box->push_back(0x4B); + // Add Common Encryption SystemID. + box->insert(box->end(), common_system_system_id_.begin(), + common_system_system_id_.end()); } std::vector MakePSSHBox(uint8_t version) { @@ -112,7 +105,7 @@ class CencUtilsTest : public testing::Test { box.push_back(1); // Add key1. - for (int i = 0; i < 16; ++i) + for (uint i = 0; i < key1.size(); ++i) box.push_back(key1[i]); // Add data_size (= 0). @@ -141,11 +134,11 @@ class CencUtilsTest : public testing::Test { box.push_back(2); // Add key1. - for (int i = 0; i < 16; ++i) + for (uint i = 0; i < key1.size(); ++i) box.push_back(key1[i]); // Add key2. - for (int i = 0; i < 16; ++i) + for (uint i = 0; i < key2.size(); ++i) box.push_back(key2[i]); // Add data_size (= 0). @@ -156,16 +149,32 @@ class CencUtilsTest : public testing::Test { return box; } + void AppendData(std::vector& pssh_box, + const std::vector& data) { + // This assumes that |pssh_box| has been created using the routines above, + // and simply appends the data to the end of it. It updates the box size + // and sets the data size. + DCHECK(data.size() < 100); + pssh_box[3] += data.size(); + pssh_box.pop_back(); + pssh_box.push_back(data.size()); + pssh_box.insert(pssh_box.end(), data.begin(), data.end()); + } + const std::vector& Key1() { return key1_; } const std::vector& Key2() { return key2_; } const std::vector& Key3() { return key3_; } const std::vector& Key4() { return key4_; } + const std::vector& CommonSystemSystemId() { + return common_system_system_id_; + } private: std::vector key1_; std::vector key2_; std::vector key3_; std::vector key4_; + std::vector common_system_system_id_; }; TEST_F(CencUtilsTest, EmptyPSSH) { @@ -214,9 +223,8 @@ TEST_F(CencUtilsTest, PSSHVersion0Plus1) { std::vector box0 = MakePSSHBox(0); std::vector box1 = MakePSSHBox(1, Key1()); - // Concatentate box1 into box0. - for (const auto& value : box1) - box0.push_back(value); + // Concatentate box1 onto end of box0. + box0.insert(box0.end(), box1.begin(), box1.end()); KeyIdList key_ids; EXPECT_TRUE(ValidatePsshInput(box0)); @@ -229,9 +237,8 @@ TEST_F(CencUtilsTest, PSSHVersion1Plus0) { std::vector box0 = MakePSSHBox(0); std::vector box1 = MakePSSHBox(1, Key1()); - // Concatentate box0 into box1. - for (const auto& value : box0) - box1.push_back(value); + // Concatentate box0 onto end of box1. + box1.insert(box1.end(), box0.begin(), box0.end()); KeyIdList key_ids; EXPECT_TRUE(ValidatePsshInput(box1)); @@ -245,12 +252,9 @@ TEST_F(CencUtilsTest, MultiplePSSHVersion1) { std::vector box1 = MakePSSHBox(1, Key3()); std::vector box2 = MakePSSHBox(1, Key4()); - // Concatentate box1 into box. - for (const auto& value : box1) - box.push_back(value); - // Concatentate box2 into box. - for (const auto& value : box2) - box.push_back(value); + // Concatentate box1 and box2 onto end of box. + box.insert(box.end(), box1.begin(), box1.end()); + box.insert(box.end(), box2.begin(), box2.end()); KeyIdList key_ids; EXPECT_TRUE(ValidatePsshInput(box)); @@ -268,11 +272,12 @@ TEST_F(CencUtilsTest, InvalidPSSH) { for (uint32 i = 1; i < box.size(); ++i) { // Modify size of data passed to be less than real size. std::vector truncated(&box[0], &box[0] + i); - EXPECT_FALSE(ValidatePsshInput(truncated)); + EXPECT_FALSE(ValidatePsshInput(truncated)) << "Failed for length " << i; EXPECT_FALSE(GetKeyIdsForCommonSystemId(truncated, &key_ids)); // Modify starting point. std::vector changed_offset(&box[i], &box[i] + box.size() - i); - EXPECT_FALSE(ValidatePsshInput(changed_offset)); + EXPECT_FALSE(ValidatePsshInput(changed_offset)) << "Failed for offset " + << i; EXPECT_FALSE(GetKeyIdsForCommonSystemId(changed_offset, &key_ids)); } } @@ -295,9 +300,7 @@ TEST_F(CencUtilsTest, InvalidFlags) { box[10] = 3; KeyIdList key_ids; - // TODO(jrummell): This should fail as the 'pssh' box is skipped. - EXPECT_TRUE(GetKeyIdsForCommonSystemId(box, &key_ids)); - EXPECT_EQ(0u, key_ids.size()); + EXPECT_FALSE(GetKeyIdsForCommonSystemId(box, &key_ids)); } TEST_F(CencUtilsTest, LongSize) { @@ -373,4 +376,140 @@ TEST_F(CencUtilsTest, HugeSize) { std::vector(data, data + arraysize(data)), &key_ids)); } +TEST_F(CencUtilsTest, GetPsshData_Version0) { + const uint8_t data_bytes[] = {0x01, 0x02, 0x03, 0x04}; + std::vector pssh_data; + + std::vector box = MakePSSHBox(0); + EXPECT_TRUE(GetPsshData(box, CommonSystemSystemId(), &pssh_data)); + EXPECT_EQ(0u, pssh_data.size()); + + std::vector data(data_bytes, data_bytes + arraysize(data_bytes)); + AppendData(box, data); + EXPECT_TRUE(GetPsshData(box, CommonSystemSystemId(), &pssh_data)); + EXPECT_EQ(data, pssh_data); +} + +TEST_F(CencUtilsTest, GetPsshData_Version1NoKeys) { + const uint8_t data_bytes[] = {0x05, 0x06, 0x07, 0x08}; + std::vector pssh_data; + + std::vector box = MakePSSHBox(1); + EXPECT_TRUE(GetPsshData(box, CommonSystemSystemId(), &pssh_data)); + EXPECT_EQ(0u, pssh_data.size()); + + std::vector data(data_bytes, data_bytes + arraysize(data_bytes)); + AppendData(box, data); + EXPECT_TRUE(GetPsshData(box, CommonSystemSystemId(), &pssh_data)); + EXPECT_EQ(data, pssh_data); +} + +TEST_F(CencUtilsTest, GetPsshData_Version1WithKeys) { + const uint8_t data_bytes[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; + std::vector pssh_data; + + std::vector box = MakePSSHBox(1, Key1()); + EXPECT_TRUE(GetPsshData(box, CommonSystemSystemId(), &pssh_data)); + EXPECT_EQ(0u, pssh_data.size()); + + std::vector data(data_bytes, data_bytes + arraysize(data_bytes)); + AppendData(box, data); + EXPECT_TRUE(GetPsshData(box, CommonSystemSystemId(), &pssh_data)); + EXPECT_EQ(data, pssh_data); +} + +TEST_F(CencUtilsTest, GetPsshData_Version2) { + std::vector pssh_data; + + std::vector box = MakePSSHBox(1, Key1()); + EXPECT_TRUE(GetPsshData(box, CommonSystemSystemId(), &pssh_data)); + + // Change the version manually, since we don't know what v2 will contain. + box[8] = 2; + EXPECT_FALSE(GetPsshData(box, CommonSystemSystemId(), &pssh_data)); +} + +TEST_F(CencUtilsTest, GetPsshData_Version2ThenVersion1) { + std::vector pssh_data; + + std::vector box_v1 = MakePSSHBox(1, Key1()); + std::vector box_v2 = MakePSSHBox(2, Key2(), Key3()); + + // Concatentate the boxes together (v2 first). + std::vector boxes; + boxes.insert(boxes.end(), box_v2.begin(), box_v2.end()); + boxes.insert(boxes.end(), box_v1.begin(), box_v1.end()); + EXPECT_TRUE(GetPsshData(boxes, CommonSystemSystemId(), &pssh_data)); + + // GetKeyIdsForCommonSystemId() should return the single key from the v1 + // 'pssh' box. + KeyIdList key_ids; + EXPECT_TRUE(GetKeyIdsForCommonSystemId(boxes, &key_ids)); + EXPECT_EQ(1u, key_ids.size()); + EXPECT_EQ(key_ids[0], Key1()); +} + +TEST_F(CencUtilsTest, GetPsshData_Version1ThenVersion2) { + std::vector pssh_data; + + std::vector box_v1 = MakePSSHBox(1, Key3()); + std::vector box_v2 = MakePSSHBox(2, Key4()); + + // Concatentate the boxes together (v1 first). + std::vector boxes; + boxes.insert(boxes.end(), box_v1.begin(), box_v1.end()); + boxes.insert(boxes.end(), box_v2.begin(), box_v2.end()); + EXPECT_TRUE(GetPsshData(boxes, CommonSystemSystemId(), &pssh_data)); + + // GetKeyIdsForCommonSystemId() should return the single key from the v1 + // 'pssh' box. + KeyIdList key_ids; + EXPECT_TRUE(GetKeyIdsForCommonSystemId(boxes, &key_ids)); + EXPECT_EQ(1u, key_ids.size()); + EXPECT_EQ(key_ids[0], Key3()); +} + +TEST_F(CencUtilsTest, GetPsshData_DifferentSystemID) { + std::vector unknown_system_id(kKey1Data, + kKey1Data + arraysize(kKey1Data)); + std::vector pssh_data; + + std::vector box = MakePSSHBox(1, Key1()); + EXPECT_TRUE(GetPsshData(box, CommonSystemSystemId(), &pssh_data)); + EXPECT_FALSE(GetPsshData(box, unknown_system_id, &pssh_data)); +} + +TEST_F(CencUtilsTest, GetPsshData_MissingData) { + const uint8_t data_bytes[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; + std::vector pssh_data; + + std::vector box = MakePSSHBox(1, Key1()); + std::vector data(data_bytes, data_bytes + arraysize(data_bytes)); + AppendData(box, data); + EXPECT_TRUE(GetPsshData(box, CommonSystemSystemId(), &pssh_data)); + + // Remove some data from the end, so now the size is incorrect. + box.pop_back(); + box.pop_back(); + EXPECT_FALSE(GetPsshData(box, CommonSystemSystemId(), &pssh_data)); +} + +TEST_F(CencUtilsTest, GetPsshData_MultiplePssh) { + const uint8_t data1_bytes[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}; + const uint8_t data2_bytes[] = {0xa1, 0xa2, 0xa3, 0xa4}; + std::vector pssh_data; + + std::vector box1 = MakePSSHBox(1, Key1()); + std::vector data1(data1_bytes, data1_bytes + arraysize(data1_bytes)); + AppendData(box1, data1); + + std::vector box2 = MakePSSHBox(0); + std::vector data2(data2_bytes, data2_bytes + arraysize(data2_bytes)); + AppendData(box2, data2); + + box1.insert(box1.end(), box2.begin(), box2.end()); + EXPECT_TRUE(GetPsshData(box1, CommonSystemSystemId(), &pssh_data)); + EXPECT_EQ(data1, pssh_data); + EXPECT_NE(data2, pssh_data); +} } // namespace media -- cgit v1.1