docs(fifa17): harden patched-client session binding

Record the per-IP -> per-session correction: why source-IP-only was unsafe (two
FIFA processes share an IP), the authoritative per-login X-UT-SID key with IP and
persona as auxiliary, the Capability/StoreMode state machine, the single-use
short-TTL launcher->session pending hand-off, activity-based session cleanup, and
the documented fail-closed residual for genuinely simultaneous same-(ip,persona)
logins. Design history is retained; the per-IP prototype is marked superseded.
This commit is contained in:
funman300
2026-08-13 04:39:53 +00:00
parent 805d754dc8
commit a82407c686
@@ -226,3 +226,75 @@ freeze · capability disappears after a sentinel freeze — **all resolve to the
The active-sentinel implementation is **not** removed. It is the else-branch of the
switch and the universal default for unpatched clients, unsupported builds, failed
patches, unknown launchers, and late capabilities. The clean path is purely additive.
---
## 13. Session binding (hardening — supersedes the per-IP prototype)
**History.** The first implementation keyed the backend capability/store-mode by
**source IP alone** (§7 as originally written). That was rejected before deployment:
two FIFA processes that share a source IP — concurrent, or a relaunch — would share
the key, so an *unverified* process could inherit a *verified* one's `clean-v1`
topology and crash on the empty-My-Packs resolver. Source IP is now **auxiliary only**
(logging, a fail-closed sid/ip sanity check, and the pending hand-off key). This
history is retained deliberately; do not treat per-IP as the design.
**Authoritative key = the per-login UTAS session id (`X-UT-SID`).** `/ut/auth` now
mints a fresh unique SID per login (was a shared constant `OPENFUT-SID-…0001`); the
client echoes it on every later call, and it is **live-confirmed present on real
`/store/purchasegroup` requests**. The SID uniquely identifies one FIFA process/login:
a relaunch re-auths → new SID; two concurrent logins → two SIDs. The legacy constant
is still accepted by the retired security-question gate only, and is **never** used to
grant `clean-v1`. A store request whose SID was opened on a different source IP is
fail-closed to sentinel (sid/ip sanity check).
**Why not persona alone:** the backend is single-account, so `personaId` cannot
distinguish two sessions, and a relaunch keeps the same persona — persona alone would
leak a prior session's mode. Persona is used only (with IP) to key the pending hand-off.
### State machine (per session, keyed by SID)
```
Capability : Unknown | ResolverV1
StoreMode : Unfrozen | Sentinel | CleanV1
/ut/auth (new SID) : Capability=Unknown, StoreMode=Unfrozen, record {ip,persona}
+ consume any pending (ip,persona) -> Capability=ResolverV1
capability registered : bind to the one live Unfrozen/Unbound session for (ip,persona)
-> Capability=ResolverV1 ; else stage single-use pending ;
else (a session exists but is frozen/ambiguous) -> ignored-late
first /store/purchasegroup : Unfrozen + ResolverV1 -> freeze CleanV1
Unfrozen + otherwise -> freeze Sentinel (consume pending first)
late capability : StoreMode already frozen -> unchanged (ignored-late, not staged)
capability lost/cleared : after a CleanV1 freeze -> stays CleanV1 (mode is cached)
session idle > TTL / reaped: session discarded (a later store with that SID -> Sentinel)
```
### Registration order + pending hand-off
The verified capability is known only after the FIFA process exists, CardsDLL is
loaded, and autopatch confirms the JG bytes — which may land before or after
`/ut/auth`, but reliably before the user opens the Store. The launcher cannot know
the SID, so its registration is matched to a session by (source_ip, persona) as a
**single-use, short-TTL pending** (`FIFA17_PENDING_TTL = 120s`) that is consumed by
exactly one session, at whichever of these happens first for that session: its
`/ut/auth` (pending predates login), the registration itself (session already live —
bound directly), or its first store request (lazy). If the Store is reached before a
capability binds, the session freezes **Sentinel** (fail-closed); a later capability
does not change it.
### Session cleanup (Task 10)
- **creation:** at `/ut/auth`.
- **last activity:** bumped on every `/store/purchasegroup` for the session.
- **freeze:** first `/store/purchasegroup`.
- **expiry:** lazy sweep on every session op removes sessions idle for
`FIFA17_SESSION_TTL = 3600s` and pendings older than `FIFA17_PENDING_TTL`. Explicit
Blaze/UTAS teardown is not reliably observable at this handler, so a conservative
activity-based TTL is used instead. Reaping only removes *expired* entries and never
affects another live session from the same IP/persona (keyed by distinct SIDs).
### Residual limitation (documented, fail-closed)
FIFA carries no launcher-controllable per-process token, so two **simultaneous** logins
from the **same (ip, persona)** cannot be disambiguated at the instant a capability is
registered while *both* are Unfrozen/Unbound. That ambiguous case resolves to
`ignored-late`**both freeze Sentinel** (safe: an unverified process is never granted
clean). The normal one-launcher-per-FIFA and sequential-relaunch flows bind correctly
(proven by matrix K/L/M). This is a UX conservativeness, never a safety hole.