| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117 |
- # -*- coding: utf-8 -*-
- # Author : Charley
- # Python : 3.12.10
- # Date : 2026/07/02
- """卡集 App Open-Auth-Sig 请求签名模块。
- 卡集(card.kaji.com,uni-app / DCloud)的鉴权头 Authorization 采用 Open-Auth-Sig
- 方案:每个请求用 [随机 swap + 当前时间戳 + 随机 nonce + 接口路径] 拼串做标准 MD5,
- 再经一张 256 项固定置换表编码成 Signature。全程不含任何密钥 / appSecret / 登录 token,
- 纯客户端即可复现,因此爬虫可长期无人值守运行,不存在凭证过期问题。
- 算法来源:app-service.js(APK 反编译产物)模块 "665c" 的 GetCrypto 函数,
- 已用真实抓包样本逐字节验证通过。videoPlay 的 body 签名来自偏移 1203166 处的 video_sign。
- """
- import time
- import random
- import hashlib
- # 256 项置换表:来自 app-service.js 偏移约 1513290 处的常量数组 i,是 0..255 的一个全排列。
- # gen_signature 会先按 swap 的 4 对字节对它做两两交换,再逐字符查表。
- I_TABLE = [
- 194, 115, 46, 172, 195, 57, 45, 12, 78, 181, 246, 168, 16, 218, 68, 248,
- 242, 184, 7, 190, 216, 59, 160, 189, 14, 117, 113, 72, 210, 0, 120, 69,
- 15, 48, 141, 128, 34, 155, 19, 38, 110, 63, 76, 240, 35, 49, 56, 47,
- 229, 196, 118, 244, 9, 174, 238, 252, 132, 143, 219, 21, 165, 22, 158, 119,
- 147, 85, 29, 233, 236, 146, 88, 27, 30, 5, 123, 221, 255, 83, 24, 247,
- 70, 111, 11, 4, 31, 159, 103, 6, 185, 52, 98, 75, 95, 142, 50, 3,
- 2, 203, 178, 109, 193, 64, 108, 179, 62, 94, 133, 180, 192, 17, 105, 214,
- 245, 28, 226, 87, 227, 215, 102, 201, 107, 202, 149, 124, 13, 208, 138, 188,
- 1, 54, 127, 139, 239, 209, 162, 243, 126, 144, 204, 100, 130, 187, 131, 207,
- 211, 217, 43, 80, 122, 112, 151, 154, 156, 161, 36, 205, 39, 140, 74, 25,
- 37, 164, 177, 33, 41, 61, 66, 234, 92, 44, 96, 79, 90, 99, 166, 153,
- 182, 150, 20, 106, 77, 93, 157, 135, 152, 104, 223, 173, 81, 134, 175, 230,
- 199, 250, 82, 10, 235, 222, 200, 114, 89, 148, 237, 65, 167, 125, 42, 198,
- 60, 129, 101, 228, 170, 86, 249, 55, 8, 97, 176, 73, 251, 213, 26, 136,
- 254, 169, 145, 225, 186, 32, 212, 197, 163, 241, 84, 67, 91, 253, 51, 224,
- 40, 191, 231, 53, 183, 58, 220, 116, 206, 232, 18, 71, 171, 23, 121, 137
- ]
- # videoPlay body 签名的固定盐,来自 app-service.js 偏移 1203166:
- # sign = md5(f"{ts}_{goodCode}_{playCode}_videoPlayKsj")
- VIDEO_SIGN_SALT = "videoPlayKsj"
- # 接口统一前缀,对应 app-service.js 里 o.app.requestVersion
- API_PREFIX = "/api/v4/"
- def _md5_hex(text: str) -> str:
- """计算字符串的标准 MD5 十六进制摘要。
- Args:
- text (str): 待哈希的明文串。
- Returns:
- str: 32 位小写十六进制 MD5 值。
- """
- return hashlib.md5(text.encode("utf-8")).hexdigest()
- def gen_signature(f_hex: str, c: list[int]) -> str:
- """将 MD5 十六进制串经置换表编码为 128 位 Signature。
- 对应 app-service.js 模块 665c 的编码逻辑:先把 32 字符的 md5-hex 当作 ASCII
- 字符串再转 hex(得到 64 字符),复制置换表并按 swap 的 4 对字节做两两交换,
- 最后对每个字符以其 ASCII 码作下标查表,输出 2 位大写 hex 拼接。
- Args:
- f_hex (str): 拼接串的 MD5 值(32 位小写 hex)。
- c (list[int]): swap 对应的 8 个字节(0-255),决定置换表的 4 对交换。
- Returns:
- str: 128 位大写十六进制的 Signature。
- """
- r = f_hex.encode("ascii").hex() # md5-hex 当 ASCII 再转 hex -> 64 字符
- a = list(I_TABLE) # 复制置换表,避免污染全局常量
- for s in range(0, len(c), 2): # 按 swap 的 4 对字节两两交换
- a[c[s]], a[c[s + 1]] = a[c[s + 1]], a[c[s]]
- out = [format(a[ord(ch)], "02X") for ch in r] # 逐字符以 ASCII 码查表
- return "".join(out)
- def gen_authorization(path: str) -> str:
- """为指定接口路径生成完整的 Open-Auth-Sig 请求头值。
- 每次调用都用当前时间戳与随机 swap / nonce 现算,天然免过期、免登录 token。
- Args:
- path (str): 接口相对路径,不含域名与 query,如 "goodlist/forsale/main"。
- 函数内部自动补 API_PREFIX 前缀、去掉 "/dataApi" 子串与 query。
- Returns:
- str: 形如 'Open-Auth-Sig Swap="..",Timestamp=..,Nonce=..,Signature=".."'。
- """
- ts = int(time.time()) # 当前 Unix 秒
- noce = random.randint(1, 500) # 随机 nonce,闭区间 [1,500]
- full_path = (API_PREFIX + path).replace("/dataApi", "").split("?")[0]
- c = random.sample(range(256), 8) # 8 个不重复字节
- swap = "".join(f"{b:02x}" for b in c) # 小写 hex,16 字符
- body = f"{swap}_{ts}_{noce}_{full_path}" # 拼接顺序:swap_ts_noce_path
- sig = gen_signature(_md5_hex(body), c)
- return f'Open-Auth-Sig Swap="{swap}",Timestamp={ts},Nonce={noce},Signature="{sig}"'
- def video_sign(ts: int, good_code: str, play_code: str) -> str:
- """计算 good/videoPlay 接口 body 里的 sign。
- 对应 app-service.js 偏移 1203166:sign = md5(f"{ts}_{goodCode}_{playCode}_videoPlayKsj")。
- Args:
- ts (int): Unix 秒时间戳,与 body 里的 ts 字段保持一致。
- good_code (str): 商品编码,取自请求 URL good/videoPlay/{goodCode}。
- play_code (str): 播放码,来自商品详情接口。
- Returns:
- str: 32 位小写十六进制的 sign。
- """
- return _md5_hex(f"{ts}_{good_code}_{play_code}_{VIDEO_SIGN_SALT}")
|