Impliment getting app token

This commit is contained in:
nab138
2026-01-26 22:24:57 -05:00
parent f1790cec3d
commit 3b3f631315
9 changed files with 283 additions and 28 deletions

View File

@@ -38,3 +38,4 @@ pbkdf2 = "0.12.2"
hmac = "0.12.1"
cbc = { version = "0.1.2", features = ["std"] }
aes = "0.8.4"
aes-gcm = "0.10.3"

View File

@@ -4,7 +4,7 @@ use std::fs;
use std::path::PathBuf;
use base64::prelude::*;
use chrono::{SubsecRound, Utc};
// use chrono::{SubsecRound, Utc};
use plist_macro::plist;
use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue};
use rootcause::prelude::*;

View File

@@ -3,10 +3,14 @@ use crate::{
auth::grandslam::{GrandSlam, GrandSlamErrorChecker},
util::plist::PlistDataExtract,
};
use aes::cipher::block_padding::Pkcs7;
use aes::{
Aes256,
cipher::{block_padding::Pkcs7, consts::U16},
};
use aes_gcm::{AeadInPlace, AesGcm, KeyInit, Nonce};
use base64::{Engine, prelude::BASE64_STANDARD};
use cbc::cipher::{BlockDecryptMut, KeyIvInit};
use hmac::{Hmac, Mac};
use hmac::Mac;
use plist::Dictionary;
use plist_macro::plist;
use reqwest::header::{HeaderMap, HeaderValue};
@@ -563,12 +567,93 @@ impl AppleAccount {
Ok(LoginState::LoggedIn)
}
fn create_session_key(usr: &SrpClientVerifier<Sha256>, name: &str) -> Result<Vec<u8>, Report> {
Ok(Hmac::<Sha256>::new_from_slice(&usr.key())?
.chain_update(name.as_bytes())
pub async fn get_app_token(&mut self, app: &str) -> Result<Dictionary, Report> {
let app = if app.contains("com.apple.gs.") {
app.to_string()
} else {
format!("com.apple.gs.{}", app)
};
let spd = self
.spd
.as_ref()
.ok_or_else(|| report!("SPD data not available, cannot get app token"))?;
let dsid = spd.get_str("adsid").context("Failed to get app token")?;
let auth_token = spd
.get_str("GsIdmsToken")
.context("Failed to get app token")?;
let session_key = spd.get_data("sk").context("Failed to get app token")?;
let c = spd.get_data("c").context("Failed to get app token")?;
let checksum = <hmac::Hmac<Sha256> as hmac::Mac>::new_from_slice(session_key)
.unwrap()
.chain_update("apptokens".as_bytes())
.chain_update(dsid.as_bytes())
.chain_update(app.as_bytes())
.finalize()
.into_bytes()
.to_vec())
.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 request = plist!(dict {
"Header": {
"Version": "1.0.1"
},
"Request": {
"app": [app],
"c": c,
"checksum": checksum,
"cpd": cpd,
"o": "apptokens",
"u": dsid,
"t": auth_token
}
});
let resp = self
.grandslam_client
.plist_request(&gs_service_url, &request, None)
.await
.context("Failed to send app token request")?
.check_grandslam_error()
.context("GrandSlam error during app token request")?;
let encrypted_token = resp
.get_data("et")
.context("Failed to get encrypted token")?;
let decrypted_token = Self::decrypt_gcm(&encrypted_token, &session_key)
.context("Failed to decrypt app token")?;
let token: Dictionary = plist::from_bytes(&decrypted_token)
.context("Failed to parse decrypted app token plist")?;
let status = token
.get_signed_integer("status-code")
.context("Failed to get status code from app token")?;
if status != 200 {
bail!("App token request failed with status code {}", status);
}
let token_dict = token
.get_dict("t")
.context("Failed to get token dictionary from app token")?;
Ok(token_dict.clone())
}
fn create_session_key(usr: &SrpClientVerifier<Sha256>, name: &str) -> Result<Vec<u8>, Report> {
Ok(
<hmac::Hmac<Sha256> as hmac::Mac>::new_from_slice(&usr.key())?
.chain_update(name.as_bytes())
.finalize()
.into_bytes()
.to_vec(),
)
}
fn decrypt_cbc(usr: &SrpClientVerifier<Sha256>, data: &[u8]) -> Result<Vec<u8>, Report> {
@@ -581,6 +666,43 @@ impl AppleAccount {
.decrypt_padded_vec_mut::<Pkcs7>(&data)?,
)
}
fn decrypt_gcm(data: &[u8], key: &[u8]) -> Result<Vec<u8>, Report> {
if data.len() < 3 + 16 + 16 {
bail!(
"Encrypted token is too short to be valid (only {} bytes)",
data.len()
);
}
let header = &data[0..3];
if header != b"XYZ" {
bail!(
"Encrypted token is in an unknown format: {}",
String::from_utf8_lossy(header)
);
}
let iv = &data[3..19];
let ciphertext_and_tag = &data[19..];
if key.len() != 32 {
bail!("Session key is not the correct length: {} bytes", key.len());
}
if iv.len() != 16 {
bail!("IV is not the correct length: {} bytes", iv.len());
}
let key = aes_gcm::Key::<AesGcm<Aes256, U16>>::from_slice(key);
let cipher = AesGcm::<Aes256, U16>::new(key);
let nonce = Nonce::<U16>::from_slice(iv);
let mut buf = ciphertext_and_tag.to_vec();
cipher
.decrypt_in_place(nonce, header, &mut buf)
.map_err(|e| report!("Failed to decrypt gcm: {}", e))?;
Ok(buf)
}
}
impl std::fmt::Display for AppleAccount {

View File

@@ -8,6 +8,7 @@ use rootcause::prelude::*;
use tracing::debug;
use crate::{
SideloadError,
anisette::AnisetteClientInfo,
util::plist::{PlistDataExtract, plist_to_xml_string},
};
@@ -166,24 +167,18 @@ impl GrandSlam {
}
pub trait GrandSlamErrorChecker {
fn check_grandslam_error(self) -> Result<Dictionary, Report<GrandSlamError>>;
}
#[derive(Debug, thiserror::Error)]
pub enum GrandSlamError {
#[error("Auth error {0}: {1}")]
AuthWithMessage(i64, String),
fn check_grandslam_error(self) -> Result<Dictionary, Report<SideloadError>>;
}
impl GrandSlamErrorChecker for Dictionary {
fn check_grandslam_error(self) -> Result<Self, Report<GrandSlamError>> {
fn check_grandslam_error(self) -> Result<Self, Report<SideloadError>> {
let result = match self.get("Status") {
Some(plist::Value::Dictionary(d)) => d,
_ => &self,
};
if result.get_signed_integer("ec").unwrap_or(0) != 0 {
bail!(GrandSlamError::AuthWithMessage(
bail!(SideloadError::AuthWithMessage(
result.get_signed_integer("ec").unwrap_or(-1),
result.get_str("em").unwrap_or("Unknown error").to_string(),
))

View File

@@ -7,6 +7,15 @@ pub mod anisette;
pub mod auth;
pub mod util;
#[derive(Debug, thiserror::Error)]
pub enum SideloadError {
#[error("Auth error {0}: {1}")]
AuthWithMessage(i64, String),
#[error("Plist parse error: {0}")]
PlistParseError(String),
}
struct ReqwestErrorFormatter;
impl ContextFormatterHook<reqwest::Error> for ReqwestErrorFormatter {

View File

@@ -1,7 +1,8 @@
use plist::Dictionary;
use plist_macro::{plist_to_xml_bytes, plist_value_to_xml_bytes, pretty_print_dictionary};
use rootcause::prelude::*;
pub fn plist_to_xml_string(p: &plist::Dictionary) -> String {
pub fn plist_to_xml_string(p: &Dictionary) -> String {
String::from_utf8(plist_to_xml_bytes(p)).unwrap()
}
@@ -9,24 +10,59 @@ pub fn plist_value_to_xml_string(p: &plist::Value) -> String {
String::from_utf8(plist_value_to_xml_bytes(p)).unwrap()
}
pub struct SensitivePlistAttachment {
pub plist: Dictionary,
}
impl SensitivePlistAttachment {
pub fn new(plist: Dictionary) -> Self {
SensitivePlistAttachment { plist }
}
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// if env variable DEBUG_SENSITIVE is set, print full plist
if std::env::var("DEBUG_SENSITIVE").is_ok() {
return writeln!(f, "{}", pretty_print_dictionary(&self.plist));
}
writeln!(
f,
"<Potentially sensitive data - set DEBUG_SENSITIVE env variable to see contents>"
)
}
}
impl std::fmt::Display for SensitivePlistAttachment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.fmt(f)
}
}
impl std::fmt::Debug for SensitivePlistAttachment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.fmt(f)
}
}
pub trait PlistDataExtract {
fn get_data(&self, key: &str) -> Result<&[u8], Report>;
fn get_str(&self, key: &str) -> Result<&str, Report>;
fn get_string(&self, key: &str) -> Result<String, Report>;
fn get_signed_integer(&self, key: &str) -> Result<i64, Report>;
fn get_dict(&self, key: &str) -> Result<&plist::Dictionary, Report>;
fn get_dict(&self, key: &str) -> Result<&Dictionary, Report>;
}
impl PlistDataExtract for plist::Dictionary {
impl PlistDataExtract for Dictionary {
fn get_data(&self, key: &str) -> Result<&[u8], Report> {
self.get(key).and_then(|v| v.as_data()).ok_or_else(|| {
report!("Plist missing data for key '{}'", key).attach(pretty_print_dictionary(self))
report!("Plist missing data for key '{}'", key)
.attach(SensitivePlistAttachment::new(self.clone()))
})
}
fn get_str(&self, key: &str) -> Result<&str, Report> {
self.get(key).and_then(|v| v.as_string()).ok_or_else(|| {
report!("Plist missing string for key '{}'", key).attach(pretty_print_dictionary(self))
report!("Plist missing string for key '{}'", key)
.attach(SensitivePlistAttachment::new(self.clone()))
})
}
@@ -36,7 +72,7 @@ impl PlistDataExtract for plist::Dictionary {
.map(|s| s.to_string())
.ok_or_else(|| {
report!("Plist missing string for key '{}'", key)
.attach(pretty_print_dictionary(self))
.attach(SensitivePlistAttachment::new(self.clone()))
})
}
@@ -45,16 +81,16 @@ impl PlistDataExtract for plist::Dictionary {
.and_then(|v| v.as_signed_integer())
.ok_or_else(|| {
report!("Plist missing signed integer for key '{}'", key)
.attach(pretty_print_dictionary(self))
.attach(SensitivePlistAttachment::new(self.clone()))
})
}
fn get_dict(&self, key: &str) -> Result<&plist::Dictionary, Report> {
fn get_dict(&self, key: &str) -> Result<&Dictionary, Report> {
self.get(key)
.and_then(|v| v.as_dictionary())
.ok_or_else(|| {
report!("Plist missing dictionary for key '{}'", key)
.attach(pretty_print_dictionary(self))
.attach(SensitivePlistAttachment::new(self.clone()))
})
}
}