115 lines
4.3 KiB
Python
115 lines
4.3 KiB
Python
"""YYB 支付协议常量、材料校验与脱敏指纹。
|
|
|
|
这里仅收敛已由成功请求验证的固定值,不负责推断或修改协议字段。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
PAY_APPID = "1450243039"
|
|
PAY_GOODS_URL = "https://pay.qq.com/midas/minipay_v2/views/cpay/goods.shtml"
|
|
PAY_SAVE_URL = f"https://api.unipay.qq.com/v1/r/{PAY_APPID}/web_save"
|
|
PAY_PAGE_INFO_URL = f"https://api.unipay.qq.com/v1/r/{PAY_APPID}/web_page_info"
|
|
PAY_FP_URL = "https://api.unipay.qq.com/cgi-bin/fp-behv.fcg"
|
|
MALL_API_URL = "https://storeapi.pay.qq.com/api/CommonCallMpgo"
|
|
|
|
PAY_WEB_VERSION = "web_1.0.6"
|
|
PAY_WEBVERSION = "minipayv2"
|
|
DEFAULT_ORDER_PF = "mds_myappjp-__mds_myappjp_PC_aW9zd2hpdGVsaX0-android"
|
|
OLD_ENCRYPT_OFFERIDS = ("1450007826", "1110165571")
|
|
|
|
# 这两个 UA 分别来自已成功的 goods 和商城请求,不能因为版本不同而强行统一。
|
|
GOODS_USER_AGENT = (
|
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
|
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36"
|
|
)
|
|
MALL_USER_AGENT = (
|
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
|
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36"
|
|
)
|
|
|
|
|
|
def _sha256_prefix_bytes(value: bytes) -> str:
|
|
return hashlib.sha256(value).hexdigest()[:12]
|
|
|
|
|
|
def _json_fingerprint(value: Any) -> str:
|
|
raw = json.dumps(value, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
|
return _sha256_prefix_bytes(raw)
|
|
|
|
|
|
def file_fingerprint(path: Path) -> str:
|
|
"""返回协议文件的短哈希,用于日志比对,不暴露内容。"""
|
|
return _sha256_prefix_bytes(path.read_bytes())
|
|
|
|
|
|
def validate_goods_materials(
|
|
args_template: list, xmidas: list[int] | None = None
|
|
) -> None:
|
|
"""校验 goods webSave VM 所需静态表结构,发现升级时尽早失败。"""
|
|
if not isinstance(args_template, list) or len(args_template) != 18:
|
|
raise ValueError("goods args-template 必须是 18 槽数组")
|
|
for index in (0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16):
|
|
if not isinstance(args_template[index], list) or not args_template[index]:
|
|
raise ValueError(f"goods args-template 槽 {index} 缺失")
|
|
expected_lengths = {
|
|
0: 16,
|
|
1: 256,
|
|
2: 256,
|
|
3: 256,
|
|
4: 256,
|
|
5: 256,
|
|
6: 16,
|
|
10: 528,
|
|
11: 1024,
|
|
12: 1024,
|
|
13: 256,
|
|
14: 256,
|
|
15: 256,
|
|
16: 256,
|
|
}
|
|
for index, expected in expected_lengths.items():
|
|
value = args_template[index][0]
|
|
if not isinstance(value, list) or len(value) != expected:
|
|
actual = len(value) if isinstance(value, list) else "非数组"
|
|
raise ValueError(
|
|
f"goods args-template 槽 {index} 长度异常: {actual} != {expected}"
|
|
)
|
|
if xmidas is not None and len(xmidas) != 59640:
|
|
raise ValueError(f"goods xMidasOps 长度异常: {len(xmidas)} != 59640")
|
|
|
|
|
|
def validate_mall_materials(transform_fixed: dict[str, Any]) -> None:
|
|
"""校验 mall 固定槽模板;动态槽仍由当前会话的 GetPayToken 填充。"""
|
|
if not isinstance(transform_fixed, dict):
|
|
raise TypeError("mall transform-fixed 必须是对象")
|
|
required = {
|
|
str(index) for index in (1, 2, 3, 4, 5, 7, 8, 9, 11, 12, 13, 14, 15, 16, 17)
|
|
}
|
|
missing = sorted(required - set(transform_fixed))
|
|
if missing:
|
|
raise ValueError(f"mall transform-fixed 缺少槽: {', '.join(missing)}")
|
|
for index in required:
|
|
if not isinstance(transform_fixed[index], list):
|
|
raise TypeError(f"mall transform-fixed 槽 {index} 不是数组")
|
|
|
|
|
|
def goods_material_diagnostics(
|
|
root: Path, args_template: list, xmidas: list[int]
|
|
) -> dict[str, Any]:
|
|
"""构造脱敏协议指纹,便于区分页面升级和服务端业务拒绝。"""
|
|
validate_goods_materials(args_template, xmidas)
|
|
replay = root / "replay"
|
|
return {
|
|
"goods_xmidas_ops_length": len(xmidas),
|
|
"args_template_slots": len(args_template),
|
|
"args_template_sha256_prefix": _json_fingerprint(args_template),
|
|
"bytecode_sha256_prefix": file_fingerprint(replay / "bytecode.json"),
|
|
"constants_sha256_prefix": file_fingerprint(replay / "constants.json"),
|
|
"web_version": PAY_WEB_VERSION,
|
|
}
|