mod state;
mod websocket;
use std::sync::Arc;
use web_time::SystemTime;
use base64::prelude::*;
use plist_macro::plist;
use reqwest::{
ClientBuilder,
header::{CONTENT_TYPE, HeaderMap, HeaderValue},
};
use reqwest_middleware::ClientBuilder as MwClientBuilder;
use rootcause::option_ext::OptionExt;
use rootcause::prelude::*;
use serde::Deserialize;
use tracing::{debug, info, warn};
use crate::anisette::remote_v3::{state::AnisetteState, websocket::WsMessage};
use crate::anisette::{AnisetteClientInfo, AnisetteData, AnisetteProvider};
use crate::auth::grandslam::GrandSlam;
use crate::util::plist::PlistDataExtract;
use crate::util::storage::{SideloadingStorage, new_storage};
use crate::{SideloadError, anisette::remote_v3::websocket::AppWebSocket};
pub const DEFAULT_ANISETTE_V3_URL: &str = "https://ani.stikstore.app";
pub struct RemoteV3AnisetteProvider {
pub state: Option<AnisetteState>,
url: String,
storage: Box<dyn SideloadingStorage>,
serial_number: String,
client_info: Option<AnisetteClientInfo>,
client: reqwest_middleware::ClientWithMiddleware,
websocket_proxy: Option<String>,
}
impl RemoteV3AnisetteProvider {
pub fn new(
url: &str,
storage: Box<dyn SideloadingStorage>,
serial_number: String,
) -> Result<Self, Report> {
Ok(Self {
state: None,
url: url.to_string(),
storage,
serial_number,
client_info: None,
client: Self::build_reqwest_client(None)?,
websocket_proxy: None,
})
}
pub fn set_websocket_proxy(mut self, websocket_proxy: Option<String>) -> Result<Self, Report> {
self.websocket_proxy = websocket_proxy;
self.client = Self::build_reqwest_client(self.websocket_proxy.clone())?;
Ok(self)
}
fn build_reqwest_client(
websocket_proxy: Option<String>,
) -> Result<reqwest_middleware::ClientWithMiddleware, Report> {
if let Some(websocket_proxy) = websocket_proxy {
use crate::auth::middleware::WasmProxyMiddleware;
let client = ClientBuilder::new().build()?;
Ok(MwClientBuilder::new(client)
.with(WasmProxyMiddleware::new(websocket_proxy))
.build())
} else {
let client = ClientBuilder::new().build()?;
Ok(MwClientBuilder::new(client).build())
}
}
pub fn default() -> Result<Self, Report> {
Self::new(
DEFAULT_ANISETTE_V3_URL,
Box::new(new_storage()),
"0".to_string(),
)
}
pub fn set_url(mut self, url: &str) -> RemoteV3AnisetteProvider {
self.url = url.to_string();
self
}
pub fn set_storage(mut self, storage: Box<dyn SideloadingStorage>) -> RemoteV3AnisetteProvider {
self.storage = storage;
self
}
pub fn set_serial_number(mut self, serial_number: String) -> RemoteV3AnisetteProvider {
self.serial_number = serial_number;
self
}
}
#[cfg_attr(feature = "wasm", async_trait::async_trait(?Send))]
#[cfg_attr(not(feature = "wasm"), async_trait::async_trait)]
impl AnisetteProvider for RemoteV3AnisetteProvider {
async fn get_anisette_data(&self) -> Result<AnisetteData, Report> {
let state = self
.state
.as_ref()
.ok_or(SideloadError::AnisetteNotProvisioned)?;
let adi_pb = state
.adi_pb
.as_ref()
.ok_or(SideloadError::AnisetteNotProvisioned)?;
let client_info = self
.client_info
.as_ref()
.ok_or(SideloadError::AnisetteNotProvisioned)?;
let headers = self
.client
.post(format!("{}/v3/get_headers", self.url))
.header(CONTENT_TYPE, "application/json")
.body(
serde_json::json!({
"identifier": BASE64_STANDARD.encode(state.keychain_identifier),
"adi_pb": BASE64_STANDARD.encode(adi_pb)
})
.to_string(),
)
.send()
.await?
.error_for_status()?
.json::<AnisetteHeaders>()
.await?;
match headers {
AnisetteHeaders::Headers {
machine_id,
one_time_password,
routing_info,
} => {
let data = AnisetteData {
machine_id,
one_time_password,
routing_info,
_device_description: client_info.client_info.clone(),
device_unique_identifier: state.get_device_id(),
_local_user_id: hex::encode(state.get_md_lu()),
generated_at: SystemTime::now(),
};
Ok(data)
}
AnisetteHeaders::GetHeadersError { message } => {
Err(report!("Failed to get anisette headers").attach(message))
}
}
}
async fn get_client_info(&mut self) -> Result<AnisetteClientInfo, Report> {
match self.client_info {
Some(ref info) => Ok(info.clone()),
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.clone());
Ok(resp)
}
}
}
fn needs_provisioning(&self) -> Result<bool, Report> {
if let Some(state) = &self.state {
Ok(!state.is_provisioned() || self.client_info.is_none())
} else {
Ok(true)
}
}
async fn provision(&mut self, gs: Arc<GrandSlam>) -> Result<(), Report> {
self.get_client_info().await?;
self.get_state(gs).await?;
Ok(())
}
}
impl RemoteV3AnisetteProvider {
async fn get_state(&mut self, gs: Arc<GrandSlam>) -> Result<&mut AnisetteState, Report> {
if self.state.is_none() {
if let Ok(Some(state)) = &self.storage.retrieve_data("anisette_state") {
if let Ok(state) = plist::from_bytes(state) {
info!("Loaded existing anisette state");
self.state = Some(state);
} else {
warn!("Failed to parse existing anisette state, starting fresh");
self.state = Some(AnisetteState::new());
}
} else {
info!("No existing anisette state found");
self.state = Some(AnisetteState::new());
}
}
let state = self.state.as_mut().ok_or_report()?;
if !state.is_provisioned() {
info!("Provisioning required...");
Self::provision(state, gs, &self.url, self.websocket_proxy.as_deref())
.await
.context("Failed to provision")?;
}
let buf = Vec::new();
let mut writer = std::io::BufWriter::new(buf);
plist::to_writer_xml(&mut writer, &state)?;
self.storage
.store_data("anisette_state", &writer.into_inner()?)?;
Ok(state)
}
async fn provisioning_headers(state: &AnisetteState) -> 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-Mme-Device-Id",
HeaderValue::from_str(&state.get_device_id())?,
);
Ok(headers)
}
async fn provision(
state: &mut AnisetteState,
gs: Arc<GrandSlam>,
url: &str,
proxy_url: Option<&str>,
) -> Result<(), Report> {
let start_provisioning = gs.get_url("midStartProvisioning")?;
let end_provisioning = gs.get_url("midFinishProvisioning")?;
let websocket_url = format!("{}/v3/provisioning_session", url)
.replace("https://", "wss://")
.replace("http://", "ws://");
debug!("Starting provisioning at {}", websocket_url);
let mut ws = AppWebSocket::connect(&websocket_url, proxy_url.as_deref())
.await
.context("Failed to connect to provisioning socket")?;
debug!("Connected to provisioning socket");
loop {
let Some(msg) = ws.next().await else { continue };
let msg = msg?;
let text = match msg {
WsMessage::Close => bail!("Provisioning socket closed unexpectedly"),
WsMessage::Text(t) => t,
};
debug!("Received provisioning message: {}", text);
let provision_msg: ProvisioningMessage = serde_json::from_str(&text)?;
match provision_msg {
ProvisioningMessage::GiveIdentifier => {
ws.send_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 response = gs
.plist_request(
&start_provisioning,
&body,
Some(Self::provisioning_headers(state).await?),
)
.await
.context("Failed to send start provisioning request")?;
let spim = response
.get_str("spim")
.context("Start provisioning response missing spim")?;
ws.send_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 response = gs
.plist_request(
&end_provisioning,
&body,
Some(Self::provisioning_headers(state).await?),
)
.await
.context("Failed to send end provisioning request")?;
ws.send_text(
serde_json::json!({
"ptm": response
.get_str("ptm")
.context("End provisioning response missing ptm")?,
"tk": response
.get_str("tk")
.context("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.close().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),
);
}
ProvisioningMessage::EndProvisioningError { message } => {
return Err(
report!("Anisette provisioning failed: end provisioning error")
.attach(message),
);
}
}
}
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 },
}
#[derive(Deserialize)]
#[serde(tag = "result")]
enum AnisetteHeaders {
GetHeadersError {
message: String,
},
Headers {
#[serde(rename = "X-Apple-I-MD-M")]
machine_id: String,
#[serde(rename = "X-Apple-I-MD")]
one_time_password: String,
#[serde(rename = "X-Apple-I-MD-RINFO")]
routing_info: String,
},
}