A by product of this change is dropped support for providing a PSSH or init data directly in any form, that includes base64.
You must now provide it as a PSSH object, e.g., `cdm.get_license_challenge(session_id, PSSH("AAAAW3Bzc2...CSEQyAA=="))`
The idea behind this is to simplify the amount of places where parsing of PSSH and Init Data to a minimal amount. The codebase is getting quite annoying with the constant jumps and places where it needs to test for base64 strings, hex strings, bytes, and direct parsed PSSH boxes or WidevinePsshData. That's a ridiculous amount of code just to take in a pssh/init data, especially when the full pssh box will eventually be discarded/unused by the Cdm, as it just cares about the init data.
Client code should pass any PSSH value they get into a PSSH object appropriately, and then store it as such, instead of as a string or bytes. This makes it overall more powerful thanks to the ability to also access the underlying PSSH data more easily with this change.
It also helps to increase contrast between a compliant Widevine Cenc Header or PSSH Box, and arbitrary data (e.g., Netflix WidevineExchange's init data) because of how you initialize the PSSH.
It also allows the user to more accurately trace the underlying final parse of the PSSH value, instead of looking at it being pinged between multiple functions.
RemoteCdm now also sends the PSSH/init_data in full box form now, the serve API will be able to handle both scenarios but in edge cases providing the full box may be the difference between a working License Request and not.
We can clear a repeated field with `del field[:]` and overwrite an entire field with `field[:] = [b"123", b"456"]`. So we can reduce this down to a single call operation.
Some systems like Caddy or Nginx will prefix their own word to the Server header, e.g., `Caddy, pywidevine server v1.2.3` so I had to change a fair bit of the code to have wider compatibility across some unknowns that may occur with the Serve header.
This can be considered the Client-side code for the `serve` feature.
The RemoteCdm object can be used with the same underlying interface as the normal `Cdm` object. Including stuff like .open(), .get_license_challenge(), .decrypt(), even same access to data like `cdm.system_id`, or even `cdm._sessions` just like normal.
However, since we don't have any private key and client ID, we spoof the super construction with dummy data. You wont have access to any data that uses the underlying Client ID and Private Key like the signer or decrypter. Any Cdm code trying to access them on RemoteCdm will fail.
This is so you can construct a Cdm object without using `.wvd` files (nor the Device class). It also improves enforcement of some required data from the Device. The underlying Device object is discarded for it's data as it won't be required.
Note that the Client ID and Private Key related variables are now stored as private `__var` variables to further amplify their private nature and to really discourage manual read write. This is not impossible to workaround in Python but further discourages manual read/writes to the variable that could cause serious issues.
The RSA Key is also no longer stored as-is. It is now stored as PSS and PKCS1_OAEP objects, as they will be used like so. This makes it even more annoying to directly read/write the RSA key (but not impossible).
All client's should implement this and handle the 400 response safely. Under normal circumstances, with good client code, the 400 responses should not happen.
This is necessary to support different Cdm devices per-user. E.g., without this change if you do /open/a_device, you will only ever be able to use `a_device` until the next server restart. Even if you do /open/b_device, it will still use `a_device`, without error or warning.
This is because it stores the device with the Cdm in the previous change from storing the session ids to storing the Cdms instead.
With this change we can now have the user specify which device they are using, which allows us to map that to a Cdm that was initialized with the respective device.
Arguably we could remove the /{device} prefix and instead do a brute check on the app["cdms"] until we find a Cdm with a matching session, but this seems like a more semantic less hacky method to the madness.
(especially since /open already used {device}, but as a postfix)
The Cdm is now stored per-secret due to the Cdm object's session limit. This is so one user (by secret key) cannot overload the server with too many sessions.
But this also fixes it so that the serve API will work for more than just 50 sessions for all users. Otherwise the user pool will eventually overload the Cdm with 50 sessions, even if they close it, it will eventually happen. Think of it like the server being overloaded prematurely.
We may need the signature for external verification, and most APIs require it to be in a SignedMessage to be accepted, even though the SignedMessage is pretty much empty (not even actually signed lol).
This stops users from loading the license twice, which wouldn't do anything wrong, but without doing this context deletion we could possibly end up with a ton of memory that would likely go unused if the same Cdm session is used a lot for a long time.
I've moved the majority of Cdm initialization from /challenge to /open, this is pretty much necessary to have a proper session setup like Cdm now has.
A session setup is required for an API like this to know what cdm to associate user's calls with. The session ID it uses is now the same session ID it actually uses in the Cdm but it's returned to the user as hex. The user is expected to provide it in hex as well.
There's a few benefits to this but the main one being storage for each "request". We can now change Service Certificate per-session for example rather than for the entire Cdm object. In a multi-threaded scenario this can be a necessity more than anything.
The device is the only bit of data left that does not get stored in a session. This is mostly due to myself not seeing it being switched out often and setting it per-session would likely be cumbersome.
Some other small improvements are all around. There's a ton of doc-string improvements, typing improvements, verification of types, and there's now custom Exceptions.
In terms of bug fixes there isn't any I fixed explicitly but a possible issue in decrypt() relating the Key Labels may now be fixed.
I've moved the Keys from the return of parse_license() to the session data, with decrypt() now loading them from the session data instead. This keeps the decryption keys out of the view of the caller but it is by no way impossible to get those keys. It is incredibly trivial to access the session and get the keys from the Cdm manually.
A session limit of 50 is still set by the Cdm.
There's no need for the user to get back the verified DrmCertificate as they could easily get it themselves. Instead return the provider ID which may be more useful to get.