Files
OpenFUT/fifa17-recon/tools/test_security_question.py
T
funman300 8cba70dc90 fifa17-recon: reconcile authoritative tools with running backend (B)
- Add 8 files present in docker/fifa17-python/tools but missing from the
  top-level tree: fut_accounts.py + 7 test_*.py contracts (all committed in
  the server's docker tree; byte-identical to the running image).
- Preserve newer responder work already matching the running container:
  utas_server.py (offlineSeason), lsx_responder_v2.py (OPENFUT_BIND),
  blaze_responder_v3b.py, autopatch.py, pow_server.py, fut_store.py,
  test_fut_contract.py, fifa17-hook-m1.sh.
- Add 30 newer ghidra_queries (draft purchase/state, SBC 9-26, runtime
  registries). Local tree is now a strict superset of B with all shared
  files byte-identical.
2026-08-10 17:08:06 -07:00

135 lines
5.1 KiB
Python

#!/usr/bin/env python3
"""Regression tests for FIFA 17's account-scoped phishing/security gate."""
import importlib
import json
import os
import sys
import tempfile
TOOLS = os.path.dirname(os.path.abspath(__file__))
if TOOLS not in sys.path:
sys.path.insert(0, TOOLS)
DEVICE_ID = "1" * 32
TRANSFORMED_ANSWER = "a" * 32 # sanitized replay value, not a real answer
class Request:
def __init__(self, method, path, sid=None):
self.command = method
self.path = path
self.headers = {"X-UT-SID": sid} if sid is not None else {}
self._body = b""
def request(utas_server, method, suffix, sid=None):
sid = utas_server.SID if sid is None else sid
h = Request(method, "/ut/game/fifa17/phishing/" + suffix, sid)
return utas_server.security_question_route(h)
def profile(state, persona_id):
path = os.path.join(state, "accounts", str(persona_id), "fifa17_profile.json")
with open(path) as f:
return json.load(f)
def main():
with tempfile.TemporaryDirectory() as state:
os.environ["FUT_ACCOUNT_PATH"] = os.path.join(state, "active_account.json")
os.environ["FUT_PROFILE_ROOT"] = os.path.join(state, "accounts")
os.environ.pop("FUT_PROFILE", None)
import fut_account
import fut_store
import fut_accounts
import utas_server
importlib.reload(fut_account)
importlib.reload(fut_store)
importlib.reload(fut_accounts)
importlib.reload(utas_server)
# New/missing state: launcher account selection initializes one account only.
fut_accounts.activate({"personaId": 771001, "personaName": "SEC_A"})
p = profile(state, 771001)
assert p["securityQuestion"] == {"version": 1, "verified": True}
# Actual trusted-device response fields parsed by CardsDLL 0x18012a170.
code, body = request(
utas_server, "GET", "trusteddevice?deviceId=" + DEVICE_ID)
assert code == 200
assert body == {
"changed": False,
"exists": True,
"locked": False,
"trusted": True,
}
# Existing initialized state survives a fresh Store instance/process view.
reopened = fut_store.Store(fut_store.profile_path_for(771001))
assert reopened.profile()["securityQuestion"] == {
"version": 1, "verified": True}
# FIFA's observed repeat-session request: POST, empty body, opaque 32-hex
# deviceId and transformed answer in the query string. The answer is accepted
# for OpenFUT compatibility but never persisted.
code, body = request(
utas_server,
"POST",
"validate?deviceId=%s&answer=%s" % (DEVICE_ID, TRANSFORMED_ANSWER),
)
assert (code, body) == (200, {})
saved = profile(state, 771001)
assert TRANSFORMED_ANSWER not in json.dumps(saved)
# Question lookup uses the three fields parsed by CardsDLL 0x180129850.
code, body = request(
utas_server, "GET", "question?deviceId=" + DEVICE_ID)
assert code == 200
assert set(body) == {"question", "attempts", "recoverAttempts"}
assert all(isinstance(body[k], int) for k in body)
# Malformed values/methods and missing sessions fail explicitly.
code, _ = request(utas_server, "POST", "validate?deviceId=bad&answer=bad")
assert code == 400
code, _ = request(
utas_server, "DELETE", "trusteddevice?deviceId=" + DEVICE_ID)
assert code == 405
h = Request(
"GET", "/ut/game/fifa17/phishing/trusteddevice?deviceId=" + DEVICE_ID)
code, _ = utas_server.security_question_route(h)
assert code == 400
# Ordinary request logging must redact answer query values.
raw_path = "/ut/game/fifa17/phishing/validate?deviceId=%s&answer=%s" % (
DEVICE_ID, TRANSFORMED_ANSWER)
safe_path = utas_server.safe_request_path(raw_path)
assert TRANSFORMED_ANSWER not in safe_path
assert "answer=%5BREDACTED%5D" in safe_path
# Multiple profiles receive independent persisted state; selecting B must not
# alter A's initialized record.
fut_accounts.activate({"personaId": 771002, "personaName": "SEC_B"})
assert profile(state, 771002)["securityQuestion"] == {
"version": 1, "verified": True}
assert profile(state, 771001)["securityQuestion"] == {
"version": 1, "verified": True}
# Legacy profile with the field removed is repaired once and persisted.
b_path = os.path.join(state, "accounts", "771002", "fifa17_profile.json")
b = profile(state, 771002)
b.pop("securityQuestion")
with open(b_path, "w") as f:
json.dump(b, f)
fut_store.STORE._p = None
code, body = request(
utas_server, "GET", "trusteddevice?deviceId=" + DEVICE_ID)
assert code == 200 and body["exists"] and body["trusted"]
assert profile(state, 771002)["securityQuestion"]["verified"] is True
print("security-question compatibility: PASS")
if __name__ == "__main__":
main()