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

780 lines
24 KiB
Python

"""Semantic model for libksse command 1114139 / EngineProxy.sted.
This is not a full native allocator port. It captures the recovered behavior
that matters for the EGID/cache_m path:
* the decoded string table used by FUN_00142bf4;
* sentinel file names written by FUN_00140ea8 and read by FUN_00142bf4;
* suffix recovery polarity: existing path selects the candidate character;
* product-keyed JSON returned by FUN_00143d98.
"""
from __future__ import annotations
import json
import hashlib
import time
from dataclasses import dataclass
from typing import Callable, Iterable
from .ksse_crc import ksse_crc32
KSSE_DFP_TABLE_RAW = (
"0123456789ABCDEF@/.Android_@KUAISHOU@a21c40ada1eb475b@"
"DFP@/sdcard/Android@NEBULA"
)
KSSE_DOCUMENTS_DIR = "/sdcard/Documents"
STED_CACHE_FILE_TOKEN = "LnNrdmVj"
STED_CACHE_FILE_NAME = ".skvec"
STED_SHARED_PREF_KEY = "kwtk_n"
DFP_SUFFIX_LEN = 0x3D
class KsseSentinelMissing(ValueError):
"""Raised when the sentinel set cannot recover a full DFP suffix."""
@dataclass(frozen=True)
class KsseDfpStringTable:
alphabet: str
android_hidden_dir_suffix: str
default_product: str
static_salt: str
dfp_prefix: str
sdcard_android_dir: str
active_product: str
@classmethod
def parse(cls, raw: str = KSSE_DFP_TABLE_RAW) -> "KsseDfpStringTable":
parts = raw.split("@")
if len(parts) != 7:
raise ValueError(f"unexpected libksse DFP table entry count: {len(parts)}")
return cls(
alphabet=parts[0],
android_hidden_dir_suffix=parts[1],
default_product=parts[2],
static_salt=parts[3],
dfp_prefix=parts[4],
sdcard_android_dir=parts[5],
active_product=parts[6],
)
@dataclass(frozen=True)
class StedJsonInsertionPlan:
"""One product-keyed JSON insertion attempt in ``FUN_00143d98``."""
output_key: str
stage: str
candidate_paths: list[str]
guard: str
@dataclass(frozen=True)
class StedPersistenceArtifacts:
"""Java/native EGID persistence artifacts after ``rq0.d.e(cache_e, cache_m)``."""
cache_json: str
in_memory_cache: dict[str, str]
shared_preferences: dict[str, str]
app_private_files: dict[str, str]
product_marker: str
native_base_path: str
native_sentinel_paths: list[str]
native_readback_json: str
def normalize_product_marker(marker: str | None, fallback: str = "NEBULA") -> str:
"""Convert native marker like ``0NEBULA`` / ``1KWE_N`` to product key."""
if not marker:
return fallback
marker = str(marker)
if marker[:1] in {"0", "1"}:
marker = marker[1:]
return marker or fallback
def ksse_md5_hex16(data: str | bytes) -> str:
"""Return native ``FUN_001575d0`` + ``FUN_00151204(..., 8)`` output.
``FUN_00151204`` uses the lowercase nibble table at ``DAT_0015cd1f``
(`0123456789abcdef...`), and top-level callers pass only the first
8 digest bytes, so the Python equivalent is the first 16 lowercase
hexadecimal characters of MD5.
"""
if isinstance(data, str):
data = data.encode("utf-8")
return hashlib.md5(data).hexdigest()[:16]
def engine_sted_product_marker(product: str = "NEBULA", writable_external_storage: bool = False) -> str:
"""Return EngineProxy.sted's native marker argument.
Java builds this as ``("1" if z else "0") + productName`` before calling
``Watermelon.jniCommand(1114139, "", str, marker)``. On targetSdk >= 30
the rq0.d.j() caller passes ``z=false``; legacy external-storage paths can
pass ``z=true`` when READ/WRITE external storage permission is available.
"""
product = normalize_product_marker(product)
return ("1" if writable_external_storage else "0") + product
def build_sted_cache_json(cache_e: str, cache_m: str, c_time_ms: int) -> str:
"""Build the compact JSONObject written by ``rq0.d.e``.
Native Java insertion order is ``c_time`` -> ``cache_e`` -> ``cache_m``.
The same text is written to SharedPreferences ``kwtk_n`` and app-private
files ``.skvec``.
"""
return json.dumps(
{
"c_time": int(c_time_ms),
"cache_e": str(cache_e),
"cache_m": str(cache_m),
},
ensure_ascii=False,
separators=(",", ":"),
)
def hidden_cache_path(base_path: str, suffix: str, table: KsseDfpStringTable | None = None) -> str:
"""Return ``base + '/.Android_' + suffix`` as constructed by native code."""
table = table or KsseDfpStringTable.parse()
return f"{str(base_path).rstrip('/')}{table.android_hidden_dir_suffix}{suffix}"
def sted_external_candidate_paths(
product_marker: str | None,
*,
table: KsseDfpStringTable | None = None,
documents_dir: str = KSSE_DOCUMENTS_DIR,
) -> list[str]:
"""First ``FUN_00143d98`` candidate pair: Documents then Android.
Native initializes the primary base from the decoded ``/sdcard/Documents``
constant and the fallback from table slot ``0x78`` (``/sdcard/Android``).
For ``KUAISHOU`` it appends the static salt; every other product key uses
``md5(product_key)[:16]``.
"""
table = table or KsseDfpStringTable.parse()
product = normalize_product_marker(product_marker, table.active_product)
suffix = table.static_salt if product == table.default_product else ksse_md5_hex16(product)
return [
hidden_cache_path(documents_dir, suffix, table),
hidden_cache_path(table.sdcard_android_dir, suffix, table),
]
def sted_product_salt_candidate_paths(
*,
table: KsseDfpStringTable | None = None,
documents_dir: str = KSSE_DOCUMENTS_DIR,
) -> list[str]:
"""Candidate pair using the static table salt ``a21c40ada1eb475b``."""
table = table or KsseDfpStringTable.parse()
return [
hidden_cache_path(documents_dir, table.static_salt, table),
hidden_cache_path(table.sdcard_android_dir, table.static_salt, table),
]
def sted_hidden_md5_candidate_paths(
*,
table: KsseDfpStringTable | None = None,
documents_dir: str = KSSE_DOCUMENTS_DIR,
active_product: str | None = None,
) -> list[str]:
"""Candidate pair using ``md5(table[0x90])[:16]``.
Static flow hashes the active table product (currently ``NEBULA``), not
the dynamic marker passed in ``param_5``.
"""
table = table or KsseDfpStringTable.parse()
product = active_product or table.active_product
suffix = ksse_md5_hex16(product)
return [
hidden_cache_path(documents_dir, suffix, table),
hidden_cache_path(table.sdcard_android_dir, suffix, table),
]
def sted_candidate_path_families(
product_marker: str | None,
*,
table: KsseDfpStringTable | None = None,
documents_dir: str = KSSE_DOCUMENTS_DIR,
) -> dict[str, list[str]]:
"""Return the ordered path families currently recovered from ``sted``."""
table = table or KsseDfpStringTable.parse()
return {
"external": sted_external_candidate_paths(
product_marker,
table=table,
documents_dir=documents_dir,
),
"product_salt": sted_product_salt_candidate_paths(
table=table,
documents_dir=documents_dir,
),
"hidden_md5": sted_hidden_md5_candidate_paths(
table=table,
documents_dir=documents_dir,
),
}
def sted_json_insertion_plan(
product_marker: str | None,
*,
table: KsseDfpStringTable | None = None,
documents_dir: str = KSSE_DOCUMENTS_DIR,
) -> list[StedJsonInsertionPlan]:
"""Return native JSON insertion attempts for ``FUN_00143d98``.
Each attempt still depends on successful suffix recovery. The native code
checks the output string length before prepending ``DFP`` and inserting the
member into the JSON object.
"""
table = table or KsseDfpStringTable.parse()
product = normalize_product_marker(product_marker, table.active_product)
plans = [
StedJsonInsertionPlan(
output_key=product,
stage="external_current",
candidate_paths=sted_external_candidate_paths(
product,
table=table,
documents_dir=documents_dir,
),
guard="always attempted first; insert only if recovered suffix is non-empty",
)
]
if product == table.active_product:
plans.append(
StedJsonInsertionPlan(
output_key=table.default_product,
stage="product_salt_default",
candidate_paths=sted_product_salt_candidate_paths(
table=table,
documents_dir=documents_dir,
),
guard="bVar6 == true; product matches table[0x90] active product",
)
)
elif product == table.default_product:
plans.append(
StedJsonInsertionPlan(
output_key=table.active_product,
stage="hidden_md5_active",
candidate_paths=sted_hidden_md5_candidate_paths(
table=table,
documents_dir=documents_dir,
),
guard="bVar1 == true; product matches table[0x30] default product",
)
)
else:
plans.extend(
[
StedJsonInsertionPlan(
output_key=table.default_product,
stage="product_salt_default",
candidate_paths=sted_product_salt_candidate_paths(
table=table,
documents_dir=documents_dir,
),
guard="!bVar1 && !bVar6; product is neither default nor active",
),
StedJsonInsertionPlan(
output_key=table.active_product,
stage="hidden_md5_active",
candidate_paths=sted_hidden_md5_candidate_paths(
table=table,
documents_dir=documents_dir,
),
guard="!bVar1 && !bVar6; product is neither default nor active",
),
]
)
return plans
def join_sentinel_base(base_path: str) -> str:
"""Return the native sentinel prefix equivalent to ``base + '/.'``."""
base = str(base_path).rstrip("/")
return f"{base}/."
def sentinel_half(index: int, char: str) -> str:
"""Return the half marker value used by FUN_00140ea8.
Native compares the byte with ASCII ``'8'``:
``0`` covers 0..7 and ``1`` covers 8..F for the uppercase hex alphabet.
"""
if len(char) != 1:
raise ValueError("char must be one byte/character")
return "0" if ord(char) < ord("8") else "1"
def sentinel_half_path(base_path: str, index: int, half: str | int) -> str:
"""Path for ``.<index>@<half>``."""
if index < 1:
raise ValueError("native sentinel indices are one-based")
half_text = str(half)
if half_text not in {"0", "1"}:
raise ValueError("half must be 0 or 1")
return f"{join_sentinel_base(base_path)}{index}@{half_text}"
def sentinel_char_path(base_path: str, index: int, char: str) -> str:
"""Path for ``.<index>_<actual_hex_char>``."""
if index < 1:
raise ValueError("native sentinel indices are one-based")
if len(char) != 1:
raise ValueError("char must be one byte/character")
return f"{join_sentinel_base(base_path)}{index}_{char}"
def sentinel_paths_for_suffix(base_path: str, suffix: str) -> list[str]:
"""Return all sentinel paths FUN_00140ea8 would materialize."""
paths: list[str] = []
for index, char in enumerate(suffix, start=1):
paths.append(sentinel_half_path(base_path, index, sentinel_half(index, char)))
paths.append(sentinel_char_path(base_path, index, char))
return paths
def recover_suffix_from_sentinels(
base_path: str,
exists: Callable[[str], bool],
*,
alphabet: str | None = None,
length: int = DFP_SUFFIX_LEN,
) -> str:
"""Recover the 61-byte DFP suffix using FUN_00142bf4's probe order."""
table = KsseDfpStringTable.parse()
alphabet = alphabet or table.alphabet
if len(alphabet) < 16:
raise ValueError("alphabet must contain at least 16 characters")
recovered: list[str] = []
for index in range(1, length + 1):
if exists(sentinel_half_path(base_path, index, "0")):
candidates = alphabet[:8]
elif exists(sentinel_half_path(base_path, index, "1")):
candidates = alphabet[8:16]
else:
candidates = alphabet[:16]
for char in candidates:
if exists(sentinel_char_path(base_path, index, char)):
recovered.append(char)
break
else:
raise KsseSentinelMissing(
f"missing sentinel for suffix index {index} under {base_path!r}"
)
return "".join(recovered)
def ksse_suffix_crc_material(
suffix: str,
*,
model: str = "",
table_raw: str = KSSE_DFP_TABLE_RAW,
) -> bytes:
"""Material that FUN_00142bf4 feeds to FUN_001567b8 / CRC32.
Static evidence shows the native code appends the raw table, then the
recovered suffix after in-place reversal, then ``ro.product.model``.
"""
return (table_raw + suffix[::-1] + model).encode("utf-8")
def ksse_suffix_crc32(
suffix: str,
*,
model: str = "",
table_raw: str = KSSE_DFP_TABLE_RAW,
) -> int:
return ksse_crc32(ksse_suffix_crc_material(suffix, model=model, table_raw=table_raw))
def dfp_suffix_from_value(
value: str,
*,
table: KsseDfpStringTable | None = None,
strict: bool = True,
) -> str:
"""Return the 61-byte suffix from ``DFP...`` or a raw suffix string."""
table = table or KsseDfpStringTable.parse()
text = str(value)
if text.startswith(table.dfp_prefix):
text = text[len(table.dfp_prefix) :]
if strict and len(text) != DFP_SUFFIX_LEN:
raise ValueError(f"DFP suffix must be {DFP_SUFFIX_LEN} characters, got {len(text)}")
return text
def ksse_dfp_suffix_crc_material(
suffix: str,
*,
model: str = "",
table_raw: str = KSSE_DFP_TABLE_RAW,
) -> bytes:
"""CRC material after native reverses recovered marker-order suffix back."""
if len(suffix) != DFP_SUFFIX_LEN:
raise ValueError(f"DFP suffix must be {DFP_SUFFIX_LEN} characters, got {len(suffix)}")
return (table_raw + suffix + model).encode("utf-8")
def ksse_dfp_suffix_crc32(
suffix: str,
*,
model: str = "",
table_raw: str = KSSE_DFP_TABLE_RAW,
) -> int:
return ksse_crc32(ksse_dfp_suffix_crc_material(suffix, model=model, table_raw=table_raw))
def sted_writer_marker_suffix(value: str, *, table: KsseDfpStringTable | None = None) -> str:
"""Suffix order written by ``FUN_00140ea8`` sentinel files.
The writer receives a Java-visible ``DFP`` value, strips the ``DFP`` prefix,
reverses the 61-byte suffix, then materializes sentinels for that reversed
byte order.
"""
return dfp_suffix_from_value(value, table=table)[::-1]
def sted_crc_marker_path(
base_path: str,
value: str,
*,
model: str = "",
table: KsseDfpStringTable | None = None,
table_raw: str = KSSE_DFP_TABLE_RAW,
) -> str:
"""Path for the CRC guard marker written after all byte sentinels."""
suffix = dfp_suffix_from_value(value, table=table)
crc = ksse_dfp_suffix_crc32(suffix, model=model, table_raw=table_raw)
return f"{join_sentinel_base(base_path)}{crc}"
def sted_writer_sentinel_paths(base_path: str, value: str) -> list[str]:
"""Per-byte sentinel files created by ``FUN_00140ea8`` for a DFP value."""
return sentinel_paths_for_suffix(base_path, sted_writer_marker_suffix(value))
def sted_writer_paths(
base_path: str,
value: str,
*,
model: str = "",
include_crc: bool = True,
) -> list[str]:
"""All deterministic marker paths created by ``FUN_00140ea8``.
File contents are just a small truth marker; path names carry the data.
"""
paths = sted_writer_sentinel_paths(base_path, value)
if include_crc:
paths.append(sted_crc_marker_path(base_path, value, model=model))
return paths
def build_sted_persistence_artifacts(
cache_e: str,
cache_m: str,
*,
c_time_ms: int | None = None,
product: str = "NEBULA",
writable_external_storage: bool = False,
model: str = "",
include_crc: bool = True,
table: KsseDfpStringTable | None = None,
documents_dir: str = KSSE_DOCUMENTS_DIR,
) -> StedPersistenceArtifacts:
"""Build deterministic Java/native persistence artifacts for an EGID.
This models the confirmed ``rq0.d.e(cache_e, cache_m)`` chain:
1. put ``c_time/cache_e/cache_m`` into the in-memory map;
2. write the same JSON to SharedPreferences key ``kwtk_n``;
3. write the same JSON to app-private file ``.skvec``;
4. call ``EngineProxy.sted(cache_e, z)`` to materialize native sentinel
files for command ``1114139``.
It deliberately does not try to reproduce the optional external
Java-serialized ``LinkedHashMap`` file from ``rq0.d.j`` because that path is
encrypted through ``uq0.o`` and is not needed for the native STED readback.
"""
table = table or KsseDfpStringTable.parse()
dfp_suffix_from_value(cache_e, table=table)
if c_time_ms is None:
c_time_ms = int(time.time() * 1000)
cache_json = build_sted_cache_json(cache_e, cache_m, c_time_ms)
product_marker = engine_sted_product_marker(
product,
writable_external_storage=writable_external_storage,
)
native_base_path = sted_external_candidate_paths(
product_marker,
table=table,
documents_dir=documents_dir,
)[0]
native_sentinel_paths = sted_writer_paths(
native_base_path,
cache_e,
model=model,
include_crc=include_crc,
)
existing = set(native_sentinel_paths)
native_readback_json = recover_sted_result_json_from_sentinels(
product_marker,
existing.__contains__,
model=model,
require_crc=include_crc,
table=table,
documents_dir=documents_dir,
)
return StedPersistenceArtifacts(
cache_json=cache_json,
in_memory_cache={
"c_time": str(int(c_time_ms)),
"cache_e": str(cache_e),
"cache_m": str(cache_m),
},
shared_preferences={STED_SHARED_PREF_KEY: cache_json},
app_private_files={STED_CACHE_FILE_NAME: cache_json},
product_marker=product_marker,
native_base_path=native_base_path,
native_sentinel_paths=native_sentinel_paths,
native_readback_json=native_readback_json,
)
def recover_dfp_suffix_from_sted_sentinels(
base_path: str,
exists: Callable[[str], bool],
*,
model: str = "",
require_crc: bool = False,
) -> str:
"""Recover the Java-visible DFP suffix from writer-created sentinels."""
marker_order = recover_suffix_from_sentinels(base_path, exists)
suffix = marker_order[::-1]
if require_crc and not exists(sted_crc_marker_path(base_path, suffix, model=model)):
raise KsseSentinelMissing(f"missing CRC marker for recovered suffix under {base_path!r}")
return suffix
def recover_dfp_value_from_sted_sentinels(
base_path: str,
exists: Callable[[str], bool],
*,
model: str = "",
require_crc: bool = False,
table: KsseDfpStringTable | None = None,
) -> str:
"""Recover ``DFP`` + suffix from writer-created sentinels."""
table = table or KsseDfpStringTable.parse()
suffix = recover_dfp_suffix_from_sted_sentinels(
base_path,
exists,
model=model,
require_crc=require_crc,
)
return table.dfp_prefix + suffix
def recover_first_dfp_value_from_candidate_paths(
candidate_paths: Iterable[str],
exists: Callable[[str], bool],
*,
model: str = "",
require_crc: bool = False,
table: KsseDfpStringTable | None = None,
) -> str | None:
"""Try native primary/fallback path order and return the first DFP value."""
table = table or KsseDfpStringTable.parse()
for base_path in candidate_paths:
try:
return recover_dfp_value_from_sted_sentinels(
base_path,
exists,
model=model,
require_crc=require_crc,
table=table,
)
except KsseSentinelMissing:
continue
return None
def recover_sted_values_from_sentinels(
product_marker: str | None,
exists: Callable[[str], bool],
*,
model: str = "",
require_crc: bool = False,
table: KsseDfpStringTable | None = None,
documents_dir: str = KSSE_DOCUMENTS_DIR,
) -> dict[str, str]:
"""Recover ``FUN_00143d98`` product-keyed values from sentinel files.
This models the Java-visible ``EngineProxy.sted(null,z)`` read path:
every planned JSON member is attempted in native insertion order, each
member uses its own primary/fallback path pair, and missing sentinel sets
simply skip that JSON member.
"""
table = table or KsseDfpStringTable.parse()
values: dict[str, str] = {}
for plan in sted_json_insertion_plan(
product_marker,
table=table,
documents_dir=documents_dir,
):
value = recover_first_dfp_value_from_candidate_paths(
plan.candidate_paths,
exists,
model=model,
require_crc=require_crc,
table=table,
)
if value:
values[plan.output_key] = value
return values
def recover_sted_result_json_from_sentinels(
product_marker: str | None,
exists: Callable[[str], bool],
*,
model: str = "",
require_crc: bool = False,
table: KsseDfpStringTable | None = None,
documents_dir: str = KSSE_DOCUMENTS_DIR,
) -> str:
"""Return compact product-keyed JSON recovered from native sentinels."""
return build_sted_result_json(
recover_sted_values_from_sentinels(
product_marker,
exists,
model=model,
require_crc=require_crc,
table=table,
documents_dir=documents_dir,
)
)
def build_sted_result_json(values: dict[str, str]) -> str:
"""Serialize product-keyed DFP values like FUN_00154a0c compact mode."""
return json.dumps(values, ensure_ascii=False, separators=(",", ":"))
def parse_sted_result_json(data: str) -> dict[str, str]:
parsed = json.loads(data)
if not isinstance(parsed, dict):
raise ValueError("sted result must be a JSON object")
return {str(key): str(value) for key, value in parsed.items()}
def select_sted_product_value(data: str | dict[str, str], marker: str | None) -> str:
"""Select current product's DFP value from FUN_00143d98-style JSON."""
values = parse_sted_result_json(data) if isinstance(data, str) else data
product = normalize_product_marker(marker)
if product in values:
return values[product]
table = KsseDfpStringTable.parse()
if table.default_product in values:
return values[table.default_product]
if values:
return next(iter(values.values()))
raise KeyError("empty sted result")
__all__ = [
"DFP_SUFFIX_LEN",
"KSSE_DOCUMENTS_DIR",
"KSSE_DFP_TABLE_RAW",
"STED_CACHE_FILE_NAME",
"STED_CACHE_FILE_TOKEN",
"STED_SHARED_PREF_KEY",
"KsseDfpStringTable",
"KsseSentinelMissing",
"StedJsonInsertionPlan",
"StedPersistenceArtifacts",
"build_sted_cache_json",
"build_sted_persistence_artifacts",
"build_sted_result_json",
"dfp_suffix_from_value",
"engine_sted_product_marker",
"hidden_cache_path",
"join_sentinel_base",
"ksse_dfp_suffix_crc32",
"ksse_dfp_suffix_crc_material",
"ksse_md5_hex16",
"ksse_suffix_crc32",
"ksse_suffix_crc_material",
"normalize_product_marker",
"parse_sted_result_json",
"recover_first_dfp_value_from_candidate_paths",
"recover_dfp_value_from_sted_sentinels",
"recover_dfp_suffix_from_sted_sentinels",
"recover_suffix_from_sentinels",
"recover_sted_result_json_from_sentinels",
"recover_sted_values_from_sentinels",
"select_sted_product_value",
"sentinel_char_path",
"sentinel_half",
"sentinel_half_path",
"sentinel_paths_for_suffix",
"sted_candidate_path_families",
"sted_external_candidate_paths",
"sted_hidden_md5_candidate_paths",
"sted_json_insertion_plan",
"sted_product_salt_candidate_paths",
"sted_crc_marker_path",
"sted_writer_marker_suffix",
"sted_writer_paths",
"sted_writer_sentinel_paths",
]