test_box_auth.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. """Tests for the SAZ-verified X-BOX-AUTHORIZATION signer (box_auth.py).
  2. The signature algorithm was verified EXACT against 4 live captures
  3. (see scripts/verify_signature_replay.py — 4/4 MATCH).
  4. """
  5. import hashlib
  6. import hmac
  7. import json
  8. from cq2_sdk.login.box_auth import (
  9. build_auth_header,
  10. sign_body,
  11. prepare_request,
  12. gen_random_32,
  13. RENDER_SIGN_KEY,
  14. BRIDGE_SIGN_KEY,
  15. APP_VERSION,
  16. )
  17. def test_gen_random_32():
  18. n = gen_random_32()
  19. assert len(n) == 32
  20. assert all(c in "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" for c in n)
  21. assert gen_random_32() != gen_random_32()
  22. # ---- Layer 2: X-BOX-AUTHORIZATION ----
  23. def test_auth_header_format():
  24. auth = build_auth_header(
  25. method="POST",
  26. url="https://api.jiyuhz.cn/auth/v3/login/safe/byPwd",
  27. body_json="{}",
  28. nonce="FIXEDNONCE123",
  29. timestamp_ms=1789459934000,
  30. )
  31. assert auth.startswith("BOXAPP-SHA256 NONCE=FIXEDNONCE123;")
  32. assert "TIMESTAMP=1789459934000;" in auth
  33. sig = auth.split("SIGNATURE=")[1]
  34. assert len(sig) == 64
  35. assert all(c in "0123456789abcdef" for c in sig)
  36. def test_auth_uses_path_not_full_url():
  37. """KEY FINDING (brute-force verified): signature uses URL path(+query),
  38. NOT the absolute URL."""
  39. a = build_auth_header(
  40. method="POST",
  41. url="https://user-api.996hezi.com/v1/WyAnticheat/riskCheck?x=1",
  42. body_json=None, nonce="N", timestamp_ms=1,
  43. )
  44. b = build_auth_header(
  45. method="POST",
  46. url="/v1/WyAnticheat/riskCheck?x=1",
  47. body_json=None, nonce="N", timestamp_ms=1,
  48. )
  49. assert a == b
  50. def test_auth_deterministic():
  51. kwargs = dict(method="GET", url="/api", body_json=None, nonce="N", timestamp_ms=1)
  52. assert build_auth_header(**kwargs) == build_auth_header(**kwargs)
  53. def test_auth_hmac_manual():
  54. """Verify against a hand-computed HMAC."""
  55. value = "GET\n/api\n1000\nNONCE\npc\ndevice\n5.1.1.9007\n\nUA\n\n"
  56. expected = hmac.new(BRIDGE_SIGN_KEY.encode(), value.encode(), hashlib.sha256).hexdigest()
  57. auth = build_auth_header(
  58. method="GET", url="/api", body_json=None, token="", device_id="device",
  59. user_agent="UA", nonce="NONCE", timestamp_ms=1000,
  60. )
  61. assert auth.endswith(expected)
  62. # ---- Layer 1: body sign ----
  63. def test_sign_body_adds_fields():
  64. body = {"a": "1", "b": "2"}
  65. signed = sign_body(body, rqtime=1000, rqrandom="RND")
  66. assert signed["rqtime"] == 1000
  67. assert signed["rqrandom"] == "RND"
  68. assert "sign" in signed
  69. assert len(signed["sign"]) == 32 # md5 hex
  70. def test_sign_body_manual_md5():
  71. body = {"username": "u", "password": "p"}
  72. signed = sign_body(body, rqtime=777, rqrandom="ABC")
  73. # sorted keys: password, username -> concat "pu"
  74. concat = "pu" + RENDER_SIGN_KEY + "777" + "ABC"
  75. expected = hashlib.md5(concat.encode()).hexdigest()
  76. assert signed["sign"] == expected
  77. def test_sign_body_object_values_json_stringified():
  78. body = {"x": {"k": 1}}
  79. signed = sign_body(body, rqtime=1, rqrandom="Z")
  80. # object value -> JSON string (compact)
  81. concat = '{"k":1}' + RENDER_SIGN_KEY + "1Z"
  82. expected = hashlib.md5(concat.encode()).hexdigest()
  83. assert signed["sign"] == expected
  84. # ---- prepare_request ----
  85. def test_prepare_request_no_layer1_by_default():
  86. body = {"loginInfo": "abc", "needConfirmLogin": True}
  87. wire, auth = prepare_request(
  88. method="POST",
  89. url="https://api.jiyuhz.cn/auth/v3/login/safe/byPwd",
  90. body=body,
  91. nonce="N",
  92. timestamp_ms=1,
  93. )
  94. parsed = json.loads(wire.decode())
  95. assert "rqtime" not in parsed # Layer1 OFF by default (SAZ-verified behavior)
  96. assert "sign" not in parsed
  97. assert auth.startswith("BOXAPP-SHA256")
  98. def test_prepare_request_logininfo_urlsafe():
  99. body = {"loginInfo": "ab+cd/ef", "needConfirmLogin": True}
  100. wire, _ = prepare_request(
  101. method="POST",
  102. url="https://x.com/login",
  103. body=body,
  104. encode_login_info_urlsafe=True,
  105. nonce="N",
  106. timestamp_ms=1,
  107. )
  108. parsed = json.loads(wire.decode())
  109. assert parsed["loginInfo"] == "ab%2Bcd%2Fef" # + -> %2B, / -> %2F
  110. def test_prepare_request_get_no_body():
  111. wire, auth = prepare_request(
  112. method="GET",
  113. url="https://pc-api.996hezi.com/box/v1/getTodayOpen?gameId=206923",
  114. nonce="N",
  115. timestamp_ms=1,
  116. )
  117. assert wire is None
  118. assert auth.startswith("BOXAPP-SHA256 NONCE=N;")