"""Device identifier helpers recovered from APK static analysis. This module covers the local identifier formatting/derivation that is visible in the Java layer. For offline simulation we also expose a deterministic synthetic EGID candidate. That candidate is not the exact DFP VM algorithm; online DFP bootstrap can still replace it when a server-issued value exists. `oDid` is not a separate visible hash algorithm in the Java layer. The app initializes it from the same local did reader before cloud DID refresh, then keeps it as the "old/original DID" field. """ from __future__ import annotations import hashlib import re import secrets from collections.abc import Iterable ANDROID_PREFIX = "ANDROID_" HEX16_RE = re.compile(r"^[0-9a-fA-F]{16}$") ANDROID_ID_RE = re.compile(r"^ANDROID_[0-9a-fA-F]{16}$") EGID_RE = re.compile(r"^DFP[0-9A-F]{61}$") OAID_RE = re.compile(r"^[0-9A-F]{64}$") def java_long_hex16(value: int) -> str: """Mirror `Long.toHexString(value)` constrained to 16 hex chars.""" return f"{value & ((1 << 64) - 1):016x}"[-16:] def java_signed_int_to_long_hex16(value: int) -> str: """Mirror Java `int -> long -> Long.toHexString()` for fallback rdid.""" value &= 0xFFFFFFFF if value & 0x80000000: value -= 0x100000000 return java_long_hex16(value) def normalize_android_suffix(value: str) -> str: suffix = value[len(ANDROID_PREFIX) :] if value.startswith(ANDROID_PREFIX) else value if not HEX16_RE.fullmatch(suffix): raise ValueError(f"expected 16 hex chars, got {value!r}") return suffix.lower() def format_android_id(suffix: str) -> str: return ANDROID_PREFIX + normalize_android_suffix(suffix) def did_from_android_id(android_id: str) -> str: """Model deviceid/i.l(): valid system android_id -> `ANDROID_`.""" return format_android_id(android_id) def did_from_random_long(value: int | None = None) -> str: """Model deviceid/i.a(): Random.nextLong() -> padded 16-hex suffix.""" if value is None: value = secrets.randbits(64) return ANDROID_PREFIX + java_long_hex16(value) def odid_from_local_did(local_did: str) -> str: """Model AppEnv.O_DID: original local DID retained before cloud refresh.""" return format_android_id(local_did) def rdid_from_gRdi2(rom_id: str) -> str: """Model deviceid/i.n(): md5(gRdi2())[16:32] -> `ANDROID_`.""" digest = hashlib.md5(rom_id.encode("utf-8")).hexdigest() return ANDROID_PREFIX + digest[16:32] def rdid_from_random_int(value: int | None = None) -> str: """Model deviceid/i.b(): SecureRandom.nextInt() fallback.""" if value is None: value = secrets.randbits(32) return ANDROID_PREFIX + java_signed_int_to_long_hex16(value) def is_android_device_id(value: str) -> bool: return bool(ANDROID_ID_RE.fullmatch(value)) def is_valid_egid(value: str) -> bool: """Validate the Java callback constraint: prefix `DFP`, total len 64.""" return bool(EGID_RE.fullmatch(value)) def is_valid_oaid(value: str) -> bool: """Validate the uppercase 64-hex OAID shape used in task/deviceInfo fields.""" return bool(OAID_RE.fullmatch(value)) def egid_from_seed_material(parts: Iterable[object]) -> str: """Build a stable offline EGID candidate from local device seed material. Runtime evidence only exposes the Java callback constraint (`DFP` prefix, 64 chars total). The real DFP value is produced by the KSecurity/DFP native flow. This helper keeps generated profiles self-consistent without depending on APP/RPC/online bootstrap, while preserving the same public shape for cookie/deviceInfo testing. """ payload = "\x1f".join("" if item is None else str(item) for item in parts) digest = hashlib.sha512(("ksjsb.egid.v1\x00" + payload).encode("utf-8")).hexdigest().upper() return "DFP" + digest[:61] def oaid_from_seed_material(parts: Iterable[object]) -> str: """Build a stable offline OAID candidate from local device seed material. The current task chain consumes OAID as a public 64-char uppercase hex device field. This keeps generated profiles reproducible without depending on a vendor OAID service call. """ payload = "\x1f".join("" if item is None else str(item) for item in parts) return hashlib.sha256(("ksjsb.oaid.v1\x00" + payload).encode("utf-8")).hexdigest().upper() __all__ = [ "ANDROID_PREFIX", "did_from_android_id", "did_from_random_long", "egid_from_seed_material", "format_android_id", "is_android_device_id", "is_valid_egid", "is_valid_oaid", "java_long_hex16", "java_signed_int_to_long_hex16", "normalize_android_suffix", "oaid_from_seed_material", "odid_from_local_did", "rdid_from_gRdi2", "rdid_from_random_int", ]