Added
- New CLI command
serve
that hosts a CDM API that can be externally accessed with authentication. This can be used to
access and/or share your CDM without exposing your Widevine device private key, or even it's identity by enforcing
Privacy Mode.- Requires installing with the
serve
extras, i.e.,pip install pywidevine[serve]
. - The default host of
127.0.0.1
blocks access outside your network, even if port-forwarded. Use
-h 0.0.0.0
to allow remote access. - Setup requires the use of a config file for configuring the CDM and authentication. An example config file named
serve.example.yml
in the project root folder has verbose documentation on available options.
- Requires installing with the
- Batch migration of WVD files by passing a folder as the path to the CLI command
migrate
. - Strict mode to
PSSH.get_as_box()
to raise an Exception if passed data is not already a box, as it has been improved
to create a new box if not detected as a box already.
Changed
- Elevated the Development Status Classifier from 4 (Beta) to 5 (Production/Stable).
- License messages passed to
Cdm.parse_license()
are now rejected if they are not ofLICENSE
type. - Service Certificates passed to
Cdm.set_service_certificate()
are now verified. This patches a trivial "exploit"
that allows an attacker to recover the plaintext Client ID from a license under Privacy Mode. See
https://gist.github.com/rlaphoenix/74acabdd7269a21845e18b621c5860ef. - Data passed to
PSSH.get_as_box()
now supports arbitrary and box data automatically as it tries to detect if it is a
valid box, otherwise makes a new box. - Renamed the
Cdm
constructor's parameterpssh
toinit_data
, as that's what the Cdm actually wants and uses,
whereas aPSSH
is anmp4
atom (aka box) containinginit_data
(a Widevine CENC Header). The full PSSH is never
kept nor ever used. It still accepts PSSH box data. - Service Certificate's Provider ID is now returned by
Cdm.set_service_certificate()
instead of the passed
certificate, of which they would already have. - The Cdm class now works more closely to the official CDM model. Instead of using one Cdm object per-request having to
provide device information each time,- You now initialize the Cdm with the Widevine device you wish to use and then open sessions with
Cdm.open()
. - You will receive a session ID that are then passed to other methods of the same Cdm object.
- The PSSH/init_data that used to be passed to the constructor is now passed to
Cdm.get_license_challenge()
. - This allows initializing one Cdm object with up to 50 sessions open at the same time.
Session limits seem to fluctuate between libraries and devices. 50 seems like a permissive value. - Once you are finished with DRM operations, discard all session (and key) data by calling
Cdm.close(session_id)
.
- You now initialize the Cdm with the Widevine device you wish to use and then open sessions with
- License Keys are no longer returned by
Cdm.parse_license()
and now must be obtained directly fromcdm._sessions
.- For example,
for key in cdm._sessions[session_id].keys: print(f"[{key.type}] {key.kid.hex}:{key.key.hex()}")
. - This is to detach the action of parsing a license as just for getting keys, as it isn't. It can be and should be
used for a lot more data like security requirements like HDCP, expiration, and more. - It is also to detour users from directly using the keys over the
Cdm.decrypt()
method.
- For example,
- Various std-lib exceptions have been replaced with custom exceptions under
pywidevine.exceptions
. - License responses can now only be parsed once by
Cdm.parse_license()
. Any further attempts will raise an
InvalidContext
exception.- This is as license context data is cleared once used to reduce data lingering in memory, otherwise the more license
requests you make without closing the session, the more and more memory is taken up. - Open multiple sessions in the same Cdm object if you need to request and parse multiple licenses on the same device.
- This is as license context data is cleared once used to reduce data lingering in memory, otherwise the more license
Removed
- Direct
DrmCertificate
s are no longer supported byCdm.set_service_certificate()
as they have no signature.
See the 3rd Change above. Provide either aSignedDrmCertificate
or aSignedMessage
containing a
SignedDrmCertificate
. ASignedMessage
containing aDrmCertificate
will also be rejected. PSSH.from_init_data()
, usePSSH.get_as_box()
.raw
parameter ofCdm
constructor, as well as CLI commands as it is now handled upstream by thePSSH
creation.
Fixed
- Detection of Widevine CENC Header data encoded as bytes in
PSSH.get_as_box()
. - Custom ValueError on missing contexts instead of the generic KeyError in
Cdm.parse_license()
. - Typing of
type_
parameter inCdm.get_license_challenge()
. - Value of
type_
parameter if is a string inCdm.get_license_challenge()
.