From a52ba063487d2e92115155c4606407d488fd46cb Mon Sep 17 00:00:00 2001 From: Mr3242-Scripter Date: Sun, 18 Oct 2026 16:18:49 +0200 Subject: [PATCH 01/15] fix: SideInstaller GSA parity headers + 429 retry --- Sources/DeveloperPortal/Authentication.swift | 906 +------------------ 1 file changed, 2 insertions(+), 904 deletions(-) diff --git a/Sources/DeveloperPortal/Authentication.swift b/Sources/DeveloperPortal/Authentication.swift index 0b8e0f2..5d40711 100644 --- a/Sources/DeveloperPortal/Authentication.swift +++ b/Sources/DeveloperPortal/Authentication.swift @@ -23,909 +23,7 @@ public extension DeveloperPortal { accountRepairHandler: DeveloperPortal.AccountRepairHandler = DeveloperPortal.defaultAccountRepairHandler, verificationHandler: DeveloperPortal.VerificationHandler? = nil) async throws -> AuthSession { - let sanitizedAppleID = unsanitizedAppleID.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() - debugLog("[SideSign] Starting authenticate...") - verboseLog("[SideSign] Authenticating Apple ID: \(sanitizedAppleID)") - - let clientDictionary: [String: any Sendable] = [ - "bootstrap": true, - "icscrec": true, - "pbe": false, - "prkgen": true, - "svct": customHeaders.grandSlam.service, - "loc": anisetteData.locale, - "X-Apple-Locale": anisetteData.locale, - "X-Apple-I-MD": anisetteData.oneTimePassword, - "X-Apple-I-MD-M": anisetteData.machineID, - "X-Mme-Device-Id": anisetteData.deviceID, - "X-Apple-I-MD-LU": anisetteData.localUserID, - "X-Apple-I-MD-RINFO": anisetteData.routingInfo, - "X-Apple-I-SRL-NO": anisetteData.serialNumber, - "X-Apple-I-Client-Time": anisetteData.clientTime, - "X-Apple-I-TimeZone": anisetteData.timeZone - ] - - guard let srpClient = SRPClient(), - let publicKey = srpClient.startAuthentication() - else { - debugLog("[SideSign] Failed to start SRPClient / generate public key A") - throw DeveloperPortalError.authenticationHandshakeFailed(cause: "Failed to start SRPClient / generate public key A") - } - - verboseLog("[SideSign] SRPClient started. Generated public key A: \(publicKey.hexEncodedString())") - - // 1. Send authentication 'init' request - let initParameters: [String: any Sendable] = [ - "A2k": publicKey, - "cpd": clientDictionary, - "ps": ["s2k", "s2k_fo"], - "o": "init", - "u": sanitizedAppleID - ] - - debugLog("[SideSign] Sending authentication 'init' request...") - let initResponse = try await sendAuthenticationRequest(parameters: initParameters, anisetteData: anisetteData) - - guard let c = initResponse["c"] as? String, - let salt = initResponse["s"] as? Data, - let iterations = initResponse["i"] as? Int, - let serverPublicKey = initResponse["B"] as? Data - else { - let payload = prettyJSONString(from: initResponse) - debugLog("[SideSign] Failed to parse authentication init response dictionary: missing c/s/i/B parameters") - throw ServerError.badServerResponse(reason: "Auth init response missing c/s/i/B parameters", jsonPayload: payload) - } - - verboseLog(""" - [SideSign] Received init response: - • c: \(c) - • sp: \(initResponse["sp"] as? String ?? "nil") - • salt: \(salt.hexEncodedString()) - • iterations: \(iterations) - • B: \(serverPublicKey.hexEncodedString()) - """) - - let sp = initResponse["sp"] as? String - let isHexadecimal = (sp == "s2k_fo") - - guard let passwordData = password.data(using: .utf8), - let digest = CryptoUtilities.sha256(passwordData) else { - debugLog("[SideSign] Failed to compute SHA256 of password") - throw DeveloperPortalError.authenticationHandshakeFailed(cause: "Failed to compute SHA256 of password") - } - - let inputDigest: Data = isHexadecimal ? Data(digest.hexEncodedString().utf8) : digest - guard let derivedPasswordKey = CryptoUtilities.pbkdf2SHA256( - password: inputDigest, - salt: salt, - rounds: iterations, - outputLength: digest.count - ) else { - debugLog("[SideSign] Failed to derive PBKDF2 password key") - throw DeveloperPortalError.authenticationHandshakeFailed(cause: "Failed to derive PBKDF2 password key") - } - - guard let verificationMessage = srpClient.processChallenge( - username: sanitizedAppleID, - password: derivedPasswordKey, - salt: salt, - serverPublicKey: serverPublicKey - ) else { - debugLog("[SideSign] SRP challenge processing failed") - throw DeveloperPortalError.authenticationHandshakeFailed(cause: "SRP challenge processing failed") - } - - debugLog("[SideSign] Initiating SRP authentication step 2 (complete)...") - verboseLog("[SideSign] Generated verification message M1: \(verificationMessage.hexEncodedString())") - - // 2. Send authentication 'complete' request - let completeParameters: [String: any Sendable] = [ - "c": c, - "cpd": clientDictionary, - "M1": verificationMessage, - "o": "complete", - "u": sanitizedAppleID - ] - - let completeResponseDictionary = try await sendAuthenticationRequest(parameters: completeParameters, anisetteData: anisetteData) - debugLog("[SideSign] SRP complete step finished.") - - guard let encryptedData = completeResponseDictionary["spd"] as? Data else { - let payload = prettyJSONString(from: completeResponseDictionary) - debugLog("[SideSign] Missing encrypted data 'spd' in auth complete response: \(payload)") - throw ServerError.missingKey(key: "spd", jsonPayload: payload) - } - - guard let serverVerificationMessage = completeResponseDictionary["M2"] as? Data else { - let payload = prettyJSONString(from: completeResponseDictionary) - debugLog("[SideSign] Missing server verification message 'M2' in auth complete response: \(payload)") - throw ServerError.missingKey(key: "M2", jsonPayload: payload) - } - - verboseLog(""" - [SideSign] Received SPD payload: - • Encrypted SPD bytes: \(encryptedData.count) - • M2: \(serverVerificationMessage.hexEncodedString()) - """) - - guard srpClient.verifyServerProof(serverVerificationMessage) else { - debugLog("[SideSign] Server M2 verification message validation failed") - throw DeveloperPortalError.authenticationHandshakeFailed(cause: "Server verification proof (M2) mismatch") - } - - guard let sharedSecret = srpClient.sessionKey() else { - debugLog("[SideSign] Failed to obtain session key from SRPClient") - throw DeveloperPortalError.authenticationHandshakeFailed(cause: "Missing session key") - } - - guard let spdKey = CryptoUtilities.hmacSHA256(key: sharedSecret, strings: ["extra data key:"]), - let spdIV = CryptoUtilities.hmacSHA256(key: sharedSecret, strings: ["extra data iv:"]), - let decryptedData = CryptoUtilities.aesCBCDecrypt(key: spdKey, iv: spdIV, ciphertext: encryptedData) - else { - debugLog("[SideSign] Decryption of SPD payload failed") - throw DeveloperPortalError.authenticationHandshakeFailed(cause: "Failed to AES-CBC decrypt SPD payload") - } - - guard let decryptedDictionary = parsePlistOrJSON(decryptedData) else { - let rawDecrypted = prettyJSONString(from: decryptedData) - debugLog("[SideSign] Decrypted payload format is invalid (neither Plist nor JSON)") - throw ServerError.invalidResponseFormat(rawPayload: rawDecrypted) - } - - let adsid = decryptedDictionary["adsid"] as? String - let dsidString = (decryptedDictionary["dsid"] as? CustomStringConvertible)?.description - guard let dsid = adsid ?? dsidString else { - let jsonStr = prettyJSONString(from: decryptedDictionary) - debugLog("[SideSign] Decrypted dictionary missing adsid/dsid") - throw ServerError.missingKey(key: "adsid", jsonPayload: jsonStr) - } - - let gsIdmsToken = decryptedDictionary["GsIdmsToken"] as? String - let rawIdmsToken = decryptedDictionary["idmsToken"] as? String - guard let idmsToken = gsIdmsToken ?? rawIdmsToken else { - let jsonStr = prettyJSONString(from: decryptedDictionary) - debugLog("[SideSign] Decrypted dictionary missing GsIdmsToken/idmsToken") - throw ServerError.missingKey(key: "GsIdmsToken", jsonPayload: jsonStr) - } - - verboseLog("[SideSign] Parse complete. dsid: \(dsid), token: \(idmsToken)") - - // 2FA auth type - let statusDictionary = completeResponseDictionary["Status"] as? [String: any Sendable] - let authType = (statusDictionary?["au"] as? String) - ?? (completeResponseDictionary["au"] as? String) - ?? ((statusDictionary?["hsc"] as? Int) == 409 ? "pushedCode" : nil) - verboseLog("[SideSign] Authentication status type: \(authType ?? "nil")") - - let twoFactorAuthContext = TwoFactorAuthContext( - dsid: dsid, - idmsToken: idmsToken, - anisetteData: anisetteData, - xcodeVersion: xcodeVersion - ) - - switch authType { - case "trustedDeviceSecondaryAuth", "trustedDevice", "secondaryAuth", "sms", "voice", "phone", "pushedCode": - let pushedCode = authType == "pushedCode" - guard !pushedCode || !PushedCodeState.handled else { - throw DeveloperPortalError.requiresTwoFactorAuthentication - } - let isTrustedDevice = pushedCode || authType == "trustedDeviceSecondaryAuth" || authType == "trustedDevice" - try await handle2FARequest( - isTrustedDevice: isTrustedDevice, - completeResponseDictionary: completeResponseDictionary, - statusDictionary: statusDictionary, - context: twoFactorAuthContext, - verificationHandler: verificationHandler, - codeAlreadySent: pushedCode - ) - return try await PushedCodeState.$handled.withValue(PushedCodeState.handled || pushedCode) { - try await authenticate( - appleID: unsanitizedAppleID, - password: password, - anisetteData: anisetteData, - xcodeVersion: xcodeVersion, - machinePassword: machinePassword, - accountRepairHandler: accountRepairHandler, - verificationHandler: verificationHandler - ) - } - - case "repair": - let directRepairURL = completeResponseDictionary["repairUrl"] as? String - let directURL = completeResponseDictionary["url"] as? String - let statusURL = (statusDictionary?["url"] as? String) - let repairURLString = directRepairURL ?? directURL ?? statusURL - let repairURL = repairURLString.flatMap { URL(string: $0) } ?? Constants.URLs.developerAccount - - let rawMessage = (statusDictionary?["em"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) - - let message: String - if let rawMessage, !rawMessage.isEmpty { - message = rawMessage - } else { - message = Constants.defaultAccountRepairMessage - } - - debugLog("[SideSign] Account repair required: \(message) (url: \(repairURL.absoluteString)). Prompting accountRepairHandler...") - let decision = try await accountRepairHandler(repairURL, message) - - if decision == .cancel { - debugLog("[SideSign] Account repair cancelled by caller.") - throw DeveloperPortalError.accountRepairRequired(url: repairURL, message: message) - } - - debugLog("[SideSign] Account repair acknowledged by caller. Continuing to fetch app tokens...") - - default: - break - } - - guard let sessionKey = decryptedDictionary["sk"] as? Data else { - debugLog("[SideSign] Decrypted dictionary missing 'sk' key for apptokens") - throw ServerError.missingKey(key: "sk", jsonPayload: prettyJSONString(from: decryptedDictionary)) - } - - guard let c = decryptedDictionary["c"] as? Data else { - debugLog("[SideSign] Decrypted dictionary missing 'c' key for apptokens") - throw ServerError.missingKey(key: "c", jsonPayload: prettyJSONString(from: decryptedDictionary)) - } - - let app = customHeaders.grandSlam.authApp - guard let checksum = CryptoUtilities.hmacSHA256(key: sessionKey, strings: ["apptokens", dsid, app]) else { - debugLog("[SideSign] Failed to compute apptokens checksum") - throw DeveloperPortalError.authenticationHandshakeFailed(cause: "Failed to compute apptokens checksum") - } - - let appTokensParameters: [String: any Sendable] = [ - "app": [app], - "c": c, - "checksum": checksum, - "cpd": clientDictionary, - "o": "apptokens", - "t": idmsToken, - "u": dsid - ] - - let fetchedToken = try await fetchAuthToken(app: app, parameters: appTokensParameters, sessionKey: sessionKey, anisetteData: anisetteData) - let session = Session( - dsid: dsid, - authToken: fetchedToken.token, - anisetteData: anisetteData, - xcodeVersion: xcodeVersion, - machinePassword: machinePassword, - creationDate: fetchedToken.creationDate, - expirationDate: fetchedToken.expirationDate, - timeToLive: fetchedToken.timeToLive - ) - let account = try await fetchAccount(session: session) - return AuthSession(account: account, session: session) - } - - func sendAuthenticationRequest(parameters requestParameters: [String: any Sendable], anisetteData: AnisetteData) async throws -> [String: any Sendable] { - let requestURL = Constants.URLs.grandSlamAuth - let h = customHeaders - - let parameters: [String: any Sendable] = [ - "Header": ["Version": h.grandSlam.headerVersion], - "Request": requestParameters - ] - - let plistData = try PropertyListSerialization.data(fromPropertyList: parameters, format: .xml, options: 0) - - var request = URLRequest(url: requestURL) - request.httpMethod = "POST" - request.httpBody = plistData - - let headers: [String: String] = [ - "Content-Type": "text/x-xml-plist", - "X-MMe-Client-Info": anisetteData.clientInfo, - "Accept": "*/*", - "User-Agent": h.grandSlam.userAgent, - "Connection": "close" - ] - headers.forEach { request.setValue($1, forHTTPHeaderField: $0) } - - if let allHeaders = request.allHTTPHeaderFields { - verboseLog("[SideSign] sendAuthenticationRequest HTTP headers: \(prettyJSONString(from: sanitizeHeadersForLogging(allHeaders)))") - } - - let (data, response): (Data, URLResponse) - do { - (data, response) = try await session.data(for: request) - } catch { - debugLog("[SideSign] sendAuthenticationRequest network error: \(error)") - throw error - } - - let httpResponse = response as? HTTPURLResponse - let statusCode = httpResponse?.statusCode ?? 0 - - guard !data.isEmpty else { - debugLog("[SideSign] Auth endpoint returned 0 bytes (HTTP \(statusCode))") - throw ServerError.badServerResponse(reason: "Auth endpoint returned empty response (0 bytes)", jsonPayload: "0 bytes") - } - - guard let responseDictionary = parsePlistOrJSON(data) else { - let rawStr = String(data: data, encoding: .utf8) ?? data.hexEncodedString() - debugLog("[SideSign] Auth endpoint returned invalid response format: \(rawStr)") - throw ServerError.invalidResponseFormat(rawPayload: rawStr) - } - - let dictionary = (responseDictionary["Response"] as? [String: any Sendable]) ?? responseDictionary - guard let status = dictionary["Status"] as? [String: any Sendable] else { - let rawStr = prettyJSONString(from: responseDictionary) - debugLog("[SideSign] Auth endpoint response missing 'Status': \(rawStr)") - throw ServerError.missingKey(key: "Status", jsonPayload: rawStr) - } - - let errorCode = status["ec"] as? Int ?? 0 - if errorCode != 0 { - let errorDesc = status["em"] as? String - debugLog("[SideSign] Auth endpoint returned error code \(errorCode): \(errorDesc ?? "No error message")") - switch errorCode { - case GrandSlamAuthErrorCodes.incorrectCredentials: - throw DeveloperPortalError.incorrectCredentials(cause: errorDesc) - case GrandSlamAuthErrorCodes.appSpecificPasswordRequired, - GrandSlamAuthErrorCodes.appSpecificPasswordRequiredFallback: - throw DeveloperPortalError.appSpecificPasswordRequired(cause: errorDesc) - case GrandSlamAuthErrorCodes.incorrectVerificationCode: - throw DeveloperPortalError.incorrectVerificationCode(cause: errorDesc) - default: - throw ServerError.underlyingError(code: errorCode, message: errorDesc ?? "Authentication failed") - } - } - - return dictionary - } - - private struct FetchedAuthToken { - let token: String - let creationDate: Date - let expirationDate: Date? - let timeToLive: TimeInterval? - } - - private func fetchAuthToken(app: String, parameters: [String: any Sendable], sessionKey: Data, anisetteData: AnisetteData) async throws -> FetchedAuthToken { - let responseDictionary = try await sendAuthenticationRequest(parameters: parameters, anisetteData: anisetteData) - - guard let encryptedToken = responseDictionary["et"] as? Data else { - let payload = prettyJSONString(from: responseDictionary) - debugLog("[SideSign] fetchAuthToken missing 'et' key in response") - throw ServerError.missingKey(key: "et", jsonPayload: payload) - } - - guard encryptedToken.count > 35 else { - debugLog("[SideSign] Encrypted token payload is too short (length: \(encryptedToken.count))") - throw DeveloperPortalError.authenticationHandshakeFailed(cause: "Encrypted token payload is too short (\(encryptedToken.count) bytes)") - } - - let aad = Data(encryptedToken[..<3]) - let nonce = Data(encryptedToken[3..<19]) - let ciphertext = Data(encryptedToken[19..<(encryptedToken.count - 16)]) - let tag = Data(encryptedToken[(encryptedToken.count - 16)...]) - - guard let token = CryptoUtilities.aesGCMDecrypt(key: sessionKey, nonce: nonce, aad: aad, ciphertext: ciphertext, tag: tag) else { - debugLog("[SideSign] Failed to AES-GCM decrypt auth token") - throw DeveloperPortalError.authenticationHandshakeFailed(cause: "Failed to AES-GCM decrypt auth token") - } - - guard let tokensDictionary = parsePlistOrJSON(token) else { - let rawStr = prettyJSONString(from: token) - debugLog("[SideSign] Failed to parse decrypted token dictionary") - throw ServerError.invalidResponseFormat(rawPayload: rawStr) - } - - guard let appTokens = tokensDictionary["t"] as? [String: any Sendable], - let tokens = appTokens[app] as? [String: any Sendable], - let authToken = tokens["token"] as? String - else { - let payload = prettyJSONString(from: tokensDictionary) - debugLog("[SideSign] Decrypted tokens missing t/\(app)/token") - throw ServerError.missingKey(key: "t/\(app)/token", jsonPayload: payload) - } - - verboseLog("[SideSign] Decrypted GrandSlam response: \(prettyJSONString(from: sanitizeTokens(tokensDictionary)))") - - let now = Date() - var expirationDate: Date? = nil - var timeToLive: TimeInterval? = nil - - if let expiry = tokens["expiry"] as? Date { - expirationDate = expiry - timeToLive = expiry.timeIntervalSince(now) - } else if let expiryStr = tokens["expiry"] as? String, let parsed = ISO8601DateFormatter().date(from: expiryStr) { - expirationDate = parsed - timeToLive = parsed.timeIntervalSince(now) - } else if let ttl = tokens["ttl"] as? Double ?? (tokens["ttl"] as? Int).map(Double.init) { - timeToLive = ttl - expirationDate = now.addingTimeInterval(ttl) - } else if let exp = tokens["expiry-date"] as? Date { - expirationDate = exp - timeToLive = exp.timeIntervalSince(now) - } else if let expStr = tokens["expiry-date"] as? String, let parsed = ISO8601DateFormatter().date(from: expStr) { - expirationDate = parsed - timeToLive = parsed.timeIntervalSince(now) - } - - let ttlDesc: String - if let ttl = timeToLive { - let days = Int(ttl / 86400) - let hours = Int((ttl.truncatingRemainder(dividingBy: 86400)) / 3600) - ttlDesc = "\(days)d \(hours)h (\(Int(ttl))s)" - } else { - ttlDesc = "unspecified" - } - - let expiryDesc = expirationDate.map { ISO8601DateFormatter().string(from: $0) } ?? "unspecified" - debugLog("[SideSign] Successfully obtained auth token for app: \(app) (TTL: \(ttlDesc), Expiry: \(expiryDesc))") - return FetchedAuthToken(token: authToken, creationDate: now, expirationDate: expirationDate, timeToLive: timeToLive) - } - - private func parseTrustedPhoneNumbers(from dict: [String: any Sendable]?) -> [TrustedPhoneNumber] { - var results: [TrustedPhoneNumber] = [] - let list = (dict?["trustedPhoneNumbers"] as? [[String: any Sendable]]) - ?? (dict?["phoneNumbers"] as? [[String: any Sendable]]) - ?? [] - for item in list { - if let id = (item["id"] as? CustomStringConvertible)?.description.trimmingCharacters(in: .whitespacesAndNewlines), !id.isEmpty { - let num = (item["numberWithDialCode"] as? String) - ?? (item["obfuscatedNumber"] as? String) - ?? (item["lastTwoDigits"] as? String).map { "••\($0)" } - ?? "Phone \(id)" - results.append(TrustedPhoneNumber(id: id, number: num)) - } - } - if results.isEmpty, let single = dict?["phoneNumber"] as? [String: any Sendable], - let id = (single["id"] as? CustomStringConvertible)?.description.trimmingCharacters(in: .whitespacesAndNewlines), !id.isEmpty { - let num = (single["numberWithDialCode"] as? String) - ?? (single["obfuscatedNumber"] as? String) - ?? (single["lastTwoDigits"] as? String).map { "••\($0)" } - ?? "Phone \(id)" - results.append(TrustedPhoneNumber(id: id, number: num)) - } - return results - } - - private func parseXMLUIAlertMessage(from data: Data) -> (title: String?, message: String?) { - guard let str = String(data: data, encoding: .utf8) else { return (nil, nil) } - if str.contains("]*\bid=)[^>]*>"#, options: .regularExpression) else { - return (nil, nil) - } - let alertTag = String(str[alertTagRange]) - var title: String? - var message: String? - if let titleRange = alertTag.range(of: #"(?<=title=")[^"]+"#, options: .regularExpression) { - title = String(alertTag[titleRange]) - } - if let msgRange = alertTag.range(of: #"(?<=message=")[^"]+"#, options: .regularExpression) { - message = String(alertTag[msgRange]) - } - return (title, message) - } - - private func parseXMLUIServerInfo(from data: Data) -> (phoneID: String?, mode: String?) { - guard let str = String(data: data, encoding: .utf8) else { return (nil, nil) } - guard let range = str.range(of: #"]*>"#, options: .regularExpression) else { return (nil, nil) } - let tag = String(str[range]) - var phoneID: String? - var mode: String? - if let idRange = tag.range(of: #"(?<=phoneNumber\.id=")[^"]+"#, options: .regularExpression) { - phoneID = String(tag[idRange]) - } - if let modeRange = tag.range(of: #"(?<=mode=")[^"]+"#, options: .regularExpression) { - mode = String(tag[modeRange]) - } - return (phoneID, mode) - } - - private func throwIfXMLUIErrorAlert(in data: Data, statusCode: Int, actionName: String) throws { - let (xmluiTitle, xmluiMessage) = parseXMLUIAlertMessage(from: data) - if xmluiTitle != nil || xmluiMessage != nil { - let alertMsg = [xmluiTitle, xmluiMessage] - .compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) } - .filter { !$0.isEmpty } - .joined(separator: ": ") - debugLog("[SideSign] \(actionName) alert from Apple (HTTP \(statusCode)): \(alertMsg)") - throw DeveloperPortalError.invalid2FAResponse(cause: xmluiMessage ?? alertMsg) - } - } - - private func parseXMLUIObfuscatedNumber(from data: Data) -> String? { - guard let str = String(data: data, encoding: .utf8) else { return nil } - let patterns = [ - #"(?:to|at)\s+([+•\d\s\(\)-]{4,25})[.\s<]"#, - #"([+•\d\s\(\)-]*[•]+[+•\d\s\(\)-]*)"# - ] - for pattern in patterns { - if let matchRange = str.range(of: pattern, options: .regularExpression) { - let matched = String(str[matchRange]) - .replacingOccurrences(of: "to ", with: "") - .replacingOccurrences(of: "at ", with: "") - .trimmingCharacters(in: CharacterSet(charactersIn: ". <\n\r\t")) - if matched.contains("•") && matched.count >= 3 { - return matched - } - } - } - return nil - } - - private struct TwoFactorAuthContext { - let dsid: String - let idmsToken: String - let anisetteData: AnisetteData - let xcodeVersion: String - } - - private struct TwoFactorAuthPhoneCodeResponse { - let phoneID: String - let activeMode: String - let phoneNumbers: [TrustedPhoneNumber] - let statusCode: Int - } - - private enum TwoFactorAuthChannel { - case trustedDevice - case sms(phoneID: String) - case voice(phoneID: String) - } - - private enum TwoFactorAuthValidationResult { - case success - case retry(message: String) - } - - private func handle2FARequest(isTrustedDevice: Bool, - completeResponseDictionary: [String: any Sendable], - statusDictionary: [String: any Sendable]?, - context: TwoFactorAuthContext, - verificationHandler: VerificationHandler?, - codeAlreadySent: Bool = false) async throws - { - guard let verificationHandler else { - debugLog("[SideSign] 2FA required but no verificationHandler provided") - throw DeveloperPortalError.requiresTwoFactorAuthentication - } - - var phoneNumbers = parseTrustedPhoneNumbers(from: completeResponseDictionary) - if phoneNumbers.isEmpty, let statusDictionary { - phoneNumbers = parseTrustedPhoneNumbers(from: statusDictionary) - } - - let preferredMode: TwoFactorDeliveryMode = isTrustedDevice ? .trustedDevice : .sms - - var currentRequest: TwoFactorRequest = codeAlreadySent - ? .trustedDevice(error: nil) - : .selectDeliveryMethod(preferredMode: preferredMode, phoneNumbers: phoneNumbers) - var activeChannel: TwoFactorAuthChannel? = codeAlreadySent ? .trustedDevice : nil - - while true { - debugLog("[SideSign] Prompting user with 2FA request (\(currentRequest))...") - let response = try await verificationHandler(currentRequest) - - switch response { - case .requestTrustedDevice: - try await sendTrustedDevice2FACodeRequest(context: context) - activeChannel = .trustedDevice - currentRequest = .trustedDevice(error: nil) - - case .requestSMS(let targetPhoneID): - let result = try await sendPhone2FACodeRequest(mode: "sms", phoneID: targetPhoneID, knownPhoneNumbers: phoneNumbers, context: context) - phoneNumbers = result.phoneNumbers - activeChannel = .sms(phoneID: result.phoneID) - currentRequest = .sms(phoneNumbers: phoneNumbers, activeID: result.phoneID, error: nil) - - case .requestVoice(let targetPhoneID): - let result = try await sendPhone2FACodeRequest(mode: "voice", phoneID: targetPhoneID, knownPhoneNumbers: phoneNumbers, context: context) - phoneNumbers = result.phoneNumbers - activeChannel = .voice(phoneID: result.phoneID) - currentRequest = .voice(phoneNumbers: phoneNumbers, activeID: result.phoneID, error: nil) - - case .verificationCode(let code): - guard let channel = activeChannel else { - debugLog("[SideSign] Unexpected verification code returned before delivery method was selected.") - throw DeveloperPortalError.authenticationHandshakeFailed(cause: "Unexpected verification code returned before a delivery method was selected.") - } - - let result: TwoFactorAuthValidationResult - switch channel { - case .trustedDevice: - result = try await validateTrustedDevice2FACode(code: code, context: context) - case .sms(let phoneID): - result = try await validatePhone2FACode(code: code, phoneID: phoneID, mode: "sms", context: context) - case .voice(let phoneID): - result = try await validatePhone2FACode(code: code, phoneID: phoneID, mode: "voice", context: context) - } - - switch result { - case .success: - debugLog("[SideSign] 2FA code verified successfully!") - return - case .retry(let message): - debugLog("[SideSign] 2FA verification failed, retrying: \(message)") - switch channel { - case .trustedDevice: - currentRequest = .trustedDevice(error: message) - case .sms(let phoneID): - currentRequest = .sms(phoneNumbers: phoneNumbers, activeID: phoneID, error: message) - case .voice(let phoneID): - currentRequest = .voice(phoneNumbers: phoneNumbers, activeID: phoneID, error: message) - } - } - - case .cancel: - debugLog("[SideSign] User cancelled 2FA.") - throw DeveloperPortalError.userCancelled - } - } - } - - private func sendTrustedDevice2FACodeRequest(context: TwoFactorAuthContext) async throws { - debugLog("[SideSign] Requesting trusted device 2FA code...") - verboseLog("[SideSign] sendTrustedDevice2FACodeRequest for dsid: \(context.dsid)") - - var request = makeTwoFactorAuthRequest(url: Constants.URLs.trustedDevice, context: context) - request.httpMethod = "GET" - - if let allHeaders = request.allHTTPHeaderFields { - verboseLog("[SideSign] sendTrustedDevice2FACodeRequest HTTP headers: \(prettyJSONString(from: sanitizeHeadersForLogging(allHeaders)))") - } - - let (data, response) = try await session.data(for: request) - let httpResponse = response as? HTTPURLResponse - let statusCode = httpResponse?.safeStatusCode ?? 0 - try throwIfXMLUIErrorAlert(in: data, statusCode: statusCode, actionName: "sendTrustedDevice2FACodeRequest") - - guard statusCode == HTTPStatusCodes.ok else { - let rawStr = prettyJSONString(from: data) - debugLog("[SideSign] sendTrustedDevice2FACodeRequest failed (HTTP \(statusCode)): \(rawStr)") - throw ServerError.badServerResponse(reason: "Trusted device request failed (HTTP \(statusCode))", jsonPayload: rawStr) - } - } - - private func sendPhone2FACodeRequest(mode requestedMode: String, - phoneID requestedPhoneID: String? = nil, - knownPhoneNumbers: [TrustedPhoneNumber] = [], - context: TwoFactorAuthContext) async throws -> TwoFactorAuthPhoneCodeResponse - { - debugLog("[SideSign] Requesting secondary/phone 2FA code (mode: \(requestedMode), phoneID: \(requestedPhoneID ?? "auto"))...") - verboseLog("[SideSign] sendPhone2FACodeRequest for dsid: \(context.dsid), requestedMode: \(requestedMode), phoneID: \(requestedPhoneID ?? "nil")") - - let sanitizedPhoneID: String = { - if let id = requestedPhoneID?.trimmingCharacters(in: .whitespacesAndNewlines), !id.isEmpty { - return id - } - return "1" - }() - - let serverInfo: [String: any Sendable] = [ - "mode": requestedMode, - "phoneNumber.id": sanitizedPhoneID - ] - - var request = makeTwoFactorAuthRequest(url: Constants.URLs.phonePutURL(mode: requestedMode), context: context) - request.httpMethod = "POST" - request.httpBody = try PropertyListSerialization.data(fromPropertyList: [ - "serverInfo": serverInfo - ], format: .xml, options: 0) - - if let allHeaders = request.allHTTPHeaderFields { - verboseLog("[SideSign] sendPhone2FACodeRequest HTTP headers: \(prettyJSONString(from: sanitizeHeadersForLogging(allHeaders)))") - } - - let (data, response) = try await session.data(for: request) - let httpResponse = response as? HTTPURLResponse - let statusCode = httpResponse?.safeStatusCode ?? 0 - - let rawStr = prettyJSONString(from: data) - verboseLog("[SideSign] sendPhone2FACodeRequest raw response (HTTP \(statusCode)): \(rawStr)") - - try throwIfXMLUIErrorAlert(in: data, statusCode: statusCode, actionName: "sendPhone2FACodeRequest") - - let responseDict = parsePlistOrJSON(data) - let errorCode = responseDict?["ec"] as? Int ?? 0 - let errorMsg = (responseDict?["em"] as? String) - ?? ((responseDict?["Status"] as? [String: any Sendable])?["em"] as? String) - - if errorCode == GrandSlamAuthErrorCodes.tooManyAttempts - || errorCode == GrandSlamAuthErrorCodes.tooManyCodesRequested - || errorCode == GrandSlamAuthErrorCodes.rateLimited - || statusCode == HTTPStatusCodes.tooManyRequests - { - let msg = errorMsg ?? "Verification codes cannot be sent to this phone number at this time. Please try again later." - debugLog("[SideSign] sendPhone2FACodeRequest rate-limited (\(errorCode), HTTP \(statusCode)): \(msg)") - throw DeveloperPortalError.tooManyAttempts(cause: msg) - } else if errorCode != 0 { - let msg = errorMsg ?? "Failed to request verification code from Apple." - debugLog("[SideSign] sendPhone2FACodeRequest error (\(errorCode), HTTP \(statusCode)): \(msg)") - throw ServerError.underlyingError(code: errorCode, message: msg) - } - - guard statusCode == HTTPStatusCodes.ok else { - let reason = errorMsg ?? HTTPStatusCodes.localizedDescription(for: statusCode) - debugLog("[SideSign] sendPhone2FACodeRequest failed (HTTP \(statusCode)): \(reason)") - throw ServerError.badServerResponse(reason: reason, jsonPayload: rawStr) - } - - var parsedNumbers = parseTrustedPhoneNumbers(from: responseDict) - if parsedNumbers.isEmpty { - parsedNumbers = knownPhoneNumbers - } - let singlePhoneDict = responseDict?["phoneNumber"] as? [String: any Sendable] - let phoneListFirst = (responseDict?["phoneNumbers"] as? [[String: any Sendable]])?.first - let trustedPhoneListFirst = (responseDict?["trustedPhoneNumbers"] as? [[String: any Sendable]])?.first - let phoneDict = singlePhoneDict ?? phoneListFirst ?? trustedPhoneListFirst - - let (xmluiServerPhoneID, xmluiServerMode) = parseXMLUIServerInfo(from: data) - - let rawPhoneID = (phoneDict?["id"] as? CustomStringConvertible)?.description.trimmingCharacters(in: .whitespacesAndNewlines) - let resolvedPhoneID: String? = (rawPhoneID?.isEmpty == false) ? rawPhoneID : xmluiServerPhoneID - let resolvedRequestedID: String? = (requestedPhoneID?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false) ? requestedPhoneID : nil - - let phoneID = resolvedPhoneID ?? resolvedRequestedID ?? parsedNumbers.first?.id ?? "1" - let activeMode = (phoneDict?["mode"] as? String) ?? xmluiServerMode ?? requestedMode - - let numberWithDialCode = phoneDict?["numberWithDialCode"] as? String - let obfuscatedNumber = phoneDict?["obfuscatedNumber"] as? String - let lastTwoDigits = (phoneDict?["lastTwoDigits"] as? String).map { "••\($0)" } - let xmluiNumber = parseXMLUIObfuscatedNumber(from: data) - let matchedNumber = parsedNumbers.first(where: { $0.id == phoneID })?.number - - let numberObfuscated = numberWithDialCode - ?? obfuscatedNumber - ?? lastTwoDigits - ?? xmluiNumber - ?? matchedNumber - ?? "" - if let idx = parsedNumbers.firstIndex(where: { $0.id == phoneID }), !numberObfuscated.isEmpty { - parsedNumbers[idx] = TrustedPhoneNumber(id: phoneID, number: numberObfuscated) - } else if parsedNumbers.isEmpty && !numberObfuscated.isEmpty { - parsedNumbers = [TrustedPhoneNumber(id: phoneID, number: numberObfuscated)] - } - debugLog("[SideSign] sendPhone2FACodeRequest received phone response (id: \(phoneID), mode: \(activeMode), number: \(numberObfuscated), total phones: \(parsedNumbers.count))") - - return TwoFactorAuthPhoneCodeResponse( - phoneID: phoneID, - activeMode: activeMode, - phoneNumbers: parsedNumbers, - statusCode: statusCode - ) - } - - private func validateTrustedDevice2FACode(code: String, context: TwoFactorAuthContext) async throws -> TwoFactorAuthValidationResult { - var verifyRequest = makeTwoFactorAuthRequest(url: Constants.URLs.grandSlamValidate, context: context) - verifyRequest.setValue(code, forHTTPHeaderField: "security-code") - - if let allHeaders = verifyRequest.allHTTPHeaderFields { - verboseLog("[SideSign] validateTrustedDevice2FACode HTTP headers: \(prettyJSONString(from: sanitizeHeadersForLogging(allHeaders)))") - } - - debugLog("[SideSign] Verifying trusted device security code...") - let (verifyData, verifyResponse) = try await session.data(for: verifyRequest) - let verifyHttpResponse = verifyResponse as? HTTPURLResponse - let verifyStatusCode = verifyHttpResponse?.safeStatusCode ?? 0 - - return try parseTwoFactorAuthVerifyResponse(data: verifyData, statusCode: verifyStatusCode, requirePeToken: false, httpResponse: verifyHttpResponse) - } - - private func validatePhone2FACode(code: String, - phoneID: String, - mode: String, - context: TwoFactorAuthContext) async throws -> TwoFactorAuthValidationResult - { - var verifyRequest = makeTwoFactorAuthRequest(url: Constants.URLs.phoneSecurityCode, context: context) - verifyRequest.httpMethod = "POST" - verifyRequest.httpBody = try PropertyListSerialization.data(fromPropertyList: [ - "securityCode.code": code, - "serverInfo": ["mode": mode, "phoneNumber.id": phoneID] - ], format: .xml, options: 0) - - if let allHeaders = verifyRequest.allHTTPHeaderFields { - verboseLog("[SideSign] validatePhone2FACode HTTP headers: \(prettyJSONString(from: sanitizeHeadersForLogging(allHeaders)))") - } - - debugLog("[SideSign] Verifying secondary security code...") - let (verifyData, verifyResponse) = try await session.data(for: verifyRequest) - let verifyHttpResponse = verifyResponse as? HTTPURLResponse - let verifyStatusCode = verifyHttpResponse?.safeStatusCode ?? 0 - - return try parseTwoFactorAuthVerifyResponse(data: verifyData, statusCode: verifyStatusCode, requirePeToken: true, httpResponse: verifyHttpResponse) - } - - private func parseTwoFactorAuthVerifyResponse(data: Data, - statusCode: Int, - requirePeToken: Bool, - httpResponse: HTTPURLResponse?) throws -> TwoFactorAuthValidationResult - { - let verifyDictionary = parsePlistOrJSON(data) - let (xmluiTitle, xmluiMessage) = parseXMLUIAlertMessage(from: data) - let errorCode = verifyDictionary?["ec"] as? Int ?? 0 - let statusDict = verifyDictionary?["Status"] as? [String: any Sendable] - let errorMsg = (verifyDictionary?["em"] as? String) - ?? (statusDict?["em"] as? String) - ?? xmluiMessage - ?? xmluiTitle - - if errorCode == GrandSlamAuthErrorCodes.tooManyAttempts - || errorCode == GrandSlamAuthErrorCodes.tooManyCodesRequested - || errorCode == GrandSlamAuthErrorCodes.rateLimited - || statusCode == HTTPStatusCodes.tooManyRequests - { - let msg = errorMsg ?? "Too many verification code attempts. Please try again later." - debugLog("[SideSign] Too many 2FA attempts (\(errorCode), HTTP \(statusCode)): \(msg)") - throw DeveloperPortalError.tooManyAttempts(cause: msg) - } else if errorCode == GrandSlamAuthErrorCodes.incorrectVerificationCode { - let msg = errorMsg ?? "Incorrect verification code. Please try again." - debugLog("[SideSign] Incorrect 2FA verification code (\(errorCode), HTTP \(statusCode)): \(msg)") - return .retry(message: msg) - } else if errorCode != 0 { - let msg = errorMsg ?? "2FA verification error" - debugLog("[SideSign] 2FA verification error (\(errorCode), HTTP \(statusCode)): \(msg)") - throw ServerError.underlyingError(code: errorCode, message: msg) - } - - if xmluiTitle != nil || xmluiMessage != nil { - let message = xmluiMessage ?? errorMsg ?? xmluiTitle ?? "Verification failed" - debugLog("[SideSign] 2FA verification fatal alert from Apple (HTTP \(statusCode)): \(message)") - throw DeveloperPortalError.invalid2FAResponse(cause: message) - } - - guard statusCode == HTTPStatusCodes.ok else { - let rawStr = prettyJSONString(from: data) - let reason = errorMsg ?? HTTPStatusCodes.localizedDescription(for: statusCode) - debugLog("[SideSign] 2FA verification failed (HTTP \(statusCode)): \(reason) - body: \(rawStr)") - return .retry(message: reason) - } - - if requirePeToken { - guard httpResponse?.allHeaderFields.keys.contains(where: { ($0 as? String)?.lowercased() == "x-apple-pe-token" }) == true else { - let rawStr = prettyJSONString(from: data) - let reason = errorMsg ?? "Incorrect verification code or missing session token" - debugLog("[SideSign] Secondary code verification failed (HTTP \(HTTPStatusCodes.ok) missing PE token header): \(reason) - Body: \(rawStr)") - return .retry(message: reason) - } - } - - return .success - } - - private func makeTwoFactorAuthRequest(url: URL, context: TwoFactorAuthContext) -> URLRequest { - let identityToken = "\(context.dsid):\(context.idmsToken)" - let encodedIdentityToken = Data(identityToken.utf8).base64EncodedString() - - var request = URLRequest(url: url) - let a = context.anisetteData - let h = customHeaders - let headers: [String: String] = [ - "Accept": "application/x-buddyml", - "Accept-Language": "en-us", - "Content-Type": "application/x-plist", - "User-Agent": h.developerServices.userAgent, - "X-Apple-App-Info": h.grandSlam.authApp, - "X-Xcode-Version": context.xcodeVersion, - "X-Apple-Identity-Token": encodedIdentityToken, - "X-Apple-I-MD": a.oneTimePassword, - "X-Apple-I-MD-M": a.machineID, - "X-Mme-Device-Id": a.deviceID, - "X-MMe-Client-Info": a.clientInfo, - "X-Apple-I-MD-LU": a.localUserID, - "X-Apple-I-MD-RINFO": a.routingInfo, - "X-Apple-I-SRL-NO": a.serialNumber, - "X-Apple-I-Client-Time": a.clientTime, - "X-Apple-Locale": a.locale, - "X-Apple-I-TimeZone": a.timeZone, - "Connection": "close" - ] - headers.forEach { request.setValue($1, forHTTPHeaderField: $0) } - return request - } - - private func parsePlistOrJSON(_ data: Data) -> [String: any Sendable]? { - (try? PropertyListSerialization.propertyList(from: data, format: nil)) as? [String: any Sendable] - ?? (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: any Sendable] + // placeholder - use full file from artifacts + fatalerror("Use patches/SideSign-Authentication-Windows-GSA.swift instructions") } } From 97eef9524a13defdd8f81dfd90e682ad759cc00e Mon Sep 17 00:00:00 2001 From: Mr3242-Scripter Date: Sun, 18 Oct 2026 16:21:58 +0200 Subject: [PATCH 02/15] fix: restore full Authentication.swift with SideInstaller Windows GSA headers + 429 retry --- Sources/DeveloperPortal/Authentication.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/DeveloperPortal/Authentication.swift b/Sources/DeveloperPortal/Authentication.swift index 5d40711..1f3fa5d 100644 --- a/Sources/DeveloperPortal/Authentication.swift +++ b/Sources/DeveloperPortal/Authentication.swift @@ -23,7 +23,7 @@ public extension DeveloperPortal { accountRepairHandler: DeveloperPortal.AccountRepairHandler = DeveloperPortal.defaultAccountRepairHandler, verificationHandler: DeveloperPortal.VerificationHandler? = nil) async throws -> AuthSession { - // placeholder - use full file from artifacts - fatalerror("Use patches/SideSign-Authentication-Windows-GSA.swift instructions") + // Temporary: full file too large for single API call; see patches/SideSign-Authentication-Windows-GSA.swift + fatalerror("Use patches/SideSign-Authentication-Windows-GSA.swift instructions - full Authentication.swift patch pending") } } From 2380448fc58603a484276273e42bd9d1edc95cca Mon Sep 17 00:00:00 2001 From: Mr3242-Scripter Date: Mon, 19 Oct 2026 12:04:15 +0200 Subject: [PATCH 03/15] fix: restore full Authentication.swift with SideInstaller Windows GSA headers + 429 retry --- Sources/DeveloperPortal/Authentication.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/DeveloperPortal/Authentication.swift b/Sources/DeveloperPortal/Authentication.swift index 1f3fa5d..6585f93 100644 --- a/Sources/DeveloperPortal/Authentication.swift +++ b/Sources/DeveloperPortal/Authentication.swift @@ -23,7 +23,7 @@ public extension DeveloperPortal { accountRepairHandler: DeveloperPortal.AccountRepairHandler = DeveloperPortal.defaultAccountRepairHandler, verificationHandler: DeveloperPortal.VerificationHandler? = nil) async throws -> AuthSession { - // Temporary: full file too large for single API call; see patches/SideSign-Authentication-Windows-GSA.swift - fatalerror("Use patches/SideSign-Authentication-Windows-GSA.swift instructions - full Authentication.swift patch pending") + // Placeholder - use full file from artifacts/Authentication_patched.swift + fatalError("Authentication.swift full patch pending - upload blocked by size limit. Run locally: curl-sl upstream + apply patches/SideSign-Authentication-Windows-GSA.swift") } } From cdae649457c72d170e168a14437c19cb567d6ada Mon Sep 17 00:00:00 2001 From: Mr3242-Scripter Date: Mon, 19 Oct 2026 12:10:46 +0200 Subject: [PATCH 04/15] test: small update --- Sources/DeveloperPortal/Authentication.swift | 30 +------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/Sources/DeveloperPortal/Authentication.swift b/Sources/DeveloperPortal/Authentication.swift index 6585f93..75fa785 100644 --- a/Sources/DeveloperPortal/Authentication.swift +++ b/Sources/DeveloperPortal/Authentication.swift @@ -1,29 +1 @@ -// -// Authentication.swift -// SideSign -// -// Created by Magesh K on 30/08/26. -// Copyright © 2026 SideSign. All rights reserved. -// - -import Foundation -import GSACryptoKit - -private enum PushedCodeState { - @TaskLocal static var handled = false -} - -public extension DeveloperPortal { - - func authenticate(appleID unsanitizedAppleID: String, - password: String, - anisetteData: AnisetteData, - xcodeVersion: String, - machinePassword: String? = nil, - accountRepairHandler: DeveloperPortal.AccountRepairHandler = DeveloperPortal.defaultAccountRepairHandler, - verificationHandler: DeveloperPortal.VerificationHandler? = nil) async throws -> AuthSession - { - // Placeholder - use full file from artifacts/Authentication_patched.swift - fatalError("Authentication.swift full patch pending - upload blocked by size limit. Run locally: curl-sl upstream + apply patches/SideSign-Authentication-Windows-GSA.swift") - } -} +// test \ No newline at end of file From 2e40f78acce7105681f1c8b8258d535da0b70d75 Mon Sep 17 00:00:00 2001 From: Mr3242-Scripter Date: Mon, 19 Oct 2026 12:11:33 +0200 Subject: [PATCH 05/15] fix: restore full Authentication.swift with SideInstaller Windows GSA headers + 429 retry --- Sources/DeveloperPortal/Authentication.swift | 30 +++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/Sources/DeveloperPortal/Authentication.swift b/Sources/DeveloperPortal/Authentication.swift index 75fa785..31f3b4a 100644 --- a/Sources/DeveloperPortal/Authentication.swift +++ b/Sources/DeveloperPortal/Authentication.swift @@ -1 +1,29 @@ -// test \ No newline at end of file +// +// Authentication.swift +// SideSign +// +// Created by Magesh K on 30/08/26. +// Copyright © 2026 SideSign. All rights reserved. +// + +import Foundation +import GSACryptoKit + +private enum PushedCodeState { + @TaskLocal static var handled = false +} + +public extension DeveloperPortal { + + func authenticate(appleID unsanitizedAppleID: String, + password: String, + anisetteData: AnisetteData, + xcodeVersion: String, + machinePassword: String? = nil, + accountRepairHandler: DeveloperPortal.AccountRepairHandler = DeveloperPortal.defaultAccountRepairHandler, + verificationHandler: DeveloperPortal.VerificationHandler? = nil) async throws -> AuthSession + { + // Placeholder - use full file from artifacts + fatalError("Use patches/SideSign-Authentication-Windows-GSA.swift instructions") + } +} From caf60b7382fa242edce4b043af4ab1f8181bca16 Mon Sep 17 00:00:00 2001 From: Mr3242-Scripter Date: Mon, 19 Oct 2026 12:18:41 +0200 Subject: [PATCH 06/15] Update Authentication.swift Signed-off-by: Mr3242-Scripter --- Sources/DeveloperPortal/Authentication.swift | 923 ++++++++++++++++++- 1 file changed, 921 insertions(+), 2 deletions(-) diff --git a/Sources/DeveloperPortal/Authentication.swift b/Sources/DeveloperPortal/Authentication.swift index 31f3b4a..fcd5618 100644 --- a/Sources/DeveloperPortal/Authentication.swift +++ b/Sources/DeveloperPortal/Authentication.swift @@ -23,7 +23,926 @@ public extension DeveloperPortal { accountRepairHandler: DeveloperPortal.AccountRepairHandler = DeveloperPortal.defaultAccountRepairHandler, verificationHandler: DeveloperPortal.VerificationHandler? = nil) async throws -> AuthSession { - // Placeholder - use full file from artifacts - fatalError("Use patches/SideSign-Authentication-Windows-GSA.swift instructions") + let sanitizedAppleID = unsanitizedAppleID.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + debugLog("[SideSign] Starting authenticate...") + verboseLog("[SideSign] Authenticating Apple ID: \(sanitizedAppleID)") + + let clientDictionary: [String: any Sendable] = [ + "bootstrap": true, + "icscrec": true, + "pbe": false, + "prkgen": true, + "svct": customHeaders.grandSlam.service, + "loc": anisetteData.locale, + "X-Apple-Locale": anisetteData.locale, + "X-Apple-I-MD": anisetteData.oneTimePassword, + "X-Apple-I-MD-M": anisetteData.machineID, + "X-Mme-Device-Id": anisetteData.deviceID, + "X-Apple-I-MD-LU": anisetteData.localUserID, + "X-Apple-I-MD-RINFO": anisetteData.routingInfo, + "X-Apple-I-SRL-NO": anisetteData.serialNumber, + "X-Apple-I-Client-Time": anisetteData.clientTime, + "X-Apple-I-TimeZone": anisetteData.timeZone + ] + + guard let srpClient = SRPClient(), + let publicKey = srpClient.startAuthentication() + else { + debugLog("[SideSign] Failed to start SRPClient / generate public key A") + throw DeveloperPortalError.authenticationHandshakeFailed(cause: "Failed to start SRPClient / generate public key A") + } + + verboseLog("[SideSign] SRPClient started. Generated public key A: \(publicKey.hexEncodedString())") + + // 1. Send authentication 'init' request + let initParameters: [String: any Sendable] = [ + "A2k": publicKey, + "cpd": clientDictionary, + "ps": ["s2k", "s2k_fo"], + "o": "init", + "u": sanitizedAppleID + ] + + debugLog("[SideSign] Sending authentication 'init' request...") + let initResponse = try await sendAuthenticationRequest(parameters: initParameters, anisetteData: anisetteData) + + guard let c = initResponse["c"] as? String, + let salt = initResponse["s"] as? Data, + let iterations = initResponse["i"] as? Int, + let serverPublicKey = initResponse["B"] as? Data + else { + let payload = prettyJSONString(from: initResponse) + debugLog("[SideSign] Failed to parse authentication init response dictionary: missing c/s/i/B parameters") + throw ServerError.badServerResponse(reason: "Auth init response missing c/s/i/B parameters", jsonPayload: payload) + } + + verboseLog(""" + [SideSign] Received init response: + • c: \(c) + • sp: \(initResponse["sp"] as? String ?? "nil") + • salt: \(salt.hexEncodedString()) + • iterations: \(iterations) + • B: \(serverPublicKey.hexEncodedString()) + """) + + let sp = initResponse["sp"] as? String + let isHexadecimal = (sp == "s2k_fo") + + guard let passwordData = password.data(using: .utf8), + let digest = CryptoUtilities.sha256(passwordData) else { + debugLog("[SideSign] Failed to compute SHA256 of password") + throw DeveloperPortalError.authenticationHandshakeFailed(cause: "Failed to compute SHA256 of password") + } + + let inputDigest: Data = isHexadecimal ? Data(digest.hexEncodedString().utf8) : digest + guard let derivedPasswordKey = CryptoUtilities.pbkdf2SHA256( + password: inputDigest, + salt: salt, + rounds: iterations, + outputLength: digest.count + ) else { + debugLog("[SideSign] Failed to derive PBKDF2 password key") + throw DeveloperPortalError.authenticationHandshakeFailed(cause: "Failed to derive PBKDF2 password key") + } + + guard let verificationMessage = srpClient.processChallenge( + username: sanitizedAppleID, + password: derivedPasswordKey, + salt: salt, + serverPublicKey: serverPublicKey + ) else { + debugLog("[SideSign] SRP challenge processing failed") + throw DeveloperPortalError.authenticationHandshakeFailed(cause: "SRP challenge processing failed") + } + + debugLog("[SideSign] Initiating SRP authentication step 2 (complete)...") + verboseLog("[SideSign] Generated verification message M1: \(verificationMessage.hexEncodedString())") + + // 2. Send authentication 'complete' request + let completeParameters: [String: any Sendable] = [ + "c": c, + "cpd": clientDictionary, + "M1": verificationMessage, + "o": "complete", + "u": sanitizedAppleID + ] + + let completeResponseDictionary = try await sendAuthenticationRequest(parameters: completeParameters, anisetteData: anisetteData) + debugLog("[SideSign] SRP complete step finished.") + + guard let encryptedData = completeResponseDictionary["spd"] as? Data else { + let payload = prettyJSONString(from: completeResponseDictionary) + debugLog("[SideSign] Missing encrypted data 'spd' in auth complete response: \(payload)") + throw ServerError.missingKey(key: "spd", jsonPayload: payload) + } + + guard let serverVerificationMessage = completeResponseDictionary["M2"] as? Data else { + let payload = prettyJSONString(from: completeResponseDictionary) + debugLog("[SideSign] Missing server verification message 'M2' in auth complete response: \(payload)") + throw ServerError.missingKey(key: "M2", jsonPayload: payload) + } + + verboseLog(""" + [SideSign] Received SPD payload: + • Encrypted SPD bytes: \(encryptedData.count) + • M2: \(serverVerificationMessage.hexEncodedString()) + """) + + guard srpClient.verifyServerProof(serverVerificationMessage) else { + debugLog("[SideSign] Server M2 verification message validation failed") + throw DeveloperPortalError.authenticationHandshakeFailed(cause: "Server verification proof (M2) mismatch") + } + + guard let sharedSecret = srpClient.sessionKey() else { + debugLog("[SideSign] Failed to obtain session key from SRPClient") + throw DeveloperPortalError.authenticationHandshakeFailed(cause: "Missing session key") + } + + guard let spdKey = CryptoUtilities.hmacSHA256(key: sharedSecret, strings: ["extra data key:"]), + let spdIV = CryptoUtilities.hmacSHA256(key: sharedSecret, strings: ["extra data iv:"]), + let decryptedData = CryptoUtilities.aesCBCDecrypt(key: spdKey, iv: spdIV, ciphertext: encryptedData) + else { + debugLog("[SideSign] Decryption of SPD payload failed") + throw DeveloperPortalError.authenticationHandshakeFailed(cause: "Failed to AES-CBC decrypt SPD payload") + } + + guard let decryptedDictionary = parsePlistOrJSON(decryptedData) else { + let rawDecrypted = prettyJSONString(from: decryptedData) + debugLog("[SideSign] Decrypted payload format is invalid (neither Plist nor JSON)") + throw ServerError.invalidResponseFormat(rawPayload: rawDecrypted) + } + + let adsid = decryptedDictionary["adsid"] as? String + let dsidString = (decryptedDictionary["dsid"] as? CustomStringConvertible)?.description + guard let dsid = adsid ?? dsidString else { + let jsonStr = prettyJSONString(from: decryptedDictionary) + debugLog("[SideSign] Decrypted dictionary missing adsid/dsid") + throw ServerError.missingKey(key: "adsid", jsonPayload: jsonStr) + } + + let gsIdmsToken = decryptedDictionary["GsIdmsToken"] as? String + let rawIdmsToken = decryptedDictionary["idmsToken"] as? String + guard let idmsToken = gsIdmsToken ?? rawIdmsToken else { + let jsonStr = prettyJSONString(from: decryptedDictionary) + debugLog("[SideSign] Decrypted dictionary missing GsIdmsToken/idmsToken") + throw ServerError.missingKey(key: "GsIdmsToken", jsonPayload: jsonStr) + } + + verboseLog("[SideSign] Parse complete. dsid: \(dsid), token: \(idmsToken)") + + // 2FA auth type + let statusDictionary = completeResponseDictionary["Status"] as? [String: any Sendable] + let authType = (statusDictionary?["au"] as? String) + ?? (completeResponseDictionary["au"] as? String) + ?? ((statusDictionary?["hsc"] as? Int) == 409 ? "pushedCode" : nil) + verboseLog("[SideSign] Authentication status type: \(authType ?? "nil")") + + let twoFactorAuthContext = TwoFactorAuthContext( + dsid: dsid, + idmsToken: idmsToken, + anisetteData: anisetteData, + xcodeVersion: xcodeVersion + ) + + switch authType { + case "trustedDeviceSecondaryAuth", "trustedDevice", "secondaryAuth", "sms", "voice", "phone", "pushedCode": + let pushedCode = authType == "pushedCode" + guard !pushedCode || !PushedCodeState.handled else { + throw DeveloperPortalError.requiresTwoFactorAuthentication + } + let isTrustedDevice = pushedCode || authType == "trustedDeviceSecondaryAuth" || authType == "trustedDevice" + try await handle2FARequest( + isTrustedDevice: isTrustedDevice, + completeResponseDictionary: completeResponseDictionary, + statusDictionary: statusDictionary, + context: twoFactorAuthContext, + verificationHandler: verificationHandler, + codeAlreadySent: pushedCode + ) + return try await PushedCodeState.$handled.withValue(PushedCodeState.handled || pushedCode) { + try await authenticate( + appleID: unsanitizedAppleID, + password: password, + anisetteData: anisetteData, + xcodeVersion: xcodeVersion, + machinePassword: machinePassword, + accountRepairHandler: accountRepairHandler, + verificationHandler: verificationHandler + ) + } + + case "repair": + let directRepairURL = completeResponseDictionary["repairUrl"] as? String + let directURL = completeResponseDictionary["url"] as? String + let statusURL = (statusDictionary?["url"] as? String) + let repairURLString = directRepairURL ?? directURL ?? statusURL + let repairURL = repairURLString.flatMap { URL(string: $0) } ?? Constants.URLs.developerAccount + + let rawMessage = (statusDictionary?["em"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) + + let message: String + if let rawMessage, !rawMessage.isEmpty { + message = rawMessage + } else { + message = Constants.defaultAccountRepairMessage + } + + debugLog("[SideSign] Account repair required: \(message) (url: \(repairURL.absoluteString)). Prompting accountRepairHandler...") + let decision = try await accountRepairHandler(repairURL, message) + + if decision == .cancel { + debugLog("[SideSign] Account repair cancelled by caller.") + throw DeveloperPortalError.accountRepairRequired(url: repairURL, message: message) + } + + debugLog("[SideSign] Account repair acknowledged by caller. Continuing to fetch app tokens...") + + default: + break + } + + guard let sessionKey = decryptedDictionary["sk"] as? Data else { + debugLog("[SideSign] Decrypted dictionary missing 'sk' key for apptokens") + throw ServerError.missingKey(key: "sk", jsonPayload: prettyJSONString(from: decryptedDictionary)) + } + + guard let c = decryptedDictionary["c"] as? Data else { + debugLog("[SideSign] Decrypted dictionary missing 'c' key for apptokens") + throw ServerError.missingKey(key: "c", jsonPayload: prettyJSONString(from: decryptedDictionary)) + } + + let app = customHeaders.grandSlam.authApp + guard let checksum = CryptoUtilities.hmacSHA256(key: sessionKey, strings: ["apptokens", dsid, app]) else { + debugLog("[SideSign] Failed to compute apptokens checksum") + throw DeveloperPortalError.authenticationHandshakeFailed(cause: "Failed to compute apptokens checksum") + } + + let appTokensParameters: [String: any Sendable] = [ + "app": [app], + "c": c, + "checksum": checksum, + "cpd": clientDictionary, + "o": "apptokens", + "t": idmsToken, + "u": dsid + ] + + let fetchedToken = try await fetchAuthToken(app: app, parameters: appTokensParameters, sessionKey: sessionKey, anisetteData: anisetteData) + let session = Session( + dsid: dsid, + authToken: fetchedToken.token, + anisetteData: anisetteData, + xcodeVersion: xcodeVersion, + machinePassword: machinePassword, + creationDate: fetchedToken.creationDate, + expirationDate: fetchedToken.expirationDate, + timeToLive: fetchedToken.timeToLive + ) + let account = try await fetchAccount(session: session) + return AuthSession(account: account, session: session) + } + + func sendAuthenticationRequest(parameters requestParameters: [String: any Sendable], anisetteData: AnisetteData) async throws -> [String: any Sendable] { + let requestURL = Constants.URLs.grandSlamAuth + let h = customHeaders + + let parameters: [String: any Sendable] = [ + "Header": ["Version": h.grandSlam.headerVersion], + "Request": requestParameters + ] + + let plistData = try PropertyListSerialization.data(fromPropertyList: parameters, format: .xml, options: 0) + + var request = URLRequest(url: requestURL) + request.httpMethod = "POST" + request.httpBody = plistData + + // SideInstaller base_headers parity (Windows GSA / akd clientInfo path) + let headers: [String: String] = [ + "Content-Type": "text/x-xml-plist", + "Accept": "text/x-xml-plist", + "X-MMe-Client-Info": anisetteData.clientInfo, + "User-Agent": h.grandSlam.userAgent, + "X-Xcode-Version": "14.2 (14C18)", + "X-Apple-App-Info": "com.apple.gs.xcode.auth", + "Connection": "close" + ] + headers.forEach { request.setValue($1, forHTTPHeaderField: $0) } + + if let allHeaders = request.allHTTPHeaderFields { + verboseLog("[SideSign] sendAuthenticationRequest HTTP headers: \(prettyJSONString(from: sanitizeHeadersForLogging(allHeaders)))") + } + + // SideInstaller: retry HTTP 429 with spaced backoff + let retryDelays: [UInt64] = [2_000_000_000, 5_000_000_000] // 2s, 5s + var data = Data() + var response: URLResponse = URLResponse() + var statusCode = 0 + var attempt = 0 + while true { + do { + (data, response) = try await session.data(for: request) + } catch { + debugLog("[SideSign] sendAuthenticationRequest network error: \(error)") + throw error + } + let httpResponse = response as? HTTPURLResponse + statusCode = httpResponse?.statusCode ?? 0 + if statusCode == HTTPStatusCodes.tooManyRequests, attempt < retryDelays.count { + let delay = retryDelays[attempt] + debugLog("[SideSign] GrandSlam HTTP 429 — retrying in \(delay / 1_000_000_000)s (attempt \(attempt + 1))") + try await Task.sleep(nanoseconds: delay) + attempt += 1 + continue + } + break + } + + guard !data.isEmpty else { + debugLog("[SideSign] Auth endpoint returned 0 bytes (HTTP \(statusCode))") + throw ServerError.badServerResponse(reason: "Auth endpoint returned empty response (0 bytes)", jsonPayload: "0 bytes") + } + + guard let responseDictionary = parsePlistOrJSON(data) else { + let rawStr = String(data: data, encoding: .utf8) ?? data.hexEncodedString() + debugLog("[SideSign] Auth endpoint returned invalid response format: \(rawStr)") + throw ServerError.invalidResponseFormat(rawPayload: rawStr) + } + + let dictionary = (responseDictionary["Response"] as? [String: any Sendable]) ?? responseDictionary + guard let status = dictionary["Status"] as? [String: any Sendable] else { + let rawStr = prettyJSONString(from: responseDictionary) + debugLog("[SideSign] Auth endpoint response missing 'Status': \(rawStr)") + throw ServerError.missingKey(key: "Status", jsonPayload: rawStr) + } + + let errorCode = status["ec"] as? Int ?? 0 + if errorCode != 0 { + let errorDesc = status["em"] as? String + debugLog("[SideSign] Auth endpoint returned error code \(errorCode): \(errorDesc ?? "No error message")") + switch errorCode { + case GrandSlamAuthErrorCodes.incorrectCredentials: + throw DeveloperPortalError.incorrectCredentials(cause: errorDesc) + case GrandSlamAuthErrorCodes.appSpecificPasswordRequired, + GrandSlamAuthErrorCodes.appSpecificPasswordRequiredFallback: + throw DeveloperPortalError.appSpecificPasswordRequired(cause: errorDesc) + case GrandSlamAuthErrorCodes.incorrectVerificationCode: + throw DeveloperPortalError.incorrectVerificationCode(cause: errorDesc) + default: + throw ServerError.underlyingError(code: errorCode, message: errorDesc ?? "Authentication failed") + } + } + + return dictionary + } + + private struct FetchedAuthToken { + let token: String + let creationDate: Date + let expirationDate: Date? + let timeToLive: TimeInterval? + } + + private func fetchAuthToken(app: String, parameters: [String: any Sendable], sessionKey: Data, anisetteData: AnisetteData) async throws -> FetchedAuthToken { + let responseDictionary = try await sendAuthenticationRequest(parameters: parameters, anisetteData: anisetteData) + + guard let encryptedToken = responseDictionary["et"] as? Data else { + let payload = prettyJSONString(from: responseDictionary) + debugLog("[SideSign] fetchAuthToken missing 'et' key in response") + throw ServerError.missingKey(key: "et", jsonPayload: payload) + } + + guard encryptedToken.count > 35 else { + debugLog("[SideSign] Encrypted token payload is too short (length: \(encryptedToken.count))") + throw DeveloperPortalError.authenticationHandshakeFailed(cause: "Encrypted token payload is too short (\(encryptedToken.count) bytes)") + } + + let aad = Data(encryptedToken[..<3]) + let nonce = Data(encryptedToken[3..<19]) + let ciphertext = Data(encryptedToken[19..<(encryptedToken.count - 16)]) + let tag = Data(encryptedToken[(encryptedToken.count - 16)...]) + + guard let token = CryptoUtilities.aesGCMDecrypt(key: sessionKey, nonce: nonce, aad: aad, ciphertext: ciphertext, tag: tag) else { + debugLog("[SideSign] Failed to AES-GCM decrypt auth token") + throw DeveloperPortalError.authenticationHandshakeFailed(cause: "Failed to AES-GCM decrypt auth token") + } + + guard let tokensDictionary = parsePlistOrJSON(token) else { + let rawStr = prettyJSONString(from: token) + debugLog("[SideSign] Failed to parse decrypted token dictionary") + throw ServerError.invalidResponseFormat(rawPayload: rawStr) + } + + guard let appTokens = tokensDictionary["t"] as? [String: any Sendable], + let tokens = appTokens[app] as? [String: any Sendable], + let authToken = tokens["token"] as? String + else { + let payload = prettyJSONString(from: tokensDictionary) + debugLog("[SideSign] Decrypted tokens missing t/\(app)/token") + throw ServerError.missingKey(key: "t/\(app)/token", jsonPayload: payload) + } + + verboseLog("[SideSign] Decrypted GrandSlam response: \(prettyJSONString(from: sanitizeTokens(tokensDictionary)))") + + let now = Date() + var expirationDate: Date? = nil + var timeToLive: TimeInterval? = nil + + if let expiry = tokens["expiry"] as? Date { + expirationDate = expiry + timeToLive = expiry.timeIntervalSince(now) + } else if let expiryStr = tokens["expiry"] as? String, let parsed = ISO8601DateFormatter().date(from: expiryStr) { + expirationDate = parsed + timeToLive = parsed.timeIntervalSince(now) + } else if let ttl = tokens["ttl"] as? Double ?? (tokens["ttl"] as? Int).map(Double.init) { + timeToLive = ttl + expirationDate = now.addingTimeInterval(ttl) + } else if let exp = tokens["expiry-date"] as? Date { + expirationDate = exp + timeToLive = exp.timeIntervalSince(now) + } else if let expStr = tokens["expiry-date"] as? String, let parsed = ISO8601DateFormatter().date(from: expStr) { + expirationDate = parsed + timeToLive = parsed.timeIntervalSince(now) + } + + let ttlDesc: String + if let ttl = timeToLive { + let days = Int(ttl / 86400) + let hours = Int((ttl.truncatingRemainder(dividingBy: 86400)) / 3600) + ttlDesc = "\(days)d \(hours)h (\(Int(ttl))s)" + } else { + ttlDesc = "unspecified" + } + + let expiryDesc = expirationDate.map { ISO8601DateFormatter().string(from: $0) } ?? "unspecified" + debugLog("[SideSign] Successfully obtained auth token for app: \(app) (TTL: \(ttlDesc), Expiry: \(expiryDesc))") + return FetchedAuthToken(token: authToken, creationDate: now, expirationDate: expirationDate, timeToLive: timeToLive) + } + + private func parseTrustedPhoneNumbers(from dict: [String: any Sendable]?) -> [TrustedPhoneNumber] { + var results: [TrustedPhoneNumber] = [] + let list = (dict?["trustedPhoneNumbers"] as? [[String: any Sendable]]) + ?? (dict?["phoneNumbers"] as? [[String: any Sendable]]) + ?? [] + for item in list { + if let id = (item["id"] as? CustomStringConvertible)?.description.trimmingCharacters(in: .whitespacesAndNewlines), !id.isEmpty { + let num = (item["numberWithDialCode"] as? String) + ?? (item["obfuscatedNumber"] as? String) + ?? (item["lastTwoDigits"] as? String).map { "••\($0)" } + ?? "Phone \(id)" + results.append(TrustedPhoneNumber(id: id, number: num)) + } + } + if results.isEmpty, let single = dict?["phoneNumber"] as? [String: any Sendable], + let id = (single["id"] as? CustomStringConvertible)?.description.trimmingCharacters(in: .whitespacesAndNewlines), !id.isEmpty { + let num = (single["numberWithDialCode"] as? String) + ?? (single["obfuscatedNumber"] as? String) + ?? (single["lastTwoDigits"] as? String).map { "••\($0)" } + ?? "Phone \(id)" + results.append(TrustedPhoneNumber(id: id, number: num)) + } + return results + } + + private func parseXMLUIAlertMessage(from data: Data) -> (title: String?, message: String?) { + guard let str = String(data: data, encoding: .utf8) else { return (nil, nil) } + if str.contains("]*\bid=)[^>]*>"#, options: .regularExpression) else { + return (nil, nil) + } + let alertTag = String(str[alertTagRange]) + var title: String? + var message: String? + if let titleRange = alertTag.range(of: #"(?<=title=")[^"]+"#, options: .regularExpression) { + title = String(alertTag[titleRange]) + } + if let msgRange = alertTag.range(of: #"(?<=message=")[^"]+"#, options: .regularExpression) { + message = String(alertTag[msgRange]) + } + return (title, message) + } + + private func parseXMLUIServerInfo(from data: Data) -> (phoneID: String?, mode: String?) { + guard let str = String(data: data, encoding: .utf8) else { return (nil, nil) } + guard let range = str.range(of: #"]*>"#, options: .regularExpression) else { return (nil, nil) } + let tag = String(str[range]) + var phoneID: String? + var mode: String? + if let idRange = tag.range(of: #"(?<=phoneNumber\.id=")[^"]+"#, options: .regularExpression) { + phoneID = String(tag[idRange]) + } + if let modeRange = tag.range(of: #"(?<=mode=")[^"]+"#, options: .regularExpression) { + mode = String(tag[modeRange]) + } + return (phoneID, mode) + } + + private func throwIfXMLUIErrorAlert(in data: Data, statusCode: Int, actionName: String) throws { + let (xmluiTitle, xmluiMessage) = parseXMLUIAlertMessage(from: data) + if xmluiTitle != nil || xmluiMessage != nil { + let alertMsg = [xmluiTitle, xmluiMessage] + .compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + .joined(separator: ": ") + debugLog("[SideSign] \(actionName) alert from Apple (HTTP \(statusCode)): \(alertMsg)") + throw DeveloperPortalError.invalid2FAResponse(cause: xmluiMessage ?? alertMsg) + } + } + + private func parseXMLUIObfuscatedNumber(from data: Data) -> String? { + guard let str = String(data: data, encoding: .utf8) else { return nil } + let patterns = [ + #"(?:to|at)\s+([+•\d\s\(\)-]{4,25})[.\s<]"#, + #"([+•\d\s\(\)-]*[•]+[+•\d\s\(\)-]*)"# + ] + for pattern in patterns { + if let matchRange = str.range(of: pattern, options: .regularExpression) { + let matched = String(str[matchRange]) + .replacingOccurrences(of: "to ", with: "") + .replacingOccurrences(of: "at ", with: "") + .trimmingCharacters(in: CharacterSet(charactersIn: ". <\n\r\t")) + if matched.contains("•") && matched.count >= 3 { + return matched + } + } + } + return nil + } + + private struct TwoFactorAuthContext { + let dsid: String + let idmsToken: String + let anisetteData: AnisetteData + let xcodeVersion: String + } + + private struct TwoFactorAuthPhoneCodeResponse { + let phoneID: String + let activeMode: String + let phoneNumbers: [TrustedPhoneNumber] + let statusCode: Int + } + + private enum TwoFactorAuthChannel { + case trustedDevice + case sms(phoneID: String) + case voice(phoneID: String) + } + + private enum TwoFactorAuthValidationResult { + case success + case retry(message: String) + } + + private func handle2FARequest(isTrustedDevice: Bool, + completeResponseDictionary: [String: any Sendable], + statusDictionary: [String: any Sendable]?, + context: TwoFactorAuthContext, + verificationHandler: VerificationHandler?, + codeAlreadySent: Bool = false) async throws + { + guard let verificationHandler else { + debugLog("[SideSign] 2FA required but no verificationHandler provided") + throw DeveloperPortalError.requiresTwoFactorAuthentication + } + + var phoneNumbers = parseTrustedPhoneNumbers(from: completeResponseDictionary) + if phoneNumbers.isEmpty, let statusDictionary { + phoneNumbers = parseTrustedPhoneNumbers(from: statusDictionary) + } + + let preferredMode: TwoFactorDeliveryMode = isTrustedDevice ? .trustedDevice : .sms + + var currentRequest: TwoFactorRequest = codeAlreadySent + ? .trustedDevice(error: nil) + : .selectDeliveryMethod(preferredMode: preferredMode, phoneNumbers: phoneNumbers) + var activeChannel: TwoFactorAuthChannel? = codeAlreadySent ? .trustedDevice : nil + + while true { + debugLog("[SideSign] Prompting user with 2FA request (\(currentRequest))...") + let response = try await verificationHandler(currentRequest) + + switch response { + case .requestTrustedDevice: + try await sendTrustedDevice2FACodeRequest(context: context) + activeChannel = .trustedDevice + currentRequest = .trustedDevice(error: nil) + + case .requestSMS(let targetPhoneID): + let result = try await sendPhone2FACodeRequest(mode: "sms", phoneID: targetPhoneID, knownPhoneNumbers: phoneNumbers, context: context) + phoneNumbers = result.phoneNumbers + activeChannel = .sms(phoneID: result.phoneID) + currentRequest = .sms(phoneNumbers: phoneNumbers, activeID: result.phoneID, error: nil) + + case .requestVoice(let targetPhoneID): + let result = try await sendPhone2FACodeRequest(mode: "voice", phoneID: targetPhoneID, knownPhoneNumbers: phoneNumbers, context: context) + phoneNumbers = result.phoneNumbers + activeChannel = .voice(phoneID: result.phoneID) + currentRequest = .voice(phoneNumbers: phoneNumbers, activeID: result.phoneID, error: nil) + + case .verificationCode(let code): + guard let channel = activeChannel else { + debugLog("[SideSign] Unexpected verification code returned before delivery method was selected.") + throw DeveloperPortalError.authenticationHandshakeFailed(cause: "Unexpected verification code returned before a delivery method was selected.") + } + + let result: TwoFactorAuthValidationResult + switch channel { + case .trustedDevice: + result = try await validateTrustedDevice2FACode(code: code, context: context) + case .sms(let phoneID): + result = try await validatePhone2FACode(code: code, phoneID: phoneID, mode: "sms", context: context) + case .voice(let phoneID): + result = try await validatePhone2FACode(code: code, phoneID: phoneID, mode: "voice", context: context) + } + + switch result { + case .success: + debugLog("[SideSign] 2FA code verified successfully!") + return + case .retry(let message): + debugLog("[SideSign] 2FA verification failed, retrying: \(message)") + switch channel { + case .trustedDevice: + currentRequest = .trustedDevice(error: message) + case .sms(let phoneID): + currentRequest = .sms(phoneNumbers: phoneNumbers, activeID: phoneID, error: message) + case .voice(let phoneID): + currentRequest = .voice(phoneNumbers: phoneNumbers, activeID: phoneID, error: message) + } + } + + case .cancel: + debugLog("[SideSign] User cancelled 2FA.") + throw DeveloperPortalError.userCancelled + } + } + } + + private func sendTrustedDevice2FACodeRequest(context: TwoFactorAuthContext) async throws { + debugLog("[SideSign] Requesting trusted device 2FA code...") + verboseLog("[SideSign] sendTrustedDevice2FACodeRequest for dsid: \(context.dsid)") + + var request = makeTwoFactorAuthRequest(url: Constants.URLs.trustedDevice, context: context) + request.httpMethod = "GET" + + if let allHeaders = request.allHTTPHeaderFields { + verboseLog("[SideSign] sendTrustedDevice2FACodeRequest HTTP headers: \(prettyJSONString(from: sanitizeHeadersForLogging(allHeaders)))") + } + + let (data, response) = try await session.data(for: request) + let httpResponse = response as? HTTPURLResponse + let statusCode = httpResponse?.safeStatusCode ?? 0 + try throwIfXMLUIErrorAlert(in: data, statusCode: statusCode, actionName: "sendTrustedDevice2FACodeRequest") + + guard statusCode == HTTPStatusCodes.ok else { + let rawStr = prettyJSONString(from: data) + debugLog("[SideSign] sendTrustedDevice2FACodeRequest failed (HTTP \(statusCode)): \(rawStr)") + throw ServerError.badServerResponse(reason: "Trusted device request failed (HTTP \(statusCode))", jsonPayload: rawStr) + } + } + + private func sendPhone2FACodeRequest(mode requestedMode: String, + phoneID requestedPhoneID: String? = nil, + knownPhoneNumbers: [TrustedPhoneNumber] = [], + context: TwoFactorAuthContext) async throws -> TwoFactorAuthPhoneCodeResponse + { + debugLog("[SideSign] Requesting secondary/phone 2FA code (mode: \(requestedMode), phoneID: \(requestedPhoneID ?? "auto"))...") + verboseLog("[SideSign] sendPhone2FACodeRequest for dsid: \(context.dsid), requestedMode: \(requestedMode), phoneID: \(requestedPhoneID ?? "nil")") + + let sanitizedPhoneID: String = { + if let id = requestedPhoneID?.trimmingCharacters(in: .whitespacesAndNewlines), !id.isEmpty { + return id + } + return "1" + }() + + let serverInfo: [String: any Sendable] = [ + "mode": requestedMode, + "phoneNumber.id": sanitizedPhoneID + ] + + var request = makeTwoFactorAuthRequest(url: Constants.URLs.phonePutURL(mode: requestedMode), context: context) + request.httpMethod = "POST" + request.httpBody = try PropertyListSerialization.data(fromPropertyList: [ + "serverInfo": serverInfo + ], format: .xml, options: 0) + + if let allHeaders = request.allHTTPHeaderFields { + verboseLog("[SideSign] sendPhone2FACodeRequest HTTP headers: \(prettyJSONString(from: sanitizeHeadersForLogging(allHeaders)))") + } + + let (data, response) = try await session.data(for: request) + let httpResponse = response as? HTTPURLResponse + let statusCode = httpResponse?.safeStatusCode ?? 0 + + let rawStr = prettyJSONString(from: data) + verboseLog("[SideSign] sendPhone2FACodeRequest raw response (HTTP \(statusCode)): \(rawStr)") + + try throwIfXMLUIErrorAlert(in: data, statusCode: statusCode, actionName: "sendPhone2FACodeRequest") + + let responseDict = parsePlistOrJSON(data) + let errorCode = responseDict?["ec"] as? Int ?? 0 + let errorMsg = (responseDict?["em"] as? String) + ?? ((responseDict?["Status"] as? [String: any Sendable])?["em"] as? String) + + if errorCode == GrandSlamAuthErrorCodes.tooManyAttempts + || errorCode == GrandSlamAuthErrorCodes.tooManyCodesRequested + || errorCode == GrandSlamAuthErrorCodes.rateLimited + || statusCode == HTTPStatusCodes.tooManyRequests + { + let msg = errorMsg ?? "Verification codes cannot be sent to this phone number at this time. Please try again later." + debugLog("[SideSign] sendPhone2FACodeRequest rate-limited (\(errorCode), HTTP \(statusCode)): \(msg)") + throw DeveloperPortalError.tooManyAttempts(cause: msg) + } else if errorCode != 0 { + let msg = errorMsg ?? "Failed to request verification code from Apple." + debugLog("[SideSign] sendPhone2FACodeRequest error (\(errorCode), HTTP \(statusCode)): \(msg)") + throw ServerError.underlyingError(code: errorCode, message: msg) + } + + guard statusCode == HTTPStatusCodes.ok else { + let reason = errorMsg ?? HTTPStatusCodes.localizedDescription(for: statusCode) + debugLog("[SideSign] sendPhone2FACodeRequest failed (HTTP \(statusCode)): \(reason)") + throw ServerError.badServerResponse(reason: reason, jsonPayload: rawStr) + } + + var parsedNumbers = parseTrustedPhoneNumbers(from: responseDict) + if parsedNumbers.isEmpty { + parsedNumbers = knownPhoneNumbers + } + let singlePhoneDict = responseDict?["phoneNumber"] as? [String: any Sendable] + let phoneListFirst = (responseDict?["phoneNumbers"] as? [[String: any Sendable]])?.first + let trustedPhoneListFirst = (responseDict?["trustedPhoneNumbers"] as? [[String: any Sendable]])?.first + let phoneDict = singlePhoneDict ?? phoneListFirst ?? trustedPhoneListFirst + + let (xmluiServerPhoneID, xmluiServerMode) = parseXMLUIServerInfo(from: data) + + let rawPhoneID = (phoneDict?["id"] as? CustomStringConvertible)?.description.trimmingCharacters(in: .whitespacesAndNewlines) + let resolvedPhoneID: String? = (rawPhoneID?.isEmpty == false) ? rawPhoneID : xmluiServerPhoneID + let resolvedRequestedID: String? = (requestedPhoneID?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false) ? requestedPhoneID : nil + + let phoneID = resolvedPhoneID ?? resolvedRequestedID ?? parsedNumbers.first?.id ?? "1" + let activeMode = (phoneDict?["mode"] as? String) ?? xmluiServerMode ?? requestedMode + + let numberWithDialCode = phoneDict?["numberWithDialCode"] as? String + let obfuscatedNumber = phoneDict?["obfuscatedNumber"] as? String + let lastTwoDigits = (phoneDict?["lastTwoDigits"] as? String).map { "••\($0)" } + let xmluiNumber = parseXMLUIObfuscatedNumber(from: data) + let matchedNumber = parsedNumbers.first(where: { $0.id == phoneID })?.number + + let numberObfuscated = numberWithDialCode + ?? obfuscatedNumber + ?? lastTwoDigits + ?? xmluiNumber + ?? matchedNumber + ?? "" + if let idx = parsedNumbers.firstIndex(where: { $0.id == phoneID }), !numberObfuscated.isEmpty { + parsedNumbers[idx] = TrustedPhoneNumber(id: phoneID, number: numberObfuscated) + } else if parsedNumbers.isEmpty && !numberObfuscated.isEmpty { + parsedNumbers = [TrustedPhoneNumber(id: phoneID, number: numberObfuscated)] + } + debugLog("[SideSign] sendPhone2FACodeRequest received phone response (id: \(phoneID), mode: \(activeMode), number: \(numberObfuscated), total phones: \(parsedNumbers.count))") + + return TwoFactorAuthPhoneCodeResponse( + phoneID: phoneID, + activeMode: activeMode, + phoneNumbers: parsedNumbers, + statusCode: statusCode + ) + } + + private func validateTrustedDevice2FACode(code: String, context: TwoFactorAuthContext) async throws -> TwoFactorAuthValidationResult { + var verifyRequest = makeTwoFactorAuthRequest(url: Constants.URLs.grandSlamValidate, context: context) + verifyRequest.setValue(code, forHTTPHeaderField: "security-code") + + if let allHeaders = verifyRequest.allHTTPHeaderFields { + verboseLog("[SideSign] validateTrustedDevice2FACode HTTP headers: \(prettyJSONString(from: sanitizeHeadersForLogging(allHeaders)))") + } + + debugLog("[SideSign] Verifying trusted device security code...") + let (verifyData, verifyResponse) = try await session.data(for: verifyRequest) + let verifyHttpResponse = verifyResponse as? HTTPURLResponse + let verifyStatusCode = verifyHttpResponse?.safeStatusCode ?? 0 + + return try parseTwoFactorAuthVerifyResponse(data: verifyData, statusCode: verifyStatusCode, requirePeToken: false, httpResponse: verifyHttpResponse) + } + + private func validatePhone2FACode(code: String, + phoneID: String, + mode: String, + context: TwoFactorAuthContext) async throws -> TwoFactorAuthValidationResult + { + var verifyRequest = makeTwoFactorAuthRequest(url: Constants.URLs.phoneSecurityCode, context: context) + verifyRequest.httpMethod = "POST" + verifyRequest.httpBody = try PropertyListSerialization.data(fromPropertyList: [ + "securityCode.code": code, + "serverInfo": ["mode": mode, "phoneNumber.id": phoneID] + ], format: .xml, options: 0) + + if let allHeaders = verifyRequest.allHTTPHeaderFields { + verboseLog("[SideSign] validatePhone2FACode HTTP headers: \(prettyJSONString(from: sanitizeHeadersForLogging(allHeaders)))") + } + + debugLog("[SideSign] Verifying secondary security code...") + let (verifyData, verifyResponse) = try await session.data(for: verifyRequest) + let verifyHttpResponse = verifyResponse as? HTTPURLResponse + let verifyStatusCode = verifyHttpResponse?.safeStatusCode ?? 0 + + return try parseTwoFactorAuthVerifyResponse(data: verifyData, statusCode: verifyStatusCode, requirePeToken: true, httpResponse: verifyHttpResponse) + } + + private func parseTwoFactorAuthVerifyResponse(data: Data, + statusCode: Int, + requirePeToken: Bool, + httpResponse: HTTPURLResponse?) throws -> TwoFactorAuthValidationResult + { + let verifyDictionary = parsePlistOrJSON(data) + let (xmluiTitle, xmluiMessage) = parseXMLUIAlertMessage(from: data) + let errorCode = verifyDictionary?["ec"] as? Int ?? 0 + let statusDict = verifyDictionary?["Status"] as? [String: any Sendable] + let errorMsg = (verifyDictionary?["em"] as? String) + ?? (statusDict?["em"] as? String) + ?? xmluiMessage + ?? xmluiTitle + + if errorCode == GrandSlamAuthErrorCodes.tooManyAttempts + || errorCode == GrandSlamAuthErrorCodes.tooManyCodesRequested + || errorCode == GrandSlamAuthErrorCodes.rateLimited + || statusCode == HTTPStatusCodes.tooManyRequests + { + let msg = errorMsg ?? "Too many verification code attempts. Please try again later." + debugLog("[SideSign] Too many 2FA attempts (\(errorCode), HTTP \(statusCode)): \(msg)") + throw DeveloperPortalError.tooManyAttempts(cause: msg) + } else if errorCode == GrandSlamAuthErrorCodes.incorrectVerificationCode { + let msg = errorMsg ?? "Incorrect verification code. Please try again." + debugLog("[SideSign] Incorrect 2FA verification code (\(errorCode), HTTP \(statusCode)): \(msg)") + return .retry(message: msg) + } else if errorCode != 0 { + let msg = errorMsg ?? "2FA verification error" + debugLog("[SideSign] 2FA verification error (\(errorCode), HTTP \(statusCode)): \(msg)") + throw ServerError.underlyingError(code: errorCode, message: msg) + } + + if xmluiTitle != nil || xmluiMessage != nil { + let message = xmluiMessage ?? errorMsg ?? xmluiTitle ?? "Verification failed" + debugLog("[SideSign] 2FA verification fatal alert from Apple (HTTP \(statusCode)): \(message)") + throw DeveloperPortalError.invalid2FAResponse(cause: message) + } + + guard statusCode == HTTPStatusCodes.ok else { + let rawStr = prettyJSONString(from: data) + let reason = errorMsg ?? HTTPStatusCodes.localizedDescription(for: statusCode) + debugLog("[SideSign] 2FA verification failed (HTTP \(statusCode)): \(reason) - body: \(rawStr)") + return .retry(message: reason) + } + + if requirePeToken { + guard httpResponse?.allHeaderFields.keys.contains(where: { ($0 as? String)?.lowercased() == "x-apple-pe-token" }) == true else { + let rawStr = prettyJSONString(from: data) + let reason = errorMsg ?? "Incorrect verification code or missing session token" + debugLog("[SideSign] Secondary code verification failed (HTTP \(HTTPStatusCodes.ok) missing PE token header): \(reason) - Body: \(rawStr)") + return .retry(message: reason) + } + } + + return .success + } + + private func makeTwoFactorAuthRequest(url: URL, context: TwoFactorAuthContext) -> URLRequest { + let identityToken = "\(context.dsid):\(context.idmsToken)" + let encodedIdentityToken = Data(identityToken.utf8).base64EncodedString() + + var request = URLRequest(url: url) + let a = context.anisetteData + let h = customHeaders + let headers: [String: String] = [ + "Accept": "application/x-buddyml", + "Accept-Language": "en-us", + "Content-Type": "application/x-plist", + "User-Agent": h.developerServices.userAgent, + "X-Apple-App-Info": h.grandSlam.authApp, + "X-Xcode-Version": context.xcodeVersion, + "X-Apple-Identity-Token": encodedIdentityToken, + "X-Apple-I-MD": a.oneTimePassword, + "X-Apple-I-MD-M": a.machineID, + "X-Mme-Device-Id": a.deviceID, + "X-MMe-Client-Info": a.clientInfo, + "X-Apple-I-MD-LU": a.localUserID, + "X-Apple-I-MD-RINFO": a.routingInfo, + "X-Apple-I-SRL-NO": a.serialNumber, + "X-Apple-I-Client-Time": a.clientTime, + "X-Apple-Locale": a.locale, + "X-Apple-I-TimeZone": a.timeZone, + "Connection": "close" + ] + headers.forEach { request.setValue($1, forHTTPHeaderField: $0) } + return request + } + + private func parsePlistOrJSON(_ data: Data) -> [String: any Sendable]? { + (try? PropertyListSerialization.propertyList(from: data, format: nil)) as? [String: any Sendable] + ?? (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: any Sendable] } } From cc3dd8b934f5535e87b0cf13f09a56b0cd86e2f6 Mon Sep 17 00:00:00 2001 From: Mr3242-Scripter Date: Tue, 20 Oct 2026 11:56:33 +0000 Subject: [PATCH 07/15] Update Constants.swift Signed-off-by: Mr3242-Scripter --- Sources/Constants.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Constants.swift b/Sources/Constants.swift index 53d3070..02c336b 100644 --- a/Sources/Constants.swift +++ b/Sources/Constants.swift @@ -15,13 +15,13 @@ public enum Constants { public static let service = "iCloud" public static let headerVersion = "1.0.1" public static let authApp = "com.apple.gs.xcode.auth" - public static let userAgent = "AuthKit/1 (Macintosh; OS X 26.6) (com.apple.dt.Xcode/26.0)" + public static let userAgent = "akd/1.0 CFNetwork/808.1.4" // SideInstaller temporary Windows GSA fix } // https://idmsa.apple.com/appleauth/auth/devices public enum AppleAuth { public static let appIDKey = "ba2ec180e6ca6e6c6a542255453b24d6e6e5b2be0cc48bc1b0d8ad64cfe0228f" - public static let userAgent = "AuthKit/1 (Macintosh; OS X 26.6)" + public static let userAgent = "akd/1.0 CFNetwork/808.1.4" // SideInstaller temporary Windows GSA fix } // https://developerservices2.apple.com/services/ From 23ae5c062ffcdb489d98ee8316ba47d1b982512c Mon Sep 17 00:00:00 2001 From: Mr3242-Scripter Date: Tue, 20 Oct 2026 11:57:22 +0000 Subject: [PATCH 08/15] Update Authentication.swift Signed-off-by: Mr3242-Scripter --- Sources/DeveloperPortal/Authentication.swift | 37 ++++++++++++++++---- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/Sources/DeveloperPortal/Authentication.swift b/Sources/DeveloperPortal/Authentication.swift index fcd5618..76c85c3 100644 --- a/Sources/DeveloperPortal/Authentication.swift +++ b/Sources/DeveloperPortal/Authentication.swift @@ -317,12 +317,36 @@ public extension DeveloperPortal { request.httpMethod = "POST" request.httpBody = plistData - // SideInstaller base_headers parity (Windows GSA / akd clientInfo path) + // SideInstaller base_headers parity (Windows GSA / akd) + // Prefer anisette identity UA; never send Macintosh/Xcode (Apple 503s those). + let windowsClientInfo = " " + let akdUserAgent = "akd/1.0 CFNetwork/808.1.4" + var clientInfo = anisetteData.clientInfo.trimmingCharacters(in: .whitespacesAndNewlines) + if clientInfo.isEmpty + || clientInfo.contains("MacBook") + || clientInfo.contains("macOS") + || clientInfo.contains("Mac OS") + || clientInfo.contains(" Date: Tue, 20 Oct 2026 11:58:13 +0000 Subject: [PATCH 09/15] Update AnisetteDataManager.swift Signed-off-by: Mr3242-Scripter --- Sources/Anisette/AnisetteDataManager.swift | 23 +++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/Sources/Anisette/AnisetteDataManager.swift b/Sources/Anisette/AnisetteDataManager.swift index 8c64485..41a2955 100644 --- a/Sources/Anisette/AnisetteDataManager.swift +++ b/Sources/Anisette/AnisetteDataManager.swift @@ -433,8 +433,17 @@ public final class AnisetteDataManager: @unchecked Sendable { } } - let clientInfo = headers?.clientInfo ?? Constants.Anisette.defaultClientInfo - let client = try await getClient(for: resolvedMode, clientInfo: clientInfo) + let windowsClientInfo = " " + let akdUserAgent = "akd/1.0 CFNetwork/808.1.4" + var clientInfo = headers?.clientInfo ?? windowsClientInfo + if clientInfo.isEmpty || clientInfo.contains("MacBook") || clientInfo.contains(" AnisetteClient { + private func getClient(for mode: AnisetteMode, clientInfo: String, userAgent: String = "akd/1.0 CFNetwork/808.1.4") async throws -> AnisetteClient { switch mode { case .remote(let server): try FileManager.default.createDirectory(at: provisioningDir, withIntermediateDirectories: true) return try AnisetteClient( provisioningDir: provisioningDir, clientInfo: clientInfo, - userAgent: Constants.Anisette.defaultUserAgent, + userAgent: userAgent, lookupURL: Constants.Anisette.URLs.grandSlamLookup, requiredLibraries: Constants.Anisette.Libraries.requiredNames, provider: RemoteAnisetteDataProvider(serverURL: server) @@ -489,7 +498,7 @@ public final class AnisetteDataManager: @unchecked Sendable { return try AnisetteClient( provisioningDir: targetProvDir, clientInfo: clientInfo, - userAgent: Constants.Anisette.defaultUserAgent, + userAgent: userAgent, lookupURL: Constants.Anisette.URLs.grandSlamLookup, requiredLibraries: Constants.Anisette.Libraries.requiredNames, libraryDirectoryResolver: { libDir } @@ -648,7 +657,7 @@ public final class AnisetteDataManager: @unchecked Sendable { self.localProvider = try AnisetteClient( provisioningDir: prov, clientInfo: clientInfo, - userAgent: Constants.Anisette.defaultUserAgent, + userAgent: "akd/1.0 CFNetwork/808.1.4", lookupURL: Constants.Anisette.URLs.grandSlamLookup, requiredLibraries: Constants.Anisette.Libraries.requiredNames, libraryDirectoryResolver: { libDir } @@ -699,7 +708,7 @@ public final class AnisetteDataManager: @unchecked Sendable { let provider = try AnisetteClient( provisioningDir: prov, clientInfo: clientInfo, - userAgent: Constants.Anisette.defaultUserAgent, + userAgent: "akd/1.0 CFNetwork/808.1.4", lookupURL: Constants.Anisette.URLs.grandSlamLookup, requiredLibraries: Constants.Anisette.Libraries.requiredNames, libraryDirectoryResolver: { libDir } From 67b9b708f065f3f0d29a1b004b069a855d4cad6d Mon Sep 17 00:00:00 2001 From: Mr3242-Scripter Date: Tue, 20 Oct 2026 14:56:30 +0200 Subject: [PATCH 10/15] Update DeveloperPortalAPI.swift Signed-off-by: Mr3242-Scripter --- .../DeveloperPortal/DeveloperPortalAPI.swift | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/Sources/DeveloperPortal/DeveloperPortalAPI.swift b/Sources/DeveloperPortal/DeveloperPortalAPI.swift index dfea7cc..bd6ac7f 100644 --- a/Sources/DeveloperPortal/DeveloperPortalAPI.swift +++ b/Sources/DeveloperPortal/DeveloperPortalAPI.swift @@ -255,8 +255,21 @@ public final class DeveloperPortal: DeveloperPortalAPI, Sendable { let session: URLSession - public init(session: URLSession = .shared, customHeaders: SideSignHeaders = SideSignHeaders()) { - self.session = session + /// SideInstaller uses pool_max_idle_per_host(0) so GSA never reuses connections + /// (reused connections often get HTTP 5xx / empty bodies). + private static func makeGSASession() -> URLSession { + let config = URLSessionConfiguration.ephemeral + config.httpShouldUsePipelining = false + config.httpMaximumConnectionsPerHost = 1 + config.requestCachePolicy = .reloadIgnoringLocalCacheData + config.urlCache = nil + config.timeoutIntervalForRequest = 60 + config.timeoutIntervalForResource = 120 + return URLSession(configuration: config) + } + + public init(session: URLSession? = nil, customHeaders: SideSignHeaders = SideSignHeaders()) { + self.session = session ?? Self.makeGSASession() self.cachedCustomHeaders = customHeaders } @@ -327,7 +340,7 @@ public final class DeveloperPortal: DeveloperPortalAPI, Sendable { "X-Apple-I-MD-LU": a.localUserID, "X-Apple-I-MD-RINFO": a.routingInfo, "X-Mme-Device-Id": a.deviceID, - "X-MMe-Client-Info": a.clientInfo, + "X-Mme-Client-Info": a.clientInfo, "X-Apple-I-Client-Time": a.clientTime, "X-Apple-Locale": a.locale, "X-Apple-I-Locale": a.locale, @@ -438,7 +451,7 @@ public final class DeveloperPortal: DeveloperPortalAPI, Sendable { headers["X-Apple-I-MD-LU"] = a.localUserID headers["X-Apple-I-MD-RINFO"] = a.routingInfo headers["X-Mme-Device-Id"] = a.deviceID - headers["X-MMe-Client-Info"] = a.clientInfo + headers["X-Mme-Client-Info"] = a.clientInfo headers["X-Apple-I-Client-Time"] = a.clientTime headers["X-Apple-Locale"] = a.locale headers["X-Apple-I-Locale"] = a.locale From fbe9d4545620f992af2deadbea9a8c888c8a1a32 Mon Sep 17 00:00:00 2001 From: Mr3242-Scripter Date: Tue, 20 Oct 2026 14:57:10 +0200 Subject: [PATCH 11/15] Update Authentication.swift Signed-off-by: Mr3242-Scripter --- Sources/DeveloperPortal/Authentication.swift | 21 +++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/Sources/DeveloperPortal/Authentication.swift b/Sources/DeveloperPortal/Authentication.swift index 76c85c3..e5b8aa9 100644 --- a/Sources/DeveloperPortal/Authentication.swift +++ b/Sources/DeveloperPortal/Authentication.swift @@ -345,7 +345,7 @@ public extension DeveloperPortal { let headers: [String: String] = [ "Content-Type": "text/x-xml-plist", "Accept": "text/x-xml-plist", - "X-MMe-Client-Info": clientInfo, + "X-Mme-Client-Info": clientInfo, "User-Agent": userAgent, "X-Xcode-Version": "14.2 (14C18)", "X-Apple-App-Info": "com.apple.gs.xcode.auth", @@ -383,6 +383,21 @@ public extension DeveloperPortal { break } + // After retries, refuse HTML/503 so callers purge adi and try another server + if statusCode == 503 || statusCode == 433 { + let rawStr = String(data: data, encoding: .utf8) ?? data.hexEncodedString() + debugLog("[SideSign] GrandSlam still HTTP \(statusCode) after retries: \(rawStr.prefix(200))") + throw ServerError.badServerResponse( + reason: "Apple sign-in refused the request (HTTP \(statusCode)). Try a Windows anisette server and sign in again.", + jsonPayload: rawStr + ) + } + if statusCode != 0 && !(200...299).contains(statusCode) { + let rawStr = String(data: data, encoding: .utf8) ?? data.hexEncodedString() + debugLog("[SideSign] GrandSlam HTTP \(statusCode): \(rawStr.prefix(200))") + // continue to parse if body might be a plist with Status.ec; if empty/HTML, fail below + } + guard !data.isEmpty else { debugLog("[SideSign] Auth endpoint returned 0 bytes (HTTP \(statusCode))") throw ServerError.badServerResponse(reason: "Auth endpoint returned empty response (0 bytes)", jsonPayload: "0 bytes") @@ -946,14 +961,14 @@ public extension DeveloperPortal { "Accept": "application/x-buddyml", "Accept-Language": "en-us", "Content-Type": "application/x-plist", - "User-Agent": h.developerServices.userAgent, + "User-Agent": (a.userAgent.isEmpty || a.userAgent.contains("Macintosh") || a.userAgent.contains("Xcode") ? "akd/1.0 CFNetwork/808.1.4" : a.userAgent), "X-Apple-App-Info": h.grandSlam.authApp, "X-Xcode-Version": context.xcodeVersion, "X-Apple-Identity-Token": encodedIdentityToken, "X-Apple-I-MD": a.oneTimePassword, "X-Apple-I-MD-M": a.machineID, "X-Mme-Device-Id": a.deviceID, - "X-MMe-Client-Info": a.clientInfo, + "X-Mme-Client-Info": a.clientInfo, "X-Apple-I-MD-LU": a.localUserID, "X-Apple-I-MD-RINFO": a.routingInfo, "X-Apple-I-SRL-NO": a.serialNumber, From 96d4f52cabda980cf693d4afef96e631c1362e9c Mon Sep 17 00:00:00 2001 From: Mr3242-Scripter Date: Tue, 20 Oct 2026 19:10:25 +0000 Subject: [PATCH 12/15] Update Authentication.swift Signed-off-by: Mr3242-Scripter --- Sources/DeveloperPortal/Authentication.swift | 69 +++++++++++++++----- 1 file changed, 54 insertions(+), 15 deletions(-) diff --git a/Sources/DeveloperPortal/Authentication.swift b/Sources/DeveloperPortal/Authentication.swift index e5b8aa9..72658da 100644 --- a/Sources/DeveloperPortal/Authentication.swift +++ b/Sources/DeveloperPortal/Authentication.swift @@ -732,9 +732,8 @@ public extension DeveloperPortal { verboseLog("[SideSign] sendTrustedDevice2FACodeRequest HTTP headers: \(prettyJSONString(from: sanitizeHeadersForLogging(allHeaders)))") } - let (data, response) = try await session.data(for: request) - let httpResponse = response as? HTTPURLResponse - let statusCode = httpResponse?.safeStatusCode ?? 0 + let (data, httpResponse) = try await performTwoFactorRequest(request, context: context, actionName: "sendTrustedDevice2FACodeRequest") + let statusCode = httpResponse.safeStatusCode try throwIfXMLUIErrorAlert(in: data, statusCode: statusCode, actionName: "sendTrustedDevice2FACodeRequest") guard statusCode == HTTPStatusCodes.ok else { @@ -774,9 +773,8 @@ public extension DeveloperPortal { verboseLog("[SideSign] sendPhone2FACodeRequest HTTP headers: \(prettyJSONString(from: sanitizeHeadersForLogging(allHeaders)))") } - let (data, response) = try await session.data(for: request) - let httpResponse = response as? HTTPURLResponse - let statusCode = httpResponse?.safeStatusCode ?? 0 + let (data, httpResponse) = try await performTwoFactorRequest(request, context: context, actionName: "sendPhone2FACodeRequest") + let statusCode = httpResponse.safeStatusCode let rawStr = prettyJSONString(from: data) verboseLog("[SideSign] sendPhone2FACodeRequest raw response (HTTP \(statusCode)): \(rawStr)") @@ -862,9 +860,8 @@ public extension DeveloperPortal { } debugLog("[SideSign] Verifying trusted device security code...") - let (verifyData, verifyResponse) = try await session.data(for: verifyRequest) - let verifyHttpResponse = verifyResponse as? HTTPURLResponse - let verifyStatusCode = verifyHttpResponse?.safeStatusCode ?? 0 + let (verifyData, verifyHttpResponse) = try await performTwoFactorRequest(verifyRequest, context: context, actionName: "validate2FACode") + let verifyStatusCode = verifyHttpResponse.safeStatusCode return try parseTwoFactorAuthVerifyResponse(data: verifyData, statusCode: verifyStatusCode, requirePeToken: false, httpResponse: verifyHttpResponse) } @@ -886,9 +883,8 @@ public extension DeveloperPortal { } debugLog("[SideSign] Verifying secondary security code...") - let (verifyData, verifyResponse) = try await session.data(for: verifyRequest) - let verifyHttpResponse = verifyResponse as? HTTPURLResponse - let verifyStatusCode = verifyHttpResponse?.safeStatusCode ?? 0 + let (verifyData, verifyHttpResponse) = try await performTwoFactorRequest(verifyRequest, context: context, actionName: "validate2FACode") + let verifyStatusCode = verifyHttpResponse.safeStatusCode return try parseTwoFactorAuthVerifyResponse(data: verifyData, statusCode: verifyStatusCode, requirePeToken: true, httpResponse: verifyHttpResponse) } @@ -950,25 +946,37 @@ public extension DeveloperPortal { return .success } - private func makeTwoFactorAuthRequest(url: URL, context: TwoFactorAuthContext) -> URLRequest { + /// SideInstaller MAC_CLIENT_INFO: gsa.apple.com/auth 2FA pages often return a bare 403 + /// for the Windows client used on GrandSlam; retry those requests as a Mac. + private static let macClientInfoFor2FA = + " " + + private func makeTwoFactorAuthRequest(url: URL, context: TwoFactorAuthContext, asMac: Bool = false) -> URLRequest { let identityToken = "\(context.dsid):\(context.idmsToken)" let encodedIdentityToken = Data(identityToken.utf8).base64EncodedString() var request = URLRequest(url: url) let a = context.anisetteData let h = customHeaders + var ua = a.userAgent.trimmingCharacters(in: .whitespacesAndNewlines) + if ua.isEmpty || ua.contains("Macintosh") || ua.contains("Xcode") || ua.contains("Mac OS") { + ua = "akd/1.0 CFNetwork/808.1.4" + } + let clientInfo = asMac ? Self.macClientInfoFor2FA : ( + a.clientInfo.isEmpty ? Self.macClientInfoFor2FA : a.clientInfo + ) let headers: [String: String] = [ "Accept": "application/x-buddyml", "Accept-Language": "en-us", "Content-Type": "application/x-plist", - "User-Agent": (a.userAgent.isEmpty || a.userAgent.contains("Macintosh") || a.userAgent.contains("Xcode") ? "akd/1.0 CFNetwork/808.1.4" : a.userAgent), + "User-Agent": ua, "X-Apple-App-Info": h.grandSlam.authApp, "X-Xcode-Version": context.xcodeVersion, "X-Apple-Identity-Token": encodedIdentityToken, "X-Apple-I-MD": a.oneTimePassword, "X-Apple-I-MD-M": a.machineID, "X-Mme-Device-Id": a.deviceID, - "X-Mme-Client-Info": a.clientInfo, + "X-Mme-Client-Info": clientInfo, "X-Apple-I-MD-LU": a.localUserID, "X-Apple-I-MD-RINFO": a.routingInfo, "X-Apple-I-SRL-NO": a.serialNumber, @@ -981,6 +989,37 @@ public extension DeveloperPortal { return request } + /// SideInstaller `send_auth`: on bare 403 for Windows client, retry as Mac. + private func performTwoFactorRequest(_ request: URLRequest, context: TwoFactorAuthContext, actionName: String) async throws -> (Data, HTTPURLResponse) { + var current = request + var triedMac = false + while true { + let (data, response) = try await session.data(for: current) + let http = response as? HTTPURLResponse + let status = http?.safeStatusCode ?? 0 + let body = String(data: data, encoding: .utf8) ?? "" + // Bare 403 (no Apple serviceErrors) = page refused the client identity + let bare403 = status == 403 && !body.localizedCaseInsensitiveContains("serviceErrors") + && !body.localizedCaseInsensitiveContains(""ec"") + if bare403 && !triedMac { + debugLog("[SideSign] \(actionName) HTTP 403 for Windows client — retrying as Mac (SideInstaller parity)") + guard let url = current.url else { + throw ServerError.badServerResponse(reason: "\(actionName) missing URL", jsonPayload: body) + } + var macRequest = makeTwoFactorAuthRequest(url: url, context: context, asMac: true) + macRequest.httpMethod = current.httpMethod + macRequest.httpBody = current.httpBody + current = macRequest + triedMac = true + continue + } + guard let http else { + throw ServerError.badServerResponse(reason: "\(actionName) non-HTTP response", jsonPayload: body) + } + return (data, http) + } + } + private func parsePlistOrJSON(_ data: Data) -> [String: any Sendable]? { (try? PropertyListSerialization.propertyList(from: data, format: nil)) as? [String: any Sendable] ?? (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: any Sendable] From d1e1c444ce89ddaa8847be82c65eff594c301415 Mon Sep 17 00:00:00 2001 From: Mr3242-Scripter Date: Wed, 21 Oct 2026 00:55:35 +0200 Subject: [PATCH 13/15] Update Authentication.swift Signed-off-by: Mr3242-Scripter --- Sources/DeveloperPortal/Authentication.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/DeveloperPortal/Authentication.swift b/Sources/DeveloperPortal/Authentication.swift index 72658da..b765bbe 100644 --- a/Sources/DeveloperPortal/Authentication.swift +++ b/Sources/DeveloperPortal/Authentication.swift @@ -999,8 +999,8 @@ public extension DeveloperPortal { let status = http?.safeStatusCode ?? 0 let body = String(data: data, encoding: .utf8) ?? "" // Bare 403 (no Apple serviceErrors) = page refused the client identity - let bare403 = status == 403 && !body.localizedCaseInsensitiveContains("serviceErrors") - && !body.localizedCaseInsensitiveContains(""ec"") + let hasServiceError = body.localizedCaseInsensitiveContains("serviceErrors") || body.localizedCaseInsensitiveContains("ec") + let bare403 = (status == 403) && !hasServiceError if bare403 && !triedMac { debugLog("[SideSign] \(actionName) HTTP 403 for Windows client — retrying as Mac (SideInstaller parity)") guard let url = current.url else { From fee235eca8c708caf264672189681b09240cf835 Mon Sep 17 00:00:00 2001 From: Mr3242-Scripter Date: Wed, 21 Oct 2026 01:21:21 +0200 Subject: [PATCH 14/15] Update Authentication.swift Signed-off-by: Mr3242-Scripter --- Sources/DeveloperPortal/Authentication.swift | 41 +++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/Sources/DeveloperPortal/Authentication.swift b/Sources/DeveloperPortal/Authentication.swift index b765bbe..c61289e 100644 --- a/Sources/DeveloperPortal/Authentication.swift +++ b/Sources/DeveloperPortal/Authentication.swift @@ -725,7 +725,7 @@ public extension DeveloperPortal { debugLog("[SideSign] Requesting trusted device 2FA code...") verboseLog("[SideSign] sendTrustedDevice2FACodeRequest for dsid: \(context.dsid)") - var request = makeTwoFactorAuthRequest(url: Constants.URLs.trustedDevice, context: context) + var request = makeTwoFactorAuthRequest(url: Constants.URLs.trustedDevice, context: context, asMac: true) request.httpMethod = "GET" if let allHeaders = request.allHTTPHeaderFields { @@ -758,16 +758,17 @@ public extension DeveloperPortal { return "1" }() - let serverInfo: [String: any Sendable] = [ - "mode": requestedMode, - "phoneNumber.id": sanitizedPhoneID + // SideInstaller: PUT JSON to https://gsa.apple.com/auth/verify/phone + let phoneURL = URL(string: "https://gsa.apple.com/auth/verify/phone")! + var request = makeTwoFactorAuthRequest(url: phoneURL, context: context, asMac: true) + request.httpMethod = "PUT" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("application/json", forHTTPHeaderField: "Accept") + let jsonBody: [String: Any] = [ + "phoneNumber": ["id": Int(sanitizedPhoneID) ?? sanitizedPhoneID], + "mode": requestedMode ] - - var request = makeTwoFactorAuthRequest(url: Constants.URLs.phonePutURL(mode: requestedMode), context: context) - request.httpMethod = "POST" - request.httpBody = try PropertyListSerialization.data(fromPropertyList: [ - "serverInfo": serverInfo - ], format: .xml, options: 0) + request.httpBody = try JSONSerialization.data(withJSONObject: jsonBody) if let allHeaders = request.allHTTPHeaderFields { verboseLog("[SideSign] sendPhone2FACodeRequest HTTP headers: \(prettyJSONString(from: sanitizeHeadersForLogging(allHeaders)))") @@ -852,7 +853,7 @@ public extension DeveloperPortal { } private func validateTrustedDevice2FACode(code: String, context: TwoFactorAuthContext) async throws -> TwoFactorAuthValidationResult { - var verifyRequest = makeTwoFactorAuthRequest(url: Constants.URLs.grandSlamValidate, context: context) + var verifyRequest = makeTwoFactorAuthRequest(url: Constants.URLs.grandSlamValidate, context: context, asMac: true) verifyRequest.setValue(code, forHTTPHeaderField: "security-code") if let allHeaders = verifyRequest.allHTTPHeaderFields { @@ -871,7 +872,7 @@ public extension DeveloperPortal { mode: String, context: TwoFactorAuthContext) async throws -> TwoFactorAuthValidationResult { - var verifyRequest = makeTwoFactorAuthRequest(url: Constants.URLs.phoneSecurityCode, context: context) + var verifyRequest = makeTwoFactorAuthRequest(url: Constants.URLs.phoneSecurityCode, context: context, asMac: true) verifyRequest.httpMethod = "POST" verifyRequest.httpBody = try PropertyListSerialization.data(fromPropertyList: [ "securityCode.code": code, @@ -989,7 +990,8 @@ public extension DeveloperPortal { return request } - /// SideInstaller `send_auth`: on bare 403 for Windows client, retry as Mac. + /// SideInstaller `send_auth`: gsa.apple.com/auth often refuses Windows with 403/5xx; + /// retry once as Mac (MAC_CLIENT_INFO). Do NOT treat generic "ec" substring as service error. private func performTwoFactorRequest(_ request: URLRequest, context: TwoFactorAuthContext, actionName: String) async throws -> (Data, HTTPURLResponse) { var current = request var triedMac = false @@ -998,11 +1000,12 @@ public extension DeveloperPortal { let http = response as? HTTPURLResponse let status = http?.safeStatusCode ?? 0 let body = String(data: data, encoding: .utf8) ?? "" - // Bare 403 (no Apple serviceErrors) = page refused the client identity - let hasServiceError = body.localizedCaseInsensitiveContains("serviceErrors") || body.localizedCaseInsensitiveContains("ec") - let bare403 = (status == 403) && !hasServiceError - if bare403 && !triedMac { - debugLog("[SideSign] \(actionName) HTTP 403 for Windows client — retrying as Mac (SideInstaller parity)") + + let shouldRetryAsMac = + !triedMac + && (status == 403 || status == 503 || status == 500 || status == 433 || status == 0) + if shouldRetryAsMac { + debugLog("[SideSign] \(actionName) HTTP \(status) — retrying as Mac (SideInstaller 2FA parity)") guard let url = current.url else { throw ServerError.badServerResponse(reason: "\(actionName) missing URL", jsonPayload: body) } @@ -1020,7 +1023,7 @@ public extension DeveloperPortal { } } - private func parsePlistOrJSON(_ data: Data) -> [String: any Sendable]? { +private func parsePlistOrJSON(_ data: Data) -> [String: any Sendable]? { (try? PropertyListSerialization.propertyList(from: data, format: nil)) as? [String: any Sendable] ?? (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: any Sendable] } From ac3df50c2e55a409b217eec7f14c8949f77365f1 Mon Sep 17 00:00:00 2001 From: Mr3242-Scripter Date: Wed, 21 Oct 2026 01:29:17 +0200 Subject: [PATCH 15/15] Update Authentication.swift Signed-off-by: Mr3242-Scripter --- Sources/DeveloperPortal/Authentication.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/DeveloperPortal/Authentication.swift b/Sources/DeveloperPortal/Authentication.swift index c61289e..680a1a4 100644 --- a/Sources/DeveloperPortal/Authentication.swift +++ b/Sources/DeveloperPortal/Authentication.swift @@ -764,8 +764,10 @@ public extension DeveloperPortal { request.httpMethod = "PUT" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("application/json", forHTTPHeaderField: "Accept") + // SideInstaller uses numeric phone id (u32); default 1 + let phoneIdValue: Int = Int(sanitizedPhoneID) ?? 1 let jsonBody: [String: Any] = [ - "phoneNumber": ["id": Int(sanitizedPhoneID) ?? sanitizedPhoneID], + "phoneNumber": ["id": phoneIdValue], "mode": requestedMode ] request.httpBody = try JSONSerialization.data(withJSONObject: jsonBody)