diff --git a/src/CompKodiProps.cpp b/src/CompKodiProps.cpp index bb72a3487..0334b8b0a 100644 --- a/src/CompKodiProps.cpp +++ b/src/CompKodiProps.cpp @@ -72,6 +72,18 @@ ADP::KODI_PROPS::CCompKodiProps::CCompKodiProps(const std::map}, + * "license": dict, + * ... }, * "keysystem_name_2" : { ... }} */ rapidjson::Document jDoc; @@ -397,6 +410,9 @@ bool ADP::KODI_PROPS::CCompKodiProps::ParseDrmConfig(const std::string& data) continue; } + //! @todo: m_licenseType temporarily assigned, to remove with the DRM config rework + m_licenseType = keySystem; + DrmCfg& drmCfg = m_drmConfigs[keySystem]; auto& jDictVal = jChildObj.value; @@ -416,10 +432,13 @@ bool ADP::KODI_PROPS::CCompKodiProps::ParseDrmConfig(const std::string& data) for (auto const& keyid : jDictLic["keyids"].GetObject()) { if (keyid.name.IsString() && keyid.value.IsString()) - drmCfg.m_keys[keyid.name.GetString()] = (keyid.value.GetString()); + drmCfg.license.keys[keyid.name.GetString()] = (keyid.value.GetString()); } } } + + //! @todo: temporary support only one DRM config + break; } return true; @@ -460,15 +479,13 @@ bool ADP::KODI_PROPS::CCompKodiProps::ParseDrmLegacyConfig(const std::string& da } m_licenseType = keySystem; - - // Clear existing value to prevent possible mix with other similar properties - m_licenseKey.clear(); + std::string licenseUrl; if (!licenseStr.empty()) { if (URL::IsValidUrl(licenseStr)) // License server URL { - m_licenseKey = licenseStr; + licenseUrl = licenseStr; } else // Assume are keyid's for ClearKey DRM { @@ -484,17 +501,27 @@ bool ADP::KODI_PROPS::CCompKodiProps::ParseDrmLegacyConfig(const std::string& da LOG::LogF(LOGERROR, "Ignored malformed ClearKey kid/key pair"); continue; } - drmCfg.m_keys[STRING::Trim(keyPair[0])] = STRING::Trim(keyPair[1]); + drmCfg.license.keys[STRING::Trim(keyPair[0])] = STRING::Trim(keyPair[1]); } } } - //! @todo: temporary stored default DRM values here just for convenience - //! since we need to construct the "license key" string - //! these values are stored also on DRM's implementation, - //! they must be placed in an appropriate place with the future DRM config rework - if (licenseHeaders.empty()) + if (keySystem == DRM::KS_CLEARKEY) + { + DrmCfg& drmCfg = m_drmConfigs[keySystem]; + + drmCfg.license.serverUrl = licenseUrl; + ParseHeaderString(drmCfg.license.reqHeaders, licenseHeaders); + // Until the future DRM config rework only the ClearKey DRM use the new properties + // so return now to keep m_licenseKey empty + return true; + } + else if (licenseHeaders.empty()) { + //! @todo: temporary stored default DRM values here just for convenience + //! since we need to construct the "license key" string + //! these values are stored also on DRM's implementation, + //! they must be placed in an appropriate place with the future DRM config rework if (keySystem == DRM::KS_WIDEVINE) licenseHeaders = "Content-Type=application%2Foctet-stream"; else if (keySystem == DRM::KS_PLAYREADY) @@ -504,6 +531,6 @@ bool ADP::KODI_PROPS::CCompKodiProps::ParseDrmLegacyConfig(const std::string& da licenseHeaders = "Content-Type=application/json"; } - m_licenseKey += "|" + licenseHeaders + "|R{SSM}|R"; + m_licenseKey = licenseUrl + "|" + licenseHeaders + "|R{SSM}|R"; return true; } diff --git a/src/CompKodiProps.h b/src/CompKodiProps.h index 69f37ca46..e207cd029 100644 --- a/src/CompKodiProps.h +++ b/src/CompKodiProps.h @@ -70,7 +70,15 @@ struct ManifestConfig struct DrmCfg { - std::map m_keys; + struct License + { + std::string serverUrl; + std::map reqHeaders; + + std::map keys; // Clearkeys kid / key + }; + + License license; // The license configuration }; class ATTR_DLL_LOCAL CCompKodiProps diff --git a/src/Session.cpp b/src/Session.cpp index d0856bff4..8ff6d7d8f 100644 --- a/src/Session.cpp +++ b/src/Session.cpp @@ -454,9 +454,10 @@ bool CSession::InitializeDRM(bool addDefaultKID /* = false */) // Use the init data provided by manifest (e.g. PSSH) initData = sessionPsshset.pssh_; } - else + 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"); } diff --git a/src/decrypters/clearkey/ClearKeyCencSingleSampleDecrypter.cpp b/src/decrypters/clearkey/ClearKeyCencSingleSampleDecrypter.cpp index ee17c5d66..f3f96f87a 100644 --- a/src/decrypters/clearkey/ClearKeyCencSingleSampleDecrypter.cpp +++ b/src/decrypters/clearkey/ClearKeyCencSingleSampleDecrypter.cpp @@ -26,7 +26,10 @@ using namespace UTILS; CClearKeyCencSingleSampleDecrypter::CClearKeyCencSingleSampleDecrypter( - std::string_view licenseUrl, const std::vector& defaultKeyId, CClearKeyDecrypter* host) + std::string_view licenseUrl, + const std::map& licenseHeaders, + const std::vector& defaultKeyId, + CClearKeyDecrypter* host) : m_host(host) { if (licenseUrl.empty()) @@ -44,10 +47,10 @@ CClearKeyCencSingleSampleDecrypter::CClearKeyCencSingleSampleDecrypter( FILESYS::SaveFile(debugFilePath, postData.c_str(), true); } - CURL::CUrl curl{licenseUrl}; + CURL::CUrl curl{licenseUrl, postData}; curl.AddHeader("Accept", "application/json"); curl.AddHeader("Content-Type", "application/json"); - curl.AddHeader("postdata", UTILS::BASE64::Encode(postData)); + curl.AddHeaders(licenseHeaders); std::string response; int statusCode = curl.Open(); @@ -83,10 +86,10 @@ CClearKeyCencSingleSampleDecrypter::CClearKeyCencSingleSampleDecrypter( return; } - std::vector keyBytes = BASE64::Decode(m_keyPairs[b64DefaultKeyId]); + const std::vector keyBytes = BASE64::Decode(m_keyPairs[b64DefaultKeyId]); if (AP4_FAILED(AP4_CencSingleSampleDecrypter::Create(AP4_CENC_CIPHER_AES_128_CTR, keyBytes.data(), - 16, 0, 0, nullptr, false, - m_singleSampleDecrypter))) + static_cast(keyBytes.size()), 0, 0, + nullptr, false, m_singleSampleDecrypter))) { LOG::LogF(LOGERROR, "Failed to create AP4_CencSingleSampleDecrypter"); } @@ -117,9 +120,9 @@ CClearKeyCencSingleSampleDecrypter::CClearKeyCencSingleSampleDecrypter( LOG::LogF(LOGERROR, "Missing KeyId \"%s\" on DRM configuration", defaultKeyId.data()); } - const AP4_UI08* ap4Key = reinterpret_cast(hexKey.data()); - AP4_CencSingleSampleDecrypter::Create(AP4_CENC_CIPHER_AES_128_CTR, ap4Key, 16, 0, 0, nullptr, - false, m_singleSampleDecrypter); + AP4_CencSingleSampleDecrypter::Create(AP4_CENC_CIPHER_AES_128_CTR, hexKey.data(), + static_cast(hexKey.size()), 0, 0, nullptr, false, + m_singleSampleDecrypter); SetParentIsOwner(false); AddSessionKey(defaultKeyId); } @@ -173,8 +176,7 @@ std::string CClearKeyCencSingleSampleDecrypter::CreateLicenseRequest( * "type":"temporary" } */ - std::string b64Kid = UTILS::BASE64::Encode(defaultKeyId); - UTILS::STRING::ReplaceAll(b64Kid, "=", ""); + std::string b64Kid = UTILS::BASE64::Encode(defaultKeyId, false); rapidjson::Document jDoc; jDoc.SetObject(); diff --git a/src/decrypters/clearkey/ClearKeyCencSingleSampleDecrypter.h b/src/decrypters/clearkey/ClearKeyCencSingleSampleDecrypter.h index 724fae69b..f707859f3 100644 --- a/src/decrypters/clearkey/ClearKeyCencSingleSampleDecrypter.h +++ b/src/decrypters/clearkey/ClearKeyCencSingleSampleDecrypter.h @@ -17,6 +17,7 @@ class CClearKeyCencSingleSampleDecrypter : public Adaptive_CencSingleSampleDecry { public: CClearKeyCencSingleSampleDecrypter(std::string_view licenseUrl, + const std::map& licenseHeaders, const std::vector& defaultKeyId, CClearKeyDecrypter* host); CClearKeyCencSingleSampleDecrypter(const std::vector& initdata, diff --git a/src/decrypters/clearkey/ClearKeyDecrypter.cpp b/src/decrypters/clearkey/ClearKeyDecrypter.cpp index aea35c5b4..cceaf61cc 100644 --- a/src/decrypters/clearkey/ClearKeyDecrypter.cpp +++ b/src/decrypters/clearkey/ClearKeyDecrypter.cpp @@ -12,6 +12,7 @@ #include "CompKodiProps.h" #include "SrvBroker.h" #include "decrypters/Helpers.h" +#include "utils/log.h" std::vector CClearKeyDecrypter::SelectKeySystems(std::string_view keySystem) { @@ -39,16 +40,27 @@ Adaptive_CencSingleSampleDecrypter* CClearKeyDecrypter::CreateSingleSampleDecryp bool skipSessionMessage, CryptoMode cryptoMode) { + if (cryptoMode != CryptoMode::AES_CTR) + { + LOG::LogF(LOGERROR, "Cannot initialize ClearKey DRM. Only \"cenc\" encryption supported."); + return nullptr; + } + CClearKeyCencSingleSampleDecrypter* decrypter = nullptr; - auto& keys = CSrvBroker::GetKodiProps().GetDrmConfig(std::string(DRM::KS_CLEARKEY)).m_keys; + auto& cfgLic = CSrvBroker::GetKodiProps().GetDrmConfig(std::string(DRM::KS_CLEARKEY)).license; + + // If keys / license url are provided by Kodi property, those of the manifest will be overwritten + + if (!cfgLic.serverUrl.empty()) + licenseUrl = cfgLic.serverUrl; - if (!keys.empty() || !initData.empty()) // Keys provided from manifest or Kodi property + if ((!cfgLic.keys.empty() || !initData.empty()) && cfgLic.serverUrl.empty()) // Keys provided from manifest or Kodi property { - decrypter = new CClearKeyCencSingleSampleDecrypter(initData, defaultkeyid, keys, this); + decrypter = new CClearKeyCencSingleSampleDecrypter(initData, defaultkeyid, cfgLic.keys, this); } else // Clearkey license server URL provided { - decrypter = new CClearKeyCencSingleSampleDecrypter(licenseUrl, defaultkeyid, this); + decrypter = new CClearKeyCencSingleSampleDecrypter(licenseUrl, cfgLic.reqHeaders, defaultkeyid, this); } if (!decrypter->HasKeys()) diff --git a/src/decrypters/widevine/WVCencSingleSampleDecrypter.cpp b/src/decrypters/widevine/WVCencSingleSampleDecrypter.cpp index 7ea559640..51923e047 100644 --- a/src/decrypters/widevine/WVCencSingleSampleDecrypter.cpp +++ b/src/decrypters/widevine/WVCencSingleSampleDecrypter.cpp @@ -459,6 +459,7 @@ bool CWVCencSingleSampleDecrypter::SendSessionMessage() } std::string encData{BASE64::Encode(blocks[2])}; + //! @todo: inappropriate use of "postdata" header, use CURL::CUrl for post request file.AddHeader("postdata", encData.c_str()); } diff --git a/src/parser/DASHTree.cpp b/src/parser/DASHTree.cpp index a0cd6b022..efb2a47ed 100644 --- a/src/parser/DASHTree.cpp +++ b/src/parser/DASHTree.cpp @@ -12,6 +12,7 @@ #include "PRProtectionParser.h" #include "SrvBroker.h" #include "common/Period.h" +#include "decrypters/Helpers.h" #include "utils/Base64Utils.h" #include "utils/CurlUtils.h" #include "utils/StringUtils.h" @@ -1336,6 +1337,34 @@ bool adaptive::CDashTree::GetProtectionData( } } + // Workaround for ClearKey: + // if license type ClearKey is set and a manifest dont contains ClearKey protection scheme + // in any case the KID is required to allow decryption (with clear keys or license URLs provided by Kodi props) + //! @todo: this should not be a task of parser, moreover missing an appropriate KID extraction from mp4 box + auto& kodiProps = CSrvBroker::GetKodiProps(); + ProtectionScheme ckProtScheme; + if (!protSelected && !protCommon && kodiProps.GetLicenseType() == DRM::KS_CLEARKEY) + { + for (const ProtectionScheme& protScheme : reprProtSchemes) + { + if (!protScheme.kid.empty()) + { + ckProtScheme.kid = protScheme.kid; + break; + } + } + if (ckProtScheme.kid.empty()) + { + for (const ProtectionScheme& protScheme : adpProtSchemes) + { + ckProtScheme.kid = protScheme.kid; + break; + } + } + if (!ckProtScheme.kid.empty()) + protCommon = &ckProtScheme; + } + bool isEncrypted{false}; std::string selectedKid; std::string selectedPssh; diff --git a/src/parser/HLSTree.cpp b/src/parser/HLSTree.cpp index 978c8eda7..dee9a9c6a 100644 --- a/src/parser/HLSTree.cpp +++ b/src/parser/HLSTree.cpp @@ -1295,6 +1295,13 @@ PLAYLIST::EncryptionType adaptive::CHLSTree::ProcessEncryption( m_currentPssh = STRING::ToVecUint8(resp.data); } + if (uriUrl.empty()) // No kid provided, assume key == kid + m_currentDefaultKID = STRING::ToHexadecimal(uriData); + + } + else if (STRING::CompareNoCase(keyFormat, DRM::URN_WIDEVINE)) + { + // Take only the KID if (STRING::KeyExists(attribs, "KEYID")) { std::string keyid = attribs["KEYID"]; @@ -1305,16 +1312,14 @@ PLAYLIST::EncryptionType adaptive::CHLSTree::ProcessEncryption( else LOG::LogF(LOGERROR, "Incorret KEYID tag format"); } - else if (uriUrl.empty()) // No kid provided, assume key == kid - m_currentDefaultKID = STRING::ToHexadecimal(uriData); + } - if (encryptMethod == "SAMPLE-AES-CTR") - m_cryptoMode = CryptoMode::AES_CTR; - else if (encryptMethod == "SAMPLE-AES") - m_cryptoMode = CryptoMode::AES_CBC; + if (encryptMethod == "SAMPLE-AES-CTR") + m_cryptoMode = CryptoMode::AES_CTR; + else if (encryptMethod == "SAMPLE-AES") + m_cryptoMode = CryptoMode::AES_CBC; - return EncryptionType::CLEARKEY; - } + return EncryptionType::CLEARKEY; } // Unsupported encryption diff --git a/src/utils/Base64Utils.cpp b/src/utils/Base64Utils.cpp index 0842e41cc..ca0a2db0f 100644 --- a/src/utils/Base64Utils.cpp +++ b/src/utils/Base64Utils.cpp @@ -44,7 +44,10 @@ constexpr unsigned char BASE64_TABLE[] = { // clang-format on } // namespace -void UTILS::BASE64::Encode(const uint8_t* input, const size_t length, std::string& output) +void UTILS::BASE64::Encode(const uint8_t* input, + const size_t length, + std::string& output, + const bool padding /* = true */) { if (input == nullptr || length == 0) return; @@ -68,40 +71,45 @@ void UTILS::BASE64::Encode(const uint8_t* input, const size_t length, std::strin output.push_back(CHARACTERS[(l >> 0) & 0x3F]); } - int left = 3 - (length % 3); - - if (length % 3) + if (padding) { - for (int i = 0; i < left; i++) - output.push_back(PADDING); + const int left = 3 - (length % 3); + + if (length % 3) + { + for (int i = 0; i < left; ++i) + output.push_back(PADDING); + } } } -std::string UTILS::BASE64::Encode(const uint8_t* input, const size_t length) +std::string UTILS::BASE64::Encode(const uint8_t* input, + const size_t length, + const bool padding /* = true */) { std::string output; - Encode(input, length, output); + Encode(input, length, output, padding); return output; } -std::string UTILS::BASE64::Encode(const std::vector& input) +std::string UTILS::BASE64::Encode(const std::vector& input, const bool padding /* = true */) { std::string output; - Encode(input.data(), input.size(), output); + Encode(input.data(), input.size(), output, padding); return output; } -std::string UTILS::BASE64::Encode(const std::vector& input) +std::string UTILS::BASE64::Encode(const std::vector& input, const bool padding /* = true */) { std::string output; - Encode(reinterpret_cast(input.data()), input.size(), output); + Encode(reinterpret_cast(input.data()), input.size(), output, padding); return output; } -std::string UTILS::BASE64::Encode(const std::string& inputStr) +std::string UTILS::BASE64::Encode(const std::string& inputStr, const bool padding /* = true */) { std::string output; - Encode(reinterpret_cast(inputStr.data()), inputStr.size(), output); + Encode(reinterpret_cast(inputStr.data()), inputStr.size(), output, padding); return output; } diff --git a/src/utils/Base64Utils.h b/src/utils/Base64Utils.h index 1b6457d95..c4fc602f6 100644 --- a/src/utils/Base64Utils.h +++ b/src/utils/Base64Utils.h @@ -18,11 +18,11 @@ namespace UTILS namespace BASE64 { -void Encode(const uint8_t* input, const size_t length, std::string& output); -std::string Encode(const uint8_t* input, const size_t length); -std::string Encode(const std::vector& input); -std::string Encode(const std::vector& input); -std::string Encode(const std::string& input); +void Encode(const uint8_t* input, const size_t length, std::string& output, const bool padding = true); +std::string Encode(const uint8_t* input, const size_t length, const bool padding = true); +std::string Encode(const std::vector& input, const bool padding = true); +std::string Encode(const std::vector& input, const bool padding = true); +std::string Encode(const std::string& input, const bool padding = true); void Decode(const char* input, const size_t length, std::vector& output); std::vector Decode(std::string_view input); diff --git a/src/utils/CurlUtils.cpp b/src/utils/CurlUtils.cpp index 2809db300..1f747ea52 100644 --- a/src/utils/CurlUtils.cpp +++ b/src/utils/CurlUtils.cpp @@ -8,6 +8,7 @@ #include "CurlUtils.h" +#include "Base64Utils.h" #include "StringUtils.h" #include "UrlUtils.h" #include "Utils.h" @@ -201,6 +202,14 @@ UTILS::CURL::CUrl::CUrl(std::string_view url) } } +UTILS::CURL::CUrl::CUrl(std::string_view url, const std::string& postData) : CUrl::CUrl(url) +{ + if (m_file.IsOpen() && !postData.empty()) + { + m_file.CURLAddOption(ADDON_CURL_OPTION_PROTOCOL, "postdata", BASE64::Encode(postData)); + } +} + UTILS::CURL::CUrl::~CUrl() { if (CSrvBroker::GetKodiProps().GetConfig().internalCookies) diff --git a/src/utils/CurlUtils.h b/src/utils/CurlUtils.h index 788a79f33..22759bb1d 100644 --- a/src/utils/CurlUtils.h +++ b/src/utils/CurlUtils.h @@ -42,6 +42,13 @@ class ATTR_DLL_LOCAL CUrl * \param url The url of the file to download */ CUrl(std::string_view url); + + /*! + * \brief Create CUrl for POST request, if the data are empty, GET will be performed. + * \param url The request url + * \param postData The data for the POST request + */ + CUrl(std::string_view url, const std::string& postData); ~CUrl(); /*! diff --git a/src/utils/StringUtils.h b/src/utils/StringUtils.h index fbe4918fb..aac37c2db 100644 --- a/src/utils/StringUtils.h +++ b/src/utils/StringUtils.h @@ -27,6 +27,12 @@ bool KeyExists(const T& container, const Key& key) return container.find(key) != std::end(container); } +template +bool KeyExists(const T& container, const std::string_view key) +{ + return container.find(key.data()) != std::end(container); +} + /*! * \brief Get map value of the specified key * \param map The map where find the value