Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
*.exe
*.DS_Store
state.plist
mac_anisette_state.plist
.zsign_cache
cargo-sources.json
3 changes: 2 additions & 1 deletion 3rdparty/omnisette/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
5 changes: 5 additions & 0 deletions 3rdparty/omnisette/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
102 changes: 98 additions & 4 deletions 3rdparty/omnisette/src/remote_anisette_v3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -188,13 +189,12 @@ impl AnisetteClient {
pub async fn new(url: String) -> Result<AnisetteClient, AnisetteError> {
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::<AnisetteClientInfo>()
.await?;
client_info.client_info = CLIENT_INFO_HEADER.to_string();
Ok(AnisetteClient { client_info, url })
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 =
"<MacBookPro13,3> <Mac OS X;10.12.6;16G29> <com.apple.AuthKit/1 (com.apple.akd/1.0)>";
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::<usize>().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();
Expand Down
2 changes: 1 addition & 1 deletion apps/plumeimpactor/src/screen/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
57 changes: 57 additions & 0 deletions crates/plume_core/src/auth/anisette_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,60 @@ impl AnisetteData {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

const MAC_CLIENT_INFO: &str = "<MacBookPro13,3> <Mac OS X;10.12.6;16G29> <com.apple.akd/1.0>";

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"],
"<MacBookPro13,3> <Mac OS X;10.12.6;16G29> <com.apple.AuthKit/1 (com.apple.akd/1.0)>"
);
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);
}
}
2 changes: 1 addition & 1 deletion crates/plume_core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down