From 780f7cfb9b0dab1189fb8fcdaaa31f005cfab597 Mon Sep 17 00:00:00 2001 From: whydr4 <42878446+whydr4@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:16:21 +0200 Subject: [PATCH 1/4] Handle 2FA codes that Apple sends on its own Since October 13 Apple often pushes the verification code to trusted devices right after the password step. In that case the complete response has hsc 409 and no "au" value, so we used to skip 2FA, ask for app tokens right away and fail with -22411 (This action cannot be completed at this time). Now hsc 409 is treated like trustedDeviceSecondaryAuth, except that we don't request a new code because that would invalidate the one already sent. This also adds an optional anisetteDataRefreshHandler so that the code check, the second sign-in and the app token request can each use a fresh one-time password instead of reusing the one from the first request. --- AltSign/Apple API/ALTAppleAPI.h | 4 + .../Sources/ALTAppleAPI+Authentication.swift | 234 +++++++++++------- 2 files changed, 148 insertions(+), 90 deletions(-) diff --git a/AltSign/Apple API/ALTAppleAPI.h b/AltSign/Apple API/ALTAppleAPI.h index 5ad1e178..3afce0a8 100644 --- a/AltSign/Apple API/ALTAppleAPI.h +++ b/AltSign/Apple API/ALTAppleAPI.h @@ -28,6 +28,10 @@ NS_ASSUME_NONNULL_BEGIN @property (class, nonatomic, readonly) ALTAppleAPI *sharedAPI; +// Optional. Apple rejects one-time passwords that were already used, so when this is set +// authentication asks for fresh anisette data before each step that follows the first sign-in. +@property (nonatomic, copy, nullable) void (^anisetteDataRefreshHandler)(void (^completionHandler)(ALTAnisetteData *_Nullable anisetteData, NSError *_Nullable error)); + /* Teams */ - (void)fetchTeamsForAccount:(ALTAccount *)account session:(ALTAppleAPISession *)session completionHandler:(void (^)(NSArray *_Nullable teams, NSError *_Nullable error))completionHandler; diff --git a/AltSign/Sources/ALTAppleAPI+Authentication.swift b/AltSign/Sources/ALTAppleAPI+Authentication.swift index 6cdc7c75..0eeed94e 100644 --- a/AltSign/Sources/ALTAppleAPI+Authentication.swift +++ b/AltSign/Sources/ALTAppleAPI+Authentication.swift @@ -53,23 +53,7 @@ public extension ALTAppleAPI do { - let clientDictionary = [ - "bootstrap": true, - "icscrec": true, - "pbe": false, - "prkgen": true, - "svct": "iCloud", - "loc": anisetteData.locale.identifier, - "X-Apple-Locale": anisetteData.locale.identifier, - "X-Apple-I-MD": anisetteData.oneTimePassword, - "X-Apple-I-MD-M": anisetteData.machineID, - "X-Mme-Device-Id": anisetteData.deviceUniqueIdentifier, - "X-Apple-I-MD-LU": anisetteData.localUserID, - "X-Apple-I-MD-RINFO": anisetteData.routingInfo, - "X-Apple-I-SRL-NO": anisetteData.deviceSerialNumber, - "X-Apple-I-Client-Time": self.dateFormatter.string(from: anisetteData.date), - "X-Apple-I-TimeZone": TimeZone.current.abbreviation() ?? "PST", - ] as [String: Any] + let clientDictionary = self.makeClientDictionary(anisetteData: anisetteData) let context = GSAContext(username: sanitizedAppleID, password: password) guard let publicKey = context.start() else { throw ALTAppleAPIError(.authenticationHandshakeFailed) } @@ -131,18 +115,33 @@ public extension ALTAppleAPI context.dsid = dsid + func signInAgain(verificationHandler: @escaping (@escaping (String?) -> Void) -> Void) + { + self.refreshAnisetteData(anisetteData) { (result) in + switch result + { + case .failure(let error): completionHandler(nil, nil, error) + case .success(let anisetteData): + self.authenticate(appleID: unsanitizedAppleID, password: password, anisetteData: anisetteData, verificationHandler: verificationHandler, completionHandler: completionHandler) + } + } + } + + // Since October 2026 Apple often sends the code to trusted devices on its own. + // It then answers with hsc 409 and no "au" value instead of "trustedDeviceSecondaryAuth". let authType = statusDictionary["au"] as? String - switch authType + let isCodeAlreadySent = (authType ?? "").isEmpty && statusDictionary["hsc"] as? Int == 409 + + switch isCodeAlreadySent ? "trustedDeviceSecondaryAuth" : authType { case "trustedDeviceSecondaryAuth": guard let verificationHandler = verificationHandler else { throw ALTAppleAPIError(.requiresTwoFactorAuthentication) } - self.requestTrustedDeviceTwoFactorCode(dsid: dsid, idmsToken: idmsToken, anisetteData: anisetteData, verificationHandler: verificationHandler) { (result) in + self.requestTrustedDeviceTwoFactorCode(dsid: dsid, idmsToken: idmsToken, anisetteData: anisetteData, isCodeAlreadySent: isCodeAlreadySent, verificationHandler: verificationHandler) { (result) in switch result { case .failure(let error): completionHandler(nil, nil, error) - case .success: - self.authenticate(appleID: unsanitizedAppleID, password: password, anisetteData: anisetteData, verificationHandler: verificationHandler, completionHandler: completionHandler) + case .success: signInAgain(verificationHandler: verificationHandler) } } @@ -153,8 +152,7 @@ public extension ALTAppleAPI switch result { case .failure(let error): completionHandler(nil, nil, error) - case .success: - self.authenticate(appleID: unsanitizedAppleID, password: password, anisetteData: anisetteData, verificationHandler: verificationHandler, completionHandler: completionHandler) + case .success: signInAgain(verificationHandler: verificationHandler) } } @@ -168,28 +166,34 @@ public extension ALTAppleAPI let app = "com.apple.gs.xcode.auth" guard let checksum = context.makeChecksum(appName: app) else { throw ALTAppleAPIError(.authenticationHandshakeFailed) } - let parameters = [ - "app": [app], - "c": c, - "checksum": checksum, - "cpd": clientDictionary, - "o": "apptokens", - "t": idmsToken, - "u": dsid - ] as [String: Any] - - self.fetchAuthToken(app: app, parameters: parameters, context: context, anisetteData: anisetteData) { (result) in - switch result - { - case .failure(let error): completionHandler(nil, nil, error) - case .success(let token): - - let session = ALTAppleAPISession(dsid: dsid, authToken: token, anisetteData: anisetteData) - self.fetchAccount(session: session) { (result) in - switch result - { - case .failure(let error): completionHandler(nil, nil, error) - case .success(let account): completionHandler(account, session, nil) + self.refreshAnisetteData(anisetteData) { (result) in + let anisetteData: ALTAnisetteData + do { anisetteData = try result.get() } + catch { return completionHandler(nil, nil, error) } + + let parameters = [ + "app": [app], + "c": c, + "checksum": checksum, + "cpd": self.makeClientDictionary(anisetteData: anisetteData), + "o": "apptokens", + "t": idmsToken, + "u": dsid + ] as [String: Any] + + self.fetchAuthToken(app: app, parameters: parameters, context: context, anisetteData: anisetteData) { (result) in + switch result + { + case .failure(let error): completionHandler(nil, nil, error) + case .success(let token): + + let session = ALTAppleAPISession(dsid: dsid, authToken: token, anisetteData: anisetteData) + self.fetchAccount(session: session) { (result) in + switch result + { + case .failure(let error): completionHandler(nil, nil, error) + case .success(let account): completionHandler(account, session, nil) + } } } } @@ -217,6 +221,45 @@ public extension ALTAppleAPI private extension ALTAppleAPI { + func makeClientDictionary(anisetteData: ALTAnisetteData) -> [String: Any] + { + let clientDictionary = [ + "bootstrap": true, + "icscrec": true, + "pbe": false, + "prkgen": true, + "svct": "iCloud", + "loc": anisetteData.locale.identifier, + "X-Apple-Locale": anisetteData.locale.identifier, + "X-Apple-I-MD": anisetteData.oneTimePassword, + "X-Apple-I-MD-M": anisetteData.machineID, + "X-Mme-Device-Id": anisetteData.deviceUniqueIdentifier, + "X-Apple-I-MD-LU": anisetteData.localUserID, + "X-Apple-I-MD-RINFO": anisetteData.routingInfo, + "X-Apple-I-SRL-NO": anisetteData.deviceSerialNumber, + "X-Apple-I-Client-Time": self.dateFormatter.string(from: anisetteData.date), + "X-Apple-I-TimeZone": TimeZone.current.abbreviation() ?? "PST", + ] as [String: Any] + + return clientDictionary + } + + func refreshAnisetteData(_ anisetteData: ALTAnisetteData, completionHandler: @escaping (Result) -> Void) + { + guard let anisetteDataRefreshHandler = self.anisetteDataRefreshHandler else { return completionHandler(.success(anisetteData)) } + + anisetteDataRefreshHandler { (anisetteData, error) in + if let anisetteData = anisetteData + { + completionHandler(.success(anisetteData)) + } + else + { + completionHandler(.failure(error ?? ALTAppleAPIError.unknown())) + } + } + } + func fetchAuthToken(app: String, parameters: [String: Any], context: GSAContext, anisetteData: ALTAnisetteData, completionHandler: @escaping (Result) -> Void) { self.sendAuthenticationRequest(parameters: parameters, anisetteData: anisetteData) { (result) in @@ -248,12 +291,68 @@ private extension ALTAppleAPI func requestTrustedDeviceTwoFactorCode(dsid: String, idmsToken: String, anisetteData: ALTAnisetteData, + isCodeAlreadySent: Bool, verificationHandler: @escaping (@escaping (String?) -> Void) -> Void, completionHandler: @escaping (Result) -> Void) { let requestURL = URL(string: "https://gsa.apple.com/auth/verify/trusteddevice")! let verifyURL = URL(string: "https://gsa.apple.com/grandslam/GsService2/validate")! + func responseHandler(verificationCode: String?) + { + guard let verificationCode = verificationCode else { return completionHandler(.failure(ALTAppleAPIError(.requiresTwoFactorAuthentication))) } + + // Typing the code can take longer than the one-time password stays valid, so fetch fresh anisette data first. + self.refreshAnisetteData(anisetteData) { (result) in + do + { + let anisetteData = try result.get() + + var request = self.makeTwoFactorCodeRequest(url: verifyURL, dsid: dsid, idmsToken: idmsToken, anisetteData: anisetteData) + request.allHTTPHeaderFields?["security-code"] = verificationCode + + self.sendGSARequest(request) { (result) in + do + { + let (data, httpResponse) = try result.get() + + guard (200 ..< 300).contains(httpResponse.statusCode) else { throw ALTAppleAPIError.unknown(statusCode: httpResponse.statusCode) } + + guard let responseDictionary = try PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any] else { + throw URLError(.badServerResponse) + } + + let errorCode = responseDictionary["ec"] as? Int ?? 0 + guard errorCode != 0 else { return completionHandler(.success(())) } + + switch errorCode + { + case -21669: throw ALTAppleAPIError(.incorrectVerificationCode) + default: + guard let errorDescription = responseDictionary["em"] as? String else { throw ALTAppleAPIError.unknown() } + + let localizedDescription = errorDescription + " (\(errorCode))" + throw NSError(domain: ALTUnderlyingAppleAPIErrorDomain, code: errorCode, userInfo: [NSLocalizedDescriptionKey: localizedDescription]) + } + } + catch + { + completionHandler(.failure(error)) + } + } + } + catch + { + completionHandler(.failure(error)) + } + } + } + + guard !isCodeAlreadySent else { + // Asking for another code here would make the one Apple already sent stop working. + return verificationHandler(responseHandler) + } + let request = self.makeTwoFactorCodeRequest(url: requestURL, dsid: dsid, idmsToken: idmsToken, anisetteData: anisetteData) self.sendGSARequest(request) { (result) in @@ -264,51 +363,6 @@ private extension ALTAppleAPI throw ALTAppleAPIError.unknown(statusCode: response.statusCode, failure: NSLocalizedString("Could not request a verification code from Apple.", comment: "")) } - func responseHandler(verificationCode: String?) - { - do - { - guard let verificationCode = verificationCode else { throw ALTAppleAPIError(.requiresTwoFactorAuthentication) } - - var request = self.makeTwoFactorCodeRequest(url: verifyURL, dsid: dsid, idmsToken: idmsToken, anisetteData: anisetteData) - request.allHTTPHeaderFields?["security-code"] = verificationCode - - self.sendGSARequest(request) { (result) in - do - { - let (data, httpResponse) = try result.get() - - guard (200 ..< 300).contains(httpResponse.statusCode) else { throw ALTAppleAPIError.unknown(statusCode: httpResponse.statusCode) } - - guard let responseDictionary = try PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any] else { - throw URLError(.badServerResponse) - } - - let errorCode = responseDictionary["ec"] as? Int ?? 0 - guard errorCode != 0 else { return completionHandler(.success(())) } - - switch errorCode - { - case -21669: throw ALTAppleAPIError(.incorrectVerificationCode) - default: - guard let errorDescription = responseDictionary["em"] as? String else { throw ALTAppleAPIError.unknown() } - - let localizedDescription = errorDescription + " (\(errorCode))" - throw NSError(domain: ALTUnderlyingAppleAPIErrorDomain, code: errorCode, userInfo: [NSLocalizedDescriptionKey: localizedDescription]) - } - } - catch - { - completionHandler(.failure(error)) - } - } - } - catch - { - completionHandler(.failure(error)) - } - } - verificationHandler(responseHandler) } catch From 6b12fdc053d202bb8dfdf3342e54444dd15aea45 Mon Sep 17 00:00:00 2001 From: kisnner <238374008+kisnner26@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:53:34 -0600 Subject: [PATCH 2/4] fix: send GrandSlam requests with the anisette provider's client identity Since October 14 2026, gsa.apple.com answers 503 with an HTML page to sign-in requests that declare an Android client, and rejects mismatched Mac identities with -80045. Requests are only accepted when the User-Agent and X-MMe-Client-Info match the client that generated the anisette data (e.g. the Windows iTunes identity served by anisette v3 servers). - Add an optional authenticationUserAgent so callers can pass the user_agent reported by their anisette provider. The default is akd's user agent instead of the Xcode one, which Apple blocks. - Send the same headers as clients that currently sign in successfully: Accept text/x-xml-plist, X-Xcode-Version, X-Apple-App-Info and Connection: close, for both GsService2 and 2FA requests. - Reduce cpd to the fields those clients send, with string values. --- AltSign/Apple API/ALTAppleAPI.h | 4 +++ .../Sources/ALTAppleAPI+Authentication.swift | 35 +++++++++---------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/AltSign/Apple API/ALTAppleAPI.h b/AltSign/Apple API/ALTAppleAPI.h index 3afce0a8..a8732081 100644 --- a/AltSign/Apple API/ALTAppleAPI.h +++ b/AltSign/Apple API/ALTAppleAPI.h @@ -32,6 +32,10 @@ NS_ASSUME_NONNULL_BEGIN // authentication asks for fresh anisette data before each step that follows the first sign-in. @property (nonatomic, copy, nullable) void (^anisetteDataRefreshHandler)(void (^completionHandler)(ALTAnisetteData *_Nullable anisetteData, NSError *_Nullable error)); +// Optional. The User-Agent sent with GrandSlam requests. It must match the client that generated the +// anisette data (anisette v3 servers report it in /v3/client_info). Defaults to akd's user agent. +@property (nonatomic, copy, nullable) NSString *authenticationUserAgent; + /* Teams */ - (void)fetchTeamsForAccount:(ALTAccount *)account session:(ALTAppleAPISession *)session completionHandler:(void (^)(NSArray *_Nullable teams, NSError *_Nullable error))completionHandler; diff --git a/AltSign/Sources/ALTAppleAPI+Authentication.swift b/AltSign/Sources/ALTAppleAPI+Authentication.swift index 0eeed94e..eedbf1f5 100644 --- a/AltSign/Sources/ALTAppleAPI+Authentication.swift +++ b/AltSign/Sources/ALTAppleAPI+Authentication.swift @@ -224,21 +224,15 @@ private extension ALTAppleAPI func makeClientDictionary(anisetteData: ALTAnisetteData) -> [String: Any] { let clientDictionary = [ - "bootstrap": true, - "icscrec": true, - "pbe": false, - "prkgen": true, + "bootstrap": "true", + "icscrec": "true", + "pbe": "false", + "prkgen": "true", "svct": "iCloud", - "loc": anisetteData.locale.identifier, - "X-Apple-Locale": anisetteData.locale.identifier, + "loc": "en_US", "X-Apple-I-MD": anisetteData.oneTimePassword, "X-Apple-I-MD-M": anisetteData.machineID, "X-Mme-Device-Id": anisetteData.deviceUniqueIdentifier, - "X-Apple-I-MD-LU": anisetteData.localUserID, - "X-Apple-I-MD-RINFO": anisetteData.routingInfo, - "X-Apple-I-SRL-NO": anisetteData.deviceSerialNumber, - "X-Apple-I-Client-Time": self.dateFormatter.string(from: anisetteData.date), - "X-Apple-I-TimeZone": TimeZone.current.abbreviation() ?? "PST", ] as [String: Any] return clientDictionary @@ -490,8 +484,8 @@ private extension ALTAppleAPI private extension ALTAppleAPI { - // Apple's servers reject outdated client identities, so use the same modern AuthKit identity for every request. - static let userAgent = "AuthKit/1 (Macintosh; OS X 26.5.2) (com.apple.dt.Xcode/26.0)" + // GrandSlam rejects requests whose User-Agent and X-MMe-Client-Info don't match the client that generated the anisette data. + var userAgent: String { self.authenticationUserAgent ?? "akd/1.0 CFNetwork/808.1.4" } func sendGSARequest(_ request: URLRequest, completionHandler: @escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) { @@ -539,8 +533,11 @@ private extension ALTAppleAPI let httpHeaders = [ "Content-Type": "text/x-xml-plist", "X-MMe-Client-Info": anisetteData.deviceDescription, - "Accept": "*/*", - "User-Agent": ALTAppleAPI.userAgent + "Accept": "text/x-xml-plist", + "X-Xcode-Version": "27.0 (27A5218g)", + "X-Apple-App-Info": "com.apple.gs.xcode.auth", + "Connection": "close", + "User-Agent": self.userAgent ] let bodyData = try PropertyListSerialization.data(fromPropertyList: parameters, format: .xml, options: 0) @@ -597,10 +594,12 @@ private extension ALTAppleAPI let encodedIdentityToken = identityTokenData.base64EncodedString() let httpHeaders = [ - "Accept": "application/x-buddyml", + "Accept": "text/x-xml-plist", + "X-Xcode-Version": "27.0 (27A5218g)", + "Connection": "close", "Accept-Language": "en-us", - "Content-Type": "application/x-plist", - "User-Agent": ALTAppleAPI.userAgent, + "Content-Type": "text/x-xml-plist", + "User-Agent": self.userAgent, "X-Apple-App-Info": "com.apple.gs.xcode.auth", "X-Apple-Identity-Token": encodedIdentityToken, "X-Apple-I-MD-M": anisetteData.machineID, From e7c95b48540325c44f1dba36c9d7cd49cad62763 Mon Sep 17 00:00:00 2001 From: kisnner <238374008+kisnner26@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:53:34 -0600 Subject: [PATCH 3/4] fix: report HTTP errors from GrandSlam instead of plist parsing errors When Apple rejects a client, GsService2 returns an HTML error page (e.g. "503 Service Temporarily Unavailable"). AltSign tried to parse it as a property list and surfaced NSCocoaErrorDomain 3840 ("unknown tag html"), which hid the real cause. Check the HTTP status of GsService2 responses and throw an error with the status code, and don't follow redirects on GrandSlam requests. --- .../Sources/ALTAppleAPI+Authentication.swift | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/AltSign/Sources/ALTAppleAPI+Authentication.swift b/AltSign/Sources/ALTAppleAPI+Authentication.swift index eedbf1f5..e65822a7 100644 --- a/AltSign/Sources/ALTAppleAPI+Authentication.swift +++ b/AltSign/Sources/ALTAppleAPI+Authentication.swift @@ -11,6 +11,19 @@ import Foundation @_exported import CAltSign import CAltSign.Private +// Don't follow redirects for GrandSlam requests so an unexpected redirect surfaces as an HTTP error. +private final class AuthenticationSessionDelegate: NSObject, URLSessionTaskDelegate +{ + static let shared = AuthenticationSessionDelegate() + func urlSession(_ session: URLSession, task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest, + completionHandler: @escaping (URLRequest?) -> Void) + { + completionHandler(nil) + } +} + public extension ALTAppleAPIError { static func unknown(statusCode: Int? = nil, failure: String? = nil, userInfo: [String: Any] = [:], sourceFile: String = #fileID, sourceLine: UInt = #line) -> ALTAppleAPIError @@ -494,7 +507,7 @@ private extension ALTAppleAPI let configuration = URLSessionConfiguration.ephemeral configuration.httpMaximumConnectionsPerHost = 1 - let session = URLSession(configuration: configuration) + let session = URLSession(configuration: configuration, delegate: AuthenticationSessionDelegate.shared, delegateQueue: nil) defer { session.finishTasksAndInvalidate() } let dataTask = session.dataTask(with: request) { (data, response, error) in @@ -550,7 +563,10 @@ private extension ALTAppleAPI self.sendGSARequest(request) { (result) in do { - let (data, _) = try result.get() + let (data, httpResponse) = try result.get() + + // GSA answers rejected clients with an HTML error page (e.g. 503), which would otherwise surface as a plist parsing error. + guard (200 ..< 300).contains(httpResponse.statusCode) else { throw ALTAppleAPIError.unknown(statusCode: httpResponse.statusCode) } guard let responseDictionary = try PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any], let dictionary = responseDictionary["Response"] as? [String: Any], From 832d9cec339ea6ef6891011833dbb7227b028a29 Mon Sep 17 00:00:00 2001 From: kisnner <238374008+kisnner26@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:53:34 -0600 Subject: [PATCH 4/4] feat: resynchronize anisette data once on hsc 434 GrandSlam answers hsc 434 with X-Apple-I-MD-DATA when the anisette data is out of sync with Apple. Add an optional anisetteDataResyncHandler that receives that value, and retry the init step once with the resynchronized anisette data, reusing the same SRP context. Without a handler, or on a second 434, fail with invalidAnisetteData as before. This mirrors the resync flow used by other anisette v3 clients. --- AltSign/Apple API/ALTAppleAPI.h | 4 + .../Sources/ALTAppleAPI+Authentication.swift | 253 ++++++++++-------- 2 files changed, 140 insertions(+), 117 deletions(-) diff --git a/AltSign/Apple API/ALTAppleAPI.h b/AltSign/Apple API/ALTAppleAPI.h index a8732081..8413918d 100644 --- a/AltSign/Apple API/ALTAppleAPI.h +++ b/AltSign/Apple API/ALTAppleAPI.h @@ -36,6 +36,10 @@ NS_ASSUME_NONNULL_BEGIN // anisette data (anisette v3 servers report it in /v3/client_info). Defaults to akd's user agent. @property (nonatomic, copy, nullable) NSString *authenticationUserAgent; +// Optional. Called once when GrandSlam answers hsc 434 (anisette data out of sync) with the X-Apple-I-MD-DATA +// value it returned. The handler should resynchronize the anisette provider and return fresh anisette data. +@property (nonatomic, copy, nullable) void (^anisetteDataResyncHandler)(NSString *sim, void (^completionHandler)(ALTAnisetteData *_Nullable anisetteData, NSError *_Nullable error)); + /* Teams */ - (void)fetchTeamsForAccount:(ALTAccount *)account session:(ALTAppleAPISession *)session completionHandler:(void (^)(NSArray *_Nullable teams, NSError *_Nullable error))completionHandler; diff --git a/AltSign/Sources/ALTAppleAPI+Authentication.swift b/AltSign/Sources/ALTAppleAPI+Authentication.swift index e65822a7..20085826 100644 --- a/AltSign/Sources/ALTAppleAPI+Authentication.swift +++ b/AltSign/Sources/ALTAppleAPI+Authentication.swift @@ -66,164 +66,181 @@ public extension ALTAppleAPI do { - let clientDictionary = self.makeClientDictionary(anisetteData: anisetteData) - let context = GSAContext(username: sanitizedAppleID, password: password) guard let publicKey = context.start() else { throw ALTAppleAPIError(.authenticationHandshakeFailed) } - let parameters = [ - "A2k": publicKey, - "cpd": clientDictionary, - "ps": ["s2k", "s2k_fo"], - "o": "init", - "u": sanitizedAppleID - ] as [String: Any] + func beginAuthentication(using anisetteData: ALTAnisetteData, hasResynchronized: Bool = false) + { + let clientDictionary = self.makeClientDictionary(anisetteData: anisetteData) + let parameters = [ + "A2k": publicKey, + "cpd": clientDictionary, + "ps": ["s2k", "s2k_fo"], + "o": "init", + "u": sanitizedAppleID + ] as [String: Any] - self.sendAuthenticationRequest(parameters: parameters, anisetteData: anisetteData) { (result) in - do - { - let responseDictionary = try result.get() + self.sendAuthenticationRequest(parameters: parameters, anisetteData: anisetteData) { (result) in + do + { + let responseDictionary = try result.get() + if let status = responseDictionary["Status"] as? [String: Any], status["hsc"] as? Int == 434 + { + guard !hasResynchronized, let resync = self.anisetteDataResyncHandler else { throw ALTAppleAPIError(.invalidAnisetteData) } + let sim = (status["X-Apple-I-MD-DATA"] as? String) ?? + (status["X-Apple-I-MD-DATA"] as? Data)?.base64EncodedString() + guard let sim, !sim.isEmpty else { throw ALTAppleAPIError(.invalidAnisetteData) } + resync(sim) { data, error in + guard let data else { return completionHandler(nil, nil, error ?? ALTAppleAPIError(.invalidAnisetteData)) } + // Same SRP context and A2k; all later steps use the resynchronized cpd. + beginAuthentication(using: data, hasResynchronized: true) + } + return + } - guard let c = responseDictionary["c"] as? String, - let salt = responseDictionary["s"] as? Data, - let iterations = responseDictionary["i"] as? Int, - let serverPublicKey = responseDictionary["B"] as? Data - else { throw URLError(.badServerResponse) } + guard let c = responseDictionary["c"] as? String, + let salt = responseDictionary["s"] as? Data, + let iterations = responseDictionary["i"] as? Int, + let serverPublicKey = responseDictionary["B"] as? Data + else { throw URLError(.badServerResponse) } - context.salt = salt - context.serverPublicKey = serverPublicKey + context.salt = salt + context.serverPublicKey = serverPublicKey - let sp = responseDictionary["sp"] as? String - let isHexadecimal = (sp == "s2k_fo") + let sp = responseDictionary["sp"] as? String + let isHexadecimal = (sp == "s2k_fo") - guard let verificationMessage = context.makeVerificationMessage(iterations: iterations, isHexadecimal: isHexadecimal) else { - throw ALTAppleAPIError(.authenticationHandshakeFailed) - } + guard let verificationMessage = context.makeVerificationMessage(iterations: iterations, isHexadecimal: isHexadecimal) else { + throw ALTAppleAPIError(.authenticationHandshakeFailed) + } - let parameters = [ - "c": c, - "cpd": clientDictionary, - "M1": verificationMessage, - "o": "complete", - "u": sanitizedAppleID - ] as [String: Any] + let parameters = [ + "c": c, + "cpd": clientDictionary, + "M1": verificationMessage, + "o": "complete", + "u": sanitizedAppleID + ] as [String: Any] - self.sendAuthenticationRequest(parameters: parameters, anisetteData: anisetteData) { (result) in - do - { - let responseDictionary = try result.get() + self.sendAuthenticationRequest(parameters: parameters, anisetteData: anisetteData) { (result) in + do + { + let responseDictionary = try result.get() - guard let serverVerificationMessage = responseDictionary["M2"] as? Data, - let serverDictionary = responseDictionary["spd"] as? Data, - let statusDictionary = responseDictionary["Status"] as? [String: Any] - else { throw URLError(.badServerResponse) } + guard let serverVerificationMessage = responseDictionary["M2"] as? Data, + let serverDictionary = responseDictionary["spd"] as? Data, + let statusDictionary = responseDictionary["Status"] as? [String: Any] + else { throw URLError(.badServerResponse) } - guard context.verifyServerVerificationMessage(serverVerificationMessage) else { throw ALTAppleAPIError(.authenticationHandshakeFailed) } - guard let decryptedData = serverDictionary.decryptedCBC(context: context) else { throw ALTAppleAPIError(.authenticationHandshakeFailed) } + guard context.verifyServerVerificationMessage(serverVerificationMessage) else { throw ALTAppleAPIError(.authenticationHandshakeFailed) } + guard let decryptedData = serverDictionary.decryptedCBC(context: context) else { throw ALTAppleAPIError(.authenticationHandshakeFailed) } - guard let decryptedDictionary = try PropertyListSerialization.propertyList(from: decryptedData, format: nil) as? [String: Any], - let dsid = decryptedDictionary["adsid"] as? String, - let idmsToken = decryptedDictionary["GsIdmsToken"] as? String - else { throw URLError(.badServerResponse) } + guard let decryptedDictionary = try PropertyListSerialization.propertyList(from: decryptedData, format: nil) as? [String: Any], + let dsid = decryptedDictionary["adsid"] as? String, + let idmsToken = decryptedDictionary["GsIdmsToken"] as? String + else { throw URLError(.badServerResponse) } - context.dsid = dsid + context.dsid = dsid - func signInAgain(verificationHandler: @escaping (@escaping (String?) -> Void) -> Void) - { - self.refreshAnisetteData(anisetteData) { (result) in - switch result - { - case .failure(let error): completionHandler(nil, nil, error) - case .success(let anisetteData): - self.authenticate(appleID: unsanitizedAppleID, password: password, anisetteData: anisetteData, verificationHandler: verificationHandler, completionHandler: completionHandler) + func signInAgain(verificationHandler: @escaping (@escaping (String?) -> Void) -> Void) + { + self.refreshAnisetteData(anisetteData) { (result) in + switch result + { + case .failure(let error): completionHandler(nil, nil, error) + case .success(let anisetteData): + self.authenticate(appleID: unsanitizedAppleID, password: password, anisetteData: anisetteData, verificationHandler: verificationHandler, completionHandler: completionHandler) + } } } - } - // Since October 2026 Apple often sends the code to trusted devices on its own. - // It then answers with hsc 409 and no "au" value instead of "trustedDeviceSecondaryAuth". - let authType = statusDictionary["au"] as? String - let isCodeAlreadySent = (authType ?? "").isEmpty && statusDictionary["hsc"] as? Int == 409 + // Since October 2026 Apple often sends the code to trusted devices on its own. + // It then answers with hsc 409 and no "au" value instead of "trustedDeviceSecondaryAuth". + let authType = statusDictionary["au"] as? String + let isCodeAlreadySent = (authType ?? "").isEmpty && statusDictionary["hsc"] as? Int == 409 - switch isCodeAlreadySent ? "trustedDeviceSecondaryAuth" : authType - { - case "trustedDeviceSecondaryAuth": - guard let verificationHandler = verificationHandler else { throw ALTAppleAPIError(.requiresTwoFactorAuthentication) } + switch isCodeAlreadySent ? "trustedDeviceSecondaryAuth" : authType + { + case "trustedDeviceSecondaryAuth": + guard let verificationHandler = verificationHandler else { throw ALTAppleAPIError(.requiresTwoFactorAuthentication) } - self.requestTrustedDeviceTwoFactorCode(dsid: dsid, idmsToken: idmsToken, anisetteData: anisetteData, isCodeAlreadySent: isCodeAlreadySent, verificationHandler: verificationHandler) { (result) in - switch result - { - case .failure(let error): completionHandler(nil, nil, error) - case .success: signInAgain(verificationHandler: verificationHandler) + self.requestTrustedDeviceTwoFactorCode(dsid: dsid, idmsToken: idmsToken, anisetteData: anisetteData, isCodeAlreadySent: isCodeAlreadySent, verificationHandler: verificationHandler) { (result) in + switch result + { + case .failure(let error): completionHandler(nil, nil, error) + case .success: signInAgain(verificationHandler: verificationHandler) + } } - } - case "secondaryAuth": - guard let verificationHandler = verificationHandler else { throw ALTAppleAPIError(.requiresTwoFactorAuthentication) } + case "secondaryAuth": + guard let verificationHandler = verificationHandler else { throw ALTAppleAPIError(.requiresTwoFactorAuthentication) } - self.requestSMSTwoFactorCode(dsid: dsid, idmsToken: idmsToken, anisetteData: anisetteData, verificationHandler: verificationHandler) { (result) in - switch result - { - case .failure(let error): completionHandler(nil, nil, error) - case .success: signInAgain(verificationHandler: verificationHandler) + self.requestSMSTwoFactorCode(dsid: dsid, idmsToken: idmsToken, anisetteData: anisetteData, verificationHandler: verificationHandler) { (result) in + switch result + { + case .failure(let error): completionHandler(nil, nil, error) + case .success: signInAgain(verificationHandler: verificationHandler) + } } - } - default: - guard let sessionKey = decryptedDictionary["sk"] as? Data, - let c = decryptedDictionary["c"] as? Data - else { throw URLError(.badServerResponse) } + default: + guard let sessionKey = decryptedDictionary["sk"] as? Data, + let c = decryptedDictionary["c"] as? Data + else { throw URLError(.badServerResponse) } - context.sessionKey = sessionKey + context.sessionKey = sessionKey - let app = "com.apple.gs.xcode.auth" - guard let checksum = context.makeChecksum(appName: app) else { throw ALTAppleAPIError(.authenticationHandshakeFailed) } + let app = "com.apple.gs.xcode.auth" + guard let checksum = context.makeChecksum(appName: app) else { throw ALTAppleAPIError(.authenticationHandshakeFailed) } - self.refreshAnisetteData(anisetteData) { (result) in - let anisetteData: ALTAnisetteData - do { anisetteData = try result.get() } - catch { return completionHandler(nil, nil, error) } + self.refreshAnisetteData(anisetteData) { (result) in + let anisetteData: ALTAnisetteData + do { anisetteData = try result.get() } + catch { return completionHandler(nil, nil, error) } - let parameters = [ - "app": [app], - "c": c, - "checksum": checksum, - "cpd": self.makeClientDictionary(anisetteData: anisetteData), - "o": "apptokens", - "t": idmsToken, - "u": dsid - ] as [String: Any] + let parameters = [ + "app": [app], + "c": c, + "checksum": checksum, + "cpd": self.makeClientDictionary(anisetteData: anisetteData), + "o": "apptokens", + "t": idmsToken, + "u": dsid + ] as [String: Any] - self.fetchAuthToken(app: app, parameters: parameters, context: context, anisetteData: anisetteData) { (result) in - switch result - { - case .failure(let error): completionHandler(nil, nil, error) - case .success(let token): + self.fetchAuthToken(app: app, parameters: parameters, context: context, anisetteData: anisetteData) { (result) in + switch result + { + case .failure(let error): completionHandler(nil, nil, error) + case .success(let token): - let session = ALTAppleAPISession(dsid: dsid, authToken: token, anisetteData: anisetteData) - self.fetchAccount(session: session) { (result) in - switch result - { - case .failure(let error): completionHandler(nil, nil, error) - case .success(let account): completionHandler(account, session, nil) + let session = ALTAppleAPISession(dsid: dsid, authToken: token, anisetteData: anisetteData) + self.fetchAccount(session: session) { (result) in + switch result + { + case .failure(let error): completionHandler(nil, nil, error) + case .success(let account): completionHandler(account, session, nil) + } } } } } } } - } - catch - { - completionHandler(nil, nil, error) + catch + { + completionHandler(nil, nil, error) + } } } - } - catch - { - completionHandler(nil, nil, error) + catch + { + completionHandler(nil, nil, error) + } } } + + beginAuthentication(using: anisetteData) } catch { @@ -573,6 +590,8 @@ private extension ALTAppleAPI let status = dictionary["Status"] as? [String: Any] else { throw URLError(.badServerResponse) } + // hsc 434 is a protocol resync, handled once by the original init. + if status["hsc"] as? Int == 434 { return completionHandler(.success(dictionary)) } let errorCode = status["ec"] as? Int ?? 0 guard errorCode != 0 else { return completionHandler(.success(dictionary)) }