135 lines
4.1 KiB
Python
135 lines
4.1 KiB
Python
"""APP `LoginHelper.b(phone)` 手机号字段加密。
|
||
|
||
静态链路:
|
||
|
||
``LoginHelper.b``
|
||
-> ``KSecurity.atlasEncrypt(phone.getBytes())``
|
||
-> ``doCommandNative(10400, ...)``
|
||
-> ``xm0.b.b().b(raw)`` 标准 Base64
|
||
|
||
该分支输出的是 **inner ZT**:
|
||
``dec0adde + header + B 表 10400 payload``,不是 DFP/广告
|
||
`deviceInfo/encData` 那种带 ``5a54...`` outer wrapper 的形态。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import base64
|
||
from pathlib import Path
|
||
|
||
from .enc_data import (
|
||
build_inner_zt_header,
|
||
kwsg_10400_ecb_encrypt,
|
||
kwsg_10400_nonce9,
|
||
load_kwsg_10400_tables,
|
||
parse_inner_zt_header,
|
||
)
|
||
|
||
|
||
# 从 APP `LoginHelper.b` 动态样本解析出的 10400 inner cfg9。
|
||
LOGINHELPER_MOBILE_CFG9 = bytes.fromhex("00cf0700eec9b64f27")
|
||
|
||
# `LoginHelper.b` 使用 10418 B 表分支;A 表会得到另一组 payload。
|
||
LOGINHELPER_MOBILE_T1_PATH = Path("out/kwsg_10418_B_T1.bin")
|
||
LOGINHELPER_MOBILE_T2_PATH = Path("out/kwsg_10418_B_T2.bin")
|
||
|
||
|
||
def _tables_or_load(t1: bytes | None, t2: bytes | None) -> tuple[bytes, bytes]:
|
||
if t1 is None and t2 is None:
|
||
return load_kwsg_10400_tables(LOGINHELPER_MOBILE_T1_PATH, LOGINHELPER_MOBILE_T2_PATH)
|
||
if t1 is None or t2 is None:
|
||
raise ValueError("t1 and t2 must be provided together")
|
||
return t1, t2
|
||
|
||
|
||
def loginhelper_encrypt_mobile_raw(
|
||
mobile: str,
|
||
*,
|
||
epoch_seconds: int | None = None,
|
||
cfg9: bytes = LOGINHELPER_MOBILE_CFG9,
|
||
t1: bytes | None = None,
|
||
t2: bytes | None = None,
|
||
) -> bytes:
|
||
"""生成 `LoginHelper.b` Base64 之前的 raw bytes。"""
|
||
|
||
if not str(mobile):
|
||
raise ValueError("mobile is empty")
|
||
if len(cfg9) != 9:
|
||
raise ValueError("cfg9 must be exactly 9 bytes")
|
||
|
||
table1, table2 = _tables_or_load(t1, t2)
|
||
encrypted_payload = kwsg_10400_ecb_encrypt(str(mobile).encode("utf-8"), table1, table2)
|
||
return (
|
||
build_inner_zt_header(
|
||
kwsg_10400_nonce9(epoch_seconds),
|
||
cfg9,
|
||
encrypted_payload,
|
||
)
|
||
+ encrypted_payload
|
||
)
|
||
|
||
|
||
def loginhelper_encrypt_mobile(
|
||
mobile: str,
|
||
*,
|
||
epoch_seconds: int | None = None,
|
||
cfg9: bytes = LOGINHELPER_MOBILE_CFG9,
|
||
t1: bytes | None = None,
|
||
t2: bytes | None = None,
|
||
) -> str:
|
||
"""复现 APP `LoginHelper.b(phone)` 返回值。"""
|
||
|
||
raw = loginhelper_encrypt_mobile_raw(
|
||
mobile,
|
||
epoch_seconds=epoch_seconds,
|
||
cfg9=cfg9,
|
||
t1=t1,
|
||
t2=t2,
|
||
)
|
||
return base64.b64encode(raw).decode("ascii")
|
||
|
||
|
||
def parse_loginhelper_encrypted_mobile(value: str) -> dict:
|
||
"""解析 `LoginHelper.b` 输出,便于对照日志样本。"""
|
||
|
||
raw = base64.b64decode(value)
|
||
parsed = parse_inner_zt_header(raw)
|
||
parsed["raw"] = raw
|
||
return parsed
|
||
|
||
|
||
def loginhelper_encrypted_mobile_matches(value: str, mobile: str) -> bool:
|
||
"""判断捕获到的 `LoginHelper.b` 密文是否属于指定手机号。
|
||
|
||
APP 每次调用时 header 里的 9 字节 nonce 会随时间变化,完整 Base64
|
||
字符串不能直接比较。手机号本身落在 10400 加密后的 payload 里,
|
||
因此同号判断只比较 cfg9、payload_len、payload 和 payload CRC。
|
||
"""
|
||
|
||
if not value or not str(mobile):
|
||
return False
|
||
try:
|
||
captured = parse_loginhelper_encrypted_mobile(value)
|
||
expected = parse_loginhelper_encrypted_mobile(loginhelper_encrypt_mobile(mobile, epoch_seconds=0))
|
||
except Exception:
|
||
return False
|
||
return (
|
||
captured.get("magic") == expected.get("magic") == bytes.fromhex("dec0adde")
|
||
and captured.get("header_size") == expected.get("header_size") == 0x20
|
||
and captured.get("cfg9") == expected.get("cfg9")
|
||
and captured.get("crc32") == expected.get("crc32")
|
||
and captured.get("payload_len") == expected.get("payload_len")
|
||
and captured.get("payload") == expected.get("payload")
|
||
)
|
||
|
||
|
||
__all__ = [
|
||
"LOGINHELPER_MOBILE_CFG9",
|
||
"LOGINHELPER_MOBILE_T1_PATH",
|
||
"LOGINHELPER_MOBILE_T2_PATH",
|
||
"loginhelper_encrypted_mobile_matches",
|
||
"loginhelper_encrypt_mobile",
|
||
"loginhelper_encrypt_mobile_raw",
|
||
"parse_loginhelper_encrypted_mobile",
|
||
]
|