Giter VIP home page Giter VIP logo

pywidevine's Introduction

pywidevine
Python Widevine CDM implementation

Build status Python version DeepSource

Linter: Ruff Dependency management: Poetry

Features

  • ๐Ÿš€ Seamless Installation via pip
  • ๐Ÿ›ก๏ธ Robust Security with message signature verification
  • ๐Ÿ™ˆ Privacy Mode with Service Certificates
  • ๐ŸŒ Servable CDM API Server and Client with Authentication
  • ๐Ÿ“ฆ Custom provision serialization format (WVD v2)
  • ๐Ÿงฐ Create, parse, or convert PSSH headers with ease
  • ๐Ÿ—ƒ๏ธ User-friendly YAML configuration
  • โค๏ธ Forever FOSS!

Installation

$ pip install pywidevine

Note If pip gives you a warning about a path not being in your PATH environment variable then promptly add that path then close all open command prompt/terminal windows, or pywidevine CLI won't work as it will not be found.

Voilร  ๐ŸŽ‰ โ€” You now have the pywidevine package installed!
You can now import pywidevine in scripts (see below).
A command-line interface is also available, try pywidevine --help.

Usage

The following is a minimal example of using pywidevine in a script to get a License for Bitmovin's Art of Motion Demo.

from pywidevine.cdm import Cdm
from pywidevine.device import Device
from pywidevine.pssh import PSSH

import requests

# prepare pssh
pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa"
            "7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==")

# load device
device = Device.load("C:/Path/To/A/Provision.wvd")

# load cdm
cdm = Cdm.from_device(device)

# open cdm session
session_id = cdm.open()

# get license challenge
challenge = cdm.get_license_challenge(session_id, pssh)

# send license challenge (assuming a generic license server SDK with no API front)
licence = requests.post("https://...", data=challenge)
licence.raise_for_status()

# parse license challenge
cdm.parse_license(session_id, licence.content)

# print keys
for key in cdm.get_keys(session_id):
    print(f"[{key.type}] {key.kid.hex}:{key.key.hex()}")

# close session, disposes of session data
cdm.close(session_id)

Note There are various features not shown in this specific example like:

  • Privacy Mode
  • Setting Service Certificates
  • Remote CDMs and Serving
  • Choosing a License Type to request
  • Creating WVD files
  • and much more!

Take a look at the methods available in the Cdm class and their doc-strings for further information. For more examples see the CLI functions which uses a lot of previously mentioned features.

Disclaimer

  1. This project requires a valid Google-provisioned Private Key and Client Identification blob which are not provided by this project.
  2. Public test provisions are available and provided by Google to use for testing projects such as this one.
  3. License Servers have the ability to block requests from any provision, and are likely already blocking test provisions on production endpoints.
  4. This project does not condone piracy or any action against the terms of the DRM systems.
  5. All efforts in this project have been the result of Reverse-Engineering, Publicly available research, and Trial & Error.

Key and Output Security

Licenses, Content Keys, and Decrypted Data is not secure in this CDM implementation.

The Content Decryption Module is meant to do all downloading, decrypting, and decoding of content, not just license acquisition. This Python implementation only does License Acquisition within the CDM.

The section of which a 'Decrypt Frame' call is made would be more of a 'Decrypt File' in this implementation. Just returning the original file in plain text defeats the point of the DRM. Even if 'Decrypt File' was somehow secure, the Content Keys used to decrypt the files are already exposed to the caller anyway, allowing them to manually decrypt.

An attack on a 'Decrypt Frame' system would be analogous to doing an HDMI capture or similar attack. This is because it would require re-encoding the video by splicing each individual frame with the right frame-rate, syncing to audio, and more.

While a 'Decrypt Video' system would be analogous to downloading a Video and passing it through a script. Not much of an attack if at all. The only protection against a system like this would be monitoring the provision and acquisitions of licenses and prevent them. This can be done by revoking the device provision, or the user or their authorization to the service.

There isn't any immediate way to secure either Key or Decrypted information within a Python environment that is not Hardware backed. Even if obfuscation or some other form of Security by Obscurity was used, this is a Software-based Content Protection Module (in Python no less) with no hardware backed security. It would be incredibly trivial to break any sort of protection against retrieving the original video data.

Though, it's not impossible. Google's Chrome Browser CDM is a simple library extension file programmed in C++ that has been improving its security using math and obscurity for years. It's getting harder and harder to break with its latest versions only being beaten by Brute-force style methods. However, they have a huge team of very skilled workers, and making a CDM in C++ has immediate security benefits and a lot of methods to obscure and obfuscate the code.

Contributors

Licensing

This software is licensed under the terms of GNU General Public License, Version 3.0.
You can find a copy of the license in the LICENSE file in the root folder.

  • Widevine Icon ยฉ Google.
  • Props to the awesome community for their shared research and insight into the Widevine Protocol and Key Derivation.

ยฉ rlaphoenix 2022-2023

pywidevine's People

Contributors

dependabot[bot] avatar mediaminister avatar rlaphoenix avatar sr0lle avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

pywidevine's Issues

500 Server Error when using RemoteCDM on BitMovin demo.

Describe the bug
500 Server Error when using RemoteCDM on BitMovin demo.

To Reproduce
Steps to reproduce the behavior:

  1. Call set_service_certificate(session_id, Cdm.common_privacy_cert)
  2. Licence set successfully.
  3. get_license_challenge successful.
  4. Posting the challenge to the license URL gives error 500. INVALID CHALLENGE

Expected behavior
Challenge is valid.

Additional context
Works fine without privacy mode, same provision on same machine.
Testing using local WVD through the CLI works as well.

Relevant code. Redacted for privacy

pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==")
#license URL
license_url = "https://cwip-shaka-proxy.appspot.com/no_auth"

cdm = RemoteCdm("ANDROID", 1234, 3, "server_url", "secret_key", "test_device_01")

# open cdm session
session_id = cdm.open()

cdm.set_service_certificate(session_id, Cdm.common_privacy_cert)

# get license challenge
challenge = cdm.get_license_challenge(session_id, pssh, "STREAMING", True)

