Git · dadoum.zone
Repositories
Help
Report an Issue
apple-crates
Code
Commits
Branches
Tags
Search
Tree:
f9cbefd
Branches
Tags
main
apple-crates
grandslam
src
grandslam
account.rs
Implement an icloud crate
Dadoum
commited
f9cbefd
at 2026-10-02 15:14:52
account.rs
Blame
History
Raw
use super::app::{AppToken, RawToken, Token, TokenBag}; use crate::grandslam::{ContinuationToken, build_client_provided_data}; use crate::http_session::{AnisetteHTTPSession, AppleError, parse_status}; use adi::proxy::{ADIError, ADIResult}; use aes::Aes256; use aes::cipher::consts::U16; use aes_gcm::aead::{Aead, Payload}; use aes_gcm::{AesGcm, KeyInit}; use hmac::{Hmac, Mac}; use log::warn; use plist::{Dictionary, Value}; use plist_macros::{array, dict}; use reqwest::{Method, RequestBuilder}; use serde::{Deserialize, Serialize}; use sha2::Sha256; use thiserror::Error; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(transparent)] pub struct AltDsid(pub(super) String); impl AltDsid { /// Read the account identifier without requiring any tokens. pub fn from_response_headers( headers: &reqwest::header::HeaderMap, ) -> Result<Option<AltDsid>, ResponseTokenError> { Ok(header_text(headers, "x-apple-alternate-id")?.map(AltDsid)) } } impl AsRef<str> for AltDsid { fn as_ref(&self) -> &str { &self.0 } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(transparent)] pub struct IdmsToken(pub(super) String); impl IdmsToken { /// Read the identity token without requiring app tokens or an account identifier. pub fn from_response_headers( headers: &reqwest::header::HeaderMap, ) -> Result<Option<IdmsToken>, ResponseTokenError> { let name = "x-apple-identity-token"; let mut values = headers.get_all(name).iter(); let token = values .next() .map(|value| header_token(value, name)) .transpose()?; if values.next().is_some() { return Err(ResponseTokenError::InvalidHeader(name)); } Ok(token.map(|(_, token)| IdmsToken(token.token))) } } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct SessionKey(Vec<u8>); #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct AuthCookie(Vec<u8>); /// Credentials used together by GrandSlam's `apptokens` operation. /// Kept separate from service tokens and the continuation token; never logged. #[derive(Clone, Serialize, Deserialize)] pub struct MasterToken { idms_token: IdmsToken, session_key: SessionKey, cookie: AuthCookie, } impl MasterToken { pub fn new(idms_token: IdmsToken, session_key: SessionKey, cookie: AuthCookie) -> Self { Self { idms_token, session_key, cookie, } } /// Web authentication replaces all three credentials together. pub fn from_response_headers( headers: &reqwest::header::HeaderMap, ) -> Result<Self, ResponseTokenError> { use base64::{Engine, prelude::BASE64_STANDARD}; let name = "x-apple-identity-token"; let mut values = headers.get_all(name).iter(); let value = values .next() .ok_or(ResponseTokenError::InvalidHeader(name))?; if values.next().is_some() { return Err(ResponseTokenError::InvalidHeader(name)); } let (_, token) = header_token(value, name)?; let decode = |name| { let value = header_text(headers, name)?.ok_or(ResponseTokenError::InvalidHeader(name))?; BASE64_STANDARD .decode(value) .map_err(|_| ResponseTokenError::InvalidHeader(name)) }; let session_key = decode("x-apple-session-key")?; let cookie = decode("x-apple-encrypted-session-key")?; if session_key.len() != 32 { return Err(ResponseTokenError::InvalidHeader("x-apple-session-key")); } if cookie.is_empty() { return Err(ResponseTokenError::InvalidHeader( "x-apple-encrypted-session-key", )); } Ok(Self { idms_token: IdmsToken(token.token), session_key: SessionKey(session_key), cookie: AuthCookie(cookie), }) } } #[derive(Debug, Clone, Deserialize)] #[serde(transparent)] pub struct ServerProvidedData(pub(super) Dictionary); impl ServerProvidedData { pub fn alt_dsid(&self) -> Option<AltDsid> { Some(AltDsid(self.0.get("adsid")?.as_string()?.to_owned())) } pub fn idms_token(&self) -> Option<IdmsToken> { Some(IdmsToken( self.0.get("GsIdmsToken")?.as_string()?.to_owned(), )) } pub fn session_key(&self) -> Option<SessionKey> { Some(SessionKey(self.0.get("sk")?.as_data()?.to_vec())) } pub fn cookie(&self) -> Option<AuthCookie> { Some(AuthCookie(self.0.get("c")?.as_data()?.to_vec())) } pub fn master_token(&self) -> Option<MasterToken> { let token = MasterToken { idms_token: self.idms_token()?, session_key: self.session_key()?, cookie: self.cookie()?, }; if token.idms_token.0.is_empty() || token.session_key.0.len() != 32 || token.cookie.0.is_empty() { return None; } Some(token) } pub fn continuation_token(&self) -> Option<ContinuationToken> { Some(ContinuationToken(self.0.get("ck")?.as_string()?.to_owned())) } pub fn tokens(&self) -> Option<TokenBag> { Some(TokenBag(self.0.get("t")?.as_dictionary()?.clone())) } } #[derive(Debug, Error)] pub enum AppTokenRequestError { #[error("Cannot proceed: {0}")] Apple(#[from] AppleError), #[error("Cannot generate device authentication data: {0}")] Anisette(#[from] ADIError), #[error("The provided authentication token is not valid")] InvalidAuthToken, // vvvv Internal errors vvvv #[error("Invalid URL bag")] InvalidURLBag, #[error("Network error: {0}")] Network(#[from] reqwest::Error), #[error("Failed to parse the app token request response: {0}")] Parsing(#[from] plist::Error), #[error("Failed to parse the app token request response")] Structure(Dictionary), #[error("Invalid token returned by the server")] InvalidResponse(Dictionary), } #[derive(Clone)] pub struct AccountHTTPSession<'lt, 'adi> { pub http_session: AnisetteHTTPSession<'lt, 'adi>, pub alt_dsid: AltDsid, } impl<'lt, 'adi> AccountHTTPSession<'lt, 'adi> { pub fn new(http_session: AnisetteHTTPSession<'lt, 'adi>, alt_dsid: AltDsid) -> Self { Self { http_session, alt_dsid, } } pub fn simple_request_builder(&self, method: Method, url: &str) -> RequestBuilder { self.http_session.simple_request_builder(method, url) } pub fn anisette_request_builder(&self, method: Method, url: &str) -> ADIResult<RequestBuilder> { self.http_session.anisette_request_builder(method, url) } /// Adds Anisette and the account ID; the endpoint's credential is supplied separately. pub fn account_request_builder(&self, method: Method, url: &str) -> ADIResult<RequestBuilder> { Ok(self .anisette_request_builder(method, url)? .header("X-Apple-I-Identity-Id", &self.alt_dsid.0)) } pub async fn get_app_token<T: AppToken>( &self, master: &MasterToken, ) -> Result<Token<T>, AppTokenRequestError> { Ok(self .get_app_token_for_service(T::APP_TOKEN_IDENTIFIER, master) .await? .into()) } /// Request a token for a service identifier supplied at runtime. pub async fn get_app_token_for_service( &self, app_token_identifier: &str, master: &MasterToken, ) -> Result<RawToken, AppTokenRequestError> { let MasterToken { idms_token, session_key, cookie, } = master; let alt_dsid = self.alt_dsid.0.as_str(); let gs_service_url = self .http_session .url_bag() .get("gsService") .and_then(Value::as_string) .ok_or(AppTokenRequestError::InvalidURLBag)?; let cpd = build_client_provided_data(&self.http_session) .map_err(AppTokenRequestError::Anisette)?; let checksum = Hmac::<Sha256>::new_from_slice(&session_key.0) .map_err(|_| AppTokenRequestError::InvalidAuthToken)? .chain_update("apptokens") .chain_update(alt_dsid) .chain_update(app_token_identifier) .finalize() .into_bytes() .to_vec(); let request_plist = dict! { "Header": dict!{ "Version": "1.0.1" }, "Request": dict!{ "u": alt_dsid, "app": array![ app_token_identifier ], "c": Value::Data(cookie.0.to_vec()), "t": idms_token.0.as_str(), "checksum": Value::Data(checksum), "cpd": cpd, "o": "apptokens", } }; let mut request_body = Vec::new(); plist::to_writer_xml(&mut request_body, &request_plist).expect("Serializing plist failed?"); let response = self .anisette_request_builder(Method::POST, gs_service_url) .map_err(AppTokenRequestError::Anisette)? .header("Content-Type", "text/x-xml-plist") .body(request_body) .send() .await .map_err(AppTokenRequestError::Network)? .error_for_status()? .bytes() .await .map_err(AppTokenRequestError::Network)?; let response_plist: Dictionary = plist::from_bytes(&response).map_err(AppTokenRequestError::Parsing)?; let response_dict = response_plist .get("Response") .and_then(|response| response.as_dictionary()) .ok_or_else(|| AppTokenRequestError::Structure(response_plist.clone()))?; let status = response_dict .get("Status") .and_then(|status| status.as_dictionary()) .ok_or_else(|| AppTokenRequestError::Structure(response_plist.clone()))?; parse_status(status)?; let encrypted_tokens = response_dict .get("et") .and_then(|et| et.as_data()) .ok_or_else(|| AppTokenRequestError::Structure(response_plist.clone()))?; if encrypted_tokens.len() < 19 { return Err(AppTokenRequestError::InvalidResponse(response_plist)); } let associated_data = &encrypted_tokens[0..3]; if associated_data != b"XYZ" { warn!("Surprised by the associated data provided by Apple: {associated_data:02X?}"); // return Err(AppTokenRequestError::InvalidResponse); } let iv = &encrypted_tokens[3..19]; let encrypted_token = &encrypted_tokens[19..]; let tokens_data = AesGcm::<Aes256, U16>::new_from_slice(&session_key.0) .map_err(|_| AppTokenRequestError::InvalidAuthToken)? .decrypt( // iv is of fixed size. It shall not fail. iv.try_into().expect("Invalid IV size??"), Payload { msg: encrypted_token, aad: associated_data, }, ) .map_err(|_| AppTokenRequestError::InvalidResponse(response_plist.clone()))?; let tokens: Dictionary = plist::from_bytes(&tokens_data).map_err(AppTokenRequestError::Parsing)?; let tokens = ServerProvidedData(tokens); tokens .tokens() .and_then(|tokens| { tokens .0 .get(app_token_identifier) .and_then(|token| plist::from_value(token).ok()) }) .ok_or_else(|| AppTokenRequestError::InvalidResponse(tokens.0)) } } pub(super) fn header_text( headers: &reqwest::header::HeaderMap, name: &'static str, ) -> Result<Option<String>, ResponseTokenError> { let mut values = headers.get_all(name).iter(); let value = values .next() .map(|value| { value .to_str() .map(str::to_owned) .map_err(|_| ResponseTokenError::InvalidHeader(name)) }) .transpose()?; if values.next().is_some() || value.as_ref().is_some_and(String::is_empty) { return Err(ResponseTokenError::InvalidHeader(name)); } Ok(value) } pub(super) fn header_token( value: &reqwest::header::HeaderValue, name: &'static str, ) -> Result<(String, RawToken), ResponseTokenError> { use base64::{Engine, prelude::BASE64_STANDARD}; let bytes = BASE64_STANDARD .decode(value.as_bytes()) .map_err(|_| ResponseTokenError::InvalidHeader(name))?; let value = std::str::from_utf8(&bytes).map_err(|_| ResponseTokenError::InvalidHeader(name))?; let fields: Vec<_> = value.split(':').collect(); if fields.len() != 4 || fields[0].is_empty() || fields[1].is_empty() { return Err(ResponseTokenError::InvalidHeader(name)); } let duration: u64 = fields[2] .parse() .map_err(|_| ResponseTokenError::InvalidHeader(name))?; let issued: u64 = fields[3] .parse() .map_err(|_| ResponseTokenError::InvalidHeader(name))?; let expiry_epoch_millis = duration .checked_mul(1000) .and_then(|duration| issued.checked_add(duration)) .ok_or(ResponseTokenError::InvalidHeader(name))?; Ok(( fields[0].to_owned(), RawToken { token: fields[1].to_owned(), duration, expiry_epoch_millis, }, )) } #[derive(Debug, Error)] pub enum ResponseTokenError { #[error("Invalid authentication response header: {0}")] InvalidHeader(&'static str), #[error("Duplicate service token in authentication response")] DuplicateService, }