ksjsb/core/device_id.py
2026-07-30 20:25:56 +08:00

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",
]