mirror of
https://github.com/hyugogirubato/KeyDive.git
synced 2026-07-15 18:40:02 +02:00
release v3.0.0
This commit is contained in:
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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')
|
||||
@@ -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 120–124
|
||||
LEVEL_TAG = b'LVL1' # Optional level tag used by QSEE environments (byte offset 128–132)
|
||||
|
||||
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
Reference in New Issue
Block a user