KeyDive/keydive/keybox.py

169 lines
6.7 KiB
Python
Raw Normal View History

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
def bytes2int(value: bytes, byteorder: Literal['big', 'little'] = 'big', signed: bool = False) -> int:
"""
Convert bytes to an integer.
Args:
value (bytes): The byte sequence to convert.
byteorder (Literal['big', 'little'], optional): The byte order for conversion. Defaults to 'big'.
signed (bool, optional): Indicates if the integer is signed. Defaults to False.
Returns:
int: The converted integer.
"""
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):
"""
Initializes the Keybox object, setting up a logger and containers for device IDs and keyboxes.
Attributes:
logger (Logger): Logger instance for logging messages.
2024-11-03 13:03:04 +00:00
device_id (list[bytes]): List of unique device IDs (32 bytes each).
keybox (dict[bytes, bytes]): Dictionary mapping device IDs to their respective keybox data.
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.
Args:
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)
assert size == 32, f'Invalid keybox length: {size}. Should be 32 bytes'
if data not in self.device_id:
2024-11-03 13:03:04 +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)
except Exception as e:
self.logger.debug('Failed to set device id: %s', e)
def set_keybox(self, data: bytes) -> None:
"""
Set the keybox from the provided data.
Args:
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)
assert size in (128, 132), f'Invalid keybox length: {size}. Should be 128 or 132 bytes'
if size == 132:
2024-11-03 13:03:04 +00:00
assert data[128:132] == b"LVL1", 'QSEE-style keybox must end with bytes "LVL1"'
2024-10-27 18:39:09 +00:00
assert data[120:124] == b"kbox", 'Invalid keybox magic'
device_id = data[0:32]
2024-11-03 13:03:04 +00:00
# Retrieve structured keybox info and log it
infos = self.__keybox_info(data)
encrypted = infos['flags'] > 10
2024-10-27 18:39:09 +00:00
self.set_device_id(data=device_id)
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:
2024-10-28 20:30:42 +00:00
self.logger.info('Receive keybox: \n\n%s\n', json.dumps(infos, indent=2))
2024-11-03 13:03:04 +00:00
# Warn if flags indicate encryption, which requires an unencrypted device token
if encrypted:
self.logger.warning('Keybox contains encrypted data. Interception of plaintext device token is needed')
2024-10-27 18:39:09 +00:00
2024-11-03 13:03:04 +00:00
# Store or update the keybox for the device if it's not already saved
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:
self.logger.debug('Failed to set keybox: %s', e)
@staticmethod
def __keybox_info(data: bytes) -> dict:
"""
Extract keybox information from the provided data.
Args:
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
2024-11-03 13:03:04 +00:00
device_token = data[48:120]
2024-10-27 18:39:09 +00:00
content = {
2024-11-03 13:03:04 +00:00
'device_id': data[0:32].decode('utf-8'), # Device's unique identifier (32 bytes)
'device_key': data[32:48], # Device-specific cryptographic key (16 bytes)
'device_token': device_token, # Token used for device authentication (72 bytes)
'keybox_tag': data[120:124].decode('utf-8'), # Magic tag indicating keybox format (4 bytes)
'crc32': bytes2int(data[124:128]), # CRC32 checksum for data integrity verification (4 bytes)
2024-10-27 18:39:09 +00:00
'level_tag': data[128:132].decode('utf-8') or None, # Optional tag indicating keybox level (4 bytes).
2024-11-03 13:03:04 +00:00
# Additional metadata parsed from the device token (Bytes 48120).
'flags': bytes2int(device_token[0:4]), # Flags indicating specific device capabilities (4 bytes).
'system_id': bytes2int(device_token[4:8]), # System identifier (4 bytes).
2024-10-27 18:39:09 +00:00
# Provisioning ID, encrypted and derived from the unique ID in the system.
2024-11-03 13:03:04 +00:00
'provisioning_id': UUID(bytes_le=device_token[8:24]), # Provisioning UUID (16 bytes).
2024-10-27 18:39:09 +00:00
# Encrypted bits containing device key, key hash, and additional flags.
2024-11-03 13:03:04 +00:00
'encrypted_bits': device_token[24:72] ## Encrypted device-specific information (48 bytes).
2024-10-27 18:39:09 +00:00
}
# Encode certain fields in base64 and convert UUIDs to string
return {
k: base64.b64encode(v).decode('utf-8') if isinstance(v, bytes) else str(v) if isinstance(v, UUID) else v
for k, v in content.items()
}
def export(self, parent: Path) -> bool:
"""
Export the keybox data to a file in the specified parent directory.
Args:
parent (Path): The parent directory where the keybox data will be saved.
Returns:
bool: True if any keybox were exported, otherwise False.
"""
keys = self.device_id & self.keybox.keys()
for k in keys:
2024-11-03 13:03:04 +00:00
# Prepare target directory and export file path
2024-10-27 18:39:09 +00:00
parent.mkdir(parents=True, exist_ok=True)
2024-11-03 13:03:04 +00:00
path_keybox_bin = parent / f"keybox.{'enc' if self.keybox[k][1] else 'bin'}"
path_keybox_bin.write_bytes(self.keybox[k][0])
2024-10-27 18:39:09 +00:00
2024-11-03 13:03:04 +00:00
if self.keybox[k][1]:
self.logger.warning('Exported encrypted keybox: %s', path_keybox_bin)
else:
self.logger.info('Exported keybox: %s', path_keybox_bin)
2024-10-27 18:39:09 +00:00
return len(keys) > 0
__all__ = ('Keybox',)