diff --git a/unshackle/core/cdm/decrypt_labs_remote_cdm.py b/unshackle/core/cdm/decrypt_labs_remote_cdm.py index c040362..960b25c 100644 --- a/unshackle/core/cdm/decrypt_labs_remote_cdm.py +++ b/unshackle/core/cdm/decrypt_labs_remote_cdm.py @@ -26,7 +26,9 @@ class DecryptLabsRemoteCDM(RemoteCdm): self.api_session_ids = {} self.license_request = None self.service_name = service_name + self.device_name = device_name self.keys = {} + self.scheme = "L1" if device_name == "L1" else "widevine" try: super().__init__(device_type, system_id, security_level, host, secret, device_name) except Exception: @@ -66,16 +68,49 @@ class DecryptLabsRemoteCDM(RemoteCdm): ) -> bytes: self.pssh = pssh + request_data = { + "init_data": self.pssh.dumps(), + "service_certificate": self.req_session.signed_device_certificate, + "scheme": self.scheme, + "service": self.service_name, + } + # Add required parameter for L1 scheme + if self.scheme == "L1": + request_data["get_cached_keys_if_exists"] = True res = self.session( self.host + "/get-request", - { - "init_data": self.pssh.dumps(), - "service_certificate": self.req_session.signed_device_certificate, - "scheme": "widevine", - "service": self.service_name, - }, + request_data, ) + # Check if we got cached keys instead of a challenge + if res.get("message_type") == "cached-keys": + # Store cached keys directly + if session_id not in self.keys: + self.keys[session_id] = [] + session_keys = self.keys[session_id] + + for cached_key in res.get("cached_keys", []): + # Handle KID format - could be hex string or UUID string + kid_str = cached_key["kid"] + try: + # Try as UUID string first + kid_uuid = UUID(kid_str) + except ValueError: + try: + # Try as hex string (like the existing code) + kid_uuid = UUID(bytes=bytes.fromhex(kid_str)) + except ValueError: + # Fallback: use Key.kid_to_uuid + kid_uuid = Key.kid_to_uuid(kid_str) + + session_keys.append(Key(kid=kid_uuid, type_="CONTENT", key=bytes.fromhex(cached_key["key"]))) + + # Return empty challenge since we already have the keys + self.license_request = "" + self.api_session_ids[session_id] = None + return b"" + + # Normal challenge response self.license_request = res["challenge"] self.api_session_ids[session_id] = res["session_id"] @@ -87,6 +122,10 @@ class DecryptLabsRemoteCDM(RemoteCdm): self.keys[session_id] = [] session_keys = self.keys[session_id] + # If we already have cached keys and no session_id_api, skip processing + if session_id_api is None and session_keys: + return + if isinstance(license_message, dict) and "keys" in license_message: session_keys.extend( [ @@ -96,14 +135,21 @@ class DecryptLabsRemoteCDM(RemoteCdm): ) else: + # Ensure license_message is base64 encoded + if isinstance(license_message, bytes): + license_response_b64 = base64.b64encode(license_message).decode() + elif isinstance(license_message, str): + license_response_b64 = license_message + else: + license_response_b64 = str(license_message) res = self.session( self.host + "/decrypt-response", { "session_id": session_id_api, "init_data": self.pssh.dumps(), "license_request": self.license_request, - "license_response": license_message, - "scheme": "widevine", + "license_response": license_response_b64, + "scheme": self.scheme, }, )