From 70ddb9319651ffc05cb9c3ec9d8fff4ef3635269 Mon Sep 17 00:00:00 2001 From: Matt Reeves Date: Thu, 15 Oct 2026 19:50:11 -0500 Subject: [PATCH 1/2] Updates AltSign to send GrandSlam requests with the anisette provider's identity Points AltSign to rileytestut/AltSign#61 (which includes #59): configurable authentication User-Agent, anisette data refresh and resync handlers, readable HTTP errors, and handling 2FA codes Apple sends on its own. --- Dependencies/AltSign | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dependencies/AltSign b/Dependencies/AltSign index 0d3c1a3cac..832d9cec33 160000 --- a/Dependencies/AltSign +++ b/Dependencies/AltSign @@ -1 +1 @@ -Subproject commit 0d3c1a3cac608e724ca0c8d7404253cd5fee9495 +Subproject commit 832d9cec339ea6ef6891011833dbb7227b028a29 From 22f766d2edb47626a082a1f775649bcf3fc43aff Mon Sep 17 00:00:00 2001 From: Matt Reeves Date: Thu, 15 Oct 2026 19:51:29 -0500 Subject: [PATCH 2/2] Signs in with Windows anisette data from remote anisette servers Apple now rejects anisette data generated with Android's CoreADI (-80045, then HTTP 503), and only accepts Windows anisette data (iTunes ADI) sent with that client's own client info and user agent. - Tries known Windows anisette servers first, and rejects servers whose client_info isn't Windows. - Sends the anisette server's user agent with GrandSlam requests. - Provisions a new anisette identity when the server's client info changes. - Resynchronizes anisette data when Apple asks to (hsc 434). Fixes https://github.com/altstoreio/AltStore/issues/1828 --- .../Operations/AuthenticationOperation.swift | 12 +++++++ .../FetchAnisetteDataOperation.swift | 31 ++++++++++++++----- AltStoreCore/Components/Keychain.swift | 3 ++ 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/AltStore/Operations/AuthenticationOperation.swift b/AltStore/Operations/AuthenticationOperation.swift index 18e54ade88..12aa72f906 100644 --- a/AltStore/Operations/AuthenticationOperation.swift +++ b/AltStore/Operations/AuthenticationOperation.swift @@ -438,8 +438,20 @@ private extension AuthenticationOperation verificationHandler = nil } + // Resynchronize anisette data when Apple says it's out of sync (hsc 434). + ALTAppleAPI.shared.anisetteDataResyncHandler = { (sim, completionHandler) in + let fetchAnisetteDataOperation = FetchAnisetteDataOperation(context: self.context) + fetchAnisetteDataOperation.sim = sim + fetchAnisetteDataOperation.resultHandler = { (result) in + completionHandler(result.value, result.error) + } + self.operationQueue.addOperation(fetchAnisetteDataOperation) + } + ALTAppleAPI.shared.authenticate(appleID: appleID, password: password, anisetteData: anisetteData, verificationHandler: verificationHandler) { (account, session, error) in + ALTAppleAPI.shared.anisetteDataResyncHandler = nil + if let account = account, let session = session { completionHandler(.success((account, session))) diff --git a/AltStore/Operations/FetchAnisetteDataOperation.swift b/AltStore/Operations/FetchAnisetteDataOperation.swift index ae6c09e34e..5ebe863e44 100644 --- a/AltStore/Operations/FetchAnisetteDataOperation.swift +++ b/AltStore/Operations/FetchAnisetteDataOperation.swift @@ -20,6 +20,10 @@ private extension URL static let appleGSALookup = URL(string: "https://gsa.apple.com/grandslam/GsService2/lookup")! } +// Apple only accepts anisette data generated with Windows' ADI (iTunes), so try servers that provide it first. +private let windowsAnisetteServers = [AnisetteServer(name: "jkcoxson", url: URL(string: "https://ani.jkcoxson.com")!), + AnisetteServer(name: "StikStore", url: URL(string: "https://ani2.stikstore.app")!)] + extension FetchAnisetteDataOperation { fileprivate struct AnisetteIdentity @@ -48,6 +52,9 @@ class FetchAnisetteDataOperation: ResultOperation, @unchecked S { let context: OperationContext + // X-Apple-I-MD-DATA Apple returned with hsc 434, used to resynchronize remote anisette data. + var sim: String? + private let session: URLSession init(context: OperationContext) @@ -184,9 +191,10 @@ private extension FetchAnisetteDataOperation func fetchAnisetteDataFromAvailableServer() async throws -> ALTAnisetteData { // Use preferred URL if it exists. - if let preferredURL = UserDefaults.shared.preferredAnisetteServerURL + if let preferredURL = UserDefaults.shared.preferredAnisetteServerURL, + let anisetteData = try? await self.fetchAnisetteData(from: preferredURL) { - return try await self.fetchAnisetteData(from: preferredURL) + return anisetteData } // No preferred URL — fetch the list of available servers. @@ -202,7 +210,7 @@ private extension FetchAnisetteDataOperation } // Shuffle and try each server. If successful, set preferred URL. - for server in servers.shuffled() + for server in windowsAnisetteServers + servers.shuffled() { do { @@ -236,10 +244,11 @@ private extension FetchAnisetteDataOperation decoder.keyDecodingStrategy = .convertFromSnakeCase let response: Response = try await self.send(URLRequest(url: clientInfoURL), decoder: decoder, anisetteServerURL: serverURL) + guard response.clientInfo.contains(" derives identifier, localUserID, deviceID) let identity: AnisetteIdentity - if let id = Keychain.shared.anisetteIdentityKey + if let id = Keychain.shared.anisetteIdentityKey, Keychain.shared.anisetteADIPB != nil, Keychain.shared.anisetteProvisioningClientInfo == response.clientInfo { identity = AnisetteIdentity(bytes: id) } @@ -251,7 +260,7 @@ private extension FetchAnisetteDataOperation // 3. Provision against this server if we don't have a cached adi.pb let adiPB: Data - if let pb = Keychain.shared.anisetteADIPB + if let pb = Keychain.shared.anisetteADIPB, Keychain.shared.anisetteProvisioningClientInfo == response.clientInfo { adiPB = pb } @@ -260,10 +269,12 @@ private extension FetchAnisetteDataOperation Logger.sideload.notice("No cached adi.pb; running anisette provisioning against \(serverURL.absoluteString, privacy: .public).") adiPB = try await self.provision(against: serverURL, clientInfo: response.clientInfo, userAgent: response.userAgent, identity: identity) Keychain.shared.anisetteADIPB = adiPB + Keychain.shared.anisetteProvisioningClientInfo = response.clientInfo } // 4. Fetch anisette headers - let (machineID, oneTimePassword, routingInfo) = try await self.fetchHeaders(from: serverURL, identity: identity, adiPB: adiPB) + let (machineID, oneTimePassword, routingInfo) = try await self.fetchHeaders(from: serverURL, identity: identity, adiPB: adiPB, sim: self.sim) + ALTAppleAPI.shared.authenticationUserAgent = response.userAgent // 5. Construct & return ALTAnisetteData return ALTAnisetteData( @@ -280,7 +291,7 @@ private extension FetchAnisetteDataOperation ) } - func fetchHeaders(from serverURL: URL, identity: AnisetteIdentity, adiPB: Data) async throws -> (machineID: String, oneTimePassword: String, routingInfo: UInt64) + func fetchHeaders(from serverURL: URL, identity: AnisetteIdentity, adiPB: Data, sim: String?) async throws -> (machineID: String, oneTimePassword: String, routingInfo: UInt64) { let headersURL = serverURL.appending(components: "v3", "get_headers") @@ -292,9 +303,13 @@ private extension FetchAnisetteDataOperation { var identifier: String var adi_pb: String + var sim: String? + var device_id: String? + var md_lu: String? } - let body = Body(identifier: identity.identifier, adi_pb: String(decoding: adiPB, as: UTF8.self)) + let body = Body(identifier: identity.identifier, adi_pb: String(decoding: adiPB, as: UTF8.self), + sim: sim, device_id: sim.map { _ in identity.deviceID }, md_lu: sim.map { _ in identity.localUserID }) request.httpBody = try JSONEncoder().encode(body) struct Response: Decodable diff --git a/AltStoreCore/Components/Keychain.swift b/AltStoreCore/Components/Keychain.swift index 851158c27d..f1b580665d 100644 --- a/AltStoreCore/Components/Keychain.swift +++ b/AltStoreCore/Components/Keychain.swift @@ -88,6 +88,9 @@ public class Keychain @KeychainItem(key: "anisetteADIPB", synchronizable: false) public var anisetteADIPB: Data? + @KeychainItem(key: "anisetteProvisioningClientInfo", synchronizable: false) + public var anisetteProvisioningClientInfo: String? + @KeychainItem(key: "devicePairingFile", synchronizable: false) public var devicePairingFile: Data?