mirror of
https://github.com/nab138/isideload.git
synced 2026-03-02 14:36:16 +01:00
Keep working on anisette
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
pub mod remote_v3;
|
||||
|
||||
pub trait AnisetteProvider {}
|
||||
|
||||
// tmp
|
||||
pub struct DefaultAnisetteProvider {}
|
||||
|
||||
impl AnisetteProvider for DefaultAnisetteProvider {}
|
||||
|
||||
92
isideload/src/anisette/remote_v3.rs
Normal file
92
isideload/src/anisette/remote_v3.rs
Normal 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 {}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
64
isideload/src/auth/grandslam.rs
Normal file
64
isideload/src/auth/grandslam.rs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod apple_account;
|
||||
pub mod grandslam;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user