mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2025-10-23 15:11:08 +00:00
fix: Resolve service name transmission and vault case sensitivity issues
Fixed DecryptLabsRemoteCDM sending 'generic' instead of proper service names and added case-insensitive vault lookups for SQLite/MySQL vaults. Also added local vault integration to DecryptLabsRemoteCDM
This commit is contained in:
@@ -353,17 +353,19 @@ class DecryptLabsRemoteCDM:
|
|||||||
Generate a license challenge using Decrypt Labs API with intelligent caching.
|
Generate a license challenge using Decrypt Labs API with intelligent caching.
|
||||||
|
|
||||||
This method implements smart caching logic that:
|
This method implements smart caching logic that:
|
||||||
1. First attempts to retrieve cached keys from the API
|
1. First checks local vaults for required keys
|
||||||
2. If required KIDs are set, compares cached keys against requirements
|
2. Attempts to retrieve cached keys from the API
|
||||||
3. Only makes a license request if keys are missing
|
3. If required KIDs are set, compares available keys (vault + cached) against requirements
|
||||||
4. Returns empty challenge if all required keys are cached
|
4. Only makes a license request if keys are missing
|
||||||
|
5. Returns empty challenge if all required keys are available
|
||||||
|
|
||||||
The intelligent caching works as follows:
|
The intelligent caching works as follows:
|
||||||
|
- Local vaults: Always checked first if available
|
||||||
- For L1/L2 devices: Always prioritizes cached keys (API automatically optimizes)
|
- For L1/L2 devices: Always prioritizes cached keys (API automatically optimizes)
|
||||||
- For other devices: Uses cache retry logic based on session state
|
- For other devices: Uses cache retry logic based on session state
|
||||||
- With required KIDs set: Only requests license for missing keys
|
- With required KIDs set: Only requests license for missing keys
|
||||||
- Without required KIDs: Returns any available cached keys
|
- Without required KIDs: Returns any available cached keys
|
||||||
- For PlayReady: Combines cached keys with license keys seamlessly
|
- For PlayReady: Combines vault, cached, and license keys seamlessly
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
session_id: Session identifier
|
session_id: Session identifier
|
||||||
@@ -372,7 +374,7 @@ class DecryptLabsRemoteCDM:
|
|||||||
privacy_mode: Whether to use privacy mode - for compatibility only
|
privacy_mode: Whether to use privacy mode - for compatibility only
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
License challenge as bytes, or empty bytes if cached keys satisfy requirements
|
License challenge as bytes, or empty bytes if available keys satisfy requirements
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
InvalidSession: If session ID is invalid
|
InvalidSession: If session ID is invalid
|
||||||
@@ -381,6 +383,7 @@ class DecryptLabsRemoteCDM:
|
|||||||
Note:
|
Note:
|
||||||
Call set_required_kids() before this method for optimal caching behavior.
|
Call set_required_kids() before this method for optimal caching behavior.
|
||||||
L1/L2 devices automatically use cached keys when available per API design.
|
L1/L2 devices automatically use cached keys when available per API design.
|
||||||
|
Local vault keys are always checked first when vaults are available.
|
||||||
"""
|
"""
|
||||||
_ = license_type, privacy_mode
|
_ = license_type, privacy_mode
|
||||||
|
|
||||||
@@ -393,6 +396,31 @@ class DecryptLabsRemoteCDM:
|
|||||||
init_data = self._get_init_data_from_pssh(pssh_or_wrm)
|
init_data = self._get_init_data_from_pssh(pssh_or_wrm)
|
||||||
already_tried_cache = session.get("tried_cache", False)
|
already_tried_cache = session.get("tried_cache", False)
|
||||||
|
|
||||||
|
if self.vaults and self._required_kids:
|
||||||
|
vault_keys = []
|
||||||
|
for kid_str in self._required_kids:
|
||||||
|
try:
|
||||||
|
clean_kid = kid_str.replace("-", "")
|
||||||
|
if len(clean_kid) == 32:
|
||||||
|
kid_uuid = UUID(hex=clean_kid)
|
||||||
|
else:
|
||||||
|
kid_uuid = UUID(hex=clean_kid.ljust(32, "0"))
|
||||||
|
key, _ = self.vaults.get_key(kid_uuid)
|
||||||
|
if key and key.count("0") != len(key):
|
||||||
|
vault_keys.append({"kid": kid_str, "key": key, "type": "CONTENT"})
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if vault_keys:
|
||||||
|
vault_kids = set(k["kid"] for k in vault_keys)
|
||||||
|
required_kids = set(self._required_kids)
|
||||||
|
|
||||||
|
if required_kids.issubset(vault_kids):
|
||||||
|
session["keys"] = vault_keys
|
||||||
|
return b""
|
||||||
|
else:
|
||||||
|
session["vault_keys"] = vault_keys
|
||||||
|
|
||||||
if self.device_name in ["L1", "L2"]:
|
if self.device_name in ["L1", "L2"]:
|
||||||
get_cached_keys = True
|
get_cached_keys = True
|
||||||
else:
|
else:
|
||||||
@@ -404,7 +432,7 @@ class DecryptLabsRemoteCDM:
|
|||||||
"get_cached_keys_if_exists": get_cached_keys,
|
"get_cached_keys_if_exists": get_cached_keys,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.device_name in ["L1", "L2", "SL2", "SL3"] and self.service_name:
|
if self.service_name:
|
||||||
request_data["service"] = self.service_name
|
request_data["service"] = self.service_name
|
||||||
|
|
||||||
if session["service_certificate"]:
|
if session["service_certificate"]:
|
||||||
@@ -441,17 +469,22 @@ class DecryptLabsRemoteCDM:
|
|||||||
"""
|
"""
|
||||||
cached_keys = data.get("cached_keys", [])
|
cached_keys = data.get("cached_keys", [])
|
||||||
parsed_keys = self._parse_cached_keys(cached_keys)
|
parsed_keys = self._parse_cached_keys(cached_keys)
|
||||||
session["keys"] = parsed_keys
|
|
||||||
|
all_available_keys = list(parsed_keys)
|
||||||
|
if "vault_keys" in session:
|
||||||
|
all_available_keys.extend(session["vault_keys"])
|
||||||
|
|
||||||
|
session["keys"] = all_available_keys
|
||||||
session["tried_cache"] = True
|
session["tried_cache"] = True
|
||||||
|
|
||||||
if self._required_kids:
|
if self._required_kids:
|
||||||
cached_kids = set()
|
available_kids = set()
|
||||||
for key in parsed_keys:
|
for key in all_available_keys:
|
||||||
if isinstance(key, dict) and "kid" in key:
|
if isinstance(key, dict) and "kid" in key:
|
||||||
cached_kids.add(key["kid"].replace("-", "").lower())
|
available_kids.add(key["kid"].replace("-", "").lower())
|
||||||
|
|
||||||
required_kids = set(self._required_kids)
|
required_kids = set(self._required_kids)
|
||||||
missing_kids = required_kids - cached_kids
|
missing_kids = required_kids - available_kids
|
||||||
|
|
||||||
if missing_kids:
|
if missing_kids:
|
||||||
session["cached_keys"] = parsed_keys
|
session["cached_keys"] = parsed_keys
|
||||||
@@ -585,51 +618,62 @@ class DecryptLabsRemoteCDM:
|
|||||||
|
|
||||||
license_keys = self._parse_keys_response(data)
|
license_keys = self._parse_keys_response(data)
|
||||||
|
|
||||||
if self.is_playready and "cached_keys" in session:
|
all_keys = []
|
||||||
"""
|
|
||||||
Combine cached keys with license keys for PlayReady content.
|
|
||||||
|
|
||||||
This ensures we have both the cached keys (obtained earlier) and
|
if "vault_keys" in session:
|
||||||
any additional keys from the license response, without duplicates.
|
all_keys.extend(session["vault_keys"])
|
||||||
"""
|
|
||||||
|
if "cached_keys" in session:
|
||||||
cached_keys = session.get("cached_keys", [])
|
cached_keys = session.get("cached_keys", [])
|
||||||
all_keys = list(cached_keys)
|
for cached_key in cached_keys:
|
||||||
|
all_keys.append(cached_key)
|
||||||
|
|
||||||
for license_key in license_keys:
|
for license_key in license_keys:
|
||||||
already_exists = False
|
already_exists = False
|
||||||
license_kid = None
|
license_kid = None
|
||||||
if isinstance(license_key, dict) and "kid" in license_key:
|
if isinstance(license_key, dict) and "kid" in license_key:
|
||||||
license_kid = license_key["kid"].replace("-", "").lower()
|
license_kid = license_key["kid"].replace("-", "").lower()
|
||||||
elif hasattr(license_key, "kid"):
|
elif hasattr(license_key, "kid"):
|
||||||
license_kid = str(license_key.kid).replace("-", "").lower()
|
license_kid = str(license_key.kid).replace("-", "").lower()
|
||||||
elif hasattr(license_key, "key_id"):
|
elif hasattr(license_key, "key_id"):
|
||||||
license_kid = str(license_key.key_id).replace("-", "").lower()
|
license_kid = str(license_key.key_id).replace("-", "").lower()
|
||||||
|
|
||||||
if license_kid:
|
if license_kid:
|
||||||
for cached_key in cached_keys:
|
for existing_key in all_keys:
|
||||||
cached_kid = None
|
existing_kid = None
|
||||||
if isinstance(cached_key, dict) and "kid" in cached_key:
|
if isinstance(existing_key, dict) and "kid" in existing_key:
|
||||||
cached_kid = cached_key["kid"].replace("-", "").lower()
|
existing_kid = existing_key["kid"].replace("-", "").lower()
|
||||||
elif hasattr(cached_key, "kid"):
|
elif hasattr(existing_key, "kid"):
|
||||||
cached_kid = str(cached_key.kid).replace("-", "").lower()
|
existing_kid = str(existing_key.kid).replace("-", "").lower()
|
||||||
elif hasattr(cached_key, "key_id"):
|
elif hasattr(existing_key, "key_id"):
|
||||||
cached_kid = str(cached_key.key_id).replace("-", "").lower()
|
existing_kid = str(existing_key.key_id).replace("-", "").lower()
|
||||||
|
|
||||||
if cached_kid == license_kid:
|
if existing_kid == license_kid:
|
||||||
already_exists = True
|
already_exists = True
|
||||||
break
|
break
|
||||||
|
|
||||||
if not already_exists:
|
if not already_exists:
|
||||||
all_keys.append(license_key)
|
all_keys.append(license_key)
|
||||||
|
|
||||||
session["keys"] = all_keys
|
session["keys"] = all_keys
|
||||||
session["cached_keys"] = None
|
session.pop("cached_keys", None)
|
||||||
else:
|
session.pop("vault_keys", None)
|
||||||
session["keys"] = license_keys
|
|
||||||
|
|
||||||
if self.vaults and session["keys"]:
|
if self.vaults and session["keys"]:
|
||||||
key_dict = {UUID(hex=key["kid"]): key["key"] for key in session["keys"] if key["type"] == "CONTENT"}
|
key_dict = {}
|
||||||
self.vaults.add_keys(key_dict)
|
for key in session["keys"]:
|
||||||
|
if key["type"] == "CONTENT":
|
||||||
|
try:
|
||||||
|
clean_kid = key["kid"].replace("-", "")
|
||||||
|
if len(clean_kid) == 32:
|
||||||
|
kid_uuid = UUID(hex=clean_kid)
|
||||||
|
else:
|
||||||
|
kid_uuid = UUID(hex=clean_kid.ljust(32, "0"))
|
||||||
|
key_dict[kid_uuid] = key["key"]
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
continue
|
||||||
|
if key_dict:
|
||||||
|
self.vaults.add_keys(key_dict)
|
||||||
|
|
||||||
def get_keys(self, session_id: bytes, type_: Optional[str] = None) -> List[Key]:
|
def get_keys(self, session_id: bytes, type_: Optional[str] = None) -> List[Key]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -28,26 +28,33 @@ class MySQL(Vault):
|
|||||||
raise PermissionError(f"MySQL vault {self.slug} has no SELECT permission.")
|
raise PermissionError(f"MySQL vault {self.slug} has no SELECT permission.")
|
||||||
|
|
||||||
def get_key(self, kid: Union[UUID, str], service: str) -> Optional[str]:
|
def get_key(self, kid: Union[UUID, str], service: str) -> Optional[str]:
|
||||||
if not self.has_table(service):
|
|
||||||
# no table, no key, simple
|
|
||||||
return None
|
|
||||||
|
|
||||||
if isinstance(kid, UUID):
|
if isinstance(kid, UUID):
|
||||||
kid = kid.hex
|
kid = kid.hex
|
||||||
|
|
||||||
|
service_variants = [service]
|
||||||
|
if service != service.lower():
|
||||||
|
service_variants.append(service.lower())
|
||||||
|
if service != service.upper():
|
||||||
|
service_variants.append(service.upper())
|
||||||
|
|
||||||
conn = self.conn_factory.get()
|
conn = self.conn_factory.get()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cursor.execute(
|
for service_name in service_variants:
|
||||||
# TODO: SQL injection risk
|
if not self.has_table(service_name):
|
||||||
f"SELECT `id`, `key_` FROM `{service}` WHERE `kid`=%s AND `key_`!=%s",
|
continue
|
||||||
(kid, "0" * 32),
|
|
||||||
)
|
cursor.execute(
|
||||||
cek = cursor.fetchone()
|
# TODO: SQL injection risk
|
||||||
if not cek:
|
f"SELECT `id`, `key_` FROM `{service_name}` WHERE `kid`=%s AND `key_`!=%s",
|
||||||
return None
|
(kid, "0" * 32),
|
||||||
return cek["key_"]
|
)
|
||||||
|
cek = cursor.fetchone()
|
||||||
|
if cek:
|
||||||
|
return cek["key_"]
|
||||||
|
|
||||||
|
return None
|
||||||
finally:
|
finally:
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
|
||||||
|
|||||||
@@ -19,22 +19,30 @@ class SQLite(Vault):
|
|||||||
self.conn_factory = ConnectionFactory(self.path)
|
self.conn_factory = ConnectionFactory(self.path)
|
||||||
|
|
||||||
def get_key(self, kid: Union[UUID, str], service: str) -> Optional[str]:
|
def get_key(self, kid: Union[UUID, str], service: str) -> Optional[str]:
|
||||||
if not self.has_table(service):
|
|
||||||
# no table, no key, simple
|
|
||||||
return None
|
|
||||||
|
|
||||||
if isinstance(kid, UUID):
|
if isinstance(kid, UUID):
|
||||||
kid = kid.hex
|
kid = kid.hex
|
||||||
|
|
||||||
conn = self.conn_factory.get()
|
conn = self.conn_factory.get()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Try both the original service name and lowercase version to handle case sensitivity issues
|
||||||
|
service_variants = [service]
|
||||||
|
if service != service.lower():
|
||||||
|
service_variants.append(service.lower())
|
||||||
|
if service != service.upper():
|
||||||
|
service_variants.append(service.upper())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cursor.execute(f"SELECT `id`, `key_` FROM `{service}` WHERE `kid`=? AND `key_`!=?", (kid, "0" * 32))
|
for service_name in service_variants:
|
||||||
cek = cursor.fetchone()
|
if not self.has_table(service_name):
|
||||||
if not cek:
|
continue
|
||||||
return None
|
|
||||||
return cek[1]
|
cursor.execute(f"SELECT `id`, `key_` FROM `{service_name}` WHERE `kid`=? AND `key_`!=?", (kid, "0" * 32))
|
||||||
|
cek = cursor.fetchone()
|
||||||
|
if cek:
|
||||||
|
return cek[1]
|
||||||
|
|
||||||
|
return None
|
||||||
finally:
|
finally:
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user