SMS 2FA fails with HTTP 412 when the trusted phone number ID isn't 1

Aperta
#424 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
3/5
Tempo stimato
1-2 giorni
Idoneità per principianti
48/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
swift

Direzione di ricerca

Start with Sources/XKit/GrandSlam/Operations/GrandSlamTwoFactorAuthenticateOperation.swift and the SMS request types in Sources/XKit/GrandSlam/Requests/Concrete/GrandSlamTrustedDeviceRequest.swift and GrandSlamValidateRequest.swift. Trace how HTTP 412 responses and trusted phone number data are handled, then check existing tests for these requests. Done means SMS login works when the trusted phone number ID is not 1, including code validation; the issue reports a successful manual test but names no automated test.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

Description

SMS 2FA login fails on accounts whose trusted phone number doesn't have ID 1. performSMSAuth hardcodes phoneNumberID = 1, so Apple rejects the request and no SMS is sent:

Logging in...
Error: PUT https://gsa.apple.com/auth/verify/phone failed: HTTP 412. Details:

The response lists the valid numbers (personal data removed):

{
  "success": 0,
  "trustedPhoneNumbers": [
    { "nonFTEU": true, "pushMode": "sms", "id": 2 }
  ],
  "mode": "sms"
}

Steps to reproduce

  1. Use an Apple ID with no Apple devices, whose only trusted number has an ID other than 1.
  2. Run xtool auth login and choose 1: Password.
  3. Enter the Apple ID and password.

Environment

  • Host OS: Linux, Ubuntu 26.04.1 LTS, x86_64
  • xtool: commit bec5aa106 (includes #416), debug build
  • Swift version: 6.4.1
  • iOS device version: n/a (no iOS device on the account)

Proposed fix

On a 412, read trustedPhoneNumbers from the response and retry with the first entry's id and nonFTEU. Use the same values when validating the code:

} catch let error as HTTPResponseError where error.status.code == 412 {
    // the ID is not always 1; Apple lists the valid ones in the 412 response
    guard let trusted = error.body
        .flatMap({ try? JSONDecoder().decode(PhoneVerificationResponse.self, from: Data($0.utf8)) })?
        .trustedPhoneNumbers.first
    else { throw error }
    phoneNumber = trusted
    try await sendSMS(to: phoneNumber)
}

The full patch is below.

