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