inputstream.adaptive icon indicating copy to clipboard operation
inputstream.adaptive copied to clipboard

[Bug] Widevine "Privacy mode" is not activated even if server/service certificate is provided

Open WallyCZ opened this issue 9 months ago • 6 comments

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

  1. Provide the service certificate via the inputstream.adaptive.server_certificate option.
  2. 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

WallyCZ avatar May 04 '25 12:05 WallyCZ

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)

WallyCZ avatar May 04 '25 13:05 WallyCZ

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

WallyCZ avatar May 04 '25 16:05 WallyCZ

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 avatar May 04 '25 18:05 CastagnaIT

@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.

WallyCZ avatar May 04 '25 18:05 WallyCZ

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

CastagnaIT avatar May 06 '25 10:05 CastagnaIT

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

CastagnaIT avatar May 06 '25 13:05 CastagnaIT