149 lines
4.7 KiB
Python
149 lines
4.7 KiB
Python
"""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_<id>`."""
|
|
|
|
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_<suffix>`."""
|
|
|
|
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",
|
|
]
|