# send license challenge (assuming a generic license server SDK with no API front)
license = requests.post(license_url, data=challenge)
license.raise_for_status()

Proto Message Type Verifications aren't strict

Describe the bug
For example, <html><body>Server error, sorry</body></html> (as UTF8 bytes) will be parsed successfully as a SignedMessage with ParseFromString() into a partial message (with minimal data supplied).

In this specific example, it will parse as a very bare LICENSE_REQUEST type SignedMessage. If this is supplied to the parse_license method it will fail in a weird way. It of course expects a LICENSE type SignedMessage, but gets a LICENSE_REQUEST type SignedMessage which would be very unexpected.

To Reproduce
Steps to reproduce the behavior:

  1. Install pip install pywidevine
  2. Construct a Cdm object.
  3. Generate a Challenge but pass <html><body>Server error, sorry</body></html> to parse_license instead of a real license response.
  4. It will raise an InvalidLicenseMessage exception stating, Expecting a LICENSE message, not a 'LICENSE_REQUEST' message.

Expected behavior
It should have failed to parse the message entirely stating a different error, being more accurate to the validity of the input data.

Screenshots
N/A

Additional context
N/A

Add endpoint /device to get device information

Is your feature request related to a problem? Please describe.
Currently RemoteCdm needs the Device Type, System ID and Security Level specified manually with absolutely no context to base off of.
When /open is called, it has to verify the values to be correct. However, what if you need to find out the values beforehand? No options without asking whoever is serving.

Describe the solution you'd like
A get call to /device e.g., /device/test_device_001 will return the Device Type, System ID, and Security Level for test_device_001. This endpoint could even be automated as part of the RemoteCdm construction procedure, before the super call.

Describe alternatives you've considered
N/A

Additional context
N/A

How to generate a token for requesting a license?

I want to contact a license url which requires a client token before issuing a license.
It seems the overall process is very similar to what is described here: https://pallycon.com/docs/en/multidrm/license/license-token/
When analyzing the data flow between the client and the streaming site I can nowhere see this token request-response.

Is this something which is done by the CDM when initializing?
As far as I understand the file "license_protocol.proto" contains definition for those security policy fields like if HDCP is required etc.

How can I a) decode a token I see in the browsers devtools to see what's in there and b) generate my own?

Provisioning device certificate based on keybox

Android devices are provisioned in factory using keyboxes, not with certificates. See for instance https://github.com/zybpp/Python/blob/master/Python/keybox/widevine/Lenovo%20TB-X705-TAB510PLUS-04020003.bin.secure

