Skip to content

Commit 0e50a92

Browse files
Clear sign-in error when this Mac isn't set up for Apple, version 0.1.4
- "Anisette not provisioned" now explains what to do: sign in to an Apple Account (or the App Store) on this Mac once, then try again. - Sign-in identity always comes from this Mac. Public anisette servers are no longer offered: Apple rejects them (403 at the verification code, -80045 at login), so the Settings picker is removed. - The public anisette client no longer crashes or hangs on unexpected server responses. - New hidden `anisette-check` engine command for diagnostics.
1 parent 687e5ec commit 0e50a92

8 files changed

Lines changed: 99 additions & 47 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,5 @@ Data lives in `~/Library/Application Support/GatePass`.
4343
## Notes
4444

4545
- Free Apple IDs: apps last 7 days, max 3 active sideloaded apps, 10 new App IDs per week. These are Apple's limits.
46-
- Apple ID login uses Apple's private APIs (same as every sideloader). On macOS, anisette data is generated locally by the system's AOSKit, so Apple sees your real Mac; set `GATEPASS_REMOTE_ANISETTE=1` to use a public anisette server instead. Your password is only sent to Apple.
46+
- Apple ID login uses Apple's private APIs (same as every sideloader). On macOS, anisette data is generated locally by the system's AOSKit, so Apple sees your real Mac and no third-party server is involved. The Mac must be signed in to an Apple Account (or the App Store) at least once; otherwise sign-in fails with "Anisette not provisioned". Public anisette servers aren't offered because Apple currently rejects them. Your password is only sent to Apple.
4747
- Use it for apps you have the right to install.

