diff --git a/.gitignore b/.gitignore index f0916ac5..e9c8fb2d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,6 @@ *.exe *.DS_Store state.plist +mac_anisette_state.plist .zsign_cache cargo-sources.json diff --git a/3rdparty/omnisette/Cargo.toml b/3rdparty/omnisette/Cargo.toml index d3d10fa0..b7adc47c 100644 --- a/3rdparty/omnisette/Cargo.toml +++ b/3rdparty/omnisette/Cargo.toml @@ -50,5 +50,6 @@ objc = "0.2" objc-foundation = "0.1" [dev-dependencies] -tokio.workspace = true +tokio = { workspace = true, features = ["macros"] } +chrono = { workspace = true, features = ["clock"] } simplelog = "0.12" diff --git a/3rdparty/omnisette/src/lib.rs b/3rdparty/omnisette/src/lib.rs index 69b8684e..979fd24c 100755 --- a/3rdparty/omnisette/src/lib.rs +++ b/3rdparty/omnisette/src/lib.rs @@ -57,8 +57,13 @@ pub enum AnisetteError { pub const DEFAULT_ANISETTE_URL: &str = "https://ani.f1sh.me/"; +// Keep the remote provider and its client identity consistent. Apple rejects the +// previous Windows anisette identity during two-factor authentication (-80046). pub const DEFAULT_ANISETTE_URL_V3: &str = "https://mac-ani.jkcoxson.com"; +// Do not reuse provisioning data created with the previous provider/identity. +pub const ANISETTE_STATE_FILE: &str = "mac_anisette_state.plist"; + #[derive(Clone, Debug)] pub struct AnisetteConfiguration { anisette_url: String, diff --git a/3rdparty/omnisette/src/remote_anisette_v3.rs b/3rdparty/omnisette/src/remote_anisette_v3.rs index 014689a0..c18702b9 100755 --- a/3rdparty/omnisette/src/remote_anisette_v3.rs +++ b/3rdparty/omnisette/src/remote_anisette_v3.rs @@ -2,7 +2,8 @@ use std::{collections::HashMap, fs, io::Cursor, path::PathBuf}; -use crate::adi_proxy::{CLIENT_INFO_HEADER, LOOKUP_CLIENT_INFO}; +use crate::ANISETTE_STATE_FILE; +use crate::adi_proxy::LOOKUP_CLIENT_INFO; use async_trait::async_trait; use base64::Engine; use base64::engine::general_purpose; @@ -188,13 +189,12 @@ impl AnisetteClient { pub async fn new(url: String) -> Result { let path = format!("{}/v3/client_info", url); let http_client = make_reqwest()?; - let mut client_info = http_client + let client_info = http_client .get(path) .send() .await? .json::() .await?; - client_info.client_info = CLIENT_INFO_HEADER.to_string(); Ok(AnisetteClient { client_info, url }) } @@ -496,7 +496,7 @@ impl AnisetteHeadersProvider for RemoteAnisetteProviderV3 { fs::create_dir_all(&self.configuration_path)?; - let config_path = self.configuration_path.join("state.plist"); + let config_path = self.configuration_path.join(ANISETTE_STATE_FILE); if self.state.is_none() { self.state = Some(if let Ok(text) = plist::from_file(&config_path) { text @@ -529,11 +529,105 @@ impl AnisetteHeadersProvider for RemoteAnisetteProviderV3 { #[cfg(test)] mod tests { + use super::{AnisetteClient, AnisetteState}; use crate::anisette_headers_provider::AnisetteHeadersProvider; use crate::remote_anisette_v3::RemoteAnisetteProviderV3; use crate::{AnisetteError, DEFAULT_ANISETTE_URL_V3}; use log::info; + #[tokio::test] + async fn regression_server_identity_used_for_provisioning_and_auth_headers() { + use std::io::{Read, Write}; + use std::net::TcpListener; + use std::time::Duration; + + const MAC_CLIENT_INFO: &str = + " "; + const USER_AGENT: &str = "akd/1.0 CFNetwork/808.3 Darwin/16.6.0"; + + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let url = format!("http://{}", listener.local_addr().unwrap()); + let server = std::thread::spawn(move || { + let responses = [ + ( + "GET /v3/client_info ", + serde_json::json!({ + "client_info": MAC_CLIENT_INFO, + "user_agent": USER_AGENT, + }), + ), + ( + "POST /v3/get_headers ", + serde_json::json!({ + "result": "Headers", + "X-Apple-I-MD-M": "test-machine", + "X-Apple-I-MD": "test-otp", + "X-Apple-I-MD-RINFO": "17106176", + }), + ), + ]; + for (expected_request, response) in responses { + let (mut stream, _) = listener.accept().unwrap(); + stream + .set_read_timeout(Some(Duration::from_secs(5))) + .unwrap(); + let mut request = Vec::new(); + let mut buffer = [0; 1024]; + loop { + let count = stream.read(&mut buffer).unwrap(); + assert_ne!(count, 0, "request closed before its body was received"); + request.extend_from_slice(&buffer[..count]); + if let Some(end) = request.windows(4).position(|bytes| bytes == b"\r\n\r\n") { + let headers = String::from_utf8_lossy(&request[..end]); + let content_length = headers + .lines() + .find_map(|line| { + let (name, value) = line.split_once(':')?; + name.eq_ignore_ascii_case("content-length") + .then(|| value.trim().parse::().unwrap()) + }) + .unwrap_or(0); + if request.len() >= end + 4 + content_length { + break; + } + } + } + assert!(String::from_utf8_lossy(&request).starts_with(expected_request)); + let body = response.to_string(); + write!(stream, + "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + body.len(), body + ).unwrap(); + } + }); + + let client = AnisetteClient::new(url).await.unwrap(); + let state = AnisetteState { + keychain_identifier: [1; 16], + adi_pb: Some(vec![2; 16]), + }; + let request = client + .build_apple_request( + &state, + reqwest::Client::new().post("http://localhost/provision"), + &client.client_info.client_info, + ) + .build() + .unwrap(); + assert_eq!(request.headers()["X-Mme-Client-Info"], MAC_CLIENT_INFO); + assert_eq!(request.headers()["User-Agent"], USER_AGENT); + + let headers = client + .get_headers(&state) + .await + .unwrap() + .get_headers("0".into()); + assert_eq!(headers["X-Mme-Client-Info"], MAC_CLIENT_INFO); + assert_eq!(headers["X-Apple-I-MD-M"], "test-machine"); + assert_eq!(headers["X-Apple-I-MD"], "test-otp"); + server.join().unwrap(); + } + #[tokio::test] async fn fetch_anisette_remote_v3() -> Result<(), AnisetteError> { crate::tests::init_logger(); diff --git a/apps/plumeimpactor/src/screen/settings.rs b/apps/plumeimpactor/src/screen/settings.rs index c2d1aa5c..11a3a495 100644 --- a/apps/plumeimpactor/src/screen/settings.rs +++ b/apps/plumeimpactor/src/screen/settings.rs @@ -93,7 +93,7 @@ impl SettingsScreen { Message::SelectTeam(_, _) => Task::none(), Message::SelectLocale(_) => Task::none(), Message::ResetAnisetteData => { - let path = crate::defaults::get_data_path().join("state.plist"); + let path = crate::defaults::get_data_path().join(plume_core::ANISETTE_STATE_FILE); let _ = std::fs::remove_file(path); Task::none() } diff --git a/crates/plume_core/src/auth/anisette_data.rs b/crates/plume_core/src/auth/anisette_data.rs index 1db931af..1f26710a 100644 --- a/crates/plume_core/src/auth/anisette_data.rs +++ b/crates/plume_core/src/auth/anisette_data.rs @@ -110,3 +110,60 @@ impl AnisetteData { } } } + +#[cfg(test)] +mod tests { + use super::*; + + const MAC_CLIENT_INFO: &str = " "; + + fn mac_anisette() -> AnisetteData { + AnisetteData { + base_headers: HashMap::from([ + ("X-Mme-Client-Info".to_string(), MAC_CLIENT_INFO.to_string()), + ("X-Apple-I-MD".to_string(), "synthetic-otp".to_string()), + ( + "X-Apple-I-MD-M".to_string(), + "synthetic-machine-id".to_string(), + ), + ]), + generated_at: SystemTime::now(), + config: AnisetteConfiguration::default(), + } + } + + #[test] + fn mac_identity_is_retained_for_two_factor_and_developer_headers() { + let data = mac_anisette(); + + let headers = data.generate_headers(false, true, true); + + assert_eq!( + headers["X-Mme-Client-Info"], + " " + ); + assert_eq!(headers["X-Apple-App-Info"], "com.apple.gs.xcode.auth"); + assert_eq!(headers["X-Xcode-Version"], "11.2 (11B41)"); + assert_eq!(headers["X-Apple-I-MD"], "synthetic-otp"); + assert_eq!(data.base_headers["X-Mme-Client-Info"], MAC_CLIENT_INFO); + } + + #[test] + fn srp_cpd_retains_anisette_without_adding_client_or_app_info() { + let data = mac_anisette(); + + let cpd = data.to_plist(true, false, false); + + assert!(!cpd.contains_key("X-Mme-Client-Info")); + assert!(!cpd.contains_key("X-Apple-App-Info")); + assert!(!cpd.contains_key("X-Xcode-Version")); + assert_eq!(cpd["X-Apple-I-MD"].as_string(), Some("synthetic-otp")); + assert_eq!( + cpd["X-Apple-I-MD-M"].as_string(), + Some("synthetic-machine-id") + ); + assert_eq!(cpd["bootstrap"].as_string(), Some("true")); + assert_eq!(cpd["svct"].as_string(), Some("iCloud")); + assert_eq!(data.base_headers["X-Mme-Client-Info"], MAC_CLIENT_INFO); + } +} diff --git a/crates/plume_core/src/lib.rs b/crates/plume_core/src/lib.rs index 9c7776f4..6e5e8e7c 100644 --- a/crates/plume_core/src/lib.rs +++ b/crates/plume_core/src/lib.rs @@ -4,7 +4,7 @@ mod utils; pub use apple_codesign::{AppleCodesignError, SettingsScope, SigningSettings, UnifiedSigner}; -pub use omnisette::AnisetteConfiguration; +pub use omnisette::{ANISETTE_STATE_FILE, AnisetteConfiguration}; pub use utils::{CertificateIdentity, MachO, MachOExt, MobileProvision};