From 7abaa04dface306c2944039c7e808c318a60b104 Mon Sep 17 00:00:00 2001 From: CastagnaIT Date: Thu, 28 Dec 2023 10:36:52 +0100 Subject: [PATCH] wip --- inputstream.adaptive/addon.xml.in | 2 +- src/CompKodiProps.cpp | 431 ++++++++++++++++-- src/CompKodiProps.h | 46 ++ src/Session.cpp | 33 +- src/Session.h | 2 +- src/decrypters/DrmFactory.cpp | 19 + src/decrypters/DrmFactory.h | 6 + src/decrypters/Helpers.cpp | 234 ++++++++++ src/decrypters/Helpers.h | 22 + src/decrypters/IDecrypter.h | 26 +- .../widevine/WVCencSingleSampleDecrypter.cpp | 7 +- src/decrypters/widevine/WVDecrypter.cpp | 6 + src/decrypters/widevine/WVDecrypter.h | 2 + src/test/CMakeLists.txt | 5 +- src/utils/Base64Utils.cpp | 13 + src/utils/Base64Utils.h | 1 + src/utils/CMakeLists.txt | 2 + src/utils/JsonUtils.cpp | 29 ++ src/utils/JsonUtils.h | 29 ++ src/utils/StringUtils.h | 18 +- 20 files changed, 879 insertions(+), 54 deletions(-) create mode 100644 src/utils/JsonUtils.cpp create mode 100644 src/utils/JsonUtils.h diff --git a/inputstream.adaptive/addon.xml.in b/inputstream.adaptive/addon.xml.in index 38db5ea04..52420c835 100644 --- a/inputstream.adaptive/addon.xml.in +++ b/inputstream.adaptive/addon.xml.in @@ -10,7 +10,7 @@ name="adaptive" extension="" tags="true" - listitemprops="license_type|license_key|license_url|license_url_append|license_data|license_flags|manifest_type|server_certificate|manifest_update_parameter|manifest_upd_params|manifest_params|manifest_headers|stream_params|stream_headers|original_audio_language|play_timeshift_buffer|pre_init_data|stream_selection_type|chooser_bandwidth_max|chooser_resolution_max|chooser_resolution_secure_max|live_delay|internal_cookies" + listitemprops="drm|drm_license|license_type|license_key|license_url|license_url_append|license_data|license_flags|manifest_type|server_certificate|manifest_update_parameter|manifest_upd_params|manifest_params|manifest_headers|stream_params|stream_headers|original_audio_language|play_timeshift_buffer|pre_init_data|stream_selection_type|chooser_bandwidth_max|chooser_resolution_max|chooser_resolution_secure_max|live_delay|internal_cookies" library_@PLATFORM@="@LIBRARY_FILENAME@"/> @PLATFORM@ diff --git a/src/CompKodiProps.cpp b/src/CompKodiProps.cpp index c140f6cbe..211d15c88 100644 --- a/src/CompKodiProps.cpp +++ b/src/CompKodiProps.cpp @@ -7,20 +7,22 @@ */ #include "CompKodiProps.h" + #include "CompSettings.h" +#include "decrypters/Helpers.h" #include "utils/StringUtils.h" #include "utils/Utils.h" #include "utils/log.h" -#include +#include using namespace UTILS; namespace { // clang-format off -constexpr std::string_view PROP_LICENSE_TYPE = "inputstream.adaptive.license_type"; -constexpr std::string_view PROP_LICENSE_KEY = "inputstream.adaptive.license_key"; +constexpr std::string_view PROP_LICENSE_TYPE = "inputstream.adaptive.license_type"; //! @todo: deprecated, to be removed on next Kodi release +constexpr std::string_view PROP_LICENSE_KEY = "inputstream.adaptive.license_key"; //! @todo: deprecated, to be removed on next Kodi release // PROP_LICENSE_URL and PROP_LICENSE_URL_APPEND has been added as workaround for Kodi PVR API bug // where limit property values to max 1024 chars, if exceeds the string is truncated. // Since some services provide license urls that exceeds 1024 chars, @@ -30,9 +32,9 @@ constexpr std::string_view PROP_LICENSE_KEY = "inputstream.adaptive.license_key" // this problem should be fixed on Kodi 22 constexpr std::string_view PROP_LICENSE_URL = "inputstream.adaptive.license_url"; constexpr std::string_view PROP_LICENSE_URL_APPEND = "inputstream.adaptive.license_url_append"; -constexpr std::string_view PROP_LICENSE_DATA = "inputstream.adaptive.license_data"; -constexpr std::string_view PROP_LICENSE_FLAGS = "inputstream.adaptive.license_flags"; -constexpr std::string_view PROP_SERVER_CERT = "inputstream.adaptive.server_certificate"; +constexpr std::string_view PROP_LICENSE_DATA = "inputstream.adaptive.license_data"; //! @todo: deprecated, to be removed on next Kodi release +constexpr std::string_view PROP_LICENSE_FLAGS = "inputstream.adaptive.license_flags"; //! @todo: deprecated, to be removed on next Kodi release +constexpr std::string_view PROP_SERVER_CERT = "inputstream.adaptive.server_certificate"; //! @todo: deprecated, to be removed on next Kodi release constexpr std::string_view PROP_MANIFEST_TYPE = "inputstream.adaptive.manifest_type"; //! @todo: deprecated, to be removed on next Kodi release constexpr std::string_view PROP_MANIFEST_UPD_PARAM = "inputstream.adaptive.manifest_update_parameter"; //! @todo: deprecated, to be removed on next Kodi release @@ -46,7 +48,10 @@ constexpr std::string_view PROP_STREAM_HEADERS = "inputstream.adaptive.stream_he constexpr std::string_view PROP_AUDIO_LANG_ORIG = "inputstream.adaptive.original_audio_language"; constexpr std::string_view PROP_PLAY_TIMESHIFT_BUFFER = "inputstream.adaptive.play_timeshift_buffer"; constexpr std::string_view PROP_LIVE_DELAY = "inputstream.adaptive.live_delay"; -constexpr std::string_view PROP_PRE_INIT_DATA = "inputstream.adaptive.pre_init_data"; +constexpr std::string_view PROP_PRE_INIT_DATA = "inputstream.adaptive.pre_init_data"; //! @todo: deprecated, to be removed on next Kodi release + +constexpr std::string_view PROP_DRM = "inputstream.adaptive.drm"; +constexpr std::string_view PROP_DRM_LICENSE = "inputstream.adaptive.drm_license"; constexpr std::string_view PROP_INTERNAL_COOKIES = "inputstream.adaptive.internal_cookies"; @@ -56,26 +61,45 @@ constexpr std::string_view PROP_CHOOSER_BANDWIDTH_MAX = "inputstream.adaptive.ch constexpr std::string_view PROP_CHOOSER_RES_MAX = "inputstream.adaptive.chooser_resolution_max"; constexpr std::string_view PROP_CHOOSER_RES_SECURE_MAX = "inputstream.adaptive.chooser_resolution_secure_max"; // clang-format on + +void LogProp(std::string_view name, std::string_view value, bool isValueRedacted = false) +{ + LOG::Log(LOGDEBUG, "Property found \"%s\" value: %s", name.data(), + isValueRedacted ? "[redacted]" : value.data()); +} + +void LogDrmJsonDictKeys(const rapidjson::Value& dict, std::string_view keySystem) +{ + if (dict.IsObject()) + { + std::string keys; + for (auto it = dict.MemberBegin(); it != dict.MemberEnd(); ++it) + { + keys += it->name.GetString(); + keys += "; "; + } + LOG::Log(LOGDEBUG, "DRM config for key system: \"%s\", parameters: %s", keySystem.data(), + keys.c_str()); + } +} } // unnamed namespace ADP::KODI_PROPS::CCompKodiProps::CCompKodiProps(const std::map& props) { std::string licenseUrl; + bool isNewDrmPropsSet = + STRING::KeyExists(props, PROP_DRM) || STRING::KeyExists(props, PROP_DRM_LICENSE); + if (!isNewDrmPropsSet) + { + //! @todo: deprecated DRM properties, all them should be removed on next Kodi version. + ParseLegacyDrm(props); + } for (const auto& prop : props) { bool logPropValRedacted{false}; - if (prop.first == PROP_LICENSE_TYPE) - { - m_licenseType = prop.second; - } - else if (prop.first == PROP_LICENSE_KEY) - { - m_licenseKey = prop.second; - logPropValRedacted = true; - } - else if (prop.first == PROP_LICENSE_URL) + if (prop.first == PROP_LICENSE_URL) { // If PROP_LICENSE_URL_APPEND is parsed before this one, we need to append it licenseUrl = prop.second + licenseUrl; @@ -86,23 +110,6 @@ ADP::KODI_PROPS::CCompKodiProps::CCompKodiProps(const std::map& props) +{ + // Translate data from old ISA properties to the new DRM configuration, + // Proceed only if "inputstream.adaptive.license_key" is set + if (!STRING::KeyExists(props, PROP_LICENSE_KEY)) + return; + + LOG::Log(LOGWARNING, + "<<< PROPERTIES DEPRECATION NOTICE >>>\n" + "DEPRECATED PROPERTIES HAS BEEN USED TO SET THE DRM CONFIGURATION.\n" + "THE FOLLOWING PROPERTIES WILL BE REMOVED STARTING FROM KODI 22:\n" + "- inputstream.adaptive.license_type\n" + "- inputstream.adaptive.license_key\n" + "- inputstream.adaptive.license_data\n" + "- inputstream.adaptive.license_flags\n" + "- inputstream.adaptive.server_certificate\n" + "- inputstream.adaptive.pre_init_data\n" + "TO AVOID PLAYBACK FAILURES, YOU SHOULD INCLUDE THE NEW PROPERTIES AS SOON AS POSSIBLE:\n" + "- inputstream.adaptive.drm\n" + "- inputstream.adaptive.drm_license\n" + "FOR MORE INFO, PLEASE READ INPUTSTREAM ADAPTIVE WIKI ON GITHUB."); + + std::string drmKeySystem = props.at(PROP_LICENSE_TYPE.data()); + LogProp(PROP_LICENSE_TYPE, drmKeySystem); + + if (drmKeySystem.empty()) + drmKeySystem = DRM::KS_NONE; + + if (!DRM::IsKeySystemSupported(drmKeySystem)) + { + LOG::LogF(LOGERROR, + "Cannot parse DRM configuration, unknown key system \"%s\" on license_type property", + drmKeySystem.c_str()); + return; + } + + // Create a DRM configuration for the specified key system + DrmCfg& drmCfg = m_drmConfigs[drmKeySystem]; + + drmCfg.m_priority = 1; + std::string propValue; + + // Parse DRM properties + + if (STRING::GetMapValue(props, std::string(PROP_LICENSE_FLAGS), propValue)) + { + if (propValue.find("persistent_storage") != std::string::npos) + drmCfg.m_isPersistentStorage = true; + if (propValue.find("force_secure_decoder") != std::string::npos) + drmCfg.m_isSecureDecoderForced = true; + + LogProp(PROP_LICENSE_FLAGS, propValue); + } + + if (STRING::GetMapValue(props, std::string(PROP_LICENSE_DATA), propValue)) + { + drmCfg.m_streamsPsshData = propValue; + LogProp(PROP_LICENSE_DATA, propValue, true); + } + + if (STRING::GetMapValue(props, std::string(PROP_PRE_INIT_DATA), propValue)) + { + drmCfg.m_preInitData = propValue; + LogProp(PROP_PRE_INIT_DATA, propValue, true); + } + + // Parse DRM license properties + + if (STRING::GetMapValue(props, std::string(PROP_SERVER_CERT), propValue)) + { + drmCfg.m_license.m_serverCertificate = propValue; + LogProp(PROP_SERVER_CERT, propValue, true); + } + + if (STRING::GetMapValue(props, std::string(PROP_LICENSE_KEY), propValue)) + { + std::vector fields = STRING::SplitToVec(propValue, '|'); + size_t fieldCount = fields.size(); + + if (drmKeySystem == DRM::KS_NONE) + { + // We assume its HLS AES-128 encrypted case + // where "inputstream.adaptive.license_key" have different fields + + // Field 1: HTTP request params to append to key URL + if (fieldCount >= 1) + drmCfg.m_license.m_reqParams = fields[0]; + + // Field 2: HTTP request headers + if (fieldCount >= 2) + ParseHeaderString(drmCfg.m_license.m_reqHeaders, fields[1]); + } + else + { + // Field 1: License server url + if (fieldCount >= 1) + drmCfg.m_license.m_serverUrl = fields[0]; + + // Field 2: HTTP request headers + if (fieldCount >= 2) + ParseHeaderString(drmCfg.m_license.m_reqHeaders, fields[1]); + + // Field 3: HTTP request data (POST request) + if (fieldCount >= 3) + drmCfg.m_license.m_reqData = fields[2]; + + // Field 4: HTTP response data (license wrappers) + if (fieldCount >= 4) + { + bool isJsonWrapper{false}; + std::string jsonWrapperCfg; + std::string_view wrapperPrefix = fields[3]; + + if (wrapperPrefix.empty() || wrapperPrefix == "R") + { + // Raw, no wrapper, no-op + } + else if (wrapperPrefix == "B") + { + drmCfg.m_license.m_wrapper = "base64"; + } + else if (STRING::StartsWith(wrapperPrefix, "BJ")) + { + isJsonWrapper = true; + drmCfg.m_license.m_wrapper = "base64+json"; + jsonWrapperCfg = wrapperPrefix.substr(2); + } + else if (STRING::StartsWith(wrapperPrefix, "JB")) + { + isJsonWrapper = true; + drmCfg.m_license.m_wrapper = "json+base64"; + jsonWrapperCfg = wrapperPrefix.substr(2); + } + else if (STRING::StartsWith(wrapperPrefix, "J")) + { + isJsonWrapper = true; + drmCfg.m_license.m_wrapper = "json"; + jsonWrapperCfg = wrapperPrefix.substr(1); + } + else if (STRING::StartsWith(wrapperPrefix, "HB")) + { + // HB has been removed, we have no info about this use case + // if someone will open an issue we can try get more info for the reimplementation + //! @todo: if no feedbacks in future this can be removed, see also todo on decrypters/Helpers.cpp + LOG::Log(LOGERROR, "The support for \"HB\" parameter in the \"Response data\" field of " + "license_key property has been removed. If this is a requirement for " + "your video service, let us know by opening an issue on GitHub."); + } + else + { + LOG::Log(LOGERROR, + "Unknown \"%s\" parameter in the \"response data\" field of license_key property", + wrapperPrefix.data()); + } + + // Parse JSON configuration + if (isJsonWrapper) + { + if (jsonWrapperCfg.empty()) + { + LOG::Log(LOGERROR, "Missing JSON dict key names in the \"response data\" field of " + "license_key property"); + } + else + { + drmCfg.m_license.m_isJsonPathTraverse = true; + // Expected format as "KeyNameForData;KeyNameForHDCP" with exact order + std::vector jPaths = STRING::SplitToVec(jsonWrapperCfg, ';'); + // Position 1: The dict Key name to get license data + if (jPaths.size() >= 1) + drmCfg.m_license.m_wrapperParams.emplace("path_data", jPaths[0]); + + // Position 2: The dict Key name to get HDCP value (optional) + if (jPaths.size() >= 2) + drmCfg.m_license.m_wrapperParams.emplace("path_hdcp", jPaths[1]); + } + } + } + } + LogProp(PROP_LICENSE_KEY, propValue, true); + } +} + +bool ADP::KODI_PROPS::CCompKodiProps::ParseDrm(std::string data) +{ + /* Expected JSON structure: + * { "keysystem_name" : { "persistent_storage" : bool, + * "force_secure_decoder" : bool, + * "streams_pssh_data" : str, + * "pre_init_data" : str, + * "priority": int }, + * "keysystem_name_2" : { ... }} + */ + rapidjson::Document jDoc; + jDoc.Parse(data.c_str(), data.size()); + + if (!jDoc.IsObject()) + { + LOG::LogF(LOGERROR, "Malformed JSON data in to \"%s\" property", PROP_DRM.data()); + return false; + } + + // Iterate key systems dict + for (auto& jChildObj : jDoc.GetObject()) + { + const char* keySystem = jChildObj.name.GetString(); + + if (!DRM::IsKeySystemSupported(keySystem)) + { + LOG::LogF(LOGERROR, "Ignored unknown key system \"%s\" on DRM property", keySystem); + continue; + } + + DrmCfg& drmCfg = m_drmConfigs[keySystem]; + auto& jDictVal = jChildObj.value; + + if (!jDictVal.IsObject()) + { + LOG::LogF(LOGERROR, + "Cannot parse key system \"%s\" value on DRM property, wrong data type", + keySystem); + continue; + } + + if (jDictVal.HasMember("persistent_storage") && jDictVal["persistent_storage"].IsBool()) + drmCfg.m_isPersistentStorage = jDictVal["persistent_storage"].GetBool(); + + if (jDictVal.HasMember("force_secure_decoder") && jDictVal["force_secure_decoder"].IsBool()) + drmCfg.m_isPersistentStorage = jDictVal["force_secure_decoder"].GetBool(); + + if (jDictVal.HasMember("streams_pssh_data") && jDictVal["streams_pssh_data"].IsString()) + drmCfg.m_streamsPsshData = jDictVal["streams_pssh_data"].GetString(); + + if (jDictVal.HasMember("pre_init_data") && jDictVal["pre_init_data"].IsString()) + drmCfg.m_preInitData = jDictVal["pre_init_data"].GetString(); + + if (jDictVal.HasMember("priority") && jDictVal["priority"].IsInt()) + drmCfg.m_priority = jDictVal["priority"].GetInt(); + + LogDrmJsonDictKeys(jDictVal, keySystem); // todo: <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< comment this, maybe an appropriate config log elsewere + } + + return true; +} + +bool ADP::KODI_PROPS::CCompKodiProps::ParseDrmLicense(std::string data) +{ + /* Expected JSON structure: + * { "keysystem_name" : { "wrapper" : str, + * "wrapper_params" : dict, + * "server_certificate" : str, + * "server_url" : str, + * "req_headers" : str, + * "req_params" : str, + * "req_data" : str }, + * "keysystem_name_2" : { ... }} + */ + rapidjson::Document jDoc; + jDoc.Parse(data.c_str(), data.size()); + + if (!jDoc.IsObject()) + { + LOG::LogF(LOGERROR, "Malformed JSON data in to \"%s\" property", PROP_DRM_LICENSE.data()); + return false; + } + + // Iterate key system dict + for (auto& jChildObj : jDoc.GetObject()) + { + const char* keySystem = jChildObj.name.GetString(); + + if (!DRM::IsKeySystemSupported(keySystem)) + { + LOG::LogF(LOGERROR, "Ignored unknown key system \"%s\" on DRM license property", keySystem); + continue; + } + + DrmCfg& drmCfg = m_drmConfigs[keySystem]; + auto& jDictVal = jChildObj.value; + + if (!jDictVal.IsObject()) + { + LOG::LogF(LOGERROR, + "Cannot parse key system \"%s\" value on DRM license property, wrong data type", + keySystem); + continue; + } + + if (jDictVal.HasMember("wrapper") && jDictVal["wrapper"].IsString()) + { + drmCfg.m_license.m_wrapper = STRING::ToLower(jDictVal["wrapper"].GetString()); + } + + if (jDictVal.HasMember("wrapper_params") && jDictVal["wrapper_params"].IsObject()) + { + // Iterate wrapper_params dict + for (auto& cWrapParam : jDictVal["wrapper_params"].GetObject()) + { + if (cWrapParam.name.IsString() && cWrapParam.value.IsString()) + { + drmCfg.m_license.m_wrapperParams.emplace(cWrapParam.name.GetString(), cWrapParam.value.GetString()); + } + } + } + + if (jDictVal.HasMember("server_certificate") && jDictVal["server_certificate"].IsString()) + drmCfg.m_license.m_serverCertificate = jDictVal["server_certificate"].GetString(); + + if (jDictVal.HasMember("server_url") && jDictVal["server_url"].IsString()) + drmCfg.m_license.m_serverUrl = jDictVal["server_url"].GetString(); + + if (jDictVal.HasMember("req_headers") && jDictVal["req_headers"].IsString()) + ParseHeaderString(drmCfg.m_license.m_reqHeaders, jDictVal["req_headers"].GetString()); + + if (jDictVal.HasMember("req_params") && jDictVal["req_params"].IsString()) + drmCfg.m_license.m_reqParams = jDictVal["req_params"].GetString(); + + if (jDictVal.HasMember("req_data") && jDictVal["req_data"].IsString()) + drmCfg.m_license.m_reqData = jDictVal["req_data"].GetString(); + + LogDrmJsonDictKeys(jDictVal, keySystem); // todo: <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< comment this, maybe an appropriate config log elsewere + } + + return true; +} diff --git a/src/CompKodiProps.h b/src/CompKodiProps.h index 6156a53f3..942f80213 100644 --- a/src/CompKodiProps.h +++ b/src/CompKodiProps.h @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -42,6 +43,40 @@ struct ChooserProps std::pair m_resolutionSecureMax; // Res. limit for DRM protected videos (values 0 means auto) }; + +struct DrmCfg +{ + struct License + { + // Multiple wrappers e.g. "base64+json", the name order defines the order + // in which data will be unwrapped, (1) base64 --> (2) json + std::string m_wrapper; + std::map m_wrapperParams; + + //! @todo: To be removed at same time of deprecated DRM properties, this is an old hack used to traverse all + //! JSON objects to find a specific key name in a dict (the new implementation use absolute JSON paths) + bool m_isJsonPathTraverse{false}; // todo<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + + std::string m_serverCertificate; // Encoded as base64, same of inputstream.adaptive.server_certificate + + std::string m_serverUrl; + std::map m_reqHeaders; // todo: make it optional, if not set get headers from manifest <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + std::string m_reqParams; + std::string m_reqData; // Encoded as base64, if set will be executed an HTTP POST request with provided data + }; + + License m_license; // The license configuration + + bool m_isPersistentStorage{false}; // same of inputstream.adaptive.license_flags + bool m_isSecureDecoderForced{false}; // same of inputstream.adaptive.license_flags + + std::string m_streamsPsshData; // Encoded as base64, same of inputstream.adaptive.license_data + std::string m_preInitData; // Encoded as base64, same of + + // todo <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + std::optional m_priority; +}; + class ATTR_DLL_LOCAL CCompKodiProps { public: @@ -92,7 +127,16 @@ class ATTR_DLL_LOCAL CCompKodiProps // \brief Specifies the chooser properties that will override XML settings const ChooserProps& GetChooserProps() const { return m_chooserProps; } + // \brief Get DRM configuration for specified keysystem, if not found will return default values + const DrmCfg& GetDrmConfig(const std::string& keySystem) { return m_drmConfigs[keySystem]; } + + const std::map& GetDrmConfigs() const { return m_drmConfigs; } + private: + void ParseLegacyDrm(const std::map& props); + bool ParseDrm(std::string data); + bool ParseDrmLicense(std::string data); + std::string m_licenseType; std::string m_licenseKey; std::string m_licenseData; @@ -112,6 +156,8 @@ class ATTR_DLL_LOCAL CCompKodiProps std::string m_drmPreInitData; bool m_isInternalCookies{false}; ChooserProps m_chooserProps; + // DRM configurations by CDM key system + std::map m_drmConfigs; }; } // namespace KODI_PROPS diff --git a/src/Session.cpp b/src/Session.cpp index 70b8057e8..68ecf5c83 100644 --- a/src/Session.cpp +++ b/src/Session.cpp @@ -23,7 +23,9 @@ #include "utils/Utils.h" #include "utils/log.h" +#include #include +#include #include @@ -139,6 +141,27 @@ void CSession::DisposeDecrypter() bool CSession::Initialize() { const auto& kodiProps = CSrvBroker::GetKodiProps(); + // Pre-initialize the DRM is available only if the DRM configuration + // has set the priority to 1, this because the manifest is downloaded later + std::string drmPreInitData; + auto& drmCustomCfgs = kodiProps.GetDrmConfigs(); + for (auto& [ks, cfg] : kodiProps.GetDrmConfigs()) + { + if (cfg.m_priority == 1 && DRM::IsKeySystemDRMSupported(ks)) + { + drmPreInitData = cfg.m_preInitData; + break; + } + } + + /* + * todo: the following code should be moved into initialize period <<<----------------------------------------------------------------------------------------------------<<< + * but as first must be solved the supportedKeySystem var + * it is used only on DASH tree, to search the supported pssh + * at the time it was implemented in a reverse way, but the parser should not have this task + * instead the parser should only collect the encryption data + * and then do other things here or on another apposite section + * // Set the DRM configuration flags if (kodiProps.IsLicensePersistentStorage()) m_drmConfig |= DRM::IDecrypter::CONFIG_PERSISTENTSTORAGE; @@ -150,18 +173,18 @@ bool CSession::Initialize() SetSupportedDecrypterURN(supportedKeySystem); LOG::Log(LOGDEBUG, "Supported URN: %s", supportedKeySystem.c_str()); } - + */ std::map manifestHeaders = kodiProps.GetManifestHeaders(); bool isSessionOpened{false}; // Preinitialize the DRM, if pre-initialisation data are provided - if (!kodiProps.GetDrmPreInitData().empty()) + if (!drmPreInitData.empty()) { std::string challengeB64; std::string sessionId; // Pre-initialize the DRM allow to generate the challenge and session ID data // used to make licensed manifest requests (via proxy callback) - if (PreInitializeDRM(challengeB64, sessionId, isSessionOpened)) + if (PreInitializeDRM(drmPreInitData, challengeB64, sessionId, isSessionOpened)) { manifestHeaders["challengeB64"] = STRING::URLEncode(challengeB64); manifestHeaders["sessionId"] = sessionId; @@ -269,11 +292,11 @@ void CSession::CheckHDCP() } } -bool CSession::PreInitializeDRM(std::string& challengeB64, +bool CSession::PreInitializeDRM(std::string_view preInitData, + std::string& challengeB64, std::string& sessionId, bool& isSessionOpened) { - std::string_view preInitData = CSrvBroker::GetKodiProps().GetDrmPreInitData(); std::string_view psshData; std::string_view kidData; // Parse the PSSH/KID data diff --git a/src/Session.h b/src/Session.h index a2d3235d5..522b96258 100644 --- a/src/Session.h +++ b/src/Session.h @@ -47,7 +47,7 @@ class ATTR_DLL_LOCAL CSession : public adaptive::AdaptiveStreamObserver * \param isSessionOpened [OUT] Will be true if the DRM session has been opened * \return True if has success, false otherwise */ - bool PreInitializeDRM(std::string& challengeB64, std::string& sessionId, bool& isSessionOpened); + bool PreInitializeDRM(std::string_view preInitData, std::string& challengeB64, std::string& sessionId, bool& isSessionOpened); /*! \brief Initialize the DRM * \param addDefaultKID Set True to add the default KID to the first session diff --git a/src/decrypters/DrmFactory.cpp b/src/decrypters/DrmFactory.cpp index 7ee3b2388..7496a2b9e 100644 --- a/src/decrypters/DrmFactory.cpp +++ b/src/decrypters/DrmFactory.cpp @@ -8,6 +8,10 @@ #include "DrmFactory.h" +#include "SrvBroker.h" +#include "CompKodiProps.h" +#include "Helpers.h" + #include #if ANDROID #include "widevineandroid/WVDecrypter.h" @@ -42,3 +46,18 @@ IDecrypter* DRM::FACTORY::GetDecrypter(STREAM_CRYPTO_KEY_SYSTEM keySystem) return nullptr; } + +bool DRM::IsKeySystemDRMSupported(std::string_view ks) +{ +#if ANDROID + if (CWVDecrypterA::IsKeySystemSupported(ks)) + return true; +#else +// Darwin embedded are apple platforms different than MacOS (e.g. IOS) +#ifndef TARGET_DARWIN_EMBEDDED + if (CWVDecrypter::IsKeySystemSupported(ks)) + return true; +#endif +#endif + return false; +} diff --git a/src/decrypters/DrmFactory.h b/src/decrypters/DrmFactory.h index c5ce3d874..0f26a77ac 100644 --- a/src/decrypters/DrmFactory.h +++ b/src/decrypters/DrmFactory.h @@ -12,6 +12,12 @@ namespace DRM { +/*! + * \brief Test if there is a compatible DRM that support the specified key system. + * \return True if DRM supported, otherwise false. + */ +bool IsKeySystemDRMSupported(std::string_view ks); + namespace FACTORY { IDecrypter* GetDecrypter(STREAM_CRYPTO_KEY_SYSTEM keySystem); diff --git a/src/decrypters/Helpers.cpp b/src/decrypters/Helpers.cpp index 40d72954f..265ba9f9a 100644 --- a/src/decrypters/Helpers.cpp +++ b/src/decrypters/Helpers.cpp @@ -8,12 +8,246 @@ #include "Helpers.h" +#include "utils/Base64Utils.h" #include "utils/DigestMD5Utils.h" +#include "utils/JsonUtils.h" #include "utils/StringUtils.h" #include "utils/UrlUtils.h" +#include "utils/XMLUtils.h" +#include "utils/log.h" +#include + +using namespace pugi; using namespace UTILS; +namespace +{ + +// Supported wrappers +enum class Wrapper +{ + AUTO, // Try auto-detect wrappers + NONE, // Implicit for raw binary data + BASE64, + JSON, + XML +}; + +// \brief Translate a wrapper string in to relative vector of enum values. +// e.g. "json+base64" --> JSON, BASE64 +std::vector TranslateWrapper(std::string_view wrapper) +{ + const std::vector wrappers = STRING::SplitToVec(wrapper, '+'); + + if (wrappers.empty()) + return {}; + + std::vector result; + // Here we have to keep the order because + // defines the order in which data will be unwrapped + for (const std::string& wrapper : wrappers) + { + if (wrapper == "auto") + result.emplace_back(Wrapper::AUTO); + else if (wrapper == "none") + result.emplace_back(Wrapper::NONE); + else if (wrapper == "base64") + result.emplace_back(Wrapper::BASE64); + else if (wrapper == "json") + result.emplace_back(Wrapper::JSON); + else if (wrapper == "xml") + result.emplace_back(Wrapper::XML); + else + { + LOG::LogF(LOGERROR, "Cannot translate license wrapper, unknown type \"%s\"", wrapper.c_str()); + return {Wrapper::AUTO}; + } + } + return result; +} + +} // unnamed namespace + +bool DRM::IsKeySystemSupported(std::string_view keySystem) +{ + return keySystem == DRM::KS_NONE || keySystem == DRM::KS_WIDEVINE || + keySystem == DRM::KS_PLAYREADY || keySystem == DRM::KS_WISEPLAY; +} + +bool DRM::WvUnwrapLicense(std::string wrapper, + std::string contentType, + std::string data, + std::string& dataOut, + int& hdcpLimit) +{ + // By default the license response should be in binary data format + // but many services have a proprietary implementations therefore + // the license data could be wrapped (such as base64, json, etc...) + // here we provide the support for some common wrappers, + // as alternative an add-on must implement a proxy where it can request + // and process the license in a custom way and so return here the binary data + + std::vector wrappers = TranslateWrapper(wrapper); + + std::map params; + + bool isAuto = (wrappers.size() == 0 || wrappers.front() == Wrapper::AUTO) && + wrappers.front() != Wrapper::NONE; + + bool isAllowedFallbacks{false}; + + if (isAuto) + { + wrappers.clear(); + // Check mime types to try detect the wrapper + if (contentType == "application/octet-stream") + { + // its binary + } + else if (contentType == "application/json") + { + if (BASE64::IsBase64(data)) + wrappers.emplace_back(Wrapper::BASE64); + + wrappers.emplace_back(Wrapper::JSON); + } + else if (contentType == "application/xml" || contentType == "text/xml") + { + wrappers.emplace_back(Wrapper::XML); + } + else if (contentType == "text/plain") + { + // Some service use text mime type for XML + isAllowedFallbacks = true; + wrappers.emplace_back(Wrapper::XML); + } + else // Unknown + { + // Assumed to be binary with a possible base64 wrap + if (BASE64::IsBase64(data)) + wrappers.emplace_back(Wrapper::BASE64); + } + } + + // Process multiple wrappers with sequential order + + for (size_t i = 0; i < wrappers.size(); ++i) + { + const auto& wrapper = wrappers[i]; + + if (wrapper == Wrapper::NONE) + { + break; + } + else if (wrapper == Wrapper::BASE64) + { + data = BASE64::DecodeToStr(data); + } + else if (wrapper == Wrapper::JSON) + { + if (params["path_data"].empty()) + { + LOG::LogF(LOGERROR, + "Cannot parse JSON license data, \"path_data\" parameter not specified"); + return false; + } + + rapidjson::Document jDoc; + jDoc.Parse(data.c_str(), data.size()); + + if (!jDoc.IsObject()) + { + LOG::LogF(LOGERROR, + "Unable to parse license data as JSON format, malformed data or wrong wrapper"); + return false; + } + + rapidjson::Value* jDataObjValue = JSON::GetValueAtPath(jDoc, params["path_data"]); + + if (!jDataObjValue || !jDataObjValue->IsString()) + { + LOG::LogF(LOGERROR, "Unable to get license data from JSON path, possible wrong path on " + "\"path_data\" parameter"); + return false; + } + + data = jDataObjValue->GetString(); + + if (!params["path_hdcp"].empty()) + { + rapidjson::Value* jHdcpObjValue = JSON::GetValueAtPath(jDoc, params["path_hdcp"]); + + if (!jHdcpObjValue || !jHdcpObjValue->IsInt()) + { + LOG::LogF(LOGERROR, "Unable to parse JSON HDCP path, possible wrong path or data type on " + "\"path_hdcp\" parameter"); + } + else + hdcpLimit = jHdcpObjValue->GetInt(); + } + + if (isAuto && BASE64::IsBase64(data)) + wrappers.emplace_back(Wrapper::BASE64); + } + else if (wrapper == Wrapper::XML) + { + if (params["path_data"].empty()) + { + LOG::LogF(LOGERROR, "Cannot parse XML license data, \"path_data\" parameter not specified"); + return false; + } + + xml_document doc; + xml_parse_result parseRes = doc.load_buffer(data.c_str(), data.size()); + + if (parseRes.status != status_ok) + { + if (isAllowedFallbacks) + { + LOG::LogF(LOGDEBUG, "License data not in XML format, fallback to binary"); + wrappers.emplace_back(Wrapper::NONE); + continue; + } + else + { + LOG::LogF(LOGERROR, "Unable to parse XML license data, malformed data or wrong wrapper"); + return false; + } + } + + pugi::xml_node node = doc.select_node(params["path_data"].c_str()).node(); + if (!node) + { + LOG::LogF(LOGERROR, "Unable to get license data from XML path, possible wrong path on " + "\"path_data\" parameter"); + return false; + } + data = node.child_value(); + + if (isAuto && BASE64::IsBase64(data)) + wrappers.emplace_back(Wrapper::BASE64); + } + } + + if (data.empty()) + { + LOG::LogF(LOGERROR, "No license data, a problem occurred while processing license wrappers"); + return false; + } + + //! @todo: the support to binary license data (with HB) that start with "\r\n\r\n" has not been reintroduced with the + //! rework of this code, this is a very old unclear addition, seem there are no info about this on web, + //! and seem no addons use it, so for now is removed, if in the future someone complain about this lack + //! will be possible reintroduce it and include more clear info about this use case. + // if (data.compare(0, 4, "\r\n\r\n") == 0) + // data.erase(0, 4); + + dataOut = data; + + return true; +} + std::string DRM::GenerateUrlDomainHash(std::string_view url) { std::string baseDomain = URL::GetBaseDomain(url.data()); diff --git a/src/decrypters/Helpers.h b/src/decrypters/Helpers.h index b255de030..cd553ac5b 100644 --- a/src/decrypters/Helpers.h +++ b/src/decrypters/Helpers.h @@ -13,6 +13,28 @@ namespace DRM { +// CDM Key systems + +constexpr std::string_view KS_NONE = "none"; // No DRM but however encrypted (e.g. AES-128 on HLS) +constexpr std::string_view KS_WIDEVINE = "com.widevine.alpha"; +constexpr std::string_view KS_PLAYREADY = "com.microsoft.playready"; +constexpr std::string_view KS_WISEPLAY = "com.huawei.wiseplay"; + +// CDM UUIDs + +constexpr std::string_view UUID_WIDEVINE = "edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"; +constexpr std::string_view UUID_PLAYREADY = "9a04f079-9840-4286-ab92-e65be0885f95"; +constexpr std::string_view UUID_WISEPLAY = "3d5e6d35-9b9a-41e8-b843-dd3c6e72c42c"; +// constexpr std::string_view UUID_COMMON = "1077efec-c0b2-4d02-ace3-3c1e52e2fb4b"; + + +bool IsKeySystemSupported(std::string_view keySystem); + +bool WvUnwrapLicense(std::string wrapper, + std::string contentType, + std::string data, + std::string& dataOut, + int& hdcpLimit); /*! * \brief Generate an hash by using the base domain of an URL. diff --git a/src/decrypters/IDecrypter.h b/src/decrypters/IDecrypter.h index 986263e2b..59be4a0c2 100644 --- a/src/decrypters/IDecrypter.h +++ b/src/decrypters/IDecrypter.h @@ -43,7 +43,31 @@ struct DecrypterCapabilites uint16_t hdcpVersion{0}; //The HDCP version streams has to be restricted 0,10,20,21,22..... int hdcpLimit{0}; // If set (> 0) streams that are greater than the multiplication of "Width x Height" cannot be played. }; - +/* +struct DrmConfig +{ + struct License + { + enum class Wrapper + { + AUTO, // Try auto-detect wrappers + NONE, // Implicit for binary data + BASE64, + JSON, + XML + }; + + // Define each wrapper used to wrap the license data, + // on multiple wrappers e.g. BASE64+JSON the order in which the wrappers are added + // defines the order in which the data will be unwrapped + std::vector m_wrappers; + // Wrapper params + std::map m_wrapperParams; + }; + + License m_license; +}; +*/ class IDecrypter { public: diff --git a/src/decrypters/widevine/WVCencSingleSampleDecrypter.cpp b/src/decrypters/widevine/WVCencSingleSampleDecrypter.cpp index c2200d203..ab5d0191f 100644 --- a/src/decrypters/widevine/WVCencSingleSampleDecrypter.cpp +++ b/src/decrypters/widevine/WVCencSingleSampleDecrypter.cpp @@ -303,7 +303,9 @@ bool CWVCencSingleSampleDecrypter::SendSessionMessage() md5.Finalize(); blocks[0].replace(insPos, 6, md5.HexDigest()); } - + LOG::LogF(LOGERROR, "licenza url: %s", blocks[0].c_str()); + LOG::LogF(LOGERROR, "licenza key: %s", m_wvCdmAdapter.GetLicenseURL().c_str()); + CURL::CUrl file{blocks[0].c_str()}; file.AddHeader("Expect", ""); @@ -477,6 +479,7 @@ bool CWVCencSingleSampleDecrypter::SendSessionMessage() resLimit = file.GetResponseHeader("X-Limit-Video"); contentType = file.GetResponseHeader("Content-Type"); + LOG::LogF(LOGERROR, "contentType: %s", contentType.c_str()); if (!resLimit.empty()) { @@ -490,7 +493,7 @@ bool CWVCencSingleSampleDecrypter::SendSessionMessage() LOG::LogF(LOGERROR, "Could not read full SessionMessage response"); return false; } - + LOG::LogF(LOGERROR, "lic response: %s", response.c_str()); if (m_host->IsDebugSaveLicense()) { std::string debugFilePath = FILESYS::PathCombine( diff --git a/src/decrypters/widevine/WVDecrypter.cpp b/src/decrypters/widevine/WVDecrypter.cpp index 3ddbf3e2f..8e6727c5b 100644 --- a/src/decrypters/widevine/WVDecrypter.cpp +++ b/src/decrypters/widevine/WVDecrypter.cpp @@ -10,6 +10,7 @@ #include "WVCdmAdapter.h" #include "WVCencSingleSampleDecrypter.h" +#include "decrypters/Helpers.h" #include "utils/Base64Utils.h" #include "utils/FileUtils.h" #include "utils/StringUtils.h" @@ -231,3 +232,8 @@ void CWVDecrypter::ReleaseBuffer(void* instance, void* buffer) if (instance) static_cast(instance)->ReleaseFrameBuffer(buffer); } + +bool CWVDecrypter::IsKeySystemSupported(std::string_view ks) +{ + return ks == DRM::KS_WIDEVINE; +} diff --git a/src/decrypters/widevine/WVDecrypter.h b/src/decrypters/widevine/WVDecrypter.h index b158aff1a..f9816f3aa 100644 --- a/src/decrypters/widevine/WVDecrypter.h +++ b/src/decrypters/widevine/WVDecrypter.h @@ -65,6 +65,8 @@ class ATTR_DLL_LOCAL CWVDecrypter : public IDecrypter virtual const char* GetProfilePath() const override { return m_strProfilePath.c_str(); } virtual const bool IsDebugSaveLicense() const override { return m_isDebugSaveLicense; } + static bool IsKeySystemSupported(std::string_view ks); + private: CWVCdmAdapter* m_WVCdmAdapter; CWVCencSingleSampleDecrypter* m_decodingDecrypter; diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 09bb8e407..d8920f02b 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -35,6 +35,7 @@ add_executable(${BINARY} ../common/SegmentBase.cpp ../common/SegmentList.cpp ../common/SegTemplate.cpp + ../decrypters/Helpers.cpp ../oscompat.cpp ../SrvBroker.cpp ../CompSettings.cpp @@ -42,14 +43,16 @@ add_executable(${BINARY} ../utils/Base64Utils.cpp ../utils/CharArrayParser.cpp ../utils/CurlUtils.cpp + ../utils/DigestMD5Utils.cpp ../utils/FileUtils.cpp + ../utils/JsonUtils.cpp ../utils/StringUtils.cpp ../utils/UrlUtils.cpp ../utils/Utils.cpp ../utils/XMLUtils.cpp ) -target_link_libraries(${BINARY} PRIVATE ${BENTO4_LIBRARIES} ${PUGIXML_LIBRARIES} ${GTEST_LIBRARIES} Threads::Threads ${CMAKE_DL_LIBS}) +target_link_libraries(${BINARY} PRIVATE ${BENTO4_LIBRARIES} ${PUGIXML_LIBRARIES} ${RAPIDJSON_LIBRARIES} ${GTEST_LIBRARIES} Threads::Threads ${CMAKE_DL_LIBS}) set(TEST_DATA_DIR "${CMAKE_SOURCE_DIR}/src/test/manifests") add_test(NAME manifest_tests COMMAND ${BINARY} "${TEST_DATA_DIR}") diff --git a/src/utils/Base64Utils.cpp b/src/utils/Base64Utils.cpp index 10e70fd70..36e04b083 100644 --- a/src/utils/Base64Utils.cpp +++ b/src/utils/Base64Utils.cpp @@ -10,6 +10,8 @@ #include "log.h" +#include + using namespace UTILS::BASE64; namespace @@ -41,6 +43,17 @@ constexpr unsigned char BASE64_TABLE[] = { // clang-format on } // namespace +bool UTILS::BASE64::IsBase64(const std::string& str) +{ + // Check if the string length is a multiple of 4 + if (str.size() % 4 == 0) + { + static const std::regex base64Regex("^[A-Za-z0-9+/]*={0,2}$"); + return std::regex_match(str, base64Regex); + } + return false; +} + void UTILS::BASE64::Encode(const uint8_t* input, const size_t length, std::string& output) { if (input == nullptr || length == 0) diff --git a/src/utils/Base64Utils.h b/src/utils/Base64Utils.h index d1423a5e9..b2103b24d 100644 --- a/src/utils/Base64Utils.h +++ b/src/utils/Base64Utils.h @@ -17,6 +17,7 @@ namespace UTILS { namespace BASE64 { +bool IsBase64(const std::string& str); void Encode(const uint8_t* input, const size_t length, std::string& output); std::string Encode(const uint8_t* input, const size_t length); diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt index a77b80958..9b0d1abe9 100644 --- a/src/utils/CMakeLists.txt +++ b/src/utils/CMakeLists.txt @@ -4,6 +4,7 @@ set(SOURCES CurlUtils.cpp DigestMD5Utils.cpp FileUtils.cpp + JsonUtils.cpp StringUtils.cpp UrlUtils.cpp Utils.cpp @@ -17,6 +18,7 @@ set(HEADERS CurlUtils.h DigestMD5Utils.h FileUtils.h + JsonUtils.h log.h StringUtils.h UrlUtils.h diff --git a/src/utils/JsonUtils.cpp b/src/utils/JsonUtils.cpp new file mode 100644 index 000000000..567b849d8 --- /dev/null +++ b/src/utils/JsonUtils.cpp @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2022 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 "JsonUtils.h" + +rapidjson::Value* UTILS::JSON::GetValueAtPath(rapidjson::Value& node, const std::string& path) +{ + size_t pos = path.find('/'); + std::string current_level = path.substr(0, pos); + + if (node.IsObject() && node.HasMember(current_level.c_str())) + { + if (pos == std::string::npos) + { + return &node[current_level.c_str()]; + } + else + { + return GetValueAtPath(node[current_level.c_str()], path.substr(pos + 1)); + } + } + + return nullptr; +} diff --git a/src/utils/JsonUtils.h b/src/utils/JsonUtils.h new file mode 100644 index 000000000..e75435511 --- /dev/null +++ b/src/utils/JsonUtils.h @@ -0,0 +1,29 @@ +/* + * 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 + +namespace UTILS +{ +namespace JSON +{ + +/*! + * \brief Get value from a JSON path e.g. "a/b/c" + * \param node The json object where get the value + * \param path The path where the value is contained + * \return The json object if found, otherwise nullptr. + */ +rapidjson::Value* GetValueAtPath(rapidjson::Value& node, const std::string& path); + +} // namespace JSON +} // namespace UTILS diff --git a/src/utils/StringUtils.h b/src/utils/StringUtils.h index 7342de3cd..c3e0bae74 100644 --- a/src/utils/StringUtils.h +++ b/src/utils/StringUtils.h @@ -21,8 +21,22 @@ namespace STRING { // \brief Template function to check if a key exists in a container e.g. -template -bool KeyExists(const T& container, const Key& key) +template +bool KeyExists(const T& container, const std::string_view& key) +{ + return container.find(key.data()) != std::end(container); +} + +// \brief Template function to check if a key exists in a container e.g. +template +bool KeyExists(const T& container, const std::string& key) +{ + return container.find(key) != std::end(container); +} + +// \brief Template function to check if a key exists in a container e.g. +template +bool KeyExists(const T& container, const char* key) { return container.find(key) != std::end(container); }