‎app/Sources/GatePass/Engine.swift‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ enum Engine {
5555
process.arguments = args
5656
var env = ProcessInfo.processInfo.environment
5757
env["RUST_LOG"] = env["RUST_LOG"] ?? "info"
58-
// Default "local": sign-in identity comes from this Mac; no third-party anisette server.
59-
let anisette = UserDefaults.standard.string(forKey: Pref.anisette) ?? "local"
60-
env["GATEPASS_ANISETTE"] = ["local", "auto", "remote"].contains(anisette) ? anisette : "local"
58+
// Sign-in identity always comes from this Mac. Public anisette servers are rejected
59+
// by Apple (403 at 2FA, -80045 at login), so they are not offered.
60+
env["GATEPASS_ANISETTE"] = "local"
6161
if UserDefaults.standard.bool(forKey: Pref.keychain) { env["GATEPASS_KEYCHAIN"] = "1" }
6262
process.environment = env
6363

‎app/Sources/GatePass/Settings.swift‎

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ enum Pref {
66
static let autoRefresh = "autoRefresh" // Bool, default true
77
static let notifications = "notifications" // Bool, default true
88
static let devMode = "devMode" // Bool, default false
9-
static let anisette = "anisette" // "local" | "auto" | "remote"
109
static let didOnboard = "didOnboard" // Bool
1110
static let keychain = "useKeychain" // Bool, default false
1211

@@ -84,7 +83,6 @@ struct GeneralSettings: View {
8483

8584
struct AdvancedSettings: View {
8685
@AppStorage(Pref.devMode) private var devMode = false
87-
@AppStorage(Pref.anisette) private var anisette = "local"
8886

8987
var body: some View {
9088
Form {
@@ -95,16 +93,6 @@ struct AdvancedSettings: View {
9593
}
9694

9795
if devMode {
98-
Section("Sign-in identity (anisette)") {
99-
Picker("Source", selection: $anisette) {
100-
Text("Local — this Mac (recommended)").tag("local")
101-
Text("Automatic — local, then public server").tag("auto")
102-
Text("Public server").tag("remote")
103-
}
104-
Text("Local means Apple sees your real Mac and no data goes to a third party. Use a public server only if local doesn't work.")
105-
.font(.caption).foregroundStyle(.secondary)
106-
}
107-
10896
Section("Troubleshooting") {
10997
Button("Open Log (engine.log)") { NSWorkspace.shared.open(EngineLog.url) }
11098
Button("Open Data Folder") {

‎engine/Cargo.lock‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎engine/Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "gatepass-engine"
3-
version = "0.1.3"
3+
version = "0.1.4"
44
edition = "2024"
55
license = "MIT"
66
description = "Headless signing/installing engine for GatePass, built on Impactor's crates."

‎engine/src/main.rs‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ enum Command {
4242
},
4343
/// Show saved accounts
4444
Accounts,
45+
/// Diagnostics: obtain sign-in identity (anisette) and print which header names came back
46+
#[command(hide = true)]
47+
AnisetteCheck,
4548
/// Free-account limits: App IDs left this week, active apps per device
4649
Quota,
4750
/// Read name/bundle id/version/icon from an .ipa without installing
@@ -131,6 +134,19 @@ pub fn anisette() -> AnisetteConfiguration {
131134
AnisetteConfiguration::default().set_configuration_path(data_path())
132135
}
133136

137+
async fn anisette_check() -> Result<()> {
138+
let data = plume_core::auth::anisette_data::AnisetteData::new(anisette()).await?;
139+
let mut names: Vec<_> = data
140+
.generate_headers(false, true, true)
141+
.into_iter()
142+
.filter(|(_, v)| !v.is_empty())
143+
.map(|(k, _)| k)
144+
.collect();
145+
names.sort();
146+
emit(json!({ "type": "anisette", "headers": names }));
147+
Ok(())
148+
}
149+
134150
pub async fn load_store() -> Result<AccountStore> {
135151
Ok(AccountStore::load(&Some(store_path())).await?)
136152
}
@@ -149,6 +165,7 @@ async fn main() {
149165
Command::Login => login().await,
150166
Command::Logout { email } => logout(email).await,
151167
Command::Accounts => accounts().await,
168+
Command::AnisetteCheck => anisette_check().await,
152169
Command::Quota => quota().await,
153170
Command::IpaInfo { ipa, icon_out } => install::ipa_info(ipa, icon_out),
154171
Command::Install { ipa, udid, opts } => install::install(ipa, udid, opts).await,
@@ -188,6 +205,10 @@ fn friendly_error(e: &anyhow::Error) -> String {
188205
"Incorrect email or password."
189206
} else if lower.contains("-21669") || lower.contains("verification code") || lower.contains("-21667") {
190207
"Incorrect verification code. Try again."
208+
} else if lower.contains("anisette not provisioned") || lower.contains("anisette error: unsupported device") {
209+
"This Mac isn't set up for Apple sign-in yet. Open System Settings and sign in to your Apple Account (or sign in to the App Store once), then try again. GatePass doesn't work in a virtual machine."
210+
} else if lower.contains("-80045") {
211+
"Apple temporarily refused this sign-in. Wait 15–30 minutes and try again."
191212
} else if lower.contains("sign-in cancelled") {
192213
"Sign-in was cancelled."
193214
} else if lower.contains("not found") && lower.contains("device") {

‎upstream/impactor/3rdparty/omnisette/src/lib.rs‎

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,13 @@ pub enum AnisetteError {
6161

6262
pub const DEFAULT_ANISETTE_URL: &str = "https://ani.f1sh.me/";
6363

64-
pub const DEFAULT_ANISETTE_URL_V3: &str = "https://mac-ani.jkcoxson.com";
64+
pub const DEFAULT_ANISETTE_URL_V3: &str = "https://ani.sidestore.io";
65+
66+
/// [GatePass] Public v3 servers tried in order. ani.sidestore.io advertises macOS 13.1;
67+
/// mac-ani.jkcoxson.com advertises macOS 10.12, which Apple may reject during 2FA,
68+
/// so it is only the last resort.
69+
pub const FALLBACK_ANISETTE_URLS_V3: &[&str] =
70+
&["https://ani.sidestore.io", "https://mac-ani.jkcoxson.com"];
6571

6672
#[derive(Clone, Debug)]
6773
pub struct AnisetteConfiguration {
@@ -141,10 +147,11 @@ impl AnisetteHeaders {
141147
pub fn get_anisette_headers_provider(
142148
configuration: AnisetteConfiguration,
143149
) -> Result<AnisetteHeadersProviderRes, AnisetteError> {
144-
// [GatePass] On macOS prefer the machine's own identity; public servers advertise
145-
// macOS 10.12, which Apple now rejects with 403 during 2FA.
146-
// GATEPASS_ANISETTE: "local" (default: never contact third-party servers),
147-
// "auto" (local, falling back to remote) or "remote".
150+
// [GatePass] On macOS use the machine's own identity. Apple rejects identities
151+
// from public anisette servers (403 during 2FA, -80045 at login), so the default
152+
// never falls back to them.
153+
// GATEPASS_ANISETTE: "local" (default), "auto" (local, then public server) or
154+
// "remote" — the latter two only for debugging.
148155
#[cfg(target_os = "macos")]
149156
{
150157
let policy = std::env::var("GATEPASS_ANISETTE").unwrap_or_else(|_| "local".into());
@@ -163,7 +170,8 @@ impl AnisetteHeaders {
163170
}
164171
}
165172

166-
// TODO: handle Err because it will just go to remote anisette and not tell the user anything
173+
// StoreServicesCore needs Android libraries we don't ship on macOS.
174+
#[cfg(not(target_os = "macos"))]
167175
if let Ok(ssc_anisette_headers_provider) =
168176
AnisetteHeaders::get_ssc_anisette_headers_provider(configuration.clone())
169177
{
@@ -178,6 +186,9 @@ impl AnisetteHeaders {
178186
configuration.macos_serial.clone(),
179187
),
180188
)));
189+
190+
#[cfg(not(feature = "remote-anisette-v3"))]
191+
Err(AnisetteError::UnsupportedDevice)
181192
}
182193

183194
pub fn get_ssc_anisette_headers_provider(

‎upstream/impactor/3rdparty/omnisette/src/remote_anisette_v3.rs‎

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ impl AnisetteClient {
263263
if message.contains("-45061") {
264264
Err(AnisetteError::AnisetteNotProvisioned)
265265
} else {
266-
panic!("Unknown error {}", message)
266+
Err(anyhow::anyhow!("Anisette server error: {message}").into())
267267
}
268268
}
269269
AnisetteHeaders::Headers {
@@ -297,22 +297,22 @@ impl AnisetteClient {
297297
let protocol_val = plist::Value::from_reader(Cursor::new(text.as_str()))?;
298298
let urls = protocol_val
299299
.as_dictionary()
300-
.unwrap()
300+
.ok_or_else(|| AnisetteError::from(anyhow::anyhow!("Unexpected provisioning response from Apple")))?
301301
.get("urls")
302-
.unwrap()
302+
.ok_or_else(|| AnisetteError::from(anyhow::anyhow!("Unexpected provisioning response from Apple")))?
303303
.as_dictionary()
304-
.unwrap();
304+
.ok_or_else(|| AnisetteError::from(anyhow::anyhow!("Unexpected provisioning response from Apple")))?;
305305

306306
let start_provisioning_url = urls
307307
.get("midStartProvisioning")
308-
.unwrap()
308+
.ok_or_else(|| AnisetteError::from(anyhow::anyhow!("Unexpected provisioning response from Apple")))?
309309
.as_string()
310-
.unwrap();
310+
.ok_or_else(|| AnisetteError::from(anyhow::anyhow!("Unexpected provisioning response from Apple")))?;
311311
let end_provisioning_url = urls
312312
.get("midFinishProvisioning")
313-
.unwrap()
313+
.ok_or_else(|| AnisetteError::from(anyhow::anyhow!("Unexpected provisioning response from Apple")))?
314314
.as_string()
315-
.unwrap();
315+
.ok_or_else(|| AnisetteError::from(anyhow::anyhow!("Unexpected provisioning response from Apple")))?;
316316
debug!(
317317
"Got provisioning urls: {} and {}",
318318
start_provisioning_url, end_provisioning_url
@@ -338,11 +338,13 @@ impl AnisetteClient {
338338
}
339339

340340
loop {
341-
let Some(Ok(data)) = connection.next().await else {
342-
continue;
341+
let data = match connection.next().await {
342+
Some(Ok(data)) => data,
343+
Some(Err(e)) => return Err(e.into()),
344+
None => return Err(anyhow::anyhow!("Anisette server closed the connection").into()),
343345
};
344346
if data.is_text() {
345-
let txt = data.to_text().unwrap();
347+
let txt = data.to_text()?;
346348
let msg: ProvisionInput = serde_json::from_str(txt)?;
347349
match msg {
348350
ProvisionInput::GiveIdentifier => {
@@ -377,15 +379,15 @@ impl AnisetteClient {
377379
let protocol_val = plist::Value::from_reader(Cursor::new(text.as_str()))?;
378380
let spim = protocol_val
379381
.as_dictionary()
380-
.unwrap()
382+
.ok_or_else(|| AnisetteError::from(anyhow::anyhow!("Unexpected provisioning response from Apple")))?
381383
.get("Response")
382-
.unwrap()
384+
.ok_or_else(|| AnisetteError::from(anyhow::anyhow!("Unexpected provisioning response from Apple")))?
383385
.as_dictionary()
384-
.unwrap()
386+
.ok_or_else(|| AnisetteError::from(anyhow::anyhow!("Unexpected provisioning response from Apple")))?
385387
.get("spim")
386-
.unwrap()
388+
.ok_or_else(|| AnisetteError::from(anyhow::anyhow!("Unexpected provisioning response from Apple")))?
387389
.as_string()
388-
.unwrap();
390+
.ok_or_else(|| AnisetteError::from(anyhow::anyhow!("Unexpected provisioning response from Apple")))?;
389391

390392
debug!("GiveStartProvisioningData");
391393
#[derive(Serialize)]
@@ -419,11 +421,11 @@ impl AnisetteClient {
419421
let protocol_val = plist::Value::from_reader(Cursor::new(text.as_str()))?;
420422
let response = protocol_val
421423
.as_dictionary()
422-
.unwrap()
424+
.ok_or_else(|| AnisetteError::from(anyhow::anyhow!("Unexpected provisioning response from Apple")))?
423425
.get("Response")
424-
.unwrap()
426+
.ok_or_else(|| AnisetteError::from(anyhow::anyhow!("Unexpected provisioning response from Apple")))?
425427
.as_dictionary()
426-
.unwrap();
428+
.ok_or_else(|| AnisetteError::from(anyhow::anyhow!("Unexpected provisioning response from Apple")))?;
427429

428430
debug!("GiveEndProvisioningData");
429431

@@ -433,8 +435,14 @@ impl AnisetteClient {
433435
tk: &'t str,
434436
}
435437
let end_provisioning = EndProvisioning {
436-
ptm: response.get("ptm").unwrap().as_string().unwrap(),
437-
tk: response.get("tk").unwrap().as_string().unwrap(),
438+
ptm: response
439+
.get("ptm")
440+
.and_then(|v| v.as_string())
441+
.ok_or_else(|| AnisetteError::from(anyhow::anyhow!("Unexpected provisioning response from Apple")))?,
442+
tk: response
443+
.get("tk")
444+
.and_then(|v| v.as_string())
445+
.ok_or_else(|| AnisetteError::from(anyhow::anyhow!("Unexpected provisioning response from Apple")))?,
438446
};
439447
connection
440448
.send(Message::Text(
@@ -489,7 +497,31 @@ impl AnisetteHeadersProvider for RemoteAnisetteProviderV3 {
489497
_skip_provisioning: bool,
490498
) -> Result<HashMap<String, String>, AnisetteError> {
491499
if self.client.is_none() {
492-
self.client = Some(AnisetteClient::new(self.client_url.clone()).await?);
500+
// [GatePass] try the configured server, then the other known public servers.
501+
let mut urls = vec![self.client_url.clone()];
502+
urls.extend(
503+
crate::FALLBACK_ANISETTE_URLS_V3
504+
.iter()
505+
.map(|u| u.to_string())
506+
.filter(|u| *u != self.client_url),
507+
);
508+
let mut last_err = None;
509+
for url in urls {
510+
match AnisetteClient::new(url.clone()).await {
511+
Ok(c) => {
512+
log::info!("Using public anisette server {url}");
513+
self.client = Some(c);
514+
break;
515+
}
516+
Err(e) => {
517+
log::warn!("Anisette server {url} unavailable: {e}");
518+
last_err = Some(e);
519+
}
520+
}
521+
}
522+
if self.client.is_none() {
523+
return Err(last_err.unwrap_or(AnisetteError::Misc));
524+
}
493525
}
494526
let client = self.client.as_ref().unwrap();
495527

@@ -518,7 +550,7 @@ impl AnisetteHeadersProvider for RemoteAnisetteProviderV3 {
518550
plist::to_file_xml(config_path, state)?;
519551
client.get_headers(&state).await?
520552
} else {
521-
panic!()
553+
return Err(err);
522554
}
523555
}
524556
};

0 commit comments

Comments
 (0)