[Bug] Widevine "Privacy mode" is not activated even if server/service certificate is provided
Describe the problem
I’m trying to integrate a provider that uses Widevine as its CDM. The provider checks the CDM module version (thanks for supporting the latest 4.10.2891.0) and apparently also verifies whether privacy mode is enabled—when it is, the license request contains encryptedClientId instead of clientId.
I supply the server certificate with the following code:
cert = 'CsECCAMSEBcFuRfMEgSGiwYzOi93KowYgrSCkgUijgIwggEKAoIBAQCZ7Vs7Mn2rXiTvw7YqlbWYUgrVvMs3UD4GRbgU2Ha430BRBEGtjOOtsRu4jE5yWl5KngeVKR1YWEAjp+GvDjipEnk5MAhhC28VjIeMfiG/+/7qd+EBnh5XgeikX0YmPRTmDoBYqGB63OBPrIRXsTeo1nzN6zNwXZg6IftO7L1KEMpHSQykfqpdQ4IY3brxyt4zkvE9b/tkQv0x4b9AsMYE0cS6TJUgpL+X7r1gkpr87vVbuvVk4tDnbNfFXHOggrmWEguDWe3OJHBwgmgNb2fG2CxKxfMTRJCnTuw3r0svAQxZ6ChD4lgvC2ufXbD8Xm7fZPvTCLRxG88SUAGcn1oJAgMBAAE6FGxpY2Vuc2Uud2lkZXZpbmUuY29tEoADrjRzFLWoNSl/JxOI+3u4y1J30kmCPN3R2jC5MzlRHrPMveoEuUS5J8EhNG79verJ1BORfm7BdqEEOEYKUDvBlSubpOTOD8S/wgqYCKqvS/zRnB3PzfV0zKwo0bQQQWz53ogEMBy9szTK/NDUCXhCOmQuVGE98K/PlspKkknYVeQrOnA+8XZ/apvTbWv4K+drvwy6T95Z0qvMdv62Qke4XEMfvKUiZrYZ/DaXlUP8qcu9u/r6DhpV51Wjx7zmVflkb1gquc9wqgi5efhn9joLK3/bNixbxOzVVdhbyqnFk8ODyFfUnaq3fkC3hR3f0kmYgI41sljnXXjqwMoW9wRzBMINk+3k6P8cbxfmJD4/Paj8FwmHDsRfuoI6Jj8M76H3CTsZCZKDJjM3BQQ6Kb2m+bQ0LMjfVDyxoRgvfF//M/EEkPrKWyU2C3YBXpxaBquO4C8A0ujVmGEEqsxN1HX9lu6c5OMm8huDxwWFd7OHMs3avGpr7RP7DUnTikXrh6X0'
list_item.setProperty('inputstream.adaptive.server_certificate', cert)
Despite this, the CDM still sends a license request with clientId in plaintext rather than encryptedClientId, while the browser’s CDM (same version) uses encryptedClientId. I’m using my own licensing proxy server, but that shouldn’t be an issue—when I forward the browser’s challenge, everything works. I’ve tested on Windows, Android, and Linux, and the behavior is identical.
Sorry if I’m overlooking something; I’m still a beginner in this area.
Possible fix
Is there a flag (similar to one in browsers) that needs to be enabled, or am I missing another prerequisite?
Steps to reproduce
- Provide the service certificate via the inputstream.adaptive.server_certificate option.
- Observe that the license request contains clientId instead of encryptedClientId.
Debug log
Sorry, can proovide the full log due to privacy issues, but I can provide some snippets if required (although I don't think it's relevant here).
2025-05-04 14:41:49.249 T:89072 warning <general>: AddOnLog: inputstream.adaptive: adaptive::CDashTree::ParseManifest: The <UTCTiming> tag element is not supported so playback problems may occur.
2025-05-04 14:41:49.249 T:89072 debug <general>: AddOnLog: inputstream.adaptive: Parsing Playready header version 4.0.0.0
2025-05-04 14:41:49.250 T:89072 info <general>: Skipped 1 duplicate messages..
2025-05-04 14:41:49.250 T:89072 info <general>: AddOnLog: inputstream.adaptive: Manifest successfully parsed (Periods: 1, Streams in first period: 3, Type: live)
2025-05-04 14:41:49.250 T:89072 debug <general>: AddOnLog: inputstream.adaptive: [Repr. chooser] Stream selection conditions
Screen resolution: 2560x1600 (may be limited by settings)
Initial bandwidth: 21399329 bit/s
2025-05-04 14:41:49.250 T:89072 debug <general>: AddOnLog: inputstream.adaptive: New period, dispose sample decrypter and reinitialize
2025-05-04 14:41:49.250 T:89072 debug <general>: AddOnLog: inputstream.adaptive: Entering encryption section
2025-05-04 14:41:49.257 T:89072 debug <general>: AddOnLog: inputstream.adaptive: [WV-CDM-Library] CDM version: 4.10.2891.0
2025-05-04 14:41:49.327 T:89072 debug <general>: AddOnLog: inputstream.adaptive: [WV-CDM-Library] CDM is initialized: true
2025-05-04 14:41:49.328 T:89072 debug <general>: AddOnLog: inputstream.adaptive: Initializing stream with KID: 030f0054da3a4c6fbd556981b889e7ce
2025-05-04 14:41:49.407 T:89072 debug <general>: AddOnLog: inputstream.adaptive: CDMMessage: 1 arrived!
2025-05-04 14:41:49.407 T:89072 debug <general>: AddOnLog: inputstream.adaptive: CWVCencSingleSampleDecrypter::SetSession: Opened widevine session ID: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
2025-05-04 14:41:49.409 T:89072 debug <general>: CurlFile::XFILE::CCurlFile::Open - <http://127.0.0.1:50316/license?asset_id=729241&program_id=191601449&file_id=109106495>
2025-05-04 14:41:49.421 T:88688 debug <CSettingsManager>: requested setting (proxy_port) was not found.
2025-05-04 14:41:49.425 T:88688 debug <CSettingsManager>: requested setting (proxy_port) was not found.
2025-05-04 14:41:50.116 T:89072 error <general>: AddOnLog: inputstream.adaptive: License server returned failure (HTTP error 500)
2025-05-04 14:41:50.116 T:89072 debug <general>: AddOnLog: inputstream.adaptive: CWVCencSingleSampleDecrypter::GetCapabilities: Keys empty
2025-05-04 14:41:50.116 T:89072 debug <general>: AddOnLog: inputstream.adaptive: Initializing stream with KID: 030f0054da3a4c6fbd556981b889e7ce
2025-05-04 14:41:50.164 T:89072 debug <general>: AddOnLog: inputstream.adaptive: CDMMessage: 1 arrived!
2025-05-04 14:41:50.164 T:89072 debug <general>: AddOnLog: inputstream.adaptive: CWVCencSingleSampleDecrypter::SetSession: Opened widevine session ID: XXXXXXXXXXXX
....
2025-05-04 14:41:55.695 T:71544 debug <general>: AddOnLog: inputstream.adaptive: CWVCencSingleSampleDecrypter::LogDecryptError: Decrypt failed with error code: 2 and KID: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
2025-05-04 14:41:55.695 T:71544 error <general>: AddOnLog: inputstream.adaptive: Decrypt Sample returns failure!
Stream manifest file(s)
No response
Additional info
CDM Widevine version: 4.10.2891.0
Operating system(s)
Windows
Operating system version(s)
11
InputStream Adaptive version(s)
21.5.13
Kodi version(s)
21
I have noticed, that Android variant has this code: https://github.com/xbmc/inputstream.adaptive/blob/b221fcb2461b74ce6f612722833a7e63d63ee404/src/decrypters/widevineandroid/WVCdmAdapter.cpp#L169 But I think it's only executed when cert is provided via file and not via inputstream.adaptive.server_certificate argument. Can't find a similar way on non-Android version (API has no setPropertyString method)
Btw. I added some logging to cdm_adaper.cc:
void CdmAdapter::SetServerCertificate(uint32_t promise_id,
const uint8_t* server_certificate_data,
uint32_t server_certificate_data_size)
{
if (server_certificate_data_size < limits::kMinCertificateLength ||
server_certificate_data_size > limits::kMaxCertificateLength) {
return;
}
Log(LogLevel::DEBUG, "SetServerCertificate: %d, %d", promise_id, server_certificate_data_size);
...
void CdmAdapter::OnResolvePromise(uint32_t promise_id)
{
Log(LogLevel::DEBUG, "OnResolvePromise: %d", promise_id);
}
...
void CdmAdapter::OnRejectPromise(uint32_t promise_id, cdm::Exception exception,
uint32_t system_code, const char* error_message, uint32_t error_message_size)
{
Log(LogLevel::WARNING, "OnRejectPromise: %d", promise_id);
}
And I see this, so it seems the certificate is accepted:
2025-05-04 18:10:24.387 T:90856 debug <general>: AddOnLog: inputstream.adaptive: [WV-CDM-Library] CDM version: 4.10.2891.0
2025-05-04 18:10:24.727 T:90856 debug <general>: AddOnLog: inputstream.adaptive: [WV-CDM-Library] CDM is initialized: true
2025-05-04 18:10:24.727 T:90856 debug <general>: AddOnLog: inputstream.adaptive: [WV-CDM-Library] SetServerCertificate: 0, 711
2025-05-04 18:10:24.728 T:90856 debug <general>: AddOnLog: inputstream.adaptive: [WV-CDM-Library] OnResolvePromise: 0
there is no widevine documentation explaining how it works, however as i recall from my past research, privacy mode can be enabled on android (ISA already enable it when certificate is set) but as far as using in the CDM library, no, it is not an automatic thing privacy mode must be implemented separately from CDM by using protobuf format with appropriate encryption
months ago i was starting an attempt to study on how it could be implemented, but no longer had time too many things to do... also i didnt have the certainty of having a way to test if really works
@CastagnaIT Are you sure about this? Isn't license request signed by the CDM so it can't be modified? I have read this analysis of the older WV-CDM version - https://github.com/tomer8007/widevine-l3-decryptor/wiki/Reversing-the-old-Widevine-Content-Decryption-Module and according this it looks like the code is inside the CDM:
The encrypted_client_id is AES-encrypted using a so-called privacy key which is encrypted itself (encrypted_privacy_key), together with an IV from encrypted_client_id_iv.
Luckily, the code that does this privacy encryption is not considered sensative at all, and it's not even obfuscated. In fact, it's easy to see it is done from the CDM's crypto/encryptor.cc source file, and we can extract the key by finding and hooking OpenSSL's aes_init_key function (which is called from EVP_CipherInit_ex).
I tested Firefox, Chrome and Edge and all are sending requests with encryptedClientId, so if you are right, all must implement the same thing.
Ok, I'll try to do some more research when I have free time and try to get where is the implementation nowadays - if in CDM or elsewhere.
Isn't license request signed by the CDM so it can't be modified?
when you create the session https://github.com/xbmc/inputstream.adaptive/blob/b221fcb2461b74ce6f612722833a7e63d63ee404/src/decrypters/widevine/WVCencSingleSampleDecrypter.cpp#L74 https://github.com/xbmc/inputstream.adaptive/blob/b221fcb2461b74ce6f612722833a7e63d63ee404/lib/cdm/cdm/media/cdm/api/content_decryption_module.h#L747 you obtain the challenge (key request) used for license request, but i dont see any other option that could be changed to have some kind of different behaviour on the challenge data
or maybe this thing dont work because here we cant have a CDM PlatformVerification, But it's a guess i don't know... you can also experiment if by setting some kind of fake storage id here https://github.com/xbmc/inputstream.adaptive/blob/b221fcb2461b74ce6f612722833a7e63d63ee404/lib/cdm/cdm/media/cdm/cdm_adapter.cc#L714-L719 and check if it has influence or not
pywidevine for example does more or less what theorized
https://github.com/devine-dl/pywidevine/blob/7ea2a72a8c12a607f0b67fd7e53f669a41d7721e/pywidevine/cdm.py#L279
when privacy_mode var is set make a custom challenge with encryptedClientId