2024-10-27 18:39:09 +00:00
|
|
|
|
import base64
|
|
|
|
|
import json
|
|
|
|
|
import logging
|
|
|
|
|
|
2024-11-03 13:03:04 +00:00
|
|
|
|
from json.encoder import encode_basestring_ascii
|
2024-10-27 18:39:09 +00:00
|
|
|
|
from typing import Literal
|
|
|
|
|
from uuid import UUID
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
2025-01-19 13:13:07 +00:00
|
|
|
|
def bytes2int(value: bytes, byteorder: Literal["big", "little"] = "big", signed: bool = False) -> int:
|
2024-10-27 18:39:09 +00:00
|
|
|
|
"""
|
2025-01-19 13:13:07 +00:00
|
|
|
|
Convert a byte sequence to an integer.
|
2024-10-27 18:39:09 +00:00
|
|
|
|
|
2025-01-19 13:13:07 +00:00
|
|
|
|
Parameters:
|
2024-10-27 18:39:09 +00:00
|
|
|
|
value (bytes): The byte sequence to convert.
|
2025-01-19 13:13:07 +00:00
|
|
|
|
byteorder (str, optional): Byte order for conversion. 'big' or 'little'. Defaults to 'big'.
|
|
|
|
|
signed (bool, optional): Whether the integer is signed. Defaults to False.
|
2024-10-27 18:39:09 +00:00
|
|
|
|
|
|
|
|
|
Returns:
|
2025-01-19 13:13:07 +00:00
|
|
|
|
int: The integer representation of the byte sequence.
|
2024-10-27 18:39:09 +00:00
|
|
|
|
"""
|
|
|
|
|
return int.from_bytes(value, byteorder=byteorder, signed=signed)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Keybox:
|
|
|
|
|
"""
|
|
|
|
|
The Keybox class handles the storage and management of device IDs and keybox data.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
"""
|
2025-01-19 13:13:07 +00:00
|
|
|
|
Initializes the Keybox object, setting up logger and containers for device IDs and keyboxes.
|
2024-10-27 18:39:09 +00:00
|
|
|
|
"""
|
|
|
|
|
self.logger = logging.getLogger(self.__class__.__name__)
|
|
|
|
|
# https://github.com/kaltura/kaltura-device-info-android/blob/master/app/src/main/java/com/kaltura/kalturadeviceinfo/MainActivity.java#L203
|
|
|
|
|
self.device_id = []
|
|
|
|
|
self.keybox = {}
|
|
|
|
|
|
|
|
|
|
def set_device_id(self, data: bytes) -> None:
|
|
|
|
|
"""
|
|
|
|
|
Set the device ID from the provided data.
|
|
|
|
|
|
2025-01-19 13:13:07 +00:00
|
|
|
|
Parameters:
|
2024-10-27 18:39:09 +00:00
|
|
|
|
data (bytes): The device ID, expected to be 32 bytes long.
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
AssertionError: If the data length is not 32 bytes.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
size = len(data)
|
2025-01-19 13:13:07 +00:00
|
|
|
|
# Ensure the device ID is exactly 32 bytes long
|
|
|
|
|
assert size == 32, f"Invalid device ID length: {size}. Should be 32 bytes"
|
2024-10-27 18:39:09 +00:00
|
|
|
|
|
2025-01-19 13:13:07 +00:00
|
|
|
|
# Add device ID to the list if it's not already present
|
2024-10-27 18:39:09 +00:00
|
|
|
|
if data not in self.device_id:
|
2025-01-19 13:13:07 +00:00
|
|
|
|
self.logger.info("Receive device id: \n\n%s\n", encode_basestring_ascii(data.decode("utf-8")))
|
2024-10-27 18:39:09 +00:00
|
|
|
|
self.device_id.append(data)
|
2025-01-19 13:13:07 +00:00
|
|
|
|
|
2024-10-27 18:39:09 +00:00
|
|
|
|
except Exception as e:
|
2025-01-19 13:13:07 +00:00
|
|
|
|
self.logger.debug("Failed to set device id: %s", e)
|
2024-10-27 18:39:09 +00:00
|
|
|
|
|
|
|
|
|
def set_keybox(self, data: bytes) -> None:
|
|
|
|
|
"""
|
|
|
|
|
Set the keybox from the provided data.
|
|
|
|
|
|
2025-01-19 13:13:07 +00:00
|
|
|
|
Parameters:
|
2024-10-27 18:39:09 +00:00
|
|
|
|
data (bytes): The keybox data, expected to be either 128 or 132 bytes long.
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
AssertionError: If the data length is not 128 or 132 bytes or does not meet other criteria.
|
|
|
|
|
"""
|
2024-11-03 13:03:04 +00:00
|
|
|
|
# https://github.com/zybpp/Python/tree/master/Python/keybox
|
2024-10-27 18:39:09 +00:00
|
|
|
|
try:
|
|
|
|
|
size = len(data)
|
2025-01-19 13:13:07 +00:00
|
|
|
|
# Validate the keybox size (128 or 132 bytes)
|
|
|
|
|
assert size in (128, 132), f"Invalid keybox length: {size}. Should be 128 or 132 bytes"
|
2024-10-27 18:39:09 +00:00
|
|
|
|
|
2025-01-19 13:13:07 +00:00
|
|
|
|
# Validate the QSEE-style keybox end
|
|
|
|
|
assert size == 128 or data[128:132] == b"LVL1", "QSEE-style keybox must end with bytes 'LVL1'"
|
2024-10-27 18:39:09 +00:00
|
|
|
|
|
2025-01-19 13:13:07 +00:00
|
|
|
|
# Validate the keybox magic (should be 'kbox')
|
|
|
|
|
assert data[120:124] == b"kbox", "Invalid keybox magic"
|
2024-10-27 18:39:09 +00:00
|
|
|
|
|
2025-01-19 13:13:07 +00:00
|
|
|
|
device_id = data[0:32] # Extract the device ID from the first 32 bytes
|
2024-11-03 13:03:04 +00:00
|
|
|
|
|
2025-01-19 13:13:07 +00:00
|
|
|
|
# Retrieve and log the structured keybox information
|
2024-11-03 13:03:04 +00:00
|
|
|
|
infos = self.__keybox_info(data)
|
2025-01-19 13:13:07 +00:00
|
|
|
|
encrypted = infos["flags"] > 10 # Check if the keybox is encrypted
|
|
|
|
|
self.set_device_id(data=device_id) # Set the device ID
|
2024-10-27 18:39:09 +00:00
|
|
|
|
|
2025-01-19 13:13:07 +00:00
|
|
|
|
# Log and store the keybox data if it's a new keybox or the device ID is updated
|
2024-11-03 13:03:04 +00:00
|
|
|
|
if (device_id in self.keybox and self.keybox[device_id] != (data, encrypted)) or device_id not in self.keybox:
|
2025-01-19 13:13:07 +00:00
|
|
|
|
self.logger.info("Receive keybox: \n\n%s\n", json.dumps(infos, indent=2))
|
2024-10-28 20:30:42 +00:00
|
|
|
|
|
2025-01-19 13:13:07 +00:00
|
|
|
|
# Warn if keybox is encrypted and interception of plaintext device token is needed
|
2024-11-03 13:03:04 +00:00
|
|
|
|
if encrypted:
|
2025-01-19 13:13:07 +00:00
|
|
|
|
self.logger.warning("Keybox contains encrypted data. Interception of plaintext device token is needed")
|
2024-10-27 18:39:09 +00:00
|
|
|
|
|
2025-01-19 13:13:07 +00:00
|
|
|
|
# Store the keybox (encrypted or not) for the device ID
|
2024-11-03 13:03:04 +00:00
|
|
|
|
if (device_id in self.keybox and not encrypted) or device_id not in self.keybox:
|
|
|
|
|
self.keybox[device_id] = (data, encrypted)
|
2024-10-27 18:39:09 +00:00
|
|
|
|
except Exception as e:
|
2025-01-19 13:13:07 +00:00
|
|
|
|
self.logger.debug("Failed to set keybox: %s", e)
|
2024-10-27 18:39:09 +00:00
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def __keybox_info(data: bytes) -> dict:
|
|
|
|
|
"""
|
|
|
|
|
Extract keybox information from the provided data.
|
|
|
|
|
|
2025-01-19 13:13:07 +00:00
|
|
|
|
Parameters:
|
2024-10-27 18:39:09 +00:00
|
|
|
|
data (bytes): The keybox data.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
dict: A dictionary containing extracted keybox information.
|
|
|
|
|
"""
|
2024-10-28 20:30:42 +00:00
|
|
|
|
# https://github.com/wvdumper/dumper/blob/main/Helpers/Keybox.py#L51
|
2025-01-19 13:13:07 +00:00
|
|
|
|
|
|
|
|
|
# Extract device-specific information from the keybox data
|
2024-11-03 13:03:04 +00:00
|
|
|
|
device_token = data[48:120]
|
2025-01-19 13:13:07 +00:00
|
|
|
|
|
|
|
|
|
# Prepare the keybox content dictionary
|
2024-10-27 18:39:09 +00:00
|
|
|
|
content = {
|
2025-01-19 13:13:07 +00:00
|
|
|
|
"device_id": data[0:32].decode("utf-8"), # Device's unique identifier (32 bytes)
|
|
|
|
|
"device_key": data[32:48], # Device cryptographic key (16 bytes)
|
|
|
|
|
"device_token": device_token, # Token for device authentication (72 bytes)
|
|
|
|
|
"keybox_tag": data[120:124].decode("utf-8"), # Magic tag (4 bytes)
|
|
|
|
|
"crc32": bytes2int(data[124:128]), # CRC32 checksum (4 bytes)
|
|
|
|
|
"level_tag": data[128:132].decode("utf-8") or None, # Optional level tag (4 bytes)
|
|
|
|
|
|
|
|
|
|
# Extract metadata from the device token (Bytes 48–120)
|
|
|
|
|
"flags": bytes2int(device_token[0:4]), # Device flags (4 bytes)
|
|
|
|
|
"system_id": bytes2int(device_token[4:8]), # System identifier (4 bytes)
|
|
|
|
|
"provisioning_id": UUID(bytes_le=device_token[8:24]), # Provisioning UUID (16 bytes)
|
|
|
|
|
"encrypted_bits": device_token[24:72] # Encrypted device-specific information (48 bytes)
|
2024-10-27 18:39:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
2025-01-19 13:13:07 +00:00
|
|
|
|
# https://github.com/ThatNotEasy/Parser-DRM/blob/main/modules/widevine.py#L84
|
|
|
|
|
# TODO: decrypt device token value
|
|
|
|
|
|
|
|
|
|
# Encode bytes as base64 and convert UUIDs to string
|
2024-10-27 18:39:09 +00:00
|
|
|
|
return {
|
2025-01-19 13:13:07 +00:00
|
|
|
|
k: base64.b64encode(v).decode("utf-8") if isinstance(v, bytes) else str(v) if isinstance(v, UUID) else v
|
2024-10-27 18:39:09 +00:00
|
|
|
|
for k, v in content.items()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def export(self, parent: Path) -> bool:
|
|
|
|
|
"""
|
|
|
|
|
Export the keybox data to a file in the specified parent directory.
|
|
|
|
|
|
2025-01-19 13:13:07 +00:00
|
|
|
|
Parameters:
|
2024-10-27 18:39:09 +00:00
|
|
|
|
parent (Path): The parent directory where the keybox data will be saved.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
bool: True if any keybox were exported, otherwise False.
|
|
|
|
|
"""
|
2025-01-19 13:13:07 +00:00
|
|
|
|
# Find matching keyboxes based on the device_id
|
2024-10-27 18:39:09 +00:00
|
|
|
|
keys = self.device_id & self.keybox.keys()
|
2025-01-19 13:13:07 +00:00
|
|
|
|
|
2024-10-27 18:39:09 +00:00
|
|
|
|
for k in keys:
|
2025-01-19 13:13:07 +00:00
|
|
|
|
# Create the parent directory if it doesn't exist
|
2024-10-27 18:39:09 +00:00
|
|
|
|
parent.mkdir(parents=True, exist_ok=True)
|
2025-01-19 13:13:07 +00:00
|
|
|
|
|
|
|
|
|
# Define the export file path and extension (encrypted or binary)
|
|
|
|
|
path_keybox_bin = parent / ("keybox." + ("enc" if self.keybox[k][1] else "bin"))
|
|
|
|
|
|
|
|
|
|
# Write the keybox data to the file
|
2024-11-03 13:03:04 +00:00
|
|
|
|
path_keybox_bin.write_bytes(self.keybox[k][0])
|
2024-10-27 18:39:09 +00:00
|
|
|
|
|
2025-01-19 13:13:07 +00:00
|
|
|
|
# Log export status based on whether the keybox is encrypted
|
2024-11-03 13:03:04 +00:00
|
|
|
|
if self.keybox[k][1]:
|
2025-01-19 13:13:07 +00:00
|
|
|
|
self.logger.warning("Exported encrypted keybox: %s", path_keybox_bin)
|
2024-11-03 13:03:04 +00:00
|
|
|
|
else:
|
2025-01-19 13:13:07 +00:00
|
|
|
|
self.logger.info("Exported keybox: %s", path_keybox_bin)
|
|
|
|
|
|
|
|
|
|
# Return True if any keyboxes were exported, otherwise False
|
2024-10-27 18:39:09 +00:00
|
|
|
|
return len(keys) > 0
|
|
|
|
|
|
|
|
|
|
|
2025-01-19 13:13:07 +00:00
|
|
|
|
__all__ = ("Keybox",)
|