Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions Sources/Anisette/AnisetteDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<PC> <Windows;6.2(0,0);9200> <com.apple.AuthKit/1 (com.apple.iTunes/12.12.4.1)>"
let akdUserAgent = "akd/1.0 CFNetwork/808.1.4"
var clientInfo = headers?.clientInfo ?? windowsClientInfo
if clientInfo.isEmpty || clientInfo.contains("MacBook") || clientInfo.contains("<Android;") {
clientInfo = windowsClientInfo
}
var userAgent = headers?.userAgent ?? akdUserAgent
if userAgent.isEmpty || userAgent.contains("Macintosh") || userAgent.contains("Xcode") {
userAgent = akdUserAgent
}
let client = try await getClient(for: resolvedMode, clientInfo: clientInfo, userAgent: userAgent)

do {
let (rawHeaders, newBlob) = try await client.getAnisetteData(
Expand Down Expand Up @@ -470,14 +479,14 @@ public final class AnisetteDataManager: @unchecked Sendable {
}
}

private func getClient(for mode: AnisetteMode, clientInfo: String) async throws -> 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)
Expand All @@ -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 }
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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 }
Expand Down
4 changes: 2 additions & 2 deletions Sources/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
179 changes: 140 additions & 39 deletions Sources/DeveloperPortal/Authentication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -317,11 +317,38 @@ public extension DeveloperPortal {
request.httpMethod = "POST"
request.httpBody = plistData

// SideInstaller base_headers parity (Windows GSA / akd)
// Prefer anisette identity UA; never send Macintosh/Xcode (Apple 503s those).
let windowsClientInfo = "<PC> <Windows;6.2(0,0);9200> <com.apple.AuthKit/1 (com.apple.iTunes/12.12.4.1)>"
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("<Android;")
|| clientInfo.contains("Xcode") {
clientInfo = windowsClientInfo
}
var userAgent = anisetteData.userAgent.trimmingCharacters(in: .whitespacesAndNewlines)
if userAgent.isEmpty {
userAgent = h.grandSlam.userAgent.trimmingCharacters(in: .whitespacesAndNewlines)
}
if userAgent.isEmpty
|| userAgent.contains("Macintosh")
|| userAgent.contains("Mac OS")
|| userAgent.contains("Xcode")
|| userAgent.contains("AuthKit/1 (Macintosh") {
userAgent = akdUserAgent
}

let headers: [String: String] = [
"Content-Type": "text/x-xml-plist",
"X-MMe-Client-Info": anisetteData.clientInfo,
"Accept": "*/*",
"User-Agent": h.grandSlam.userAgent,
"Accept": "text/x-xml-plist",
"X-Mme-Client-Info": clientInfo,
"User-Agent": 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) }
Expand All @@ -330,16 +357,46 @@ public extension DeveloperPortal {
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
// SideInstaller: retry HTTP 429 / 503 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
let retryable = statusCode == HTTPStatusCodes.tooManyRequests || statusCode == 503
if retryable, attempt < retryDelays.count {
let delay = retryDelays[attempt]
debugLog("[SideSign] GrandSlam HTTP \(statusCode) — retrying in \(delay / 1_000_000_000)s (attempt \(attempt + 1))")
try await Task.sleep(nanoseconds: delay)
attempt += 1
continue
}
break
}

let httpResponse = response as? HTTPURLResponse
let statusCode = httpResponse?.statusCode ?? 0
// 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))")
Expand Down Expand Up @@ -668,16 +725,15 @@ 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 {
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 {
Expand All @@ -702,24 +758,26 @@ 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")
// SideInstaller uses numeric phone id (u32); default 1
let phoneIdValue: Int = Int(sanitizedPhoneID) ?? 1
let jsonBody: [String: Any] = [
"phoneNumber": ["id": phoneIdValue],
"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)))")
}

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)")
Expand Down Expand Up @@ -797,17 +855,16 @@ 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 {
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
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)
}
Expand All @@ -817,7 +874,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,
Expand All @@ -829,9 +886,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)
}
Expand Down Expand Up @@ -893,25 +949,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 =
"<Mac15,7> <macOS;27.0;26A5378j> <com.apple.AuthKit/1 (com.apple.akd/1.0)>"

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": h.developerServices.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,
Expand All @@ -924,7 +992,40 @@ public extension DeveloperPortal {
return request
}

private func parsePlistOrJSON(_ data: Data) -> [String: any Sendable]? {
/// 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
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) ?? ""

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)
}
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]
}
Expand Down
Loading