diff --git a/Cargo.lock b/Cargo.lock index c7a0bb1..bc9f3a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array 0.14.7", +] + [[package]] name = "aes" version = "0.8.4" @@ -19,6 +29,20 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -276,9 +300,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array 0.14.7", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "data-encoding" version = "2.10.0" @@ -474,6 +508,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "h2" version = "0.4.13" @@ -801,6 +845,7 @@ name = "isideload" version = "0.2.0" dependencies = [ "aes", + "aes-gcm", "async-trait", "base64", "cbc", @@ -920,6 +965,7 @@ name = "minimal" version = "0.1.0" dependencies = [ "isideload", + "plist-macro", "tokio", "tracing", "tracing-subscriber", @@ -1009,6 +1055,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl-probe" version = "0.2.1" @@ -1065,6 +1117,18 @@ dependencies = [ "plist", ] +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -1185,7 +1249,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", - "rand_core", + "rand_core 0.9.5", ] [[package]] @@ -1195,7 +1259,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", ] [[package]] @@ -1903,6 +1976,16 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" diff --git a/examples/minimal/Cargo.toml b/examples/minimal/Cargo.toml index a96136b..cdafa1f 100644 --- a/examples/minimal/Cargo.toml +++ b/examples/minimal/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] isideload = { path = "../../isideload" } +plist-macro = "0.1.3" tokio = { version = "1.49.0", features = ["rt-multi-thread", "macros"] } tracing = "0.1.44" tracing-subscriber = "0.3.22" diff --git a/examples/minimal/src/main.rs b/examples/minimal/src/main.rs index d36dc2f..7d5c170 100644 --- a/examples/minimal/src/main.rs +++ b/examples/minimal/src/main.rs @@ -3,6 +3,7 @@ use std::env; use isideload::{ anisette::remote_v3::RemoteV3AnisetteProvider, auth::apple_account::AppleAccountBuilder, }; +use plist_macro::pretty_print_dictionary; use tracing::Level; use tracing_subscriber::FmtSubscriber; @@ -36,8 +37,15 @@ async fn main() { .login(apple_password, get_2fa_code) .await; - match account { + match &account { Ok(a) => println!("Logged in. {}", a), Err(e) => eprintln!("Failed to log in to Apple ID: {:?}", e), } + + let app_token = account.unwrap().get_app_token("xcode.auth").await; + + match app_token { + Ok(t) => println!("App token: {}", pretty_print_dictionary(&t)), + Err(e) => eprintln!("Failed to get app token: {:?}", e), + } } diff --git a/isideload/Cargo.toml b/isideload/Cargo.toml index cd52436..b200d13 100644 --- a/isideload/Cargo.toml +++ b/isideload/Cargo.toml @@ -38,3 +38,4 @@ pbkdf2 = "0.12.2" hmac = "0.12.1" cbc = { version = "0.1.2", features = ["std"] } aes = "0.8.4" +aes-gcm = "0.10.3" diff --git a/isideload/src/anisette/remote_v3/mod.rs b/isideload/src/anisette/remote_v3/mod.rs index ecbe913..d292734 100644 --- a/isideload/src/anisette/remote_v3/mod.rs +++ b/isideload/src/anisette/remote_v3/mod.rs @@ -4,7 +4,7 @@ use std::fs; use std::path::PathBuf; use base64::prelude::*; -use chrono::{SubsecRound, Utc}; +// use chrono::{SubsecRound, Utc}; use plist_macro::plist; use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue}; use rootcause::prelude::*; diff --git a/isideload/src/auth/apple_account.rs b/isideload/src/auth/apple_account.rs index 8326158..09b6def 100644 --- a/isideload/src/auth/apple_account.rs +++ b/isideload/src/auth/apple_account.rs @@ -3,10 +3,14 @@ use crate::{ auth::grandslam::{GrandSlam, GrandSlamErrorChecker}, util::plist::PlistDataExtract, }; -use aes::cipher::block_padding::Pkcs7; +use aes::{ + Aes256, + cipher::{block_padding::Pkcs7, consts::U16}, +}; +use aes_gcm::{AeadInPlace, AesGcm, KeyInit, Nonce}; use base64::{Engine, prelude::BASE64_STANDARD}; use cbc::cipher::{BlockDecryptMut, KeyIvInit}; -use hmac::{Hmac, Mac}; +use hmac::Mac; use plist::Dictionary; use plist_macro::plist; use reqwest::header::{HeaderMap, HeaderValue}; @@ -563,12 +567,93 @@ impl AppleAccount { Ok(LoginState::LoggedIn) } - fn create_session_key(usr: &SrpClientVerifier, name: &str) -> Result, Report> { - Ok(Hmac::::new_from_slice(&usr.key())? - .chain_update(name.as_bytes()) + pub async fn get_app_token(&mut self, app: &str) -> Result { + let app = if app.contains("com.apple.gs.") { + app.to_string() + } else { + format!("com.apple.gs.{}", app) + }; + + let spd = self + .spd + .as_ref() + .ok_or_else(|| report!("SPD data not available, cannot get app token"))?; + + let dsid = spd.get_str("adsid").context("Failed to get app token")?; + let auth_token = spd + .get_str("GsIdmsToken") + .context("Failed to get app token")?; + let session_key = spd.get_data("sk").context("Failed to get app token")?; + let c = spd.get_data("c").context("Failed to get app token")?; + + let checksum = as hmac::Mac>::new_from_slice(session_key) + .unwrap() + .chain_update("apptokens".as_bytes()) + .chain_update(dsid.as_bytes()) + .chain_update(app.as_bytes()) .finalize() .into_bytes() - .to_vec()) + .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 request = plist!(dict { + "Header": { + "Version": "1.0.1" + }, + "Request": { + "app": [app], + "c": c, + "checksum": checksum, + "cpd": cpd, + "o": "apptokens", + "u": dsid, + "t": auth_token + } + }); + + let resp = self + .grandslam_client + .plist_request(&gs_service_url, &request, None) + .await + .context("Failed to send app token request")? + .check_grandslam_error() + .context("GrandSlam error during app token request")?; + + let encrypted_token = resp + .get_data("et") + .context("Failed to get encrypted token")?; + + let decrypted_token = Self::decrypt_gcm(&encrypted_token, &session_key) + .context("Failed to decrypt app token")?; + + let token: Dictionary = plist::from_bytes(&decrypted_token) + .context("Failed to parse decrypted app token plist")?; + + let status = token + .get_signed_integer("status-code") + .context("Failed to get status code from app token")?; + if status != 200 { + bail!("App token request failed with status code {}", status); + } + let token_dict = token + .get_dict("t") + .context("Failed to get token dictionary from app token")?; + + Ok(token_dict.clone()) + } + + fn create_session_key(usr: &SrpClientVerifier, name: &str) -> Result, Report> { + Ok( + as hmac::Mac>::new_from_slice(&usr.key())? + .chain_update(name.as_bytes()) + .finalize() + .into_bytes() + .to_vec(), + ) } fn decrypt_cbc(usr: &SrpClientVerifier, data: &[u8]) -> Result, Report> { @@ -581,6 +666,43 @@ impl AppleAccount { .decrypt_padded_vec_mut::(&data)?, ) } + + fn decrypt_gcm(data: &[u8], key: &[u8]) -> Result, Report> { + if data.len() < 3 + 16 + 16 { + bail!( + "Encrypted token is too short to be valid (only {} bytes)", + data.len() + ); + } + let header = &data[0..3]; + if header != b"XYZ" { + bail!( + "Encrypted token is in an unknown format: {}", + String::from_utf8_lossy(header) + ); + } + let iv = &data[3..19]; + let ciphertext_and_tag = &data[19..]; + + if key.len() != 32 { + bail!("Session key is not the correct length: {} bytes", key.len()); + } + if iv.len() != 16 { + bail!("IV is not the correct length: {} bytes", iv.len()); + } + + let key = aes_gcm::Key::>::from_slice(key); + let cipher = AesGcm::::new(key); + let nonce = Nonce::::from_slice(iv); + + let mut buf = ciphertext_and_tag.to_vec(); + + cipher + .decrypt_in_place(nonce, header, &mut buf) + .map_err(|e| report!("Failed to decrypt gcm: {}", e))?; + + Ok(buf) + } } impl std::fmt::Display for AppleAccount { diff --git a/isideload/src/auth/grandslam.rs b/isideload/src/auth/grandslam.rs index ee0f4cf..5a6edf6 100644 --- a/isideload/src/auth/grandslam.rs +++ b/isideload/src/auth/grandslam.rs @@ -8,6 +8,7 @@ use rootcause::prelude::*; use tracing::debug; use crate::{ + SideloadError, anisette::AnisetteClientInfo, util::plist::{PlistDataExtract, plist_to_xml_string}, }; @@ -166,24 +167,18 @@ impl GrandSlam { } pub trait GrandSlamErrorChecker { - fn check_grandslam_error(self) -> Result>; -} - -#[derive(Debug, thiserror::Error)] -pub enum GrandSlamError { - #[error("Auth error {0}: {1}")] - AuthWithMessage(i64, String), + fn check_grandslam_error(self) -> Result>; } impl GrandSlamErrorChecker for Dictionary { - fn check_grandslam_error(self) -> Result> { + fn check_grandslam_error(self) -> Result> { let result = match self.get("Status") { Some(plist::Value::Dictionary(d)) => d, _ => &self, }; if result.get_signed_integer("ec").unwrap_or(0) != 0 { - bail!(GrandSlamError::AuthWithMessage( + bail!(SideloadError::AuthWithMessage( result.get_signed_integer("ec").unwrap_or(-1), result.get_str("em").unwrap_or("Unknown error").to_string(), )) diff --git a/isideload/src/lib.rs b/isideload/src/lib.rs index 3253576..5a03840 100644 --- a/isideload/src/lib.rs +++ b/isideload/src/lib.rs @@ -7,6 +7,15 @@ pub mod anisette; pub mod auth; pub mod util; +#[derive(Debug, thiserror::Error)] +pub enum SideloadError { + #[error("Auth error {0}: {1}")] + AuthWithMessage(i64, String), + + #[error("Plist parse error: {0}")] + PlistParseError(String), +} + struct ReqwestErrorFormatter; impl ContextFormatterHook for ReqwestErrorFormatter { diff --git a/isideload/src/util/plist.rs b/isideload/src/util/plist.rs index b593125..35528cb 100644 --- a/isideload/src/util/plist.rs +++ b/isideload/src/util/plist.rs @@ -1,7 +1,8 @@ +use plist::Dictionary; use plist_macro::{plist_to_xml_bytes, plist_value_to_xml_bytes, pretty_print_dictionary}; use rootcause::prelude::*; -pub fn plist_to_xml_string(p: &plist::Dictionary) -> String { +pub fn plist_to_xml_string(p: &Dictionary) -> String { String::from_utf8(plist_to_xml_bytes(p)).unwrap() } @@ -9,24 +10,59 @@ pub fn plist_value_to_xml_string(p: &plist::Value) -> String { String::from_utf8(plist_value_to_xml_bytes(p)).unwrap() } +pub struct SensitivePlistAttachment { + pub plist: Dictionary, +} + +impl SensitivePlistAttachment { + pub fn new(plist: Dictionary) -> Self { + SensitivePlistAttachment { plist } + } + + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // if env variable DEBUG_SENSITIVE is set, print full plist + if std::env::var("DEBUG_SENSITIVE").is_ok() { + return writeln!(f, "{}", pretty_print_dictionary(&self.plist)); + } + writeln!( + f, + "" + ) + } +} + +impl std::fmt::Display for SensitivePlistAttachment { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.fmt(f) + } +} + +impl std::fmt::Debug for SensitivePlistAttachment { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.fmt(f) + } +} + pub trait PlistDataExtract { fn get_data(&self, key: &str) -> Result<&[u8], Report>; fn get_str(&self, key: &str) -> Result<&str, Report>; fn get_string(&self, key: &str) -> Result; fn get_signed_integer(&self, key: &str) -> Result; - fn get_dict(&self, key: &str) -> Result<&plist::Dictionary, Report>; + fn get_dict(&self, key: &str) -> Result<&Dictionary, Report>; } -impl PlistDataExtract for plist::Dictionary { +impl PlistDataExtract for Dictionary { fn get_data(&self, key: &str) -> Result<&[u8], Report> { self.get(key).and_then(|v| v.as_data()).ok_or_else(|| { - report!("Plist missing data for key '{}'", key).attach(pretty_print_dictionary(self)) + report!("Plist missing data for key '{}'", key) + .attach(SensitivePlistAttachment::new(self.clone())) }) } fn get_str(&self, key: &str) -> Result<&str, Report> { self.get(key).and_then(|v| v.as_string()).ok_or_else(|| { - report!("Plist missing string for key '{}'", key).attach(pretty_print_dictionary(self)) + report!("Plist missing string for key '{}'", key) + .attach(SensitivePlistAttachment::new(self.clone())) }) } @@ -36,7 +72,7 @@ impl PlistDataExtract for plist::Dictionary { .map(|s| s.to_string()) .ok_or_else(|| { report!("Plist missing string for key '{}'", key) - .attach(pretty_print_dictionary(self)) + .attach(SensitivePlistAttachment::new(self.clone())) }) } @@ -45,16 +81,16 @@ impl PlistDataExtract for plist::Dictionary { .and_then(|v| v.as_signed_integer()) .ok_or_else(|| { report!("Plist missing signed integer for key '{}'", key) - .attach(pretty_print_dictionary(self)) + .attach(SensitivePlistAttachment::new(self.clone())) }) } - fn get_dict(&self, key: &str) -> Result<&plist::Dictionary, Report> { + fn get_dict(&self, key: &str) -> Result<&Dictionary, Report> { self.get(key) .and_then(|v| v.as_dictionary()) .ok_or_else(|| { report!("Plist missing dictionary for key '{}'", key) - .attach(pretty_print_dictionary(self)) + .attach(SensitivePlistAttachment::new(self.clone())) }) } }