Keep working on anisette

This commit is contained in:
nab138
2026-01-22 12:34:34 -05:00
parent f4687ac3be
commit 4e50c5c1d4
11 changed files with 534 additions and 123 deletions

View File

@@ -18,5 +18,8 @@ install = ["dep:idevice"]
idevice = { version = "0.1.51", optional = true }
plist = "1.8.0"
plist-macro = "0.1.0"
log = "0.4"
reqwest = { version = "0.13.1", features = ["json", "gzip"] }
thiserror = "2.0.17"
thiserror-context = "0.1.2"
chrono = "0.4.43"

View File

@@ -1,6 +1,3 @@
pub mod remote_v3;
pub trait AnisetteProvider {}
// tmp
pub struct DefaultAnisetteProvider {}
impl AnisetteProvider for DefaultAnisetteProvider {}

View File

@@ -0,0 +1,92 @@
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 {}

View File

@@ -1,5 +1,11 @@
use crate::Result;
use crate::{
SideloadResult as Result,
anisette::{AnisetteProvider, remote_v3::RemoteV3AnisetteProvider},
auth::grandslam::GrandSlam,
};
use log::{info, warn};
use reqwest::{Certificate, ClientBuilder};
use thiserror_context::Context;
const APPLE_ROOT: &[u8] = include_bytes!("./apple_root.der");
@@ -7,13 +13,13 @@ pub struct AppleAccount {
pub email: String,
pub spd: Option<plist::Dictionary>,
pub client: reqwest::Client,
pub anisette: Box<dyn crate::anisette::AnisetteProvider>,
pub anisette: Box<dyn AnisetteProvider>,
}
#[derive(Debug)]
pub struct AppleAccountBuilder {
email: String,
debug: Option<bool>,
anisette: Option<Box<dyn AnisetteProvider>>,
}
impl AppleAccountBuilder {
@@ -25,6 +31,7 @@ impl AppleAccountBuilder {
Self {
email: email.to_string(),
debug: None,
anisette: None,
}
}
@@ -37,14 +44,28 @@ impl AppleAccountBuilder {
self
}
/// Build the AppleAccount
pub fn anisette(mut self, anisette: impl AnisetteProvider + 'static) -> Self {
self.anisette = Some(Box::new(anisette));
self
}
/// Build the AppleAccount and log in
///
/// # Arguments
/// - `password`: The Apple ID password
/// - `two_factor_callback`: A callback function that returns the two-factor authentication code
/// # Errors
/// Returns an error if the reqwest client cannot be built
pub fn login(self) -> Result<AppleAccount> {
pub async fn login<F>(self, _password: &str, _two_factor_callback: F) -> Result<AppleAccount>
where
F: Fn() -> Result<String>,
{
let debug = self.debug.unwrap_or(false);
let anisette = self
.anisette
.unwrap_or_else(|| Box::new(RemoteV3AnisetteProvider::default()));
AppleAccount::login(&self.email, debug)
AppleAccount::login(&self.email, debug, anisette).await
}
}
@@ -60,14 +81,27 @@ impl AppleAccount {
/// Log in to an Apple account with the given email
///
/// Reccomended to use the AppleAccountBuilder instead
pub fn login(email: &str, debug: bool) -> Result<Self> {
pub async fn login(
email: &str,
debug: bool,
anisette: Box<dyn AnisetteProvider>,
) -> Result<Self> {
info!("Logging in to apple ID: {}", email);
if debug {
warn!("Debug mode enabled: this is a security risk!");
}
let client = Self::build_client(debug)?;
let mut gs = GrandSlam::new(&client);
gs.get_url_bag()
.await
.context("Failed to get URL bag from GrandSlam")?;
Ok(AppleAccount {
email: email.to_string(),
spd: None,
client,
anisette: Box::new(crate::anisette::DefaultAnisetteProvider {}),
anisette,
})
}

View File

@@ -0,0 +1,64 @@
use crate::SideloadResult as Result;
use log::debug;
use plist::Dictionary;
use plist_macro::pretty_print_dictionary;
use reqwest::header::HeaderValue;
const URL_BAG: &str = "https://gsa.apple.com/grandslam/GsService2/lookup";
pub struct GrandSlam<'a> {
client: &'a reqwest::Client,
url_bag: Option<Dictionary>,
}
impl<'a> GrandSlam<'a> {
/// Create a new GrandSlam instance
///
/// # Arguments
/// - `client`: The reqwest client to use for requests
pub fn new(client: &'a reqwest::Client) -> Self {
Self {
client,
url_bag: None,
}
}
/// Get the URL bag from GrandSlam
pub async fn get_url_bag(&mut self) -> Result<&Dictionary> {
if self.url_bag.is_none() {
debug!("Fetching URL bag from GrandSlam");
let resp = self
.client
.get(URL_BAG)
.headers(Self::base_headers())
.send()
.await?
.text()
.await?;
let dict: Dictionary = plist::from_bytes(resp.as_bytes())?;
debug!("{}", pretty_print_dictionary(&dict));
self.url_bag = Some(dict);
}
Ok(self.url_bag.as_ref().unwrap())
}
fn base_headers() -> reqwest::header::HeaderMap {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert("Context-Type", HeaderValue::from_static("text/x-xml-plist"));
headers.insert("Accept", HeaderValue::from_static("text/x-xml-plist"));
headers.insert(
"X-Mme-Client-Info",
HeaderValue::from_static(
"<MacBookPro13,2> <macOS;13.1;22C65> <com.apple.AuthKit/1 (com.apple.dt.Xcode/3594.4.19)>",
),
);
headers.insert("User-Agent", HeaderValue::from_static("Xcode"));
headers.insert("X-Xcode-Version", HeaderValue::from_static("14.2 (14C18)"));
headers.insert(
"X-Apple-App-Info",
HeaderValue::from_static("com.apple.gs.xcode.auth"),
);
headers
}
}

View File

@@ -1 +1,2 @@
pub mod apple_account;
pub mod grandslam;

View File

@@ -1,12 +1,21 @@
use thiserror::Error as ThisError;
use thiserror_context::{Context, impl_context};
pub mod anisette;
pub mod auth;
#[derive(Debug, ThisError)]
pub enum Error {
#[error("Reqwest error: {0}")]
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),
}
pub type Result<T> = std::result::Result<T, Error>;
impl_context!(Error(ErrorInner));
pub type SideloadResult<T> = std::result::Result<T, Error>;