Developer API implimentation

This commit is contained in:
nab138
2026-01-27 23:00:30 -05:00
parent 0b69a0b238
commit aee5eaf26e
13 changed files with 271 additions and 204 deletions

View File

@@ -20,11 +20,10 @@ plist = "1.8"
plist-macro = "0.1.3"
reqwest = { version = "0.13.1", features = ["json", "gzip"] }
thiserror = "2.0.17"
chrono = "0.4.43"
async-trait = "0.1.89"
serde = "1.0.228"
rand = "0.9.2"
uuid = "1.19.0"
uuid = {version = "1.20.0", features = ["v4"] }
sha2 = "0.10.9"
tracing = "0.1.44"
tokio-tungstenite = { version = "0.28.0", features = ["rustls-tls-webpki-roots"] }

View File

@@ -1,7 +1,6 @@
pub mod remote_v3;
use crate::auth::grandslam::GrandSlam;
use chrono::{DateTime, SubsecRound, Utc};
use plist::Dictionary;
use plist_macro::plist;
use reqwest::header::HeaderMap;
@@ -20,14 +19,15 @@ pub struct AnisetteData {
machine_id: String,
one_time_password: String,
pub routing_info: String,
device_description: String,
_device_description: String,
device_unique_identifier: String,
local_user_id: String,
_local_user_id: String,
}
// Some headers don't seem to be required. I guess not including them is technically more efficient soooo
impl AnisetteData {
pub fn get_headers(&self, serial: String) -> HashMap<String, String> {
// let dt: DateTime<Utc> = Utc::now().round_subsecs(0);
pub fn get_headers(&self) -> HashMap<String, String> {
//let dt: DateTime<Utc> = Utc::now().round_subsecs(0);
HashMap::from_iter(
[
@@ -55,8 +55,8 @@ impl AnisetteData {
)
}
pub fn get_header_map(&self, serial: String) -> HeaderMap {
let headers_map = self.get_headers(serial);
pub fn get_header_map(&self) -> HeaderMap {
let headers_map = self.get_headers();
let mut header_map = HeaderMap::new();
for (key, value) in headers_map {
@@ -69,8 +69,8 @@ impl AnisetteData {
header_map
}
pub fn get_client_provided_data(&self, serial: String) -> Dictionary {
let headers = self.get_headers(serial);
pub fn get_client_provided_data(&self) -> Dictionary {
let headers = self.get_headers();
let mut cpd = plist!(dict {
"bootstrap": "true",

View File

@@ -4,7 +4,6 @@ use std::fs;
use std::path::PathBuf;
use base64::prelude::*;
// use chrono::{SubsecRound, Utc};
use plist_macro::plist;
use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue};
use rootcause::prelude::*;
@@ -110,9 +109,9 @@ impl AnisetteProvider for RemoteV3AnisetteProvider {
machine_id,
one_time_password,
routing_info,
device_description: client_info,
_device_description: client_info,
device_unique_identifier: state.get_device_id(),
local_user_id: hex::encode(&state.get_md_lu()),
_local_user_id: hex::encode(&state.get_md_lu()),
};
Ok(data)

View File

@@ -1,6 +1,9 @@
use crate::{
anisette::{AnisetteData, AnisetteProvider, remote_v3::RemoteV3AnisetteProvider},
auth::grandslam::{GrandSlam, GrandSlamErrorChecker},
anisette::{AnisetteData, AnisetteProvider},
auth::{
builder::AppleAccountBuilder,
grandslam::{GrandSlam, GrandSlamErrorChecker},
},
util::plist::PlistDataExtract,
};
use aes::{
@@ -22,8 +25,6 @@ use srp::{
};
use tracing::{debug, info, warn};
const SERIAL_NUMBER: &str = "2";
pub struct AppleAccount {
pub email: String,
pub spd: Option<plist::Dictionary>,
@@ -34,12 +35,6 @@ pub struct AppleAccount {
debug: bool,
}
pub struct AppleAccountBuilder {
email: String,
debug: Option<bool>,
anisette_provider: Option<Box<dyn AnisetteProvider>>,
}
#[derive(Debug)]
pub enum LoginState {
LoggedIn,
@@ -49,67 +44,6 @@ pub enum LoginState {
NeedsLogin,
}
impl AppleAccountBuilder {
/// Create a new AppleAccountBuilder with the given email
///
/// # Arguments
/// - `email`: The Apple ID email address
pub fn new(email: &str) -> Self {
Self {
email: email.to_string(),
debug: None,
anisette_provider: None,
}
}
/// DANGER Set whether to enable debug mode
///
/// # Arguments
/// - `debug`: If true, accept invalid certificates and enable verbose connection logging
pub fn danger_debug(mut self, debug: bool) -> Self {
self.debug = Some(debug);
self
}
pub fn anisette_provider(mut self, anisette_provider: impl AnisetteProvider + 'static) -> Self {
self.anisette_provider = Some(Box::new(anisette_provider));
self
}
/// Build the AppleAccount without logging in
///
/// # Errors
/// Returns an error if the reqwest client cannot be built
pub async fn build(self) -> Result<AppleAccount, Report> {
let debug = self.debug.unwrap_or(false);
let anisette_provider = self
.anisette_provider
.unwrap_or_else(|| Box::new(RemoteV3AnisetteProvider::default()));
AppleAccount::new(&self.email, anisette_provider, debug).await
}
/// 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 async fn login<F>(
self,
password: &str,
two_factor_callback: F,
) -> Result<AppleAccount, Report>
where
F: Fn() -> Option<String>,
{
let mut account = self.build().await?;
account.login(password, two_factor_callback).await?;
Ok(account)
}
}
impl AppleAccount {
/// Create a new AppleAccountBuilder with the given email
///
@@ -395,7 +329,7 @@ impl AppleAccount {
}
async fn build_2fa_headers(&mut self) -> Result<HeaderMap, Report> {
let mut headers = self.anisette_data.get_header_map(SERIAL_NUMBER.to_string());
let mut headers = self.anisette_data.get_header_map();
let spd = self
.spd
@@ -427,9 +361,7 @@ impl AppleAccount {
debug!("GrandSlam service URL: {}", gs_service_url);
let cpd = self
.anisette_data
.get_client_provided_data(SERIAL_NUMBER.to_string());
let cpd = self.anisette_data.get_client_provided_data();
let srp_client = SrpClient::<Sha256>::new(&G_2048);
let a: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
@@ -567,7 +499,7 @@ impl AppleAccount {
Ok(LoginState::LoggedIn)
}
pub async fn get_app_token(&mut self, app: &str) -> Result<Dictionary, Report> {
pub async fn get_app_token(&mut self, app: &str) -> Result<AppToken, Report> {
let app = if app.contains("com.apple.gs.") {
app.to_string()
} else {
@@ -596,16 +528,14 @@ impl AppleAccount {
.to_vec();
let gs_service_url = self.grandslam_client.get_url("gsService").await?;
let cpd = self
.anisette_data
.get_client_provided_data(SERIAL_NUMBER.to_string());
let cpd = self.anisette_data.get_client_provided_data();
let request = plist!(dict {
"Header": {
"Version": "1.0.1"
},
"Request": {
"app": [app],
"app": [app.clone()],
"c": c,
"checksum": checksum,
"cpd": cpd,
@@ -642,8 +572,24 @@ impl AppleAccount {
let token_dict = token
.get_dict("t")
.context("Failed to get token dictionary from app token")?;
let app_token = token_dict
.get_dict(&app)
.context("Failed to get app token string")?;
Ok(token_dict.clone())
let app_token = AppToken {
token: app_token
.get_str("token")
.context("Failed to get app token string")?
.to_string(),
duration: app_token
.get_signed_integer("duration")
.context("Failed to get app token duration")? as u64,
expiry: app_token
.get_signed_integer("expiry")
.context("Failed to get app token expiry")? as u64,
};
Ok(app_token)
}
fn create_session_key(usr: &SrpClientVerifier<Sha256>, name: &str) -> Result<Vec<u8>, Report> {
@@ -715,3 +661,10 @@ impl std::fmt::Display for AppleAccount {
write!(f, "{} ({:?})", self.email, self.login_state)
}
}
#[derive(Debug)]
pub struct AppToken {
pub token: String,
pub duration: u64,
pub expiry: u64,
}

View File

@@ -0,0 +1,73 @@
use rootcause::prelude::*;
use crate::{
anisette::{AnisetteProvider, remote_v3::RemoteV3AnisetteProvider},
auth::apple_account::AppleAccount,
};
pub struct AppleAccountBuilder {
email: String,
debug: Option<bool>,
anisette_provider: Option<Box<dyn AnisetteProvider>>,
}
impl AppleAccountBuilder {
/// Create a new AppleAccountBuilder with the given email
///
/// # Arguments
/// - `email`: The Apple ID email address
pub fn new(email: &str) -> Self {
Self {
email: email.to_string(),
debug: None,
anisette_provider: None,
}
}
/// DANGER Set whether to enable debug mode
///
/// # Arguments
/// - `debug`: If true, accept invalid certificates and enable verbose connection logging
pub fn danger_debug(mut self, debug: bool) -> Self {
self.debug = Some(debug);
self
}
pub fn anisette_provider(mut self, anisette_provider: impl AnisetteProvider + 'static) -> Self {
self.anisette_provider = Some(Box::new(anisette_provider));
self
}
/// Build the AppleAccount without logging in
///
/// # Errors
/// Returns an error if the reqwest client cannot be built
pub async fn build(self) -> Result<AppleAccount, Report> {
let debug = self.debug.unwrap_or(false);
let anisette_provider = self
.anisette_provider
.unwrap_or_else(|| Box::new(RemoteV3AnisetteProvider::default()));
AppleAccount::new(&self.email, anisette_provider, debug).await
}
/// 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 async fn login<F>(
self,
password: &str,
two_factor_callback: F,
) -> Result<AppleAccount, Report>
where
F: Fn() -> Option<String>,
{
let mut account = self.build().await?;
account.login(password, two_factor_callback).await?;
Ok(account)
}
}

View File

@@ -134,15 +134,15 @@ impl GrandSlam {
"X-Mme-Client-Info",
HeaderValue::from_str(&self.client_info.client_info)?,
);
// headers.insert(
// "User-Agent",
// HeaderValue::from_str(&self.client_info.user_agent)?,
// );
// 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.insert(
"User-Agent",
HeaderValue::from_str(&self.client_info.user_agent)?,
);
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"),
);
Ok(headers)
}

View File

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

View File

@@ -1,13 +1,98 @@
use std::sync::Arc;
use plist::Dictionary;
use plist_macro::plist;
use rootcause::prelude::*;
use uuid::Uuid;
use crate::auth::apple_account::AppleAccount;
use crate::{
anisette::AnisetteData,
auth::{
apple_account::{AppToken, AppleAccount},
grandslam::GrandSlam,
},
dev::device_type::DeveloperDeviceType,
util::plist::{PlistDataExtract, plist_to_xml_string},
};
struct DeveloperSession {
apple_account: Arc<AppleAccount>,
pub struct DeveloperSession<'a> {
token: AppToken,
adsid: String,
client: &'a GrandSlam,
anisette_data: &'a AnisetteData,
}
impl DeveloperSession {
pub fn new(apple_account: Arc<AppleAccount>) -> Self {
DeveloperSession { apple_account }
impl<'a> DeveloperSession<'a> {
pub fn new(
token: AppToken,
adsid: String,
client: &'a GrandSlam,
anisette_data: &'a AnisetteData,
) -> Self {
DeveloperSession {
token,
adsid,
client,
anisette_data,
}
}
pub async fn from_account(account: &'a mut AppleAccount) -> Result<Self, Report> {
let token = account
.get_app_token("xcode.auth")
.await
.context("Failed to get xcode token from Apple account")?;
let spd = account
.spd
.as_ref()
.ok_or_else(|| report!("SPD not available, cannot get adsid"))?;
Ok(DeveloperSession::new(
token,
spd.get_string("adsid")?,
&account.grandslam_client,
&account.anisette_data,
))
}
pub async fn send_developer_request(
&self,
url: &str,
body: Option<Dictionary>,
) -> Result<Dictionary, Report> {
let body = body.unwrap_or_else(|| Dictionary::new());
let base = plist!(dict {
"clientId": "XABBG36SBA",
"protocolVersion": "QH65B2",
"requestId": Uuid::new_v4().to_string().to_uppercase(),
"userLocale": ["en_US"],
});
let body = base.into_iter().chain(body.into_iter()).collect();
let text = self
.client
.post(url)?
.body(plist_to_xml_string(&body))
.header("X-Apple-GS-Token", &self.token.token)
.header("X-Apple-I-Identity-Id", &self.adsid)
.headers(self.anisette_data.get_header_map())
.send()
.await?
.error_for_status()
.context("Developer request failed")?
.text()
.await
.context("Failed to read developer request response text")?;
let dict: Dictionary = plist::from_bytes(text.as_bytes())
.context("Failed to parse developer request plist")?;
Ok(dict)
}
pub async fn list_teams(&self) -> Result<Dictionary, Report> {
self.send_developer_request(&DeveloperDeviceType::Any.dev_url("listTeams"), None)
.await
}
}

View File

@@ -0,0 +1,26 @@
#[derive(Debug, Clone)]
pub enum DeveloperDeviceType {
Any,
Ios,
Tvos,
Watchos,
}
impl DeveloperDeviceType {
pub fn url_segment(&self) -> &'static str {
match self {
DeveloperDeviceType::Any => "",
DeveloperDeviceType::Ios => "ios/",
DeveloperDeviceType::Tvos => "tvos/",
DeveloperDeviceType::Watchos => "watchos/",
}
}
pub fn dev_url(&self, endpoint: &str) -> String {
format!(
"https://developerservices2.apple.com/services/QH65B2/{}{}.action?clientId=XABBG36SBA",
self.url_segment(),
endpoint,
)
}
}

View File

@@ -1 +1,2 @@
pub mod developer_session;
pub mod device_type;