Skip to content

Commit 647c08d

Browse files
authored
Merge pull request #1860 from CastagnaIT/ck_cbcs
[ClearKey] Implemented CBCS support
2 parents c62e516 + 37e01a0 commit 647c08d

File tree

6 files changed

+254
-123
lines changed

6 files changed

+254
-123
lines changed

plugin.video.isasamples/menu_data.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,15 @@
371371
'drm': '{"org.w3.clearkey": {"license": {"keyids": {"eb676abbcb345e96bbcf616630f1a3da": "100b6c20940f779a4589152b57d2dacb"}}}}',
372372
}
373373
},
374+
'Axiom v9multiFormat [clear key, keys on property]': {
375+
SI_ENCRYPT: 'DRMCK,CBCS',
376+
SI_FEATURE: 'ADPV',
377+
SI_CONFIG: {
378+
'manifest_url': 'https://media.axprod.net/TestVectors/v9-MultiFormat/Encrypted_Cbcs/Manifest.mpd',
379+
'drm_legacy': 'org.w3.clearkey|f8c80c25690f47368132430e5c6994ce:7bc99cb1dd0623cd0b5065056a57a1dd',
380+
'drm': '{"org.w3.clearkey": {"license": {"keyids": {"f8c80c25690f47368132430e5c6994ce": "7bc99cb1dd0623cd0b5065056a57a1dd"}}}}',
381+
}
382+
},
374383
},
375384
},
376385
'Manifest HLS': {

src/decrypters/clearkey/ClearKeyCencSingleSampleDecrypter.cpp

Lines changed: 189 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -40,51 +40,14 @@ CClearKeyCencSingleSampleDecrypter::CClearKeyCencSingleSampleDecrypter(
4040
return;
4141
}
4242

43-
std::string licenseData;
44-
std::vector<uint8_t> uriData;
43+
// Make license request to get KID/KEY pairs
44+
std::vector<uint8_t> licenseData;
4545

46-
if (URL::GetUriByteData(licenseUri, uriData)) // Provided license data in URI format
46+
// Check if provided license data in URI format, otherwise make the license request
47+
if (!URL::GetUriByteData(licenseUri, licenseData))
4748
{
48-
licenseData.assign(uriData.begin(), uriData.end());
49-
}
50-
else // Make the request to the server by using URL
51-
{
52-
const std::string postData = CreateLicenseRequest(defaultKeyId);
53-
54-
if (CSrvBroker::GetSettings().IsDebugLicense())
55-
{
56-
const std::string debugFilePath =
57-
FILESYS::PathCombine(m_host->GetLibraryPath(), "ClearKey.init");
58-
FILESYS::SaveFile(debugFilePath, postData.c_str(), true);
59-
}
60-
61-
CURL::CUrl curl{std::string(licenseUri), postData};
62-
curl.AddHeader("Accept", "application/json");
63-
curl.AddHeader("Content-Type", "application/json");
64-
curl.AddHeaders(licenseHeaders);
65-
66-
std::string response;
67-
int statusCode = curl.Open();
68-
if (statusCode == -1 || statusCode >= 400)
69-
{
70-
LOG::Log(LOGERROR, "License server returned failure (HTTP error %i)", statusCode);
71-
return;
72-
}
73-
74-
if (curl.Read(response) != CURL::ReadStatus::IS_EOF)
75-
{
76-
LOG::LogF(LOGERROR, "Could not read the license server response");
49+
if (!MakeLicenseRequest(std::string(licenseUri), licenseHeaders, defaultKeyId, licenseData))
7750
return;
78-
}
79-
80-
if (CSrvBroker::GetSettings().IsDebugLicense())
81-
{
82-
const std::string debugFilePath =
83-
FILESYS::PathCombine(m_host->GetLibraryPath(), "ClearKey.response");
84-
FILESYS::SaveFile(debugFilePath, response, true);
85-
}
86-
87-
licenseData = response;
8851
}
8952

9053
if (!ParseLicenseResponse(licenseData))
@@ -93,16 +56,14 @@ CClearKeyCencSingleSampleDecrypter::CClearKeyCencSingleSampleDecrypter(
9356
return;
9457
}
9558

96-
const std::string b64DefaultKeyId = BASE64::Encode(defaultKeyId);
97-
if (!STRING::KeyExists(m_keyPairs, b64DefaultKeyId))
59+
if (!STRING::KeyExists(m_kidPairs, defaultKeyId))
9860
{
99-
LOG::LogF(LOGERROR, "Key not found on license data");
61+
LOG::LogF(LOGERROR, "License data does not have the required KID");
62+
m_kidPairs.clear();
10063
return;
10164
}
10265

103-
const std::vector<uint8_t> keyBytes = BASE64::Decode(m_keyPairs[b64DefaultKeyId]);
104-
105-
InitDecrypter(defaultKeyId, keyBytes);
66+
InitDecrypter();
10667
}
10768