Full patch
diff --git a/Sources/XKit/GrandSlam/Operations/GrandSlamTwoFactorAuthenticateOperation.swift b/Sources/XKit/GrandSlam/Operations/GrandSlamTwoFactorAuthenticateOperation.swift
index 5a41e02..a0f1dde 100644
--- a/Sources/XKit/GrandSlam/Operations/GrandSlamTwoFactorAuthenticateOperation.swift
+++ b/Sources/XKit/GrandSlam/Operations/GrandSlamTwoFactorAuthenticateOperation.swift
@@ -25,11 +25,37 @@ struct GrandSlamTwoFactorAuthenticateOperation {
     let loginData: GrandSlamLoginData
     unowned let delegate: TwoFactorAuthDelegate
 
+    private struct TrustedPhoneNumber: Decodable {
+        let id: Int
+        var nonFTEU: Bool?
+    }
+
+    private struct PhoneVerificationResponse: Decodable {
+        let trustedPhoneNumbers: [TrustedPhoneNumber]
+    }
+
+    private func sendSMS(to phoneNumber: TrustedPhoneNumber) async throws {
+        try await client.send(GrandSlamSMSAuthRequest(
+            loginData: loginData,
+            phoneNumberID: phoneNumber.id,
+            nonFTEU: phoneNumber.nonFTEU
+        ))
+    }
+
     private func performSMSAuth() async throws {
-        // TODO: parse phone number list from o=complete req?
-        let phoneNumberID = 1
+        var phoneNumber = TrustedPhoneNumber(id: 1)
         do {
-            try await client.send(GrandSlamSMSAuthRequest(loginData: loginData, phoneNumberID: phoneNumberID))
+            do {
+                try await sendSMS(to: phoneNumber)
+            } catch let error as HTTPResponseError where error.status.code == 412 {
+                // the ID is not always 1; Apple lists the valid ones in the 412 response
+                guard let trusted = error.body
+                    .flatMap({ try? JSONDecoder().decode(PhoneVerificationResponse.self, from: Data($0.utf8)) })?
+                    .trustedPhoneNumbers.first
+                else { throw error }
+                phoneNumber = trusted
+                try await sendSMS(to: phoneNumber)
+            }
         } catch let error as HTTPResponseError where error.status.code == 403 {
             d.out.print("""
             warning: Could not send verification code via SMS. Falling back to trusted device authentication...
@@ -37,15 +63,16 @@ struct GrandSlamTwoFactorAuthenticateOperation {
             try await performTrustedDeviceAuth()
             return
         }
-        try await validateCode(phoneNumberID: phoneNumberID)
+        try await validateCode(phoneNumber: phoneNumber)
     }
 
-    private func validateCode(phoneNumberID: Int? = nil) async throws {
+    private func validateCode(phoneNumber: TrustedPhoneNumber? = nil) async throws {
         guard let code = await self.delegate.fetchCode() else { throw Error.userCancelled }
-        let request: any GrandSlamRequest = if let phoneNumberID {
+        let request: any GrandSlamRequest = if let phoneNumber {
             GrandSlamValidateSMSRequest(
                 loginData: loginData,
-                phoneNumberID: phoneNumberID,
+                phoneNumberID: phoneNumber.id,
+                nonFTEU: phoneNumber.nonFTEU,
                 verificationCode: code
             )
         } else {
diff --git a/Sources/XKit/GrandSlam/Requests/Concrete/GrandSlamTrustedDeviceRequest.swift b/Sources/XKit/GrandSlam/Requests/Concrete/GrandSlamTrustedDeviceRequest.swift
index 628ba9f..545a7f4 100644
--- a/Sources/XKit/GrandSlam/Requests/Concrete/GrandSlamTrustedDeviceRequest.swift
+++ b/Sources/XKit/GrandSlam/Requests/Concrete/GrandSlamTrustedDeviceRequest.swift
@@ -39,6 +39,7 @@ struct GrandSlamSMSAuthRequest: GrandSlamTwoFactorRequest {
 
     let loginData: GrandSlamLoginData
     let phoneNumberID: Int
+    var nonFTEU: Bool?
 
     var extraHeaders: [String: String] {
         [
@@ -47,9 +48,15 @@ struct GrandSlamSMSAuthRequest: GrandSlamTwoFactorRequest {
         ]
     }
 
+    private var phoneNumber: [String: Any] {
+        var phoneNumber: [String: Any] = ["id": phoneNumberID]
+        if let nonFTEU { phoneNumber["nonFTEU"] = nonFTEU }
+        return phoneNumber
+    }
+
     func content(anisetteData: AnisetteData) -> GrandSlamRequestContent {
         .put([
-            "phoneNumber": ["id": phoneNumberID],
+            "phoneNumber": phoneNumber,
             "mode": "sms",
         ], type: .json)
     }
diff --git a/Sources/XKit/GrandSlam/Requests/Concrete/GrandSlamValidateRequest.swift b/Sources/XKit/GrandSlam/Requests/Concrete/GrandSlamValidateRequest.swift
index b93c614..b9f1e13 100644
--- a/Sources/XKit/GrandSlam/Requests/Concrete/GrandSlamValidateRequest.swift
+++ b/Sources/XKit/GrandSlam/Requests/Concrete/GrandSlamValidateRequest.swift
@@ -34,6 +34,7 @@ struct GrandSlamValidateSMSRequest: GrandSlamTwoFactorRequest {
 
     let loginData: GrandSlamLoginData
     let phoneNumberID: Int
+    var nonFTEU: Bool?
     let verificationCode: String
 
     var extraHeaders: [String: String] {
@@ -43,10 +44,16 @@ struct GrandSlamValidateSMSRequest: GrandSlamTwoFactorRequest {
         ]
     }
 
+    private var phoneNumber: [String: Any] {
+        var phoneNumber: [String: Any] = ["id": phoneNumberID]
+        if let nonFTEU { phoneNumber["nonFTEU"] = nonFTEU }
+        return phoneNumber
+    }
+
     func content(anisetteData: AnisetteData) -> GrandSlamRequestContent {
         .post([
             "securityCode": ["code": verificationCode],
-            "phoneNumber": ["id": phoneNumberID],
+            "phoneNumber": phoneNumber,
             "mode": "sms",
         ], type: .json)
     }

Testing

With this patch, the SMS arrived and login completed on the affected account.

Not tested:

  • Whether nonFTEU is actually required
  • Accounts with more than one trusted number (the patch always picks the first)

The diagnosis and patch were written with Claude Code. I tested the login on my account.

Lingua principale
Swift
Stelle
5.5k
Fork
163
Merge medio
1g 12h
PR unite (30g)
32

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di xtool-org/xtool

Tutte le issue di xtool-org/xtool

Issue simili

Altre issue su Swift

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.