release v3.0.0

This commit is contained in:
hyugogirubato
2025-06-09 18:12:49 +02:00
parent 2f74b4c5b7
commit 82d6a903dd
28 changed files with 5384 additions and 1620 deletions
+55
View File
@@ -0,0 +1,55 @@
from keydive.adb.vendor import Vendor
# Maximum clear API level for Keybox
# https://cs.android.com/android/platform/superproject/+/android14-qpr3-release:trusty/user/app/sample/hwcrypto/keybox/keybox.c
KEYBOX_MAX_CLEAR_API = 28
# https://cs.android.com/search?q=oemcrypto&sq=&ss=android%2Fplatform%2Fsuperproject
OEM_CRYPTO_API = (
# Mapping of function names across different API levels (obfuscated names may vary).
'rnmsglvj', 'polorucp', 'kqzqahjq', 'pldrclfq', 'kgaitijd', 'cwkfcplc', 'crhqcdet', 'ulns', 'dnvffnze', 'ygjiljer',
'qbjxtubz', 'qkfrcjtw', 'rbhjspoh', 'zgtjmxko', 'igrqajte', 'ofskesua', 'qllcoacg', 'pukctkiv', 'ehdqmfmd',
'xftzvkwx', 'gndskkuk', 'wcggmnnx', 'kaatohcz', 'ktmgdchz', 'jkcwonus', 'ehmduqyt', 'vewtuecx', 'mxrbzntq',
'isyowgmp', 'flzfkhbc', 'rtgejgqb', 'sxxprljw', 'ebxjbtxl', 'pcmtpkrj', 'uegpdzus', 'ncmqbmbc'
# Add more as needed for different versions.
)
# https://developers.google.com/widevine
CDM_FUNCTIONS = (
'UsePrivacyMode',
'GetCdmClientPropertySet',
'PrepareKeyRequest',
'AesCbcKey',
'Read', '_x1c36',
'runningcrc',
'_oecc07', '_lcc07',
'_oecc04', '_lcc04',
'_oecc49', '_lcc49',
'_oecc12', '_lcc12',
'_oecc95', '_lcc95',
'_oecc21', '_lcc21',
'GenerateDerivedKeys'
)
# https://developer.android.com/tools/releases/platforms
CDM_VENDOR_API = (
Vendor('mediaserver', 22, (11, '1.0'), 'libwvdrmengine.so'),
Vendor('mediadrmserver', 24, (11, '1.0'), 'libwvdrmengine.so'),
Vendor('android.hardware.drm@1.0-service.widevine', 26, (13, '5.1.0'), 'libwvhidl.so'),
Vendor('android.hardware.drm@1.1-service.widevine', 28, (14, '14.0.0'), 'libwvhidl.so'),
Vendor('android.hardware.drm@1.2-service.widevine', 29, (15, '15.0.0'), 'libwvhidl.so'),
Vendor('android.hardware.drm@1.3-service.widevine', 30, (16, '16.0.0'), 'libwvhidl.so'),
Vendor('android.hardware.drm@1.4-service.widevine', 31, (16, '16.1.0'), 'libwvhidl.so'),
Vendor('android.hardware.drm-service.widevine', 33, (17, '17.0.0'), 'libwvaidl.so'),
Vendor('android.hardware.drm-service.widevine', 34, (18, '18.0.0'), 'android.hardware.drm-service.widevine')
)
# https://github.com/kaltura/kaltura-device-info-android/blob/master/app/src/main/java/com/kaltura/kalturadeviceinfo/Collector.java#L317
CDM_PROPERTIES = {
'CompanyName': 'ro.product.manufacturer',
'ModelName': 'ro.product.model',
'ArchitectureName': 'ro.product.cpu.abi',
'DeviceName': 'ro.product.device',
'ProductName': 'ro.product.name',
'BuildInfo': 'ro.build.fingerprint'
}
+1000
View File
File diff suppressed because it is too large Load Diff
+113
View File
@@ -0,0 +1,113 @@
from enum import Enum
from typing import Optional
from construct import BitStruct, Bytes, Const
from construct import Enum as CEnum
from construct import Int8ub, Int16ub
from construct import Optional as COptional
from construct import Padded, Padding, Struct, this
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
from keydive.drm.protocol.license_pb2 import ClientIdentification
class DeviceTypes(Enum):
CHROME = 1
ANDROID = 2
class _Structures:
magic = Const(b"WVD")
header = Struct(
"signature" / magic,
"version" / Int8ub
)
v2 = Struct(
"signature" / magic,
"version" / Const(2, Int8ub),
"type_" / CEnum(
Int8ub,
**{t.name: t.value for t in DeviceTypes}
),
"security_level" / Int8ub,
"flags" / Padded(1, COptional(BitStruct(
# no per-device flags yet
Padding(8)
))),
"private_key_len" / Int16ub,
"private_key" / Bytes(this.private_key_len),
"client_id_len" / Int16ub,
"client_id" / Bytes(this.client_id_len)
)
class Device:
"""
A minimal implementation of a WVD (Widevine Device) structure,
modeled after the pywidevine library.
This class provides a lightweight alternative to the full `pywidevine.Device`,
retaining only the essential fields for constructing and serializing Widevine device
blobs in version 2 format, without performing certificate validation or VMP handling.
"""
Structures = _Structures
supported_structure = Structures.v2
def __init__(
self,
type_: DeviceTypes,
security_level: int,
flags: Optional[dict],
private_key: RSAPrivateKey,
client_id: ClientIdentification,
):
"""
Initialize the Device object.
Args:
type_ (DeviceTypes or str): The type of device.
security_level (int): The DRM security level (e.g., 1, 3).
flags (Optional[dict]): Optional feature flags (reserved for future use).
private_key (RSAPrivateKey): The device's RSA private key.
client_id (ClientIdentification): The protobuf client identifier.
"""
self.type = DeviceTypes[type_] if isinstance(type_, str) else type_
self.security_level = security_level
self.flags = flags or {}
self.private_key = private_key
self.client_id = client_id
def dumps(self) -> bytes:
"""
Serialize the Device object into a WVD-compatible binary format.
Returns:
bytes: A binary representation of the device data.
"""
# Serialize the client ID protobuf message
bin_client_id = self.client_id.SerializeToString()
# Serialize the private RSA key to DER format
bin_private_key = self.private_key.private_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
)
# Build the structured binary representation
return self.supported_structure.build(dict(
version=2,
type_=self.type.value,
security_level=self.security_level,
flags=self.flags,
private_key_len=len(bin_private_key),
private_key=bin_private_key,
client_id_len=len(bin_client_id),
client_id=bin_client_id
))
__all__ = ('Device', 'DeviceTypes')
+243
View File
@@ -0,0 +1,243 @@
from uuid import UUID
from crccheck.crc import Crc32Mpeg2
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher
from cryptography.hazmat.primitives.ciphers.algorithms import AES
from cryptography.hazmat.primitives.ciphers.modes import ECB
from cryptography.hazmat.primitives.padding import PKCS7
# Convert bytes to a big-endian unsigned integer
bytes2int = lambda x: int.from_bytes(x, byteorder='big', signed=False)
class KeyBox:
"""
Represents a binary DRM keybox containing a device's unique identifiers and AES key.
A KeyBox is composed of:
- A 32-byte stable ID
- A 16-byte AES key
- A 72-byte device ID blob (may contain encrypted metadata)
- A 4-byte 'kbox' tag
- A 4-byte CRC32 checksum
- [Optional] A 4-byte 'LVL1' tag (QSEE-specific)
This class supports both parsing and serialization of this binary format.
"""
MAGIC_TAG = b'kbox' # Magic tag required at byte offset 120124
LEVEL_TAG = b'LVL1' # Optional level tag used by QSEE environments (byte offset 128132)
def __init__(self):
self._stable_id = b'' # 32-byte stable device ID
self._device_aes_key = b'' # 16-byte AES encryption key
self._device_id = b'' # 72-byte encrypted or encoded device-specific ID blob
@property
def stable_id(self) -> bytes:
"""
Returns the stable ID of the device (32 bytes).
Returns:
bytes: The stable device ID.
"""
return self._stable_id
@stable_id.setter
def stable_id(self, value: bytes) -> None:
"""
Sets the 32-byte stable ID of the device.
Args:
value (bytes): A 32-byte stable ID.
Raises:
AssertionError: If `value` is not exactly 32 bytes long.
"""
assert len(value) == 32, f'Invalid stable ID length: expected 32 bytes, got {len(value)}'
self._stable_id = value
@property
def device_aes_key(self) -> bytes:
"""
Returns the AES encryption key used by the device (16 bytes).
Returns:
bytes: The device AES key.
"""
return self._device_aes_key
@device_aes_key.setter
def device_aes_key(self, value: bytes) -> None:
"""
Sets the 16-byte AES key for the device.
Args:
value (bytes): A 16-byte AES key.
Raises:
AssertionError: If `value` is not exactly 16 bytes long.
"""
assert len(value) == 16, f'Invalid AES key length: expected 16 bytes, got {len(value)}'
self._device_aes_key = value
@property
def device_id(self) -> bytes:
"""
Returns the 72-byte device ID blob.
Returns:
bytes: The device-specific identifier blob.
"""
return self._device_id
@device_id.setter
def device_id(self, value: bytes) -> None:
"""
Sets the device ID blob (72 bytes).
Args:
value (bytes): A 72-byte device ID.
Raises:
AssertionError: If `value` is not exactly 72 bytes long.
"""
assert len(value) == 72, f'Invalid device ID length: expected 72 bytes, got {len(value)}'
self._device_id = value
def ParseFromString(self, serialized: bytes) -> None:
"""
Parses and validates a binary keybox structure into its individual fields.
The expected format is:
- 32 bytes: stable ID
- 16 bytes: AES key
- 72 bytes: device ID
- 4 bytes: keybox tag (must be "kbox")
- 4 bytes: CRC32 checksum (MPEG-2 variant)
- [Optional] 4 bytes: level tag ("LVL1") for QSEE devices
Args:
serialized (bytes): Raw keybox data (128 or 132 bytes).
Raises:
AssertionError: If data is malformed or fails validation.
"""
# https://github.com/zybpp/Python/tree/master/Python/keybox
# Size of the entire serialized keybox (should be either 128 bytes for standard keybox,
# or 132 bytes for QSEE variant which appends an additional 4-byte level tag)
size = len(serialized)
# Validate that the size is either of the two expected formats
assert size in (128, 132), f'Invalid keybox size: expected 128 or 132 bytes, got {size}'
# Slice fields from the input
self.stable_id = serialized[0:32] # Device's unique identifier (32 bytes)
self.device_aes_key = serialized[32:48] # Device cryptographic key (16 bytes)
self.device_id = serialized[48:120] # Token for device authentication (72 bytes)
magic_tag = serialized[120:124] # Magic tag (4 bytes)
body_crc = bytes2int(serialized[124:128]) # CRC32 checksum (4 bytes)
level_tag = serialized[128:132] # Optional level tag (4 bytes)
# Ensure the 4-byte magic tag is exactly "kbox"
assert magic_tag == self.MAGIC_TAG, 'Keybox tag mismatch: expected "kbox"'
# Validate the checksum using CRC32 MPEG-2 algorithm on the first 124 bytes
# This ensures data integrity and prevents accidental corruption or tampering
assert body_crc == Crc32Mpeg2.calc(serialized[:124]), 'CRC32 validation failed'
# If the optional QSEE tag is present, ensure it exactly matches "LVL1"
# This tag typically denotes that the keybox is trusted or verified by Qualcomm Secure Execution Environment
assert size == 128 or level_tag == self.LEVEL_TAG, 'Unexpected QSEE tag: expected "LVL1"'
@property
def device_info(self) -> dict:
"""
Attempts to decode device_id contents into meaningful fields.
The layout is decoded if the first 4 bytes (flag) are <= 10.
Otherwise, the format is assumed encrypted or proprietary.
Returns:
dict: Decoded fields such as 'flag', 'system_id', 'provisioning_id', and 'encrypted_bits'.
Returns an empty dict if layout is unknown.
"""
# Interpret device_id fields if the flag is within expected range (<= 10)
flag = bytes2int(self.device_id[0:4])
if not flag > 10:
infos = {
'flag': flag, # Device flag (4 bytes)
'system_id': bytes2int(self.device_id[4:8]), # System identifier (4 bytes)
'provisioning_id': UUID(bytes_le=self.device_id[8:24]), # Provisioning UUID (16 bytes)
'encrypted_bits': self.device_id[24:72] # Encrypted device-specific information (48 bytes)
}
else:
# Encrypted format or unknown layout; optionally decrypt if format is known
# https://github.com/ThatNotEasy/Parser-DRM/blob/main/modules/widevine.py#L84
"""
# Padding to AES block size (16 bytes)
padder = PKCS7(AES.block_size).padder()
enc_padded_data = padder.update(self.device_id[0:4]) + padder.finalize()
# Create AES-ECB cipher with device key
cipher = Cipher(
algorithm=AES(self.device_aes_key),
mode=ECB(),
backend=default_backend()
)
# Decrypt padded data
decryptor = cipher.decryptor()
dec_padded_data = decryptor.update(enc_padded_data) + decryptor.finalize()
# Remove padding to get original data
unpadder = PKCS7(AES.block_size).unpadder()
dec_data = unpadder.update(dec_padded_data) + unpadder.finalize()
"""
infos = {}
return infos
@property
def keybox_info(self) -> dict:
"""
Extracts all readable keybox metadata for external consumption.
Returns:
dict: A dictionary containing raw and interpreted keybox data.
"""
return {
'stable_id': self.stable_id,
'device_aes_key': self.device_aes_key,
'device_id': self.device_id,
**self.device_info
}
def SerializeToString(self) -> bytes:
"""
Serializes the current keybox structure to a binary blob.
Returns:
bytes: A 128-byte serialized keybox with checksum.
Raises:
AssertionError: If any required field is missing.
"""
# Ensure that all essential components are set; without them, a valid keybox cannot be constructed
assert self.stable_id and self.device_aes_key and self.device_id, 'Cannot serialize: one or more required fields are missing'
# This forms the first 124 bytes of the final binary output.
serialized = self.stable_id + self.device_aes_key + self.device_id + self.MAGIC_TAG
# Calculate a CRC32 checksum using the MPEG-2 variant on the first 124 bytes
# This helps verify data integrity when the structure is later read or validated
body_crc = Crc32Mpeg2.calc(serialized)
# Convert the CRC integer into a 4-byte big-endian format and append it to the structure
# This completes the full 128-byte keybox format required by consumers or hardware modules
return serialized + int.to_bytes(body_crc, length=4, byteorder='big', signed=False)
__all__ = ('KeyBox',)
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff