From aee5eaf26e8068a850ff2f21c3492d076ed47eb9 Mon Sep 17 00:00:00 2001 From: nab138 Date: Tue, 27 Jan 2026 23:00:30 -0500 Subject: [PATCH] Developer API implimentation --- Cargo.lock | 88 +----------------- examples/minimal/Cargo.toml | 1 + examples/minimal/src/main.rs | 27 ++++-- isideload/Cargo.toml | 3 +- isideload/src/anisette/mod.rs | 18 ++-- isideload/src/anisette/remote_v3/mod.rs | 5 +- isideload/src/auth/apple_account.rs | 115 +++++++----------------- isideload/src/auth/builder.rs | 73 +++++++++++++++ isideload/src/auth/grandslam.rs | 18 ++-- isideload/src/auth/mod.rs | 1 + isideload/src/dev/developer_session.rs | 99 ++++++++++++++++++-- isideload/src/dev/device_type.rs | 26 ++++++ isideload/src/dev/mod.rs | 1 + 13 files changed, 271 insertions(+), 204 deletions(-) create mode 100644 isideload/src/auth/builder.rs create mode 100644 isideload/src/dev/device_type.rs diff --git a/Cargo.lock b/Cargo.lock index bc9f3a5..b700466 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,15 +43,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "async-compression" version = "0.4.37" @@ -190,19 +181,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" -[[package]] -name = "chrono" -version = "0.4.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link", -] - [[package]] name = "cipher" version = "0.4.4" @@ -661,30 +639,6 @@ dependencies = [ "windows-registry", ] -[[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - [[package]] name = "icu_collections" version = "2.1.1" @@ -849,7 +803,6 @@ dependencies = [ "async-trait", "base64", "cbc", - "chrono", "futures-util", "hex", "hmac", @@ -965,6 +918,7 @@ name = "minimal" version = "0.1.0" dependencies = [ "isideload", + "plist", "plist-macro", "tokio", "tracing", @@ -2018,10 +1972,11 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" dependencies = [ + "getrandom 0.3.4", "js-sys", "wasm-bindgen", ] @@ -2187,41 +2142,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-link" version = "0.2.1" diff --git a/examples/minimal/Cargo.toml b/examples/minimal/Cargo.toml index cdafa1f..8b737d3 100644 --- a/examples/minimal/Cargo.toml +++ b/examples/minimal/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] isideload = { path = "../../isideload" } +plist = "1.8.0" plist-macro = "0.1.3" tokio = { version = "1.49.0", features = ["rt-multi-thread", "macros"] } tracing = "0.1.44" diff --git a/examples/minimal/src/main.rs b/examples/minimal/src/main.rs index c7d9ce1..aec33c1 100644 --- a/examples/minimal/src/main.rs +++ b/examples/minimal/src/main.rs @@ -1,16 +1,19 @@ use std::env; use isideload::{ - anisette::remote_v3::RemoteV3AnisetteProvider, auth::apple_account::AppleAccountBuilder, + anisette::remote_v3::RemoteV3AnisetteProvider, auth::apple_account::AppleAccount, + dev::developer_session::DeveloperSession, }; -use tracing::Level; + +use plist_macro::pretty_print_dictionary; +use tracing::{Level, debug}; use tracing_subscriber::FmtSubscriber; #[tokio::main] async fn main() { isideload::init().expect("Failed to initialize error reporting"); let subscriber = FmtSubscriber::builder() - .with_max_level(Level::DEBUG) + .with_max_level(Level::INFO) .finish(); tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); @@ -31,7 +34,7 @@ async fn main() { Some(code.trim().to_string()) }; - let account = AppleAccountBuilder::new(apple_id) + let account = AppleAccount::builder(apple_id) .anisette_provider(RemoteV3AnisetteProvider::default().set_serial_number("2".to_string())) .login(apple_password, get_2fa_code) .await; @@ -41,10 +44,16 @@ async fn main() { Err(e) => eprintln!("Failed to log in to Apple ID: {:?}", e), } - let app_token = account.unwrap().get_app_token("xcode.auth").await; + let mut account = account.unwrap(); - match app_token { - Ok(t) => println!("App token acquired"), - Err(e) => eprintln!("Failed to get app token: {:?}", e), - } + let dev_session = DeveloperSession::from_account(&mut account) + .await + .expect("Failed to create developer session"); + + let res = dev_session + .list_teams() + .await + .expect("Failed to list teams"); + + println!("{}", pretty_print_dictionary(&res)); } diff --git a/isideload/Cargo.toml b/isideload/Cargo.toml index b200d13..657d926 100644 --- a/isideload/Cargo.toml +++ b/isideload/Cargo.toml @@ -20,11 +20,10 @@ plist = "1.8" plist-macro = "0.1.3" reqwest = { version = "0.13.1", features = ["json", "gzip"] } thiserror = "2.0.17" -chrono = "0.4.43" async-trait = "0.1.89" serde = "1.0.228" rand = "0.9.2" -uuid = "1.19.0" +uuid = {version = "1.20.0", features = ["v4"] } sha2 = "0.10.9" tracing = "0.1.44" tokio-tungstenite = { version = "0.28.0", features = ["rustls-tls-webpki-roots"] } diff --git a/isideload/src/anisette/mod.rs b/isideload/src/anisette/mod.rs index 1894915..4514634 100644 --- a/isideload/src/anisette/mod.rs +++ b/isideload/src/anisette/mod.rs @@ -1,7 +1,6 @@ pub mod remote_v3; use crate::auth::grandslam::GrandSlam; -use chrono::{DateTime, SubsecRound, Utc}; use plist::Dictionary; use plist_macro::plist; use reqwest::header::HeaderMap; @@ -20,14 +19,15 @@ pub struct AnisetteData { machine_id: String, one_time_password: String, pub routing_info: String, - device_description: String, + _device_description: String, device_unique_identifier: String, - local_user_id: String, + _local_user_id: String, } +// Some headers don't seem to be required. I guess not including them is technically more efficient soooo impl AnisetteData { - pub fn get_headers(&self, serial: String) -> HashMap { - // let dt: DateTime = Utc::now().round_subsecs(0); + pub fn get_headers(&self) -> HashMap { + //let dt: DateTime = Utc::now().round_subsecs(0); HashMap::from_iter( [ @@ -55,8 +55,8 @@ impl AnisetteData { ) } - pub fn get_header_map(&self, serial: String) -> HeaderMap { - let headers_map = self.get_headers(serial); + pub fn get_header_map(&self) -> HeaderMap { + let headers_map = self.get_headers(); let mut header_map = HeaderMap::new(); for (key, value) in headers_map { @@ -69,8 +69,8 @@ impl AnisetteData { header_map } - pub fn get_client_provided_data(&self, serial: String) -> Dictionary { - let headers = self.get_headers(serial); + pub fn get_client_provided_data(&self) -> Dictionary { + let headers = self.get_headers(); let mut cpd = plist!(dict { "bootstrap": "true", diff --git a/isideload/src/anisette/remote_v3/mod.rs b/isideload/src/anisette/remote_v3/mod.rs index d292734..8a39140 100644 --- a/isideload/src/anisette/remote_v3/mod.rs +++ b/isideload/src/anisette/remote_v3/mod.rs @@ -4,7 +4,6 @@ use std::fs; use std::path::PathBuf; use base64::prelude::*; -// use chrono::{SubsecRound, Utc}; use plist_macro::plist; use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue}; use rootcause::prelude::*; @@ -110,9 +109,9 @@ impl AnisetteProvider for RemoteV3AnisetteProvider { machine_id, one_time_password, routing_info, - device_description: client_info, + _device_description: client_info, device_unique_identifier: state.get_device_id(), - local_user_id: hex::encode(&state.get_md_lu()), + _local_user_id: hex::encode(&state.get_md_lu()), }; Ok(data) diff --git a/isideload/src/auth/apple_account.rs b/isideload/src/auth/apple_account.rs index 09b6def..4f60d9f 100644 --- a/isideload/src/auth/apple_account.rs +++ b/isideload/src/auth/apple_account.rs @@ -1,6 +1,9 @@ use crate::{ - anisette::{AnisetteData, AnisetteProvider, remote_v3::RemoteV3AnisetteProvider}, - auth::grandslam::{GrandSlam, GrandSlamErrorChecker}, + anisette::{AnisetteData, AnisetteProvider}, + auth::{ + builder::AppleAccountBuilder, + grandslam::{GrandSlam, GrandSlamErrorChecker}, + }, util::plist::PlistDataExtract, }; use aes::{ @@ -22,8 +25,6 @@ use srp::{ }; use tracing::{debug, info, warn}; -const SERIAL_NUMBER: &str = "2"; - pub struct AppleAccount { pub email: String, pub spd: Option, @@ -34,12 +35,6 @@ pub struct AppleAccount { debug: bool, } -pub struct AppleAccountBuilder { - email: String, - debug: Option, - anisette_provider: Option>, -} - #[derive(Debug)] pub enum LoginState { LoggedIn, @@ -49,67 +44,6 @@ pub enum LoginState { NeedsLogin, } -impl AppleAccountBuilder { - /// Create a new AppleAccountBuilder with the given email - /// - /// # Arguments - /// - `email`: The Apple ID email address - pub fn new(email: &str) -> Self { - Self { - email: email.to_string(), - debug: None, - anisette_provider: None, - } - } - - /// DANGER Set whether to enable debug mode - /// - /// # Arguments - /// - `debug`: If true, accept invalid certificates and enable verbose connection logging - pub fn danger_debug(mut self, debug: bool) -> Self { - self.debug = Some(debug); - self - } - - pub fn anisette_provider(mut self, anisette_provider: impl AnisetteProvider + 'static) -> Self { - self.anisette_provider = Some(Box::new(anisette_provider)); - self - } - - /// Build the AppleAccount without logging in - /// - /// # Errors - /// Returns an error if the reqwest client cannot be built - pub async fn build(self) -> Result { - let debug = self.debug.unwrap_or(false); - let anisette_provider = self - .anisette_provider - .unwrap_or_else(|| Box::new(RemoteV3AnisetteProvider::default())); - - AppleAccount::new(&self.email, anisette_provider, debug).await - } - - /// Build the AppleAccount and log in - /// - /// # Arguments - /// - `password`: The Apple ID password - /// - `two_factor_callback`: A callback function that returns the two-factor authentication code - /// # Errors - /// Returns an error if the reqwest client cannot be built - pub async fn login( - self, - password: &str, - two_factor_callback: F, - ) -> Result - where - F: Fn() -> Option, - { - let mut account = self.build().await?; - account.login(password, two_factor_callback).await?; - Ok(account) - } -} - impl AppleAccount { /// Create a new AppleAccountBuilder with the given email /// @@ -395,7 +329,7 @@ impl AppleAccount { } async fn build_2fa_headers(&mut self) -> Result { - let mut headers = self.anisette_data.get_header_map(SERIAL_NUMBER.to_string()); + let mut headers = self.anisette_data.get_header_map(); let spd = self .spd @@ -427,9 +361,7 @@ impl AppleAccount { debug!("GrandSlam service URL: {}", gs_service_url); - let cpd = self - .anisette_data - .get_client_provided_data(SERIAL_NUMBER.to_string()); + let cpd = self.anisette_data.get_client_provided_data(); let srp_client = SrpClient::::new(&G_2048); let a: Vec = (0..32).map(|_| rand::random::()).collect(); @@ -567,7 +499,7 @@ impl AppleAccount { Ok(LoginState::LoggedIn) } - pub async fn get_app_token(&mut self, app: &str) -> Result { + pub async fn get_app_token(&mut self, app: &str) -> Result { let app = if app.contains("com.apple.gs.") { app.to_string() } else { @@ -596,16 +528,14 @@ impl AppleAccount { .to_vec(); let gs_service_url = self.grandslam_client.get_url("gsService").await?; - let cpd = self - .anisette_data - .get_client_provided_data(SERIAL_NUMBER.to_string()); + let cpd = self.anisette_data.get_client_provided_data(); let request = plist!(dict { "Header": { "Version": "1.0.1" }, "Request": { - "app": [app], + "app": [app.clone()], "c": c, "checksum": checksum, "cpd": cpd, @@ -642,8 +572,24 @@ impl AppleAccount { let token_dict = token .get_dict("t") .context("Failed to get token dictionary from app token")?; + let app_token = token_dict + .get_dict(&app) + .context("Failed to get app token string")?; - Ok(token_dict.clone()) + let app_token = AppToken { + token: app_token + .get_str("token") + .context("Failed to get app token string")? + .to_string(), + duration: app_token + .get_signed_integer("duration") + .context("Failed to get app token duration")? as u64, + expiry: app_token + .get_signed_integer("expiry") + .context("Failed to get app token expiry")? as u64, + }; + + Ok(app_token) } fn create_session_key(usr: &SrpClientVerifier, name: &str) -> Result, Report> { @@ -715,3 +661,10 @@ impl std::fmt::Display for AppleAccount { write!(f, "{} ({:?})", self.email, self.login_state) } } + +#[derive(Debug)] +pub struct AppToken { + pub token: String, + pub duration: u64, + pub expiry: u64, +} diff --git a/isideload/src/auth/builder.rs b/isideload/src/auth/builder.rs new file mode 100644 index 0000000..18bd03e --- /dev/null +++ b/isideload/src/auth/builder.rs @@ -0,0 +1,73 @@ +use rootcause::prelude::*; + +use crate::{ + anisette::{AnisetteProvider, remote_v3::RemoteV3AnisetteProvider}, + auth::apple_account::AppleAccount, +}; + +pub struct AppleAccountBuilder { + email: String, + debug: Option, + anisette_provider: Option>, +} + +impl AppleAccountBuilder { + /// Create a new AppleAccountBuilder with the given email + /// + /// # Arguments + /// - `email`: The Apple ID email address + pub fn new(email: &str) -> Self { + Self { + email: email.to_string(), + debug: None, + anisette_provider: None, + } + } + + /// DANGER Set whether to enable debug mode + /// + /// # Arguments + /// - `debug`: If true, accept invalid certificates and enable verbose connection logging + pub fn danger_debug(mut self, debug: bool) -> Self { + self.debug = Some(debug); + self + } + + pub fn anisette_provider(mut self, anisette_provider: impl AnisetteProvider + 'static) -> Self { + self.anisette_provider = Some(Box::new(anisette_provider)); + self + } + + /// Build the AppleAccount without logging in + /// + /// # Errors + /// Returns an error if the reqwest client cannot be built + pub async fn build(self) -> Result { + let debug = self.debug.unwrap_or(false); + let anisette_provider = self + .anisette_provider + .unwrap_or_else(|| Box::new(RemoteV3AnisetteProvider::default())); + + AppleAccount::new(&self.email, anisette_provider, debug).await + } + + /// Build the AppleAccount and log in + /// + /// # Arguments + /// - `password`: The Apple ID password + /// - `two_factor_callback`: A callback function that returns the two-factor authentication code + /// # Errors + /// Returns an error if the reqwest client cannot be built + pub async fn login( + self, + password: &str, + two_factor_callback: F, + ) -> Result + where + F: Fn() -> Option, + { + let mut account = self.build().await?; + account.login(password, two_factor_callback).await?; + Ok(account) + } +} diff --git a/isideload/src/auth/grandslam.rs b/isideload/src/auth/grandslam.rs index 5a6edf6..71d1800 100644 --- a/isideload/src/auth/grandslam.rs +++ b/isideload/src/auth/grandslam.rs @@ -134,15 +134,15 @@ impl GrandSlam { "X-Mme-Client-Info", HeaderValue::from_str(&self.client_info.client_info)?, ); - // headers.insert( - // "User-Agent", - // HeaderValue::from_str(&self.client_info.user_agent)?, - // ); - // headers.insert("X-Xcode-Version", HeaderValue::from_static("14.2 (14C18)")); - // headers.insert( - // "X-Apple-App-Info", - // HeaderValue::from_static("com.apple.gs.xcode.auth"), - // ); + headers.insert( + "User-Agent", + HeaderValue::from_str(&self.client_info.user_agent)?, + ); + headers.insert("X-Xcode-Version", HeaderValue::from_static("14.2 (14C18)")); + headers.insert( + "X-Apple-App-Info", + HeaderValue::from_static("com.apple.gs.xcode.auth"), + ); Ok(headers) } diff --git a/isideload/src/auth/mod.rs b/isideload/src/auth/mod.rs index 9aa7ce1..8ef46dc 100644 --- a/isideload/src/auth/mod.rs +++ b/isideload/src/auth/mod.rs @@ -1,2 +1,3 @@ pub mod apple_account; +pub mod builder; pub mod grandslam; diff --git a/isideload/src/dev/developer_session.rs b/isideload/src/dev/developer_session.rs index 4c27197..18e351b 100644 --- a/isideload/src/dev/developer_session.rs +++ b/isideload/src/dev/developer_session.rs @@ -1,13 +1,98 @@ -use std::sync::Arc; +use plist::Dictionary; +use plist_macro::plist; +use rootcause::prelude::*; +use uuid::Uuid; -use crate::auth::apple_account::AppleAccount; +use crate::{ + anisette::AnisetteData, + auth::{ + apple_account::{AppToken, AppleAccount}, + grandslam::GrandSlam, + }, + dev::device_type::DeveloperDeviceType, + util::plist::{PlistDataExtract, plist_to_xml_string}, +}; -struct DeveloperSession { - apple_account: Arc, +pub struct DeveloperSession<'a> { + token: AppToken, + adsid: String, + client: &'a GrandSlam, + anisette_data: &'a AnisetteData, } -impl DeveloperSession { - pub fn new(apple_account: Arc) -> Self { - DeveloperSession { apple_account } +impl<'a> DeveloperSession<'a> { + pub fn new( + token: AppToken, + adsid: String, + client: &'a GrandSlam, + anisette_data: &'a AnisetteData, + ) -> Self { + DeveloperSession { + token, + adsid, + client, + anisette_data, + } + } + + pub async fn from_account(account: &'a mut AppleAccount) -> Result { + let token = account + .get_app_token("xcode.auth") + .await + .context("Failed to get xcode token from Apple account")?; + + let spd = account + .spd + .as_ref() + .ok_or_else(|| report!("SPD not available, cannot get adsid"))?; + + Ok(DeveloperSession::new( + token, + spd.get_string("adsid")?, + &account.grandslam_client, + &account.anisette_data, + )) + } + + pub async fn send_developer_request( + &self, + url: &str, + body: Option, + ) -> Result { + let body = body.unwrap_or_else(|| Dictionary::new()); + + let base = plist!(dict { + "clientId": "XABBG36SBA", + "protocolVersion": "QH65B2", + "requestId": Uuid::new_v4().to_string().to_uppercase(), + "userLocale": ["en_US"], + }); + + let body = base.into_iter().chain(body.into_iter()).collect(); + + let text = self + .client + .post(url)? + .body(plist_to_xml_string(&body)) + .header("X-Apple-GS-Token", &self.token.token) + .header("X-Apple-I-Identity-Id", &self.adsid) + .headers(self.anisette_data.get_header_map()) + .send() + .await? + .error_for_status() + .context("Developer request failed")? + .text() + .await + .context("Failed to read developer request response text")?; + + let dict: Dictionary = plist::from_bytes(text.as_bytes()) + .context("Failed to parse developer request plist")?; + + Ok(dict) + } + + pub async fn list_teams(&self) -> Result { + self.send_developer_request(&DeveloperDeviceType::Any.dev_url("listTeams"), None) + .await } } diff --git a/isideload/src/dev/device_type.rs b/isideload/src/dev/device_type.rs new file mode 100644 index 0000000..bcc5fb5 --- /dev/null +++ b/isideload/src/dev/device_type.rs @@ -0,0 +1,26 @@ +#[derive(Debug, Clone)] +pub enum DeveloperDeviceType { + Any, + Ios, + Tvos, + Watchos, +} + +impl DeveloperDeviceType { + pub fn url_segment(&self) -> &'static str { + match self { + DeveloperDeviceType::Any => "", + DeveloperDeviceType::Ios => "ios/", + DeveloperDeviceType::Tvos => "tvos/", + DeveloperDeviceType::Watchos => "watchos/", + } + } + + pub fn dev_url(&self, endpoint: &str) -> String { + format!( + "https://developerservices2.apple.com/services/QH65B2/{}{}.action?clientId=XABBG36SBA", + self.url_segment(), + endpoint, + ) + } +} diff --git a/isideload/src/dev/mod.rs b/isideload/src/dev/mod.rs index 09da96f..dd3c3fb 100644 --- a/isideload/src/dev/mod.rs +++ b/isideload/src/dev/mod.rs @@ -1 +1,2 @@ pub mod developer_session; +pub mod device_type;