diff --git a/icloud-auth/Cargo.toml b/icloud-auth/Cargo.toml index 24bc631..097a05d 100644 --- a/icloud-auth/Cargo.toml +++ b/icloud-auth/Cargo.toml @@ -6,25 +6,26 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -serde = { version = "1.0.147", features = ["derive"] } -serde_json = { version = "1.0.87" } -base64 = "0.13.1" +serde = { version = "1", features = ["derive"] } +serde_json = { version = "1" } +base64 = "0.22" srp = { version = "0.6.0", path = "./rustcrypto-srp" } -pbkdf2 = { version = "0.11.0" } -sha2 = { version = "0.10.6" } -rand = { version = "0.8.5" } -rustls = { version = "0.20.7" } -rustls-pemfile = { version = "1.0.1" } -plist = { version = "1.3.1" } +pbkdf2 = "0.11" +sha2 = "0.10" +rand = "0.9" +rustls = { version = "0.23", default-features = false, features = ["ring"] } +rustls-pemfile = "2.2" +plist = "1.7.2" hmac = "0.12.1" num-bigint = "0.4.3" cbc = { version = "0.1.2", features = ["std"] } aes = "0.8.2" -pkcs7 = "0.3.0" +pkcs7 = "0.4.1" reqwest = { version = "0.11.14", features = ["blocking", "json", "default-tls"] } -omnisette = {path = "../omnisette", features = ["remote-anisette-v3"]} -thiserror = "1.0.58" +omnisette = { path = "../omnisette", version = "0.1.3", features = ["remote-anisette-v3"] } +thiserror = "2" tokio = "1" +aes-gcm = "0.10.3" [dev-dependencies] tokio = { version = "1", features = ["rt", "macros"] } diff --git a/icloud-auth/rustcrypto-srp/Cargo.toml b/icloud-auth/rustcrypto-srp/Cargo.toml index fc898af..63f7808 100644 --- a/icloud-auth/rustcrypto-srp/Cargo.toml +++ b/icloud-auth/rustcrypto-srp/Cargo.toml @@ -14,15 +14,15 @@ rust-version = "1.56" [dependencies] num-bigint = "0.4" -generic-array = "0.14" +generic-array = "1" digest = "0.10" lazy_static = "1.2" subtle = "2.4" -base64 = "0.21.0" +base64 = "0.22" [dev-dependencies] -hex-literal = "0.3" +hex-literal = "1" num-traits = "0.2" -rand = "0.8" +rand = "0.9" sha1 = "0.10" sha2 = "0.10" diff --git a/icloud-auth/src/client.rs b/icloud-auth/src/client.rs index 52135fd..e5b431f 100644 --- a/icloud-auth/src/client.rs +++ b/icloud-auth/src/client.rs @@ -1,13 +1,16 @@ -use std::str::FromStr; - -// use crate::anisette::AnisetteData; use crate::{anisette::AnisetteData, Error}; -use aes::cipher::block_padding::Pkcs7; +use aes::{ + cipher::{block_padding::Pkcs7, consts::U16}, + Aes256, +}; +use aes_gcm::{aead::KeyInit, AeadInPlace, AesGcm, Nonce}; +use base64::{engine::general_purpose, Engine}; use cbc::cipher::{BlockDecryptMut, KeyIvInit}; -use hmac::{Hmac, Mac}; +use hmac::Mac; use omnisette::AnisetteConfiguration; use reqwest::{ - header::{HeaderMap, HeaderName, HeaderValue}, Certificate, Client, ClientBuilder, Proxy, Response + header::{HeaderMap, HeaderName, HeaderValue}, + Certificate, Client, ClientBuilder, Response, }; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; @@ -15,6 +18,7 @@ use srp::{ client::{SrpClient, SrpClientVerifier}, groups::G_2048, }; +use std::str::FromStr; use tokio::sync::Mutex; const GSA_ENDPOINT: &str = "https://gsa.apple.com/grandslam/GsService2"; @@ -91,10 +95,11 @@ pub struct AppleAccount { // pub spd: Option, //mutable spd pub spd: Option, + pub apple_id: String, client: Client, } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct AppToken { pub app_tokens: plist::Dictionary, pub auth_token: String, @@ -121,7 +126,7 @@ struct VerifyCode { #[derive(Serialize, Debug, Clone)] struct PhoneNumber { - id: u32 + id: u32, } #[derive(Serialize, Debug, Clone)] @@ -129,7 +134,7 @@ struct PhoneNumber { pub struct VerifyBody { phone_number: PhoneNumber, mode: String, - security_code: Option + security_code: Option, } #[repr(C)] @@ -139,7 +144,7 @@ pub struct TrustedPhoneNumber { pub number_with_dial_code: String, pub last_two_digits: String, pub push_mode: String, - pub id: u32 + pub id: u32, } #[derive(Deserialize)] @@ -156,19 +161,9 @@ pub struct AuthenticationExtras { pub new_state: Option, } -// impl Send2FAToDevices { -// pub fn send_2fa_to_devices(&self) -> LoginResponse { -// self.account.send_2fa_to_devices().unwrap() -// } -// } - -// impl Verify2FA { -// pub fn verify_2fa(&self, tfa_code: &str) -> LoginResponse { -// self.account.verify_2fa(&tfa_code).unwrap() -// } -// } - -async fn parse_response(res: Result) -> Result { +async fn parse_response( + res: Result, +) -> Result { let res = res?.text().await?; let res: plist::Dictionary = plist::from_bytes(res.as_bytes())?; let res: plist::Value = res.get("Response").unwrap().to_owned(); @@ -179,14 +174,22 @@ async fn parse_response(res: Result) -> Result Result { + pub async fn new( + config: AnisetteConfiguration, + apple_id: String, + ) -> Result { let anisette = AnisetteData::new(config).await?; - Ok(Self::new_with_anisette(anisette)?) + Self::new_with_anisette(anisette, apple_id) } - pub fn new_with_anisette(anisette: AnisetteData) -> Result { + pub fn new_with_anisette( + anisette: AnisetteData, + apple_id: String, + ) -> Result { let client = ClientBuilder::new() .add_root_certificate(Certificate::from_der(APPLE_ROOT)?) + // uncomment when debugging w/ charles proxy + // .danger_accept_invalid_certs(true) .http1_title_case_headers() .connection_verbose(true) .build()?; @@ -194,13 +197,14 @@ impl AppleAccount { Ok(AppleAccount { client, anisette: Mutex::new(anisette), + apple_id, spd: None, }) } pub async fn login( - appleid_closure: impl Fn() -> (String, String), - tfa_closure: impl Fn() -> String, + appleid_closure: impl Fn() -> Result<(String, String), String>, + tfa_closure: impl Fn() -> Result, config: AnisetteConfiguration, ) -> Result { let anisette = AnisetteData::new(config).await?; @@ -217,7 +221,6 @@ impl AppleAccount { pub async fn get_app_token(&self, app_name: &str) -> Result { let spd = self.spd.as_ref().unwrap(); - // println!("spd: {:#?}", spd); let dsid = spd.get("adsid").unwrap().as_string().unwrap(); let auth_token = spd.get("GsIdmsToken").unwrap().as_string().unwrap(); @@ -225,11 +228,6 @@ impl AppleAccount { let sk = spd.get("sk").unwrap().as_data().unwrap(); let c = spd.get("c").unwrap().as_data().unwrap(); - println!("adsid: {}", dsid); - println!("GsIdmsToken: {}", auth_token); - // println!("spd: {:#?}", spd); - println!("sk: {:#?}", sk); - println!("c: {:#?}", c); let checksum = Self::create_checksum(&sk.to_vec(), dsid, app_name); @@ -270,26 +268,81 @@ impl AppleAccount { plist::to_writer_xml(&mut buffer, &packet)?; let buffer = String::from_utf8(buffer).unwrap(); - println!("{:?}", gsa_headers.clone()); - println!("{:?}", buffer); - let res = self .client .post(GSA_ENDPOINT) .headers(gsa_headers.clone()) .body(buffer) - .send().await; + .send() + .await; let res = parse_response(res).await?; let err_check = Self::check_error(&res); if err_check.is_err() { return Err(err_check.err().unwrap()); } - println!("{:?}", res); - todo!() + + let encrypted_token = res + .get("et") + .ok_or(Error::Parse)? + .as_data() + .ok_or(Error::Parse)?; + + if encrypted_token.len() < 3 + 16 + 16 { + return Err(Error::Parse); + } + let header = &encrypted_token[0..3]; + if header != b"XYZ" { + return Err(Error::AuthSrpWithMessage( + 0, + "Encrypted token is in an unknown format.".to_string(), + )); + } + let iv = &encrypted_token[3..19]; + let ciphertext_and_tag = &encrypted_token[19..]; + + if sk.len() != 32 { + return Err(Error::Parse); + } + if iv.len() != 16 { + return Err(Error::Parse); + } + + let key = aes_gcm::Key::>::from_slice(sk); + 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(|_| { + Error::AuthSrpWithMessage( + 0, + "Failed to decrypt app token (AES-256/GCM aes-gcm).".to_string(), + ) + })?; + + let decrypted_token: plist::Dictionary = + plist::from_bytes(&buf).map_err(|_| Error::Parse)?; + + let t_val = decrypted_token.get("t").ok_or(Error::Parse)?; + let app_tokens = t_val.as_dictionary().ok_or(Error::Parse)?; + let app_token_dict = app_tokens.get(app_name).ok_or(Error::Parse)?; + let app_token = app_token_dict.as_dictionary().ok_or(Error::Parse)?; + let token = app_token + .get("token") + .and_then(|v| v.as_string()) + .ok_or(Error::Parse)?; + + Ok(AppToken { + app_tokens: app_tokens.clone(), + auth_token: token.to_string(), + app: app_name.to_string(), + }) } fn create_checksum(session_key: &Vec, dsid: &str, app_name: &str) -> Vec { - Hmac::::new_from_slice(&session_key) + as hmac::Mac>::new_from_slice(session_key.as_slice()) .unwrap() .chain_update("apptokens".as_bytes()) .chain_update(dsid.as_bytes()) @@ -312,32 +365,50 @@ impl AppleAccount { /// /// let anisette = AnisetteData::new(); /// let account = AppleAccount::login( - /// || ("test@waffle.me", "password") - /// || "123123", + /// || Ok(("test@waffle.me", "password")) + /// || Ok("123123"), /// anisette /// ); /// ``` /// Note: You would not provide the 2FA code like this, you would have to actually ask input for it. //TODO: add login_with_anisette and login, where login autodetcts anisette - pub async fn login_with_anisette (String, String), G: Fn() -> String>( + pub async fn login_with_anisette< + F: Fn() -> Result<(String, String), String>, + G: Fn() -> Result, + >( appleid_closure: F, tfa_closure: G, anisette: AnisetteData, ) -> Result { - let mut _self = AppleAccount::new_with_anisette(anisette)?; - let (username, password) = appleid_closure(); + let (username, password) = appleid_closure().map_err(|e| { + Error::AuthSrpWithMessage(0, format!("Failed to get Apple ID credentials: {}", e)) + })?; + let mut _self = AppleAccount::new_with_anisette(anisette, username.clone())?; + let mut response = _self.login_email_pass(&username, &password).await?; loop { match response { LoginState::NeedsDevice2FA => response = _self.send_2fa_to_devices().await?, LoginState::Needs2FAVerification => { - response = _self.verify_2fa(tfa_closure()).await? - } - LoginState::NeedsSMS2FA => { - response = _self.send_sms_2fa_to_devices(1).await? + response = _self + .verify_2fa(tfa_closure().map_err(|e| { + Error::AuthSrpWithMessage(0, format!("Failed to get 2FA code: {}", e)) + })?) + .await? } + LoginState::NeedsSMS2FA => response = _self.send_sms_2fa_to_devices(1).await?, LoginState::NeedsSMS2FAVerification(body) => { - response = _self.verify_sms_2fa(tfa_closure(), body).await? + response = _self + .verify_sms_2fa( + tfa_closure().map_err(|e| { + Error::AuthSrpWithMessage( + 0, + format!("Failed to get SMS 2FA code: {}", e), + ) + })?, + body, + ) + .await? } LoginState::NeedsLogin => { response = _self.login_email_pass(&username, &password).await? @@ -345,9 +416,9 @@ impl AppleAccount { LoginState::LoggedIn => return Ok(_self), LoginState::NeedsExtraStep(step) => { if _self.get_pet().is_some() { - return Ok(_self) + return Ok(_self); } else { - return Err(Error::ExtraStep(step)) + return Err(Error::ExtraStep(step)); } } } @@ -355,17 +426,40 @@ impl AppleAccount { } pub fn get_pet(&self) -> Option { - let Some(token) = self.spd.as_ref().unwrap().get("t") else { - return None - }; - Some(token.as_dictionary().unwrap().get("com.apple.gs.idms.pet") - .unwrap().as_dictionary().unwrap().get("token").unwrap().as_string().unwrap().to_string()) + Some( + self.spd.as_ref().unwrap().get("t")? + .as_dictionary() + .unwrap() + .get("com.apple.gs.idms.pet") + .unwrap() + .as_dictionary() + .unwrap() + .get("token") + .unwrap() + .as_string() + .unwrap() + .to_string(), + ) } pub fn get_name(&self) -> (String, String) { ( - self.spd.as_ref().unwrap().get("fn").unwrap().as_string().unwrap().to_string(), - self.spd.as_ref().unwrap().get("ln").unwrap().as_string().unwrap().to_string() + self.spd + .as_ref() + .unwrap() + .get("fn") + .unwrap() + .as_string() + .unwrap() + .to_string(), + self.spd + .as_ref() + .unwrap() + .get("ln") + .unwrap() + .as_string() + .unwrap() + .to_string(), ) } @@ -423,7 +517,8 @@ impl AppleAccount { .post(GSA_ENDPOINT) .headers(gsa_headers.clone()) .body(buffer) - .send().await; + .send() + .await; let res = parse_response(res).await?; let err_check = Self::check_error(&res); @@ -436,9 +531,7 @@ impl AppleAccount { let iters = res.get("i").unwrap().as_signed_integer().unwrap(); let c = res.get("c").unwrap().as_string().unwrap(); - let mut password_hasher = sha2::Sha256::new(); - password_hasher.update(&password.as_bytes()); - let hashed_password = password_hasher.finalize(); + let hashed_password = Sha256::digest(password.as_bytes()); let mut password_buf = [0u8; 32]; pbkdf2::pbkdf2::>( @@ -449,7 +542,7 @@ impl AppleAccount { ); let verifier: SrpClientVerifier = srp_client - .process_reply(&a, &username.as_bytes(), &password_buf, salt, b_pub) + .process_reply(&a, username.as_bytes(), &password_buf, salt, b_pub) .unwrap(); let m = verifier.proof(); @@ -476,7 +569,8 @@ impl AppleAccount { .post(GSA_ENDPOINT) .headers(gsa_headers.clone()) .body(buffer) - .send().await; + .send() + .await; let res = parse_response(res).await?; let err_check = Self::check_error(&res); @@ -485,7 +579,7 @@ impl AppleAccount { } // println!("{:?}", res); let m2 = res.get("M2").unwrap().as_data().unwrap(); - verifier.verify_server(&m2).unwrap(); + verifier.verify_server(m2).unwrap(); let spd = res.get("spd").unwrap().as_data().unwrap(); let decrypted_spd = Self::decrypt_cbc(&verifier, spd); @@ -499,15 +593,15 @@ impl AppleAccount { return match s.as_str() { "trustedDeviceSecondaryAuth" => Ok(LoginState::NeedsDevice2FA), "secondaryAuth" => Ok(LoginState::NeedsSMS2FA), - _unk => Ok(LoginState::NeedsExtraStep(_unk.to_string())) - } + _unk => Ok(LoginState::NeedsExtraStep(_unk.to_string())), + }; } Ok(LoginState::LoggedIn) } fn create_session_key(usr: &SrpClientVerifier, name: &str) -> Vec { - Hmac::::new_from_slice(&usr.key()) + as hmac::Mac>::new_from_slice(usr.key()) .unwrap() .chain_update(name.as_bytes()) .finalize() @@ -522,7 +616,7 @@ impl AppleAccount { cbc::Decryptor::::new_from_slices(&extra_data_key, extra_data_iv) .unwrap() - .decrypt_padded_vec_mut::(&data) + .decrypt_padded_vec_mut::(data) .unwrap() } @@ -533,58 +627,58 @@ impl AppleAccount { .client .get("https://gsa.apple.com/auth/verify/trusteddevice") .headers(headers.await) - .send().await?; + .send() + .await?; if !res.status().is_success() { return Err(Error::AuthSrp); } - return Ok(LoginState::Needs2FAVerification); + Ok(LoginState::Needs2FAVerification) } pub async fn send_sms_2fa_to_devices(&self, phone_id: u32) -> Result { let headers = self.build_2fa_headers(true); - let body = VerifyBody { - phone_number: PhoneNumber { - id: phone_id - }, + phone_number: PhoneNumber { id: phone_id }, mode: "sms".to_string(), - security_code: None + security_code: None, }; let res = self .client - .put("https://gsa.apple.com/auth/verify/phone/") + .get("https://gsa.apple.com/auth") .headers(headers.await) - .json(&body) - .send().await?; + .send() + .await?; if !res.status().is_success() { return Err(Error::AuthSrp); } - return Ok(LoginState::NeedsSMS2FAVerification(body)); + Ok(LoginState::NeedsSMS2FAVerification(body)) } pub async fn get_auth_extras(&self) -> Result { let headers = self.build_2fa_headers(true); - let req = self.client + let req = self + .client .get("https://gsa.apple.com/auth") .headers(headers.await) .header("Accept", "application/json") - .send().await?; + .send() + .await?; let status = req.status().as_u16(); let mut new_state = req.json::().await?; if status == 201 { new_state.new_state = Some(LoginState::NeedsSMS2FAVerification(VerifyBody { phone_number: PhoneNumber { - id: new_state.trusted_phone_numbers.first().unwrap().id + id: new_state.trusted_phone_numbers.first().unwrap().id, }, mode: "sms".to_string(), - security_code: None + security_code: None, })); } @@ -592,6 +686,7 @@ impl AppleAccount { } pub async fn verify_2fa(&self, code: String) -> Result { + // println!("Verifying 2fa Code {}", code.clone()); let headers = self.build_2fa_headers(false); // println!("Recieved code: {}", code); let res = self @@ -602,17 +697,21 @@ impl AppleAccount { HeaderName::from_str("security-code").unwrap(), HeaderValue::from_str(&code).unwrap(), ) - .send().await?; + .send() + .await?; - let res: plist::Dictionary = - plist::from_bytes(res.text().await?.as_bytes())?; + let res: plist::Dictionary = plist::from_bytes(res.text().await?.as_bytes())?; Self::check_error(&res)?; Ok(LoginState::NeedsLogin) } - pub async fn verify_sms_2fa(&self, code: String, mut body: VerifyBody) -> Result { + pub async fn verify_sms_2fa( + &self, + code: String, + mut body: VerifyBody, + ) -> Result { let headers = self.build_2fa_headers(true).await; // println!("Recieved code: {}", code); @@ -624,7 +723,8 @@ impl AppleAccount { .headers(headers) .header("accept", "application/json") .json(&body) - .send().await?; + .send() + .await?; if res.status() != 200 { return Err(Error::Bad2faCode); @@ -636,7 +736,7 @@ impl AppleAccount { fn check_error(res: &plist::Dictionary) -> Result<(), Error> { let res = match res.get("Status") { Some(plist::Value::Dictionary(d)) => d, - _ => &res, + _ => res, }; if res.get("ec").unwrap().as_signed_integer().unwrap() != 0 { @@ -654,7 +754,7 @@ impl AppleAccount { let dsid = spd.get("adsid").unwrap().as_string().unwrap(); let token = spd.get("GsIdmsToken").unwrap().as_string().unwrap(); - let identity_token = base64::encode(format!("{}:{}", dsid, token)); + let identity_token = general_purpose::STANDARD.encode(format!("{}:{}", dsid, token)); let valid_anisette = self.get_anisette().await; @@ -690,4 +790,57 @@ impl AppleAccount { headers } + + pub async fn send_request( + &self, + url: &str, + body: Option, + ) -> Result { + let spd = self.spd.as_ref().unwrap(); + let app_token = self.get_app_token("com.apple.gs.xcode.auth").await?; + let valid_anisette = self.get_anisette().await; + + let mut headers = HeaderMap::new(); + headers.insert("Content-Type", HeaderValue::from_static("text/x-xml-plist")); + headers.insert("Accept", HeaderValue::from_static("text/x-xml-plist")); + headers.insert("Accept-Language", HeaderValue::from_static("en-us")); + headers.insert("User-Agent", HeaderValue::from_static("Xcode")); + headers.insert( + "X-Apple-I-Identity-Id", + HeaderValue::from_str(spd.get("adsid").unwrap().as_string().unwrap()).unwrap(), + ); + headers.insert( + "X-Apple-GS-Token", + HeaderValue::from_str(&app_token.auth_token).unwrap(), + ); + + for (k, v) in valid_anisette.generate_headers(false, true, true) { + headers.insert( + HeaderName::from_bytes(k.as_bytes()).unwrap(), + HeaderValue::from_str(&v).unwrap(), + ); + } + + if let Ok(locale) = valid_anisette.get_header("x-apple-locale") { + headers.insert("X-Apple-Locale", HeaderValue::from_str(&locale).unwrap()); + } + + let response = if let Some(body) = body { + let mut buf = Vec::new(); + plist::to_writer_xml(&mut buf, &body)?; + self.client + .post(url) + .headers(headers) + .body(buf) + .send() + .await? + } else { + self.client.get(url).headers(headers).send().await? + }; + + let response = response.text().await?; + + let response: plist::Dictionary = plist::from_bytes(response.as_bytes())?; + Ok(response) + } } diff --git a/icloud-auth/tests/auth_debug.rs b/icloud-auth/tests/auth_debug.rs index c68c392..aeb3b1c 100644 --- a/icloud-auth/tests/auth_debug.rs +++ b/icloud-auth/tests/auth_debug.rs @@ -1,8 +1,5 @@ -// use icloud_auth::ani -use std::sync::Arc; - +use base64::engine::{general_purpose, Engine}; use num_bigint::BigUint; -use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use srp::{ client::{SrpClient, SrpClientVerifier}, @@ -11,15 +8,18 @@ use srp::{ #[cfg(test)] mod tests { + use super::*; #[test] fn auth_debug() { // not a real account - let bytes_a = base64::decode("XChHXELsQ+ljxTFbvRMUsGJxiDIlOh9f8e+JzoegmVcOdAXXtPNzkHpAbAgSjyA+vXrTA93+BUu8EJ9+4xZu9g==").unwrap(); + let bytes_a = general_purpose::STANDARD.decode("XChHXELsQ+ljxTFbvRMUsGJxiDIlOh9f8e+JzoegmVcOdAXXtPNzkHpAbAgSjyA+vXrTA93+BUu8EJ9+4xZu9g==").unwrap(); let username = "apple3@f1sh.me"; let password = "WaffleTest123"; - let salt = base64::decode("6fK6ailLUcp2kJswJVrKjQ==").unwrap(); + let salt = general_purpose::STANDARD + .decode("6fK6ailLUcp2kJswJVrKjQ==") + .unwrap(); let iters = 20832; let mut password_hasher = sha2::Sha256::new(); @@ -41,7 +41,9 @@ mod tests { // apub: N2XHuh/4P1urPoBvDocF0RCRIl2pliZYqg9p6wGH0nnJdckJPn3M00jEqoM4teqH03HjG1murdcZiNHb5YayufW//+asW01XB7nYIIVvGiUFLRypYITEKYWBQ6h2q02GaZspYJKy98V8Fwcvr0ri+al7zJo1X1aoRKINyjV5TywhhwmTleI1qJkf+JBRYKKqO1XFtOTpQsysWD3ZJdK3K78kSgT3q0kXE3oDRMiHPAO77GFJZErYTuvI6QPRbOgcrn+RKV6AsjR5tUQAoSGRdtibdZTAQijJg788qVg+OFVCNZoY9GYVxa+Ze1bPGdkkgCYicTE8iNFG9KlJ+QpKgQ== - let a_random = base64::decode("ywN1O32vmBogb5Fyt9M7Tn8bbzLtDDbcYgPFpSy8n9E=").unwrap(); + let a_random = general_purpose::STANDARD + .decode("ywN1O32vmBogb5Fyt9M7Tn8bbzLtDDbcYgPFpSy8n9E=") + .unwrap(); let client = SrpClient::::new(&G_2048); let a_pub_compute = @@ -49,14 +51,21 @@ mod tests { // expect it to be same to a_pub println!( "compute a_pub: {:?}", - base64::encode(&a_pub_compute.to_bytes_be()) + general_purpose::STANDARD.encode(&a_pub_compute.to_bytes_be()) ); - let b_pub = base64::decode("HlWxsRmNi/9DCGxYCoqCTfdSvpbx3mrgFLQfOsgf3Rojn7MQQN/g63PwlBghUcVVB4//yAaRRnz/VIByl8thA9AKuVZl8k52PAHKSh4e7TuXSeYCFr0+GYu8/hFdMDl42219uzSuOXuaKGVKq6hxEAf3n3uXXgQRkXWtLFJ5nn1wq/emf46hYAHzc/pYyvckAdh9WDCw95IXbzKD8LcPw/0ZQoydMuXgW2ZKZ52fiyEs94IZ7L5RLL7jY1nVdwtsp2fxeqiZ3DNmVZ2GdNrbJGT//160tyd2evtUtehr8ygXNzjWdjV0cc4+1F38ywSPFyieVzVTYzDywRllgo3A5A==").unwrap(); - println!("fixed b_pub: {:?}", base64::encode(&b_pub)); + let b_pub = general_purpose::STANDARD.decode("HlWxsRmNi/9DCGxYCoqCTfdSvpbx3mrgFLQfOsgf3Rojn7MQQN/g63PwlBghUcVVB4//yAaRRnz/VIByl8thA9AKuVZl8k52PAHKSh4e7TuXSeYCFr0+GYu8/hFdMDl42219uzSuOXuaKGVKq6hxEAf3n3uXXgQRkXWtLFJ5nn1wq/emf46hYAHzc/pYyvckAdh9WDCw95IXbzKD8LcPw/0ZQoydMuXgW2ZKZ52fiyEs94IZ7L5RLL7jY1nVdwtsp2fxeqiZ3DNmVZ2GdNrbJGT//160tyd2evtUtehr8ygXNzjWdjV0cc4+1F38ywSPFyieVzVTYzDywRllgo3A5A==").unwrap(); + println!( + "fixed b_pub: {:?}", + general_purpose::STANDARD.encode(&b_pub) + ); println!(""); - println!("salt: {:?} iterations: {:?}", base64::encode(&salt), iters); + println!( + "salt: {:?} iterations: {:?}", + general_purpose::STANDARD.encode(&salt), + iters + ); let verifier: SrpClientVerifier = SrpClient::::process_reply( &client, diff --git a/icloud-auth/tests/gsa_auth.rs b/icloud-auth/tests/gsa_auth.rs index 6770245..ae4336d 100644 --- a/icloud-auth/tests/gsa_auth.rs +++ b/icloud-auth/tests/gsa_auth.rs @@ -22,16 +22,21 @@ mod tests { input.trim().to_string() }); - let appleid_closure = move || (email.clone(), password.clone()); + let appleid_closure = move || Ok((email.clone(), password.clone())); // ask console for 2fa code, make sure it is only 6 digits, no extra characters let tfa_closure = || { println!("Enter 2FA code: "); let mut input = String::new(); std::io::stdin().read_line(&mut input).unwrap(); - input.trim().to_string() + Ok(input.trim().to_string()) }; - let acc = AppleAccount::login(appleid_closure, tfa_closure, AnisetteConfiguration::new() - .set_configuration_path(PathBuf::from_str("anisette_test").unwrap())).await; + let acc = AppleAccount::login( + appleid_closure, + tfa_closure, + AnisetteConfiguration::new() + .set_configuration_path(PathBuf::from_str("anisette_test").unwrap()), + ) + .await; let account = acc.unwrap(); println!("data {:?}", account.get_name()); diff --git a/omnisette/Cargo.toml b/omnisette/Cargo.toml index 1f54873..785c301 100644 --- a/omnisette/Cargo.toml +++ b/omnisette/Cargo.toml @@ -1,7 +1,10 @@ [package] name = "omnisette" -version = "0.1.0" +version = "0.1.3" edition = "2021" +description = "A library to generate \"anisette\" data. Modified from Sidestore/apple-private-apis" +repository = "https://github.com/nab138/apple-private-apis" +license = "MPL-2.0" [features] remote-anisette = [] @@ -10,11 +13,11 @@ default = ["remote-anisette", "dep:remove-async-await"] remote-anisette-v3 = ["async", "dep:serde", "dep:serde_json", "dep:tokio-tungstenite", "dep:futures-util", "dep:chrono"] [dependencies] -base64 = "0.21" +base64 = "0.22" hex = "0.4" plist = "1.4" -reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "rustls-tls"] } -rand = "0.8" +reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "rustls-tls", "gzip"] } +rand = "0.9" sha2 = "0.10" uuid = { version = "1.3", features = [ "v4", "fast-rng", "macro-diagnostics" ] } android-loader = { git = "https://github.com/Dadoum/android-loader", branch = "bigger_pages" } @@ -24,12 +27,13 @@ async-trait = { version = "0.1", optional = true } remove-async-await = { version = "1.0", optional = true } serde = { version = "1.0", features = ["derive"], optional = true } serde_json = { version = "1.0.115", optional = true } -tokio-tungstenite = { version = "0.20.1", optional = true, features = ["rustls-tls-webpki-roots"] } +tokio-tungstenite = { version = "0.27.0", features = ["rustls-tls-webpki-roots"], optional = true } futures-util = { version = "0.3.28", optional = true } chrono = { version = "0.4.37", optional = true } -thiserror = "1.0.58" +thiserror = "2" anyhow = "1.0.81" + [target.'cfg(target_os = "macos")'.dependencies] dlopen2 = "0.4" objc = "0.2" diff --git a/omnisette/src/remote_anisette_v3.rs b/omnisette/src/remote_anisette_v3.rs index d5f28a9..f73fb14 100644 --- a/omnisette/src/remote_anisette_v3.rs +++ b/omnisette/src/remote_anisette_v3.rs @@ -1,26 +1,24 @@ - // Implementing the SideStore Anisette v3 protocol use std::{collections::HashMap, fs, io::Cursor, path::PathBuf}; +use async_trait::async_trait; use base64::engine::general_purpose; +use base64::Engine; use chrono::{DateTime, SubsecRound, Utc}; +use futures_util::{stream::StreamExt, SinkExt}; use log::debug; use plist::{Data, Dictionary}; +use rand::Rng; use reqwest::{Client, ClientBuilder, RequestBuilder}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use rand::Rng; -use sha2::{Sha256, Digest}; +use sha2::{Digest, Sha256}; +use std::fmt::Write; use tokio_tungstenite::{connect_async, tungstenite::Message}; use uuid::Uuid; -use futures_util::{stream::StreamExt, SinkExt}; -use std::fmt::Write; -use base64::Engine; -use async_trait::async_trait; use crate::{anisette_headers_provider::AnisetteHeadersProvider, AnisetteError}; - fn plist_to_string(value: &T) -> Result { plist_to_buf(value).map(|val| String::from_utf8(val).unwrap()) } @@ -43,7 +41,7 @@ fn bin_serialize_opt(x: &Option>, s: S) -> Result where S: Serializer, { - x.clone().map(|i| Data::new(i)).serialize(s) + x.clone().map(Data::new).serialize(s) } fn bin_deserialize_opt<'de, D>(d: D) -> Result>, D::Error> @@ -78,8 +76,6 @@ fn base64_decode(data: &str) -> Vec { general_purpose::STANDARD.decode(data.trim()).unwrap() } - - #[derive(Deserialize)] struct AnisetteClientInfo { client_info: String, @@ -88,17 +84,23 @@ struct AnisetteClientInfo { #[derive(Serialize, Deserialize)] pub struct AnisetteState { - #[serde(serialize_with = "bin_serialize", deserialize_with = "bin_deserialize_16")] + #[serde( + serialize_with = "bin_serialize", + deserialize_with = "bin_deserialize_16" + )] keychain_identifier: [u8; 16], - #[serde(serialize_with = "bin_serialize_opt", deserialize_with = "bin_deserialize_opt")] + #[serde( + serialize_with = "bin_serialize_opt", + deserialize_with = "bin_deserialize_opt" + )] adi_pb: Option>, } impl Default for AnisetteState { fn default() -> Self { AnisetteState { - keychain_identifier: rand::thread_rng().gen::<[u8; 16]>(), - adi_pb: None + keychain_identifier: rand::rng().random::<[u8; 16]>(), + adi_pb: None, } } } @@ -114,7 +116,7 @@ impl AnisetteState { fn md_lu(&self) -> [u8; 32] { let mut hasher = Sha256::new(); - hasher.update(&self.keychain_identifier); + hasher.update(self.keychain_identifier); hasher.finalize().into() } @@ -124,7 +126,7 @@ impl AnisetteState { } pub struct AnisetteClient { client_info: AnisetteClientInfo, - url: String + url: String, } #[derive(Serialize)] @@ -141,25 +143,34 @@ pub struct AnisetteData { routing_info: String, device_description: String, local_user_id: String, - device_unique_identifier: String + device_unique_identifier: String, } impl AnisetteData { pub fn get_headers(&self, serial: String) -> HashMap { let dt: DateTime = Utc::now().round_subsecs(0); - + HashMap::from_iter([ - ("X-Apple-I-Client-Time".to_string(), dt.format("%+").to_string().replace("+00:00", "Z")), + ( + "X-Apple-I-Client-Time".to_string(), + dt.format("%+").to_string().replace("+00:00", "Z"), + ), ("X-Apple-I-SRL-NO".to_string(), serial), ("X-Apple-I-TimeZone".to_string(), "UTC".to_string()), ("X-Apple-Locale".to_string(), "en_US".to_string()), ("X-Apple-I-MD-RINFO".to_string(), self.routing_info.clone()), ("X-Apple-I-MD-LU".to_string(), self.local_user_id.clone()), - ("X-Mme-Device-Id".to_string(), self.device_unique_identifier.clone()), + ( + "X-Mme-Device-Id".to_string(), + self.device_unique_identifier.clone(), + ), ("X-Apple-I-MD".to_string(), self.one_time_password.clone()), ("X-Apple-I-MD-M".to_string(), self.machine_id.clone()), - ("X-Mme-Client-Info".to_string(), self.device_description.clone()), - ].into_iter()) + ( + "X-Mme-Client-Info".to_string(), + self.device_description.clone(), + ), + ]) } } @@ -174,19 +185,24 @@ impl AnisetteClient { pub async fn new(url: String) -> Result { let path = format!("{}/v3/client_info", url); let http_client = make_reqwest()?; - let client_info = http_client.get(path) - .send().await? - .json::().await?; - Ok(AnisetteClient { - client_info, - url - }) + let client_info = http_client + .get(path) + .send() + .await? + .json::() + .await?; + Ok(AnisetteClient { client_info, url }) } - fn build_apple_request(&self, state: &AnisetteState, builder: RequestBuilder) -> RequestBuilder { + fn build_apple_request( + &self, + state: &AnisetteState, + builder: RequestBuilder, + ) -> RequestBuilder { let dt: DateTime = Utc::now().round_subsecs(0); - builder.header("X-Mme-Client-Info", &self.client_info.client_info) + builder + .header("X-Mme-Client-Info", &self.client_info.client_info) .header("User-Agent", &self.client_info.user_agent) .header("Content-Type", "text/x-xml-plist") .header("X-Apple-I-MD-LU", encode_hex(&state.md_lu())) @@ -207,14 +223,19 @@ impl AnisetteClient { } let body = GetHeadersBody { identifier: base64_encode(&state.keychain_identifier), - adi_pb: base64_encode(state.adi_pb.as_ref().ok_or(AnisetteError::AnisetteNotProvisioned)?), + adi_pb: base64_encode( + state + .adi_pb + .as_ref() + .ok_or(AnisetteError::AnisetteNotProvisioned)?, + ), }; #[derive(Deserialize)] #[serde(tag = "result")] enum AnisetteHeaders { GetHeadersError { - message: String + message: String, }, Headers { #[serde(rename = "X-Apple-I-MD-M")] @@ -223,13 +244,16 @@ impl AnisetteClient { one_time_password: String, #[serde(rename = "X-Apple-I-MD-RINFO")] routing_info: String, - } + }, } - let headers = http_client.post(path) + let headers = http_client + .post(path) .json(&body) - .send().await? - .json::().await?; + .send() + .await? + .json::() + .await?; match headers { AnisetteHeaders::GetHeadersError { message } => { if message.contains("-45061") { @@ -237,38 +261,62 @@ impl AnisetteClient { } else { panic!("Unknown error {}", message) } - }, - AnisetteHeaders::Headers { machine_id, one_time_password, routing_info } => { - Ok(AnisetteData { - machine_id, - one_time_password, - routing_info, - device_description: self.client_info.client_info.clone(), - local_user_id: encode_hex(&state.md_lu()), - device_unique_identifier: state.device_id() - }) } + AnisetteHeaders::Headers { + machine_id, + one_time_password, + routing_info, + } => Ok(AnisetteData { + machine_id, + one_time_password, + routing_info, + device_description: self.client_info.client_info.clone(), + local_user_id: encode_hex(&state.md_lu()), + device_unique_identifier: state.device_id(), + }), } } pub async fn provision(&self, state: &mut AnisetteState) -> Result<(), AnisetteError> { debug!("Provisioning Anisette"); let http_client = make_reqwest()?; - let resp = self.build_apple_request(&state, http_client.get("https://gsa.apple.com/grandslam/GsService2/lookup")) - .send().await?; + let resp = self + .build_apple_request( + state, + http_client.get("https://gsa.apple.com/grandslam/GsService2/lookup"), + ) + .send() + .await?; let text = resp.text().await?; let protocol_val = plist::Value::from_reader(Cursor::new(text.as_str()))?; - let urls = protocol_val.as_dictionary().unwrap().get("urls").unwrap().as_dictionary().unwrap(); - - let start_provisioning_url = urls.get("midStartProvisioning").unwrap().as_string().unwrap(); - let end_provisioning_url = urls.get("midFinishProvisioning").unwrap().as_string().unwrap(); - debug!("Got provisioning urls: {} and {}", start_provisioning_url, end_provisioning_url); + let urls = protocol_val + .as_dictionary() + .unwrap() + .get("urls") + .unwrap() + .as_dictionary() + .unwrap(); + + let start_provisioning_url = urls + .get("midStartProvisioning") + .unwrap() + .as_string() + .unwrap(); + let end_provisioning_url = urls + .get("midFinishProvisioning") + .unwrap() + .as_string() + .unwrap(); + debug!( + "Got provisioning urls: {} and {}", + start_provisioning_url, end_provisioning_url + ); - let provision_ws_url = format!("{}/v3/provisioning_session", self.url).replace("https://", "wss://"); + let provision_ws_url = + format!("{}/v3/provisioning_session", self.url).replace("https://", "wss://"); let (mut connection, _) = connect_async(&provision_ws_url).await?; - #[derive(Deserialize)] #[serde(tag = "result")] enum ProvisionInput { @@ -276,17 +324,17 @@ impl AnisetteClient { GiveStartProvisioningData, GiveEndProvisioningData { #[allow(dead_code)] // it's not even dead, rust just has problems - cpim: String + cpim: String, }, ProvisioningSuccess { #[allow(dead_code)] // it's not even dead, rust just has problems - adi_pb: String - } + adi_pb: String, + }, } loop { let Some(Ok(data)) = connection.next().await else { - continue + continue; }; if data.is_text() { let txt = data.to_text().unwrap(); @@ -295,44 +343,77 @@ impl AnisetteClient { ProvisionInput::GiveIdentifier => { #[derive(Serialize)] struct Identifier { - identifier: String // base64 + identifier: String, // base64 } - let identifier = Identifier { identifier: base64_encode(&state.keychain_identifier) }; - connection.send(Message::Text(serde_json::to_string(&identifier)?)).await?; - }, + let identifier = Identifier { + identifier: base64_encode(&state.keychain_identifier), + }; + connection + .send(Message::Text(serde_json::to_string(&identifier)?.into())) + .await?; + } ProvisionInput::GiveStartProvisioningData => { let http_client = make_reqwest()?; - let body_data = ProvisionBodyData { header: Dictionary::new(), request: Dictionary::new() }; - let resp = self.build_apple_request(state, http_client.post(start_provisioning_url)) + let body_data = ProvisionBodyData { + header: Dictionary::new(), + request: Dictionary::new(), + }; + let resp = self + .build_apple_request(state, http_client.post(start_provisioning_url)) .body(plist_to_string(&body_data)?) - .send().await?; + .send() + .await?; let text = resp.text().await?; let protocol_val = plist::Value::from_reader(Cursor::new(text.as_str()))?; - let spim = protocol_val.as_dictionary().unwrap().get("Response").unwrap().as_dictionary().unwrap() - .get("spim").unwrap().as_string().unwrap(); - + let spim = protocol_val + .as_dictionary() + .unwrap() + .get("Response") + .unwrap() + .as_dictionary() + .unwrap() + .get("spim") + .unwrap() + .as_string() + .unwrap(); + debug!("GiveStartProvisioningData"); #[derive(Serialize)] struct Spim { - spim: String // base64 + spim: String, // base64 } - let spim = Spim { spim: spim.to_string() }; - connection.send(Message::Text(serde_json::to_string(&spim)?)).await?; - }, + let spim = Spim { + spim: spim.to_string(), + }; + connection + .send(Message::Text(serde_json::to_string(&spim)?.into())) + .await?; + } ProvisionInput::GiveEndProvisioningData { cpim } => { let http_client = make_reqwest()?; - let body_data = ProvisionBodyData { header: Dictionary::new(), request: Dictionary::from_iter([("cpim", cpim)].into_iter()) }; - let resp = self.build_apple_request(state, http_client.post(end_provisioning_url)) + let body_data = ProvisionBodyData { + header: Dictionary::new(), + request: Dictionary::from_iter([("cpim", cpim)].into_iter()), + }; + let resp = self + .build_apple_request(state, http_client.post(end_provisioning_url)) .body(plist_to_string(&body_data)?) - .send().await?; + .send() + .await?; let text = resp.text().await?; let protocol_val = plist::Value::from_reader(Cursor::new(text.as_str()))?; - let response = protocol_val.as_dictionary().unwrap().get("Response").unwrap().as_dictionary().unwrap(); + let response = protocol_val + .as_dictionary() + .unwrap() + .get("Response") + .unwrap() + .as_dictionary() + .unwrap(); debug!("GiveEndProvisioningData"); - + #[derive(Serialize)] struct EndProvisioning<'t> { ptm: &'t str, @@ -342,8 +423,12 @@ impl AnisetteClient { ptm: response.get("ptm").unwrap().as_string().unwrap(), tk: response.get("tk").unwrap().as_string().unwrap(), }; - connection.send(Message::Text(serde_json::to_string(&end_provisioning)?)).await?; - }, + connection + .send(Message::Text( + serde_json::to_string(&end_provisioning)?.into(), + )) + .await?; + } ProvisionInput::ProvisioningSuccess { adi_pb } => { debug!("ProvisioningSuccess"); state.adi_pb = Some(base64_decode(&adi_pb)); @@ -360,23 +445,26 @@ impl AnisetteClient { } } - pub struct RemoteAnisetteProviderV3 { client_url: String, client: Option, pub state: Option, configuration_path: PathBuf, - serial: String + serial: String, } impl RemoteAnisetteProviderV3 { - pub fn new(url: String, configuration_path: PathBuf, serial: String) -> RemoteAnisetteProviderV3 { + pub fn new( + url: String, + configuration_path: PathBuf, + serial: String, + ) -> RemoteAnisetteProviderV3 { RemoteAnisetteProviderV3 { client_url: url, client: None, state: None, configuration_path, - serial + serial, } } } @@ -393,7 +481,7 @@ impl AnisetteHeadersProvider for RemoteAnisetteProviderV3 { let client = self.client.as_ref().unwrap(); fs::create_dir_all(&self.configuration_path)?; - + let config_path = self.configuration_path.join("state.plist"); if self.state.is_none() { self.state = Some(if let Ok(text) = plist::from_file(&config_path) { @@ -408,16 +496,18 @@ impl AnisetteHeadersProvider for RemoteAnisetteProviderV3 { client.provision(state).await?; plist::to_file_xml(&config_path, state)?; } - let data = match client.get_headers(&state).await { + let data = match client.get_headers(state).await { Ok(data) => data, Err(err) => { if matches!(err, AnisetteError::AnisetteNotProvisioned) { state.adi_pb = None; client.provision(state).await?; plist::to_file_xml(config_path, state)?; - client.get_headers(&state).await? - } else { panic!() } - }, + client.get_headers(state).await? + } else { + panic!() + } + } }; Ok(data.get_headers(self.serial.clone())) } @@ -434,12 +524,37 @@ mod tests { async fn fetch_anisette_remote_v3() -> Result<(), AnisetteError> { crate::tests::init_logger(); - let mut provider = RemoteAnisetteProviderV3::new(DEFAULT_ANISETTE_URL_V3.to_string(), "anisette_test".into(), "0".to_string()); + let mut provider = RemoteAnisetteProviderV3::new( + DEFAULT_ANISETTE_URL_V3.to_string(), + "anisette_test".into(), + "0".to_string(), + ); info!( "Remote headers: {:?}", - (&mut provider as &mut dyn AnisetteHeadersProvider).get_authentication_headers().await? + (&mut provider as &mut dyn AnisetteHeadersProvider) + .get_authentication_headers() + .await? ); Ok(()) } -} + #[cfg(not(feature = "async"))] + #[test] + fn fetch_anisette_auto() -> Result<()> { + use crate::{AnisetteConfiguration, AnisetteHeaders}; + use log::info; + use std::path::PathBuf; + + crate::tests::init_logger(); + + let mut provider = AnisetteHeaders::get_anisette_headers_provider( + AnisetteConfiguration::new() + .set_configuration_path(PathBuf::new().join("anisette_test")), + )?; + info!( + "Headers: {:?}", + provider.provider.get_authentication_headers()? + ); + Ok(()) + } +}