kj_auth.py 5.6 KB

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