mirror of
https://github.com/nab138/isideload.git
synced 2026-03-02 14:36:16 +01:00
Anisette provisioning works
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1,3 @@
|
|||||||
target
|
target
|
||||||
|
|
||||||
|
state.plist
|
||||||
675
Cargo.lock
generated
675
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -5,5 +5,6 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
isideload = { path = "../../isideload" }
|
isideload = { path = "../../isideload" }
|
||||||
env_logger = "0.11.8"
|
|
||||||
tokio = { version = "1.49.0", features = ["rt-multi-thread", "macros"] }
|
tokio = { version = "1.49.0", features = ["rt-multi-thread", "macros"] }
|
||||||
|
tracing = "0.1.44"
|
||||||
|
tracing-subscriber = "0.3.22"
|
||||||
|
|||||||
@@ -3,10 +3,15 @@ use std::{env, path::PathBuf};
|
|||||||
use isideload::{
|
use isideload::{
|
||||||
anisette::remote_v3::RemoteV3AnisetteProvider, auth::apple_account::AppleAccountBuilder,
|
anisette::remote_v3::RemoteV3AnisetteProvider, auth::apple_account::AppleAccountBuilder,
|
||||||
};
|
};
|
||||||
|
use tracing::Level;
|
||||||
|
use tracing_subscriber::FmtSubscriber;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
env_logger::init();
|
let subscriber = FmtSubscriber::builder()
|
||||||
|
.with_max_level(Level::DEBUG)
|
||||||
|
.finish();
|
||||||
|
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
|
||||||
|
|
||||||
let args: Vec<String> = env::args().collect();
|
let args: Vec<String> = env::args().collect();
|
||||||
let _app_path = PathBuf::from(
|
let _app_path = PathBuf::from(
|
||||||
@@ -22,13 +27,17 @@ async fn main() {
|
|||||||
let mut code = String::new();
|
let mut code = String::new();
|
||||||
println!("Enter 2FA code:");
|
println!("Enter 2FA code:");
|
||||||
std::io::stdin().read_line(&mut code).unwrap();
|
std::io::stdin().read_line(&mut code).unwrap();
|
||||||
Ok(code.trim().to_string())
|
Some(code.trim().to_string())
|
||||||
};
|
};
|
||||||
|
|
||||||
let _account = AppleAccountBuilder::new(apple_id)
|
let account = AppleAccountBuilder::new(apple_id)
|
||||||
.danger_debug(true)
|
.danger_debug(true)
|
||||||
.anisette(RemoteV3AnisetteProvider::default().set_serial_number("2".to_string()))
|
.anisette(RemoteV3AnisetteProvider::default().set_serial_number("2".to_string()))
|
||||||
.login(apple_password, get_2fa_code)
|
.login(apple_password, get_2fa_code)
|
||||||
.await
|
.await;
|
||||||
.unwrap();
|
|
||||||
|
match account {
|
||||||
|
Ok(_account) => println!("Successfully logged in to Apple ID"),
|
||||||
|
Err(e) => eprintln!("Failed to log in to Apple ID: {}", e),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,20 @@ install = ["dep:idevice"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
idevice = { version = "0.1.51", optional = true }
|
idevice = { version = "0.1.51", optional = true }
|
||||||
plist = "1.8.0"
|
plist = "1.8"
|
||||||
plist-macro = "0.1.0"
|
plist-macro = "0.1.3"
|
||||||
log = "0.4"
|
|
||||||
reqwest = { version = "0.13.1", features = ["json", "gzip"] }
|
reqwest = { version = "0.13.1", features = ["json", "gzip"] }
|
||||||
thiserror = "2.0.17"
|
thiserror = "2.0.17"
|
||||||
thiserror-context = "0.1.2"
|
|
||||||
chrono = "0.4.43"
|
chrono = "0.4.43"
|
||||||
|
async-trait = "0.1.89"
|
||||||
|
serde = "1.0.228"
|
||||||
|
rand = "0.9.2"
|
||||||
|
uuid = "1.19.0"
|
||||||
|
sha2 = "0.10.9"
|
||||||
|
tracing = "0.1.44"
|
||||||
|
tokio-tungstenite = { version = "0.28.0", features = ["rustls-tls-webpki-roots"] }
|
||||||
|
rootcause = "0.11.1"
|
||||||
|
futures-util = "0.3.31"
|
||||||
|
serde_json = "1.0.149"
|
||||||
|
base64 = "0.22.1"
|
||||||
|
hex = "0.4.3"
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
pub mod remote_v3;
|
pub mod remote_v3;
|
||||||
|
|
||||||
pub trait AnisetteProvider {}
|
use crate::auth::grandslam::GrandSlam;
|
||||||
|
use rootcause::prelude::*;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
pub struct AnisetteClientInfo {
|
||||||
|
pub client_info: String,
|
||||||
|
pub user_agent: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait AnisetteProvider {
|
||||||
|
async fn get_anisette_headers(
|
||||||
|
&mut self,
|
||||||
|
gs: &mut GrandSlam,
|
||||||
|
) -> Result<HashMap<String, String>, Report>;
|
||||||
|
|
||||||
|
async fn get_client_info(&mut self) -> Result<AnisetteClientInfo, Report>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use chrono::{DateTime, SubsecRound, Utc};
|
|
||||||
use reqwest::header::{HeaderMap, HeaderValue};
|
|
||||||
|
|
||||||
use crate::SideloadResult as Result;
|
|
||||||
use crate::anisette::AnisetteProvider;
|
|
||||||
|
|
||||||
pub const DEFAULT_ANISETTE_V3_URL: &str = "https://ani.sidestore.io";
|
|
||||||
|
|
||||||
pub struct RemoteV3AnisetteProvider {
|
|
||||||
url: String,
|
|
||||||
config_path: PathBuf,
|
|
||||||
serial_number: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RemoteV3AnisetteProvider {
|
|
||||||
/// Create a new RemoteV3AnisetteProvider with the given URL and config path
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// - `url`: The URL of the remote anisette service
|
|
||||||
/// - `config_path`: The path to the config file
|
|
||||||
/// - `serial_number`: The serial number of the device
|
|
||||||
pub fn new(url: &str, config_path: PathBuf, serial_number: String) -> Self {
|
|
||||||
Self {
|
|
||||||
url: url.to_string(),
|
|
||||||
config_path,
|
|
||||||
serial_number,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_url(mut self, url: &str) -> RemoteV3AnisetteProvider {
|
|
||||||
self.url = url.to_string();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_config_path(mut self, config_path: PathBuf) -> RemoteV3AnisetteProvider {
|
|
||||||
self.config_path = config_path;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_serial_number(mut self, serial_number: String) -> RemoteV3AnisetteProvider {
|
|
||||||
self.serial_number = serial_number;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for RemoteV3AnisetteProvider {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new(DEFAULT_ANISETTE_V3_URL, PathBuf::new(), "0".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct AnisetteData {
|
|
||||||
machine_id: String,
|
|
||||||
one_time_password: String,
|
|
||||||
routing_info: String,
|
|
||||||
device_description: String,
|
|
||||||
device_unique_identifier: String,
|
|
||||||
local_user_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AnisetteData {
|
|
||||||
pub fn get_headers(&self, serial: String) -> Result<HeaderMap> {
|
|
||||||
let dt: DateTime<Utc> = Utc::now().round_subsecs(0);
|
|
||||||
|
|
||||||
let mut headers = HeaderMap::new();
|
|
||||||
|
|
||||||
for (key, value) in vec![
|
|
||||||
(
|
|
||||||
"X-Apple-I-Client-Time",
|
|
||||||
dt.format("%+").to_string().replace("+00:00", "Z"),
|
|
||||||
),
|
|
||||||
("X-Apple-I-SRL-NO", serial),
|
|
||||||
("X-Apple-I-TimeZone", "UTC".to_string()),
|
|
||||||
("X-Apple-Locale", "en_US".to_string()),
|
|
||||||
("X-Apple-I-MD-RINFO", self.routing_info.clone()),
|
|
||||||
("X-Apple-I-MD-LU", self.local_user_id.clone()),
|
|
||||||
("X-Mme-Device-Id", self.device_unique_identifier.clone()),
|
|
||||||
("X-Apple-I-MD", self.one_time_password.clone()),
|
|
||||||
("X-Apple-I-MD-M", self.machine_id.clone()),
|
|
||||||
("X-Mme-Client-Info", self.device_description.clone()),
|
|
||||||
] {
|
|
||||||
headers.insert(key, HeaderValue::from_str(&value)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(headers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AnisetteProvider for RemoteV3AnisetteProvider {}
|
|
||||||
393
isideload/src/anisette/remote_v3/mod.rs
Normal file
393
isideload/src/anisette/remote_v3/mod.rs
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
mod state;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use base64::prelude::*;
|
||||||
|
use chrono::{DateTime, SubsecRound, Utc};
|
||||||
|
use plist_macro::plist;
|
||||||
|
use reqwest::header::{HeaderMap, HeaderValue};
|
||||||
|
use rootcause::prelude::*;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tokio_tungstenite::tungstenite::Message;
|
||||||
|
use tracing::{debug, info};
|
||||||
|
|
||||||
|
use crate::anisette::remote_v3::state::AnisetteState;
|
||||||
|
use crate::anisette::{AnisetteClientInfo, AnisetteProvider};
|
||||||
|
use crate::auth::grandslam::GrandSlam;
|
||||||
|
use crate::util::plist::plist_to_xml_string;
|
||||||
|
use futures_util::{SinkExt, StreamExt};
|
||||||
|
|
||||||
|
pub const DEFAULT_ANISETTE_V3_URL: &str = "https://ani.sidestore.io";
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AnisetteData {
|
||||||
|
machine_id: String,
|
||||||
|
one_time_password: String,
|
||||||
|
routing_info: String,
|
||||||
|
device_description: String,
|
||||||
|
device_unique_identifier: String,
|
||||||
|
local_user_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnisetteData {
|
||||||
|
pub fn get_headers(&self, serial: String) -> HashMap<String, String> {
|
||||||
|
let dt: DateTime<Utc> = 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-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-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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RemoteV3AnisetteProvider {
|
||||||
|
pub state: Option<AnisetteState>,
|
||||||
|
url: String,
|
||||||
|
config_path: PathBuf,
|
||||||
|
serial_number: String,
|
||||||
|
client_info: Option<AnisetteClientInfo>,
|
||||||
|
client: reqwest::Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RemoteV3AnisetteProvider {
|
||||||
|
/// Create a new RemoteV3AnisetteProvider with the given URL and config path
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// - `url`: The URL of the remote anisette service
|
||||||
|
/// - `config_path`: The path to the config file
|
||||||
|
/// - `serial_number`: The serial number of the device
|
||||||
|
///
|
||||||
|
pub fn new(url: &str, config_path: PathBuf, serial_number: String) -> Self {
|
||||||
|
Self {
|
||||||
|
state: None,
|
||||||
|
url: url.to_string(),
|
||||||
|
config_path,
|
||||||
|
serial_number,
|
||||||
|
client_info: None,
|
||||||
|
client: reqwest::ClientBuilder::new()
|
||||||
|
.danger_accept_invalid_certs(true)
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_url(mut self, url: &str) -> RemoteV3AnisetteProvider {
|
||||||
|
self.url = url.to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_config_path(mut self, config_path: PathBuf) -> RemoteV3AnisetteProvider {
|
||||||
|
self.config_path = config_path;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_serial_number(mut self, serial_number: String) -> RemoteV3AnisetteProvider {
|
||||||
|
self.serial_number = serial_number;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RemoteV3AnisetteProvider {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(DEFAULT_ANISETTE_V3_URL, PathBuf::new(), "0".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl AnisetteProvider for RemoteV3AnisetteProvider {
|
||||||
|
async fn get_anisette_headers(
|
||||||
|
&mut self,
|
||||||
|
gs: &mut GrandSlam,
|
||||||
|
) -> Result<HashMap<String, String>, Report> {
|
||||||
|
let state = self.get_state(gs).await?;
|
||||||
|
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_client_info(&mut self) -> Result<AnisetteClientInfo, Report> {
|
||||||
|
self.ensure_client_info().await?;
|
||||||
|
Ok(self.client_info.as_ref().unwrap().clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RemoteV3AnisetteProvider {
|
||||||
|
async fn ensure_client_info(&mut self) -> Result<(), Report> {
|
||||||
|
if self.client_info.is_none() {
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.get(format!("{}/v3/client_info", self.url))
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.json::<AnisetteClientInfo>()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
self.client_info = Some(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("Got client client_info: {:?}", self.client_info);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_state(&mut self, gs: &mut GrandSlam) -> Result<&mut AnisetteState, Report> {
|
||||||
|
let state_path = self.config_path.join("state.plist");
|
||||||
|
fs::create_dir_all(&self.config_path)?;
|
||||||
|
if self.state.is_none() {
|
||||||
|
if let Ok(state) = plist::from_file(&state_path) {
|
||||||
|
debug!("Loaded existing anisette state from {:?}", state_path);
|
||||||
|
self.state = Some(state);
|
||||||
|
} else {
|
||||||
|
debug!("No existing anisette state found");
|
||||||
|
self.state = Some(AnisetteState::new());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = self.state.as_mut().unwrap();
|
||||||
|
if !state.is_provisioned() {
|
||||||
|
info!("Provisioning required...");
|
||||||
|
Self::provision(state, gs, &self.url)
|
||||||
|
.await
|
||||||
|
.context("Failed to provision")?;
|
||||||
|
}
|
||||||
|
plist::to_file_xml(&state_path, &state)?;
|
||||||
|
|
||||||
|
Ok(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn provisioning_headers(
|
||||||
|
state: &AnisetteState,
|
||||||
|
gs: &mut GrandSlam,
|
||||||
|
) -> Result<HeaderMap, Report> {
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert(
|
||||||
|
"X-Apple-I-MD-LU",
|
||||||
|
HeaderValue::from_str(&hex::encode(state.get_md_lu()))?,
|
||||||
|
);
|
||||||
|
headers.insert(
|
||||||
|
"X-Apple-I-Client-Time",
|
||||||
|
HeaderValue::from_str(
|
||||||
|
&Utc::now()
|
||||||
|
.round_subsecs(0)
|
||||||
|
.format("%+")
|
||||||
|
.to_string()
|
||||||
|
.replace("+00:00", "Z"),
|
||||||
|
)?,
|
||||||
|
);
|
||||||
|
headers.insert("X-Apple-I-TimeZone", HeaderValue::from_static("UTC"));
|
||||||
|
headers.insert("X-Apple-Locale", HeaderValue::from_static("en_US"));
|
||||||
|
headers.insert(
|
||||||
|
"X-Mme-Device-Id",
|
||||||
|
HeaderValue::from_str(&state.get_device_id())?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(headers)
|
||||||
|
}
|
||||||
|
async fn provision(
|
||||||
|
state: &mut AnisetteState,
|
||||||
|
gs: &mut GrandSlam,
|
||||||
|
url: &str,
|
||||||
|
) -> Result<(), Report> {
|
||||||
|
info!("Starting provisioning");
|
||||||
|
let urls = gs.get_url_bag().await?;
|
||||||
|
|
||||||
|
let start_provisioning = urls
|
||||||
|
.get("midStartProvisioning")
|
||||||
|
.and_then(|v| v.as_string())
|
||||||
|
.ok_or(report!("Missing URL bag entry for midStartProvisioning"))?
|
||||||
|
.to_string();
|
||||||
|
let end_provisioning = urls
|
||||||
|
.get("midFinishProvisioning")
|
||||||
|
.and_then(|v| v.as_string())
|
||||||
|
.ok_or(report!("Missing URL bag entry for midFinishProvisioning"))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let websocket_url = format!("{}/v3/provisioning_session", url)
|
||||||
|
.replace("https://", "wss://")
|
||||||
|
.replace("http://", "ws://");
|
||||||
|
let (mut ws_stream, _) = tokio_tungstenite::connect_async(&websocket_url)
|
||||||
|
.await
|
||||||
|
.context("Failed to connect anisette provisioning socket")?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let Some(msg) = ws_stream.next().await else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let msg = msg.context("Failed to read anisette provisioning socket message")?;
|
||||||
|
if msg.is_close() {
|
||||||
|
bail!("Anisette provisioning socket closed unexpectedly");
|
||||||
|
}
|
||||||
|
let msg = msg
|
||||||
|
.into_text()
|
||||||
|
.context("Failed to parse provisioning message")?;
|
||||||
|
|
||||||
|
debug!("Received provisioning message: {}", msg);
|
||||||
|
let provision_msg: ProvisioningMessage =
|
||||||
|
serde_json::from_str(&msg).context("Unknown provisioning message")?;
|
||||||
|
|
||||||
|
match provision_msg {
|
||||||
|
ProvisioningMessage::GiveIdentifier => {
|
||||||
|
ws_stream
|
||||||
|
.send(Message::Text(
|
||||||
|
serde_json::json!({
|
||||||
|
"identifier": BASE64_STANDARD.encode(&state.keychain_identifier),
|
||||||
|
})
|
||||||
|
.to_string()
|
||||||
|
.into(),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.context("Failed to send identifier")?;
|
||||||
|
}
|
||||||
|
ProvisioningMessage::GiveStartProvisioningData => {
|
||||||
|
let body = plist!(dict {
|
||||||
|
"Header": {},
|
||||||
|
"Request": {}
|
||||||
|
});
|
||||||
|
|
||||||
|
let resp = gs
|
||||||
|
.post(&start_provisioning)?
|
||||||
|
.headers(Self::provisioning_headers(state, gs).await?)
|
||||||
|
.body(plist_to_xml_string(&body))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("Failed to send start provisioning request")?
|
||||||
|
.error_for_status()
|
||||||
|
.context("Start provisioning request returned error")?
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.context("Failed to read start provisioning response text")?;
|
||||||
|
|
||||||
|
let resp_plist: plist::Dictionary = plist::from_bytes(resp.as_bytes())
|
||||||
|
.context("Failed to parse start provisioning response plist")?;
|
||||||
|
|
||||||
|
let spim = resp_plist
|
||||||
|
.get("Response")
|
||||||
|
.and_then(|v| v.as_dictionary())
|
||||||
|
.and_then(|d| d.get("spim"))
|
||||||
|
.and_then(|v| v.as_string())
|
||||||
|
.ok_or(report!("Start provisioning response missing spim"))?;
|
||||||
|
|
||||||
|
ws_stream
|
||||||
|
.send(Message::Text(
|
||||||
|
serde_json::json!({
|
||||||
|
"spim": spim,
|
||||||
|
})
|
||||||
|
.to_string()
|
||||||
|
.into(),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.context("Failed to send start provisioning data")?;
|
||||||
|
}
|
||||||
|
ProvisioningMessage::GiveEndProvisioningData { cpim } => {
|
||||||
|
let body = plist!(dict {
|
||||||
|
"Header": {},
|
||||||
|
"Request": {
|
||||||
|
"cpim": cpim,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let resp = gs
|
||||||
|
.post(&end_provisioning)?
|
||||||
|
.headers(Self::provisioning_headers(state, gs).await?)
|
||||||
|
.body(plist_to_xml_string(&body))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("Failed to send end provisioning request")?
|
||||||
|
.error_for_status()
|
||||||
|
.context("End provisioning request returned error")?
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.context("Failed to read end provisioning response text")?;
|
||||||
|
|
||||||
|
let resp_plist: plist::Dictionary = plist::from_bytes(resp.as_bytes())
|
||||||
|
.context("Failed to parse end provisioning response plist")?;
|
||||||
|
let response = resp_plist
|
||||||
|
.get("Response")
|
||||||
|
.and_then(|v| v.as_dictionary())
|
||||||
|
.ok_or(report!(
|
||||||
|
"End provisioning response missing Response dictionary"
|
||||||
|
))?;
|
||||||
|
|
||||||
|
ws_stream
|
||||||
|
.send(Message::Text(
|
||||||
|
serde_json::json!({
|
||||||
|
"ptm": response
|
||||||
|
.get("ptm")
|
||||||
|
.and_then(|v| v.as_string())
|
||||||
|
.ok_or(report!("End provisioning response missing ptm"))?,
|
||||||
|
"tk": response
|
||||||
|
.get("tk")
|
||||||
|
.and_then(|v| v.as_string())
|
||||||
|
.ok_or(report!("End provisioning response missing tk"))?,
|
||||||
|
})
|
||||||
|
.to_string()
|
||||||
|
.into(),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.context("Failed to send start provisioning data")?;
|
||||||
|
}
|
||||||
|
ProvisioningMessage::ProvisioningSuccess { adi_pb } => {
|
||||||
|
state.adi_pb = Some(BASE64_STANDARD.decode(adi_pb)?);
|
||||||
|
ws_stream.close(None).await?;
|
||||||
|
info!("Provisioning successful");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ProvisioningMessage::Timeout => bail!("Anisette provisioning timed out"),
|
||||||
|
ProvisioningMessage::InvalidIdentifier => {
|
||||||
|
bail!("Anisette provisioning failed: invalid identifier")
|
||||||
|
}
|
||||||
|
ProvisioningMessage::StartProvisioningError { message } => {
|
||||||
|
return Err(
|
||||||
|
report!("Anisette provisioning failed: start provisioning error")
|
||||||
|
.attach(message)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ProvisioningMessage::EndProvisioningError { message } => {
|
||||||
|
return Err(
|
||||||
|
report!("Anisette provisioning failed: end provisioning error")
|
||||||
|
.attach(message)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(tag = "result")]
|
||||||
|
enum ProvisioningMessage {
|
||||||
|
GiveIdentifier,
|
||||||
|
GiveStartProvisioningData,
|
||||||
|
GiveEndProvisioningData { cpim: String },
|
||||||
|
ProvisioningSuccess { adi_pb: String },
|
||||||
|
Timeout,
|
||||||
|
InvalidIdentifier,
|
||||||
|
StartProvisioningError { message: String },
|
||||||
|
EndProvisioningError { message: String },
|
||||||
|
}
|
||||||
81
isideload/src/anisette/remote_v3/state.rs
Normal file
81
isideload/src/anisette/remote_v3/state.rs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
// Serialization/Desieralization borrowed from https://github.com/SideStore/apple-private-apis/blob/master/omnisette/src/remote_anisette_v3.rs
|
||||||
|
|
||||||
|
use plist::Data;
|
||||||
|
use rand::Rng;
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
fn bin_serialize<S>(x: &[u8], s: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
s.serialize_bytes(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bin_serialize_opt<S>(x: &Option<Vec<u8>>, s: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
x.clone().map(|i| Data::new(i)).serialize(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bin_deserialize_opt<'de, D>(d: D) -> Result<Option<Vec<u8>>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s: Option<Data> = Deserialize::deserialize(d)?;
|
||||||
|
Ok(s.map(|i| i.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bin_deserialize_16<'de, D>(d: D) -> Result<[u8; 16], D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s: Data = Deserialize::deserialize(d)?;
|
||||||
|
let s: Vec<u8> = s.into();
|
||||||
|
Ok(s.try_into().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct AnisetteState {
|
||||||
|
#[serde(
|
||||||
|
serialize_with = "bin_serialize",
|
||||||
|
deserialize_with = "bin_deserialize_16"
|
||||||
|
)]
|
||||||
|
pub keychain_identifier: [u8; 16],
|
||||||
|
#[serde(
|
||||||
|
serialize_with = "bin_serialize_opt",
|
||||||
|
deserialize_with = "bin_deserialize_opt"
|
||||||
|
)]
|
||||||
|
pub adi_pb: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AnisetteState {
|
||||||
|
fn default() -> Self {
|
||||||
|
AnisetteState {
|
||||||
|
keychain_identifier: rand::rng().random::<[u8; 16]>(),
|
||||||
|
adi_pb: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnisetteState {
|
||||||
|
pub fn new() -> AnisetteState {
|
||||||
|
AnisetteState::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_provisioned(&self) -> bool {
|
||||||
|
self.adi_pb.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_md_lu(&self) -> [u8; 32] {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(&self.keychain_identifier);
|
||||||
|
hasher.finalize().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_device_id(&self) -> String {
|
||||||
|
Uuid::from_bytes(self.keychain_identifier).to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,15 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
SideloadResult as Result,
|
|
||||||
anisette::{AnisetteProvider, remote_v3::RemoteV3AnisetteProvider},
|
anisette::{AnisetteProvider, remote_v3::RemoteV3AnisetteProvider},
|
||||||
auth::grandslam::GrandSlam,
|
auth::grandslam::GrandSlam,
|
||||||
};
|
};
|
||||||
use log::{info, warn};
|
use rootcause::prelude::*;
|
||||||
use reqwest::{Certificate, ClientBuilder};
|
use tracing::{info, warn};
|
||||||
use thiserror_context::Context;
|
|
||||||
|
|
||||||
const APPLE_ROOT: &[u8] = include_bytes!("./apple_root.der");
|
|
||||||
|
|
||||||
pub struct AppleAccount {
|
pub struct AppleAccount {
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub spd: Option<plist::Dictionary>,
|
pub spd: Option<plist::Dictionary>,
|
||||||
pub client: reqwest::Client,
|
|
||||||
pub anisette: Box<dyn AnisetteProvider>,
|
pub anisette: Box<dyn AnisetteProvider>,
|
||||||
|
pub grandslam_client: GrandSlam,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AppleAccountBuilder {
|
pub struct AppleAccountBuilder {
|
||||||
@@ -56,16 +52,20 @@ impl AppleAccountBuilder {
|
|||||||
/// - `two_factor_callback`: A callback function that returns the two-factor authentication code
|
/// - `two_factor_callback`: A callback function that returns the two-factor authentication code
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Returns an error if the reqwest client cannot be built
|
/// Returns an error if the reqwest client cannot be built
|
||||||
pub async fn login<F>(self, _password: &str, _two_factor_callback: F) -> Result<AppleAccount>
|
pub async fn login<F>(
|
||||||
|
self,
|
||||||
|
password: &str,
|
||||||
|
two_factor_callback: F,
|
||||||
|
) -> Result<AppleAccount, Report>
|
||||||
where
|
where
|
||||||
F: Fn() -> Result<String>,
|
F: Fn() -> Option<String>,
|
||||||
{
|
{
|
||||||
let debug = self.debug.unwrap_or(false);
|
let debug = self.debug.unwrap_or(false);
|
||||||
let anisette = self
|
let anisette = self
|
||||||
.anisette
|
.anisette
|
||||||
.unwrap_or_else(|| Box::new(RemoteV3AnisetteProvider::default()));
|
.unwrap_or_else(|| Box::new(RemoteV3AnisetteProvider::default()));
|
||||||
|
|
||||||
AppleAccount::login(&self.email, debug, anisette).await
|
AppleAccount::login(&self.email, password, two_factor_callback, anisette, debug).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,43 +83,36 @@ impl AppleAccount {
|
|||||||
/// Reccomended to use the AppleAccountBuilder instead
|
/// Reccomended to use the AppleAccountBuilder instead
|
||||||
pub async fn login(
|
pub async fn login(
|
||||||
email: &str,
|
email: &str,
|
||||||
|
password: &str,
|
||||||
|
two_factor_callback: impl Fn() -> Option<String>,
|
||||||
|
mut anisette: Box<dyn AnisetteProvider>,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
anisette: Box<dyn AnisetteProvider>,
|
) -> Result<Self, Report> {
|
||||||
) -> Result<Self> {
|
|
||||||
info!("Logging in to apple ID: {}", email);
|
info!("Logging in to apple ID: {}", email);
|
||||||
if debug {
|
if debug {
|
||||||
warn!("Debug mode enabled: this is a security risk!");
|
warn!("Debug mode enabled: this is a security risk!");
|
||||||
}
|
}
|
||||||
let client = Self::build_client(debug)?;
|
|
||||||
|
|
||||||
let mut gs = GrandSlam::new(&client);
|
let client_info = anisette
|
||||||
gs.get_url_bag()
|
.get_client_info()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get URL bag from GrandSlam")?;
|
.context("Failed to get anisette client info")?;
|
||||||
|
let mut grandslam_client = GrandSlam::new(client_info, debug);
|
||||||
|
let url_bag = grandslam_client
|
||||||
|
.get_url_bag()
|
||||||
|
.await
|
||||||
|
.context("Failed to get URL bag for login")?;
|
||||||
|
|
||||||
|
let headers = anisette
|
||||||
|
.get_anisette_headers(&mut grandslam_client)
|
||||||
|
.await
|
||||||
|
.context("Failed to get anisette headers for login")?;
|
||||||
|
|
||||||
Ok(AppleAccount {
|
Ok(AppleAccount {
|
||||||
email: email.to_string(),
|
email: email.to_string(),
|
||||||
spd: None,
|
spd: None,
|
||||||
client,
|
|
||||||
anisette,
|
anisette,
|
||||||
|
grandslam_client,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build a reqwest client with the Apple root certificate
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// - `debug`: DANGER, If true, accept invalid certificates and enable verbose connection logging
|
|
||||||
/// # Errors
|
|
||||||
/// Returns an error if the reqwest client cannot be built
|
|
||||||
pub fn build_client(debug: bool) -> Result<reqwest::Client> {
|
|
||||||
let cert = Certificate::from_der(APPLE_ROOT)?;
|
|
||||||
let client = ClientBuilder::new()
|
|
||||||
.add_root_certificate(cert)
|
|
||||||
.http1_title_case_headers()
|
|
||||||
.danger_accept_invalid_certs(debug)
|
|
||||||
.connection_verbose(debug)
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
Ok(client)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +1,102 @@
|
|||||||
use crate::SideloadResult as Result;
|
|
||||||
use log::debug;
|
|
||||||
use plist::Dictionary;
|
use plist::Dictionary;
|
||||||
use plist_macro::pretty_print_dictionary;
|
use reqwest::{Certificate, ClientBuilder, header::HeaderValue};
|
||||||
use reqwest::header::HeaderValue;
|
use rootcause::prelude::*;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
use crate::anisette::AnisetteClientInfo;
|
||||||
|
|
||||||
|
const APPLE_ROOT: &[u8] = include_bytes!("./apple_root.der");
|
||||||
const URL_BAG: &str = "https://gsa.apple.com/grandslam/GsService2/lookup";
|
const URL_BAG: &str = "https://gsa.apple.com/grandslam/GsService2/lookup";
|
||||||
|
|
||||||
pub struct GrandSlam<'a> {
|
pub struct GrandSlam {
|
||||||
client: &'a reqwest::Client,
|
pub client: reqwest::Client,
|
||||||
url_bag: Option<Dictionary>,
|
pub client_info: AnisetteClientInfo,
|
||||||
|
pub url_bag: Option<Dictionary>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> GrandSlam<'a> {
|
impl GrandSlam {
|
||||||
/// Create a new GrandSlam instance
|
/// Create a new GrandSlam instance
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// - `client`: The reqwest client to use for requests
|
/// - `client`: The reqwest client to use for requests
|
||||||
pub fn new(client: &'a reqwest::Client) -> Self {
|
pub fn new(client_info: AnisetteClientInfo, debug: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
client,
|
client: Self::build_reqwest_client(debug).unwrap(),
|
||||||
|
client_info,
|
||||||
url_bag: None,
|
url_bag: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the URL bag from GrandSlam
|
/// Get the URL bag from GrandSlam
|
||||||
pub async fn get_url_bag(&mut self) -> Result<&Dictionary> {
|
pub async fn get_url_bag(&mut self) -> Result<&Dictionary, Report> {
|
||||||
if self.url_bag.is_none() {
|
if self.url_bag.is_none() {
|
||||||
debug!("Fetching URL bag from GrandSlam");
|
debug!("Fetching URL bag from GrandSlam");
|
||||||
let resp = self
|
let resp = self
|
||||||
.client
|
.client
|
||||||
.get(URL_BAG)
|
.get(URL_BAG)
|
||||||
.headers(Self::base_headers())
|
.headers(self.base_headers()?)
|
||||||
.send()
|
.send()
|
||||||
.await?
|
.await
|
||||||
|
.context("Failed to fetch URL Bag")?
|
||||||
.text()
|
.text()
|
||||||
.await?;
|
.await
|
||||||
let dict: Dictionary = plist::from_bytes(resp.as_bytes())?;
|
.context("Failed to read URL Bag response text")?;
|
||||||
debug!("{}", pretty_print_dictionary(&dict));
|
|
||||||
self.url_bag = Some(dict);
|
let dict: Dictionary =
|
||||||
|
plist::from_bytes(resp.as_bytes()).context("Failed to parse URL Bag plist")?;
|
||||||
|
let urls = dict
|
||||||
|
.get("urls")
|
||||||
|
.and_then(|v| v.as_dictionary())
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| report!("URL Bag plist missing 'urls' dictionary"))?;
|
||||||
|
|
||||||
|
self.url_bag = Some(urls);
|
||||||
}
|
}
|
||||||
Ok(self.url_bag.as_ref().unwrap())
|
Ok(self.url_bag.as_ref().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn base_headers() -> reqwest::header::HeaderMap {
|
pub fn post(&self, url: &str) -> Result<reqwest::RequestBuilder, Report> {
|
||||||
|
let builder = self.client.post(url).headers(self.base_headers()?);
|
||||||
|
|
||||||
|
Ok(builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_headers(&self) -> Result<reqwest::header::HeaderMap, Report> {
|
||||||
let mut headers = reqwest::header::HeaderMap::new();
|
let mut headers = reqwest::header::HeaderMap::new();
|
||||||
headers.insert("Context-Type", HeaderValue::from_static("text/x-xml-plist"));
|
headers.insert("Content-Type", HeaderValue::from_static("text/x-xml-plist"));
|
||||||
headers.insert("Accept", HeaderValue::from_static("text/x-xml-plist"));
|
headers.insert("Accept", HeaderValue::from_static("text/x-xml-plist"));
|
||||||
headers.insert(
|
headers.insert(
|
||||||
"X-Mme-Client-Info",
|
"X-Mme-Client-Info",
|
||||||
HeaderValue::from_static(
|
HeaderValue::from_str(&self.client_info.client_info)?,
|
||||||
"<MacBookPro13,2> <macOS;13.1;22C65> <com.apple.AuthKit/1 (com.apple.dt.Xcode/3594.4.19)>",
|
);
|
||||||
),
|
headers.insert(
|
||||||
|
"User-Agent",
|
||||||
|
HeaderValue::from_str(&self.client_info.user_agent)?,
|
||||||
);
|
);
|
||||||
headers.insert("User-Agent", HeaderValue::from_static("Xcode"));
|
|
||||||
headers.insert("X-Xcode-Version", HeaderValue::from_static("14.2 (14C18)"));
|
headers.insert("X-Xcode-Version", HeaderValue::from_static("14.2 (14C18)"));
|
||||||
headers.insert(
|
headers.insert(
|
||||||
"X-Apple-App-Info",
|
"X-Apple-App-Info",
|
||||||
HeaderValue::from_static("com.apple.gs.xcode.auth"),
|
HeaderValue::from_static("com.apple.gs.xcode.auth"),
|
||||||
);
|
);
|
||||||
|
|
||||||
headers
|
Ok(headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a reqwest client with the Apple root certificate
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// - `debug`: DANGER, If true, accept invalid certificates and enable verbose connection logging
|
||||||
|
/// # Errors
|
||||||
|
/// Returns an error if the reqwest client cannot be built
|
||||||
|
pub fn build_reqwest_client(debug: bool) -> Result<reqwest::Client, Report> {
|
||||||
|
let cert = Certificate::from_der(APPLE_ROOT)?;
|
||||||
|
let client = ClientBuilder::new()
|
||||||
|
.add_root_certificate(cert)
|
||||||
|
.http1_title_case_headers()
|
||||||
|
.danger_accept_invalid_certs(debug)
|
||||||
|
.connection_verbose(debug)
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
Ok(client)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,3 @@
|
|||||||
use thiserror::Error as ThisError;
|
|
||||||
use thiserror_context::{Context, impl_context};
|
|
||||||
|
|
||||||
pub mod anisette;
|
pub mod anisette;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
|
pub mod util;
|
||||||
#[derive(Debug, ThisError)]
|
|
||||||
pub enum ErrorInner {
|
|
||||||
#[error("Failed sending request: {0}")]
|
|
||||||
Reqwest(#[from] reqwest::Error),
|
|
||||||
|
|
||||||
#[error("Failed parsing plist: {0}")]
|
|
||||||
Plist(#[from] plist::Error),
|
|
||||||
|
|
||||||
#[error("Invalid Header: {0}")]
|
|
||||||
InvalidHeader(#[from] reqwest::header::InvalidHeaderValue),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl_context!(Error(ErrorInner));
|
|
||||||
|
|
||||||
pub type SideloadResult<T> = std::result::Result<T, Error>;
|
|
||||||
|
|||||||
1
isideload/src/util/mod.rs
Normal file
1
isideload/src/util/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod plist;
|
||||||
9
isideload/src/util/plist.rs
Normal file
9
isideload/src/util/plist.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
use plist_macro::{plist_to_xml_bytes, plist_value_to_xml_bytes};
|
||||||
|
|
||||||
|
pub fn plist_to_xml_string(p: &plist::Dictionary) -> String {
|
||||||
|
String::from_utf8(plist_to_xml_bytes(p)).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn plist_value_to_xml_string(p: &plist::Value) -> String {
|
||||||
|
String::from_utf8(plist_value_to_xml_bytes(p)).unwrap()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user