From c3333ed2df7f7ddd6e6d9cd4d4d924964f01634f Mon Sep 17 00:00:00 2001 From: ValorBao <139973913+ValorBao@users.noreply.github.com> Date: Tue, 9 Sep 2025 23:43:16 +0800 Subject: [PATCH 01/21] Feature/screenshot (#26) * "dvt: add screen shot and change read_message in multiple message fragments" * "dvt: Add processing multiple fragments to the message module in dvt " * "cargo fmt" * Rename screen_shot to screenshot --------- Co-authored-by: Jackson Coxson --- idevice/src/services/dvt/message.rs | 58 +++++++++------- idevice/src/services/dvt/mod.rs | 1 + idevice/src/services/dvt/screenshot.rs | 64 ++++++++++++++++++ tools/Cargo.toml | 5 ++ tools/src/screenshot.rs | 94 ++++++++++++++++++++++++++ 5 files changed, 198 insertions(+), 24 deletions(-) create mode 100644 idevice/src/services/dvt/screenshot.rs create mode 100644 tools/src/screenshot.rs diff --git a/idevice/src/services/dvt/message.rs b/idevice/src/services/dvt/message.rs index 4298e77..36e4fbe 100644 --- a/idevice/src/services/dvt/message.rs +++ b/idevice/src/services/dvt/message.rs @@ -392,24 +392,35 @@ impl Message { /// # Errors /// * Various IdeviceError variants for IO and parsing failures pub async fn from_reader(reader: &mut R) -> Result { - let mut buf = [0u8; 32]; - reader.read_exact(&mut buf).await?; - - let mheader = MessageHeader { - magic: u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]), - header_len: u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]), - fragment_id: u16::from_le_bytes([buf[8], buf[9]]), - fragment_count: u16::from_le_bytes([buf[10], buf[11]]), - length: u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]), - identifier: u32::from_le_bytes([buf[16], buf[17], buf[18], buf[19]]), - conversation_index: u32::from_le_bytes([buf[20], buf[21], buf[22], buf[23]]), - channel: u32::from_le_bytes([buf[24], buf[25], buf[26], buf[27]]), - expects_reply: u32::from_le_bytes([buf[28], buf[29], buf[30], buf[31]]) == 1, + let mut packet_data: Vec = Vec::new(); + // loop for deal with multiple fragments + let mheader = loop { + let mut buf = [0u8; 32]; + reader.read_exact(&mut buf).await?; + let header = MessageHeader { + magic: u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]), + header_len: u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]), + fragment_id: u16::from_le_bytes([buf[8], buf[9]]), + fragment_count: u16::from_le_bytes([buf[10], buf[11]]), + length: u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]), + identifier: u32::from_le_bytes([buf[16], buf[17], buf[18], buf[19]]), + conversation_index: u32::from_le_bytes([buf[20], buf[21], buf[22], buf[23]]), + channel: u32::from_le_bytes([buf[24], buf[25], buf[26], buf[27]]), + expects_reply: u32::from_le_bytes([buf[28], buf[29], buf[30], buf[31]]) == 1, + }; + if header.fragment_count > 1 && header.fragment_id == 0 { + // when reading multiple message fragments, the first fragment contains only a message header. + continue; + } + let mut buf = vec![0u8; header.length as usize]; + reader.read_exact(&mut buf).await?; + packet_data.extend(buf); + if header.fragment_id == header.fragment_count - 1 { + break header; + } }; - - let mut buf = [0u8; 16]; - reader.read_exact(&mut buf).await?; - + // read the payload header + let buf = &packet_data[0..16]; let pheader = PayloadHeader { flags: u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]), aux_length: u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]), @@ -417,18 +428,17 @@ impl Message { buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], buf[15], ]), }; - let aux = if pheader.aux_length > 0 { - let mut buf = vec![0u8; pheader.aux_length as usize]; - reader.read_exact(&mut buf).await?; + let buf = packet_data[16..(16 + pheader.aux_length as usize)].to_vec(); Some(Aux::from_bytes(buf)?) } else { None }; - - let mut buf = vec![0u8; (pheader.total_length - pheader.aux_length as u64) as usize]; - reader.read_exact(&mut buf).await?; - + // read the data + let need_len = (pheader.total_length - pheader.aux_length as u64) as usize; + let buf = packet_data + [(pheader.aux_length + 16) as usize..pheader.aux_length as usize + 16 + need_len] + .to_vec(); let data = if buf.is_empty() { None } else { diff --git a/idevice/src/services/dvt/mod.rs b/idevice/src/services/dvt/mod.rs index d7db667..86a5fa6 100644 --- a/idevice/src/services/dvt/mod.rs +++ b/idevice/src/services/dvt/mod.rs @@ -9,6 +9,7 @@ pub mod location_simulation; pub mod message; pub mod process_control; pub mod remote_server; +pub mod screenshot; impl RsdService for remote_server::RemoteServerClient> { fn rsd_service_name() -> std::borrow::Cow<'static, str> { diff --git a/idevice/src/services/dvt/screenshot.rs b/idevice/src/services/dvt/screenshot.rs new file mode 100644 index 0000000..cd021f8 --- /dev/null +++ b/idevice/src/services/dvt/screenshot.rs @@ -0,0 +1,64 @@ +//! Screenshot service client for iOS instruments protocol. +//! +//! This module provides a client for interacting with the screenshot service +//! on iOS devices through the instruments protocol. It allows taking screenshots from the device. +//! + +use plist::Value; + +use crate::{ + IdeviceError, ReadWrite, + dvt::remote_server::{Channel, RemoteServerClient}, + obf, +}; + +/// Client for take screenshot operations on iOS devices +/// +/// Provides methods for take screnn_shot through the +/// instruments protocol. Each instance maintains its own communication channel. +pub struct ScreenshotClient<'a, R: ReadWrite> { + /// The underlying channel for communication + channel: Channel<'a, R>, +} + +impl<'a, R: ReadWrite> ScreenshotClient<'a, R> { + /// Creates a new ScreenshotClient + /// + /// # Arguments + /// * `client` - The base RemoteServerClient to use + /// + /// # Returns + /// * `Ok(ScreenshotClient)` - Connected client instance + /// * `Err(IdeviceError)` - If channel creation fails + /// + /// # Errors + /// * Propagates errors from channel creation + pub async fn new(client: &'a mut RemoteServerClient) -> Result { + let channel = client + .make_channel(obf!("com.apple.instruments.server.services.screenshot")) + .await?; // Drop `&mut client` before continuing + + Ok(Self { channel }) + } + + /// Take screenshot from the device + /// + /// # Returns + /// * `Ok(Vec)` - the bytes of the screenshot + /// * `Err(IdeviceError)` - If communication fails + /// + /// # Errors + /// * `IdeviceError::UnexpectedResponse` if server response is invalid + /// * Other communication or serialization errors + pub async fn take_screenshot(&mut self) -> Result, IdeviceError> { + let method = Value::String("takeScreenshot".into()); + + self.channel.call_method(Some(method), None, true).await?; + + let msg = self.channel.read_message().await?; + match msg.data { + Some(Value::Data(data)) => Ok(data), + _ => Err(IdeviceError::UnexpectedResponse), + } + } +} diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 57b5828..3c63429 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -125,6 +125,11 @@ path = "src/pcapd.rs" name = "preboard" path = "src/preboard.rs" + +[[bin]] +name = "screenshot" +path = "src/screenshot.rs" + [dependencies] idevice = { path = "../idevice", features = ["full"], default-features = false } tokio = { version = "1.43", features = ["full"] } diff --git a/tools/src/screenshot.rs b/tools/src/screenshot.rs new file mode 100644 index 0000000..bb6f617 --- /dev/null +++ b/tools/src/screenshot.rs @@ -0,0 +1,94 @@ +use clap::{Arg, Command}; +use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; +use std::fs; + +mod common; + +#[tokio::main] +async fn main() { + env_logger::init(); + let matches = Command::new("screen_shot") + .about("take screenshot") + .arg( + Arg::new("host") + .long("host") + .value_name("HOST") + .help("IP address of the device"), + ) + .arg( + Arg::new("pairing_file") + .long("pairing-file") + .value_name("PATH") + .help("Path to the pairing file"), + ) + .arg( + Arg::new("udid") + .value_name("UDID") + .help("UDID of the device (overrides host/pairing file)") + .index(1), + ) + .arg( + Arg::new("output") + .short('o') + .long("output") + .value_name("FILE") + .help("Output file path for the screenshot (default: ./screenshot.png)") + .default_value("screenshot.png"), + ) + .arg( + Arg::new("about") + .long("about") + .help("Show about information") + .action(clap::ArgAction::SetTrue), + ) + .get_matches(); + + if matches.get_flag("about") { + print!("screen_shot - take screenshot from ios device"); + println!("Copyright (c) 2025 Jackson Coxson"); + return; + } + + let udid = matches.get_one::("udid"); + let host = matches.get_one::("host"); + let pairing_file = matches.get_one::("pairing_file"); + let output_path = matches.get_one::("output").unwrap(); + + let provider = + match common::get_provider(udid, host, pairing_file, "take_screenshot-jkcoxson").await { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + let proxy = CoreDeviceProxy::connect(&*provider) + .await + .expect("no core proxy"); + let rsd_port = proxy.handshake.server_rsd_port; + + let adapter = proxy.create_software_tunnel().expect("no software tunnel"); + let mut adapter = adapter.to_async_handle(); + let stream = adapter.connect(rsd_port).await.expect("no RSD connect"); + + // Make the connection to RemoteXPC + let mut handshake = RsdHandshake::new(stream).await.unwrap(); + let mut ts_client = + idevice::dvt::remote_server::RemoteServerClient::connect_rsd(&mut adapter, &mut handshake) + .await + .expect("Failed to connect"); + ts_client.read_message(0).await.expect("no read??"); + + let mut ts_client = idevice::dvt::screenshot::ScreenshotClient::new(&mut ts_client) + .await + .expect("Unable to get channel for take screenshot"); + let res = ts_client + .take_screenshot() + .await + .expect("Failed to take screenshot"); + + match fs::write(output_path, &res) { + Ok(_) => println!("Screenshot saved to: {}", output_path), + Err(e) => eprintln!("Failed to write screenshot to file: {}", e), + } +} From 378c84e6ef6e6beb009cd94ee9e492a7dd0dd2b2 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 9 Sep 2025 09:44:58 -0600 Subject: [PATCH 02/21] Bump version --- Cargo.lock | 2 +- idevice/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5c76807..cdd6a56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1133,7 +1133,7 @@ dependencies = [ [[package]] name = "idevice" -version = "0.1.41" +version = "0.1.42" dependencies = [ "async-stream", "base64", diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index f172e4b..b5dbf79 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -2,7 +2,7 @@ name = "idevice" description = "A Rust library to interact with services on iOS devices." authors = ["Jackson Coxson"] -version = "0.1.41" +version = "0.1.42" edition = "2024" license = "MIT" documentation = "https://docs.rs/idevice" From 482c9805c45aebe53df239f33e4743fd50400e58 Mon Sep 17 00:00:00 2001 From: ValorBao <139973913+ValorBao@users.noreply.github.com> Date: Wed, 17 Sep 2025 03:44:13 +0800 Subject: [PATCH 03/21] feat: add screenshotr for iOS versions below iOS17 (#27) * feat: add screenshot for iOS versions below iOS17 * "cargo fmt" * Style cleanup --------- Co-authored-by: Jackson Coxson --- idevice/Cargo.toml | 1 + idevice/src/services/mod.rs | 2 + idevice/src/services/screenshotr.rs | 123 ++++++++++++++++++++++++++++ tools/src/screenshot.rs | 58 ++++++++----- 4 files changed, 162 insertions(+), 22 deletions(-) create mode 100644 idevice/src/services/screenshotr.rs diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index b5dbf79..87165fe 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -90,6 +90,7 @@ preboard_service = [] obfuscate = ["dep:obfstr"] restore_service = [] rsd = ["xpc"] +screenshotr = [] syslog_relay = ["dep:bytes"] tcp = ["tokio/net"] tunnel_tcp_stack = [ diff --git a/idevice/src/services/mod.rs b/idevice/src/services/mod.rs index 24afbd6..e5e3c91 100644 --- a/idevice/src/services/mod.rs +++ b/idevice/src/services/mod.rs @@ -41,6 +41,8 @@ pub mod preboard_service; pub mod restore_service; #[cfg(feature = "rsd")] pub mod rsd; +#[cfg(feature = "screenshotr")] +pub mod screenshotr; #[cfg(feature = "springboardservices")] pub mod springboardservices; #[cfg(feature = "syslog_relay")] diff --git a/idevice/src/services/screenshotr.rs b/idevice/src/services/screenshotr.rs new file mode 100644 index 0000000..4105242 --- /dev/null +++ b/idevice/src/services/screenshotr.rs @@ -0,0 +1,123 @@ +//! iOS screenshotr service client +//! +//! Provides functionality for interacting with the screenshot service on iOS devices below iOS 17, +//! which allows taking screenshots. + +use crate::{Idevice, IdeviceError, IdeviceService, obf}; +use log::{debug, warn}; +use std::borrow::Cow; +use tokio::io::AsyncReadExt; +pub struct ScreenshotService { + /// Underlying device connection + pub idevice: Idevice, +} +impl IdeviceService for ScreenshotService { + fn service_name() -> Cow<'static, str> { + obf!("com.apple.mobile.screenshotr") + } + + async fn from_stream(idevice: Idevice) -> Result { + let mut client = Self::new(idevice); + // Perform DeviceLink handshake first + client.dl_version_exchange().await?; + Ok(client) + } +} + +impl ScreenshotService { + pub fn new(idevice: Idevice) -> Self { + Self { idevice } + } + + async fn dl_version_exchange(&mut self) -> Result<(), IdeviceError> { + debug!("Starting DeviceLink version exchange"); + // 1) Receive DLMessageVersionExchange + let (msg, _arr) = self.receive_dl_message().await?; + if msg != "DLMessageVersionExchange" { + warn!("Expected DLMessageVersionExchange, got {msg}"); + return Err(IdeviceError::UnexpectedResponse); + } + + // 2) Send DLVersionsOk with version 400 + let out = vec![ + plist::Value::String("DLMessageVersionExchange".into()), + plist::Value::String("DLVersionsOk".into()), + plist::Value::Integer(400u64.into()), + ]; + self.send_dl_array(out).await?; + + // 3) Receive DLMessageDeviceReady + let (msg2, _arr2) = self.receive_dl_message().await?; + if msg2 != "DLMessageDeviceReady" { + warn!("Expected DLMessageDeviceReady, got {msg2}"); + return Err(IdeviceError::UnexpectedResponse); + } + Ok(()) + } + + /// Sends a raw DL array as binary plist + async fn send_dl_array(&mut self, array: Vec) -> Result<(), IdeviceError> { + self.idevice.send_bplist(plist::Value::Array(array)).await + } + + /// Receives any DL* message and returns (message_tag, full_array_value) + pub async fn receive_dl_message(&mut self) -> Result<(String, plist::Value), IdeviceError> { + if let Some(socket) = &mut self.idevice.socket { + let mut buf = [0u8; 4]; + socket.read_exact(&mut buf).await?; + let len = u32::from_be_bytes(buf); + let mut body = vec![0; len as usize]; + socket.read_exact(&mut body).await?; + let value: plist::Value = plist::from_bytes(&body)?; + if let plist::Value::Array(arr) = &value + && let Some(plist::Value::String(tag)) = arr.first() + { + return Ok((tag.clone(), value)); + } + warn!("Invalid DL message format"); + Err(IdeviceError::UnexpectedResponse) + } else { + Err(IdeviceError::NoEstablishedConnection) + } + } + + pub async fn take_screenshot(&mut self) -> Result, IdeviceError> { + // Send DLMessageTakeScreenshot + + let message_type_dict = crate::plist!(dict { + "MessageType": "ScreenShotRequest" + }); + + let out = vec![ + plist::Value::String("DLMessageProcessMessage".into()), + plist::Value::Dictionary(message_type_dict), + ]; + self.send_dl_array(out).await?; + + // Receive DLMessageScreenshotData + let (msg, value) = self.receive_dl_message().await?; + if msg != "DLMessageProcessMessage" { + warn!("Expected DLMessageProcessMessage, got {msg}"); + return Err(IdeviceError::UnexpectedResponse); + } + + if let plist::Value::Array(arr) = &value + && arr.len() == 2 + { + if let Some(plist::Value::Dictionary(dict)) = arr.get(1) { + if let Some(plist::Value::Data(data)) = dict.get("ScreenShotData") { + Ok(data.clone()) + } else { + warn!("Invalid ScreenShotData format"); + Err(IdeviceError::UnexpectedResponse) + } + } else { + warn!("Invalid DLMessageScreenshotData format"); + Err(IdeviceError::UnexpectedResponse) + } + } else { + warn!("Invalid DLMessageScreenshotData format"); + Err(IdeviceError::UnexpectedResponse) + } + } +} diff --git a/tools/src/screenshot.rs b/tools/src/screenshot.rs index bb6f617..ead4a01 100644 --- a/tools/src/screenshot.rs +++ b/tools/src/screenshot.rs @@ -2,6 +2,8 @@ use clap::{Arg, Command}; use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; use std::fs; +use idevice::screenshotr::ScreenshotService; + mod common; #[tokio::main] @@ -62,31 +64,43 @@ async fn main() { return; } }; - let proxy = CoreDeviceProxy::connect(&*provider) + let mut res: Vec = Vec::new(); + if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await { + let rsd_port = proxy.handshake.server_rsd_port; + + let adapter = proxy.create_software_tunnel().expect("no software tunnel"); + let mut adapter = adapter.to_async_handle(); + let stream = adapter.connect(rsd_port).await.expect("no RSD connect"); + + // Make the connection to RemoteXPC + let mut handshake = RsdHandshake::new(stream).await.unwrap(); + let mut ts_client = idevice::dvt::remote_server::RemoteServerClient::connect_rsd( + &mut adapter, + &mut handshake, + ) .await - .expect("no core proxy"); - let rsd_port = proxy.handshake.server_rsd_port; + .expect("Failed to connect"); + ts_client.read_message(0).await.expect("no read??"); - let adapter = proxy.create_software_tunnel().expect("no software tunnel"); - let mut adapter = adapter.to_async_handle(); - let stream = adapter.connect(rsd_port).await.expect("no RSD connect"); - - // Make the connection to RemoteXPC - let mut handshake = RsdHandshake::new(stream).await.unwrap(); - let mut ts_client = - idevice::dvt::remote_server::RemoteServerClient::connect_rsd(&mut adapter, &mut handshake) + let mut ts_client = idevice::dvt::screenshot::ScreenshotClient::new(&mut ts_client) .await - .expect("Failed to connect"); - ts_client.read_message(0).await.expect("no read??"); - - let mut ts_client = idevice::dvt::screenshot::ScreenshotClient::new(&mut ts_client) - .await - .expect("Unable to get channel for take screenshot"); - let res = ts_client - .take_screenshot() - .await - .expect("Failed to take screenshot"); - + .expect("Unable to get channel for take screenshot"); + res = ts_client + .take_screenshot() + .await + .expect("Failed to take screenshot"); + } else { + let mut screenshot_client = match ScreenshotService::connect(&*provider).await { + Ok(client) => client, + Err(e) => { + eprintln!( + "Unable to connect to screenshotr service: {e} Ensure Developer Disk Image is mounted." + ); + return; + } + }; + res = screenshot_client.take_screenshot().await.unwrap(); + } match fs::write(output_path, &res) { Ok(_) => println!("Screenshot saved to: {}", output_path), Err(e) => eprintln!("Failed to write screenshot to file: {}", e), From da17fa01dcab86250d20c93e033c7bdf899a073c Mon Sep 17 00:00:00 2001 From: Abdullah Al-Banna Date: Tue, 16 Sep 2025 22:54:00 +0300 Subject: [PATCH 04/21] support installing .ipcc packages (#25) * resolve conflicts Signed-off-by: abdullah-albanna * resolve conflicts Signed-off-by: abdullah-albanna * fix typos * fix clippy --------- Signed-off-by: abdullah-albanna --- Cargo.lock | 66 +++++ idevice/Cargo.toml | 10 +- idevice/src/lib.rs | 7 + idevice/src/utils/installation.rs | 306 ---------------------- idevice/src/utils/installation/helpers.rs | 273 +++++++++++++++++++ idevice/src/utils/installation/mod.rs | 186 +++++++++++++ 6 files changed, 541 insertions(+), 307 deletions(-) delete mode 100644 idevice/src/utils/installation.rs create mode 100644 idevice/src/utils/installation/helpers.rs create mode 100644 idevice/src/utils/installation/mod.rs diff --git a/Cargo.lock b/Cargo.lock index cdd6a56..ae33fa5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,6 +109,19 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-compression" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddb939d66e4ae03cee6091612804ba446b12878410cfa17f785f4dd67d4014e8" +dependencies = [ + "flate2", + "futures-core", + "futures-io", + "memchr", + "pin-project-lite", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -137,6 +150,21 @@ version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" +[[package]] +name = "async_zip" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c50d65ce1b0e0cb65a785ff615f78860d7754290647d3b983208daa4f85e6" +dependencies = [ + "async-compression", + "crc32fast", + "futures-lite", + "pin-project", + "thiserror 2.0.12", + "tokio", + "tokio-util", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -814,7 +842,10 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" dependencies = [ + "fastrand", "futures-core", + "futures-io", + "parking", "pin-project-lite", ] @@ -1136,6 +1167,7 @@ name = "idevice" version = "0.1.42" dependencies = [ "async-stream", + "async_zip", "base64", "byteorder", "bytes", @@ -1714,6 +1746,26 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2531,6 +2583,20 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.8.23" diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 87165fe..4a255e5 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -56,6 +56,8 @@ x509-cert = { version = "0.2", optional = true, features = [ obfstr = { version = "0.4", optional = true } +async_zip = { version = "0.0.18", optional = true } + [dev-dependencies] tokio = { version = "1.43", features = ["full"] } tun-rs = { version = "2.0.8", features = ["async_tokio"] } @@ -78,7 +80,13 @@ diagnostics_relay = [] dvt = ["dep:byteorder", "dep:ns-keyed-archive"] heartbeat = ["tokio/macros", "tokio/time"] house_arrest = ["afc"] -installation_proxy = [] +installation_proxy = [ + "dep:async_zip", + "dep:futures", + "async_zip/tokio", + "async_zip/deflate", + "tokio/fs" +] springboardservices = [] misagent = [] mobile_image_mounter = ["dep:sha2"] diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 33fb862..fb55bc2 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -713,6 +713,10 @@ pub enum IdeviceError { IntegerOverflow = -65, #[error("canceled by user")] CanceledByUser = -66, + + #[cfg(feature = "installation_proxy")] + #[error("malformed package archive: {0}")] + MalformedPackageArchive(#[from] async_zip::error::ZipError) = -67, } impl IdeviceError { @@ -868,6 +872,9 @@ impl IdeviceError { IdeviceError::MalformedCommand => -64, IdeviceError::IntegerOverflow => -65, IdeviceError::CanceledByUser => -66, + + #[cfg(feature = "installation_proxy")] + IdeviceError::MalformedPackageArchive(_) => -67, } } } diff --git a/idevice/src/utils/installation.rs b/idevice/src/utils/installation.rs deleted file mode 100644 index 748363a..0000000 --- a/idevice/src/utils/installation.rs +++ /dev/null @@ -1,306 +0,0 @@ -//! High-level install/upgrade helpers -//! -//! This module provides convenient wrappers that mirror ideviceinstaller's -//! behavior by uploading a package to `PublicStaging` via AFC and then -//! issuing `Install`/`Upgrade` commands through InstallationProxy. -//! -//! Notes: -//! - The package path used by InstallationProxy must be a path inside the -//! AFC jail (e.g. `PublicStaging/`) -//! - For `.ipa` files, we upload the whole file to `PublicStaging/` -//! - For directories (developer bundles), we recursively mirror the directory -//! into `PublicStaging/` and pass that directory path. - -use std::path::Path; - -use crate::{ - IdeviceError, IdeviceService, - provider::IdeviceProvider, - services::{ - afc::{AfcClient, opcode::AfcFopenMode}, - installation_proxy::InstallationProxyClient, - }, -}; - -const PUBLIC_STAGING: &str = "PublicStaging"; - -/// Result of a prepared upload, containing the remote path to use in Install/Upgrade -struct UploadedPackageInfo { - /// Path inside the AFC jail for InstallationProxy `PackagePath` - remote_package_path: String, -} - -/// Ensure `PublicStaging` exists on device via AFC -async fn ensure_public_staging(afc: &mut AfcClient) -> Result<(), IdeviceError> { - // Try to stat and if it fails, create directory - match afc.get_file_info(PUBLIC_STAGING).await { - Ok(_) => Ok(()), - Err(_) => afc.mk_dir(PUBLIC_STAGING).await, - } -} - -/// Upload a single file to a destination path on device using AFC -async fn afc_upload_file( - afc: &mut AfcClient, - local_path: &Path, - remote_path: &str, -) -> Result<(), IdeviceError> { - let mut fd = afc.open(remote_path, AfcFopenMode::WrOnly).await?; - let bytes = tokio::fs::read(local_path).await?; - fd.write(&bytes).await?; - fd.close().await -} - -/// Recursively upload a directory to device via AFC (mirror contents) -async fn afc_upload_dir( - afc: &mut AfcClient, - local_dir: &Path, - remote_dir: &str, -) -> Result<(), IdeviceError> { - use std::collections::VecDeque; - afc.mk_dir(remote_dir).await.ok(); - - let mut queue: VecDeque<(std::path::PathBuf, String)> = VecDeque::new(); - queue.push_back((local_dir.to_path_buf(), remote_dir.to_string())); - - while let Some((cur_local, cur_remote)) = queue.pop_front() { - let mut rd = tokio::fs::read_dir(&cur_local).await?; - while let Some(entry) = rd.next_entry().await? { - let meta = entry.metadata().await?; - let name = entry.file_name(); - let name = name.to_string_lossy().into_owned(); - if name == "." || name == ".." { - continue; - } - let child_local = entry.path(); - let child_remote = format!("{}/{}", cur_remote, name); - if meta.is_dir() { - afc.mk_dir(&child_remote).await.ok(); - queue.push_back((child_local, child_remote)); - } else if meta.is_file() { - afc_upload_file(afc, &child_local, &child_remote).await?; - } - } - } - Ok(()) -} - -/// Upload a package to `PublicStaging` and return its InstallationProxy path -/// -/// - If `local_path` is a file, it will be uploaded to `PublicStaging/` -/// - If it is a directory, it will be mirrored to `PublicStaging/` -async fn upload_package_to_public_staging>( - provider: &dyn IdeviceProvider, - local_path: P, -) -> Result { - // Connect to AFC via the generic service connector - let mut afc = AfcClient::connect(provider).await?; - - ensure_public_staging(&mut afc).await?; - - let local_path = local_path.as_ref(); - let file_name: String = local_path - .file_name() - .map(|s| s.to_string_lossy().into_owned()) - .ok_or_else(|| IdeviceError::InvalidArgument)?; - let remote_path = format!("{}/{}", PUBLIC_STAGING, file_name); - - let meta = tokio::fs::metadata(local_path).await?; - if meta.is_dir() { - afc_upload_dir(&mut afc, local_path, &remote_path).await?; - } else { - afc_upload_file(&mut afc, local_path, &remote_path).await?; - } - - Ok(UploadedPackageInfo { - remote_package_path: remote_path, - }) -} - -/// Install an application by first uploading the local package and then invoking InstallationProxy. -/// -/// - Accepts a local file path or directory path. -/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults. -pub async fn install_package>( - provider: &dyn IdeviceProvider, - local_path: P, - options: Option, -) -> Result<(), IdeviceError> { - let UploadedPackageInfo { - remote_package_path, - } = upload_package_to_public_staging(provider, local_path).await?; - - let mut inst = InstallationProxyClient::connect(provider).await?; - inst.install(remote_package_path, options).await -} - -/// Upgrade an application by first uploading the local package and then invoking InstallationProxy. -/// -/// - Accepts a local file path or directory path. -/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults. -pub async fn upgrade_package>( - provider: &dyn IdeviceProvider, - local_path: P, - options: Option, -) -> Result<(), IdeviceError> { - let UploadedPackageInfo { - remote_package_path, - } = upload_package_to_public_staging(provider, local_path).await?; - - let mut inst = InstallationProxyClient::connect(provider).await?; - inst.upgrade(remote_package_path, options).await -} - -/// Same as `install_package` but providing a callback that receives `(percent_complete, state)` -/// updates while InstallationProxy performs the operation. -pub async fn install_package_with_callback, Fut, S>( - provider: &dyn IdeviceProvider, - local_path: P, - options: Option, - callback: impl Fn((u64, S)) -> Fut, - state: S, -) -> Result<(), IdeviceError> -where - Fut: std::future::Future, - S: Clone, -{ - let UploadedPackageInfo { - remote_package_path, - } = upload_package_to_public_staging(provider, local_path).await?; - - let mut inst = InstallationProxyClient::connect(provider).await?; - inst.install_with_callback(remote_package_path, options, callback, state) - .await -} - -/// Same as `upgrade_package` but providing a callback that receives `(percent_complete, state)` -/// updates while InstallationProxy performs the operation. -pub async fn upgrade_package_with_callback, Fut, S>( - provider: &dyn IdeviceProvider, - local_path: P, - options: Option, - callback: impl Fn((u64, S)) -> Fut, - state: S, -) -> Result<(), IdeviceError> -where - Fut: std::future::Future, - S: Clone, -{ - let UploadedPackageInfo { - remote_package_path, - } = upload_package_to_public_staging(provider, local_path).await?; - - let mut inst = InstallationProxyClient::connect(provider).await?; - inst.upgrade_with_callback(remote_package_path, options, callback, state) - .await -} - -/// Upload raw bytes to `PublicStaging/` via AFC and return the remote package path. -/// -/// - This is useful when the package is not present on disk or is generated in-memory. -async fn upload_bytes_to_public_staging( - provider: &dyn IdeviceProvider, - data: impl AsRef<[u8]>, - remote_name: &str, -) -> Result { - // Connect to AFC - let mut afc = AfcClient::connect(provider).await?; - ensure_public_staging(&mut afc).await?; - - let remote_path = format!("{}/{}", PUBLIC_STAGING, remote_name); - let mut fd = afc.open(&remote_path, AfcFopenMode::WrOnly).await?; - fd.write(data.as_ref()).await?; - fd.close().await?; - - Ok(UploadedPackageInfo { - remote_package_path: remote_path, - }) -} - -/// Install an application from raw bytes by first uploading them to `PublicStaging` and then -/// invoking InstallationProxy `Install`. -/// -/// - `remote_name` determines the remote filename under `PublicStaging`. -/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults. -pub async fn install_bytes( - provider: &dyn IdeviceProvider, - data: impl AsRef<[u8]>, - remote_name: &str, - options: Option, -) -> Result<(), IdeviceError> { - let UploadedPackageInfo { - remote_package_path, - } = upload_bytes_to_public_staging(provider, data, remote_name).await?; - let mut inst = InstallationProxyClient::connect(provider).await?; - inst.install(remote_package_path, options).await -} - -/// Same as `install_bytes` but providing a callback that receives `(percent_complete, state)` -/// updates while InstallationProxy performs the install operation. -/// -/// Tip: -/// - When embedding assets into the binary, you can pass `include_bytes!("path/to/app.ipa")` -/// as the `data` argument and choose a desired `remote_name` (e.g. `"MyApp.ipa"`). -pub async fn install_bytes_with_callback( - provider: &dyn IdeviceProvider, - data: impl AsRef<[u8]>, - remote_name: &str, - options: Option, - callback: impl Fn((u64, S)) -> Fut, - state: S, -) -> Result<(), IdeviceError> -where - Fut: std::future::Future, - S: Clone, -{ - let UploadedPackageInfo { - remote_package_path, - } = upload_bytes_to_public_staging(provider, data, remote_name).await?; - let mut inst = InstallationProxyClient::connect(provider).await?; - inst.install_with_callback(remote_package_path, options, callback, state) - .await -} - -/// Upgrade an application from raw bytes by first uploading them to `PublicStaging` and then -/// invoking InstallationProxy `Upgrade`. -/// -/// - `remote_name` determines the remote filename under `PublicStaging`. -/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults. -pub async fn upgrade_bytes( - provider: &dyn IdeviceProvider, - data: impl AsRef<[u8]>, - remote_name: &str, - options: Option, -) -> Result<(), IdeviceError> { - let UploadedPackageInfo { - remote_package_path, - } = upload_bytes_to_public_staging(provider, data, remote_name).await?; - let mut inst = InstallationProxyClient::connect(provider).await?; - inst.upgrade(remote_package_path, options).await -} - -/// Same as `upgrade_bytes` but providing a callback that receives `(percent_complete, state)` -/// updates while InstallationProxy performs the upgrade operation. -/// -/// Tip: -/// - When embedding assets into the binary, you can pass `include_bytes!("path/to/app.ipa")` -/// as the `data` argument and choose a desired `remote_name` (e.g. `"MyApp.ipa"`). -pub async fn upgrade_bytes_with_callback( - provider: &dyn IdeviceProvider, - data: impl AsRef<[u8]>, - remote_name: &str, - options: Option, - callback: impl Fn((u64, S)) -> Fut, - state: S, -) -> Result<(), IdeviceError> -where - Fut: std::future::Future, - S: Clone, -{ - let UploadedPackageInfo { - remote_package_path, - } = upload_bytes_to_public_staging(provider, data, remote_name).await?; - let mut inst = InstallationProxyClient::connect(provider).await?; - inst.upgrade_with_callback(remote_package_path, options, callback, state) - .await -} diff --git a/idevice/src/utils/installation/helpers.rs b/idevice/src/utils/installation/helpers.rs new file mode 100644 index 0000000..bf69c48 --- /dev/null +++ b/idevice/src/utils/installation/helpers.rs @@ -0,0 +1,273 @@ +use std::{io::Cursor, path::Path}; + +use async_zip::base::read::seek::ZipFileReader; +use futures::AsyncReadExt as _; +use tokio::io::{AsyncBufRead, AsyncSeek, BufReader}; + +use crate::{ + IdeviceError, IdeviceService, + afc::{AfcClient, opcode::AfcFopenMode}, + plist, + provider::IdeviceProvider, +}; + +pub const PUBLIC_STAGING: &str = "PublicStaging"; + +pub const IPCC_REMOTE_FILE: &str = "idevice.ipcc"; + +pub const IPA_REMOTE_FILE: &str = "idevice.ipa"; + +/// Result of a prepared upload, containing the remote path to use in Install/Upgrade +pub struct InstallPackage { + /// Path inside the AFC jail for InstallationProxy `PackagePath` + pub remote_package_path: String, + + // Each package type has a special option that has to be passed + pub options: plist::Value, +} + +/// Represent the type of package being installed. +pub enum PackageType { + Ipcc, // Carrier bundle package + // an IPA package needs the build id to be installed + Ipa(String), // iOS app package + Unknown, +} + +impl PackageType { + pub fn get_remote_file(&self) -> Result<&'static str, IdeviceError> { + match self { + Self::Ipcc => Ok(IPCC_REMOTE_FILE), + Self::Ipa(_) => Ok(IPA_REMOTE_FILE), + Self::Unknown => Err(IdeviceError::InstallationProxyOperationFailed( + "invalid package".into(), + )), + } + } +} + +/// Ensure `PublicStaging` exists on device via AFC +pub async fn ensure_public_staging(afc: &mut AfcClient) -> Result<(), IdeviceError> { + // Try to stat and if it fails, create directory + match afc.get_file_info(PUBLIC_STAGING).await { + Ok(_) => Ok(()), + Err(_) => afc.mk_dir(PUBLIC_STAGING).await, + } +} + +// Get the bundle id of a package by looping through it's files and looking inside of the +// `Info.plist` +pub async fn get_bundle_id(file: &mut T) -> Result +where + T: AsyncBufRead + AsyncSeek + Unpin, +{ + let mut zip_file = ZipFileReader::with_tokio(file).await?; + + for i in 0..zip_file.file().entries().len() { + let mut entry_reader = zip_file.reader_with_entry(i).await?; + let entry = entry_reader.entry(); + + let inner_file_path = entry + .filename() + .as_str() + .map_err(|_| IdeviceError::Utf8Error)? + .trim_end_matches('/'); + + let path_segments_count = inner_file_path.split('/').count(); + + // there's multiple `Info.plist` files, we only need the one that's in the root of the + // package + // + // 1 2 3 + // which is in this case: Playload -> APP_NAME.app -> Info.plist + if inner_file_path.ends_with("Info.plist") && path_segments_count == 3 { + let mut info_plist_bytes = Vec::new(); + entry_reader.read_to_end(&mut info_plist_bytes).await?; + + let info_plist: plist::Value = plist::from_bytes(&info_plist_bytes)?; + + if let Some(bundle_id) = info_plist + .as_dictionary() + .and_then(|dict| dict.get("CFBundleIdentifier")) + .and_then(|v| v.as_string()) + { + return Ok(bundle_id.to_string()); + } + } + } + + Err(IdeviceError::NotFound) +} + +/// Determines the type of package based on its content (IPA or IPCC). +pub async fn determine_package_type>( + package: &P, +) -> Result { + let mut package_cursor = BufReader::new(Cursor::new(package.as_ref())); + + let mut archive = ZipFileReader::with_tokio(&mut package_cursor).await?; + + // the first index is the first folder name, which is probably `Payload` + // + // we need the folder inside of that `Payload`, which has an extension that we can + // determine the type of the package from it, hence the second index + let inside_folder = archive.reader_with_entry(1).await?; + + let folder_name = inside_folder + .entry() + .filename() + .as_str() + .map_err(|_| IdeviceError::Utf8Error)? + .split('/') + .nth(1) + // only if the package does not have anything inside of the `Payload` folder + .ok_or(async_zip::error::ZipError::EntryIndexOutOfBounds)? + .to_string(); + + let bundle_id = get_bundle_id(&mut package_cursor).await?; + + if folder_name.ends_with(".bundle") { + Ok(PackageType::Ipcc) + } else if folder_name.ends_with(".app") { + Ok(PackageType::Ipa(bundle_id)) + } else { + Ok(PackageType::Unknown) + } +} + +/// Upload a single file to a destination path on device using AFC +pub async fn afc_upload_file>( + afc: &mut AfcClient, + file: F, + remote_path: &str, +) -> Result<(), IdeviceError> { + let mut fd = afc.open(remote_path, AfcFopenMode::WrOnly).await?; + fd.write(file.as_ref()).await?; + fd.close().await +} + +/// Recursively upload a directory to device via AFC (mirror contents) +pub async fn afc_upload_dir( + afc: &mut AfcClient, + local_dir: &Path, + remote_dir: &str, +) -> Result<(), IdeviceError> { + use std::collections::VecDeque; + afc.mk_dir(remote_dir).await.ok(); + + let mut queue: VecDeque<(std::path::PathBuf, String)> = VecDeque::new(); + queue.push_back((local_dir.to_path_buf(), remote_dir.to_string())); + + while let Some((cur_local, cur_remote)) = queue.pop_front() { + let mut rd = tokio::fs::read_dir(&cur_local).await?; + while let Some(entry) = rd.next_entry().await? { + let meta = entry.metadata().await?; + let name = entry.file_name(); + let name = name.to_string_lossy().into_owned(); + if name == "." || name == ".." { + continue; + } + let child_local = entry.path(); + let child_remote = format!("{cur_remote}/{name}"); + if meta.is_dir() { + afc.mk_dir(&child_remote).await.ok(); + queue.push_back((child_local, child_remote)); + } else if meta.is_file() { + afc_upload_file(afc, tokio::fs::read(&child_local).await?, &child_remote).await?; + } + } + } + Ok(()) +} + +/// Upload a file to `PublicStaging` and return its InstallationProxy path +async fn upload_file_to_public_staging>( + provider: &dyn IdeviceProvider, + file: P, +) -> Result { + // Connect to AFC via the generic service connector + let mut afc = AfcClient::connect(provider).await?; + + ensure_public_staging(&mut afc).await?; + + let file = file.as_ref(); + + let package_type = determine_package_type(&file).await?; + + let remote_path = format!("{PUBLIC_STAGING}/{}", package_type.get_remote_file()?); + + afc_upload_file(&mut afc, file, &remote_path).await?; + + let options = match package_type { + PackageType::Ipcc => plist!({"PackageType": "CarrierBundle"}), + PackageType::Ipa(build_id) => plist!({"CFBundleIdentifier": build_id}), + PackageType::Unknown => plist!({}), + }; + + Ok(InstallPackage { + remote_package_path: remote_path, + options, + }) +} + +/// Recursively Upload a directory of file to `PublicStaging` +async fn upload_dir_to_public_staging>( + provider: &dyn IdeviceProvider, + file: P, +) -> Result { + let mut afc = AfcClient::connect(provider).await?; + + ensure_public_staging(&mut afc).await?; + + let file = file.as_ref(); + + let remote_path = format!("{PUBLIC_STAGING}/{IPA_REMOTE_FILE}"); + + afc_upload_dir(&mut afc, file, &remote_path).await?; + + Ok(InstallPackage { + remote_package_path: remote_path, + options: plist!({"PackageType": "Developer"}), + }) +} + +pub async fn prepare_file_upload( + provider: &dyn IdeviceProvider, + data: impl AsRef<[u8]>, + caller_options: Option, +) -> Result { + let InstallPackage { + remote_package_path, + options, + } = upload_file_to_public_staging(provider, data).await?; + let full_options = plist!({ + :, + caller_options: Option, +) -> Result { + let InstallPackage { + remote_package_path, + options, + } = upload_dir_to_public_staging(provider, &local_path).await?; + + let full_options = plist!({ + :`) +//! - For `.ipa` files, we upload the whole file to `PublicStaging/` +//! - For directories (developer bundles), we recursively mirror the directory +//! into `PublicStaging/` and pass that directory path. + +mod helpers; +use std::path::Path; + +use helpers::{InstallPackage, prepare_dir_upload, prepare_file_upload}; + +use crate::{ + IdeviceError, IdeviceService, provider::IdeviceProvider, + services::installation_proxy::InstallationProxyClient, +}; + +/// Install an application by first uploading the local package and then invoking InstallationProxy. +/// +/// - Accepts a local file path or directory path. +/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults. +pub async fn install_package>( + provider: &dyn IdeviceProvider, + local_path: P, + options: Option, +) -> Result<(), IdeviceError> { + install_package_with_callback(provider, local_path, options, |_| async {}, ()).await +} + +/// Same as `install_package` but providing a callback that receives `(percent_complete, state)` +/// updates while InstallationProxy performs the operation. +pub async fn install_package_with_callback, Fut, S>( + provider: &dyn IdeviceProvider, + local_path: P, + options: Option, + callback: impl Fn((u64, S)) -> Fut, + state: S, +) -> Result<(), IdeviceError> +where + Fut: std::future::Future, + S: Clone, +{ + let metadata = tokio::fs::metadata(&local_path).await?; + + if metadata.is_dir() { + let InstallPackage { + remote_package_path, + options, + } = prepare_dir_upload(provider, local_path, options).await?; + let mut inst = InstallationProxyClient::connect(provider).await?; + + inst.upgrade_with_callback(remote_package_path, Some(options), callback, state) + .await + } else { + let data = tokio::fs::read(&local_path).await?; + install_bytes_with_callback(provider, data, options, callback, state).await + } +} + +/// Upgrade an application by first uploading the local package and then invoking InstallationProxy. +/// +/// - Accepts a local file path or directory path. +/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults. +pub async fn upgrade_package>( + provider: &dyn IdeviceProvider, + local_path: P, + options: Option, +) -> Result<(), IdeviceError> { + upgrade_package_with_callback(provider, local_path, options, |_| async {}, ()).await +} + +/// Same as `upgrade_package` but providing a callback that receives `(percent_complete, state)` +/// updates while InstallationProxy performs the operation. +pub async fn upgrade_package_with_callback, Fut, S>( + provider: &dyn IdeviceProvider, + local_path: P, + options: Option, + callback: impl Fn((u64, S)) -> Fut, + state: S, +) -> Result<(), IdeviceError> +where + Fut: std::future::Future, + S: Clone, +{ + let metadata = tokio::fs::metadata(&local_path).await?; + + if metadata.is_dir() { + let InstallPackage { + remote_package_path, + options, + } = prepare_dir_upload(provider, local_path, options).await?; + let mut inst = InstallationProxyClient::connect(provider).await?; + + inst.upgrade_with_callback(remote_package_path, Some(options), callback, state) + .await + } else { + let data = tokio::fs::read(&local_path).await?; + upgrade_bytes_with_callback(provider, data, options, callback, state).await + } +} + +/// Install an application from raw bytes by first uploading them to `PublicStaging` and then +/// invoking InstallationProxy `Install`. +/// +/// - `remote_name` determines the remote filename under `PublicStaging`. +/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults. +pub async fn install_bytes( + provider: &dyn IdeviceProvider, + data: impl AsRef<[u8]>, + options: Option, +) -> Result<(), IdeviceError> { + install_bytes_with_callback(provider, data, options, |_| async {}, ()).await +} + +/// Same as `install_bytes` but providing a callback that receives `(percent_complete, state)` +/// updates while InstallationProxy performs the install operation. +/// +/// Tip: +/// - When embedding assets into the binary, you can pass `include_bytes!("path/to/app.ipa")` +/// as the `data` argument and choose a desired `remote_name` (e.g. `"MyApp.ipa"`). +pub async fn install_bytes_with_callback( + provider: &dyn IdeviceProvider, + data: impl AsRef<[u8]>, + options: Option, + callback: impl Fn((u64, S)) -> Fut, + state: S, +) -> Result<(), IdeviceError> +where + Fut: std::future::Future, + S: Clone, +{ + let InstallPackage { + remote_package_path, + options, + } = prepare_file_upload(provider, data, options).await?; + let mut inst = InstallationProxyClient::connect(provider).await?; + + inst.install_with_callback(remote_package_path, Some(options), callback, state) + .await +} + +/// Upgrade an application from raw bytes by first uploading them to `PublicStaging` and then +/// invoking InstallationProxy `Upgrade`. +/// +/// - `remote_name` determines the remote filename under `PublicStaging`. +/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults. +pub async fn upgrade_bytes( + provider: &dyn IdeviceProvider, + data: impl AsRef<[u8]>, + options: Option, +) -> Result<(), IdeviceError> { + upgrade_bytes_with_callback(provider, data, options, |_| async {}, ()).await +} + +/// Same as `upgrade_bytes` but providing a callback that receives `(percent_complete, state)` +/// updates while InstallationProxy performs the upgrade operation. +/// +/// Tip: +/// - When embedding assets into the binary, you can pass `include_bytes!("path/to/app.ipa")` +/// as the `data` argument and choose a desired `remote_name` (e.g. `"MyApp.ipa"`). +pub async fn upgrade_bytes_with_callback( + provider: &dyn IdeviceProvider, + data: impl AsRef<[u8]>, + options: Option, + callback: impl Fn((u64, S)) -> Fut, + state: S, +) -> Result<(), IdeviceError> +where + Fut: std::future::Future, + S: Clone, +{ + let InstallPackage { + remote_package_path, + options, + } = prepare_file_upload(provider, data, options).await?; + let mut inst = InstallationProxyClient::connect(provider).await?; + + inst.upgrade_with_callback(remote_package_path, Some(options), callback, state) + .await +} From 34fb39f12dbe32f478d982c42bf3adbd6a59111b Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 16 Sep 2025 14:05:03 -0600 Subject: [PATCH 05/21] Clean up cargo clippy warnings --- idevice/Cargo.toml | 7 ++++--- tools/src/pcapd.rs | 1 - tools/src/screenshot.rs | 15 ++++++++------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 4a255e5..86a1808 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -81,11 +81,11 @@ dvt = ["dep:byteorder", "dep:ns-keyed-archive"] heartbeat = ["tokio/macros", "tokio/time"] house_arrest = ["afc"] installation_proxy = [ - "dep:async_zip", + "dep:async_zip", "dep:futures", - "async_zip/tokio", + "async_zip/tokio", "async_zip/deflate", - "tokio/fs" + "tokio/fs", ] springboardservices = [] misagent = [] @@ -135,6 +135,7 @@ full = [ "preboard_service", "restore_service", "rsd", + "screenshotr", "springboardservices", "syslog_relay", "tcp", diff --git a/tools/src/pcapd.rs b/tools/src/pcapd.rs index 167a4aa..9cef909 100644 --- a/tools/src/pcapd.rs +++ b/tools/src/pcapd.rs @@ -7,7 +7,6 @@ use idevice::{ }; mod common; -mod pcap; #[tokio::main] async fn main() { diff --git a/tools/src/screenshot.rs b/tools/src/screenshot.rs index ead4a01..9978ef4 100644 --- a/tools/src/screenshot.rs +++ b/tools/src/screenshot.rs @@ -64,8 +64,8 @@ async fn main() { return; } }; - let mut res: Vec = Vec::new(); - if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await { + + let res = if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await { let rsd_port = proxy.handshake.server_rsd_port; let adapter = proxy.create_software_tunnel().expect("no software tunnel"); @@ -85,10 +85,10 @@ async fn main() { let mut ts_client = idevice::dvt::screenshot::ScreenshotClient::new(&mut ts_client) .await .expect("Unable to get channel for take screenshot"); - res = ts_client + ts_client .take_screenshot() .await - .expect("Failed to take screenshot"); + .expect("Failed to take screenshot") } else { let mut screenshot_client = match ScreenshotService::connect(&*provider).await { Ok(client) => client, @@ -99,9 +99,10 @@ async fn main() { return; } }; - res = screenshot_client.take_screenshot().await.unwrap(); - } - match fs::write(output_path, &res) { + screenshot_client.take_screenshot().await.unwrap() + }; + + match fs::write(output_path, res) { Ok(_) => println!("Screenshot saved to: {}", output_path), Err(e) => eprintln!("Failed to write screenshot to file: {}", e), } From a2242c38dd4497604f59d1addaf129d0801fdbc8 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 16 Sep 2025 14:38:25 -0600 Subject: [PATCH 06/21] Remove iOS builds from CI checks --- .github/workflows/ci.yml | 9 +-------- justfile | 2 +- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 510daa2..e4dc6bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,8 +30,7 @@ jobs: - name: Install rustup targets run: | - rustup target add aarch64-apple-ios && rustup target add x86_64-apple-ios && \ - rustup target add aarch64-apple-ios-sim && rustup target add aarch64-apple-darwin && \ + rustup target add aarch64-apple-darwin && \ rustup target add x86_64-apple-darwin && cargo install --force --locked bindgen-cli - name: Build all Apple targets and examples/tools @@ -45,12 +44,6 @@ jobs: path: | target/*apple*/release/libidevice_ffi.a - - name: Upload macOS+iOS XCFramework - uses: actions/upload-artifact@v4 - with: - name: idevice-xcframework - path: swift/bundle.zip - - name: Upload C examples/tools uses: actions/upload-artifact@v4 with: diff --git a/justfile b/justfile index 9b3788c..9e5d593 100644 --- a/justfile +++ b/justfile @@ -6,7 +6,7 @@ check-features: ci-check: build-ffi-native build-tools-native build-cpp build-c cargo clippy --all-targets --all-features -- -D warnings cargo fmt -- --check -macos-ci-check: ci-check xcframework +macos-ci-check: ci-check cd tools && cargo build --release --target x86_64-apple-darwin windows-ci-check: build-ffi-native build-tools-native build-cpp From 23c8808ae74a922f9a162e9af07e9b0f254104d8 Mon Sep 17 00:00:00 2001 From: Nicholas Sharp Date: Sat, 20 Sep 2025 23:46:07 -0400 Subject: [PATCH 07/21] Include InvalidArgument with afc and installation_proxy features (#29) --- idevice/src/lib.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index fb55bc2..29f14d7 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -692,7 +692,10 @@ pub enum IdeviceError { #[error("failed to parse bytes as valid utf8")] Utf8Error = -56, - #[cfg(feature = "debug_proxy")] + #[cfg(any( + feature = "debug_proxy", + all(feature = "afc", feature = "installation_proxy") + ))] #[error("invalid argument passed")] InvalidArgument = -57, @@ -861,7 +864,10 @@ impl IdeviceError { IdeviceError::NotEnoughBytes(_, _) => -55, IdeviceError::Utf8Error => -56, - #[cfg(feature = "debug_proxy")] + #[cfg(any( + feature = "debug_proxy", + all(feature = "afc", feature = "installation_proxy") + ))] IdeviceError::InvalidArgument => -57, IdeviceError::UnknownErrorType(_) => -59, From 224fabfa69aaa07e1c3033877c43066b6ac682f0 Mon Sep 17 00:00:00 2001 From: Abdullah Al-Banna Date: Sun, 21 Sep 2025 19:29:08 +0300 Subject: [PATCH 08/21] Implement AFC file seek (#28) * Implement AFC file seek * refactored to be more readable --- idevice/src/services/afc/file.rs | 117 ++++++++++++++++++------------- 1 file changed, 67 insertions(+), 50 deletions(-) diff --git a/idevice/src/services/afc/file.rs b/idevice/src/services/afc/file.rs index 5727d97..49f771b 100644 --- a/idevice/src/services/afc/file.rs +++ b/idevice/src/services/afc/file.rs @@ -1,5 +1,7 @@ // Jackson Coxson +use std::io::SeekFrom; + use crate::IdeviceError; use super::{ @@ -19,28 +21,76 @@ pub struct FileDescriptor<'a> { } impl FileDescriptor<'_> { - /// Closes the file descriptor - pub async fn close(self) -> Result<(), IdeviceError> { - let header_payload = self.fd.to_le_bytes().to_vec(); + /// Generic helper to send an AFC packet and read the response + async fn send_packet( + &mut self, + opcode: AfcOpcode, + header_payload: Vec, + payload: Vec, + ) -> Result { let header_len = header_payload.len() as u64 + AfcPacketHeader::LEN; - let header = AfcPacketHeader { magic: super::MAGIC, - entire_len: header_len, + entire_len: header_len + payload.len() as u64, header_payload_len: header_len, packet_num: self.client.package_number, - operation: AfcOpcode::FileClose, + operation: opcode, }; self.client.package_number += 1; let packet = AfcPacket { header, header_payload, - payload: Vec::new(), + payload, }; self.client.send(packet).await?; - self.client.read().await?; + self.client.read().await + } + + /// Returns the current cursor position for the file + pub async fn seek_tell(&mut self) -> Result { + let header_payload = self.fd.to_le_bytes().to_vec(); + let res = self + .send_packet(AfcOpcode::FileTell, header_payload, Vec::new()) + .await?; + + let cur_pos = res + .header_payload + .get(..8) + .ok_or(IdeviceError::UnexpectedResponse)? + .try_into() + .map(u64::from_le_bytes) + .map_err(|_| IdeviceError::UnexpectedResponse)?; + + Ok(cur_pos) + } + + /// Moves the file cursor + pub async fn seek(&mut self, pos: SeekFrom) -> Result<(), IdeviceError> { + let (offset, whence) = match pos { + SeekFrom::Start(off) => (off as i64, 0), + SeekFrom::Current(off) => (off, 1), + SeekFrom::End(off) => (off, 2), + }; + + let mut header_payload = Vec::new(); + header_payload.extend(self.fd.to_le_bytes()); + header_payload.extend((whence as u64).to_le_bytes()); + header_payload.extend(offset.to_le_bytes()); + + self.send_packet(AfcOpcode::FileSeek, header_payload, Vec::new()) + .await?; + + Ok(()) + } + + /// Closes the file descriptor + pub async fn close(mut self) -> Result<(), IdeviceError> { + let header_payload = self.fd.to_le_bytes().to_vec(); + + self.send_packet(AfcOpcode::FileClose, header_payload, Vec::new()) + .await?; Ok(()) } @@ -49,32 +99,18 @@ impl FileDescriptor<'_> { /// # Returns /// A vector containing the file's data pub async fn read(&mut self) -> Result, IdeviceError> { - // Get the file size first - let mut bytes_left = self.client.get_file_info(&self.path).await?.size; + let seek_pos = self.seek_tell().await? as usize; + let file_info = self.client.get_file_info(&self.path).await?; + let mut bytes_left = file_info.size.saturating_sub(seek_pos); let mut collected_bytes = Vec::with_capacity(bytes_left); while bytes_left > 0 { let mut header_payload = self.fd.to_le_bytes().to_vec(); header_payload.extend_from_slice(&MAX_TRANSFER.to_le_bytes()); - let header_len = header_payload.len() as u64 + AfcPacketHeader::LEN; + let res = self + .send_packet(AfcOpcode::Read, header_payload, Vec::new()) + .await?; - let header = AfcPacketHeader { - magic: super::MAGIC, - entire_len: header_len, - header_payload_len: header_len, - packet_num: self.client.package_number, - operation: AfcOpcode::Read, - }; - self.client.package_number += 1; - - let packet = AfcPacket { - header, - header_payload, - payload: Vec::new(), - }; - - self.client.send(packet).await?; - let res = self.client.read().await?; bytes_left -= res.payload.len(); collected_bytes.extend(res.payload); } @@ -87,29 +123,10 @@ impl FileDescriptor<'_> { /// # Arguments /// * `bytes` - Data to write to the file pub async fn write(&mut self, bytes: &[u8]) -> Result<(), IdeviceError> { - let chunks = bytes.chunks(MAX_TRANSFER as usize); - - for chunk in chunks { + for chunk in bytes.chunks(MAX_TRANSFER as usize) { let header_payload = self.fd.to_le_bytes().to_vec(); - let header_len = header_payload.len() as u64 + AfcPacketHeader::LEN; - - let header = AfcPacketHeader { - magic: super::MAGIC, - entire_len: header_len + chunk.len() as u64, - header_payload_len: header_len, - packet_num: self.client.package_number, - operation: AfcOpcode::Write, - }; - self.client.package_number += 1; - - let packet = AfcPacket { - header, - header_payload, - payload: chunk.to_vec(), - }; - - self.client.send(packet).await?; - self.client.read().await?; + self.send_packet(AfcOpcode::Write, header_payload, chunk.to_vec()) + .await?; } Ok(()) } From 54dbbbb558a14e6e10f2f72d3a07ece12f4728b2 Mon Sep 17 00:00:00 2001 From: ValorBao <139973913+ValorBao@users.noreply.github.com> Date: Tue, 23 Sep 2025 00:43:29 +0800 Subject: [PATCH 09/21] feat: support simulate location below ios17 (#30) * "feat: support simulate location below ios17" * "cargo fmt" --- idevice/src/services/mod.rs | 3 + idevice/src/services/simulate_location.rs | 38 ++++++ tools/src/location_simulation.rs | 142 ++++++++++++++-------- 3 files changed, 132 insertions(+), 51 deletions(-) create mode 100644 idevice/src/services/simulate_location.rs diff --git a/idevice/src/services/mod.rs b/idevice/src/services/mod.rs index e5e3c91..a4d520d 100644 --- a/idevice/src/services/mod.rs +++ b/idevice/src/services/mod.rs @@ -47,3 +47,6 @@ pub mod screenshotr; pub mod springboardservices; #[cfg(feature = "syslog_relay")] pub mod syslog_relay; + +#[cfg(feature = "location_simulation")] +pub mod simulate_location; diff --git a/idevice/src/services/simulate_location.rs b/idevice/src/services/simulate_location.rs new file mode 100644 index 0000000..dd78f64 --- /dev/null +++ b/idevice/src/services/simulate_location.rs @@ -0,0 +1,38 @@ +use crate::{Idevice, IdeviceError, IdeviceService, obf}; + +pub struct LocationSimulationService { + idevice: Idevice, +} + +impl IdeviceService for LocationSimulationService { + fn service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.dt.simulatelocation") + } + + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) + } +} + +impl LocationSimulationService { + pub fn new(idevice: Idevice) -> Self { + Self { idevice } + } + + pub async fn clear(&mut self) -> Result<(), IdeviceError> { + let message: [u8; 4] = [0x00, 0x00, 0x00, 0x01]; + self.idevice.send_raw(&message).await?; + Ok(()) + } + + pub async fn set(&mut self, latitude: &str, longtiude: &str) -> Result<(), IdeviceError> { + let message: [u8; 4] = [0x00, 0x00, 0x00, 0x00]; + let latitude_len = latitude.len() as u32; + let longtiude_len = longtiude.len() as u32; + let latitude_bytes = [&latitude_len.to_be_bytes(), latitude.as_bytes()].concat(); + let longitude_bytes = [&longtiude_len.to_be_bytes(), longtiude.as_bytes()].concat(); + let data = [&message[..], &latitude_bytes[..], &longitude_bytes[..]].concat(); + self.idevice.send_raw(data.as_slice()).await?; + Ok(()) + } +} diff --git a/tools/src/location_simulation.rs b/tools/src/location_simulation.rs index c017fbb..2f0a128 100644 --- a/tools/src/location_simulation.rs +++ b/tools/src/location_simulation.rs @@ -4,6 +4,8 @@ use clap::{Arg, Command}; use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; +use idevice::dvt::location_simulation::LocationSimulationClient; +use idevice::services::simulate_location::LocationSimulationService; mod common; #[tokio::main] @@ -63,65 +65,103 @@ async fn main() { return; } }; - let proxy = CoreDeviceProxy::connect(&*provider) + + if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await { + let rsd_port = proxy.handshake.server_rsd_port; + + let adapter = proxy.create_software_tunnel().expect("no software tunnel"); + let mut adapter = adapter.to_async_handle(); + let stream = adapter.connect(rsd_port).await.expect("no RSD connect"); + + // Make the connection to RemoteXPC + let mut handshake = RsdHandshake::new(stream).await.unwrap(); + + let mut ls_client = idevice::dvt::remote_server::RemoteServerClient::connect_rsd( + &mut adapter, + &mut handshake, + ) .await - .expect("no core proxy"); - let rsd_port = proxy.handshake.server_rsd_port; - - let adapter = proxy.create_software_tunnel().expect("no software tunnel"); - let mut adapter = adapter.to_async_handle(); - let stream = adapter.connect(rsd_port).await.expect("no RSD connect"); - - // Make the connection to RemoteXPC - let mut handshake = RsdHandshake::new(stream).await.unwrap(); - - let mut ls_client = - idevice::dvt::remote_server::RemoteServerClient::connect_rsd(&mut adapter, &mut handshake) - .await - .expect("Failed to connect"); - ls_client.read_message(0).await.expect("no read??"); - - let mut ls_client = - idevice::dvt::location_simulation::LocationSimulationClient::new(&mut ls_client) + .expect("Failed to connect"); + ls_client.read_message(0).await.expect("no read??"); + let mut ls_client = LocationSimulationClient::new(&mut ls_client) .await .expect("Unable to get channel for location simulation"); - - if matches.subcommand_matches("clear").is_some() { - ls_client.clear().await.expect("Unable to clear"); - println!("Location cleared!"); - } else if let Some(matches) = matches.subcommand_matches("set") { - let latitude: &String = match matches.get_one("latitude") { - Some(l) => l, - None => { - eprintln!("No latitude passed! Pass -h for help"); - return; - } - }; - let latitude: f64 = latitude.parse().expect("Failed to parse as float"); - let longitude: &String = match matches.get_one("longitude") { - Some(l) => l, - None => { - eprintln!("No longitude passed! Pass -h for help"); - return; - } - }; - let longitude: f64 = longitude.parse().expect("Failed to parse as float"); - ls_client - .set(latitude, longitude) - .await - .expect("Failed to set location"); - - println!("Location set!"); - println!("Press ctrl-c to stop"); - loop { + if matches.subcommand_matches("clear").is_some() { + ls_client.clear().await.expect("Unable to clear"); + println!("Location cleared!"); + } else if let Some(matches) = matches.subcommand_matches("set") { + let latitude: &String = match matches.get_one("latitude") { + Some(l) => l, + None => { + eprintln!("No latitude passed! Pass -h for help"); + return; + } + }; + let latitude: f64 = latitude.parse().expect("Failed to parse as float"); + let longitude: &String = match matches.get_one("longitude") { + Some(l) => l, + None => { + eprintln!("No longitude passed! Pass -h for help"); + return; + } + }; + let longitude: f64 = longitude.parse().expect("Failed to parse as float"); ls_client .set(latitude, longitude) .await .expect("Failed to set location"); - tokio::time::sleep(std::time::Duration::from_secs(5)).await; + + println!("Location set!"); + println!("Press ctrl-c to stop"); + loop { + ls_client + .set(latitude, longitude) + .await + .expect("Failed to set location"); + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + } + } else { + eprintln!("Invalid usage, pass -h for help"); } } else { - eprintln!("Invalid usage, pass -h for help"); - } + let mut location_client = match LocationSimulationService::connect(&*provider).await { + Ok(client) => client, + Err(e) => { + eprintln!( + "Unable to connect to simulate_location service: {e} Ensure Developer Disk Image is mounted." + ); + return; + } + }; + if matches.subcommand_matches("clear").is_some() { + location_client.clear().await.expect("Unable to clear"); + println!("Location cleared!"); + } else if let Some(matches) = matches.subcommand_matches("set") { + let latitude: &String = match matches.get_one("latitude") { + Some(l) => l, + None => { + eprintln!("No latitude passed! Pass -h for help"); + return; + } + }; + + let longitude: &String = match matches.get_one("longitude") { + Some(l) => l, + None => { + eprintln!("No longitude passed! Pass -h for help"); + return; + } + }; + location_client + .set(latitude, longitude) + .await + .expect("Failed to set location"); + + println!("Location set!"); + } else { + eprintln!("Invalid usage, pass -h for help"); + } + }; + return; } From 7506ecd62e0f40f046ffc08c03ef6de8b6233cde Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Sun, 21 Sep 2025 10:45:12 -0600 Subject: [PATCH 10/21] Create files for mobileactivationd --- idevice/Cargo.toml | 2 + idevice/src/services/mobileactivationd.rs | 91 +++++++++++++++++ idevice/src/services/mod.rs | 2 + tools/Cargo.toml | 5 +- tools/src/activation.rs | 114 ++++++++++++++++++++++ 5 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 idevice/src/services/mobileactivationd.rs create mode 100644 tools/src/activation.rs diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 86a1808..8ce15ff 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -90,6 +90,7 @@ installation_proxy = [ springboardservices = [] misagent = [] mobile_image_mounter = ["dep:sha2"] +mobileactivationd = ["dep:reqwest"] mobilebackup2 = [] location_simulation = [] pair = ["chrono/default", "tokio/time", "dep:sha2", "dep:rsa", "dep:x509-cert"] @@ -129,6 +130,7 @@ full = [ "location_simulation", "misagent", "mobile_image_mounter", + "mobileactivationd", "mobilebackup2", "pair", "pcapd", diff --git a/idevice/src/services/mobileactivationd.rs b/idevice/src/services/mobileactivationd.rs new file mode 100644 index 0000000..6a5aca1 --- /dev/null +++ b/idevice/src/services/mobileactivationd.rs @@ -0,0 +1,91 @@ +//! mobileactivationd activates iOS devices. +//! This isn't a normal service, as it requires a new connection for each request. +//! As such, this service requires a provider itself, instead of temporary usage of one. + +use plist::Dictionary; + +use crate::{Idevice, IdeviceError, IdeviceService, lockdown::LockdownClient, obf}; + +pub struct MobileActivationdClient<'a> { + provider: &'a dyn crate::provider::IdeviceProvider, +} + +/// Internal structure for temporary service connections. +/// This struct exists to take advantage of the service trait. +struct MobileActivationdInternal { + pub idevice: Idevice, +} + +impl IdeviceService for MobileActivationdInternal { + /// Returns the service name as registered with lockdownd + fn service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.mobileactivationd") + } + + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) + } +} + +impl MobileActivationdInternal { + fn new(idevice: Idevice) -> Self { + Self { idevice } + } +} + +impl<'a> MobileActivationdClient<'a> { + pub fn new(provider: &'a dyn crate::provider::IdeviceProvider) -> Self { + Self { provider } + } + + pub async fn state(&self) -> Result { + if let Ok(res) = self.send_command("GetActivationStateRequest", None).await + && let Some(v) = res.get("Value").and_then(|x| x.as_string()) + { + Ok(v.to_string()) + } else { + let mut lc = LockdownClient::connect(self.provider).await?; + lc.start_session(&self.provider.get_pairing_file().await?) + .await?; + + let res = lc.get_value(Some("ActivationState"), None).await?; + if let Some(v) = res.as_string() { + Ok(v.to_string()) + } else { + Err(IdeviceError::UnexpectedResponse) + } + } + } + + pub async fn activated(&self) -> Result { + Ok(self.state().await? == "Activated") + } + + /// Deactivates the device. + /// Protocol gives no response on whether it worked or not, so good luck + pub async fn deactivate(&self) -> Result<(), IdeviceError> { + self.send_command("DeactivateRequest", None).await?; + Ok(()) + } + + async fn send_command( + &self, + command: impl Into, + value: Option<&str>, + ) -> Result { + let mut service = self.service_connect().await?; + let command = command.into(); + let req = crate::plist!({ + "Command": command, + "Value":? value, + }); + service.send_plist(req).await?; + service.read_plist().await + } + + async fn service_connect(&self) -> Result { + Ok(MobileActivationdInternal::connect(self.provider) + .await? + .idevice) + } +} diff --git a/idevice/src/services/mod.rs b/idevice/src/services/mod.rs index a4d520d..f827678 100644 --- a/idevice/src/services/mod.rs +++ b/idevice/src/services/mod.rs @@ -29,6 +29,8 @@ pub mod lockdown; pub mod misagent; #[cfg(feature = "mobile_image_mounter")] pub mod mobile_image_mounter; +#[cfg(feature = "mobileactivationd")] +pub mod mobileactivationd; #[cfg(feature = "mobilebackup2")] pub mod mobilebackup2; #[cfg(feature = "syslog_relay")] diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 3c63429..95b4a42 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -125,11 +125,14 @@ path = "src/pcapd.rs" name = "preboard" path = "src/preboard.rs" - [[bin]] name = "screenshot" path = "src/screenshot.rs" +[[bin]] +name = "activation" +path = "src/activation.rs" + [dependencies] idevice = { path = "../idevice", features = ["full"], default-features = false } tokio = { version = "1.43", features = ["full"] } diff --git a/tools/src/activation.rs b/tools/src/activation.rs new file mode 100644 index 0000000..74bef0e --- /dev/null +++ b/tools/src/activation.rs @@ -0,0 +1,114 @@ +// Jackson Coxson + +use clap::{Arg, Command}; +use idevice::{ + IdeviceService, amfi::AmfiClient, lockdown::LockdownClient, + mobileactivationd::MobileActivationdClient, +}; + +mod common; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let matches = Command::new("activation") + .about("mobileactivationd") + .arg( + Arg::new("host") + .long("host") + .value_name("HOST") + .help("IP address of the device"), + ) + .arg( + Arg::new("pairing_file") + .long("pairing-file") + .value_name("PATH") + .help("Path to the pairing file"), + ) + .arg( + Arg::new("udid") + .value_name("UDID") + .help("UDID of the device (overrides host/pairing file)") + .index(1), + ) + .arg( + Arg::new("about") + .long("about") + .help("Show about information") + .action(clap::ArgAction::SetTrue), + ) + .subcommand(Command::new("state").about("Gets the activation state")) + .subcommand(Command::new("deactivate").about("Deactivates the device")) + .get_matches(); + + if matches.get_flag("about") { + println!("activation - activate the device"); + println!("Copyright (c) 2025 Jackson Coxson"); + return; + } + + let udid = matches.get_one::("udid"); + let host = matches.get_one::("host"); + let pairing_file = matches.get_one::("pairing_file"); + + let provider = match common::get_provider(udid, host, pairing_file, "activation-jkcoxson").await + { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + + let activation_client = MobileActivationdClient::new(&*provider); + let mut lc = LockdownClient::connect(&*provider) + .await + .expect("no lockdown"); + lc.start_session(&provider.get_pairing_file().await.unwrap()) + .await + .expect("no TLS"); + let udid = lc + .get_value(Some("UniqueDeviceID"), None) + .await + .expect("no udid") + .into_string() + .unwrap(); + + if matches.subcommand_matches("state").is_some() { + let s = activation_client.state().await.expect("no state"); + println!("Activation State: {s}"); + } else if matches.subcommand_matches("deactivate").is_some() { + println!("CAUTION: You are deactivating {udid}, press enter to continue."); + let mut input = String::new(); + std::io::stdin().read_line(&mut input).ok(); + activation_client.deactivate().await.expect("no deactivate"); + // } else if matches.subcommand_matches("accept").is_some() { + // amfi_client + // .accept_developer_mode() + // .await + // .expect("Failed to show"); + // } else if matches.subcommand_matches("status").is_some() { + // let status = amfi_client + // .get_developer_mode_status() + // .await + // .expect("Failed to get status"); + // println!("Enabled: {status}"); + // } else if let Some(matches) = matches.subcommand_matches("state") { + // let uuid: &String = match matches.get_one("uuid") { + // Some(u) => u, + // None => { + // eprintln!("No UUID passed. Invalid usage, pass -h for help"); + // return; + // } + // }; + // let status = amfi_client + // .trust_app_signer(uuid) + // .await + // .expect("Failed to get state"); + // println!("Enabled: {status}"); + } else { + eprintln!("Invalid usage, pass -h for help"); + } + return; +} From 2e6a0a72a5c43fc658329df9b9f1a83a2f200df4 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 24 Sep 2025 12:07:23 -0600 Subject: [PATCH 11/21] Add DeepWiki badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 50a311a..05e3fea 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ this library interfaces with lockdownd and usbmuxd to perform actions on an iOS device that a Mac normally would. For help and information, join the [idevice Discord](https://discord.gg/qtgv6QtYbV) +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/jkcoxson/idevice) ## State From d2b9b7acba21e220c769eb39b319dcd1c81f167d Mon Sep 17 00:00:00 2001 From: se2crid <151872490+se2crid@users.noreply.github.com> Date: Wed, 24 Sep 2025 20:39:48 +0200 Subject: [PATCH 12/21] Update README.md (#32) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 05e3fea..2311f81 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ async fn main() { // We'll ask usbmuxd for a device let mut usbmuxd = UsbmuxdConnection::default() .await - .expect("Unable to connect to usbmxud") + .expect("Unable to connect to usbmuxd"); let devs = usbmuxd.get_devices().unwrap(); if devs.is_empty() { eprintln!("No devices connected!"); @@ -119,12 +119,12 @@ async fn main() { } ``` -More examples are in the ``tools`` crate and in the crate documentation. +More examples are in the [`tools`](tools/) crate and in the crate documentation. ## FFI For use in other languages, a small FFI crate has been created to start exposing -idevice. Example C programs can be found in this repository. +idevice. Example C programs can be found in the [`ffi/examples`](ffi/examples/) directory. ## Version Policy From ec811693471f6af4201e06a16a15a294afbd9f55 Mon Sep 17 00:00:00 2001 From: ValorBao <139973913+ValorBao@users.noreply.github.com> Date: Thu, 25 Sep 2025 23:34:13 +0800 Subject: [PATCH 13/21] Feature/dvt_notifications (#31) * "feat: Monitor memory and app notifications " * "[MOD] treat both as the negative and positive representation of the channel code in the response" * "feat: dvt add the notifications to Monitor memory and app notifications" * "[MOD]print the notifications in tools" * PR cleanup * Fix clippy warning --------- Co-authored-by: Jackson Coxson --- idevice/src/services/dvt/message.rs | 4 +- idevice/src/services/dvt/mod.rs | 1 + idevice/src/services/dvt/notifications.rs | 163 ++++++++++++++++++++++ idevice/src/services/dvt/remote_server.rs | 1 + tools/Cargo.toml | 5 + tools/src/activation.rs | 3 +- tools/src/notifications.rs | 101 ++++++++++++++ 7 files changed, 275 insertions(+), 3 deletions(-) create mode 100644 idevice/src/services/dvt/notifications.rs create mode 100644 tools/src/notifications.rs diff --git a/idevice/src/services/dvt/message.rs b/idevice/src/services/dvt/message.rs index 36e4fbe..a852ce8 100644 --- a/idevice/src/services/dvt/message.rs +++ b/idevice/src/services/dvt/message.rs @@ -405,7 +405,9 @@ impl Message { length: u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]), identifier: u32::from_le_bytes([buf[16], buf[17], buf[18], buf[19]]), conversation_index: u32::from_le_bytes([buf[20], buf[21], buf[22], buf[23]]), - channel: u32::from_le_bytes([buf[24], buf[25], buf[26], buf[27]]), + //treat both as the negative and positive representation of the channel code in the response + // the same when performing fragmentation + channel: i32::abs(i32::from_le_bytes([buf[24], buf[25], buf[26], buf[27]])) as u32, expects_reply: u32::from_le_bytes([buf[28], buf[29], buf[30], buf[31]]) == 1, }; if header.fragment_count > 1 && header.fragment_id == 0 { diff --git a/idevice/src/services/dvt/mod.rs b/idevice/src/services/dvt/mod.rs index 86a5fa6..71e9b1f 100644 --- a/idevice/src/services/dvt/mod.rs +++ b/idevice/src/services/dvt/mod.rs @@ -7,6 +7,7 @@ use crate::{Idevice, IdeviceError, ReadWrite, RsdService, obf}; #[cfg(feature = "location_simulation")] pub mod location_simulation; pub mod message; +pub mod notifications; pub mod process_control; pub mod remote_server; pub mod screenshot; diff --git a/idevice/src/services/dvt/notifications.rs b/idevice/src/services/dvt/notifications.rs new file mode 100644 index 0000000..f942ad4 --- /dev/null +++ b/idevice/src/services/dvt/notifications.rs @@ -0,0 +1,163 @@ +//! Notificaitons service client for iOS instruments protocol. +//! +//! Monitor memory and app notifications + +use crate::{ + IdeviceError, ReadWrite, + dvt::{ + message::AuxValue, + remote_server::{Channel, RemoteServerClient}, + }, + obf, +}; +use log::warn; +use plist::Value; + +#[derive(Debug)] +pub struct NotificationInfo { + notification_type: String, + mach_absolute_time: i64, + exec_name: String, + app_name: String, + pid: u32, + state_description: String, +} + +pub struct NotificationsClient<'a, R: ReadWrite> { + /// The underlying channel used for communication + pub channel: Channel<'a, R>, +} + +impl<'a, R: ReadWrite> NotificationsClient<'a, R> { + /// Opens a new channel on the remote server client for app notifications + /// + /// # Arguments + /// * `client` - The remote server client to connect with + /// + /// # Returns + /// The client on success, IdeviceError on failure + pub async fn new(client: &'a mut RemoteServerClient) -> Result { + let channel = client + .make_channel(obf!( + "com.apple.instruments.server.services.mobilenotifications" + )) + .await?; // Drop `&mut client` before continuing + + Ok(Self { channel }) + } + + /// set the applicaitons and memory notifications enabled + pub async fn start_notifications(&mut self) -> Result<(), IdeviceError> { + let application_method = Value::String("setApplicationStateNotificationsEnabled:".into()); + self.channel + .call_method( + Some(application_method), + Some(vec![AuxValue::archived_value(true)]), + false, + ) + .await?; + let memory_method = Value::String("setMemoryNotificationsEnabled:".into()); + self.channel + .call_method( + Some(memory_method), + Some(vec![AuxValue::archived_value(true)]), + false, + ) + .await?; + Ok(()) + } + + /// Reads the next notification from the service + pub async fn get_notification(&mut self) -> Result { + let message = self.channel.read_message().await?; + let mut notification = NotificationInfo { + notification_type: "".to_string(), + mach_absolute_time: 0, + exec_name: String::new(), + app_name: String::new(), + pid: 0, + state_description: String::new(), + }; + if let Some(aux) = message.aux { + for v in aux.values { + match v { + AuxValue::Array(a) => match ns_keyed_archive::decode::from_bytes(&a) { + Ok(archive) => { + if let Some(dict) = archive.into_dictionary() { + for (key, value) in dict.into_iter() { + match key.as_str() { + "mach_absolute_time" => { + if let Value::Integer(time) = value { + notification.mach_absolute_time = + time.as_signed().unwrap_or(0); + } + } + "execName" => { + if let Value::String(name) = value { + notification.exec_name = name; + } + } + "appName" => { + if let Value::String(name) = value { + notification.app_name = name; + } + } + "pid" => { + if let Value::Integer(pid) = value { + notification.pid = + pid.as_unsigned().unwrap_or(0) as u32; + } + } + "state_description" => { + if let Value::String(desc) = value { + notification.state_description = desc; + } + } + _ => { + warn!("Unknown notificaton key: {} = {:?}", key, value); + } + } + } + } + } + Err(e) => { + warn!("Failed to decode archive: {:?}", e); + } + }, + _ => { + warn!("Non-array aux value: {:?}", v); + } + } + } + } + + if let Some(Value::String(data)) = message.data { + notification.notification_type = data; + Ok(notification) + } else { + Err(IdeviceError::UnexpectedResponse) + } + } + + /// set the applicaitons and memory notifications disable + pub async fn stop_notifications(&mut self) -> Result<(), IdeviceError> { + let application_method = Value::String("setApplicationStateNotificationsEnabled:".into()); + self.channel + .call_method( + Some(application_method), + Some(vec![AuxValue::archived_value(false)]), + false, + ) + .await?; + let memory_method = Value::String("setMemoryNotificationsEnabled:".into()); + self.channel + .call_method( + Some(memory_method), + Some(vec![AuxValue::archived_value(false)]), + false, + ) + .await?; + + Ok(()) + } +} diff --git a/idevice/src/services/dvt/remote_server.rs b/idevice/src/services/dvt/remote_server.rs index c4111a8..82da4c8 100644 --- a/idevice/src/services/dvt/remote_server.rs +++ b/idevice/src/services/dvt/remote_server.rs @@ -201,6 +201,7 @@ impl RemoteServerClient { let message = Message::new(mheader, pheader, aux, data); debug!("Sending message: {message:#?}"); + self.idevice.write_all(&message.serialize()).await?; self.idevice.flush().await?; diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 95b4a42..43d0d5e 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -125,6 +125,7 @@ path = "src/pcapd.rs" name = "preboard" path = "src/preboard.rs" + [[bin]] name = "screenshot" path = "src/screenshot.rs" @@ -133,6 +134,10 @@ path = "src/screenshot.rs" name = "activation" path = "src/activation.rs" +[[bin]] +name = "notifications" +path = "src/notifications.rs" + [dependencies] idevice = { path = "../idevice", features = ["full"], default-features = false } tokio = { version = "1.43", features = ["full"] } diff --git a/tools/src/activation.rs b/tools/src/activation.rs index 74bef0e..6746608 100644 --- a/tools/src/activation.rs +++ b/tools/src/activation.rs @@ -2,8 +2,7 @@ use clap::{Arg, Command}; use idevice::{ - IdeviceService, amfi::AmfiClient, lockdown::LockdownClient, - mobileactivationd::MobileActivationdClient, + IdeviceService, lockdown::LockdownClient, mobileactivationd::MobileActivationdClient, }; mod common; diff --git a/tools/src/notifications.rs b/tools/src/notifications.rs new file mode 100644 index 0000000..8428ef3 --- /dev/null +++ b/tools/src/notifications.rs @@ -0,0 +1,101 @@ +// Monitor memory and app notifications + +use clap::{Arg, Command}; +use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; +mod common; + +#[tokio::main] +async fn main() { + env_logger::init(); + let matches = Command::new("notifications") + .about("start notifications") + .arg( + Arg::new("host") + .long("host") + .value_name("HOST") + .help("IP address of the device"), + ) + .arg( + Arg::new("pairing_file") + .long("pairing-file") + .value_name("PATH") + .help("Path to the pairing file"), + ) + .arg( + Arg::new("udid") + .value_name("UDID") + .help("UDID of the device (overrides host/pairing file)") + .index(1), + ) + .arg( + Arg::new("about") + .long("about") + .help("Show about information") + .action(clap::ArgAction::SetTrue), + ) + .get_matches(); + + if matches.get_flag("about") { + print!("notifications - start notifications to ios device"); + println!("Copyright (c) 2025 Jackson Coxson"); + return; + } + + let udid = matches.get_one::("udid"); + let host = matches.get_one::("host"); + let pairing_file = matches.get_one::("pairing_file"); + + let provider = + match common::get_provider(udid, host, pairing_file, "notifications-jkcoxson").await { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + + let proxy = CoreDeviceProxy::connect(&*provider) + .await + .expect("no core proxy"); + let rsd_port = proxy.handshake.server_rsd_port; + + let adapter = proxy.create_software_tunnel().expect("no software tunnel"); + + let mut adapter = adapter.to_async_handle(); + let stream = adapter.connect(rsd_port).await.expect("no RSD connect"); + + // Make the connection to RemoteXPC + let mut handshake = RsdHandshake::new(stream).await.unwrap(); + let mut ts_client = + idevice::dvt::remote_server::RemoteServerClient::connect_rsd(&mut adapter, &mut handshake) + .await + .expect("Failed to connect"); + ts_client.read_message(0).await.expect("no read??"); + let mut notification_client = + idevice::dvt::notifications::NotificationsClient::new(&mut ts_client) + .await + .expect("Unable to get channel for notifications"); + notification_client + .start_notifications() + .await + .expect("Failed to start notifications"); + + // Handle Ctrl+C gracefully + loop { + tokio::select! { + _ = tokio::signal::ctrl_c() => { + println!("\nShutdown signal received, exiting."); + break; + } + + // Branch 2: Wait for the next batch of notifications. + result = notification_client.get_notification() => { + if let Err(e) = result { + eprintln!("Failed to get notifications: {}", e); + } else { + println!("Received notifications: {:#?}", result.unwrap()); + } + } + } + } +} From 9f7e57bb21d31f0ecf107eda36c19640c2e056cb Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Thu, 25 Sep 2025 10:04:55 -0600 Subject: [PATCH 14/21] Add cpp bindings for image mounter --- Cargo.lock | 4 +- cpp/examples/mounter.cpp | 230 ++++++++++++++++ cpp/include/idevice++/lockdown.hpp | 1 - .../idevice++/mobile_image_mounter.hpp | 87 +++++++ cpp/include/idevice++/option.hpp | 29 +++ cpp/src/mobile_image_mounter.cpp | 246 ++++++++++++++++++ ffi/Cargo.toml | 2 +- 7 files changed, 595 insertions(+), 4 deletions(-) create mode 100644 cpp/examples/mounter.cpp create mode 100644 cpp/include/idevice++/mobile_image_mounter.hpp create mode 100644 cpp/src/mobile_image_mounter.cpp diff --git a/Cargo.lock b/Cargo.lock index ae33fa5..9e59f73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1825,9 +1825,9 @@ dependencies = [ [[package]] name = "plist_ffi" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a5ca928241bc2e8c5fd28b81772962389efdbfcb71dfc9ec694369e063cb3a" +checksum = "35ed070b06d9f2fdd7e816ef784fb07b09672f2acf37527f810dbedf450b7769" dependencies = [ "cbindgen", "cc", diff --git a/cpp/examples/mounter.cpp b/cpp/examples/mounter.cpp new file mode 100644 index 0000000..b6598e8 --- /dev/null +++ b/cpp/examples/mounter.cpp @@ -0,0 +1,230 @@ +#include +#include +#include +#include +#include + +// Idevice++ library headers +#include +#include +#include +#include +#include + +// --- Helper Functions --- + +/** + * @brief Reads an entire file into a byte vector. + * @param path The path to the file. + * @return A vector containing the file's data. + */ +std::vector read_file(const std::string& path) { + std::ifstream file(path, std::ios::binary | std::ios::ate); + if (!file) { + throw std::runtime_error("Failed to open file: " + path); + } + std::streamsize size = file.tellg(); + file.seekg(0, std::ios::beg); + std::vector buffer(size); + if (!file.read(reinterpret_cast(buffer.data()), size)) { + throw std::runtime_error("Failed to read file: " + path); + } + return buffer; +} + +/** + * @brief Prints the command usage instructions. + */ +void print_usage(const char* prog_name) { + std::cerr << "Usage: " << prog_name << " [options] \n\n" + << "A tool to manage developer images on a device.\n\n" + << "Options:\n" + << " --udid Target a specific device by its UDID.\n\n" + << "Subcommands:\n" + << " list List mounted images.\n" + << " unmount Unmount the developer image.\n" + << " mount [mount_options] Mount a developer image.\n\n" + << "Mount Options:\n" + << " --image (Required) Path to the DeveloperDiskImage.dmg.\n" + << " --signature (Required for iOS < 17) Path to the .signature file.\n" + << " --manifest (Required for iOS 17+) Path to the BuildManifest.plist.\n" + << " --trustcache (Required for iOS 17+) Path to the trust cache file.\n" + << std::endl; +} + +// --- Main Logic --- + +int main(int argc, char** argv) { + idevice_init_logger(Debug, Disabled, NULL); + // --- 1. Argument Parsing --- + if (argc < 2) { + print_usage(argv[0]); + return 1; + } + + std::string udid_arg; + std::string subcommand; + std::string image_path; + std::string signature_path; + std::string manifest_path; + std::string trustcache_path; + + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + if (arg == "--udid" && i + 1 < argc) { + udid_arg = argv[++i]; + } else if (arg == "--image" && i + 1 < argc) { + image_path = argv[++i]; + } else if (arg == "--signature" && i + 1 < argc) { + signature_path = argv[++i]; + } else if (arg == "--manifest" && i + 1 < argc) { + manifest_path = argv[++i]; + } else if (arg == "--trustcache" && i + 1 < argc) { + trustcache_path = argv[++i]; + } else if (arg == "list" || arg == "mount" || arg == "unmount") { + subcommand = arg; + } else if (arg == "--help" || arg == "-h") { + print_usage(argv[0]); + return 0; + } + } + + if (subcommand.empty()) { + std::cerr << "Error: No subcommand specified. Use 'list', 'mount', or 'unmount'." + << std::endl; + print_usage(argv[0]); + return 1; + } + + try { + // --- 2. Device Connection --- + auto u = + IdeviceFFI::UsbmuxdConnection::default_new(0).expect("Failed to connect to usbmuxd"); + auto devices = u.get_devices().expect("Failed to get devices from usbmuxd"); + if (devices.empty()) { + throw std::runtime_error("No devices connected."); + } + + IdeviceFFI::UsbmuxdDevice* target_dev = nullptr; + if (!udid_arg.empty()) { + for (auto& dev : devices) { + if (dev.get_udid().unwrap_or("") == udid_arg) { + target_dev = &dev; + break; + } + } + if (!target_dev) { + throw std::runtime_error("Device with UDID " + udid_arg + " not found."); + } + } else { + target_dev = &devices[0]; // Default to the first device + } + + auto udid = target_dev->get_udid().expect("Device has no UDID"); + auto id = target_dev->get_id().expect("Device has no ID"); + + IdeviceFFI::UsbmuxdAddr addr = IdeviceFFI::UsbmuxdAddr::default_new(); + auto prov = IdeviceFFI::Provider::usbmuxd_new(std::move(addr), 0, udid, id, "mounter-tool") + .expect("Failed to create provider"); + + // --- 3. Connect to Lockdown & Get iOS Version --- + auto lockdown_client = + IdeviceFFI::Lockdown::connect(prov).expect("Lockdown connect failed"); + + auto pairing_file = prov.get_pairing_file().expect("Failed to get pairing file"); + lockdown_client.start_session(pairing_file).expect("Failed to start session"); + + auto version_plist = lockdown_client.get_value("ProductVersion", NULL) + .expect("Failed to get ProductVersion"); + PList::String version_node(version_plist); + std::string version_str = version_node.GetValue(); + std::cout << "Version string: " << version_str << std::endl; + + if (version_str.empty()) { + throw std::runtime_error( + "Failed to get a valid ProductVersion string from the device."); + } + int major_version = std::stoi(version_str); + + // --- 4. Connect to MobileImageMounter --- + auto mounter_client = IdeviceFFI::MobileImageMounter::connect(prov).expect( + "Failed to connect to image mounter"); + + // --- 5. Execute Subcommand --- + if (subcommand == "list") { + auto images = mounter_client.copy_devices().expect("Failed to get images"); + std::cout << "Mounted Images:\n"; + for (plist_t p : images) { + PList::Dictionary dict(p); + std::cout << dict.ToXml() << std::endl; + } + + } else if (subcommand == "unmount") { + const char* unmount_path = (major_version < 17) ? "/Developer" : "/System/Developer"; + mounter_client.unmount_image(unmount_path).expect("Failed to unmount image"); + std::cout << "Successfully unmounted image from " << unmount_path << std::endl; + + } else if (subcommand == "mount") { + if (image_path.empty()) { + throw std::runtime_error("Mount command requires --image "); + } + auto image_data = read_file(image_path); + + if (major_version < 17) { + if (signature_path.empty()) { + throw std::runtime_error("iOS < 17 requires --signature "); + } + auto signature_data = read_file(signature_path); + mounter_client + .mount_developer(image_data.data(), + image_data.size(), + signature_data.data(), + signature_data.size()) + .expect("Failed to mount developer image"); + } else { // iOS 17+ + if (manifest_path.empty() || trustcache_path.empty()) { + throw std::runtime_error("iOS 17+ requires --manifest and --trustcache paths"); + } + auto manifest_data = read_file(manifest_path); + auto trustcache_data = read_file(trustcache_path); + + auto chip_id_plist = lockdown_client.get_value(nullptr, "UniqueChipID") + .expect("Failed to get UniqueChipID"); + PList::Integer chip_id_node(chip_id_plist); + uint64_t unique_chip_id = chip_id_node.GetValue(); + + std::function progress_callback = [](size_t n, size_t d) { + if (d == 0) { + return; + } + double percent = (static_cast(n) / d) * 100.0; + std::cout << "\rProgress: " << std::fixed << std::setprecision(2) << percent + << "%" << std::flush; + if (n == d) { + std::cout << std::endl; + } + }; + + mounter_client + .mount_personalized_with_callback(prov, + image_data.data(), + image_data.size(), + trustcache_data.data(), + trustcache_data.size(), + manifest_data.data(), + manifest_data.size(), + nullptr, // info_plist + unique_chip_id, + progress_callback) + .expect("Failed to mount personalized image"); + } + std::cout << "Successfully mounted image." << std::endl; + } + + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + return 1; + } + + return 0; +} diff --git a/cpp/include/idevice++/lockdown.hpp b/cpp/include/idevice++/lockdown.hpp index 14dfdff..baa60d0 100644 --- a/cpp/include/idevice++/lockdown.hpp +++ b/cpp/include/idevice++/lockdown.hpp @@ -1,4 +1,3 @@ - #pragma once #include #include diff --git a/cpp/include/idevice++/mobile_image_mounter.hpp b/cpp/include/idevice++/mobile_image_mounter.hpp new file mode 100644 index 0000000..96637e4 --- /dev/null +++ b/cpp/include/idevice++/mobile_image_mounter.hpp @@ -0,0 +1,87 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +namespace IdeviceFFI { + +using MobileImageMounterPtr = + std::unique_ptr>; + +class MobileImageMounter { + public: + // Factory: connect via Provider + static Result connect(Provider& provider); + + // Factory: wrap an existing Idevice socket (consumes it on success) + static Result from_socket(Idevice&& socket); + + // Ops + Result, FfiError> copy_devices(); + Result, FfiError> lookup_image(std::string image_type); + Result upload_image(std::string image_type, + const uint8_t* image_data, + size_t image_size, + const uint8_t* signature_data, + size_t signature_size); + Result mount_image(std::string image_type, + const uint8_t* signature_data, + size_t signature_size, + const uint8_t* trust_cache_data, + size_t trust_cache_size, + plist_t info_plist); + Result unmount_image(std::string mount_path); + Result query_developer_mode_status(); + Result mount_developer(const uint8_t* image_data, + size_t image_size, + const uint8_t* signature_data, + size_t signature_size); + Result, FfiError> query_personalization_manifest( + std::string image_type, const uint8_t* signature_data, size_t signature_size); + Result, FfiError> query_nonce(std::string personalized_image_type); + Result query_personalization_identifiers(std::string image_type); + Result roll_personalization_nonce(); + Result roll_cryptex_nonce(); + Result mount_personalized(Provider& provider, + const uint8_t* image_data, + size_t image_size, + const uint8_t* trust_cache_data, + size_t trust_cache_size, + const uint8_t* build_manifest_data, + size_t build_manifest_size, + plist_t info_plist, + uint64_t unique_chip_id); + Result + mount_personalized_with_callback(Provider& provider, + const uint8_t* image_data, + size_t image_size, + const uint8_t* trust_cache_data, + size_t trust_cache_size, + const uint8_t* build_manifest_data, + size_t build_manifest_size, + plist_t info_plist, + uint64_t unique_chip_id, + std::function& lambda); + + // RAII / moves + ~MobileImageMounter() noexcept = default; + MobileImageMounter(MobileImageMounter&&) noexcept = default; + MobileImageMounter& operator=(MobileImageMounter&&) noexcept = default; + MobileImageMounter(const MobileImageMounter&) = delete; + MobileImageMounter& operator=(const MobileImageMounter&) = delete; + + ImageMounterHandle* raw() const noexcept { return handle_.get(); } + static MobileImageMounter adopt(ImageMounterHandle* h) noexcept { + return MobileImageMounter(h); + } + + private: + explicit MobileImageMounter(ImageMounterHandle* h) noexcept : handle_(h) {} + MobileImageMounterPtr handle_{}; +}; + +} // namespace IdeviceFFI diff --git a/cpp/include/idevice++/option.hpp b/cpp/include/idevice++/option.hpp index ccc9f09..c2a44e6 100644 --- a/cpp/include/idevice++/option.hpp +++ b/cpp/include/idevice++/option.hpp @@ -8,6 +8,7 @@ #pragma once +#include #include #include #include @@ -115,6 +116,34 @@ template class Option { return has_ ? std::move(*ptr()) : static_cast(f()); } + T expect(const char* message) && { + if (is_none()) { + std::fprintf(stderr, "Fatal (expect) error: %s\n", message); + std::terminate(); + } + T tmp = std::move(*ptr()); + reset(); + return tmp; + } + + // Returns a mutable reference from an lvalue Result + T& expect(const char* message) & { + if (is_none()) { + std::fprintf(stderr, "Fatal (expect) error: %s\n", message); + std::terminate(); + } + return *ptr(); + } + + // Returns a const reference from a const lvalue Result + const T& expect(const char* message) const& { + if (is_none()) { + std::fprintf(stderr, "Fatal (expect) error: %s\n", message); + std::terminate(); + } + return *ptr(); + } + // map template auto map(F&& f) const& -> Option::type> { diff --git a/cpp/src/mobile_image_mounter.cpp b/cpp/src/mobile_image_mounter.cpp new file mode 100644 index 0000000..adb3dfc --- /dev/null +++ b/cpp/src/mobile_image_mounter.cpp @@ -0,0 +1,246 @@ +// Jackson Coxson + +#include +#include + +namespace IdeviceFFI { + +// -------- Anonymous Namespace for Helpers -------- +namespace { +/** + * @brief A C-style trampoline function to call back into a C++ std::function. + * + * This function is passed to the Rust FFI layer. It receives a void* context, + * which it casts back to the original std::function object to invoke it. + */ +extern "C" void progress_trampoline(size_t progress, size_t total, void* context) { + if (context) { + auto& callback_fn = *static_cast*>(context); + callback_fn(progress, total); + } +} +} // namespace + +// -------- Factory Methods -------- + +Result MobileImageMounter::connect(Provider& provider) { + ImageMounterHandle* handle = nullptr; + FfiError e(::image_mounter_connect(provider.raw(), &handle)); + if (e) { + return Err(e); + } + return Ok(MobileImageMounter::adopt(handle)); +} + +Result MobileImageMounter::from_socket(Idevice&& socket) { + ImageMounterHandle* handle = nullptr; + // The Rust FFI function consumes the socket, so we must release it from the + // C++ RAII wrapper's control. An `Idevice::release()` method is assumed here. + FfiError e(::image_mounter_new(socket.release(), &handle)); + if (e) { + return Err(e); + } + return Ok(MobileImageMounter::adopt(handle)); +} + +// -------- Ops -------- + +Result, FfiError> MobileImageMounter::copy_devices() { + plist_t* devices_raw = nullptr; + size_t devices_len = 0; + + FfiError e(::image_mounter_copy_devices(this->raw(), &devices_raw, &devices_len)); + if (e) { + return Err(e); + } + + std::vector devices; + if (devices_raw) { + devices.assign(devices_raw, devices_raw + devices_len); + } + + return Ok(std::move(devices)); +} + +Result, FfiError> MobileImageMounter::lookup_image(std::string image_type) { + uint8_t* signature_raw = nullptr; + size_t signature_len = 0; + + FfiError e(::image_mounter_lookup_image( + this->raw(), image_type.c_str(), &signature_raw, &signature_len)); + if (e) { + return Err(e); + } + + std::vector signature(signature_len); + std::memcpy(signature.data(), signature_raw, signature_len); + idevice_data_free(signature_raw, signature_len); + + return Ok(std::move(signature)); +} + +Result MobileImageMounter::upload_image(std::string image_type, + const uint8_t* image_data, + size_t image_size, + const uint8_t* signature_data, + size_t signature_size) { + FfiError e(::image_mounter_upload_image( + this->raw(), image_type.c_str(), image_data, image_size, signature_data, signature_size)); + return e ? Result(Err(e)) : Result(Ok()); +} + +Result MobileImageMounter::mount_image(std::string image_type, + const uint8_t* signature_data, + size_t signature_size, + const uint8_t* trust_cache_data, + size_t trust_cache_size, + plist_t info_plist) { + FfiError e(::image_mounter_mount_image(this->raw(), + image_type.c_str(), + signature_data, + signature_size, + trust_cache_data, + trust_cache_size, + info_plist)); + return e ? Result(Err(e)) : Result(Ok()); +} + +Result MobileImageMounter::unmount_image(std::string mount_path) { + FfiError e(::image_mounter_unmount_image(this->raw(), mount_path.c_str())); + return e ? Result(Err(e)) : Result(Ok()); +} + +Result MobileImageMounter::query_developer_mode_status() { + int status_c = 0; + FfiError e(::image_mounter_query_developer_mode_status(this->raw(), &status_c)); + if (e) { + return Err(e); + } + return Ok(status_c != 0); +} + +Result MobileImageMounter::mount_developer(const uint8_t* image_data, + size_t image_size, + const uint8_t* signature_data, + size_t signature_size) { + FfiError e(::image_mounter_mount_developer( + this->raw(), image_data, image_size, signature_data, signature_size)); + return e ? Result(Err(e)) : Result(Ok()); +} + +Result, FfiError> MobileImageMounter::query_personalization_manifest( + std::string image_type, const uint8_t* signature_data, size_t signature_size) { + uint8_t* manifest_raw = nullptr; + size_t manifest_len = 0; + FfiError e(::image_mounter_query_personalization_manifest(this->raw(), + image_type.c_str(), + signature_data, + signature_size, + &manifest_raw, + &manifest_len)); + if (e) { + return Err(e); + } + + std::vector manifest(manifest_len); + std::memcpy(manifest.data(), manifest_raw, manifest_len); + idevice_data_free(manifest_raw, manifest_len); + + return Ok(std::move(manifest)); +} + +Result, FfiError> +MobileImageMounter::query_nonce(std::string personalized_image_type) { + uint8_t* nonce_raw = nullptr; + size_t nonce_len = 0; + const char* image_type_c = + personalized_image_type.empty() ? nullptr : personalized_image_type.c_str(); + + FfiError e(::image_mounter_query_nonce(this->raw(), image_type_c, &nonce_raw, &nonce_len)); + if (e) { + return Err(e); + } + + std::vector nonce(nonce_len); + std::memcpy(nonce.data(), nonce_raw, nonce_len); + idevice_data_free(nonce_raw, nonce_len); + + return Ok(std::move(nonce)); +} + +Result +MobileImageMounter::query_personalization_identifiers(std::string image_type) { + plist_t identifiers = nullptr; + const char* image_type_c = image_type.empty() ? nullptr : image_type.c_str(); + + FfiError e( + ::image_mounter_query_personalization_identifiers(this->raw(), image_type_c, &identifiers)); + if (e) { + return Err(e); + } + + // The caller now owns the returned `plist_t` and is responsible for freeing it. + return Ok(identifiers); +} + +Result MobileImageMounter::roll_personalization_nonce() { + FfiError e(::image_mounter_roll_personalization_nonce(this->raw())); + return e ? Result(Err(e)) : Result(Ok()); +} + +Result MobileImageMounter::roll_cryptex_nonce() { + FfiError e(::image_mounter_roll_cryptex_nonce(this->raw())); + return e ? Result(Err(e)) : Result(Ok()); +} + +Result MobileImageMounter::mount_personalized(Provider& provider, + const uint8_t* image_data, + size_t image_size, + const uint8_t* trust_cache_data, + size_t trust_cache_size, + const uint8_t* build_manifest_data, + size_t build_manifest_size, + plist_t info_plist, + uint64_t unique_chip_id) { + FfiError e(::image_mounter_mount_personalized(this->raw(), + provider.raw(), + image_data, + image_size, + trust_cache_data, + trust_cache_size, + build_manifest_data, + build_manifest_size, + info_plist, + unique_chip_id)); + return e ? Result(Err(e)) : Result(Ok()); +} + +Result +MobileImageMounter::mount_personalized_with_callback(Provider& provider, + const uint8_t* image_data, + size_t image_size, + const uint8_t* trust_cache_data, + size_t trust_cache_size, + const uint8_t* build_manifest_data, + size_t build_manifest_size, + plist_t info_plist, + uint64_t unique_chip_id, + std::function& lambda) { + + FfiError e(::image_mounter_mount_personalized_with_callback(this->raw(), + provider.raw(), + image_data, + image_size, + trust_cache_data, + trust_cache_size, + build_manifest_data, + build_manifest_size, + info_plist, + unique_chip_id, + progress_trampoline, + &lambda /* context */)); + + return e ? Result(Err(e)) : Result(Ok()); +} + +} // namespace IdeviceFFI diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml index 833c700..57e6ef5 100644 --- a/ffi/Cargo.toml +++ b/ffi/Cargo.toml @@ -13,7 +13,7 @@ once_cell = "1.21.1" tokio = { version = "1.44.1", features = ["full"] } libc = "0.2.171" plist = "1.7.1" -plist_ffi = { version = "0.1.5" } +plist_ffi = { version = "0.1.6" } uuid = { version = "1.12", features = ["v4"], optional = true } [target.'cfg(windows)'.dependencies] From 604aec72f27baa710606ba11bc132f5dec200f21 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Thu, 25 Sep 2025 12:55:13 -0600 Subject: [PATCH 15/21] Add developer mode not enabled error kind --- idevice/src/lib.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 29f14d7..19b4af1 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -720,6 +720,9 @@ pub enum IdeviceError { #[cfg(feature = "installation_proxy")] #[error("malformed package archive: {0}")] MalformedPackageArchive(#[from] async_zip::error::ZipError) = -67, + + #[error("Developer mode is not enabled")] + DeveloperModeNotEnabled = -68, } impl IdeviceError { @@ -734,6 +737,8 @@ impl IdeviceError { fn from_device_error_type(e: &str, context: &plist::Dictionary) -> Option { if e.contains("NSDebugDescription=Canceled by user.") { return Some(Self::CanceledByUser); + } else if e.contains("Developer mode is not enabled.") { + return Some(Self::DeveloperModeNotEnabled); } match e { "GetProhibited" => Some(Self::GetProhibited), @@ -881,6 +886,7 @@ impl IdeviceError { #[cfg(feature = "installation_proxy")] IdeviceError::MalformedPackageArchive(_) => -67, + IdeviceError::DeveloperModeNotEnabled => -68, } } } From 9ed4cd8a550cdf3962fe094713f03aed7ecd3113 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Thu, 25 Sep 2025 13:55:23 -0600 Subject: [PATCH 16/21] Fix mounter cpp build on Linux --- cpp/src/mobile_image_mounter.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/src/mobile_image_mounter.cpp b/cpp/src/mobile_image_mounter.cpp index adb3dfc..d41ce35 100644 --- a/cpp/src/mobile_image_mounter.cpp +++ b/cpp/src/mobile_image_mounter.cpp @@ -2,6 +2,7 @@ #include #include +#include namespace IdeviceFFI { From c6d63d1f5d3d7f7ebe1b5a95193c63a6f85b94df Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 30 Sep 2025 19:02:56 -0600 Subject: [PATCH 17/21] Add heartbeat cpp bindings --- cpp/include/idevice++/heartbeat.hpp | 40 ++++++++++++++++++++++++ cpp/src/heartbeat.cpp | 48 +++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 cpp/include/idevice++/heartbeat.hpp create mode 100644 cpp/src/heartbeat.cpp diff --git a/cpp/include/idevice++/heartbeat.hpp b/cpp/include/idevice++/heartbeat.hpp new file mode 100644 index 0000000..48dae0a --- /dev/null +++ b/cpp/include/idevice++/heartbeat.hpp @@ -0,0 +1,40 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace IdeviceFFI { + +using HeartbeatPtr = + std::unique_ptr>; + +class Heartbeat { + public: + // Factory: connect via Provider + static Result connect(Provider& provider); + + // Factory: wrap an existing Idevice socket (consumes it on success) + static Result from_socket(Idevice&& socket); + + // Ops + Result send_polo(); + Result get_marco(u_int64_t interval); + + // RAII / moves + ~Heartbeat() noexcept = default; + Heartbeat(Heartbeat&&) noexcept = default; + Heartbeat& operator=(Heartbeat&&) noexcept = default; + Heartbeat(const Heartbeat&) = delete; + Heartbeat& operator=(const Heartbeat&) = delete; + + HeartbeatClientHandle* raw() const noexcept { return handle_.get(); } + static Heartbeat adopt(HeartbeatClientHandle* h) noexcept { return Heartbeat(h); } + + private: + explicit Heartbeat(HeartbeatClientHandle* h) noexcept : handle_(h) {} + HeartbeatPtr handle_{}; +}; + +} // namespace IdeviceFFI diff --git a/cpp/src/heartbeat.cpp b/cpp/src/heartbeat.cpp new file mode 100644 index 0000000..04c2040 --- /dev/null +++ b/cpp/src/heartbeat.cpp @@ -0,0 +1,48 @@ +// Jackson Coxson + +#include +#include +#include +#include +#include + +namespace IdeviceFFI { + +Result Heartbeat::connect(Provider& provider) { + HeartbeatClientHandle* out = nullptr; + FfiError e(::heartbeat_connect(provider.raw(), &out)); + if (e) { + provider.release(); + return Err(e); + } + return Ok(Heartbeat::adopt(out)); +} + +Result Heartbeat::from_socket(Idevice&& socket) { + HeartbeatClientHandle* out = nullptr; + FfiError e(::heartbeat_new(socket.raw(), &out)); + if (e) { + return Err(e); + } + socket.release(); + return Ok(Heartbeat::adopt(out)); +} + +Result Heartbeat::send_polo() { + FfiError e(::heartbeat_send_polo(handle_.get())); + if (e) { + return Err(e); + } + return Ok(); +} + +Result Heartbeat::get_marco(u_int64_t interval) { + u_int64_t new_interval = 0; + FfiError e(::heartbeat_get_marco(handle_.get(), interval, &new_interval)); + if (e) { + return Err(e); + } + return Ok(new_interval); +} + +} // namespace IdeviceFFI From 02f818a42aeb88f3508277308ec47ae76592ac44 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 30 Sep 2025 19:52:40 -0600 Subject: [PATCH 18/21] Add installation_proxy cpp bindings --- cpp/include/idevice++/installation_proxy.hpp | 60 +++++ cpp/src/installation_proxy.cpp | 240 +++++++++++++++++++ ffi/src/installation_proxy.rs | 2 +- 3 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 cpp/include/idevice++/installation_proxy.hpp create mode 100644 cpp/src/installation_proxy.cpp diff --git a/cpp/include/idevice++/installation_proxy.hpp b/cpp/include/idevice++/installation_proxy.hpp new file mode 100644 index 0000000..66165d6 --- /dev/null +++ b/cpp/include/idevice++/installation_proxy.hpp @@ -0,0 +1,60 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +namespace IdeviceFFI { + +using InstallationProxyPtr = + std::unique_ptr>; + +class InstallationProxy { + public: + // Factory: connect via Provider + static Result connect(Provider& provider); + + // Factory: wrap an existing Idevice socket (consumes it on success) + static Result from_socket(Idevice&& socket); + + // Ops + Result, FfiError> + get_apps(Option application_type, + Option> bundle_identifiers); + Result install(std::string package_path, Option options); + Result install_with_callback(std::string package_path, + Option options, + std::function& lambda); + Result upgrade(std::string package_path, Option options); + Result upgrade_with_callback(std::string package_path, + Option options, + std::function& lambda); + Result uninstall(std::string package_path, Option options); + Result uninstall_with_callback(std::string package_path, + Option options, + std::function& lambda); + Result check_capabilities_match(std::vector capabilities, + Option options); + Result, FfiError> browse(Option options); + + // RAII / moves + ~InstallationProxy() noexcept = default; + InstallationProxy(InstallationProxy&&) noexcept = default; + InstallationProxy& operator=(InstallationProxy&&) noexcept = default; + InstallationProxy(const InstallationProxy&) = delete; + InstallationProxy& operator=(const InstallationProxy&) = delete; + + InstallationProxyClientHandle* raw() const noexcept { return handle_.get(); } + static InstallationProxy adopt(InstallationProxyClientHandle* h) noexcept { + return InstallationProxy(h); + } + + private: + explicit InstallationProxy(InstallationProxyClientHandle* h) noexcept : handle_(h) {} + InstallationProxyPtr handle_{}; +}; + +} // namespace IdeviceFFI diff --git a/cpp/src/installation_proxy.cpp b/cpp/src/installation_proxy.cpp new file mode 100644 index 0000000..e01918f --- /dev/null +++ b/cpp/src/installation_proxy.cpp @@ -0,0 +1,240 @@ +// Jackson Coxson + +#include +#include +#include +#include +#include + +namespace IdeviceFFI { + +// -------- Anonymous Namespace for Helpers -------- +namespace { +/** + * @brief A C-style trampoline function to call back into a C++ std::function. + * + * This function is passed to the Rust FFI layer. It receives a void* context, + * which it casts back to the original std::function object to invoke it. + */ +extern "C" void progress_trampoline(u_int64_t progress, void* context) { + if (context) { + auto& callback_fn = *static_cast*>(context); + callback_fn(progress); + } +} +} // namespace + +// -------- Factory Methods -------- + +Result InstallationProxy::connect(Provider& provider) { + InstallationProxyClientHandle* handle = nullptr; + FfiError e(::installation_proxy_connect(provider.raw(), &handle)); + if (e) { + return Err(e); + } + return Ok(InstallationProxy::adopt(handle)); +} + +Result InstallationProxy::from_socket(Idevice&& socket) { + InstallationProxyClientHandle* handle = nullptr; + // The Rust FFI function consumes the socket, so we must release it from the + // C++ RAII wrapper's control. An `Idevice::release()` method is assumed here. + FfiError e(::installation_proxy_new(socket.release(), &handle)); + if (e) { + return Err(e); + } + return Ok(InstallationProxy::adopt(handle)); +} + +// -------- Ops -------- + +Result, FfiError> +InstallationProxy::get_apps(Option application_type, + Option> bundle_identifiers) { + plist_t* apps_raw = nullptr; + size_t apps_len = 0; + + const char* application_type_ptr = NULL; + if (application_type.is_some()) { + application_type_ptr = application_type.unwrap().c_str(); + } + + std::vector c_bundle_id; + size_t bundle_identifiers_len = 0; + if (bundle_identifiers.is_some()) { + c_bundle_id.reserve(bundle_identifiers.unwrap().size()); + for (auto& a : bundle_identifiers.unwrap()) { + c_bundle_id.push_back(a.c_str()); + } + } + + FfiError e(::installation_proxy_get_apps( + this->raw(), + application_type_ptr, + c_bundle_id.empty() ? nullptr : const_cast(c_bundle_id.data()), + bundle_identifiers_len, + apps_raw, + &apps_len)); + if (e) { + return Err(e); + } + + std::vector apps; + if (apps_raw) { + apps.assign(apps_raw, apps_raw + apps_len); + } + + return Ok(std::move(apps)); +} + +Result InstallationProxy::install(std::string package_path, + Option options) { + plist_t unwrapped_options; + if (options.is_some()) { + unwrapped_options = std::move(options).unwrap(); + } else { + unwrapped_options = NULL; + } + + FfiError e(::installation_proxy_install(this->raw(), package_path.c_str(), &unwrapped_options)); + if (e) { + return Err(e); + } + return Ok(); +} + +Result InstallationProxy::install_with_callback( + std::string package_path, Option options, std::function& lambda + +) { + plist_t unwrapped_options; + if (options.is_some()) { + unwrapped_options = std::move(options).unwrap(); + } else { + unwrapped_options = NULL; + } + + FfiError e(::installation_proxy_install_with_callback( + this->raw(), package_path.c_str(), &unwrapped_options, progress_trampoline, &lambda)); + if (e) { + return Err(e); + } + return Ok(); +} + +Result InstallationProxy::upgrade(std::string package_path, + Option options) { + plist_t unwrapped_options; + if (options.is_some()) { + unwrapped_options = std::move(options).unwrap(); + } else { + unwrapped_options = NULL; + } + + FfiError e(::installation_proxy_upgrade(this->raw(), package_path.c_str(), &unwrapped_options)); + if (e) { + return Err(e); + } + return Ok(); +} + +Result InstallationProxy::upgrade_with_callback( + std::string package_path, Option options, std::function& lambda + +) { + plist_t unwrapped_options; + if (options.is_some()) { + unwrapped_options = std::move(options).unwrap(); + } else { + unwrapped_options = NULL; + } + + FfiError e(::installation_proxy_upgrade_with_callback( + this->raw(), package_path.c_str(), &unwrapped_options, progress_trampoline, &lambda)); + if (e) { + return Err(e); + } + return Ok(); +} + +Result InstallationProxy::uninstall(std::string package_path, + Option options) { + plist_t unwrapped_options; + if (options.is_some()) { + unwrapped_options = std::move(options).unwrap(); + } else { + unwrapped_options = NULL; + } + + FfiError e( + ::installation_proxy_uninstall(this->raw(), package_path.c_str(), &unwrapped_options)); + if (e) { + return Err(e); + } + return Ok(); +} + +Result InstallationProxy::uninstall_with_callback( + std::string package_path, Option options, std::function& lambda + +) { + plist_t unwrapped_options; + if (options.is_some()) { + unwrapped_options = std::move(options).unwrap(); + } else { + unwrapped_options = NULL; + } + + FfiError e(::installation_proxy_uninstall_with_callback( + this->raw(), package_path.c_str(), &unwrapped_options, progress_trampoline, &lambda)); + if (e) { + return Err(e); + } + return Ok(); +} + +Result +InstallationProxy::check_capabilities_match(std::vector capabilities, + Option options) { + plist_t unwrapped_options; + if (options.is_some()) { + unwrapped_options = std::move(options).unwrap(); + } else { + unwrapped_options = NULL; + } + + bool res = false; + FfiError e(::installation_proxy_check_capabilities_match( + this->raw(), + capabilities.empty() ? nullptr : capabilities.data(), + capabilities.size(), + unwrapped_options, + &res)); + return e ? Result(Err(e)) : Result(Ok(res)); +} + +Result, FfiError> InstallationProxy::browse(Option options) { + plist_t* apps_raw = nullptr; + size_t apps_len = 0; + + plist_t unwrapped_options; + if (options.is_some()) { + unwrapped_options = std::move(options).unwrap(); + } else { + unwrapped_options = NULL; + } + + FfiError e(::installation_proxy_browse(this->raw(), unwrapped_options, &apps_raw, &apps_len)); + if (e) { + return Err(e); + } + + std::vector apps; + if (apps_raw) { + apps.assign(apps_raw, apps_raw + apps_len); + } + + return Ok(std::move(apps)); +} + +} // namespace IdeviceFFI diff --git a/ffi/src/installation_proxy.rs b/ffi/src/installation_proxy.rs index a749ea4..e3ef63c 100644 --- a/ffi/src/installation_proxy.rs +++ b/ffi/src/installation_proxy.rs @@ -25,7 +25,7 @@ pub struct InstallationProxyClientHandle(pub InstallationProxyClient); /// `provider` must be a valid pointer to a handle allocated by this library /// `client` must be a valid, non-null pointer to a location where the handle will be stored #[unsafe(no_mangle)] -pub unsafe extern "C" fn installation_proxy_connect_tcp( +pub unsafe extern "C" fn installation_proxy_connect( provider: *mut IdeviceProviderHandle, client: *mut *mut InstallationProxyClientHandle, ) -> *mut IdeviceFfiError { From b49d5194b97b738da6d57fe6d9e42cb4e621081f Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 30 Sep 2025 19:56:41 -0600 Subject: [PATCH 19/21] Remove MacOS u64 cpp header type --- cpp/src/heartbeat.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/cpp/src/heartbeat.cpp b/cpp/src/heartbeat.cpp index 04c2040..7882e2b 100644 --- a/cpp/src/heartbeat.cpp +++ b/cpp/src/heartbeat.cpp @@ -4,7 +4,6 @@ #include #include #include -#include namespace IdeviceFFI { From dc06ba08026f9b13826c5047bd6ccb6c5312901d Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 1 Oct 2025 08:18:34 -0600 Subject: [PATCH 20/21] Implement process control cpp bindings --- cpp/include/idevice++/process_control.hpp | 41 +++++++++++++ cpp/src/process_control.cpp | 73 +++++++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 cpp/include/idevice++/process_control.hpp create mode 100644 cpp/src/process_control.cpp diff --git a/cpp/include/idevice++/process_control.hpp b/cpp/include/idevice++/process_control.hpp new file mode 100644 index 0000000..806dcfb --- /dev/null +++ b/cpp/include/idevice++/process_control.hpp @@ -0,0 +1,41 @@ +// Jackson Coxson + +#pragma once +#include +#include +#include +#include + +namespace IdeviceFFI { + +using ProcessControlPtr = + std::unique_ptr>; + +class ProcessControl { + public: + // Factory: borrows the RemoteServer; not consumed + static Result create(RemoteServer& server); + + Result launch_app(std::string bundle_id, + Option> env_vars, + Option> arguments, + bool start_suspended, + bool kill_existing); + Result kill_app(u_int64_t pid); + Result disable_memory_limit(u_int64_t pid); + + ~ProcessControl() noexcept = default; + ProcessControl(ProcessControl&&) noexcept = default; + ProcessControl& operator=(ProcessControl&&) noexcept = default; + ProcessControl(const ProcessControl&) = delete; + ProcessControl& operator=(const ProcessControl&) = delete; + + ProcessControlHandle* raw() const noexcept { return handle_.get(); } + static ProcessControl adopt(ProcessControlHandle* h) noexcept { return ProcessControl(h); } + + private: + explicit ProcessControl(ProcessControlHandle* h) noexcept : handle_(h) {} + ProcessControlPtr handle_{}; +}; + +} // namespace IdeviceFFI diff --git a/cpp/src/process_control.cpp b/cpp/src/process_control.cpp new file mode 100644 index 0000000..2764aac --- /dev/null +++ b/cpp/src/process_control.cpp @@ -0,0 +1,73 @@ +// Jackson Coxson + +#include + +namespace IdeviceFFI { + +Result ProcessControl::create(RemoteServer& server) { + ProcessControlHandle* out = nullptr; + FfiError e(::process_control_new(server.raw(), &out)); + if (e) { + return Err(e); + } + return Ok(ProcessControl::adopt(out)); +} + +Result ProcessControl::launch_app(std::string bundle_id, + Option> env_vars, + Option> arguments, + bool start_suspended, + bool kill_existing) { + std::vector c_env_vars; + size_t env_vars_len = 0; + if (env_vars.is_some()) { + c_env_vars.reserve(env_vars.unwrap().size()); + for (auto& a : env_vars.unwrap()) { + c_env_vars.push_back(a.c_str()); + } + } + + std::vector c_arguments; + size_t arguments_len = 0; + if (arguments.is_some()) { + c_arguments.reserve(arguments.unwrap().size()); + for (auto& a : arguments.unwrap()) { + c_arguments.push_back(a.c_str()); + } + } + + u_int64_t pid = 0; + + FfiError e(::process_control_launch_app( + handle_.get(), + bundle_id.c_str(), + c_env_vars.empty() ? nullptr : const_cast(c_env_vars.data()), + env_vars_len, + c_arguments.empty() ? nullptr : const_cast(c_arguments.data()), + arguments_len, + start_suspended, + kill_existing, + &pid)); + if (e) { + return Err(e); + } + return Ok(pid); +} + +Result ProcessControl::kill_app(u_int64_t pid) { + FfiError e(::process_control_kill_app(handle_.get(), pid)); + if (e) { + return Err(e); + } + return Ok(); +} + +Result ProcessControl::disable_memory_limit(u_int64_t pid) { + FfiError e(::process_control_disable_memory_limit(handle_.get(), pid)); + if (e) { + return Err(e); + } + return Ok(); +} + +} // namespace IdeviceFFI From 50fd3284f8f676873b17bad6da683d0f2b517089 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 1 Oct 2025 18:51:35 -0600 Subject: [PATCH 21/21] Update readme --- README.md | 61 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 2311f81..0cbd84b 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ # idevice -A Rust library for interacting with iOS services. +A pure Rust library for interacting with iOS services. Inspired by [libimobiledevice](https://github.com/libimobiledevice/libimobiledevice) -and [pymobiledevice3](https://github.com/doronz88/pymobiledevice3), -this library interfaces with lockdownd and usbmuxd to perform actions +[pymobiledevice3](https://github.com/doronz88/pymobiledevice3), +and [go-ios](https://github.com/danielpaulus/go-ios) +this library interfaces with lockdownd, usbmuxd, and RSD to perform actions on an iOS device that a Mac normally would. For help and information, join the [idevice Discord](https://discord.gg/qtgv6QtYbV) + [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/jkcoxson/idevice) ## State @@ -18,6 +20,25 @@ This library is in development and research stage. Releases are being published to crates.io for use in other projects, but the API and feature-set are far from final or even planned. +## Why use this? + +libimobiledevice is a groundbreaking library. Unfortunately, it hasn't +been seriously updated in a long time, and does not support many modern +iOS features. + +Libraries such as pymobiledevice3 and go-ios have popped up to fill that +gap, but both lacked the support I needed for embedding into applications +and server programs. Python requires an interpreter, and Go's current +ability to be embedded in other languages is lacking. + +This library is currently used in popular apps such as +[StikDebug](https://github.com/StephenDev0/StikDebug), +[CrossCode](https://github.com/nab138/CrossCode) +and +[Protokolle](https://github.com/khcrysalis/Protokolle). +``idevice`` has proven there is a need. It's currently deployed on tens of +thousands of devices, all across the world. + ## Features To keep dependency bloat and compile time down, everything is contained in features. @@ -126,16 +147,36 @@ More examples are in the [`tools`](tools/) crate and in the crate documentation. For use in other languages, a small FFI crate has been created to start exposing idevice. Example C programs can be found in the [`ffi/examples`](ffi/examples/) directory. -## Version Policy +### C++ -As Apple prohibits downgrading to older versions, this library will -not keep compatibility for older versions than the current stable release. +"Hey wait a second, there's a lot of C++ code in this library!!" +C++ bindings have been made for many of idevice's features. This allows smooth +and safer usage in C++ and Swift codebases. -## Developer Disk Images +## Technical Explanation -doronz88 is kind enough to maintain a [repo](https://github.com/doronz88/DeveloperDiskImage) -for disk images and personalized images. -On MacOS, you can find them at ``~/Library/Developer/DeveloperDiskImages``. +There are so many layers and protocols in this library, many stacked on top of +one another. It's difficult to describe the magnitude that is Apple's interfaces. + +I would recommend reading the DeepWiki explanations and overviews to get an idea +of how this library and their associated protocols work. But a general overview is: + +### Lockdown + +1. A lockdown service is accessible via a port given by lockdown +1. Lockdown is accessible by USB or TCP via TLS +1. USB is accessible via usbmuxd +1. usbmuxd is accessed through a unix socket +1. That Unix socket has its own protocol + +### RemoteXPC/RSD + +1. An RSD service is discovered through a RemoteXPC handshake response +1. RemoteXPC is transferred over non-compliant HTTP/2 +1. That HTTP/2 is accessed through an NCM USB interface or CoreDeviceProxy +1. CoreDeviceProxy is a lockdown service, see above + +This doesn't even touch RPPairing, which is still a mystery as of writing. ## License