Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[backport][DRM][ClearKey] Add license url by DRM config, CK as DRM replacement #1641

Merged
merged 11 commits into from
Aug 19, 2024
53 changes: 40 additions & 13 deletions src/CompKodiProps.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ ADP::KODI_PROPS::CCompKodiProps::CCompKodiProps(const std::map<std::string, std:
{
std::string licenseUrl;

if ((STRING::KeyExists(props, PROP_LICENSE_TYPE) || STRING::KeyExists(props, PROP_LICENSE_KEY)) &&
STRING::KeyExists(props, PROP_DRM_LEGACY))
{
LOG::Log(LOGERROR, "WRONG DRM CONFIGURATION. A mixed use of DRM properties are not supported.\n"
"Please fix your configuration by setting only one of these:\n"
" - Simple method: \"inputstream.adaptive.drm_legacy\"\n"
" - Advanced method: \"inputstream.adaptive.license_type\" with optional "
"\"inputstream.adaptive.license_key\"\n"
"For more details, see the Github Wiki Integration page.");
return;
}

for (const auto& prop : props)
{
bool logPropValRedacted{false};
Expand Down Expand Up @@ -374,7 +386,8 @@ bool ADP::KODI_PROPS::CCompKodiProps::ParseDrmConfig(const std::string& data)
* "streams_pssh_data" : str,
* "pre_init_data" : str,
* "priority": int,
* "keyids": list<dict>},
* "license": dict,
* ... },
* "keysystem_name_2" : { ... }}
*/
rapidjson::Document jDoc;
Expand All @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -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
{
Expand All @@ -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)
Expand All @@ -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;
}
10 changes: 9 additions & 1 deletion src/CompKodiProps.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,15 @@ struct ManifestConfig

struct DrmCfg
{
std::map<std::string, std::string> m_keys;
struct License
{
std::string serverUrl;
std::map<std::string, std::string> reqHeaders;

std::map<std::string, std::string> keys; // Clearkeys kid / key
};

License license; // The license configuration
};

class ATTR_DLL_LOCAL CCompKodiProps
Expand Down
3 changes: 2 additions & 1 deletion src/Session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down
24 changes: 13 additions & 11 deletions src/decrypters/clearkey/ClearKeyCencSingleSampleDecrypter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@
using namespace UTILS;

CClearKeyCencSingleSampleDecrypter::CClearKeyCencSingleSampleDecrypter(
std::string_view licenseUrl, const std::vector<uint8_t>& defaultKeyId, CClearKeyDecrypter* host)
std::string_view licenseUrl,
const std::map<std::string, std::string>& licenseHeaders,
const std::vector<uint8_t>& defaultKeyId,
CClearKeyDecrypter* host)
: m_host(host)
{
if (licenseUrl.empty())
Expand All @@ -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();
Expand Down Expand Up @@ -83,10 +86,10 @@ CClearKeyCencSingleSampleDecrypter::CClearKeyCencSingleSampleDecrypter(
return;
}

std::vector<uint8_t> keyBytes = BASE64::Decode(m_keyPairs[b64DefaultKeyId]);
const std::vector<uint8_t> 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<AP4_Size>(keyBytes.size()), 0, 0,
nullptr, false, m_singleSampleDecrypter)))
{
LOG::LogF(LOGERROR, "Failed to create AP4_CencSingleSampleDecrypter");
}
Expand Down Expand Up @@ -117,9 +120,9 @@ CClearKeyCencSingleSampleDecrypter::CClearKeyCencSingleSampleDecrypter(
LOG::LogF(LOGERROR, "Missing KeyId \"%s\" on DRM configuration", defaultKeyId.data());
}

const AP4_UI08* ap4Key = reinterpret_cast<const AP4_UI08*>(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<AP4_Size>(hexKey.size()), 0, 0, nullptr, false,
m_singleSampleDecrypter);
SetParentIsOwner(false);
AddSessionKey(defaultKeyId);
}
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class CClearKeyCencSingleSampleDecrypter : public Adaptive_CencSingleSampleDecry
{
public:
CClearKeyCencSingleSampleDecrypter(std::string_view licenseUrl,
const std::map<std::string, std::string>& licenseHeaders,
const std::vector<uint8_t>& defaultKeyId,
CClearKeyDecrypter* host);
CClearKeyCencSingleSampleDecrypter(const std::vector<uint8_t>& initdata,
Expand Down
20 changes: 16 additions & 4 deletions src/decrypters/clearkey/ClearKeyDecrypter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "CompKodiProps.h"
#include "SrvBroker.h"
#include "decrypters/Helpers.h"
#include "utils/log.h"

std::vector<std::string_view> CClearKeyDecrypter::SelectKeySystems(std::string_view keySystem)
{
Expand Down Expand Up @@ -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())
Expand Down
1 change: 1 addition & 0 deletions src/decrypters/widevine/WVCencSingleSampleDecrypter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

Expand Down
29 changes: 29 additions & 0 deletions src/parser/DASHTree.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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;
Expand Down
21 changes: 13 additions & 8 deletions src/parser/HLSTree.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand All @@ -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
Expand Down
Loading