File size: 4,077 Bytes
5db4eb7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#!/usr/bin/env python3
"""解密并校验数美 deviceprofile 请求体(payload.json)

用途:
  1. 用 uid 派生 priId 解密 data,还原明文指纹对象(对照 capture_payload.js 的输出)
  2. 校验完整性字段 er/tn:按 JS 语义重算拼接串并比对
  3. 校验各字段的 DES 明文(可选,需提供密钥表)

依赖:pip install pycryptodome

用法:
  python decode_payload.py payload.json --uid 88888888-8888-4888-8888-888888888888 [--dump]
"""
import argparse
import base64
import gzip
import hashlib
import json
import sys

try:
    from Crypto.Cipher import DES
except ImportError:  # pragma: no cover
    print("需要 pycryptodome: pip install pycryptodome", file=sys.stderr)
    sys.exit(1)

AES_IV = "0102030405060708"


def aes_cbc_decrypt_hex(pri_id: str, data_hex: str) -> bytes:
    from Crypto.Cipher import AES

    raw = bytes.fromhex(data_hex)
    dec = AES.new(pri_id.encode(), AES.MODE_CBC, AES_IV.encode()).decrypt(raw)
    return dec.rstrip(b"\x00")


def des_decrypt_b64(key: str, value: str) -> str:
    raw = base64.b64decode(value)
    out = DES.new(key.encode(), DES.MODE_ECB).decrypt(raw)
    return out.rstrip(b"\x00").decode("utf-8", "replace")


def js_value_string(v) -> str:
    """复刻 JS 中 ``'' + value`` 的字符串化行为。"""
    if isinstance(v, bool):
        return "true" if v else "false"
    if isinstance(v, dict):
        return "[object Object]"  # 嵌套对象不递归
    if isinstance(v, list):
        return ",".join("" if x is None else js_value_string(x) for x in v)
    if v is None:
        return "null"
    return str(v)


def compute_tn(obj: dict) -> str:
    """复刻 SDK 的 tn:按键排序拼接值,数字 ×10000(键名与分隔符不参与)。"""
    parts = []
    for k in sorted(obj.keys()):
        v = obj[k]
        if isinstance(v, bool):
            parts.append(js_value_string(v))
        elif isinstance(v, (int, float)):
            parts.append(str(int(v * 0x2710)))
        else:
            parts.append(js_value_string(v))
    return hashlib.md5("".join(parts).encode("utf-8")).hexdigest()


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("payload", help="capture_payload.js 产出的 payload.json")
    ap.add_argument("--uid", required=True, help="签发时的 uid(priId = md5(uid)[:16])")
    ap.add_argument("--dump", action="store_true", help="打印全部明文字段")
    ap.add_argument("--er-key", default="2j48yowr", help="er/tn 字段的 DES 密钥(默认当前版本)")
    ap.add_argument(
        "--undefined-key",
        action="append",
        default=None,
        help="值为 undefined、只在 tn 里以 \"undefined\" 出现的字段(默认 yu,可重复传)",
    )
    args = ap.parse_args()

    payload = json.load(open(args.payload, encoding="utf-8"))
    pri_id = hashlib.md5(args.uid.encode()).hexdigest()[:16]
    b64 = aes_cbc_decrypt_hex(pri_id, payload["data"])
    obj = json.loads(gzip.decompress(base64.b64decode(b64)))

    print("解密成功:字段数 =", len(obj), "| protocol =", obj.get("protocol"))

    er = obj.get("er")
    if er:
        got_tn = des_decrypt_b64(args.er_key, er)
        # JSON 会丢弃值为 undefined 的字段,但 tn 拼接里仍以字符串 "undefined" 参与。
        undefined_keys = args.undefined_key if args.undefined_key is not None else ["yu"]
        tn_input = {k: v for k, v in obj.items() if k != "er"}
        for k in undefined_keys:
            tn_input.setdefault(k, "undefined")
        calc_tn = compute_tn(tn_input)
        print("er 解密 =", got_tn)
        print("本地重算 =", calc_tn)
        print("tn 校验:", "一致 ✅" if got_tn == calc_tn else "不一致 ❌(拼接语义或字段集合有差异)")
    else:
        print("payload 中没有 er 字段(旧版本协议可能是明文 tn/nb)")

    if args.dump:
        for k in sorted(obj.keys()):
            print("%-16s %s" % (k, js_value_string(obj[k])[:100]))
    return 0


if __name__ == "__main__":
    sys.exit(main())