mirror of
https://github.com/fenleon/open-madrid.git
synced 2026-10-22 16:12:35 +00:00
registration: the prefpane presents the LOGIN RUN's anisette set (session-wide reuse, wire-pinned)
The trapped-binary decode (2026-10-15) shows ONE anisette mint serves the whole session — GSA hops, 2FA validate, postdata, both prefpane POSTs — so the PET is paired with the OTP window it was issued under. The rev-25/26 'fresh set at the sign-in POST' reading is corrected (confounded with the replay-guard era). GsaLoginChain mints one cpd per login run and exposes it as lastLoginAnisette (Client-Time frozen at the mint); GsaDelegateLoginClient's prefpane branch reuses a supplied Client-Time instead of re-minting. VEN step-2 wire fact recorded in spec rev 46: zero anisette on initializeValidation, no step-2 binding. Also wire-proven live: the GSA init 'ps' field is REQUIRED (omission answers ec=-80001).
This commit is contained in:
1 parent
9da53e31f8
commit
2ff994751c
5 files changed
+43
-14
No files matched your search
@@ -127,6 +127,10 @@ class GsaCpd(
|
||||
anisette[AnisetteHeaderProvider.DEVICE_ID]?.let { put(AnisetteHeaderProvider.DEVICE_ID, it) }
|
||||
}
|
||||
|
||||
/** The set's `X-Apple-I-Client-Time` value (this mint's frozen time) — the delegate
|
||||
* sign-in reuses it (the accepted wire carries one Client-Time session-wide). */
|
||||
fun clientTime(): String? = anisette[AnisetteHeaderProvider.CLIENT_TIME]
|
||||
|
||||
companion object {
|
||||
/** The anisette header names the §1.5 cpd carries as keys (values from the provider). */
|
||||
val ANISETTE_KEYS = listOf(
|
||||
|
||||
@@ -164,7 +164,7 @@ class GsaDelegateLoginClient(
|
||||
// endpoint's verdict is ICLOUD_INVALID_DEVICE_TOKEN — the fabricated sha256(keychain-id)
|
||||
// form is the prime suspect for the 400. GSA keeps its recorded set (rev 15).
|
||||
val anisette = if (prefpane) {
|
||||
request.anisette
|
||||
val mapped = request.anisette
|
||||
.filterKeys { !it.equals("X-Apple-I-MD-LU", true) }
|
||||
// The accepted prefpane request's exact wire casing (live capture 2026-10-14,
|
||||
// captures/c56k/mint-repro/ob-req-002.bin): real AuthKit sends mixed-case
|
||||
@@ -176,9 +176,12 @@ class GsaDelegateLoginClient(
|
||||
mapOf(
|
||||
"X-Apple-Locale" to "en_US",
|
||||
"X-Apple-I-Timezone" to "UTC",
|
||||
// fresh per request, the get_headers Z form (capture 2026-10-14)
|
||||
"X-Apple-I-Client-Time" to iso8601Z(System.currentTimeMillis()),
|
||||
)
|
||||
// Client-Time rides the supplied set when it carries one — the LOGIN RUN's
|
||||
// frozen mint (the accepted wire reuses ONE anisette set session-wide,
|
||||
// 2026-10-15 decode); fresh only when the caller minted none.
|
||||
if (mapped.keys.any { it.equals("X-Apple-I-Client-Time", true) }) mapped
|
||||
else mapped + ("X-Apple-I-Client-Time" to iso8601Z(System.currentTimeMillis()))
|
||||
} else {
|
||||
request.anisette
|
||||
}
|
||||
|
||||
@@ -207,6 +207,8 @@ class GsaLoginClient(
|
||||
"A2k" to a2k,
|
||||
"cpd" to cpd,
|
||||
"o" to OPERATION_INIT,
|
||||
// Wire-proven 2026-10-15: `ps` is REQUIRED — omitting it (the trapped-binary
|
||||
// decode's "no ps" reading) answers ec=-80001 "Protocols are invalid" live.
|
||||
"ps" to protocols,
|
||||
"u" to username,
|
||||
),
|
||||
|
||||
@@ -150,9 +150,16 @@ class GsaLoginChain(
|
||||
private val delegateClient = GsaDelegateLoginClient(http, endpoint = delegateEndpoint, logEvent = logEvent)
|
||||
private val postdataClient = GsaPostdataClient(http, logEvent = logEvent)
|
||||
|
||||
/** §1.5: the filtered anisette header set + the akd-variant headers, one per login. */
|
||||
private suspend fun gsaHeaders(): Map<String, String> =
|
||||
headerConfig?.let { cpd().headersFor() + it.toHeaders() } ?: cpd().headersFor()
|
||||
/**
|
||||
* The anisette header set of the last completed login run — the same set its GSA hops
|
||||
* presented, `X-Apple-I-Client-Time` included (frozen at the run's mint). The delegate
|
||||
* sign-in must present THIS set: the accepted wire (trapped binary, 2026-10-15) reuses
|
||||
* ONE minted set across the entire session — GSA init/complete, 2FA validate, postdata,
|
||||
* the prefpane POSTs — so the PET is paired with the OTP window it was issued under.
|
||||
* Empty until the first login run.
|
||||
*/
|
||||
var lastLoginAnisette: Map<String, String> = emptyMap()
|
||||
private set
|
||||
|
||||
/**
|
||||
* The §1.5 postdata liveness event — the account device-record registration the reference
|
||||
@@ -200,8 +207,16 @@ class GsaLoginChain(
|
||||
*/
|
||||
suspend fun loginWithPasswordHash(username: String, passwordHash: ByteArray): GsaLoginResult {
|
||||
val requestUuid = newRequestUuid()
|
||||
val cpdDict = cpd().dictFor(requestUuid)
|
||||
val gsaHeaders = gsaHeaders()
|
||||
// ONE anisette mint per login run: the cpd dict and the POST headers carry the same
|
||||
// set (the accepted wire reuses one set across the whole session, 2026-10-15), and
|
||||
// the run's set is what the delegate sign-in must present (lastLoginAnisette).
|
||||
val cpdSet = cpd()
|
||||
val anisetteHeaders = cpdSet.headersFor()
|
||||
lastLoginAnisette = cpdSet.clientTime()
|
||||
?.let { anisetteHeaders + (AnisetteHeaderProvider.CLIENT_TIME to it) }
|
||||
?: anisetteHeaders
|
||||
val cpdDict = cpdSet.dictFor(requestUuid)
|
||||
val gsaHeaders = headerConfig?.let { anisetteHeaders + it.toHeaders() } ?: anisetteHeaders
|
||||
val srp = GsaSrpClient(random)
|
||||
val a2k = srp.begin()
|
||||
val init = loginClient.init(
|
||||
@@ -342,12 +357,16 @@ class GsaLoginChain(
|
||||
* the §1.1 blob (the chunk-C exchange mints it — caller-supplied here). [timezone] defaults
|
||||
* to the recorded value (§1.5 C59); [clientInfo] defaults to the recorded bracketed AOSKit
|
||||
* form when the device config is wired ([GsaHeaderConfig.signInClientInfo]), falling back to
|
||||
* the bare recorded AOSKit inner component. [anisette] must be FRESHLY minted for this
|
||||
* call — the one-time OTP headers spent on the GSA hops are refused here (observed live;
|
||||
* the reference client mints per request). [validationData] must be a FRESH mint per
|
||||
* sign-in (minted at sign-in time, never a login-start blob carried across the 2FA wait) —
|
||||
* a consumed blob answers HTTP 409, the stamp REPLAY refusal, not a blob rejection
|
||||
* (rev 26, §1.4's replay guard).
|
||||
* the bare recorded AOSKit inner component. [anisette] is the LOGIN RUN's set
|
||||
* ([lastLoginAnisette] — the same set the GSA hops of the PET's session presented,
|
||||
* Client-Time frozen at that mint): the accepted wire (trapped binary, 2026-10-15)
|
||||
* reuses ONE anisette set across the whole session, so the prefpane POST pairs the
|
||||
* PET with the OTP window it was issued under. The earlier "freshly minted here"
|
||||
* reading (rev 25/26) predates the loginDelegates endpoint fix and is confounded
|
||||
* with the stamp replay-guard refusals of that era. [validationData] must be a FRESH
|
||||
* mint per sign-in (minted at sign-in time, never a login-start blob carried across
|
||||
* the 2FA wait) — a consumed blob answers HTTP 409, the stamp REPLAY refusal, not a
|
||||
* blob rejection (rev 26, §1.4's replay guard).
|
||||
*/
|
||||
suspend fun idsDelegateCredentials(
|
||||
username: String,
|
||||
|
||||
Reference in new issue
Block a user