From 7d9223f659d5b8b588f20cb1d1768c0472409990 Mon Sep 17 00:00:00 2001 From: CastagnaIT Date: Thu, 29 Aug 2024 17:08:15 +0200 Subject: [PATCH 1/6] Reorganized PSSH utils --- src/Session.cpp | 4 +- src/decrypters/CMakeLists.txt | 4 + .../HelperPr.cpp} | 95 ++---- src/decrypters/HelperPr.h | 60 ++++ src/decrypters/HelperWv.cpp | 198 +++++++++++ src/decrypters/HelperWv.h | 45 +++ src/decrypters/Helpers.cpp | 307 ++++++++++-------- src/decrypters/Helpers.h | 94 ++++-- src/parser/CMakeLists.txt | 2 - src/parser/DASHTree.cpp | 6 +- src/parser/HLSTree.cpp | 12 +- src/parser/PRProtectionParser.h | 89 ----- src/parser/SmoothTree.cpp | 10 +- src/parser/SmoothTree.h | 9 +- src/test/CMakeLists.txt | 3 +- 15 files changed, 581 insertions(+), 357 deletions(-) rename src/{parser/PRProtectionParser.cpp => decrypters/HelperPr.cpp} (73%) create mode 100644 src/decrypters/HelperPr.h create mode 100644 src/decrypters/HelperWv.cpp create mode 100644 src/decrypters/HelperWv.h delete mode 100644 src/parser/PRProtectionParser.h diff --git a/src/Session.cpp b/src/Session.cpp index b80b6e0a2..f8fe78161 100644 --- a/src/Session.cpp +++ b/src/Session.cpp @@ -422,9 +422,7 @@ bool CSession::InitializeDRM(bool addDefaultKID /* = false */) LOG::Log(LOGDEBUG, "License data: Create Widevine PSSH for SmoothStreaming %s", licenseData.empty() ? "" : "(with custom data)"); - std::vector wvPsshData; - if (DRM::MakeWidevinePsshData(defaultKid, licenseData, wvPsshData)) - DRM::MakePssh(DRM::ID_WIDEVINE, wvPsshData, initData); + initData = DRM::PSSH::MakeWidevine({defaultKid}, licenseData); } } else if (licenseType == "com.microsoft.playready") diff --git a/src/decrypters/CMakeLists.txt b/src/decrypters/CMakeLists.txt index 717eb2fc0..50ef58ecb 100644 --- a/src/decrypters/CMakeLists.txt +++ b/src/decrypters/CMakeLists.txt @@ -1,11 +1,15 @@ set(SOURCES DrmFactory.cpp Helpers.cpp + HelperPr.cpp + HelperWv.cpp ) set(HEADERS DrmFactory.h Helpers.h + HelperPr.h + HelperWv.h IDecrypter.h ) diff --git a/src/parser/PRProtectionParser.cpp b/src/decrypters/HelperPr.cpp similarity index 73% rename from src/parser/PRProtectionParser.cpp rename to src/decrypters/HelperPr.cpp index 79a6f441f..0b0efe4f3 100644 --- a/src/parser/PRProtectionParser.cpp +++ b/src/decrypters/HelperPr.cpp @@ -1,14 +1,13 @@ /* - * Copyright (C) 2023 Team Kodi + * Copyright (C) 2024 Team Kodi * This file is part of Kodi - https://kodi.tv * * SPDX-License-Identifier: GPL-2.0-or-later * See LICENSES/README.md for more information. */ -#include "PRProtectionParser.h" - -#include "decrypters/Helpers.h" +#include "HelperPr.h" +#include "utils/log.h" #include "utils/Base64Utils.h" #include "utils/CharArrayParser.h" #include "utils/StringUtils.h" @@ -24,23 +23,39 @@ using namespace UTILS; namespace { constexpr uint16_t PLAYREADY_WRM_TAG = 0x0001; + +// \brief Convert a PlayReady KID to Widevine KID format +std::vector ConvertKidtoWv(std::vector kid) +{ + if (kid.size() != 16) + return {}; + + std::vector remapped; + static const size_t remap[16] = {3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15}; + // Reordering bytes + for (size_t i{0}; i < 16; ++i) + { + remapped.emplace_back(kid[remap[i]]); + } + return remapped; +} } // unnamed namespace -bool adaptive::PRProtectionParser::ParseHeader(std::string_view prHeaderBase64) +bool DRM::PRHeaderParser::Parse(std::string_view prHeaderBase64) { - return ParseHeader(BASE64::Decode(prHeaderBase64)); + return Parse(BASE64::Decode(prHeaderBase64)); } -bool adaptive::PRProtectionParser::ParseHeader(const std::vector& prHeader) +bool DRM::PRHeaderParser::Parse(const std::vector& prHeader) { m_KID.clear(); m_licenseURL.clear(); - m_PSSH.clear(); + m_initData.clear(); if (prHeader.empty()) return false; - m_PSSH = prHeader; + m_initData = prHeader; // Parse header object data CCharArrayParser charParser; @@ -176,7 +191,7 @@ bool adaptive::PRProtectionParser::ParseHeader(const std::vector& prHea else if (algid == "AESCBC") m_encryption = EncryptionType::AESCBC; } - } + } } } } @@ -186,7 +201,7 @@ bool adaptive::PRProtectionParser::ParseHeader(const std::vector& prHea std::vector prKid = BASE64::Decode(kidBase64); if (prKid.size() == 16) { - m_KID = DRM::ConvertPrKidtoWvKid(prKid); + m_KID = ConvertKidtoWv(prKid); } else LOG::LogF(LOGWARNING, "KID size %zu instead of 16, KID ignored.", prKid.size()); @@ -197,61 +212,3 @@ bool adaptive::PRProtectionParser::ParseHeader(const std::vector& prHea return true; } - -bool adaptive::CPsshParser::Parse(const std::vector& data) -{ - CCharArrayParser charParser; - charParser.Reset(data.data(), data.size()); - - // BMFF box header (4byte size + 4byte type) - if (charParser.CharsLeft() < 8) - return false; - const uint32_t boxSize = charParser.ReadNextUnsignedInt(); - - if (std::memcmp(charParser.GetDataPos(), m_boxTypePssh, 4) != 0) - { - LOG::LogF(LOGERROR, "Wrong PSSH box type."); - return false; - } - charParser.SkipChars(4); - - // Box header - if (charParser.CharsLeft() < 4) - return false; - const uint32_t header = charParser.ReadNextUnsignedInt(); - - m_version = (header >> 24) & 0x000000FF; - m_flags = header & 0x00FFFFFF; - - // SystemID - if (charParser.CharsLeft() < 16) - return false; - charParser.ReadNextArray(16, m_systemId); - - if (m_version == 1) - { - // KeyIDs - if (charParser.CharsLeft() < 4) - return false; - uint32_t kidCount = charParser.ReadNextUnsignedInt(); - while (kidCount > 0) - { - if (charParser.CharsLeft() < 16) - return false; - - std::vector kid; - if (charParser.ReadNextArray(16, kid)) - m_keyIds.emplace_back(kid); - - kidCount--; - } - } - // Data - if (charParser.CharsLeft() < 4) - return false; - const uint32_t dataSize = charParser.ReadNextUnsignedInt(); - if (!charParser.ReadNextArray(dataSize, m_data)) - return false; - - return true; -} diff --git a/src/decrypters/HelperPr.h b/src/decrypters/HelperPr.h new file mode 100644 index 000000000..c990b9d2e --- /dev/null +++ b/src/decrypters/HelperPr.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include +#include +#include +#include + +namespace DRM +{ + +// \brief DRM PlayReady header protection parser +class PRHeaderParser +{ +public: + enum class EncryptionType + { + UNKNOWN, + AESCTR, // cenc + AESCBC, // cbcs + }; + + /*! + * \brief Parse PlayReady header data. + * \param prHeader The PlayReady header data as base64 string + * \return True if parsed with success, otherwise false + */ + bool Parse(std::string_view prHeaderBase64); + + bool Parse(const std::vector& prHeader); + + /*! + * \brief Determines if there is PlayReady protection + * \return True if there is PlayReady protection, otherwise false + */ + bool HasProtection() const { return !m_initData.empty(); } + + /*! + * \brief Get keyid as 16 bytes format (converted for Widevine DRM) + */ + const std::vector& GetKID() const { return m_KID; } + EncryptionType GetEncryption() const { return m_encryption; } + std::string_view GetLicenseURL() const { return m_licenseURL; } + const std::vector& GetInitData() const { return m_initData; } + +private: + std::vector m_KID; + EncryptionType m_encryption{EncryptionType::UNKNOWN}; + std::string m_licenseURL; + std::vector m_initData; +}; + +} // namespace DRM diff --git a/src/decrypters/HelperWv.cpp b/src/decrypters/HelperWv.cpp new file mode 100644 index 000000000..846dc87c3 --- /dev/null +++ b/src/decrypters/HelperWv.cpp @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2024 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "HelperWv.h" +#include "utils/log.h" + +#include + +namespace +{ +constexpr uint8_t PSSHBOX_HEADER_PSSH[4] = {0x70, 0x73, 0x73, 0x68}; +constexpr uint8_t PSSHBOX_HEADER_VER0[4] = {0x00, 0x00, 0x00, 0x00}; + +// Protection scheme identifying the encryption algorithm. The protection +// scheme is represented as a uint32 value. The uint32 contains 4 bytes each +// representing a single ascii character in one of the 4CC protection scheme values. +enum class WIDEVINE_PROT_SCHEME +{ + CENC = 0x63656E63, + CBC1 = 0x63626331, + CENS = 0x63656E73, + CBCS = 0x63626373 +}; + +/*! + * \brief Make a protobuf tag. + * \param fieldNumber The field number + * \param wireType The wire type: + * 0 = varint (int32, int64, uint32, uint64, sint32, sint64, bool, enum) + * 1 = 64 bit (fixed64, sfixed64, double) + * 2 = Length-delimited (string, bytes, embedded messages, packed repeated fields) + * 5 = 32 bit (fixed32, sfixed32, float) + * \return The tag. + */ +int MakeProtobufTag(int fieldNumber, int wireType) +{ + return (fieldNumber << 3) | wireType; +} + +// \brief Write a protobuf varint value to the data +void WriteProtobufVarint(std::vector& data, int size) +{ + do + { + uint8_t byte = size & 127; + size >>= 7; + if (size > 0) + byte |= 128; // Varint continuation + data.emplace_back(byte); + } while (size > 0); +} + +// \brief Read a protobuf varint value from the data +int ReadProtobufVarint(const std::vector& data, size_t& offset) +{ + int value = 0; + int shift = 0; + while (true) + { + uint8_t byte = data[offset++]; + value |= (byte & 0x7F) << shift; + if (!(byte & 0x80)) + break; + shift += 7; + } + return value; +} + +/*! + * \brief Replace in a vector, a sequence of vector data with another one. + * \param data The data to be modified + * \param sequence The sequence of data to be searched + * \param replace The data used to replace the sequence + * \return True if the data has been modified, otherwise false. + */ +bool ReplaceVectorSeq(std::vector& data, + const std::vector& sequence, + const std::vector& replace) +{ + auto it = std::search(data.begin(), data.end(), sequence.begin(), sequence.end()); + if (it != data.end()) + { + it = data.erase(it, it + sequence.size()); + data.insert(it, replace.begin(), replace.end()); + return true; + } + return false; +} + +std::vector ConvertKidToUUIDVec(const std::vector& kid) +{ + if (kid.size() != 16) + return {}; + + static char hexDigits[] = "0123456789abcdef"; + std::vector uuid; + uuid.reserve(32); + + for (size_t i = 0; i < 16; ++i) + { + if (i == 4 || i == 6 || i == 8 || i == 10) + uuid.emplace_back('-'); + + uuid.emplace_back(hexDigits[kid[i] >> 4]); + uuid.emplace_back(hexDigits[kid[i] & 15]); + } + + return uuid; +} +} // unnamed namespace + +std::vector DRM::MakeWidevinePsshData(const std::vector>& keyIds, + std::vector contentIdData) +{ + std::vector wvPsshData; + + if (keyIds.empty()) + { + LOG::LogF(LOGERROR, "Cannot make Widevine PSSH, key id's must be supplied"); + return {}; + } + + // The generated synthesized Widevine PSSH box require minimal contents: + // - The key_id field set with the KID + // - The content_id field copied from the key_id field (but we allow custom content) + + // Create "key_id" field, id: 2 (repeated if multiples) + for (const std::vector& keyId : keyIds) + { + wvPsshData.push_back(MakeProtobufTag(2, 2)); + WriteProtobufVarint(wvPsshData, static_cast(keyId.size())); // Write data size + wvPsshData.insert(wvPsshData.end(), keyId.begin(), keyId.end()); + } + + // Prepare "content_id" data + const std::vector& keyId = keyIds.front(); + + if (contentIdData.empty()) // If no data, by default add the KID if single + { + if (keyIds.size() == 1) + contentIdData.insert(contentIdData.end(), keyId.begin(), keyId.end()); + } + else + { + // Replace placeholders if needed + static const std::vector phKid = {'{', 'K', 'I', 'D', '}'}; + ReplaceVectorSeq(contentIdData, phKid, keyId); + + static const std::vector phUuid = {'{', 'U', 'U', 'I', 'D', '}'}; + const std::vector kidUuid = ConvertKidToUUIDVec(keyId); + ReplaceVectorSeq(contentIdData, phUuid, kidUuid); + } + + if (!contentIdData.empty()) + { + // Create "content_id" field, id: 4 + wvPsshData.push_back(MakeProtobufTag(4, 2)); + WriteProtobufVarint(wvPsshData, static_cast(contentIdData.size())); // Write data size + wvPsshData.insert(wvPsshData.end(), contentIdData.begin(), contentIdData.end()); + } + + // Create "protection_scheme" field, id: 9 + // wvPsshData.push_back(MakeProtobufTag(9, 0)); + // WriteProtobufVarint(wvPsshData, static_cast(WIDEVINE_PROT_SCHEME::CENC)); + + return wvPsshData; +} + +void DRM::ParseWidevinePssh(const std::vector& wvPsshData, + std::vector>& keyIds) +{ + keyIds.clear(); + + size_t offset = 0; + while (offset < wvPsshData.size()) + { + uint8_t tag = wvPsshData[offset++]; + int fieldNumber = tag >> 3; + int wireType = tag & 0x07; + + if (fieldNumber == 2 && wireType == 2) // "key_id" field, id: 2 + { + int length = ReadProtobufVarint(wvPsshData, offset); + keyIds.emplace_back(wvPsshData.begin() + offset, wvPsshData.begin() + offset + length); + } + else // Skip other fields + { + int length = ReadProtobufVarint(wvPsshData, offset); + if (wireType != 0) // Wire type 0 has no field size + offset += length; + } + } +} diff --git a/src/decrypters/HelperWv.h b/src/decrypters/HelperWv.h new file mode 100644 index 000000000..623404bb5 --- /dev/null +++ b/src/decrypters/HelperWv.h @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include +#include + +namespace DRM +{ + +/*! + * \brief Generate a synthesized Widevine PSSH. + * (WidevinePsshData as google protobuf format + * https://github.com/devine-dl/pywidevine/blob/master/pywidevine/license_protocol.proto) + * \param kid The KeyId + * \param contentIdData Custom content for the "content_id" field as bytes + * Placeholders allowed: + * {KID} To inject the KID as bytes + * {UUID} To inject the KID as UUID string format + * \return The pssh if has success, otherwise empty value. + */ +std::vector MakeWidevinePsshData(const std::vector>& keyIds, + std::vector contentIdData); + +/*! + * \brief Generate a synthesized Widevine PSSH. + * (WidevinePsshData as google protobuf format + * https://github.com/devine-dl/pywidevine/blob/master/pywidevine/license_protocol.proto) + * \param kid The KeyId + * \param contentIdData Custom content for the "content_id" field as bytes + * Placeholders allowed: + * {KID} To inject the KID as bytes + * {UUID} To inject the KID as UUID string format + * \return The pssh if has success, otherwise empty value. + */ +void ParseWidevinePssh(const std::vector& wvPsshData, + std::vector>& keyIds); + +} // namespace DRM diff --git a/src/decrypters/Helpers.cpp b/src/decrypters/Helpers.cpp index 8cf1300fa..26ec1adfe 100644 --- a/src/decrypters/Helpers.cpp +++ b/src/decrypters/Helpers.cpp @@ -8,7 +8,10 @@ #include "Helpers.h" +#include "HelperPr.h" +#include "HelperWv.h" #include "utils/Base64Utils.h" +#include "utils/CharArrayParser.h" #include "utils/DigestMD5Utils.h" #include "utils/StringUtils.h" #include "utils/UrlUtils.h" @@ -21,66 +24,15 @@ using namespace UTILS; namespace { constexpr uint8_t PSSHBOX_HEADER_PSSH[4] = {0x70, 0x73, 0x73, 0x68}; -constexpr uint8_t PSSHBOX_HEADER_VER0[4] = {0x00, 0x00, 0x00, 0x00}; - -// Protection scheme identifying the encryption algorithm. The protection -// scheme is represented as a uint32 value. The uint32 contains 4 bytes each -// representing a single ascii character in one of the 4CC protection scheme values. -enum class WIDEVINE_PROT_SCHEME { - CENC = 0x63656E63, - CBC1 = 0x63626331, - CENS = 0x63656E73, - CBCS = 0x63626373 -}; - -/*! - * \brief Make a protobuf tag. - * \param fieldNumber The field number - * \param wireType The wire type: - * 0 = varint (int32, int64, uint32, uint64, sint32, sint64, bool, enum) - * 1 = 64 bit (fixed64, sfixed64, double) - * 2 = Length-delimited (string, bytes, embedded messages, packed repeated fields) - * 5 = 32 bit (fixed32, sfixed32, float) - * \return The tag. - */ -int MakeProtobufTag(int fieldNumber, int wireType) -{ - return (fieldNumber << 3) | wireType; -} -// \brief Write a protobuf varint value to the data -void WriteProtobufVarint(std::vector& data, int size) +void WriteBigEndianInt(std::vector& data, const uint32_t value) { - do - { - uint8_t byte = size & 127; - size >>= 7; - if (size > 0) - byte |= 128; // Varint continuation - data.emplace_back(byte); - } while (size > 0); + data.emplace_back(static_cast((value >> 24) & 0xFF)); + data.emplace_back(static_cast((value >> 16) & 0xFF)); + data.emplace_back(static_cast((value >> 8) & 0xFF)); + data.emplace_back(static_cast(value & 0xFF)); } -/*! - * \brief Replace in a vector, a sequence of vector data with another one. - * \param data The data to be modified - * \param sequence The sequence of data to be searched - * \param replace The data used to replace the sequence - * \return True if the data has been modified, otherwise false. - */ -bool ReplaceVectorSeq(std::vector& data, - const std::vector& sequence, - const std::vector& replace) -{ - auto it = std::search(data.begin(), data.end(), sequence.begin(), sequence.end()); - if (it != data.end()) - { - it = data.erase(it, it + sequence.size()); - data.insert(it, replace.begin(), replace.end()); - return true; - } - return false; -} } // unnamed namespace std::string DRM::GenerateUrlDomainHash(std::string_view url) @@ -174,122 +126,197 @@ std::string DRM::ConvertKidBytesToUUID(std::vector kid) return uuid; } -std::vector DRM::ConvertKidToUUIDVec(const std::vector& kid) +bool DRM::IsValidPsshHeader(const std::vector& pssh) { - if (kid.size() != 16) + return pssh.size() >= 8 && std::equal(pssh.begin() + 4, pssh.begin() + 8, PSSHBOX_HEADER_PSSH); +} + +std::vector DRM::PSSH::Make(const uint8_t* systemId, + const std::vector>& keyIds, + const std::vector& initData, + const uint8_t version, + const uint32_t flags) +{ + if (!systemId) + { + LOG::LogF(LOGERROR, "Cannot make PSSH, no system id"); + return {}; + } + if (version > 1) + { + LOG::LogF(LOGERROR, "Cannot make PSSH, version %u not supported", version); + return {}; + } + if (initData.empty() && keyIds.empty()) + { + LOG::LogF(LOGERROR, "Cannot make PSSH, init data or key id's must be supplied"); return {}; + } - static char hexDigits[] = "0123456789abcdef"; - std::vector uuid; - uuid.reserve(32); + std::vector psshBox; + psshBox.resize(4, 0); // Size field of 4 bytes (updated later) - for (size_t i = 0; i < 16; ++i) - { - if (i == 4 || i == 6 || i == 8 || i == 10) - uuid.emplace_back('-'); + psshBox.insert(psshBox.end(), PSSHBOX_HEADER_PSSH, PSSHBOX_HEADER_PSSH + 4); - uuid.emplace_back(hexDigits[kid[i] >> 4]); - uuid.emplace_back(hexDigits[kid[i] & 15]); - } + psshBox.emplace_back(version); - return uuid; -} + psshBox.push_back((flags >> 16) & 0xFF); + psshBox.push_back((flags >> 8) & 0xFF); + psshBox.push_back(flags & 0xFF); -std::vector DRM::ConvertPrKidtoWvKid(std::vector kid) -{ - if (kid.size() != 16) - return {}; + psshBox.insert(psshBox.end(), systemId, systemId + 16); - std::vector remapped; - static const size_t remap[16] = {3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15}; - // Reordering bytes - for (size_t i{0}; i < 16; ++i) + if (version == 1) // If version 1, add KID's { - remapped.emplace_back(kid[remap[i]]); + WriteBigEndianInt(psshBox, static_cast(keyIds.size())); + for (const std::vector& keyId : keyIds) + { + if (keyId.size() != 16) + { + LOG::LogF(LOGERROR, "Cannot make PSSH, wrong KID size"); + return {}; + } + psshBox.insert(psshBox.end(), keyId.begin(), keyId.end()); + } } - return remapped; + + // Add init data size + WriteBigEndianInt(psshBox, static_cast(initData.size())); + + // Add init data + psshBox.insert(psshBox.end(), initData.begin(), initData.end()); + + // Update box size (first 4 bytes) + const uint32_t boxSize = static_cast(psshBox.size()); + psshBox[0] = static_cast((boxSize >> 24) & 0xFF); + psshBox[1] = static_cast((boxSize >> 16) & 0xFF); + psshBox[2] = static_cast((boxSize >> 8) & 0xFF); + psshBox[3] = static_cast(boxSize & 0xFF); + + return psshBox; } -bool DRM::IsValidPsshHeader(const std::vector& pssh) +std::vector DRM::PSSH::MakeWidevine(const std::vector>& keyIds, + const std::vector& initData, + const uint8_t version, + const uint32_t flags) { - return pssh.size() >= 8 && std::equal(pssh.begin() + 4, pssh.begin() + 8, PSSHBOX_HEADER_PSSH); + // Make Widevine pssh data + const std::vector wvPsshData = DRM::MakeWidevinePsshData(keyIds, initData); + if (wvPsshData.empty()) + return {}; + + return Make(ID_WIDEVINE, keyIds, wvPsshData); } -bool DRM::MakeWidevinePsshData(const std::vector& kid, - std::vector contentIdData, - std::vector& wvPsshData) +bool DRM::PSSH::Parse(const std::vector& data) { - wvPsshData.clear(); + ResetData(); + CCharArrayParser charParser; + charParser.Reset(data.data(), data.size()); - if (kid.empty()) + // BMFF box header (4byte size + 4byte type) + if (charParser.CharsLeft() < 8) + { + LOG::LogF(LOGERROR, "Cannot parse PSSH data, malformed data."); + return false; + } + const uint32_t boxSize = charParser.ReadNextUnsignedInt(); + + if (!std::equal(charParser.GetDataPos(), charParser.GetDataPos() + 4, PSSHBOX_HEADER_PSSH)) + { + LOG::LogF(LOGERROR, "Cannot parse PSSH data, no PSSH box type."); + return false; + } + charParser.SkipChars(4); + + // Box header + if (charParser.CharsLeft() < 4) + { + LOG::LogF(LOGERROR, "Cannot parse PSSH data, malformed data."); return false; + } - // The generated synthesized Widevine PSSH box require minimal contents: - // - The key_id field set with the KID - // - The content_id field copied from the key_id field (but we allow custom content) + const uint32_t header = charParser.ReadNextUnsignedInt(); - // Create "key_id" field, id: 2 (can be repeated if multiples) - wvPsshData.push_back(MakeProtobufTag(2, 2)); - WriteProtobufVarint(wvPsshData, static_cast(kid.size())); // Write data size - wvPsshData.insert(wvPsshData.end(), kid.begin(), kid.end()); + m_version = (header >> 24) & 0x000000FF; + m_flags = header & 0x00FFFFFF; - // Prepare "content_id" data - if (contentIdData.empty()) // If no data, by default add the KID + // SystemID + if (charParser.CharsLeft() < 16) { - contentIdData.insert(contentIdData.end(), kid.begin(), kid.end()); + LOG::LogF(LOGERROR, "Cannot parse PSSH data, malformed data."); + return false; } - else - { - // Replace placeholders if needed - static const std::vector phKid = {'{', 'K', 'I', 'D', '}'}; - ReplaceVectorSeq(contentIdData, phKid, kid); + charParser.ReadNextArray(16, m_systemId); - static const std::vector phUuid = {'{', 'U', 'U', 'I', 'D', '}'}; - const std::vector kidUuid = ConvertKidToUUIDVec(kid); - ReplaceVectorSeq(contentIdData, phUuid, kidUuid); - } + if (m_version == 1) // If version 1, get key id's from pssh field + { + // KeyIDs + if (charParser.CharsLeft() < 4) + { + LOG::LogF(LOGERROR, "Cannot parse PSSH data, malformed data."); + return false; + } - // Create "content_id" field, id: 4 - wvPsshData.push_back(MakeProtobufTag(4, 2)); - WriteProtobufVarint(wvPsshData, static_cast(contentIdData.size())); // Write data size - wvPsshData.insert(wvPsshData.end(), contentIdData.begin(), contentIdData.end()); + uint32_t kidCount = charParser.ReadNextUnsignedInt(); + while (kidCount > 0) + { + if (charParser.CharsLeft() < 16) + { + LOG::LogF(LOGERROR, "Cannot parse PSSH data, malformed data."); + return false; + } - // Create "protection_scheme" field, id: 9 - // wvPsshData.push_back(MakeProtobufTag(9, 0)); - // WriteProtobufVarint(wvPsshData, static_cast(WIDEVINE_PROT_SCHEME::CENC)); + std::vector kid; + if (charParser.ReadNextArray(16, kid)) + m_keyIds.emplace_back(kid); - return true; -} + kidCount--; + } + } -bool DRM::MakePssh(const uint8_t* systemId, - const std::vector& initData, - std::vector& psshData) -{ - if (!systemId) + // Get init data + if (charParser.CharsLeft() < 4) + { + LOG::LogF(LOGERROR, "Cannot parse PSSH data, malformed data."); return false; + } + const uint32_t dataSize = charParser.ReadNextUnsignedInt(); + if (!charParser.ReadNextArray(dataSize, m_initData)) + { + LOG::LogF(LOGERROR, "Cannot parse PSSH data, malformed data."); + return false; + } - psshData.clear(); - psshData.resize(4, 0); // Size field 4 bytes (updated later) - psshData.insert(psshData.end(), PSSHBOX_HEADER_PSSH, PSSHBOX_HEADER_PSSH + 4); - psshData.insert(psshData.end(), PSSHBOX_HEADER_VER0, PSSHBOX_HEADER_VER0 + 4); - psshData.insert(psshData.end(), systemId, systemId + 16); - - // Add init data size (4 bytes) - const uint32_t initDataSize = static_cast(initData.size()); - psshData.emplace_back(static_cast((initDataSize >> 24) & 0xFF)); - psshData.emplace_back(static_cast((initDataSize >> 16) & 0xFF)); - psshData.emplace_back(static_cast((initDataSize >> 8) & 0xFF)); - psshData.emplace_back(static_cast(initDataSize & 0xFF)); + // Parse init data, where needed - // Add init data - psshData.insert(psshData.end(), initData.begin(), initData.end()); + if (std::equal(m_systemId.cbegin(), m_systemId.cend(), ID_WIDEVINE)) + { + if (m_version == 0) + DRM::ParseWidevinePssh(m_initData, m_keyIds); + } + else if (m_version == 0 && std::equal(m_systemId.cbegin(), m_systemId.cend(), ID_PLAYREADY)) + { + DRM::PRHeaderParser hParser; + if (hParser.Parse(m_initData)) + { + if (m_version == 0) + m_keyIds.emplace_back(hParser.GetKID()); - // Update box size (first 4 bytes) - const uint32_t boxSize = static_cast(psshData.size()); - psshData[0] = static_cast((boxSize >> 24) & 0xFF); - psshData[1] = static_cast((boxSize >> 16) & 0xFF); - psshData[2] = static_cast((boxSize >> 8) & 0xFF); - psshData[3] = static_cast(boxSize & 0xFF); + m_licenseUrl = hParser.GetLicenseURL(); + } + } return true; } + +void DRM::PSSH::ResetData() +{ + m_version = 0; + m_flags = 0; + m_systemId.clear(); + m_keyIds.clear(); + m_initData.clear(); + m_licenseUrl.clear(); +} diff --git a/src/decrypters/Helpers.h b/src/decrypters/Helpers.h index 346eaa3b8..63d130fab 100644 --- a/src/decrypters/Helpers.h +++ b/src/decrypters/Helpers.h @@ -35,7 +35,8 @@ constexpr std::string_view URN_COMMON = "urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52 constexpr uint8_t ID_WIDEVINE[16] = {0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, 0xd5, 0x1d, 0x21, 0xed}; - +constexpr uint8_t ID_PLAYREADY[16] = {0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86, + 0xab, 0x92, 0xe6, 0x5b, 0xe0, 0x88, 0x5f, 0x95}; bool IsKeySystemSupported(std::string_view keySystem); @@ -67,42 +68,63 @@ std::vector ConvertKidStrToBytes(std::string_view kidStr); */ std::string ConvertKidBytesToUUID(std::vector kid); -std::vector ConvertKidToUUIDVec(const std::vector& kid); - -/*! - * \brief Convert a PlayReady KeyId of 16 bytes to a Widevine KeyId. - * \param kid The PlayReady KeyId - * \return The Widevine KeyId, otherwise empty if fails. - */ -std::vector ConvertPrKidtoWvKid(std::vector kid); - bool IsValidPsshHeader(const std::vector& pssh); -/*! - * \brief Generate a synthesized Widevine PSSH. - * (WidevinePsshData as google protobuf format - * https://github.com/devine-dl/pywidevine/blob/master/pywidevine/license_protocol.proto) - * \param kid The KeyId - * \param contentIdData Custom content for the "content_id" field as bytes - * Placeholders allowed: - * {KID} To inject the KID as bytes - * {UUID} To inject the KID as UUID string format - * \param wvPsshData[OUT] The generated Widevine PSSH - * \return True if has success, otherwise false. - */ -bool MakeWidevinePsshData(const std::vector& kid, - std::vector contentIdData, - std::vector& wvPsshData); - -/*! - * \brief Generate a PSSH box (version 0, no KID's). - * \param systemUuid The DRM System ID (expected 16 bytes) - * \param initData The init data e.g. WidevinePsshData - * \param psshData[OUT] The generated PSSH - * \return True if has success, otherwise false. - */ -bool MakePssh(const uint8_t* systemId, - const std::vector& initData, - std::vector& psshData); +class PSSH +{ +public: + /*! + * \brief Generate a PSSH box. + * https://w3c.github.io/encrypted-media/format-registry/initdata/cenc.html#common-system + * \param systemId The DRM System ID (expected 16 bytes) + * \param keyIds The key id's + * \param initData[OPT] The pssh data e.g. WidevinePsshData + * \param version[OPT] The pssh box version (0 or 1) + * \param flags[OPT] The pssh box flags + * \return The pssh if has success, otherwise empty. + */ + static std::vector Make(const uint8_t* systemId, + const std::vector>& keyIds, + const std::vector& initData = {}, + const uint8_t version = 0, + const uint32_t flags = 0); + + /*! + * \brief Generate a PSSH box for Widevine. + * \param keyIds The key id's + * \param initData[OPT] Custom init data for the "content_id" field of WidevinePsshData struct + * \param version[OPT] The pssh box version (0 or 1) + * \param flags[OPT] The pssh box flags + * \return The pssh if has success, otherwise empty. + */ + static std::vector MakeWidevine(const std::vector>& keyIds, + const std::vector& initData = {}, + const uint8_t version = 0, + const uint32_t flags = 0); + + /*! + * \brief Parse a PSSH, and the PSSH init data. + * \param data The PSSH + * \return True has success, otherwise false. + */ + bool Parse(const std::vector& data); + + uint8_t GetVersion() const { return m_version; } + uint32_t GetFlags() const { return m_flags; } + const std::vector& GetSystemId() const { return m_systemId; } + const std::vector>& GetKeyIds() const { return m_keyIds; } + const std::vector& GetInitData() const { return m_initData; } + std::string GetLicenseUrl() const { return m_licenseUrl; } + +private: + void ResetData(); + + uint8_t m_version{0}; + uint32_t m_flags{0}; + std::vector m_systemId; + std::vector> m_keyIds; + std::vector m_initData; + std::string m_licenseUrl; +}; }; // namespace DRM diff --git a/src/parser/CMakeLists.txt b/src/parser/CMakeLists.txt index 2b0abbcbd..da92ad153 100644 --- a/src/parser/CMakeLists.txt +++ b/src/parser/CMakeLists.txt @@ -3,7 +3,6 @@ set(SOURCES DASHTree.cpp HLSTree.cpp SmoothTree.cpp - PRProtectionParser.cpp ) set(HEADERS @@ -11,7 +10,6 @@ set(HEADERS DASHTree.h HLSTree.h SmoothTree.h - PRProtectionParser.h ) add_dir_sources(SOURCES HEADERS) diff --git a/src/parser/DASHTree.cpp b/src/parser/DASHTree.cpp index 18261c1e3..49b2f3a25 100644 --- a/src/parser/DASHTree.cpp +++ b/src/parser/DASHTree.cpp @@ -9,9 +9,9 @@ #include "DASHTree.h" #include "CompKodiProps.h" -#include "PRProtectionParser.h" #include "SrvBroker.h" #include "common/Period.h" +#include "decrypters/HelperPr.h" #include "decrypters/Helpers.h" #include "utils/Base64Utils.h" #include "utils/CurlUtils.h" @@ -1286,8 +1286,8 @@ void adaptive::CDashTree::ParseTagContentProtection( } else if (childName == "mspr:pro" || childName == "pro") { - PRProtectionParser parser; - if (parser.ParseHeader(node.child_value())) + DRM::PRHeaderParser parser; + if (parser.Parse(node.child_value())) protScheme.kid = STRING::ToHexadecimal(parser.GetKID()); } } diff --git a/src/parser/HLSTree.cpp b/src/parser/HLSTree.cpp index e9620061a..b3789da3a 100644 --- a/src/parser/HLSTree.cpp +++ b/src/parser/HLSTree.cpp @@ -9,9 +9,9 @@ #include "HLSTree.h" #include "CompKodiProps.h" -#include "PRProtectionParser.h" #include "SrvBroker.h" #include "aes_decrypter.h" +#include "decrypters/HelperPr.h" #include "decrypters/Helpers.h" #include "kodi/tools/StringUtils.h" #include "utils/Base64Utils.h" @@ -1264,7 +1264,7 @@ PLAYLIST::EncryptionType adaptive::CHLSTree::ProcessEncryption( // If there is no KID, try to get it from pssh data if (m_currentDefaultKID.empty()) { - CPsshParser parser; + DRM::PSSH parser; if (parser.Parse(m_currentPssh) && parser.GetKeyIds().size() > 0) m_currentDefaultKID = STRING::ToHexadecimal(parser.GetKeyIds()[0]); } @@ -1290,17 +1290,17 @@ PLAYLIST::EncryptionType adaptive::CHLSTree::ProcessEncryption( m_currentPssh = uriData; - PRProtectionParser parser; + DRM::PRHeaderParser parser; - if (parser.ParseHeader(m_currentPssh) && !parser.GetKID().empty()) + if (parser.Parse(m_currentPssh) && !parser.GetKID().empty()) { m_licenseUrl = parser.GetLicenseURL(); m_currentDefaultKID = STRING::ToHexadecimal(parser.GetKID()); auto encryptionType = parser.GetEncryption(); - if (encryptionType == PRProtectionParser::EncryptionType::AESCTR) + if (encryptionType == DRM::PRHeaderParser::EncryptionType::AESCTR) m_cryptoMode = CryptoMode::AES_CTR; - else if (encryptionType == PRProtectionParser::EncryptionType::AESCBC) + else if (encryptionType == DRM::PRHeaderParser::EncryptionType::AESCBC) m_cryptoMode = CryptoMode::AES_CBC; } else diff --git a/src/parser/PRProtectionParser.h b/src/parser/PRProtectionParser.h deleted file mode 100644 index 2ea72ca1f..000000000 --- a/src/parser/PRProtectionParser.h +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (C) 2023 Team Kodi - * This file is part of Kodi - https://kodi.tv - * - * SPDX-License-Identifier: GPL-2.0-or-later - * See LICENSES/README.md for more information. - */ - -#pragma once - -#include -#include - -#ifdef INPUTSTREAM_TEST_BUILD -#include "test/KodiStubs.h" -#else -#include -#endif - -namespace adaptive -{ -// \brief DRM PlayReady header protection parser -class ATTR_DLL_LOCAL PRProtectionParser -{ -public: - PRProtectionParser() {} - ~PRProtectionParser() {} - - enum class EncryptionType - { - UNKNOWN, - AESCTR, // cenc - AESCBC, // cbcs - }; - - /*! - * \brief Parse PlayReady header data. - * \param prHeader The PlayReady header data as base64 string - * \return True if parsed with success, otherwise false - */ - bool ParseHeader(std::string_view prHeaderBase64); - - bool ParseHeader(const std::vector& prHeader); - - /*! - * \brief Determines if there is PlayReady protection - * \return True if there is PlayReady protection, otherwise false - */ - bool HasProtection() const { return !m_PSSH.empty(); } - - /*! - * \brief Get keyid as 16 bytes format (converted for Widevine DRM) - */ - const std::vector& GetKID() const { return m_KID; } - EncryptionType GetEncryption() const { return m_encryption; } - std::string_view GetLicenseURL() const { return m_licenseURL; } - const std::vector& GetPSSH() const { return m_PSSH; } - -private: - std::vector m_KID; - EncryptionType m_encryption{EncryptionType::UNKNOWN}; - std::string m_licenseURL; - std::vector m_PSSH; -}; - -// \brief Parse PSSH data format (ref. https://w3c.github.io/encrypted-media/format-registry/initdata/cenc.html#common-system) -class ATTR_DLL_LOCAL CPsshParser -{ -public: - bool Parse(const std::vector& data); - - const std::vector& GetSystemId() const { return m_systemId; } - - /*! - * \brief Get keyid's as 16 bytes format - */ - const std::vector>& GetKeyIds() const { return m_keyIds; } - const std::vector& GetData() const { return m_data; } - -private: - const uint8_t m_boxTypePssh[4]{'p', 's', 's', 'h'}; - uint8_t m_version{0}; - uint32_t m_flags{0}; - std::vector m_systemId; - std::vector> m_keyIds; - std::vector m_data; -}; - -} // namespace adaptive diff --git a/src/parser/SmoothTree.cpp b/src/parser/SmoothTree.cpp index 25abe0d34..98fe3362c 100644 --- a/src/parser/SmoothTree.cpp +++ b/src/parser/SmoothTree.cpp @@ -8,12 +8,12 @@ #include "SmoothTree.h" +#include "decrypters/HelperPr.h" #include "utils/StringUtils.h" #include "utils/UrlUtils.h" #include "utils/Utils.h" #include "utils/XMLUtils.h" #include "utils/log.h" -#include "PRProtectionParser.h" #include "pugixml.hpp" using namespace adaptive; @@ -89,7 +89,7 @@ bool adaptive::CSmoothTree::ParseManifest(const std::string& data) m_totalTime = period->GetDuration() * 1000 / period->GetTimescale(); // Parse tag - PRProtectionParser protParser; + DRM::PRHeaderParser protParser; xml_node nodeProt = nodeSSM.child("Protection"); if (nodeProt) { @@ -103,7 +103,7 @@ bool adaptive::CSmoothTree::ParseManifest(const std::string& data) if (STRING::Contains(XML::GetAttrib(nodeProtHead, "SystemID"), "9A04F079-9840-4286-AB92-E65BE0885F95")) { - if (protParser.ParseHeader(nodeProtHead.child_value())) + if (protParser.Parse(nodeProtHead.child_value())) { period->SetEncryptionState(EncryptionState::ENCRYPTED_DRM); m_licenseUrl = protParser.GetLicenseURL(); @@ -133,7 +133,7 @@ bool adaptive::CSmoothTree::ParseManifest(const std::string& data) void adaptive::CSmoothTree::ParseTagStreamIndex(pugi::xml_node nodeSI, PLAYLIST::CPeriod* period, - const PRProtectionParser& protParser) + const DRM::PRHeaderParser& protParser) { std::unique_ptr adpSet = CAdaptationSet::MakeUniquePtr(period); @@ -189,7 +189,7 @@ void adaptive::CSmoothTree::ParseTagStreamIndex(pugi::xml_node nodeSI, if (protParser.HasProtection() && (adpSet->GetStreamType() == StreamType::VIDEO || adpSet->GetStreamType() == StreamType::AUDIO)) { - psshSetPos = InsertPsshSet(StreamType::VIDEO_AUDIO, period, adpSet.get(), protParser.GetPSSH(), + psshSetPos = InsertPsshSet(StreamType::VIDEO_AUDIO, period, adpSet.get(), protParser.GetInitData(), STRING::ToHexadecimal(protParser.GetKID())); } diff --git a/src/parser/SmoothTree.h b/src/parser/SmoothTree.h index 9bfdc1b91..3dfeb1e31 100644 --- a/src/parser/SmoothTree.h +++ b/src/parser/SmoothTree.h @@ -15,10 +15,13 @@ namespace pugi class xml_node; } +namespace DRM +{ +class PRHeaderParser; +} + namespace adaptive { -// Forward -class PRProtectionParser; class ATTR_DLL_LOCAL CSmoothTree : public AdaptiveTree { @@ -45,7 +48,7 @@ class ATTR_DLL_LOCAL CSmoothTree : public AdaptiveTree void ParseTagStreamIndex(pugi::xml_node nodeSI, PLAYLIST::CPeriod* period, - const PRProtectionParser& protParser); + const DRM::PRHeaderParser& protParser); void ParseTagQualityLevel(pugi::xml_node nodeQI, PLAYLIST::CAdaptationSet* adpSet, const uint32_t timescale, diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 693b4d084..b0d3180e0 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -12,10 +12,11 @@ add_executable(${BINARY} TestHelper.cpp TestUtils.cpp ../decrypters/Helpers.cpp + ../decrypters/HelperPr.cpp + ../decrypters/HelperWv.cpp ../parser/DASHTree.cpp ../parser/HLSTree.cpp ../parser/SmoothTree.cpp - ../parser/PRProtectionParser.cpp ../common/AdaptationSet.cpp ../common/AdaptiveStream.cpp ../common/AdaptiveTree.cpp From 5535a6bd6b705f472037b47d68ef122bc84e550e Mon Sep 17 00:00:00 2001 From: CastagnaIT Date: Thu, 29 Aug 2024 16:18:04 +0200 Subject: [PATCH 2/6] [Session] Cleanups and improvements to parse KID --- src/Session.cpp | 146 ++++++++++++++++++++++--------------- src/Session.h | 5 +- src/decrypters/Helpers.cpp | 14 ++++ src/decrypters/Helpers.h | 7 ++ src/parser/HLSTree.cpp | 8 -- 5 files changed, 110 insertions(+), 70 deletions(-) diff --git a/src/Session.cpp b/src/Session.cpp index f8fe78161..57df1e689 100644 --- a/src/Session.cpp +++ b/src/Session.cpp @@ -392,15 +392,15 @@ bool CSession::InitializeDRM(bool addDefaultKID /* = false */) if (session.m_cencSingleSampleDecrypter) continue; - std::vector initData; - std::string drmOptionalKeyParam; - - CPeriod::PSSHSet& sessionPsshset = m_adaptiveTree->m_currentPeriod->GetPSSHSets()[ses]; + const CPeriod::PSSHSet& sessionPsshset = m_adaptiveTree->m_currentPeriod->GetPSSHSets()[ses]; if (sessionPsshset.adaptation_set_->GetStreamType() == StreamType::NOTYPE) continue; - const std::vector defaultKid = DRM::ConvertKidStrToBytes(sessionPsshset.defaultKID_); + std::vector initData = sessionPsshset.pssh_; + std::string defaultKidStr = sessionPsshset.defaultKID_; + std::string drmOptionalKeyParam; + std::string_view licenseDataStr = CSrvBroker::GetKodiProps().GetLicenseData(); if (m_adaptiveTree->GetTreeType() == adaptive::TreeType::SMOOTH_STREAMING) @@ -422,7 +422,8 @@ bool CSession::InitializeDRM(bool addDefaultKID /* = false */) LOG::Log(LOGDEBUG, "License data: Create Widevine PSSH for SmoothStreaming %s", licenseData.empty() ? "" : "(with custom data)"); - initData = DRM::PSSH::MakeWidevine({defaultKid}, licenseData); + initData = + DRM::PSSH::MakeWidevine({DRM::ConvertKidStrToBytes(defaultKidStr)}, licenseData); } } else if (licenseType == "com.microsoft.playready") @@ -449,22 +450,28 @@ bool CSession::InitializeDRM(bool addDefaultKID /* = false */) initData = BASE64::Decode(licenseDataStr); } - if (initData.empty() && sessionPsshset.m_licenseUrl.empty()) + // If no KID, but init data, extract the KID from init data + if (!initData.empty() && defaultKidStr.empty()) { - if (!sessionPsshset.pssh_.empty()) + DRM::PSSH parser; + if (parser.Parse(initData) && !parser.GetKeyIds().empty()) { - // Use the init data provided by manifest (e.g. PSSH) - initData = sessionPsshset.pssh_; - } - else if (licenseType != DRM::KS_CLEARKEY) - { - // Try extract the PSSH/KID from the stream - // only if clearkeys are not used (use case e.g. Widevine manifest tested with ClearKey DRM) - if (!ExtractStreamProtectionData(sessionPsshset, initData, m_adaptiveTree->m_supportedKeySystems)) - LOG::Log(LOGERROR, "License data: Cannot extract PSSH/KID data from the stream"); + LOG::Log(LOGDEBUG, "Default KID parsed from init data"); + defaultKidStr = STRING::ToHexadecimal(parser.GetKeyIds()[0]); } } + //! @todo: as is implemented InitializeDRM will initialize all PSSHSet's also when are not used, + //! therefore ExtractStreamProtectionData can perform many (not needed) downloads of mp4 init files + if ((initData.empty() && licenseType != DRM::KS_CLEARKEY) || defaultKidStr.empty()) + { + // Try extract the PSSH/KID from the stream + ExtractStreamProtectionData(sessionPsshset, defaultKidStr, initData, + m_adaptiveTree->m_supportedKeySystems); + } + + const std::vector defaultKid = DRM::ConvertKidStrToBytes(defaultKidStr); + if (addDefaultKID && ses == 1 && session.m_cencSingleSampleDecrypter) { // If the CDM has been pre-initialized, on non-android systems @@ -476,7 +483,7 @@ bool CSession::InitializeDRM(bool addDefaultKID /* = false */) if (m_decrypter && !defaultKid.empty()) { - LOG::Log(LOGDEBUG, "Initializing stream with KID: %s", sessionPsshset.defaultKID_.c_str()); + LOG::Log(LOGDEBUG, "Initializing stream with KID: %s", defaultKidStr.c_str()); for (size_t i{1}; i < ses; ++i) { @@ -1455,13 +1462,18 @@ bool CSession::SeekChapter(int ch) return false; } -bool CSession::ExtractStreamProtectionData(PLAYLIST::CPeriod::PSSHSet& sessionPsshset, +void CSession::ExtractStreamProtectionData(const PLAYLIST::CPeriod::PSSHSet& psshSet, + std::string& defaultKid, std::vector& initData, - std::vector keySystems) + const std::vector& keySystems) { - auto initialRepr = m_reprChooser->GetRepresentation(sessionPsshset.adaptation_set_); + auto initialRepr = m_reprChooser->GetRepresentation(psshSet.adaptation_set_); + + if (initialRepr->GetContainerType() != ContainerType::MP4) + return; - CStream stream{m_adaptiveTree, sessionPsshset.adaptation_set_, initialRepr}; + LOG::LogF(LOGDEBUG, "Parse protection data from stream"); + CStream stream{m_adaptiveTree, psshSet.adaptation_set_, initialRepr}; stream.m_isEnabled = true; stream.m_adStream.start_stream(); @@ -1473,54 +1485,39 @@ bool CSession::ExtractStreamProtectionData(PLAYLIST::CPeriod::PSSHSet& sessionPs { LOG::LogF(LOGERROR, "No MOOV atom in stream"); stream.Disable(); - return false; + return; } - AP4_Array& pssh{movie->GetPsshAtoms()}; - for (std::string_view keySystem : keySystems) + AP4_Track* track = + movie->GetTrack(static_cast(stream.m_adStream.GetTrackType())); + + if (track) // Try extract the default KID from tenc / piff mp4 box { - std::vector systemIdBytes; - STRING::ToHexBytes(DRM::UrnToSystemId(keySystem), systemIdBytes); + AP4_ProtectedSampleDescription* protSampleDesc = + static_cast(track->GetSampleDescription(0)); - for (unsigned int i = 0; initData.size() == 0 && i < pssh.ItemCount(); i++) + if (protSampleDesc) { - if (std::memcmp(pssh[i].GetSystemId(), systemIdBytes.data(), 16) == 0) + AP4_ProtectionSchemeInfo* psi = protSampleDesc->GetSchemeInfo(); + if (psi) { - const AP4_DataBuffer& dataBuf = pssh[i].GetData(); - - initData.insert(initData.end(), dataBuf.GetData(), dataBuf.GetData() + dataBuf.GetDataSize()); - - if (sessionPsshset.defaultKID_.empty()) + AP4_ContainerAtom* schi = protSampleDesc->GetSchemeInfo()->GetSchiAtom(); + if (schi) { - if (pssh[i].GetKid(0)) + AP4_TencAtom* tenc = + AP4_DYNAMIC_CAST(AP4_TencAtom, schi->GetChild(AP4_ATOM_TYPE_TENC, 0)); + if (tenc) { - sessionPsshset.defaultKID_ = STRING::ToHexadecimal(pssh[i].GetKid(0), 16); + defaultKid = STRING::ToHexadecimal(tenc->GetDefaultKid(), 16); } - else if (AP4_Track* track = movie->GetTrack( - static_cast(stream.m_adStream.GetTrackType()))) + else { - AP4_ProtectedSampleDescription* m_protectedDesc = - static_cast(track->GetSampleDescription(0)); - AP4_ContainerAtom* schi; - if (m_protectedDesc->GetSchemeInfo() && - (schi = m_protectedDesc->GetSchemeInfo()->GetSchiAtom())) + AP4_PiffTrackEncryptionAtom* piff = + AP4_DYNAMIC_CAST(AP4_PiffTrackEncryptionAtom, + schi->GetChild(AP4_UUID_PIFF_TRACK_ENCRYPTION_ATOM, 0)); + if (piff) { - AP4_TencAtom* tenc{ - AP4_DYNAMIC_CAST(AP4_TencAtom, schi->GetChild(AP4_ATOM_TYPE_TENC, 0)) }; - if (tenc) - { - sessionPsshset.defaultKID_ = STRING::ToHexadecimal(tenc->GetDefaultKid(), 16); - } - else - { - AP4_PiffTrackEncryptionAtom* piff{ - AP4_DYNAMIC_CAST(AP4_PiffTrackEncryptionAtom, - schi->GetChild(AP4_UUID_PIFF_TRACK_ENCRYPTION_ATOM, 0)) }; - if (piff) - { - sessionPsshset.defaultKID_ = STRING::ToHexadecimal(piff->GetDefaultKid(), 16); - } - } + defaultKid = STRING::ToHexadecimal(piff->GetDefaultKid(), 16); } } } @@ -1528,6 +1525,35 @@ bool CSession::ExtractStreamProtectionData(PLAYLIST::CPeriod::PSSHSet& sessionPs } } + if (initData.empty() || defaultKid.empty()) + { + const std::vector systemIds = DRM::UrnsToSystemIds(keySystems); + AP4_Array& pssh{movie->GetPsshAtoms()}; + + for (unsigned int i = 0; i < pssh.ItemCount(); ++i) + { + AP4_PsshAtom& psshAtom = pssh[i]; + + std::string systemId = STRING::ToHexadecimal(psshAtom.GetSystemId(), 16); + + // Check if the system id is supported + if (std::find(systemIds.cbegin(), systemIds.cend(), systemId) != systemIds.cend()) + { + const AP4_DataBuffer& dataBuf = psshAtom.GetData(); + const std::vector psshData{dataBuf.GetData(), + dataBuf.GetData() + dataBuf.GetDataSize()}; + + initData = DRM::PSSH::Make(psshAtom.GetSystemId(), {}, psshData); + + if (psshAtom.GetKid(0)) + { + defaultKid = STRING::ToHexadecimal(pssh[i].GetKid(0), 16); + } + + break; + } + } + } + stream.Disable(); - return !initData.empty(); } diff --git a/src/Session.h b/src/Session.h index 3d06779d3..666ea23ea 100644 --- a/src/Session.h +++ b/src/Session.h @@ -334,9 +334,10 @@ class ATTR_DLL_LOCAL CSession : public adaptive::AdaptiveStreamObserver */ void DisposeDecrypter(); - bool ExtractStreamProtectionData(PLAYLIST::CPeriod::PSSHSet& sessionPsshset, + void ExtractStreamProtectionData(const PLAYLIST::CPeriod::PSSHSet& psshSet, + std::string& defaultKid, std::vector& initData, - std::vector keySystems); + const std::vector& keySystems); private: std::string m_manifestUrl; diff --git a/src/decrypters/Helpers.cpp b/src/decrypters/Helpers.cpp index 26ec1adfe..dd50ee015 100644 --- a/src/decrypters/Helpers.cpp +++ b/src/decrypters/Helpers.cpp @@ -80,6 +80,20 @@ std::string DRM::UrnToSystemId(std::string_view urn) return sysId; } +std::vector DRM::UrnsToSystemIds(const std::vector& urns) +{ + std::vector sids; + + for (std::string_view urn : urns) + { + std::string sid = DRM::UrnToSystemId(urn); + if (!sid.empty()) + sids.emplace_back(DRM::UrnToSystemId(urn)); + } + + return sids; +} + bool DRM::IsKeySystemSupported(std::string_view keySystem) { return keySystem == DRM::KS_NONE || keySystem == DRM::KS_WIDEVINE || diff --git a/src/decrypters/Helpers.h b/src/decrypters/Helpers.h index 63d130fab..8cb68f8ff 100644 --- a/src/decrypters/Helpers.h +++ b/src/decrypters/Helpers.h @@ -54,6 +54,13 @@ std::string GenerateUrlDomainHash(std::string_view url); */ std::string UrnToSystemId(std::string_view urn); +/*! + * \brief Convert a list of DRM URN's to System ID's. + * \param urn The URN + * \return The System ID's, failed conversions are not included. + */ +std::vector UrnsToSystemIds(const std::vector& urns); + /*! * \brief Convert a hexdecimal KeyId of 32 chars to 16 bytes. * \param kidStr The hexdecimal KeyId diff --git a/src/parser/HLSTree.cpp b/src/parser/HLSTree.cpp index b3789da3a..e57f8afc5 100644 --- a/src/parser/HLSTree.cpp +++ b/src/parser/HLSTree.cpp @@ -1261,14 +1261,6 @@ PLAYLIST::EncryptionType adaptive::CHLSTree::ProcessEncryption( LOG::LogF(LOGERROR, "Incorrect KEYID tag format"); } - // If there is no KID, try to get it from pssh data - if (m_currentDefaultKID.empty()) - { - DRM::PSSH parser; - if (parser.Parse(m_currentPssh) && parser.GetKeyIds().size() > 0) - m_currentDefaultKID = STRING::ToHexadecimal(parser.GetKeyIds()[0]); - } - if (encryptMethod == "SAMPLE-AES-CTR") m_cryptoMode = CryptoMode::AES_CTR; else if (encryptMethod == "SAMPLE-AES") From 84a3f08fdec3f7dae7a895d4479dfc83713ee597 Mon Sep 17 00:00:00 2001 From: CastagnaIT Date: Thu, 29 Aug 2024 16:38:18 +0200 Subject: [PATCH 3/6] [DashTree] Add missing playready init data --- src/parser/DASHTree.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/parser/DASHTree.cpp b/src/parser/DASHTree.cpp index 49b2f3a25..0870d0c84 100644 --- a/src/parser/DASHTree.cpp +++ b/src/parser/DASHTree.cpp @@ -1286,9 +1286,16 @@ void adaptive::CDashTree::ParseTagContentProtection( } else if (childName == "mspr:pro" || childName == "pro") { - DRM::PRHeaderParser parser; - if (parser.Parse(node.child_value())) - protScheme.kid = STRING::ToHexadecimal(parser.GetKID()); + if (protScheme.kid.empty() || protScheme.pssh.empty()) + { + DRM::PRHeaderParser parser; + if (parser.Parse(node.child_value())) + { + protScheme.kid = STRING::ToHexadecimal(parser.GetKID()); + protScheme.pssh = + BASE64::Encode(DRM::PSSH::Make(DRM::ID_PLAYREADY, {}, parser.GetInitData())); + } + } } } From 0f99abe8a255029de8b8fc7aeb30ee2750a6b8bb Mon Sep 17 00:00:00 2001 From: CastagnaIT Date: Thu, 29 Aug 2024 16:58:36 +0200 Subject: [PATCH 4/6] Ensure to return a standard pssh for playready To full remove in the widevine decrypter the init data conversion --- src/parser/HLSTree.cpp | 4 ++-- src/parser/SmoothTree.cpp | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/parser/HLSTree.cpp b/src/parser/HLSTree.cpp index e57f8afc5..fb6f9c87f 100644 --- a/src/parser/HLSTree.cpp +++ b/src/parser/HLSTree.cpp @@ -1280,11 +1280,11 @@ PLAYLIST::EncryptionType adaptive::CHLSTree::ProcessEncryption( return EncryptionType::NOT_SUPPORTED; } - m_currentPssh = uriData; + m_currentPssh = DRM::PSSH::Make(DRM::ID_PLAYREADY, {}, uriData); DRM::PRHeaderParser parser; - if (parser.Parse(m_currentPssh) && !parser.GetKID().empty()) + if (parser.Parse(uriData) && !parser.GetKID().empty()) { m_licenseUrl = parser.GetLicenseURL(); m_currentDefaultKID = STRING::ToHexadecimal(parser.GetKID()); diff --git a/src/parser/SmoothTree.cpp b/src/parser/SmoothTree.cpp index 98fe3362c..93fb18e7d 100644 --- a/src/parser/SmoothTree.cpp +++ b/src/parser/SmoothTree.cpp @@ -8,6 +8,7 @@ #include "SmoothTree.h" +#include "decrypters/Helpers.h" #include "decrypters/HelperPr.h" #include "utils/StringUtils.h" #include "utils/UrlUtils.h" @@ -189,7 +190,9 @@ void adaptive::CSmoothTree::ParseTagStreamIndex(pugi::xml_node nodeSI, if (protParser.HasProtection() && (adpSet->GetStreamType() == StreamType::VIDEO || adpSet->GetStreamType() == StreamType::AUDIO)) { - psshSetPos = InsertPsshSet(StreamType::VIDEO_AUDIO, period, adpSet.get(), protParser.GetInitData(), + const std::vector initData = DRM::PSSH::Make(DRM::ID_PLAYREADY, {}, protParser.GetInitData()); + + psshSetPos = InsertPsshSet(StreamType::VIDEO_AUDIO, period, adpSet.get(), initData, STRING::ToHexadecimal(protParser.GetKID())); } From 7fbb91047a014126dffb2c436bbaef724616bdf8 Mon Sep 17 00:00:00 2001 From: CastagnaIT Date: Thu, 29 Aug 2024 17:15:31 +0200 Subject: [PATCH 5/6] Removed init data conversion from widevine decrypters All code paths where PSSH is retrieved should be now all covered with appropriate conversions where needed --- .../widevine/WVCencSingleSampleDecrypter.cpp | 29 ------------------- .../WVCencSingleSampleDecrypter.cpp | 21 -------------- 2 files changed, 50 deletions(-) diff --git a/src/decrypters/widevine/WVCencSingleSampleDecrypter.cpp b/src/decrypters/widevine/WVCencSingleSampleDecrypter.cpp index 0a2432261..481e8349c 100644 --- a/src/decrypters/widevine/WVCencSingleSampleDecrypter.cpp +++ b/src/decrypters/widevine/WVCencSingleSampleDecrypter.cpp @@ -71,35 +71,6 @@ CWVCencSingleSampleDecrypter::CWVCencSingleSampleDecrypter(CWVCdmAdapter& drm, m_wvCdmAdapter.insertssd(this); - // No cenc init data with PSSH box format, create one - if (memcmp(pssh.data() + 4, "pssh", 4) != 0) - { - // This will request a new session and initializes session_id and message members in cdm_adapter. - // message will be used to create a license request in the step after CreateSession call. - // Initialization data is the widevine cdm pssh code in google proto style found in mpd schemeIdUri - - // PSSH box version 0 (no kid's) - static const uint8_t atomHeader[12] = {0x00, 0x00, 0x00, 0x00, 0x70, 0x73, - 0x73, 0x68, 0x00, 0x00, 0x00, 0x00}; - - static const uint8_t widevineSystemId[16] = {0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, 0x4a, 0xce, - 0xa3, 0xc8, 0x27, 0xdc, 0xd5, 0x1d, 0x21, 0xed}; - - std::vector psshAtom; - psshAtom.assign(atomHeader, atomHeader + 12); // PSSH Box header - psshAtom.insert(psshAtom.end(), widevineSystemId, widevineSystemId + 16); // System ID - // Add data size bytes - psshAtom.resize(30, 0); // 2 zero bytes - psshAtom.emplace_back(static_cast((pssh.size()) >> 8)); - psshAtom.emplace_back(static_cast(pssh.size())); - - psshAtom.insert(psshAtom.end(), pssh.begin(), pssh.end()); // Data - // Update box size - psshAtom[2] = static_cast(psshAtom.size() >> 8); - psshAtom[3] = static_cast(psshAtom.size()); - m_pssh = psshAtom; - } - if (CSrvBroker::GetSettings().IsDebugLicense()) { std::string debugFilePath = diff --git a/src/decrypters/widevineandroid/WVCencSingleSampleDecrypter.cpp b/src/decrypters/widevineandroid/WVCencSingleSampleDecrypter.cpp index 96945725b..184a1459e 100644 --- a/src/decrypters/widevineandroid/WVCencSingleSampleDecrypter.cpp +++ b/src/decrypters/widevineandroid/WVCencSingleSampleDecrypter.cpp @@ -50,27 +50,6 @@ CWVCencSingleSampleDecrypterA::CWVCencSingleSampleDecrypterA(CWVCdmAdapterA& drm } m_pssh = pssh; - // No cenc init data with PSSH box format, create one - if (memcmp(pssh.data() + 4, "pssh", 4) != 0) - { - // PSSH box version 0 (no kid's) - static const uint8_t atomHeader[12] = {0x00, 0x00, 0x00, 0x00, 0x70, 0x73, - 0x73, 0x68, 0x00, 0x00, 0x00, 0x00}; - - std::vector psshAtom; - psshAtom.assign(atomHeader, atomHeader + 12); // PSSH Box header - psshAtom.insert(psshAtom.end(), m_mediaDrm.GetKeySystem(), m_mediaDrm.GetKeySystem() + 16); // System ID - // Add data size bytes - psshAtom.resize(30, 0); // 2 zero bytes - psshAtom.emplace_back(static_cast((m_pssh.size()) >> 8)); - psshAtom.emplace_back(static_cast(m_pssh.size())); - - psshAtom.insert(psshAtom.end(), m_pssh.begin(), m_pssh.end()); // Data - // Update box size - psshAtom[2] = static_cast(psshAtom.size() >> 8); - psshAtom[3] = static_cast(psshAtom.size()); - m_pssh = psshAtom; - } if (CSrvBroker::GetSettings().IsDebugLicense()) { From 3b215ec3806ce8e71cf81e43163865c0296bb3f5 Mon Sep 17 00:00:00 2001 From: CastagnaIT Date: Fri, 30 Aug 2024 14:50:56 +0200 Subject: [PATCH 6/6] [HelperPr] add method to fix playready header Currently unused just to make tests since there are problems with some streams part of these fixes come from exoplayer --- src/decrypters/HelperPr.cpp | 170 +++++++++++++++++++++++++++++++++- src/decrypters/HelperPr.h | 8 ++ src/utils/CharArrayParser.cpp | 16 ++++ src/utils/CharArrayParser.h | 7 ++ 4 files changed, 200 insertions(+), 1 deletion(-) diff --git a/src/decrypters/HelperPr.cpp b/src/decrypters/HelperPr.cpp index 0b0efe4f3..0b8ef0a9d 100644 --- a/src/decrypters/HelperPr.cpp +++ b/src/decrypters/HelperPr.cpp @@ -23,6 +23,7 @@ using namespace UTILS; namespace { constexpr uint16_t PLAYREADY_WRM_TAG = 0x0001; +constexpr const char* PLAYREADY_MOCK_LA_URL = "https://www.mock.la.url"; // \brief Convert a PlayReady KID to Widevine KID format std::vector ConvertKidtoWv(std::vector kid) @@ -39,6 +40,76 @@ std::vector ConvertKidtoWv(std::vector kid) } return remapped; } + +void FixWRMHeader(std::string& xmlData) +{ + // Parse XML header data + xml_document doc; + xml_parse_result parseRes = doc.load_buffer(xmlData.c_str(), xmlData.size()); + if (parseRes.status != status_ok) + { + LOG::LogF(LOGERROR, "Failed to parse the Playready header, error code: %i", parseRes.status); + return; + } + + xml_node nodeWRM = doc.child("WRMHEADER"); + if (!nodeWRM) + { + LOG::LogF(LOGERROR, " node not found."); + return; + } + + std::string_view ver = XML::GetAttrib(nodeWRM, "version"); + + xml_node nodeDATA = nodeWRM.child("DATA"); + if (!nodeDATA) + { + LOG::LogF(LOGERROR, " node not found."); + return; + } + + // On version 4.0.0.0 CHECKSUM tag is mandatory for ALGID: AESCTR and COCKTAIL + // since we cannot generate the checksum value convert to header v4.1.0.0 + if (STRING::StartsWith(ver, "4.0") && !nodeDATA.child("CHECKSUM") && nodeDATA.child("KID") && + nodeDATA.child("PROTECTINFO")) + { + pugi::xml_attribute attrVer = nodeWRM.attribute("version"); + attrVer.set_value("4.1.0.0"); + + std::string kid = nodeDATA.child("KID").child_value(); + nodeDATA.remove_child("KID"); + + xml_node nodePROTECTINFO = nodeDATA.child("PROTECTINFO"); + + std::string algid = nodePROTECTINFO.child("ALGID").child_value(); + nodePROTECTINFO.remove_child("ALGID"); + nodePROTECTINFO.remove_child("KEYLEN"); + + // Create KID tag + pugi::xml_node newNodeKid = nodePROTECTINFO.append_child("KID"); + newNodeKid.append_attribute("ALGID") = algid.c_str(); + newNodeKid.append_attribute("VALUE") = kid.c_str(); + + LOG::Log(LOGDEBUG, "Converted PlayReady header to v4.1.0.0, due to missing CHECKSUM tag."); + } + + xml_node nodeLaUrl = nodeDATA.child("LA_URL"); + + if (!nodeLaUrl) + { + // Missing LA_URL add a mock value + pugi::xml_node newNodeLaUrl = nodeDATA.append_child("LA_URL"); + newNodeLaUrl.append_child(pugi::node_pcdata).set_value(PLAYREADY_MOCK_LA_URL); + LOG::Log(LOGDEBUG, "Fix missing LA_URL to PlayReady header."); + } + + std::ostringstream oss; + doc.save(oss, " ", + pugi::format_raw | pugi::format_no_declaration | pugi::format_no_empty_element_tags, + pugi::encoding_utf16_le); + xmlData = oss.str(); +} + } // unnamed namespace bool DRM::PRHeaderParser::Parse(std::string_view prHeaderBase64) @@ -208,7 +279,104 @@ bool DRM::PRHeaderParser::Parse(const std::vector& prHeader) } xml_node nodeLAURL = nodeDATA.child("LA_URL"); - m_licenseURL = nodeLAURL.child_value(); + if (nodeLAURL) + { + if (nodeLAURL.child_value() != PLAYREADY_MOCK_LA_URL) + m_licenseURL = nodeLAURL.child_value(); + } return true; } + +std::vector DRM::FixPrHeader(const std::vector& prHeader) +{ + if (prHeader.empty()) + return {}; + + std::vector newHdr; + + // Parse header object data + CCharArrayParser charParser; + charParser.Reset(prHeader.data(), prHeader.size()); + + if (charParser.CharsLeft() < 4) + { + LOG::LogF(LOGERROR, "Failed parse PlayReady object, no \"length\" field"); + return {}; + } + + // Size to be updated later + const uint32_t size = charParser.ReadNextLEUnsignedInt(); + uint32_t extraSize = 0; + newHdr.resize(4, 0); + + if (charParser.CharsLeft() < 2) + { + LOG::LogF(LOGERROR, "Failed parse PlayReady object, no number of object records"); + return {}; + } + + uint16_t numRecords = charParser.ReadLENextUnsignedShort(); + newHdr.emplace_back(static_cast(numRecords & 0xFF)); + newHdr.emplace_back(static_cast((numRecords >> 8) & 0xFF)); + + std::string xmlData; + + for (uint16_t i = 0; i < numRecords; i++) + { + if (charParser.CharsLeft() < 2) + { + LOG::LogF(LOGERROR, "Failed parse PlayReady object record %u, cannot read record type", i); + return {}; + } + uint16_t recordType = charParser.ReadLENextUnsignedShort(); + newHdr.emplace_back(static_cast(recordType & 0xFF)); + newHdr.emplace_back(static_cast((recordType >> 8) & 0xFF)); + + if (charParser.CharsLeft() < 2) + { + LOG::LogF(LOGERROR, "Failed parse PlayReady object record %u, cannot read record size", i); + return {}; + } + uint16_t recordSize = charParser.ReadLENextUnsignedShort(); + + if (charParser.CharsLeft() < recordSize) + { + LOG::LogF(LOGERROR, "Failed parse PlayReady object record %u, cannot read WRM header", i); + return {}; + } + if ((recordType & PLAYREADY_WRM_TAG) == PLAYREADY_WRM_TAG) + { + xmlData = charParser.ReadNextString(recordSize); + + extraSize = static_cast(xmlData.size()); + FixWRMHeader(xmlData); + extraSize = static_cast(xmlData.size()) - extraSize; + + // Add updated data size + newHdr.emplace_back(static_cast(xmlData.size() & 0xFF)); + newHdr.emplace_back(static_cast((xmlData.size() >> 8) & 0xFF)); + // Add updated data + newHdr.insert(newHdr.end(), xmlData.begin(), xmlData.end()); + } + else + { + // Add data size + newHdr.emplace_back(static_cast(recordSize & 0xFF)); + newHdr.emplace_back(static_cast((recordSize >> 8) & 0xFF)); + // Add data + newHdr.insert(newHdr.end(), charParser.GetDataPos(), charParser.GetDataPos() + recordSize); + + charParser.SkipChars(recordSize); + } + } + + // Update box size + const uint32_t newSize = size + extraSize; + newHdr[0] = static_cast(newSize & 0xFF); + newHdr[1] = static_cast((newSize >> 8) & 0xFF); + newHdr[2] = static_cast((newSize >> 16) & 0xFF); + newHdr[3] = static_cast((newSize >> 24) & 0xFF); + + return newHdr; +} diff --git a/src/decrypters/HelperPr.h b/src/decrypters/HelperPr.h index c990b9d2e..f1195dd59 100644 --- a/src/decrypters/HelperPr.h +++ b/src/decrypters/HelperPr.h @@ -57,4 +57,12 @@ class PRHeaderParser std::vector m_initData; }; +/*! + * \brief Fix the PlayReady header. + * If the LA_URL tag is missing, injects a mock LA_URL value to avoid causing the CDM to throw when creating the key request. + * The LA_URL attribute is optional but some Android PlayReady implementations are known to require it. + * Check if on v4.0.0.0 the CHECKSUM tag is missing, this is required, if so convert to v4.1.0.0 that dont need it. + */ +std::vector FixPrHeader(const std::vector& prHeader); + } // namespace DRM diff --git a/src/utils/CharArrayParser.cpp b/src/utils/CharArrayParser.cpp index c5ca0e8b5..b92e05fea 100644 --- a/src/utils/CharArrayParser.cpp +++ b/src/utils/CharArrayParser.cpp @@ -125,6 +125,22 @@ uint32_t UTILS::CCharArrayParser::ReadNextUnsignedInt() (static_cast(m_data[m_position - 1]) & 0xFF); } +uint32_t UTILS::CCharArrayParser::ReadNextLEUnsignedInt() +{ + if (!m_data) + { + LOG::LogF(LOGERROR, "{} - No data to read"); + return 0; + } + m_position += 4; + if (m_position > m_limit) + LOG::LogF(LOGERROR, "{} - Position out of range"); + return (static_cast(m_data[m_position - 4]) & 0xFF) | + (static_cast(m_data[m_position - 3]) & 0xFF) << 8 | + (static_cast(m_data[m_position - 2]) & 0xFF) << 16 | + (static_cast(m_data[m_position - 1]) & 0xFF) << 24; +} + uint64_t UTILS::CCharArrayParser::ReadNextUnsignedInt64() { if (!m_data) diff --git a/src/utils/CharArrayParser.h b/src/utils/CharArrayParser.h index 21eb05988..8ad649fc8 100644 --- a/src/utils/CharArrayParser.h +++ b/src/utils/CharArrayParser.h @@ -95,6 +95,13 @@ class CCharArrayParser */ uint32_t ReadNextUnsignedInt(); + /*! + * \brief Reads the next four chars little endian as unsigned short value (it is assumed + * that the caller has already checked the availability of the data for its length) + * \return The unsigned int value + */ + uint32_t ReadNextLEUnsignedInt(); + /*! * \brief Reads the next eight chars as unsigned int64 value (it is assumed * that the caller has already checked the availability of the data for its length)