10869
CClearKeyCencSingleSampleDecrypter::CClearKeyCencSingleSampleDecrypter(
@@ -111,62 +72,138 @@ CClearKeyCencSingleSampleDecrypter::CClearKeyCencSingleSampleDecrypter(
11172
CClearKeyDecrypter* host)
11273
: m_host(host)
11374
{
114-
std::vector<uint8_t> hexKey;
11575
// Currently HLS manifest only support this
116-
// and the init data should contain only the key
117-
hexKey = initData;
76+
// the initData should contain only the key
77+
m_kidPairs.emplace(defaultKeyId, initData);
11878

119-
InitDecrypter(defaultKeyId, hexKey);
79+
InitDecrypter();
12080
}
12181

122-
void CClearKeyCencSingleSampleDecrypter::InitDecrypter(const std::vector<uint8_t>& defaultKeyId,
123-
const std::vector<uint8_t>& key)
82+
void CClearKeyCencSingleSampleDecrypter::InitDecrypter()
12483
{
125-
AP4_CencSingleSampleDecrypter::Create(AP4_CENC_CIPHER_AES_128_CTR, key.data(),
126-
static_cast<AP4_Size>(key.size()), 0, 0, nullptr, false,
127-
m_singleSampleDecrypter);
12884
SetParentIsOwner(false);
129-
AddSessionKey(defaultKeyId);
13085

13186
// Define a session id
13287
m_sessionId = "ck_" + std::to_string(g_sessionIdCount++);
13388
}
13489

135-
void CClearKeyCencSingleSampleDecrypter::AddSessionKey(const std::vector<uint8_t>& keyId)
90+
bool CClearKeyCencSingleSampleDecrypter::HasKeyId(const std::vector<uint8_t>& keyid)
13691
{
137-
if (std::find(m_keyIds.begin(), m_keyIds.end(), keyId) == m_keyIds.end())
138-
m_keyIds.emplace_back(keyId);
92+
return STRING::KeyExists(m_kidPairs, keyid);
13993
}
14094

141-
bool CClearKeyCencSingleSampleDecrypter::HasKeyId(const std::vector<uint8_t>& keyid)
95+
AP4_UI32 CClearKeyCencSingleSampleDecrypter::AddPool()
14296
{
143-
if (!keyid.empty())
97+
const AP4_UI32 poolId = static_cast<AP4_UI32>(m_pool.size());
98+
99+
m_pool.emplace(poolId, PINFO());
100+
101+
return poolId;
102+
}
103+
104+
void CClearKeyCencSingleSampleDecrypter::RemovePool(AP4_UI32 poolId)
105+
{
106+
m_pool.erase(poolId);
107+
}
108+
109+
AP4_Result CClearKeyCencSingleSampleDecrypter::SetFragmentInfo(AP4_UI32 poolId,
110+
const std::vector<uint8_t>& keyId,
111+
const AP4_UI08 nalLengthSize,
112+
AP4_DataBuffer& annexbSpsPps,
113+
AP4_UI32 flags,
114+
CryptoInfo cryptoInfo)
115+
{
116+
if (!STRING::KeyExists(m_pool, poolId))
144117
{
145-
for (const std::vector<uint8_t>& key : m_keyIds)
146-
{
147-
if (key == keyid)
148-
return true;
149-
}
118+
LOG::LogF(LOGERROR, "Cannot set fragment info, the pool id %u dont exist", poolId);
119+
return AP4_ERROR_INVALID_PARAMETERS;
150120
}
151-
return false;
121+
122+
if (cryptoInfo.m_mode != CryptoMode::NONE && cryptoInfo.m_mode != CryptoMode::AES_CTR &&
123+
cryptoInfo.m_mode != CryptoMode::AES_CBC)
124+
{
125+
LOG::LogF(LOGERROR, "Cannot set fragment info, unsupported crypto mode (%i)",
126+
static_cast<int>(cryptoInfo.m_mode));
127+
return AP4_ERROR_INVALID_PARAMETERS;
128+
}
129+
130+
PINFO& pInfo = m_pool[poolId];
131+
FINFO& fInfo = pInfo.fInfo;
132+
133+
// Compare the encryption info from the previous fragment to see if it has been changed,
134+
// if so, the decrypter will have to be recreated
135+
pInfo.isChanged = fInfo.kid != keyId || fInfo.cryptoInfo.m_mode != cryptoInfo.m_mode ||
136+
fInfo.cryptoInfo.m_cryptBlocks != cryptoInfo.m_cryptBlocks ||
137+
fInfo.cryptoInfo.m_skipBlocks != cryptoInfo.m_skipBlocks;
138+
139+
// Update with the current fragment info
140+
fInfo.kid = keyId;
141+
fInfo.cryptoInfo = cryptoInfo;
142+
143+
return AP4_SUCCESS;
152144
}
153145

154146
AP4_Result CClearKeyCencSingleSampleDecrypter::DecryptSampleData(
155-
AP4_UI32 pool_id,
156-
AP4_DataBuffer& data_in,
157-
AP4_DataBuffer& data_out,
147+
AP4_UI32 poolId,
148+
AP4_DataBuffer& dataIn,
149+
AP4_DataBuffer& dataOut,
158150
const AP4_UI08* iv,
159-
unsigned int subsample_count,
160-
const AP4_UI16* bytes_of_cleartext_data,
161-
const AP4_UI32* bytes_of_encrypted_data)
151+
unsigned int subsampleCount,
152+
const AP4_UI16* bytesOfCleartextData,
153+
const AP4_UI32* bytesOfEncryptedData)
162154
{
163-
if (!m_singleSampleDecrypter)
155+
if (m_pool.empty())
156+
{
157+
LOG::LogF(LOGERROR, "Cannot decrypt data, the pool is empty");
158+
return AP4_ERROR_INTERNAL;
159+
}
160+
if (!STRING::KeyExists(m_pool, poolId))
164161
{
165-
return AP4_FAILURE;
162+
LOG::LogF(LOGERROR, "Cannot decrypt data, the pool id %u dont exist", poolId);
163+
return AP4_ERROR_INVALID_PARAMETERS;
166164
}
167-
return (m_singleSampleDecrypter)
168-
->DecryptSampleData(data_in, data_out, iv, subsample_count, bytes_of_cleartext_data,
169-
bytes_of_encrypted_data);
165+
166+
PINFO& pInfo = m_pool[poolId];
167+
auto& decrypter = pInfo.decrypter;
168+
169+
if (!decrypter || pInfo.isChanged)
170+
{
171+
const auto& fInfo = pInfo.fInfo;
172+
173+
if (!STRING::KeyExists(m_kidPairs, fInfo.kid))
174+
{
175+
// In theory when there is a license server url, could be possible to request new KID/KEY pair
176+
// making a new HTTP license request e.g. the case of key rotation, but it needs to be tested properly
177+
LOG::LogF(LOGERROR, "Cannot decrypt data, due to missing KID/KEY from the license data");
178+
return AP4_ERROR_INVALID_STATE;
179+
}
180+
181+
AP4_CencSingleSampleDecrypter* pDecrypter{nullptr};
182+
const std::vector<uint8_t>& key = m_kidPairs[fInfo.kid];
183+
184+
AP4_UI32 cypherType{AP4_CENC_CIPHER_NONE};
185+
bool resetIvEachSubsample{false};
186+
187+
if (fInfo.cryptoInfo.m_mode == CryptoMode::AES_CTR)
188+
{
189+
cypherType = AP4_CENC_CIPHER_AES_128_CTR;
190+
}
191+
else if (fInfo.cryptoInfo.m_mode == CryptoMode::AES_CBC)
192+
{
193+
cypherType = AP4_CENC_CIPHER_AES_128_CBC;
194+
// CBCS reset the IV at each subsample, see https://github.com/axiomatic-systems/Bento4/commit/ab07e3acc7befc821554bc5e271df1201681e954
195+
resetIvEachSubsample = true;
196+
}
197+
198+
AP4_CencSingleSampleDecrypter::Create(
199+
cypherType, key.data(), static_cast<AP4_Size>(key.size()), fInfo.cryptoInfo.m_cryptBlocks,
200+
fInfo.cryptoInfo.m_skipBlocks, nullptr, resetIvEachSubsample, pDecrypter);
201+
202+
decrypter = std::unique_ptr<AP4_CencSingleSampleDecrypter>{std::exchange(pDecrypter, nullptr)};
203+
}
204+
205+
return decrypter->DecryptSampleData(dataIn, dataOut, iv, subsampleCount, bytesOfCleartextData,
206+
bytesOfEncryptedData);
170207
}
171208