As far as I understand, a device can not make requests to service providers with that keybox (even though this seems to be a valid ClientIdentification looking at license_protocol.proto?). If I understand correctly, a device first needs to call a provisioning endpoint (at least that's what the MediaDrm Android API make it look like). Checking a strings libwvhidl.so |grep provision I'm guessing the URL is https://www.googleapis.com/certificateprovisioning/v1/devicecertificates/create?key=AIzaSyB-5OLKTx2iU5mko18DfdwK5611JIjbUhE (but that can confirmed that by interception provisioning calls). I see in other reversed pb there is a ProvisioningRequest message (though that other source doesn't give the content of ProvisioningRequest)

Would it be possible for pywidevine to implement provisioning of device certificates based on keyboxes?

ClientIdentification returns empty object, halting create-device

Describe the bug

When running create-device, it raises an exception. I've been fiddling around and it's because at the Device __init__ step, the function ClientIdentification returns an almost empty object.

The original code is this:

def  __init__(...):
  ...
  self.client_id = ClientIdentification()

After that instruction, self.client_id prints as an empty string, its ByteSize method returns 0, the ClientCapabilities prints as an empty string, and the __sizeof__() is 64.

To Reproduce
I've run this command:

pywidevine create-device -t CHROME -l 3 -k ./my/key -c my/client_blob -v ./my/vmp_blob -o dv

The same happens when the -v argument is ommited.

Expected behavior
A .wvd file should be created.

Support for Licenses with a 128-byte Session Key

I've noticed that on Chrome license's responses, the session key which is normally 256 bytes long to match the length of the device private key, now is 128 bytes. It seems to suggest it is decrypted with a less secure private key (very unlikely) or the device private key is used elsewhere and differently.

I am not sure if I am missing anything or I am misunderstanding, but has this been noted or researched? or maybe this is on web based CDM's only? Padding the session key is not an option that works.

pip install lacks yaml dependency

I tried it on Arch Linux with:

pipx install pywidevine

and on MacOS with:

pip install pywidevine

After installing it, I run pywidevine -h and this is the output:

Traceback (most recent call last):
  File "/opt/homebrew/bin/pywidevine", line 5, in <module>
    from pywidevine.main import main
  File "/opt/homebrew/lib/python3.11/site-packages/pywidevine/main.py", line 9, in <module>
    import yaml
ModuleNotFoundError: No module named 'yaml'

Could not parse license message

I fully admit, I'm out of my depth here, but I have my CDM and wvd file, I know where to find PSSH and the licensing URL, and I was successfully able to pull keys from bitmovin and tg4.ie as tests... but when I went to pull from a live site I wanted the video from I got a "Could not parse license_message as a SignedMessage, Error parsing message" response.

I also noticed that the PSSH value was MUCH longer than both of my test sites...

What am I doing wrong?

Full response here:

INFO:root:pywidevine version 1.8.0 Copyright (c) 2022-2024 rlaphoenix
INFO:root:https://github.com/devine-dl/pywidevine
INFO:license:[+] Loaded Device (7283 L3)
INFO:license:[+] Loaded CDM
INFO:license:[+] Opened CDM Session: 2438e9eb69e80503839dbad22c8e3282
INFO:license:[+] Created License Request Message (Challenge)
INFO:license:[+] Got License Message
Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/pywidevine/cdm.py", line 407, in parse_license
    signed_message.ParseFromString(license_message)
google.protobuf.message.DecodeError: Error parsing message

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.12/bin/pywidevine", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/click/core.py", line 1157, in __call__
    return self.main(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/click/core.py", line 1078, in main
    rv = self.invoke(ctx)
         ^^^^^^^^^^^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/click/core.py", line 1688, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/click/core.py", line 1434, in invoke
    return ctx.invoke(self.callback, **ctx.params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/click/core.py", line 783, in invoke
    return __callback(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/pywidevine/main.py", line 116, in license_
    cdm.parse_license(session_id, licence)
  File "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/pywidevine/cdm.py", line 411, in parse_license
    raise InvalidLicenseMessage(f"Could not parse license_message as a SignedMessage, {e}")
pywidevine.exceptions.InvalidLicenseMessage: Could not parse license_message as a SignedMessage, Error parsing message

Does a Widevine library need to be installed first?

You did not mention if this require a widevine to be install first ?
does it require the widevine ? example i have ubuntu do i need to install first the widevine.so to make this work ? or it doesn't matter if i have a widevine or not.

Has anyone ever managed to figure out what oemcrypto_core_message is?

Has anyone ever managed to figure out what oemcrypto_core_message is?

I've been looking into this project which includes info on OEMCrypto function calls, but I don't have an Android to try it with.

Example from a license request:
00 00 00 01 00 00 00 14 00 05 00 10 28 a8 d7 b9 03 7e b7 93
{uint32; 1 for request, 2 for response} {uint32; length of entire string} 00 05 00 10 {8-byte nonce}

Its corresponding response:
00 00 00 02 00 00 01 c8 00 05 00 10 28 a8 d7 b9 03 7e b7 93 00 00 00 50 00 00 00 10 00 00 00 62 00 00 00 40 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 51 80 00 00 00 00 00 01 51 80 00 00 00 00 00 00 00 00 00 00 00 08 00 00 00 a8 00 00 00 10 00 00 00 ba 00 00 00 10 00 00 00 cc 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 00 00 01 00 00 00 00 10 00 00 01 12 00 00 00 10 00 00 01 24 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 00 00 01 58 00 00 00 10 00 00 01 6a 00 00 00 10 00 00 01 7c 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 00 00 01 b3 00 00 00 10 00 00 01 c5 00 00 00 10 00 00 01 d7 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 00 00 02 0b 00 00 00 10 00 00 02 1d 00 00 00 10 00 00 02 2f 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 00 00 02 63 00 00 00 10 00 00 02 75 00 00 00 10 00 00 02 87 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 00 00 02 bb 00 00 00 10 00 00 02 cd 00 00 00 10 00 00 02 df 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 00 00 03 13 00 00 00 10 00 00 03 25 00 00 00 10 00 00 03 37 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 ca df 6b 58 bd 1d 6b 08 98 c5 c9 d6 81 b9 48 99 51 c0 7e 12 e0 32 ea db be af 5e 36 4e 3a 2d 2f
{uint32; 1 for request, 2 for response} {uint32; length of entire string} 00 05 00 10 {the same 8-byte nonce from the request} 00 00 00 50 00 00 00 10 00 00 00 62 00 00 00 40 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 51 80 00 00 00 00 00 01 51 80 00 00 00 00 00 00 00 00 00 00 00 08 00 00 00 a8 00 00 00 10 00 00 00 ba 00 00 00 10 00 00 00 cc 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 00 00 01 00 00 00 00 10 00 00 01 12 00 00 00 10 00 00 01 24 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 00 00 01 58 00 00 00 10 00 00 01 6a 00 00 00 10 00 00 01 7c 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 00 00 01 b3 00 00 00 10 00 00 01 c5 00 00 00 10 00 00 01 d7 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 00 00 02 0b 00 00 00 10 00 00 02 1d 00 00 00 10 00 00 02 2f 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 00 00 02 63 00 00 00 10 00 00 02 75 00 00 00 10 00 00 02 87 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 00 00 02 bb 00 00 00 10 00 00 02 cd 00 00 00 10 00 00 02 df 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 00 00 03 13 00 00 00 10 00 00 03 25 00 00 00 10 00 00 03 37 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 {seems like a sha256 hash, possibly a hmac}

It does not seem to be encrypted.

Originally posted by @0xmerp in #19 (comment)

How to use

Is the Provisioned CDM Path, using same style with others ?

Thank you

Could not parse license_message as a SignedMessage, Error parsing message

I use the latest version of pywidevine: '1.5.3'
The license request requires the server certificate so I run the code as following and both successful return the license as expected compared to the requested with Web-browser on Desktop or on Android App.

session_id = cdm.open()

#First license request to get cert
response = requests.post(lic_url, headers=headers, data=cdm.service_certificate_challenge)
print("First lic content request to get cert", response.content)

service_cert = response.json()['license']

provider_id = cdm.set_service_certificate(session_id, service_cert)
cdm.set_service_certificate(session_id, service_cert)
challenge = cdm.get_license_challenge(session_id, PSSH(pssh_i), privacy_mode=True)
#second request to get actual license to parse license
licence_res = requests.post(lic_url, headers=headers, data=challenge)
print("The lic to parse:", licence_res.json()['license'])

cdm.parse_license(session_id, licence_res.json()['license'])

Here is the result:

First lic content request to get cert b'{"ec":0,"license":"CAUSxwUKwQIIAxIQFwW5F8wSBIaLBjM6L3cqjBiCtIKSBSKOAjCCAQoCggEBAJntWzsyfateJO/DtiqVtZhSCtW8yzdQPgZFuBTYdrjfQFEEQa2M462xG7iMTnJaXkqeB5UpHVhYQCOn4a8OOKkSeTkwCGELbxWMh4x+Ib/7/up34QGeHleB6KRfRiY9FOYOgFioYHrc4E+shFexN6jWfM3rM3BdmDoh+07svUoQykdJDKR+ql1DghjduvHK3jOS8T1v+2RC/THhv0CwxgTRxLpMlSCkv5fuvWCSmvzu9Vu69WTi0Ods18Vcc6CCuZYSC4NZ7c4kcHCCaA1vZ8bYLErF8xNEkKdO7DevSy8BDFnoKEPiWC8La59dsPxebt9k+9MItHEbzxJQAZyfWgkCAwEAAToUbGljZW5zZS53aWRldmluZS5jb20SgAOuNHMUtag1KX8nE4j7e7jLUnfSSYI83dHaMLkzOVEes8y96gS5RLknwSE0bv296snUE5F+bsF2oQQ4RgpQO8GVK5uk5M4PxL/CCpgIqq9L/NGcHc/N9XTMrCjRtBBBbPneiAQwHL2zNMr80NQJeEI6ZC5UYT3wr8+WykqSSdhV5Cs6cD7xdn9qm9Nta/gr52u/DLpP3lnSq8x2/rZCR7hcQx+8pSJmthn8NpeVQ/ypy727+voOGlXnVaPHvOZV+WRvWCq5z3CqCLl5+Gf2Ogsrf9s2LFvE7NVV2FvKqcWTw4PIV9Sdqrd+QLeFHd/SSZiAjjWyWOddeOrAyhb3BHMEwg2T7eTo/xxvF+YkPj89qPwXCYcOxF+6gjomPwzvofcJOxkJkoMmMzcFBDopvab5tDQsyN9UPLGhGC98X/8z8QSQ+spbJTYLdgFenFoGq47gLwDS6NWYYQSqzE3Udf2W7pzk4ybyG4PHBYV3s4cyzdq8amvtE/sNSdOKReuHpfQ="}'

The lic to parse: U01XVgAAA1MAAACLAiUQCQQRCAMMDwcCCwYFDhASCrWq5aAv9o6CC2z1BG/VsxQAAABgzrgfFeTCUq+nl+rBaFqggUAD8BxGtQm0XRVr6rd6oIlZEJBf3VFBqxPrrcPZTmXzUAatwns7fNMGKSGBNgWl+0InqA4R0EGSHa3ME68M4kHJk6VQ1xiDZZxkpZ4LgALFJwfyqzHnL+nf5ugAA1BeDavncl0a3MwoDX4ILrkRHDuyURjOqmngxNaSNV4GrJrCjbQ8QR/ME0LNMGspeq6UT5nYwA0EUmGuaq735UqZzJS0goVcj0ATXOD3td/jaUFmI8ntIR93vzhfeoI6p5DszxffHJl5kTiU/XsGtWrb0becPYzT0ZOgsj3VPYI7i+mxPXBC/BsTTElQA1S/aZG0GeVuv8yfWMIYi200Wxw6FybdZ2+Z8wv8/j4viuHkrec5HA0tCFUlNMHWWUWiiyi4dET1NutCBzmZeaoI8VjrPqOMYow0m9Kft33tBzeVOC2jEZRlF2nEoi1rdZe7sJqRO7fNLgJpos4wd4fioCMrrMJIudvudOkhsQucXY+lPj2FqMqGZHuYfVOs1OH7jNdpu728BXefUU09rSrltbMCqr4hjZ8/oWVlg/3X40KwsE3sccjMdeylw1Vc3MGxpxaH7eDo3Ln7wNiP94TTIlAC+MtI5NDZWmDkHSG4z8tSvgHgK1ATdrQzPt679iF7V7aDPUabxfo9w0j0C7ZWzONm83Y+y74sg1vg8MpWbjAT+OwPFanhi7xoh1uk2Bc7yvYNik+heDJc26DNwkOnuWRoy8iWJYnqxDjhwESXXror+GvcBmWlSn5/13wn7gXKyI0hAViImBHXwoFE3XntEpyt4NoKGoJOR50s6lH5Y2+Vm7AyQZZKJXkoplP7Zi1QlvjpiHnx1LV64P5n5vERlXw3FY0ya2vtKQV2mj4fGDZurJr/NZQpMnnMD/hOjncFJU+CKFYMRJo8cys88S5iiCSEW3FBIc3bEYo42a3uvPgIql3iLNqb22yXWDGnBWlctqoHbNWD9XtCNsdWZtM8EUc9ATzUUI9VxsT1xQAMNvvgNqA+LQJbMNv7t5Jdujg7AkV9rT9SBUROrmDbpp1anqrQP7o=

Here is the result of parsing the license:

File /usr/local/lib/python3.11/site-packages/pywidevine/cdm.py:392, in Cdm.parse_license(self, session_id, license_message)
    390             raise DecodeError(license_message)
    391     except DecodeError as e:
--> 392         raise InvalidLicenseMessage(f"Could not parse license_message as a SignedMessage, {e}")
    393     license_message = signed_message
    395 if not isinstance(license_message, SignedMessage):

InvalidLicenseMessage: Could not parse license_message as a SignedMessage, Error parsing message

Could you please check this case and suggest me how to solve this problem? Thank you!

Add Rate-limiting to serve API

Is your feature request related to a problem? Please describe.
Users could abuse by spamming requests for all sorts of reasons.

Describe the solution you'd like
Something like Flask-Limiter but for aiohttp.

Describe alternatives you've considered
N/A

Additional context
N/A

PSSH parse containing playready and widevine pssh

eme logger outputs pssh containing both playready and widevine at some sites
currently only the first pssh box is parsed
parse all box and if widevine pssh is found give priority to that one

sample
AAACsnBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAApKSAgAAAQABAIgCPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AHQAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABhAHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQAUgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgBXAFoAYQAzAGUAOQB3ADMAQgA5AGIAWgB2AEIAbAA5AHoAYwB0ACsAaABnAD0APQA8AC8ASwBJAEQAPgA8AEMASABFAEMASwBTAFUATQA+AC8AdQBVAEYARgBwADkAMgAyAE8ARQA9ADwALwBDAEgARQBDAEsAUwBVAE0APgA8AEwAQQBfAFUAUgBMAD4AaAB0AHQAcAA6AC8ALwBwAGwAYQB5AHIAZQBhAGQAeQB3AGUAYgAuAGMAbABhAHIAbwB2AGkAZABlAG8ALgBuAGUAdAAvAHIAaQBnAGgAdABzAG0AYQBuAGEAZwBlAHIALgBhAHMAbQB4ADwALwBMAEEAXwBVAFIATAA+ADwALwBEAEEAVABBAD4APAAvAFcAUgBNAEgARQBBAEQARQBSAD4AAAAASXBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAACkIARIQWZa3e9w3B9bZvBl9zct+hhoDZGxhIghwZS5saWdhMSoCU0QyAA==

Serve API has no way to get more than 1 type of Keys

Describe the bug
Since the context data is cleared once a license message is parsed, you have one change to use /keys endpoint of serve.
Since /keys has no way of returning more than one type of keys, we can never have both Content keys and enc/mac keys.

To Reproduce
Steps to reproduce the behavior:

  1. pywidevine serve <config>
  2. Open a Session
  3. Get a Challenge with /challenge
  4. Provide a License Message with /keys/CONTENT to get content keys
  5. Provide a License Message with /keys/SIGNING to get signing keys

Step 5 here will fail as the context data for that License Message's respective challenge would have been cleared.

Expected behavior
The two types of keys to be returned on the two separate requests.

A good solution would be a /parse endpoint, which you can then get keys from a loaded and parsed
session with /keys. This would also remove unnecessary re-parsing of a challenge in the first place.

Screenshots
N/A

Additional context
N/A

How to pass header?

Hi,

I need to pass user-agent in request header
I tried to create a header.py and added user-agent: Mozilla... as header in header.py

and then import header but its not working...

Does someone know how to pass user-agent? in header request?

send license challenge (assuming a generic license server SDK with no API front)
licence = requests.post("https://...", data=challenge)
licence.raise_for_status()

Support for Python 3.12

Installed PYthon 3.12 on my windows 10. When I try to do pip install pywidevine i get this:

ERROR: Ignored the following versions that require a different python version: 1.0.0 Requires-Python >=3.7,<3.11; 1.0.1 Requires-Python >=3.7,<3.11; 1.1.0 Requires-Python >=3.7,<3.11; 1.1.1 Requires-Python >=3.7,<3.11; 1.2.0 Requires-Python >=3.7,<3.11; 1.2.1 Requires-Python >=3.7,<3.11; 1.3.0 Requires-Python >=3.7,<3.11; 1.3.1 Requires-Python >=3.7,<3.11; 1.4.0 Requires-Python >=3.7,<3.11; 1.4.1 Requires-Python >=3.7,<3.11; 1.4.2 Requires-Python >=3.7,<3.11; 1.4.3 Requires-Python >=3.7,<3.11; 1.4.4 Requires-Python >=3.7,<3.11; 1.5.0 Requires-Python >=3.7,<3.11; 1.5.1 Requires-Python >=3.7,<3.11; 1.5.2 Requires-Python >=3.7,<3.11; 1.5.3 Requires-Python >=3.7,<3.11; 1.6.0 Requires-Python >=3.7,<3.12
ERROR: Could not find a version that satisfies the requirement pywidevine (from versions: none)
ERROR: No matching distribution found for pywidevine

Support for server returning multiple SignedDrmCertificates

Is your feature request related to a problem? Please describe.
I'm trying to fetch key from a server but it fails with "partial decode" error when validating the certificate.

It appears that the server is returning 2 certificates. I managed to decode the message using the following declaration.

message SignedDrmCertificates {
  repeated SignedDrmCertificate drm_certificate = 1;
}

Unfortunately, I don't have enough knowledge about the widevine underlying to tell how this kind of answer should be handled.

Here is a base64 license server response: cert.txt

Headers mismatch

Need help,

i got this error
ERROR:license:[-] Failed to send challenge: [403] {"code":403,"message":"Headers mismatch"}

I think this need header for lic url,
which folder to place headers.py ?

i use pywidevine version 1.8.0

License request from pywidevine different to the one constructed on the device

Describe the bug
License request constructed by pywidevine gets rejected by license server while the device itself plays the video without issue. The situation indicates that the license server has a means to distinguish the license request constructed by pywidevine comparing to chrome on the device.

The request to license server is constructed correctly, otp, token, headers. Confirmed by more users. So started to wonder if there is anything that the license server can distinguish other than the key+blob since these are correct.

To Reproduce
Create Android device in emulator, Pixel 6 Pro, Android 11. Extract keys for CDM version 16.0.0. Construct license challenge for vdocipher license server. Challenge has token that contains otp, playback info, href, tech, and license request. Make a post request.

Expected behavior
A correctly constructed license request that will in request to license server return the license.

Additional context
pywidevine test succeeds.

The context may look like out of concern of pywidevine at first sight. However, the case indicates that the license server can distinguish license request from pywidevine from android 11's chrome. Therefore it should be a concern of pywidevine construct license request that is identical to the browser on the device.

The license request succeeds with 200, but an exception of 'InvalidLicenseMessage' is thrown

I am using a CDM dumped from my Xiaomi device, which works fine for various services. However, for a certain service, I am experiencing the issue mentioned in the title.

I am using disposable bearer tokens for this service, which I can generate on my own and which work properly, so I don't think there is an issue with the headers.

What should I do in this case?
.
.
The following is the response for the license:

'{"service_version_info":{"license_sdk_version":"17.0.1","license_service_version":"DRMtoday"},"supported_tracks":[{"type":"HD","key_id":"Q8eOvWDlSx+MIAvU8K44CQ=="}],"message_type":"LICENSE_REQUEST","status":"OK","license":"CAISiQMKXAogRkI3MTcxNzMwMDAwMDAwMDAxMDAwMDAwMDAwMDAwMDASJgoQPLpOHP3/S2q450lWhjqM/hABMhA6VmJ0Nfyg/h/6+aO8TxPwIAEoADjl/UhA5f1ISNG1rKEGEiIIARABGAAg5f1IKOX9SDDl/Ug4AEIASABQAFgAYAFwAHgAGmYSEAjdFgyJTJzU4FFEUqqU7nIaUG3PpL20gH8pXs5xKXEgFHAHUdNw+G8PHZBrHk9VI4SaPO0nCE2dvs+s72kQ1DlIjM3lEiB2xO8ppwXRYA6lf5CUOiwQptzlxz8+kPG739E7IAEalAEKEEPHjr1g5UsfjCAL1PCuOAkSEKNPXhmgNWGwksn2VEwboZUaIPBguUWx1+7/ys4zHNoI6LcFTecpab8hUvJXrK0i+z5GIAIoATIICAAQKhgAIAA6CAgAECoYACAAQjQKIIwlBSr23yYYmBxNtbfatK3uXtZ82m9ER5Tryl7JnEUxEhD+EYrd5QlDNzh8qhDJrWFIING1rKEGOAAaIA5YaN4hEbbkvgVbzcl+v5hvGMZ63X2eenSLJDoH6sJLIoACpY8LxEV2FRwtalxAnHHBru0zcTG06ZTKKt60bHonCw4LJl/RpNNuepQfdP4Kx7pYY1pobhj45/dPGP8/Zm6IAfnJT3aW0e+nC/e+dKsbE3aDg2NhclpNF7SlAtt+v+vkLpeualRb8AoXyAc5+iiGv1PAMtYxU2vduqg0jpx6nUyCFNllJ8H8BatBMvuvUqgLsfjo43hjuEeq3iscptm4ziJJ4e5Uta5p3ZHvUBVBJBJKZ/divJl22J/VFt5NG/ZSh8jZzw/SA963QS9Hogg3LK94pEn5pJNGTT4TXrNa1NTBRLjSVbkKaHJ4V+QQp3vjSRDnKwpyvPWssE6RkEPH+zoICgYxNy4wLjFAAVgA","platform":"android"}'

The following are the details of the exception:

Could not parse license_message as a SignedMessage, Error parsing message
google.protobuf.message.DecodeError: Error parsing message

During handling of the above exception, another exception occurred:

File "F:\getkeys.py", line 38, in
cdm.parse_license(session_id, licence.content)
pywidevine.exceptions.InvalidLicenseMessage: Could not parse license_message as a SignedMessage, Error parsing message
.
.
Lastly, thank you very much for developing such an amazing program!

Error while using pssh.dumps() on some PSSH

Describe the bug
Error while using .dumps() on some PSSH, for testing used: https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd (AAAARHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAACQIARIBNRoNd2lkZXZpbmVfdGVzdCIKMjAxNV90ZWFycyoCU0Q=), this is only video with that problem as far as I tested

To Reproduce
Steps to reproduce the behavior:

  1. Install pywidevine
  2. Create basic script
from pywidevine.pssh import PSSH

# https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd

pssh = PSSH('AAAARHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAACQIARIBNRoNd2lkZXZpbmVfdGVzdCIKMjAxNV90ZWFycyoCU0Q=')

print(pssh.dumps())
  1. Run it
  2. See error.

Expected behavior
pssh.dumps() works fine

Additional context

Traceback (most recent call last):
  File "test\pssh.py", line 8, in <module>
    print(pssh.dumps())
  File "lib\site-packages\pywidevine\pssh.py", line 240, in dumps     
    return base64.b64encode(self.dump()).decode()
  File "lib\site-packages\pywidevine\pssh.py", line 234, in dump      
    key_IDs=self.key_ids,
  File "lib\site-packages\pywidevine\pssh.py", line 200, in key_ids   
    return [
  File "lib\site-packages\pywidevine\pssh.py", line 202, in <listcomp>
    UUID(bytes=key_id) if len(key_id) == 16 else UUID(hex=key_id.decode())
  File "lib\uuid.py", line 177, in __init__
    raise ValueError('badly formed hexadecimal UUID string')
ValueError: badly formed hexadecimal UUID string

Cant parse license url

I have error but requests response is work but cant find key

pywidevine\cdm.py", line 393, in parse_license
raise InvalidLicenseMessage(f"Could not parse license_message as a SignedMessage, {e}")
pywidevine.exceptions.InvalidLicenseMessage: Could not parse license_message as a SignedMessage, Error parsing message

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0',
    'Accept': '*/*',
    'Accept-Language': 'en',
    # 'Accept-Encoding': 'gzip, deflate, br',
    'Content-Type': 'application/json',
    'Referer': 'https://astrogo.astro.com.my/',
    'cache-control': 'no-cache , no-store',
    'Origin': 'https://astrogo.astro.com.my',
    'Sec-Fetch-Dest': 'empty',
    'Sec-Fetch-Mode': 'cors',
    'Sec-Fetch-Site': 'same-site',
    'authorization': 'Bearer eyJraWQiOiI2OWM2MzRkMi0zNjBiLTQ0YzEtODUwZC05',
    'Connection': 'keep-alive',
}

json_data = {
    'contentID': '601',
    'contentType': 2,
    'authorizationToken': 'AAAAQAAAAKEAAAB',
    'authorizationTokenType': '1',
    'licenseChallenge': 'CAES7SsSIwohCgsiAzYwMUjj3JWl0Hfy5q6ho/eyujdwdo6nYgGsbLgpkoUAAAAAQAAABQABQAQ3hym9dgCxVs=',
    'playbackSessionCookie': None,
}

Add /load or /parse endpoint that returns the full License message

Is your feature request related to a problem? Please describe.
Client code for pywidevine serve functionality currently has no way of getting information like required security level, or other misc information contained within licenses.

Describe the solution you'd like
We should add a /load endpoint that parses a License Response SignedMessage and stores the parsed License message with the keys decrypted in the session.

From there /keys could load the parsed and decrypted License message from the session, and return the keys as JSON. Realistically we don't need /keys endpoint in this scenario as the code should handle the response from /load to get keys.

Describe alternatives you've considered
One alternative to all this is if the client needs that information it could just parse the license message it sent to /keys itself, and then read that information as it isn't protected.

Additional context
In a secure Cdm implementation, this misc information would be required. In fact, returning the keys in plain text at all, or even passing alone a decryption key would not be safe.

Add /service_certificate endpoint for loading Service Certificates

Is your feature request related to a problem? Please describe.
When using /challenge, specifying a service certificate (if you intend to use one) is required on every /challenge call which is simply unnecessary. We shouldn't need to set a service certificate explicitly on each /challenge call. Instead, set it only when you want to set the initial service certificate or change it to another.

Describe the solution you'd like
Have a /service_certificate (or similar) endpoint to call the underlying cdm.set_service_certificate() method instead of doing this within /challenge. This reduces the amount of data that passes through /challenge, reduces the complexity of /challenge, and allows users to only change service certificates when needed.

Describe alternatives you've considered
There's not really any alternative I can think of. This would be something the API schema would need to change.

Additional context
When implementing client code for the API schema, it's quite strange to see that the original set_service_certificate method can be used as-is, considering the service certificate is more so set with /challenge (during get_license_challenge()). It's just not correct.

Various queries.

Hello greetings I have several questions I hope I can solve them by this means.

  • How is it used?
  • I have two files that are blob ID and blob key how can I make it work?
  • How can I configure the files and make it work?
  • It works with L1 will also be compatible?

Auto select device in whitelist?

It would be great if remote cdm can select random device in user's whitelist automatically.
Thus, users no need to provide specific device in api request if they have multiple device key.

Add support for a Remote-Accessible Cdm

Is your feature request related to a problem? Please describe.
You may want to set up your environment for the Python Cdm to be usable by another user, but have the sensitive provision RSA key and Client ID blob kept out of reach.

Currently, there is no option for this sort of scenario without manually altering the Cdm code entirely, or wrapping it in an API (e.g., fastapi or flask).

Describe the solution you'd like
There are a few ways I can do this and I cannot decide between them for the best way.

The first would be to implement an abstracted RemoteCdm class in which developers can implement the different methods get_license_challenge and so on, linking them to their own different API schemas or remote connection methods. This would likely be the easiest on my part, but provide almost no benefit to the actual ender user using this package as they could pretty much already do this if they implement exactly this themselves and ignore type-hint errors.

The second method would be to implement RemoteCdm in full, as a specific API schema. I would have to also add code to serve one of these Remote CDM APIs, perhaps through aiohttp, flask, or such, so that the user can actually create and use a RemoteCdm without doing the guesswork themselves on serving the API. This would all result in almost no work for the user other than needing to deal with using pywidevine to run the API server, and to manage authentication/API tokens.

Describe alternatives you've considered
An alternative method would be to look at making Abstract Device classes rather than Cdm classes. Since the Device class is semantic to the root device keys/access, it would be more ideal. The problem is if it is remote, we shouldn't be providing the Private Keys and such as plain text, but we need to as the Cdm class expects there to be a private_key class instance attribute.

We could move the code that deals with the generation and parsing of license acquisition to the Device class, and therefore abstract it for Local/Remote Device classes, but that reduces the actual workload of the Cdm class to almost nothing. It would end up being more of a proxy to the Device classes' methods, and make using the Cdm class almost pointless. Dealing with storage and fetching of session related data would also be complicated under such a scenario.

Additional context

Add opportunity to generate pssh with default_KID only

First of all thanks for this awesome project and your work.

I am working with some mpd files which only contains a default_KID.
A PSSH for Widevine/Playready is not given.

      <ContentProtection
        schemeIdUri="urn:mpeg:dash:mp4protection:2011"
        value="cenc"
        cenc:default_KID="79F8D3B2-3E0B-D27E-09E0-38435167AE13">
      </ContentProtection>

      <!-- Common Encryption -->
      <ContentProtection
        schemeIdUri="urn:mpeg:dash:mp4protection:2011"
        value="cenc"
        cenc:default_KID="2E7F8229-C1C6-012D-3BAF-8778CBA29C69">
      </ContentProtection>

In the shaka-project there is a fuction to generate a widevine pssh Box only by default_KID's:
https://github.com/shaka-project/shaka-packager/blob/main/packager/tools/pssh/pssh-box.py#L171
The needed proto file is located here: https://github.com/shaka-project/shaka-packager/blob/main/packager/media/base/widevine_pssh_data.proto

I am using it now like this in combination with your project:

    widevine_key_video = "79F8D3B2-3E0B-D27E-09E0-38435167AE13"
    widevine_key_audio = "2E7F8229-C1C6-012D-3BAF-8778CBA29C69"

    def generate_pssh(self):
        import base64
        import struct
        import widevine_pssh_data_pb2 as widevine_pssh_data_pb2
        from pywidevine.pssh import PSSH

        wv = widevine_pssh_data_pb2.WidevinePsshData()
        wv.key_id.extend([base64.b16decode(widevine_key_video).replace('-','')])
        wv.key_id.extend([base64.b16decode(widevine_key_audio).replace('-','')])
        # 'cenc' is the default, so omitted to save bytes.
        wv.protection_scheme = struct.unpack('>L', 'cenc'.encode())[0]
        pssh_box = base64.b64encode(wv.SerializeToString()).decode("utf-8")

        pssh = PSSH(pssh_box)
        
        return pssh.dumps()

Is it be possible to implement the option to generate a pssh only by default_KID's into pywidevine ?

Thanks in advance.

Web UI at / for the Serve API

Is your feature request related to a problem? Please describe.
This would help tremendously with user-experience of quick calls of the CDM through manual means, as well as the possibility to manage and list the devices the user has access to, and perhaps also an admin side to manage permissions as well.

Describe the solution you'd like
A web UI with some fields to make the serve API calls.
So let's say your serve API is hosted at serve.domain.tld:80, you could head to https://serve.domain.tld, input your secret key, then make various calls like opening a session, getting challenges, parsing, and so on.

Describe alternatives you've considered
N/A

Additional context
N/A

How to convert `mspr:pro` to `cenc:pssh`

I only found PlayReady's mspr:pro in the pmd file, how can I convert it to cenc:pssh?

<?xml version='1.0' ?>
<MPD mediaPresentationDuration='PT12M9.033S' minBufferTime='PT2.0S' profiles='urn:mpeg:dash:profile:isoff-on-demand:2011' type='static' xmlns='urn:mpeg:dash:schema:mpd:2011' xmlns:cenc='urn:mpeg:cenc:2013' xmlns:mspr='urn:microsoft:playready'>
<Period>
<AdaptationSet maxHeight='1080' maxWidth='1920' minHeight='1080' minWidth='1920' mimeType='video/mp4' segmentAlignment='true' startWithSAP='1'>
<Representation bandwidth='4848906' codecs='avc1.640028' frameRate='30' height='1080' id='video-avc1' scanType='progressive' width='1920'>
        <ContentProtection cenc:default_KID='c2bc9cc9-1376-400f-8bde-732b9cf519ba' schemeIdUri='urn:mpeg:dash:mp4protection:2011' value='cenc'/>
<ContentProtection schemeIdUri='urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95'><mspr:pro>pAIAAAEAAQCaAjwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4AeQBaAHkAOAB3AG4AWQBUAEQAMABDAEwAMwBuAE0AcgBuAFAAVQBaAHUAZwA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBKAHMAWgBBAEEARgBRAFkANgBsAGMAPQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APABMAEEAXwBVAFIATAA+AGgAdAB0AHAAOgAvAC8AcwB0AHIAZQBhAG0AaQBuAGcAMQAuAHgAYwByAGUAYQBtAC4AbgBlAHQAOgA4ADAAOAAwAC8AcABsAGEAeQByAGUAYQBkAHkALwBSAGkAZwBoAHQAcwBNAGEAbgBhAGcAZQByAC4AYQBzAG0AeAA8AC8ATABBAF8AVQBSAEwAPgA8AC8ARABBAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA==</mspr:pro></ContentProtection>
<ContentProtection schemeIdUri='urn:uuid:EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED'/>
<BaseURL>https://xxxx/xxxxxx/xxxxxx.rb?r=1DyFJbi5cVYEynbTt9w7gzc5TGPmiCjyYcfgym9B5cGIg33LfBpuMrsK0A3p7L7_xWhui4ruzYGMtWvip7T29uOIJUrBEItq09sWjkZXalbYrI4UTLNocnr6Ji1Lw_KPAUfhy-tMmSG4HFcNdRD8FwK3e0PJGDiODg6F8kha8Aj_TXRVaiyh_KKBC2u5kHC0</BaseURL>
        <SegmentBase indexRange='1623-6034'>
          <Initialization range='0-1622'/>
        </SegmentBase>
      </Representation></AdaptationSet>
</Period></MPD>

v0 pssh dump when init_data contain key_id

Describe the bug
Cannot dump v0 pssh when init_data contain key_id

To Reproduce
Steps to reproduce the behavior:

  1. pip install pywidevine
from pywidevine.pssh import PSSH

# v0 pssh not contain key_id
a = 'AAAAR3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAACcIARoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQ='
b = PSSH(a)
print(b.dump().hex())
# dump v0 pssh
# >>000000477073736800000000edef8ba979d64acea3c827dcd51d21ed0000002708011a0d7769646576696e655f746573742210666b6a336c6a61536466616c6b72336a2a024844
print(b.key_ids)
# >>[]
c = PSSH.new(key_ids=None, init_data=b.init_data, version=0, flags=0)
print(c.dump().hex())
# dump v0 pssh
# >>000000477073736800000000edef8ba979d64acea3c827dcd51d21ed0000002708011a0d7769646576696e655f746573742210666b6a336c6a61536466616c6b72336a2a024844
print(c.key_ids)
# >>[]


# v0 pssh contain key_id
d = 'AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA=='
e = PSSH(d)
print(e.dump().hex())
# dump v1 pssh
# >>0000006f7073736801000000edef8ba979d64acea3c827dcd51d21ed00000001eb676abbcb345e96bbcf616630f1a3da0000003b08011210eb676abbcb345e96bbcf616630f1a3da1a0d7769646576696e655f746573742210666b6a336c6a61536466616c6b72336a2a0248443200
print(e.key_ids)
# >>[UUID('eb676abb-cb34-5e96-bbcf-616630f1a3da')]

f = PSSH.new(key_ids=None, init_data=e.init_data, version=0, flags=0)
print(f.dump().hex())
# dump v1 pssh
# >>0000006f7073736801000000edef8ba979d64acea3c827dcd51d21ed00000001eb676abbcb345e96bbcf616630f1a3da0000003b08011210eb676abbcb345e96bbcf616630f1a3da1a0d7769646576696e655f746573742210666b6a336c6a61536466616c6b72336a2a0248443200
print(f.key_ids)
# >>[UUID('eb676abb-cb34-5e96-bbcf-616630f1a3da')]


f.set_key_ids([])
print(f.dump().hex())
# dump v0 pssh but key_id is removed from init_data
# >>000000497073736800000000edef8ba979d64acea3c827dcd51d21ed0000002908011a0d7769646576696e655f746573742210666b6a336c6a61536466616c6b72336a2a0248443200

Expected behavior
all should be dump with v0 pssh

400 error bad request

from pywidevine.cdm import Cdm
from pywidevine.device import Device
from pywidevine.pssh import PSSH
from base64 import b64encode
import base64
import requests
import json
url = "https://cox-mds.az.cox.comcast.net/license"
#requestc = b64encode(challenge)

#response = requests.request("POST", url, headers=headers, data=payload)

prepare pssh

pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsSJDk5NjllYWJjLWE3ZmUtMDk2Yi1jZjQzLWZmNzYwNGUwZjljNSITODI5MTA3NDQwMzczMDYyNDE2Mw==")

load device

device = Device.load("device.wvd")

load cdm

cdm = Cdm.from_device(device)

open cdm session

session_id = cdm.open()

get license challenge

challenge = cdm.get_license_challenge(session_id, pssh)
print(b64encode(challenge))
payload = {"keySystem":"widevine",
"licenseRequest": b64encode(challenge),
"contentMetadata":"","mediaUsage":"stream","accessToken":""}
headers = {
"Accept": "application/vnd.xcal.mds.licenseResponse+json; version=1",
"Accept-Language": "en-US,en;q=0.9",
"Connection": "keep-alive",
"Content-Type": "application/vnd.xcal.mds.licenseRequest+json; version=1",
"Origin": "https://watchtv.cox.com",
"Referer": "https://watchtv.cox.com/",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "cross-site",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36",
"X-MoneyTrace": ""

}

send license challenge (assuming a generic license server SDK with no API front)

licence = requests.post(url, headers=headers, data=payload)
#print(license.text)
licence.raise_for_status()
l = response.json()['license']
print(l)

parse license challenge

cdm.parse_license(session_id, l)

print keys

for key in cdm.get_keys(session_id):
print(f"[{key.type}] {key.kid.hex}:{key.key.hex()}")

close session, disposes of session data

cdm.close(session_id)

this code is giving 400 error if you can correct it

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.