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

135 lines
4.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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