Registration: PET fallback to the spd token map (the live-confirmed post-2FA delivery channel — the header was absent on that path); sign-in failures carry the response error shape

This commit is contained in:
fenleon committed 2026-09-29 13:25:15 +02:00
1 parent 9e00f95e3b
commit 322e24c6d2
2 files changed
+43 -4

No files matched your search

@@ -164,11 +164,34 @@ class GsaDelegateLoginClient(
val encoded = XmlPlist.encode(body)
val response = http.post(endpoint, headers, encoded, CONTENT_TYPE)
if (response.status != 200) {
throw GsaLoginException("iCloud sign-in POST $endpoint → HTTP ${response.status}")
throw GsaLoginException(
"iCloud sign-in POST $endpoint → HTTP ${response.status}${errorDetail(response.body)}",
)
}
return parseResponse(response.body)
}
/** Top-level error-shape facts only (keys, integer codes, error strings) — never token values. */
private fun errorDetail(bytes: ByteArray): String {
val parsed = try {
Plist.parse(bytes)
} catch (_: Exception) {
return ""
}
val dict = stringKeyedDictOrNull(parsed) ?: return " (${typeName(parsed)})"
val scalars = dict.entries
.mapNotNull { (k, v) ->
when {
v is Int || v is Long -> "$k=$v"
v is String && k !in setOf("dsid", "auth-token", "adsid") -> "$k=$v"
v is Map<*, *> -> "$k keys=${stringKeyedDictOrNull(v)?.keys ?: v.keys}"
else -> null
}
}
.joinToString(", ")
return " — body: $scalars"
}
private fun parseResponse(bytes: ByteArray): IcloudSignInResponse {
val parsed = try {
Plist.parse(bytes)
@@ -181,14 +181,27 @@ class GsaLoginChain(
val spd = complete.spd?.let { GsaSpd.parse(GsaSpdDecryptor.decrypt(it, proof.k)) }
return when (val secondaryAuth = complete.secondaryAuth) {
null -> {
val result = GsaLoginResult.Authenticated(spd = spd, pet = complete.pet)
// The PET rides the response headers when Apple sends them; on the post-2FA
// SRP re-login it arrives instead inside the spd token map under the PET
// service id (observed live — the header was absent on that path). A no-2FA
// login's spd carries only com.apple.gs.appleid.auth and no PET.
val pet = complete.pet
?: spd?.tokens[PET_SERVICE_ID]?.let {
GsaPet(
it.token,
it.expiresAtEpochMs
?: (clock() + GsaTokenHeaders.PET_DEFAULT_EXPIRY_SECONDS * 1000),
)
}
logEvent("gsa: pet=${complete.pet != null} spdToken=${pet != null} tokens=${spd?.tokens?.keys}")
val result = GsaLoginResult.Authenticated(spd = spd, pet = pet)
credentialStore?.save(
GsaPersistedCredentials(
username = username,
passwordHash = passwordHash,
adsid = spd?.adsid,
petToken = complete.pet?.token,
petExpiresAtEpochMs = complete.pet?.expiresAtEpochMs,
petToken = pet?.token,
petExpiresAtEpochMs = pet?.expiresAtEpochMs,
),
)
result
@@ -344,5 +357,8 @@ class GsaLoginChain(
companion object {
const val GSA_STATUS_SECONDARY = GsaStatus.AU_SECONDARY_AUTH
const val GSA_STATUS_TRUSTED = GsaStatus.AU_TRUSTED_DEVICE
/** The spd token-map service id under which the PET is delivered (§1.5 PET). */
const val PET_SERVICE_ID = "com.apple.gs.idms.pet"
}
}