172209
std::string CClearKeyCencSingleSampleDecrypter::CreateLicenseRequest(
@@ -189,7 +226,54 @@ std::string CClearKeyCencSingleSampleDecrypter::CreateLicenseRequest(
189226
return jData.dump(-1, ' ', false, njson::error_handler_t::ignore);
190227
}
191228

192-
bool CClearKeyCencSingleSampleDecrypter::ParseLicenseResponse(std::string data)
229+
bool CClearKeyCencSingleSampleDecrypter::MakeLicenseRequest(
230+
const std::string& url,
231+
const std::map<std::string, std::string>& headers,
232+
const std::vector<uint8_t>& kid,
233+
std::vector<uint8_t>& licenseData)
234+
{
235+
const std::string postData = CreateLicenseRequest(kid);
236+
237+
if (CSrvBroker::GetSettings().IsDebugLicense())
238+
{
239+
const std::string debugFilePath =
240+
FILESYS::PathCombine(m_host->GetLibraryPath(), "ClearKey.init");
241+
FILESYS::SaveFile(debugFilePath, postData.c_str(), true);
242+
}
243+
244+
CURL::CUrl curl{url, postData};
245+
curl.AddHeader("Accept", "application/json");
246+
curl.AddHeader("Content-Type", "application/json");
247+
curl.AddHeaders(headers);
248+
249+
int statusCode = curl.Open();
250+
if (statusCode == -1 || statusCode >= 400)
251+
{
252+
LOG::Log(LOGERROR, "License server returned failure (HTTP error %i)", statusCode);
253+
return false;
254+
}
255+
256+
std::string responseData;
257+
258+
if (curl.Read(responseData) != CURL::ReadStatus::IS_EOF)
259+
{
260+
LOG::LogF(LOGERROR, "Could not read the license server response");
261+
return false;
262+
}
263+
264+
if (CSrvBroker::GetSettings().IsDebugLicense())
265+
{
266+
const std::string debugFilePath =
267+
FILESYS::PathCombine(m_host->GetLibraryPath(), "ClearKey.response");
268+
FILESYS::SaveFile(debugFilePath, responseData, true);
269+
}
270+
271+
licenseData.assign(responseData.begin(), responseData.end());
272+
273+
return true;
274+
}
275+
276+
bool CClearKeyCencSingleSampleDecrypter::ParseLicenseResponse(const std::vector<uint8_t>& data)
193277
{
194278
/* Expected JSON structure for license response:
195279
* { "keys": [
@@ -219,36 +303,47 @@ bool CClearKeyCencSingleSampleDecrypter::ParseLicenseResponse(std::string data)
219303
return false;
220304
}
221305

222-
for (const auto& item : jData["keys"]) // Iterate array
306+
for (const auto& jwkValue : jData["keys"]) // Iterate array
223307
{
224-
if (!item.is_object())
308+
if (!jwkValue.is_object())
225309
{
226310
LOG::LogF(LOGERROR, "Unexpected JSON format in the license response");
227311
continue;
228312
}
229313

314+
if (jwkValue.contains("kty") && jwkValue["kty"].is_string() &&
315+
jwkValue["kty"].get<std::string>() != "oct")
316+
{
317+
LOG::LogF(LOGWARNING, "Ignored JWK set, due to unsupported \"%s\" key type",
318+
jwkValue["kty"].get<std::string>().c_str());
319+
continue;
320+
}
321+
230322
std::string b64Key;
231323
std::string b64KeyId;
232324

233-
if (item.contains("k") && item["k"].is_string())
234-
b64Key = item["k"].get<std::string>();
325+
if (jwkValue.contains("k") && jwkValue["k"].is_string())
326+
b64Key = jwkValue["k"].get<std::string>();
235327

236-
if (item.contains("kid") && item["kid"].is_string())
237-
b64KeyId = item["kid"].get<std::string>();
238-
239-
if (b64Key.empty() || b64KeyId.empty())
240-
{
241-
LOG::LogF(LOGERROR, "Malformed key pair value in the license response");
242-
continue;
243-
}
328+
if (jwkValue.contains("kid") && jwkValue["kid"].is_string())
329+
b64KeyId = jwkValue["kid"].get<std::string>();
244330

245331
b64Key = BASE64::UrlSafeDecode(b64Key);
246332
BASE64::AddPadding(b64Key);
247333

248334
b64KeyId = BASE64::UrlSafeDecode(b64KeyId);
249335
BASE64::AddPadding(b64KeyId);
250336

251-
m_keyPairs.emplace(b64KeyId, b64Key);
337+
const std::vector<uint8_t> kidBytes = BASE64::Decode(b64KeyId);
338+
const std::vector<uint8_t> keyBytes = BASE64::Decode(b64Key);
339+
340+
if (kidBytes.empty() || keyBytes.empty())
341+
{
342+
LOG::LogF(LOGERROR, "Malformed key pair value in the license response");
343+
continue;
344+
}
345+
346+
m_kidPairs.insert_or_assign(kidBytes, keyBytes);
252347
}
253348

254349
return true;

0 commit comments

Comments
 (0)