Implement TSS support

This commit is contained in:
Jackson Coxson
2025-02-05 00:41:10 -07:00
parent afbcfa87a7
commit 732940581e
7 changed files with 544 additions and 12 deletions

View File

@@ -22,7 +22,7 @@ log = { version = "0.4" }
env_logger = { version = "0.11" } env_logger = { version = "0.11" }
indexmap = { version = "2.7", features = ["serde"], optional = true } indexmap = { version = "2.7", features = ["serde"], optional = true }
uuid = { version = "1.12", features = ["serde"], optional = true } uuid = { version = "1.12", features = ["serde", "v4"], optional = true }
async-recursion = { version = "1.1", optional = true } async-recursion = { version = "1.1", optional = true }
base64 = { version = "0.22", optional = true } base64 = { version = "0.22", optional = true }
@@ -30,6 +30,8 @@ serde_json = { version = "1", optional = true }
json = { version = "0.12", optional = true } json = { version = "0.12", optional = true }
byteorder = { version = "1.5", optional = true } byteorder = { version = "1.5", optional = true }
reqwest = { version = "0.12", features = ["json"], optional = true }
[features] [features]
core_device_proxy = ["dep:serde_json", "dep:json", "dep:byteorder"] core_device_proxy = ["dep:serde_json", "dep:json", "dep:byteorder"]
@@ -38,6 +40,7 @@ installation_proxy = []
mounter = [] mounter = []
usbmuxd = [] usbmuxd = []
tcp = ["tokio/net"] tcp = ["tokio/net"]
tss = ["dep:uuid", "dep:reqwest"]
xpc = [ xpc = [
"tokio/full", "tokio/full",
"dep:indexmap", "dep:indexmap",
@@ -53,6 +56,7 @@ full = [
"usbmuxd", "usbmuxd",
"xpc", "xpc",
"tcp", "tcp",
"tss",
] ]
# Why: https://github.com/rust-lang/cargo/issues/1197 # Why: https://github.com/rust-lang/cargo/issues/1197

View File

@@ -13,8 +13,11 @@ pub mod lockdownd;
pub mod mounter; pub mod mounter;
pub mod pairing_file; pub mod pairing_file;
pub mod provider; pub mod provider;
#[cfg(feature = "tss")]
pub mod tss;
#[cfg(feature = "usbmuxd")] #[cfg(feature = "usbmuxd")]
pub mod usbmuxd; pub mod usbmuxd;
mod util;
#[cfg(feature = "xpc")] #[cfg(feature = "xpc")]
pub mod xpc; pub mod xpc;
@@ -216,6 +219,13 @@ pub enum IdeviceError {
#[error("usb bad version")] #[error("usb bad version")]
UsbBadVersion, UsbBadVersion,
#[error("bad build manifest")]
BadBuildManifest,
#[cfg(feature = "tss")]
#[error("http reqwest error")]
Reqwest(#[from] reqwest::Error),
#[error("unknown error `{0}` returned from device")] #[error("unknown error `{0}` returned from device")]
UnknownErrorType(String), UnknownErrorType(String),
} }

View File

@@ -2,6 +2,7 @@
// Abstractions for lockdownd // Abstractions for lockdownd
use log::error; use log::error;
use plist::Value;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{pairing_file, Idevice, IdeviceError, IdeviceService}; use crate::{pairing_file, Idevice, IdeviceError, IdeviceService};
@@ -37,7 +38,7 @@ impl LockdowndClient {
pub fn new(idevice: Idevice) -> Self { pub fn new(idevice: Idevice) -> Self {
Self { idevice } Self { idevice }
} }
pub async fn get_value(&mut self, value: impl Into<String>) -> Result<String, IdeviceError> { pub async fn get_value(&mut self, value: impl Into<String>) -> Result<Value, IdeviceError> {
let req = LockdowndRequest { let req = LockdowndRequest {
label: self.idevice.label.clone(), label: self.idevice.label.clone(),
key: Some(value.into()), key: Some(value.into()),
@@ -47,7 +48,7 @@ impl LockdowndClient {
self.idevice.send_plist(message).await?; self.idevice.send_plist(message).await?;
let message: plist::Dictionary = self.idevice.read_plist().await?; let message: plist::Dictionary = self.idevice.read_plist().await?;
match message.get("Value") { match message.get("Value") {
Some(m) => Ok(plist::from_value(m)?), Some(m) => Ok(m.to_owned()),
None => Err(IdeviceError::UnexpectedResponse), None => Err(IdeviceError::UnexpectedResponse),
} }
} }

View File

@@ -1,6 +1,9 @@
// Jackson Coxson // Jackson Coxson
use crate::{lockdownd::LockdowndClient, Idevice, IdeviceError, IdeviceService}; use crate::{
lockdownd::LockdowndClient, tss::TSSRequest, util::hashmap_to_dictionary, Idevice,
IdeviceError, IdeviceService,
};
pub struct ImageMounter { pub struct ImageMounter {
idevice: Idevice, idevice: Idevice,
@@ -51,6 +54,26 @@ impl ImageMounter {
} }
} }
/// Looks up an image and returns the signature
pub async fn lookup_image(
&mut self,
image_type: impl Into<String>,
) -> Result<Vec<u8>, IdeviceError> {
let image_type = image_type.into();
let mut req = plist::Dictionary::new();
req.insert("Command".into(), "LookupImage".into());
req.insert("ImageType".into(), image_type.into());
self.idevice
.send_plist(plist::Value::Dictionary(req))
.await?;
let res = self.idevice.read_plist().await?;
match res.get("ImageSignature") {
Some(plist::Value::Data(signature)) => Ok(signature.clone()),
_ => Err(IdeviceError::NotFound),
}
}
pub async fn upload_image( pub async fn upload_image(
&mut self, &mut self,
image_type: impl Into<String>, image_type: impl Into<String>,
@@ -129,6 +152,28 @@ impl ImageMounter {
Ok(()) Ok(())
} }
/// Unmounts an image at a specified path.
/// Use ``/Developer`` for pre-iOS 17 developer images.
/// Use ``/System/Developer`` for personalized images.
pub async fn unmount_image(
&mut self,
mount_path: impl Into<String>,
) -> Result<(), IdeviceError> {
let mount_path = mount_path.into();
let mut req = plist::Dictionary::new();
req.insert("Command".into(), "UnmountImage".into());
req.insert("MountPath".into(), mount_path.into());
self.idevice
.send_plist(plist::Value::Dictionary(req))
.await?;
let res = self.idevice.read_plist().await?;
match res.get("Status") {
Some(plist::Value::String(s)) if s.as_str() == "Complete" => Ok(()),
_ => Err(IdeviceError::UnexpectedResponse),
}
}
/// Queries the personalization manifest from the device. /// Queries the personalization manifest from the device.
/// On failure, the socket must be closed and reestablished. /// On failure, the socket must be closed and reestablished.
pub async fn query_personalization_manifest( pub async fn query_personalization_manifest(
@@ -153,4 +198,334 @@ impl ImageMounter {
_ => Err(IdeviceError::NotFound), _ => Err(IdeviceError::NotFound),
} }
} }
pub async fn query_developer_mode_status(&mut self) -> Result<bool, IdeviceError> {
let mut req = plist::Dictionary::new();
req.insert("Command".into(), "QueryDeveloperModeStatus".into());
self.idevice
.send_plist(plist::Value::Dictionary(req))
.await?;
let res = self.idevice.read_plist().await?;
match res.get("DeveloperModeStatus") {
Some(plist::Value::Boolean(status)) => Ok(*status),
_ => Err(IdeviceError::UnexpectedResponse),
}
}
pub async fn query_nonce(
&mut self,
personalized_image_type: Option<String>,
) -> Result<Vec<u8>, IdeviceError> {
let mut req = plist::Dictionary::new();
req.insert("Command".into(), "QueryNonce".into());
if let Some(image_type) = personalized_image_type {
req.insert("PersonalizedImageType".into(), image_type.into());
}
self.idevice
.send_plist(plist::Value::Dictionary(req))
.await?;
let res = self.idevice.read_plist().await?;
match res.get("PersonalizationNonce") {
Some(plist::Value::Data(nonce)) => Ok(nonce.clone()),
_ => Err(IdeviceError::UnexpectedResponse),
}
}
pub async fn query_personalization_identifiers(
&mut self,
image_type: Option<String>,
) -> Result<plist::Dictionary, IdeviceError> {
let mut req = plist::Dictionary::new();
req.insert("Command".into(), "QueryPersonalizationIdentifiers".into());
if let Some(image_type) = image_type {
req.insert("PersonalizedImageType".into(), image_type.into());
}
self.idevice
.send_plist(plist::Value::Dictionary(req))
.await?;
let res = self.idevice.read_plist().await?;
match res.get("PersonalizationIdentifiers") {
Some(plist::Value::Dictionary(identifiers)) => Ok(identifiers.clone()),
_ => Err(IdeviceError::UnexpectedResponse),
}
}
pub async fn roll_personalization_nonce(&mut self) -> Result<(), IdeviceError> {
let mut req = plist::Dictionary::new();
req.insert("Command".into(), "RollPersonalizationNonce".into());
self.idevice
.send_plist(plist::Value::Dictionary(req))
.await?;
Ok(())
}
pub async fn roll_cryptex_nonce(&mut self) -> Result<(), IdeviceError> {
let mut req = plist::Dictionary::new();
req.insert("Command".into(), "RollCryptexNonce".into());
self.idevice
.send_plist(plist::Value::Dictionary(req))
.await?;
Ok(())
}
pub async fn mount_developer(
&mut self,
image: &[u8],
signature: Vec<u8>,
) -> Result<(), IdeviceError> {
self.upload_image("Developer", &image, signature.clone())
.await?;
self.mount_image(
"Developer",
signature,
Vec::new(),
plist::Value::Dictionary(plist::Dictionary::new()),
)
.await?;
Ok(())
}
pub async fn mount_personalized(
&mut self,
image: Vec<u8>,
trust_cache: Vec<u8>,
build_manifest: &[u8],
info_plist: Option<plist::Value>,
unique_chip_id: u64,
) -> Result<(), IdeviceError> {
// Try to fetch personalization manifest
let manifest = match self
.query_personalization_manifest("DeveloperDiskImage", image.clone()) // TODO:
.await
{
Ok(manifest) => manifest,
Err(IdeviceError::NotFound) => {
// Get manifest from TSS
let manifest_dict: plist::Dictionary = plist::from_bytes(build_manifest)?;
self.get_manifest_from_tss(&manifest_dict, unique_chip_id)
.await?
}
Err(e) => return Err(e),
};
self.upload_image("Personalized", &image, manifest.clone())
.await?;
let mut extras = plist::Dictionary::new();
if let Some(info) = info_plist {
extras.insert("ImageInfoPlist".into(), info);
}
extras.insert(
"ImageTrustCache".into(),
plist::Value::Data(trust_cache.clone()),
);
self.mount_image(
"Personalized",
manifest,
trust_cache,
plist::Value::Dictionary(extras),
)
.await?;
Ok(())
}
#[cfg(feature = "tss")]
pub async fn get_manifest_from_tss(
&mut self,
build_manifest: &plist::Dictionary,
unique_chip_id: u64,
) -> Result<Vec<u8>, IdeviceError> {
use log::{debug, warn};
let mut request = TSSRequest::new();
let personalization_identifiers = self.query_personalization_identifiers(None).await?;
for (key, val) in &personalization_identifiers {
if key.starts_with("Ap,") {
request.insert(key, val.clone());
}
}
let board_id = match personalization_identifiers.get("BoardId") {
Some(plist::Value::Integer(b)) => match b.as_unsigned() {
Some(b) => b,
None => return Err(IdeviceError::UnexpectedResponse),
},
_ => {
return Err(IdeviceError::UnexpectedResponse);
}
};
let chip_id = match personalization_identifiers.get("ChipID") {
Some(plist::Value::Integer(b)) => match b.as_unsigned() {
Some(b) => b,
None => return Err(IdeviceError::UnexpectedResponse),
},
_ => {
return Err(IdeviceError::UnexpectedResponse);
}
};
request.insert("@ApImg4Ticket", true);
request.insert("@BBTicket", true);
request.insert("ApBoardID", board_id);
request.insert("ApChipID", chip_id);
request.insert("ApECID", unique_chip_id);
request.insert(
"ApNonce",
plist::Value::Data(
self.query_nonce(Some("DeveloperDiskImage".to_string()))
.await?,
),
);
request.insert("ApProductionMode", true);
request.insert("ApSecurityDomain", 1);
request.insert("ApSecurityMode", true);
request.insert("SepNonce", plist::Value::Data(vec![0; 20]));
request.insert("UID_MODE", false);
let identities = match build_manifest.get("BuildIdentities") {
Some(plist::Value::Array(i)) => i,
_ => {
return Err(IdeviceError::BadBuildManifest);
}
};
let mut build_identity = None;
for id in identities {
let id = match id {
plist::Value::Dictionary(id) => id,
_ => {
debug!("build identity wasn't a dictionary");
continue;
}
};
let ap_board_id = match id.get("ApBoardID") {
Some(plist::Value::String(a)) => a,
_ => {
debug!("Build identity contained no ApBoardID");
continue;
}
};
let ap_board_id = match u64::from_str_radix(ap_board_id.trim_start_matches("0x"), 16) {
Ok(a) => a,
Err(_) => {
debug!("Could not parse {ap_board_id} as usize");
continue;
}
};
if ap_board_id != board_id {
continue;
}
let ap_chip_id = match id.get("ApChipID") {
Some(plist::Value::String(a)) => a,
_ => {
debug!("Build identity contained no ApChipID");
continue;
}
};
let ap_chip_id = match u64::from_str_radix(ap_chip_id.trim_start_matches("0x"), 16) {
Ok(a) => a,
Err(_) => {
debug!("Could not parse {ap_board_id} as usize");
continue;
}
};
if ap_chip_id != chip_id {
continue;
}
build_identity = Some(id.to_owned());
break;
}
let build_identity = match build_identity {
Some(b) => b,
None => {
return Err(IdeviceError::BadBuildManifest);
}
};
let manifest = match build_identity.get("Manifest") {
Some(plist::Value::Dictionary(m)) => m,
_ => {
return Err(IdeviceError::BadBuildManifest);
}
};
let mut parameters = plist::Dictionary::new();
parameters.insert("ApProductionMode".into(), true.into());
parameters.insert("ApSecurityDomain".into(), 1.into());
parameters.insert("ApSecurityMode".into(), true.into());
parameters.insert("ApSupportsImg4".into(), true.into());
for (key, manifest_item) in manifest {
let manifest_item = match manifest_item {
plist::Value::Dictionary(m) => m,
_ => {
debug!("Manifest item wasn't a dictionary");
continue;
}
};
let info = match manifest_item.get("Info") {
Some(plist::Value::Dictionary(i)) => i,
_ => {
debug!("Manifest item didn't contain info");
continue;
}
};
match info.get("Trusted") {
Some(plist::Value::Boolean(t)) => {
if !t {
debug!("Info item isn't trusted");
continue;
}
}
_ => {
debug!("Info didn't contain trusted bool");
continue;
}
}
let mut tss_entry = manifest_item.clone();
tss_entry.remove("Info");
if let Some(plist::Value::Dictionary(l)) = manifest.get("LoadableTrustCache") {
if let Some(plist::Value::Dictionary(i)) = l.get("Info") {
if let Some(plist::Value::Array(rules)) = i.get("RestoreRequestRules") {
crate::tss::apply_restore_request_rules(&mut tss_entry, &parameters, rules);
}
}
}
if manifest_item.get("Digest").is_none() {
tss_entry.insert("Digest".into(), plist::Value::Data(vec![]));
}
request.insert(key, tss_entry);
}
let res = request.send().await?;
let mut res = match res {
plist::Value::Dictionary(r) => r,
_ => {
warn!("Apple returned a non-dictionary plist");
return Err(IdeviceError::UnexpectedResponse);
}
};
match res.remove("ApImg4Ticket") {
Some(plist::Value::Data(d)) => Ok(d),
_ => {
warn!("TSS response didn't contain ApImg4Ticket data");
Err(IdeviceError::UnexpectedResponse)
}
}
}
} }

140
idevice/src/tss.rs Normal file
View File

@@ -0,0 +1,140 @@
// Jackson Coxson
// Thanks pymobiledevice3
use log::{debug, warn};
use plist::Value;
use crate::{util::plist_to_bytes, IdeviceError};
const TSS_CLIENT_VERSION_STRING: &str = "libauthinstall-1033.0.2";
const TSS_CONTROLLER_ACTION_URL: &str = "http://gs.apple.com/TSS/controller?action=2";
pub struct TSSRequest {
inner: plist::Dictionary,
}
impl TSSRequest {
pub fn new() -> Self {
let mut inner = plist::Dictionary::new();
inner.insert("@HostPlatformInfo".into(), "mac".into());
inner.insert("@VersionInfo".into(), TSS_CLIENT_VERSION_STRING.into());
inner.insert(
"@UUID".into(),
uuid::Uuid::new_v4().to_string().to_uppercase().into(),
);
Self {
inner: Default::default(),
}
}
pub fn insert(&mut self, key: impl Into<String>, val: impl Into<Value>) {
let key = key.into();
let val = val.into();
self.inner.insert(key, val);
}
pub async fn send(&self) -> Result<plist::Value, IdeviceError> {
let client = reqwest::Client::new();
let res = client
.post(TSS_CONTROLLER_ACTION_URL)
.header("Cache-Control", "no-cache")
.header("Content-type", "text/xml; charset=\"utf-8\"")
.header("User-Agent", "InetURL/1.0")
.header("Expect", "")
.body(plist_to_bytes(&self.inner))
.send()
.await?
.text()
.await?;
debug!("Apple responeded with {res}");
let res = res.trim_start_matches("MESSAGE=");
if !res.starts_with("SUCCESS") {
return Err(IdeviceError::UnexpectedResponse);
}
let res = res.split("REQUEST_STRING=").collect::<Vec<&str>>();
if res.len() < 2 {
return Err(IdeviceError::UnexpectedResponse);
}
Ok(plist::from_bytes(res[1].as_bytes())?)
}
}
impl Default for TSSRequest {
fn default() -> Self {
Self::new()
}
}
pub fn apply_restore_request_rules(
input: &mut plist::Dictionary,
parameters: &plist::Dictionary,
rules: &Vec<plist::Value>,
) {
for rule in rules {
if let plist::Value::Dictionary(rule) = rule {
let mut conditions_fulfulled = true;
let conditions = match rule.get("Conditions") {
Some(plist::Value::Dictionary(c)) => c,
_ => {
warn!("Conditions doesn't exist or wasn't a dictionary!!");
continue;
}
};
for (key, value) in conditions {
let value2 = match key.as_str() {
"ApRawProductionMode" => parameters.get("ApProductionMode"),
"ApCurrentProductionMode" => parameters.get("ApProductionMode"),
"ApRawSecurityMode" => parameters.get("ApSecurityMode"),
"ApRequiresImage4" => parameters.get("ApSupportsImg4"),
"ApDemotionPolicyOverride" => parameters.get("DemotionPolicy"),
"ApInRomDFU" => parameters.get("ApInRomDFU"),
_ => {
warn!("Unhandled key {key}");
None
}
};
conditions_fulfulled = match value2 {
Some(value2) => value == value2,
None => false,
};
if !conditions_fulfulled {
break;
}
}
if !conditions_fulfulled {
continue;
}
let actions = match rule.get("Actions") {
Some(plist::Value::Dictionary(a)) => a,
_ => {
warn!("Actions doesn't exist or wasn't a dictionary!!");
continue;
}
};
for (key, value) in actions {
if let Some(i) = value.as_unsigned_integer() {
if i == 255 {
continue;
}
}
if let Some(i) = value.as_signed_integer() {
if i == 255 {
continue;
}
}
input.insert(key.to_owned(), value.to_owned());
}
} else {
warn!("Rule wasn't a dictionary");
}
}
}

View File

@@ -1,5 +1,6 @@
// Jackson Coxson // Jackson Coxson
use crate::util::plist_to_bytes;
use log::warn; use log::warn;
#[derive(Debug)] #[derive(Debug)]
@@ -11,14 +12,6 @@ pub struct RawPacket {
pub plist: plist::Dictionary, pub plist: plist::Dictionary,
} }
fn plist_to_bytes(p: &plist::Dictionary) -> Vec<u8> {
let buf = Vec::new();
let mut writer = std::io::BufWriter::new(buf);
plist::to_writer_xml(&mut writer, &p).unwrap();
writer.into_inner().unwrap()
}
impl RawPacket { impl RawPacket {
pub fn new(plist: plist::Dictionary, version: u32, message: u32, tag: u32) -> RawPacket { pub fn new(plist: plist::Dictionary, version: u32, message: u32, tag: u32) -> RawPacket {
let plist_bytes = plist_to_bytes(&plist); let plist_bytes = plist_to_bytes(&plist);

9
idevice/src/util.rs Normal file
View File

@@ -0,0 +1,9 @@
// Jackson Coxson
pub fn plist_to_bytes(p: &plist::Dictionary) -> Vec<u8> {
let buf = Vec::new();
let mut writer = std::io::BufWriter::new(buf);
plist::to_writer_xml(&mut writer, &p).unwrap();
writer.into_inner().unwrap()
}