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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user