From 52b2db404fea45c69672abc59dca67c8980c0d99 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Sun, 6 Apr 2025 10:03:09 -0600 Subject: [PATCH] Document modules and public methods --- ffi/src/lockdown.rs | 14 +- idevice/src/afc/mod.rs | 4 +- idevice/src/core_device_proxy/mod.rs | 100 ++++++++++-- idevice/src/debug_proxy.rs | 109 ++++++++++++- idevice/src/dvt/location_simulation.rs | 64 +++++++- idevice/src/dvt/message.rs | 189 ++++++++++++++++++--- idevice/src/dvt/process_control.rs | 92 ++++++++++- idevice/src/dvt/remote_server.rs | 140 +++++++++++++++- idevice/src/heartbeat.rs | 64 +++++++- idevice/src/installation_proxy.rs | 67 +++++++- idevice/src/lib.rs | 129 ++++++++++++++- idevice/src/lockdown.rs | 114 +++++++++++-- idevice/src/misagent.rs | 97 ++++++++++- idevice/src/mobile_image_mounter.rs | 218 +++++++++++++++++++++++-- idevice/src/pairing_file.rs | 74 ++++++++- idevice/src/provider.rs | 52 +++++- idevice/src/springboardservices.rs | 57 ++++++- idevice/src/tcp/adapter.rs | 169 ++++++++++++++++++- idevice/src/tss.rs | 60 ++++++- idevice/src/tunneld.rs | 45 ++++- idevice/src/usbmuxd/mod.rs | 110 ++++++++++++- idevice/src/util.rs | 53 +++++- idevice/src/xpc/cdtunnel.rs | 28 ---- idevice/src/xpc/mod.rs | 65 +++++++- tools/src/ideviceinfo.rs | 4 +- tools/src/mounter.rs | 4 +- 26 files changed, 1949 insertions(+), 173 deletions(-) delete mode 100644 idevice/src/xpc/cdtunnel.rs diff --git a/ffi/src/lockdown.rs b/ffi/src/lockdown.rs index c7e2577..acfa5f4 100644 --- a/ffi/src/lockdown.rs +++ b/ffi/src/lockdown.rs @@ -2,14 +2,14 @@ use std::ffi::c_void; -use idevice::{IdeviceError, IdeviceService, lockdown::LockdowndClient}; +use idevice::{IdeviceError, IdeviceService, lockdown::LockdownClient}; use crate::{ IdeviceErrorCode, IdeviceHandle, IdevicePairingFile, RUNTIME, provider::{TcpProviderHandle, UsbmuxdProviderHandle}, }; -pub struct LockdowndClientHandle(pub LockdowndClient); +pub struct LockdowndClientHandle(pub LockdownClient); /// Connects to lockdownd service using TCP provider /// @@ -33,10 +33,10 @@ pub unsafe extern "C" fn lockdownd_connect_tcp( return IdeviceErrorCode::InvalidArg; } - let res: Result = RUNTIME.block_on(async move { + let res: Result = RUNTIME.block_on(async move { let provider_box = unsafe { Box::from_raw(provider) }; let provider_ref = &provider_box.0; - let result = LockdowndClient::connect(provider_ref).await; + let result = LockdownClient::connect(provider_ref).await; std::mem::forget(provider_box); result }); @@ -76,10 +76,10 @@ pub unsafe extern "C" fn lockdownd_connect_usbmuxd( return IdeviceErrorCode::InvalidArg; } - let res: Result = RUNTIME.block_on(async move { + let res: Result = RUNTIME.block_on(async move { let provider_box = unsafe { Box::from_raw(provider) }; let provider_ref = &provider_box.0; - let result = LockdowndClient::connect(provider_ref).await; + let result = LockdownClient::connect(provider_ref).await; std::mem::forget(provider_box); result }); @@ -115,7 +115,7 @@ pub unsafe extern "C" fn lockdownd_new( return IdeviceErrorCode::InvalidArg; } let socket = unsafe { Box::from_raw(socket) }.0; - let r = LockdowndClient::new(socket); + let r = LockdownClient::new(socket); let boxed = Box::new(LockdowndClientHandle(r)); unsafe { *client = Box::into_raw(boxed) }; IdeviceErrorCode::IdeviceSuccess diff --git a/idevice/src/afc/mod.rs b/idevice/src/afc/mod.rs index fc0864e..76ecbbb 100644 --- a/idevice/src/afc/mod.rs +++ b/idevice/src/afc/mod.rs @@ -7,7 +7,7 @@ use log::warn; use opcode::AfcOpcode; use packet::{AfcPacket, AfcPacketHeader}; -use crate::{lockdown::LockdowndClient, Idevice, IdeviceError, IdeviceService}; +use crate::{lockdown::LockdownClient, Idevice, IdeviceError, IdeviceService}; pub mod errors; pub mod opcode; @@ -47,7 +47,7 @@ impl IdeviceService for AfcClient { async fn connect( provider: &dyn crate::provider::IdeviceProvider, ) -> Result { - let mut lockdown = LockdowndClient::connect(provider).await?; + let mut lockdown = LockdownClient::connect(provider).await?; lockdown .start_session(&provider.get_pairing_file().await?) .await?; diff --git a/idevice/src/core_device_proxy/mod.rs b/idevice/src/core_device_proxy/mod.rs index 1dd309f..b9a6664 100644 --- a/idevice/src/core_device_proxy/mod.rs +++ b/idevice/src/core_device_proxy/mod.rs @@ -1,13 +1,28 @@ -// Jackson Coxson +//! CoreDeviceProxy and CDTunnelPacket utilities for interacting with +//! iOS's CoreDeviceProxy service. This service starts an L3 (TUN) tunnel +//! for "trusted" services introduced in iOS 17. +//! +//! This module handles the construction and parsing of `CDTunnelPacket` messages +//! and manages a handshake and data tunnel to the CoreDeviceProxy daemon on iOS devices. +//! +//! # Overview +//! - `CDTunnelPacket` is used to parse and serialize packets sent over the CoreDeviceProxy channel. +//! - `CoreDeviceProxy` is a service client that initializes the tunnel, handles handshakes, +//! and optionally supports creating a software-based TCP/IP tunnel (behind a feature flag). +//! +//! # Features +//! - `tunnel_tcp_stack`: Enables software TCP/IP tunnel creation using a virtual adapter. See the tcp moduel. -use crate::{lockdown::LockdowndClient, Idevice, IdeviceError, IdeviceService}; +use crate::{lockdown::LockdownClient, Idevice, IdeviceError, IdeviceService}; use byteorder::{BigEndian, WriteBytesExt}; use serde::{Deserialize, Serialize}; use std::io::{self, Write}; +/// A representation of a CDTunnel packet used in the CoreDeviceProxy protocol. #[derive(Debug, PartialEq)] pub struct CDTunnelPacket { + /// The body of the packet, typically JSON-encoded data. body: Vec, } @@ -15,27 +30,32 @@ impl CDTunnelPacket { const MAGIC: &'static [u8] = b"CDTunnel"; /// Parses a byte slice into a `CDTunnelPacket`. + /// + /// # Arguments + /// + /// * `input` - A byte slice containing the raw packet data. + /// + /// # Returns + /// + /// * `Ok(CDTunnelPacket)` if the input is a valid packet. + /// * `Err(IdeviceError)` if parsing fails due to invalid magic, length, or size. pub fn parse(input: &[u8]) -> Result { if input.len() < Self::MAGIC.len() + 2 { return Err(IdeviceError::CdtunnelPacketTooShort); } - // Validate the magic bytes if &input[0..Self::MAGIC.len()] != Self::MAGIC { return Err(IdeviceError::CdtunnelPacketInvalidMagic); } - // Parse the body length let length_offset = Self::MAGIC.len(); let body_length = u16::from_be_bytes([input[length_offset], input[length_offset + 1]]) as usize; - // Validate the body length if input.len() < length_offset + 2 + body_length { return Err(IdeviceError::PacketSizeMismatch); } - // Extract the body let body_start = length_offset + 2; let body = input[body_start..body_start + body_length].to_vec(); @@ -43,37 +63,55 @@ impl CDTunnelPacket { } /// Serializes the `CDTunnelPacket` into a byte vector. + /// + /// # Returns + /// + /// * `Ok(Vec)` containing the serialized packet. + /// * `Err(io::Error)` if writing to the output buffer fails. pub fn serialize(&self) -> io::Result> { let mut output = Vec::new(); - // Write the magic bytes output.write_all(Self::MAGIC)?; - - // Write the body length output.write_u16::(self.body.len() as u16)?; - - // Write the body output.write_all(&self.body)?; Ok(output) } } +/// A high-level client for the `com.apple.internal.devicecompute.CoreDeviceProxy` service. +/// +/// Handles session negotiation, handshake, and tunnel communication. pub struct CoreDeviceProxy { + /// The underlying idevice connection used for communication. pub idevice: Idevice, + /// The handshake response received during initialization. pub handshake: HandshakeResponse, + /// The maximum transmission unit used for reading. pub mtu: u32, } impl IdeviceService for CoreDeviceProxy { + /// Returns the name of the service used for launching the CoreDeviceProxy. fn service_name() -> &'static str { "com.apple.internal.devicecompute.CoreDeviceProxy" } + /// Connects to the CoreDeviceProxy service + /// + /// # Arguments + /// + /// * `provider` - An implementation of `IdeviceProvider` that supplies + /// pairing data and socket connections. + /// + /// # Returns + /// + /// * `Ok(CoreDeviceProxy)` if connection and handshake succeed. + /// * `Err(IdeviceError)` if any step fails. async fn connect( provider: &dyn crate::provider::IdeviceProvider, ) -> Result { - let mut lockdown = LockdowndClient::connect(provider).await?; + let mut lockdown = LockdownClient::connect(provider).await?; lockdown .start_session(&provider.get_pairing_file().await?) .await?; @@ -91,6 +129,7 @@ impl IdeviceService for CoreDeviceProxy { } } +/// Request sent to initiate the handshake with the CoreDeviceProxy. #[derive(Serialize)] struct HandshakeRequest { #[serde(rename = "type")] @@ -98,13 +137,18 @@ struct HandshakeRequest { mtu: u32, } +/// Parameters returned as part of the handshake response from the proxy server. #[derive(Debug, Serialize, Deserialize)] pub struct ClientParameters { + /// The MTU (maximum transmission unit) for the connection. pub mtu: u16, + /// The IP address assigned to the client. pub address: String, + /// The subnet mask for the tunnel. pub netmask: String, } +/// Handshake response structure received from the CoreDeviceProxy. #[derive(Debug, Serialize, Deserialize)] pub struct HandshakeResponse { #[serde(rename = "clientParameters")] @@ -120,6 +164,16 @@ pub struct HandshakeResponse { impl CoreDeviceProxy { const DEFAULT_MTU: u32 = 16000; + /// Constructs a new `CoreDeviceProxy` by performing a handshake on the given `Idevice`. + /// + /// # Arguments + /// + /// * `idevice` - The connected `Idevice` socket. + /// + /// # Returns + /// + /// * `Ok(CoreDeviceProxy)` on successful handshake. + /// * `Err(IdeviceError)` if the handshake fails. pub async fn new(mut idevice: Idevice) -> Result { let req = HandshakeRequest { packet_type: "clientHandshakeRequest".to_string(), @@ -152,15 +206,37 @@ impl CoreDeviceProxy { }) } + /// Sends a raw data packet through the tunnel. + /// + /// # Arguments + /// + /// * `data` - The raw bytes to send. + /// + /// # Returns + /// + /// * `Ok(())` if the data is successfully sent. + /// * `Err(IdeviceError)` if sending fails. pub async fn send(&mut self, data: &[u8]) -> Result<(), IdeviceError> { self.idevice.send_raw(data).await?; Ok(()) } + /// Receives up to `mtu` bytes from the tunnel. + /// + /// # Returns + /// + /// * `Ok(Vec)` containing the received data. + /// * `Err(IdeviceError)` if reading fails. pub async fn recv(&mut self) -> Result, IdeviceError> { self.idevice.read_any(self.mtu).await } + /// Creates a software-based TCP tunnel adapter, if the `tunnel_tcp_stack` feature is enabled. + /// + /// # Returns + /// + /// * `Ok(Adapter)` for the software TCP stack. + /// * `Err(IdeviceError)` if IP parsing or socket extraction fails. #[cfg(feature = "tunnel_tcp_stack")] pub fn create_software_tunnel(self) -> Result { let our_ip = self diff --git a/idevice/src/debug_proxy.rs b/idevice/src/debug_proxy.rs index 338801a..a8de60e 100644 --- a/idevice/src/debug_proxy.rs +++ b/idevice/src/debug_proxy.rs @@ -1,5 +1,8 @@ -// Jackson Coxson -// https://sourceware.org/gdb/current/onlinedocs/gdb.html/Packets.html#Packets +//! GDB Remote Debugging Protocol Implementation for iOS Devices +//! +//! Provides functionality for communicating with the iOS debug server using the +//! GDB Remote Serial Protocol as documented at: +//! https://sourceware.org/gdb/current/onlinedocs/gdb.html/Packets.html#Packets use log::debug; use std::fmt::Write; @@ -7,25 +10,47 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; use crate::{IdeviceError, ReadWrite}; +/// The service name for the debug proxy as registered with lockdownd pub const SERVICE_NAME: &str = "com.apple.internal.dt.remote.debugproxy"; +/// Client for interacting with the iOS debug proxy service +/// +/// Implements the GDB Remote Serial Protocol for communicating with debugserver +/// on iOS devices. Handles packet formatting, checksums, and acknowledgments. pub struct DebugProxyClient { + /// The underlying socket connection to debugproxy pub socket: R, + /// Flag indicating whether ACK mode is disabled pub noack_mode: bool, } +/// Represents a debugserver command with arguments +/// +/// Commands follow the GDB Remote Serial Protocol format: +/// $[]# pub struct DebugserverCommand { + /// The command name (e.g. "qSupported", "vCont") pub name: String, + /// Command arguments that will be hex-encoded pub argv: Vec, } impl DebugserverCommand { + /// Creates a new debugserver command + /// + /// # Arguments + /// * `name` - The command name (without leading $) + /// * `argv` - Arguments that will be hex-encoded in the packet pub fn new(name: String, argv: Vec) -> Self { Self { name, argv } } } impl DebugProxyClient { + /// Creates a new debug proxy client with default settings + /// + /// # Arguments + /// * `socket` - Established connection to debugproxy service pub fn new(socket: R) -> Self { Self { socket, @@ -33,10 +58,24 @@ impl DebugProxyClient { } } + /// Consumes the client and returns the underlying socket pub fn into_inner(self) -> R { self.socket } + /// Sends a command to debugserver and waits for response + /// + /// Formats the command according to GDB Remote Serial Protocol: + /// $[]# + /// + /// # Arguments + /// * `command` - The command and arguments to send + /// + /// # Returns + /// The response string if successful, None if no response received + /// + /// # Errors + /// Returns `IdeviceError` if communication fails pub async fn send_command( &mut self, command: DebugserverCommand, @@ -69,6 +108,16 @@ impl DebugProxyClient { Ok(response) } + /// Reads a response packet from debugserver + /// + /// Handles the GDB Remote Serial Protocol response format: + /// $# + /// + /// # Returns + /// The response data without protocol framing if successful + /// + /// # Errors + /// Returns `IdeviceError` if communication fails or protocol is violated pub async fn read_response(&mut self) -> Result, IdeviceError> { let mut buffer = Vec::new(); let mut received_char = [0u8; 1]; @@ -103,11 +152,28 @@ impl DebugProxyClient { Ok(Some(response)) } + /// Sends raw bytes directly to the debugproxy connection + /// + /// # Arguments + /// * `bytes` - The raw bytes to send + /// + /// # Errors + /// Returns `IdeviceError` if writing fails pub async fn send_raw(&mut self, bytes: &[u8]) -> Result<(), IdeviceError> { self.socket.write_all(bytes).await?; Ok(()) } + /// Reads raw bytes from the debugproxy connection + /// + /// # Arguments + /// * `len` - Maximum number of bytes to read + /// + /// # Returns + /// The received data as a string + /// + /// # Errors + /// Returns `IdeviceError` if reading fails or data isn't valid UTF-8 pub async fn read(&mut self, len: usize) -> Result { let mut buf = vec![0; len]; let r = self.socket.read(&mut buf).await?; @@ -115,6 +181,19 @@ impl DebugProxyClient { Ok(String::from_utf8_lossy(&buf[..r]).to_string()) } + /// Sets program arguments using the 'A' command + /// + /// Formats arguments according to GDB protocol: + /// A,, + /// + /// # Arguments + /// * `argv` - Program arguments to set + /// + /// # Returns + /// The debugserver response + /// + /// # Errors + /// Returns `IdeviceError` if arguments are empty or communication fails pub async fn set_argv(&mut self, argv: Vec) -> Result { if argv.is_empty() { return Err(IdeviceError::InvalidArgument); @@ -157,26 +236,45 @@ impl DebugProxyClient { Ok(response) } + /// Sends an acknowledgment (+) + /// + /// # Errors + /// Returns `IdeviceError` if writing fails pub async fn send_ack(&mut self) -> Result<(), IdeviceError> { self.socket.write_all(b"+").await?; Ok(()) } + /// Sends a negative acknowledgment (-) + /// + /// # Errors + /// Returns `IdeviceError` if writing fails pub async fn send_noack(&mut self) -> Result<(), IdeviceError> { self.socket.write_all(b"-").await?; Ok(()) } + /// Enables or disables ACK mode + /// + /// When disabled, the client won't expect or send acknowledgments + /// + /// # Arguments + /// * `enabled` - Whether to enable ACK mode pub fn set_ack_mode(&mut self, enabled: bool) { self.noack_mode = !enabled; } } +/// Calculates the checksum for a GDB protocol packet +/// +/// The checksum is computed as the modulo 256 sum of all characters +/// between '$' and '#', formatted as two lowercase hex digits. fn calculate_checksum(data: &str) -> String { let checksum = data.bytes().fold(0u8, |acc, byte| acc.wrapping_add(byte)); format!("{:02x}", checksum) } +/// Hex-encodes bytes as uppercase string fn hex_encode(bytes: &[u8]) -> String { bytes.iter().fold(String::new(), |mut output, b| { let _ = write!(output, "{b:02X}"); @@ -185,16 +283,21 @@ fn hex_encode(bytes: &[u8]) -> String { } impl From for DebugserverCommand { + /// Converts a string into a debugserver command by splitting on whitespace + /// + /// The first token becomes the command name, remaining tokens become arguments fn from(s: String) -> Self { - // Split string into command and arguments let mut split = s.split_whitespace(); let command = split.next().unwrap_or("").to_string(); let arguments: Vec = split.map(|s| s.to_string()).collect(); Self::new(command, arguments) } } + impl From<&str> for DebugserverCommand { + /// Converts a string slice into a debugserver command fn from(s: &str) -> DebugserverCommand { s.to_string().into() } } + diff --git a/idevice/src/dvt/location_simulation.rs b/idevice/src/dvt/location_simulation.rs index 30685e0..7099c44 100644 --- a/idevice/src/dvt/location_simulation.rs +++ b/idevice/src/dvt/location_simulation.rs @@ -1,24 +1,72 @@ -// Jackson Coxson +//! Location Simulation service client for iOS instruments protocol. +//! +//! This module abstracts simulating the device's location over +//! the remote server protocol. Note that a connection must be +//! maintained to keep location simulated. +//! +//! # Example +//! ```rust,no_run +//! #[tokio::main] +//! async fn main() -> Result<(), IdeviceError> { +//! // Create base client (implementation specific) +//! let mut client = RemoteServerClient::new(your_transport); +//! +//! // Create process control client +//! let mut process_control = ProcessControlClient::new(&mut client).await?; +//! +//! // Launch an app +//! let pid = process_control.launch_app( +//! "com.example.app", +//! None, // Environment variables +//! None, // Arguments +//! false, // Start suspended +//! true // Kill existing +//! ).await?; +//! println!("Launched app with PID: {}", pid); +//! +//! // Disable memory limits +//! process_control.disable_memory_limit(pid).await?; +//! +//! // Kill the app +//! process_control.kill_app(pid).await?; +//! +//! Ok(()) +//! } +//! ``` use plist::Value; -use crate::{dvt::message::AuxValue, IdeviceError, ReadWrite}; - -use super::remote_server::{Channel, RemoteServerClient}; +use crate::{ + dvt::{ + message::AuxValue, + remote_server::{Channel, RemoteServerClient}, + }, + IdeviceError, ReadWrite, +}; const IDENTIFIER: &str = "com.apple.instruments.server.services.LocationSimulation"; +/// A client for the location simulation service pub struct LocationSimulationClient<'a, R: ReadWrite> { + /// The underlying channel used for communication channel: Channel<'a, R>, } impl<'a, R: ReadWrite> LocationSimulationClient<'a, R> { + /// Opens a new channel on the remote server client for location simulation + /// + /// # 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(IDENTIFIER).await?; // Drop `&mut client` before continuing Ok(Self { channel }) } + /// Clears the set GPS location pub async fn clear(&mut self) -> Result<(), IdeviceError> { let method = Value::String("stopLocationSimulation".into()); @@ -29,6 +77,14 @@ impl<'a, R: ReadWrite> LocationSimulationClient<'a, R> { Ok(()) } + /// Sets the GPS location + /// + /// # Arguments + /// * `latitude` - The f64 latitude value + /// * `longitude` - The f64 longitude value + /// + /// # Errors + /// Returns an IdeviceError on failure pub async fn set(&mut self, latitude: f64, longitude: f64) -> Result<(), IdeviceError> { let method = Value::String("simulateLocationWithLatitude:longitude:".into()); diff --git a/idevice/src/dvt/message.rs b/idevice/src/dvt/message.rs index 26b6e69..156d352 100644 --- a/idevice/src/dvt/message.rs +++ b/idevice/src/dvt/message.rs @@ -1,68 +1,172 @@ -// Jackson Coxson -// Messages contain: -// - 32 byte header -// - 16 byte payload header -// - Optional auxiliary -// - 16 byte aux header, useless -// - Aux data -// - Payload (NSKeyedArchive) +//! Instruments protocol message format implementation +//! +//! This module handles the serialization and deserialization of messages used in +//! the iOS instruments protocol. The message format consists of: +//! - 32-byte message header +//! - 16-byte payload header +//! - Optional auxiliary data section +//! - Payload data (typically NSKeyedArchive format) +//! +//! # Message Structure +//! ```text +//! +---------------------+ +//! | MessageHeader | 32 bytes +//! +---------------------+ +//! | PayloadHeader | 16 bytes +//! +---------------------+ +//! | AuxHeader | 16 bytes (if aux present) +//! | Aux data | variable length +//! +---------------------+ +//! | Payload data | variable length (NSKeyedArchive) +//! +---------------------+ +//! ``` +//! +//! # Example +//! ```rust,no_run +//! use plist::Value; +//! use your_crate::IdeviceError; +//! use your_crate::dvt::message::{Message, MessageHeader, PayloadHeader, AuxValue}; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), IdeviceError> { +//! // Create a new message +//! let header = MessageHeader::new( +//! 1, // fragment_id +//! 1, // fragment_count +//! 123, // identifier +//! 0, // conversation_index +//! 42, // channel +//! true // expects_reply +//! ); +//! +//! let message = Message::new( +//! header, +//! PayloadHeader::method_invocation(), +//! Some(AuxValue::from_values(vec![ +//! AuxValue::String("param".into()), +//! AuxValue::U32(123), +//! ])), +//! Some(Value::String("data".into())) +//! ); +//! +//! // Serialize message +//! let bytes = message.serialize(); +//! +//! // Deserialize message (from async reader) +//! # let mut reader = &bytes[..]; +//! let deserialized = Message::from_reader(&mut reader).await?; +//! # Ok(()) +//! # } use plist::Value; use tokio::io::{AsyncRead, AsyncReadExt}; use crate::IdeviceError; +/// Message header containing metadata about the message +/// +/// 32-byte structure that appears at the start of every message #[derive(Debug, Clone, PartialEq)] pub struct MessageHeader { - magic: u32, // 0x795b3d1f - header_len: u32, // will always be 32 bytes + /// Magic number identifying the protocol (0x1F3D5B79) + magic: u32, + /// Length of this header (always 32) + header_len: u32, + /// Fragment identifier for multipart messages fragment_id: u16, + /// Total number of fragments fragment_count: u16, - length: u32, // Length of of the payload + /// Total length of payload (headers + aux + data) + length: u32, + /// Unique message identifier identifier: u32, + /// Conversation tracking index conversation_index: u32, + /// Channel number this message belongs to pub channel: u32, + /// Whether a reply is expected expects_reply: bool, } +/// Payload header containing information about the message contents +/// +/// 16-byte structure following the message header #[derive(Debug, Default, Clone, PartialEq)] pub struct PayloadHeader { + /// Flags controlling message processing flags: u32, + /// Length of auxiliary data section aux_length: u32, + /// Total length of payload (aux + data) total_length: u64, } +/// Header for auxiliary data section +/// +/// 16-byte structure preceding auxiliary data #[derive(Debug, Default, PartialEq)] pub struct AuxHeader { + /// Buffer size hint (often 496) buffer_size: u32, + /// Unknown field (typically 0) unknown: u32, + /// Actual size of auxiliary data aux_size: u32, + /// Unknown field (typically 0) unknown2: u32, } +/// Auxiliary data container +/// +/// Contains a header and a collection of typed values #[derive(Debug, PartialEq)] pub struct Aux { + /// Auxiliary data header pub header: AuxHeader, + /// Collection of auxiliary values pub values: Vec, } +/// Typed auxiliary value that can be included in messages #[derive(PartialEq)] pub enum AuxValue { - String(String), // 0x01 - Array(Vec), // 0x02 - U32(u32), // 0x03 - I64(i64), // 0x06 + /// UTF-8 string value (type 0x01) + String(String), + /// Raw byte array (type 0x02) + Array(Vec), + /// 32-bit unsigned integer (type 0x03) + U32(u32), + /// 64-bit signed integer (type 0x06) + I64(i64), } +/// Complete protocol message #[derive(Debug, PartialEq)] pub struct Message { + /// Message metadata header pub message_header: MessageHeader, + /// Payload description header pub payload_header: PayloadHeader, + /// Optional auxiliary data pub aux: Option, + /// Optional payload data (typically NSKeyedArchive) pub data: Option, } impl Aux { + /// Parses auxiliary data from bytes + /// + /// # Arguments + /// * `bytes` - Raw byte slice containing auxiliary data + /// + /// # Returns + /// * `Ok(Aux)` - Parsed auxiliary data + /// * `Err(IdeviceError)` - If parsing fails + /// + /// # Errors + /// * `IdeviceError::NotEnoughBytes` if input is too short + /// * `IdeviceError::UnknownAuxValueType` for unsupported types + /// * `IdeviceError` for other parsing failures pub fn from_bytes(bytes: Vec) -> Result { if bytes.len() < 16 { return Err(IdeviceError::NotEnoughBytes(bytes.len(), 24)); @@ -129,8 +233,12 @@ impl Aux { Ok(Self { header, values }) } - // Creates the default struct - // Note that the header isn't updated until serialization + /// Creates new auxiliary data from values + /// + /// Note: Header fields are populated during serialization + /// + /// # Arguments + /// * `values` - Collection of auxiliary values to include pub fn from_values(values: Vec) -> Self { Self { header: AuxHeader::default(), @@ -138,7 +246,9 @@ impl Aux { } } - /// Serializes the values with the correctly sized header + /// Serializes auxiliary data to bytes + /// + /// Includes properly formatted header with updated size fields pub fn serialize(&self) -> Vec { let mut values_payload = Vec::new(); for v in self.values.iter() { @@ -180,14 +290,27 @@ impl Aux { } impl AuxValue { - // Returns an array AuxType + /// Creates an auxiliary value containing NSKeyedArchived data + /// + /// # Arguments + /// * `v` - Plist value to archive pub fn archived_value(v: impl Into) -> Self { Self::Array(ns_keyed_archive::encode::encode_to_bytes(v.into()).expect("Failed to encode")) } } impl MessageHeader { - /// Creates a new header. Note that during serialization, the length will be updated + /// Creates a new message header + /// + /// Note: Length field is updated during message serialization + /// + /// # Arguments + /// * `fragment_id` - Identifier for message fragments + /// * `fragment_count` - Total fragments in message + /// * `identifier` - Unique message ID + /// * `conversation_index` - Conversation tracking number + /// * `channel` - Channel number + /// * `expects_reply` - Whether response is expected pub fn new( fragment_id: u16, fragment_count: u16, @@ -209,6 +332,7 @@ impl MessageHeader { } } + /// Serializes header to bytes pub fn serialize(&self) -> Vec { let mut res = Vec::new(); res.extend_from_slice(&self.magic.to_le_bytes()); @@ -226,10 +350,12 @@ impl MessageHeader { } impl PayloadHeader { + /// Creates a new payload header pub fn new() -> Self { Self::default() } + /// Serializes header to bytes pub fn serialize(&self) -> Vec { let mut res = Vec::new(); res.extend_from_slice(&self.flags.to_le_bytes()); @@ -239,6 +365,7 @@ impl PayloadHeader { res } + /// Creates header for method invocation messages pub fn method_invocation() -> Self { Self { flags: 2, @@ -246,12 +373,24 @@ impl PayloadHeader { } } + /// Updates flags to indicate reply expectation pub fn apply_expects_reply_map(&mut self) { self.flags |= 0x1000 } } impl Message { + /// Reads and parses a message from an async reader + /// + /// # Arguments + /// * `reader` - Async reader to read from + /// + /// # Returns + /// * `Ok(Message)` - Parsed message + /// * `Err(IdeviceError)` - If reading/parsing fails + /// + /// # 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?; @@ -304,6 +443,13 @@ impl Message { }) } + /// Creates a new message + /// + /// # Arguments + /// * `message_header` - Message metadata + /// * `payload_header` - Payload description + /// * `aux` - Optional auxiliary data + /// * `data` - Optional payload data pub fn new( message_header: MessageHeader, payload_header: PayloadHeader, @@ -318,6 +464,9 @@ impl Message { } } + /// Serializes message to bytes + /// + /// Updates length fields in headers automatically pub fn serialize(&self) -> Vec { let aux = match &self.aux { Some(a) => a.serialize(), diff --git a/idevice/src/dvt/process_control.rs b/idevice/src/dvt/process_control.rs index 0457fbf..0d36823 100644 --- a/idevice/src/dvt/process_control.rs +++ b/idevice/src/dvt/process_control.rs @@ -1,4 +1,38 @@ -// Jackson Coxson +//! Process Control service client for iOS instruments protocol. +//! +//! This module provides a client for interacting with the process control service +//! on iOS devices through the instruments protocol. It allows launching, killing, +//! and managing processes on the device. +//! +//! # Example +//! ```rust,no_run +//! #[tokio::main] +//! async fn main() -> Result<(), IdeviceError> { +//! // Create base client (implementation specific) +//! let mut client = RemoteServerClient::new(your_transport); +//! +//! // Create process control client +//! let mut process_control = ProcessControlClient::new(&mut client).await?; +//! +//! // Launch an app +//! let pid = process_control.launch_app( +//! "com.example.app", +//! None, // Environment variables +//! None, // Arguments +//! false, // Start suspended +//! true // Kill existing +//! ).await?; +//! println!("Launched app with PID: {}", pid); +//! +//! // Disable memory limits +//! process_control.disable_memory_limit(pid).await?; +//! +//! // Kill the app +//! process_control.kill_app(pid).await?; +//! +//! Ok(()) +//! } +//! ``` use log::warn; use plist::{Dictionary, Value}; @@ -9,17 +43,49 @@ use super::remote_server::{Channel, RemoteServerClient}; const IDENTIFIER: &str = "com.apple.instruments.server.services.processcontrol"; +/// Client for process control operations on iOS devices +/// +/// Provides methods for launching, killing, and managing processes through the +/// instruments protocol. Each instance maintains its own communication channel. pub struct ProcessControlClient<'a, R: ReadWrite> { + /// The underlying channel for communication channel: Channel<'a, R>, } impl<'a, R: ReadWrite> ProcessControlClient<'a, R> { + /// Creates a new ProcessControlClient + /// + /// # Arguments + /// * `client` - The base RemoteServerClient to use + /// + /// # Returns + /// * `Ok(ProcessControlClient)` - 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(IDENTIFIER).await?; // Drop `&mut client` before continuing Ok(Self { channel }) } + /// Launches an application on the device + /// + /// # Arguments + /// * `bundle_id` - The bundle identifier of the app to launch + /// * `env_vars` - Optional environment variables dictionary + /// * `arguments` - Optional launch arguments dictionary + /// * `start_suspended` - Whether to start the process suspended + /// * `kill_existing` - Whether to kill existing instances of the app + /// + /// # Returns + /// * `Ok(u64)` - PID of the launched process + /// * `Err(IdeviceError)` - If launch fails + /// + /// # Errors + /// * `IdeviceError::UnexpectedResponse` if server response is invalid + /// * Other communication or serialization errors pub async fn launch_app( &mut self, bundle_id: impl Into, @@ -76,6 +142,17 @@ impl<'a, R: ReadWrite> ProcessControlClient<'a, R> { } } + /// Kills a running process + /// + /// # Arguments + /// * `pid` - Process ID to kill + /// + /// # Returns + /// * `Ok(())` - If kill request was sent successfully + /// * `Err(IdeviceError)` - If communication fails + /// + /// # Note + /// This method doesn't wait for confirmation that the process was killed. pub async fn kill_app(&mut self, pid: u64) -> Result<(), IdeviceError> { self.channel .call_method( @@ -88,6 +165,19 @@ impl<'a, R: ReadWrite> ProcessControlClient<'a, R> { Ok(()) } + /// Disables memory limits for a process + /// + /// # Arguments + /// * `pid` - Process ID to modify + /// + /// # Returns + /// * `Ok(())` - If memory limits were disabled + /// * `Err(IdeviceError)` - If operation fails + /// + /// # Errors + /// * `IdeviceError::DisableMemoryLimitFailed` if device reports failure + /// * `IdeviceError::UnexpectedResponse` for invalid responses + /// * Other communication errors pub async fn disable_memory_limit(&mut self, pid: u64) -> Result<(), IdeviceError> { self.channel .call_method( diff --git a/idevice/src/dvt/remote_server.rs b/idevice/src/dvt/remote_server.rs index 561f321..9e91a53 100644 --- a/idevice/src/dvt/remote_server.rs +++ b/idevice/src/dvt/remote_server.rs @@ -1,4 +1,53 @@ -// Jackson Coxson +//! Remote Server Client implementation for iOS instruments protocol. +//! +//! This module provides a client for communicating with iOS devices through the +//! remote server protocol used by instruments. It handles channel management and +//! message passing between the host and device. +//! +//! Remote Server communicates via NSKeyedArchives. These archives are binary plists +//! formatted specifically for naive recreation at the target. +//! Requests are sent as method calls to objective C objects on the device. +//! +//! # Overview +//! The client manages multiple communication channels and provides methods for: +//! - Creating new channels +//! - Sending method calls +//! - Reading responses +//! +//! # Example +//! ```rust,no_run +//! use std::sync::Arc; +//! use tokio::net::TcpStream; +//! use your_crate::{ReadWrite, IdeviceError}; +//! use your_crate::instruments::RemoteServerClient; +//! +//! #[tokio::main] +//! async fn main() -> Result<(), IdeviceError> { +//! // Establish connection to device over the tunnel (see XPC docs) +//! let transport = TcpStream::connect("1.2.3.4:1234").await?; +//! +//! // Create client +//! let mut client = RemoteServerClient::new(transport); +//! +//! // Read the first message +//! client.read_message(0).await?; +//! +//! // Call a method on root channel +//! client.call_method( +//! 0, +//! Some("someMethod"), +//! Some(vec![AuxValue::String("param".into())]), +//! true +//! ).await?; +//! +//! // Read response +//! let response = client.read_message(0).await?; +//! println!("Got response: {:?}", response); +//! +//! +//! Ok(()) +//! } +//! ``` use std::collections::{HashMap, VecDeque}; @@ -6,27 +55,46 @@ use log::{debug, warn}; use tokio::io::AsyncWriteExt; use crate::{ - dvt::message::{Aux, Message, MessageHeader, PayloadHeader}, + dvt::message::{Aux, AuxValue, Message, MessageHeader, PayloadHeader}, IdeviceError, ReadWrite, }; -use super::message::AuxValue; - +/// Message type identifier for instruments protocol pub const INSTRUMENTS_MESSAGE_TYPE: u32 = 2; +/// Client for communicating with iOS remote server protocol +/// +/// Manages multiple communication channels and handles message serialization/deserialization. +/// Each channel operates independently and maintains its own message queue. pub struct RemoteServerClient { + /// The underlying device connection idevice: R, + /// Counter for message identifiers current_message: u32, + /// Next available channel number new_channel: u32, + /// Map of channel numbers to their message queues channels: HashMap>, } +/// Handle to a specific communication channel +/// +/// Provides channel-specific operations for use on the remote server client. pub struct Channel<'a, R: ReadWrite> { + /// Reference to parent client client: &'a mut RemoteServerClient, + /// Channel number this handle operates on channel: u32, } impl RemoteServerClient { + /// Creates a new RemoteServerClient with the given transport + /// + /// # Arguments + /// * `idevice` - The underlying transport implementing ReadWrite + /// + /// # Returns + /// A new client instance with root channel (0) initialized pub fn new(idevice: R) -> Self { let mut channels = HashMap::new(); channels.insert(0, VecDeque::new()); @@ -38,10 +106,12 @@ impl RemoteServerClient { } } + /// Consumes the client and returns the underlying transport pub fn into_inner(self) -> R { self.idevice } + /// Returns a handle to the root channel (channel 0) pub fn root_channel(&mut self) -> Channel { Channel { client: self, @@ -49,6 +119,18 @@ impl RemoteServerClient { } } + /// Creates a new channel with the given identifier + /// + /// # Arguments + /// * `identifier` - String identifier for the new channel + /// + /// # Returns + /// * `Ok(Channel)` - Handle to the new channel + /// * `Err(IdeviceError)` - If channel creation fails + /// + /// # Errors + /// * `IdeviceError::UnexpectedResponse` if server responds with unexpected data + /// * Other IO or serialization errors pub async fn make_channel( &mut self, identifier: impl Into, @@ -89,6 +171,20 @@ impl RemoteServerClient { }) } + /// Calls a method on the specified channel + /// + /// # Arguments + /// * `channel` - Channel number to call method on + /// * `data` - Optional method data (plist value) + /// * `args` - Optional arguments for the method + /// * `expect_reply` - Whether to expect a response + /// + /// # Returns + /// * `Ok(())` - If method was successfully called + /// * `Err(IdeviceError)` - If call failed + /// + /// # Errors + /// IO or serialization errors pub async fn call_method( &mut self, channel: u32, @@ -110,6 +206,20 @@ impl RemoteServerClient { Ok(()) } + /// Reads the next message from the specified channel + /// + /// Checks cached messages first, then reads from transport if needed. + /// + /// # Arguments + /// * `channel` - Channel number to read from + /// + /// # Returns + /// * `Ok(Message)` - The received message + /// * `Err(IdeviceError)` - If read failed + /// + /// # Errors + /// * `IdeviceError::UnknownChannel` if channel doesn't exist + /// * Other IO or deserialization errors pub async fn read_message(&mut self, channel: u32) -> Result { // Determine if we already have a message cached let cache = match self.channels.get_mut(&channel) { @@ -140,10 +250,32 @@ impl RemoteServerClient { } impl Channel<'_, R> { + /// Reads the next message from the remote server on this channel + /// + /// # Returns + /// * `Ok(Message)` - The received message + /// * `Err(IdeviceError)` - If read failed + /// + /// # Errors + /// * `IdeviceError::UnknownChannel` if channel doesn't exist + /// * Other IO or deserialization errors pub async fn read_message(&mut self) -> Result { self.client.read_message(self.channel).await } + /// Calls a method on the specified channel + /// + /// # Arguments + /// * `method` - Optional method data (plist value) + /// * `args` - Optional arguments for the method + /// * `expect_reply` - Whether to expect a response + /// + /// # Returns + /// * `Ok(())` - If method was successfully called + /// * `Err(IdeviceError)` - If call failed + /// + /// # Errors + /// IO or serialization errors pub async fn call_method( &mut self, method: Option>, diff --git a/idevice/src/heartbeat.rs b/idevice/src/heartbeat.rs index 0fc6ca5..2fa6792 100644 --- a/idevice/src/heartbeat.rs +++ b/idevice/src/heartbeat.rs @@ -1,21 +1,49 @@ -// Jackson Coxson -// Abstractions for the heartbeat service on iOS +//! iOS Device Heartbeat Service Abstraction +//! +//! iOS automatically closes service connections if there is no heartbeat client connected and +//! responding. -use crate::{lockdown::LockdowndClient, Idevice, IdeviceError, IdeviceService}; +use crate::{lockdown::LockdownClient, Idevice, IdeviceError, IdeviceService}; +/// Client for interacting with the iOS device heartbeat service +/// +/// The heartbeat service provides a keep-alive mechanism and can notify when +/// the device enters sleep mode or disconnects. +/// Note that a running heartbeat client is required to access other services on the device. +/// Implements the standard "Marco-Polo" protocol +/// where the host sends "Polo" in response to the device's "Marco". pub struct HeartbeatClient { + /// The underlying device connection with established heartbeat service pub idevice: Idevice, } impl IdeviceService for HeartbeatClient { + /// Returns the heartbeat service name as registered with lockdownd fn service_name() -> &'static str { "com.apple.mobile.heartbeat" } + /// Establishes a connection to the heartbeat service + /// + /// # Arguments + /// * `provider` - Device connection provider + /// + /// # Returns + /// A connected `HeartbeatClient` instance + /// + /// # Errors + /// Returns `IdeviceError` if any step of the connection process fails + /// + /// # Process + /// 1. Connects to lockdownd service + /// 2. Starts a lockdown session + /// 3. Requests the heartbeat service port + /// 4. Establishes connection to the heartbeat port + /// 5. Optionally starts TLS if required by service async fn connect( provider: &dyn crate::provider::IdeviceProvider, ) -> Result { - let mut lockdown = LockdowndClient::connect(provider).await?; + let mut lockdown = LockdownClient::connect(provider).await?; lockdown .start_session(&provider.get_pairing_file().await?) .await?; @@ -34,10 +62,31 @@ impl IdeviceService for HeartbeatClient { } impl HeartbeatClient { + /// Creates a new heartbeat client from an existing device connection + /// + /// # Arguments + /// * `idevice` - Pre-established device connection pub fn new(idevice: Idevice) -> Self { Self { idevice } } + /// Waits for and processes a "Marco" message from the device + /// + /// This will either: + /// - Return the heartbeat interval if received + /// - Return a timeout error if no message received in time + /// - Return a sleep notification if device is going to sleep + /// + /// # Arguments + /// * `interval` - Timeout in seconds to wait for message + /// + /// # Returns + /// The heartbeat interval in seconds if successful + /// + /// # Errors + /// - `HeartbeatTimeout` if no message received before interval + /// - `HeartbeatSleepyTime` if device is going to sleep + /// - `UnexpectedResponse` for malformed messages pub async fn get_marco(&mut self, interval: u64) -> Result { // Get a plist or wait for the interval let rec = tokio::select! { @@ -67,6 +116,13 @@ impl HeartbeatClient { } } + /// Sends a "Polo" response to the device + /// + /// This acknowledges receipt of a "Marco" message and maintains + /// the connection keep-alive. + /// + /// # Errors + /// Returns `IdeviceError` if the message fails to send pub async fn send_polo(&mut self) -> Result<(), IdeviceError> { let mut req = plist::Dictionary::new(); req.insert("Command".into(), "Polo".into()); diff --git a/idevice/src/installation_proxy.rs b/idevice/src/installation_proxy.rs index 4d55f3d..b638d62 100644 --- a/idevice/src/installation_proxy.rs +++ b/idevice/src/installation_proxy.rs @@ -1,23 +1,48 @@ -// Jackson Coxson -// Incomplete implementation for installation_proxy +//! iOS Installation Proxy Service Client +//! +//! Provides functionality for interacting with the installation_proxy service on iOS devices, +//! which allows querying and managing installed applications. use std::collections::HashMap; -use crate::{lockdown::LockdowndClient, Idevice, IdeviceError, IdeviceService}; +use crate::{lockdown::LockdownClient, Idevice, IdeviceError, IdeviceService}; +/// Client for interacting with the iOS installation proxy service +/// +/// This service provides access to information about installed applications +/// and can perform application management operations. pub struct InstallationProxyClient { + /// The underlying device connection with established installation_proxy service pub idevice: Idevice, } impl IdeviceService for InstallationProxyClient { + /// Returns the installation proxy service name as registered with lockdownd fn service_name() -> &'static str { "com.apple.mobile.installation_proxy" } + /// Establishes a connection to the installation proxy service + /// + /// # Arguments + /// * `provider` - Device connection provider + /// + /// # Returns + /// A connected `InstallationProxyClient` instance + /// + /// # Errors + /// Returns `IdeviceError` if any step of the connection process fails + /// + /// # Process + /// 1. Connects to lockdownd service + /// 2. Starts a lockdown session + /// 3. Requests the installation proxy service port + /// 4. Establishes connection to the service port + /// 5. Optionally starts TLS if required by service async fn connect( provider: &dyn crate::provider::IdeviceProvider, ) -> Result { - let mut lockdown = LockdowndClient::connect(provider).await?; + let mut lockdown = LockdownClient::connect(provider).await?; lockdown .start_session(&provider.get_pairing_file().await?) .await?; @@ -35,14 +60,39 @@ impl IdeviceService for InstallationProxyClient { } impl InstallationProxyClient { + /// Creates a new installation proxy client from an existing device connection + /// + /// # Arguments + /// * `idevice` - Pre-established device connection pub fn new(idevice: Idevice) -> Self { Self { idevice } } - /// Gets installed apps on the device + /// Retrieves information about installed applications + /// /// # Arguments - /// `application_type` - The application type to filter by - /// `bundle_identifiers` - The identifiers to filter by + /// * `application_type` - Optional filter for application type: + /// - "System" for system applications + /// - "User" for user-installed applications + /// - "Any" for all applications (default) + /// * `bundle_identifiers` - Optional list of specific bundle IDs to query + /// + /// # Returns + /// A HashMap mapping bundle identifiers to application information plist values + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Communication fails + /// - The response is malformed + /// - The service returns an error + /// + /// # Example + /// ```rust + /// let apps = client.get_apps(Some("User".to_string()), None).await?; + /// for (bundle_id, info) in apps { + /// println!("{}: {:?}", bundle_id, info); + /// } + /// ``` pub async fn get_apps( &mut self, application_type: Option, @@ -55,7 +105,7 @@ impl InstallationProxyClient { .into_iter() .map(plist::Value::String) .collect::>(); - options.insert("BundleIDs".into(), ids.into()).unwrap(); + options.insert("BundleIDs".into(), ids.into()); } options.insert("ApplicationType".into(), application_type.into()); @@ -75,3 +125,4 @@ impl InstallationProxyClient { } } } + diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 4645f65..4354920 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -12,7 +12,7 @@ pub mod dvt; #[cfg(feature = "heartbeat")] pub mod heartbeat; #[cfg(feature = "xpc")] -pub mod http2; +mod http2; #[cfg(feature = "installation_proxy")] pub mod installation_proxy; pub mod lockdown; @@ -49,24 +49,58 @@ use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; pub use util::{pretty_print_dictionary, pretty_print_plist}; +/// A trait combining all required characteristics for a device communication socket +/// +/// This serves as a convenience trait for any type that can be used as an asynchronous +/// read/write socket for device communication. Combines common async I/O traits with +/// thread safety and debugging requirements. +/// +/// Tokio's TcpStream and UnixStream implement this trait. pub trait ReadWrite: AsyncRead + AsyncWrite + Unpin + Send + Sync + std::fmt::Debug {} + +// Blanket implementation for any compatible type impl ReadWrite for T {} +/// Interface for services that can be connected to on an iOS device +/// +/// Implement this trait to define new services that can be accessed through the +/// device connection protocol. pub trait IdeviceService: Sized { + /// Returns the service name as advertised by the device fn service_name() -> &'static str; + + /// Establishes a connection to this service + /// + /// # Arguments + /// * `provider` - The device provider that can supply connections fn connect( provider: &dyn IdeviceProvider, ) -> impl std::future::Future> + Send; } +/// Type alias for boxed device connection sockets +/// +/// Used to enable dynamic dispatch of different connection types while maintaining +/// the required ReadWrite characteristics. pub type IdeviceSocket = Box; +/// Main handle for communicating with an iOS device +/// +/// Manages the connection socket and provides methods for common device operations +/// and message exchange. pub struct Idevice { - socket: Option>, // in a box for now to use the ReadWrite trait for further uses + /// The underlying connection socket, boxed for dynamic dispatch + socket: Option>, + /// Unique label identifying this connection label: String, } impl Idevice { + /// Creates a new device connection handle + /// + /// # Arguments + /// * `socket` - The established connection socket + /// * `label` - Unique identifier for this connection pub fn new(socket: Box, label: impl Into) -> Self { Self { socket: Some(socket), @@ -74,6 +108,15 @@ impl Idevice { } } + /// Queries the device type + /// + /// Sends a QueryType request and parses the response + /// + /// # Returns + /// The device type string on success + /// + /// # Errors + /// Returns `IdeviceError` if communication fails or response is invalid pub async fn get_type(&mut self) -> Result { let mut req = plist::Dictionary::new(); req.insert("Label".into(), self.label.clone().into()); @@ -87,6 +130,12 @@ impl Idevice { } } + /// Performs RSD (Remote Service Discovery) check-in procedure + /// + /// Establishes the basic service connection protocol + /// + /// # Errors + /// Returns `IdeviceError` if the protocol sequence isn't followed correctly pub async fn rsd_checkin(&mut self) -> Result<(), IdeviceError> { let mut req = plist::Dictionary::new(); req.insert("Label".into(), self.label.clone().into()); @@ -116,7 +165,13 @@ impl Idevice { Ok(()) } - /// Sends a plist to the socket + /// Sends a plist-formatted message to the device + /// + /// # Arguments + /// * `message` - The plist value to send + /// + /// # Errors + /// Returns `IdeviceError` if serialization or transmission fails async fn send_plist(&mut self, message: plist::Value) -> Result<(), IdeviceError> { if let Some(socket) = &mut self.socket { debug!("Sending plist: {}", pretty_print_plist(&message)); @@ -135,11 +190,30 @@ impl Idevice { } } - /// Sends raw bytes to the socket + /// Sends raw binary data to the device + /// + /// # Arguments + /// * `message` - The bytes to send + /// + /// # Errors + /// Returns `IdeviceError` if transmission fails async fn send_raw(&mut self, message: &[u8]) -> Result<(), IdeviceError> { self.send_raw_with_progress(message, |_| async {}, ()).await } + /// Sends raw binary data with progress callbacks + /// + /// # Arguments + /// * `message` - The bytes to send + /// * `callback` - Progress callback invoked after each chunk + /// * `state` - Arbitrary state passed to callback + /// + /// # Type Parameters + /// * `Fut` - Future type returned by callback + /// * `S` - Type of state passed to callback + /// + /// # Errors + /// Returns `IdeviceError` if transmission fails async fn send_raw_with_progress( &mut self, message: &[u8], @@ -165,7 +239,16 @@ impl Idevice { } } - /// Reads raw bytes from the socket + /// Reads exactly `len` bytes from the device + /// + /// # Arguments + /// * `len` - Exact number of bytes to read + /// + /// # Returns + /// The received bytes + /// + /// # Errors + /// Returns `IdeviceError` if reading fails or connection is closed prematurely async fn read_raw(&mut self, len: usize) -> Result, IdeviceError> { if let Some(socket) = &mut self.socket { let mut buf = vec![0; len]; @@ -176,7 +259,16 @@ impl Idevice { } } - /// Reads bytes from the socket until it doesn't + /// Reads up to `max_size` bytes from the device + /// + /// # Arguments + /// * `max_size` - Maximum number of bytes to read + /// + /// # Returns + /// The received bytes (may be shorter than max_size) + /// + /// # Errors + /// Returns `IdeviceError` if reading fails async fn read_any(&mut self, max_size: u32) -> Result, IdeviceError> { if let Some(socket) = &mut self.socket { let mut buf = vec![0; max_size as usize]; @@ -187,7 +279,13 @@ impl Idevice { } } - /// Read a plist from the socket + /// Reads a plist-formatted message from the device + /// + /// # Returns + /// The parsed plist dictionary + /// + /// # Errors + /// Returns `IdeviceError` if reading, parsing fails, or device reports an error async fn read_plist(&mut self) -> Result { if let Some(socket) = &mut self.socket { debug!("Reading response size"); @@ -213,7 +311,13 @@ impl Idevice { } } - /// Wraps current connection in TLS + /// Upgrades the connection to TLS using device pairing credentials + /// + /// # Arguments + /// * `pairing_file` - Contains the device's identity and certificates + /// + /// # Errors + /// Returns `IdeviceError` if TLS handshake fails or credentials are invalid pub async fn start_session( &mut self, pairing_file: &pairing_file::PairingFile, @@ -235,6 +339,7 @@ impl Idevice { } } +/// Comprehensive error type for all device communication failures #[derive(Error, Debug)] #[non_exhaustive] pub enum IdeviceError { @@ -362,6 +467,14 @@ pub enum IdeviceError { } impl IdeviceError { + /// Converts a device-reported error string to a typed error + /// + /// # Arguments + /// * `e` - The error string from device + /// * `context` - Full plist context containing additional error details + /// + /// # Returns + /// Some(IdeviceError) if the string maps to a known error type, None otherwise fn from_device_error_type(e: &str, context: &plist::Dictionary) -> Option { match e { "GetProhibited" => Some(Self::GetProhibited), diff --git a/idevice/src/lockdown.rs b/idevice/src/lockdown.rs index df91bce..f690e02 100644 --- a/idevice/src/lockdown.rs +++ b/idevice/src/lockdown.rs @@ -1,5 +1,7 @@ -// Jackson Coxson -// Abstractions for lockdownd +//! iOS Lockdown Service Client +//! +//! Provides functionality for interacting with the lockdown service on iOS devices, +//! which is the primary service for device management and service discovery. use log::error; use plist::Value; @@ -7,15 +9,33 @@ use serde::{Deserialize, Serialize}; use crate::{pairing_file, Idevice, IdeviceError, IdeviceService}; -pub struct LockdowndClient { +/// Client for interacting with the iOS lockdown service +/// +/// This is the primary service for device management and provides: +/// - Access to device information and settings +/// - Service discovery and port allocation +/// - Session management and security +pub struct LockdownClient { + /// The underlying device connection with established lockdown service pub idevice: crate::Idevice, } -impl IdeviceService for LockdowndClient { +impl IdeviceService for LockdownClient { + /// Returns the lockdown service name as registered with the device fn service_name() -> &'static str { "com.apple.mobile.lockdown" } + /// Establishes a connection to the lockdown service + /// + /// # Arguments + /// * `provider` - Device connection provider + /// + /// # Returns + /// A connected `LockdownClient` instance + /// + /// # Errors + /// Returns `IdeviceError` if connection fails async fn connect( provider: &dyn crate::provider::IdeviceProvider, ) -> Result { @@ -24,22 +44,48 @@ impl IdeviceService for LockdowndClient { } } +/// Internal structure for lockdown protocol requests #[derive(Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] -struct LockdowndRequest { +struct LockdownRequest { label: String, key: Option, request: String, } -impl LockdowndClient { +impl LockdownClient { + /// The default TCP port for the lockdown service pub const LOCKDOWND_PORT: u16 = 62078; + /// Creates a new lockdown client from an existing device connection + /// + /// # Arguments + /// * `idevice` - Pre-established device connection pub fn new(idevice: Idevice) -> Self { Self { idevice } } + + /// Retrieves a specific value from the device + /// + /// # Arguments + /// * `value` - The name of the value to retrieve (e.g., "DeviceName") + /// + /// # Returns + /// The requested value as a plist Value + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Communication fails + /// - The requested value doesn't exist + /// - The response is malformed + /// + /// # Example + /// ```rust + /// let device_name = client.get_value("DeviceName").await?; + /// println!("Device name: {:?}", device_name); + /// ``` pub async fn get_value(&mut self, value: impl Into) -> Result { - let req = LockdowndRequest { + let req = LockdownRequest { label: self.idevice.label.clone(), key: Some(value.into()), request: "GetValue".to_string(), @@ -53,8 +99,25 @@ impl LockdowndClient { } } + /// Retrieves all available values from the device + /// + /// # Returns + /// A dictionary containing all device values + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Communication fails + /// - The response is malformed + /// + /// # Example + /// ```rust + /// let all_values = client.get_all_values().await?; + /// for (key, value) in all_values { + /// println!("{}: {:?}", key, value); + /// } + /// ``` pub async fn get_all_values(&mut self) -> Result { - let req = LockdowndRequest { + let req = LockdownRequest { label: self.idevice.label.clone(), key: None, request: "GetValue".to_string(), @@ -68,7 +131,19 @@ impl LockdowndClient { } } - /// Starts a TLS session with the client + /// Starts a secure TLS session with the device + /// + /// # Arguments + /// * `pairing_file` - Contains the device's identity and certificates + /// + /// # Returns + /// `Ok(())` on successful session establishment + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - No connection is established + /// - The session request is denied + /// - TLS handshake fails pub async fn start_session( &mut self, pairing_file: &pairing_file::PairingFile, @@ -116,11 +191,21 @@ impl LockdowndClient { Ok(()) } - /// Asks lockdownd to pretty please start a service for us + /// Requests to start a service on the device + /// /// # Arguments - /// `identifier` - The identifier for the service you want to start + /// * `identifier` - The service identifier (e.g., "com.apple.debugserver") + /// /// # Returns - /// The port number and whether to enable SSL on success, `IdeviceError` on failure + /// A tuple containing: + /// - The port number where the service is available + /// - A boolean indicating whether SSL should be used + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - The service cannot be started + /// - The response is malformed + /// - The requested service doesn't exist pub async fn start_service( &mut self, identifier: impl Into, @@ -144,7 +229,7 @@ impl LockdowndClient { if let Some(port) = port.as_unsigned() { Ok((port as u16, ssl)) } else { - error!("Port isn't an unsiged integer!"); + error!("Port isn't an unsigned integer!"); Err(IdeviceError::UnexpectedResponse) } } @@ -156,7 +241,8 @@ impl LockdowndClient { } } -impl From for LockdowndClient { +impl From for LockdownClient { + /// Converts an existing device connection into a lockdown client fn from(value: Idevice) -> Self { Self::new(value) } diff --git a/idevice/src/misagent.rs b/idevice/src/misagent.rs index 3a2f2bf..b6a2e04 100644 --- a/idevice/src/misagent.rs +++ b/idevice/src/misagent.rs @@ -1,24 +1,51 @@ -// Jackson Coxson -// Incomplete implementation for installation_proxy +//! iOS Mobile Installation Agent (misagent) Client +//! +//! Provides functionality for interacting with the misagent service on iOS devices, +//! which manages provisioning profiles and certificates. use log::warn; use plist::Dictionary; -use crate::{lockdown::LockdowndClient, Idevice, IdeviceError, IdeviceService}; +use crate::{lockdown::LockdownClient, Idevice, IdeviceError, IdeviceService}; +/// Client for interacting with the iOS misagent service +/// +/// The misagent service handles: +/// - Installation of provisioning profiles +/// - Removal of provisioning profiles +/// - Querying installed profiles pub struct MisagentClient { + /// The underlying device connection with established misagent service pub idevice: Idevice, } impl IdeviceService for MisagentClient { + /// Returns the misagent service name as registered with lockdownd fn service_name() -> &'static str { "com.apple.misagent" } + /// Establishes a connection to the misagent service + /// + /// # Arguments + /// * `provider` - Device connection provider + /// + /// # Returns + /// A connected `MisagentClient` instance + /// + /// # Errors + /// Returns `IdeviceError` if any step of the connection process fails + /// + /// # Process + /// 1. Connects to lockdownd service + /// 2. Starts a lockdown session + /// 3. Requests the misagent service port + /// 4. Establishes connection to the service port + /// 5. Optionally starts TLS if required by service async fn connect( provider: &dyn crate::provider::IdeviceProvider, ) -> Result { - let mut lockdown = LockdowndClient::connect(provider).await?; + let mut lockdown = LockdownClient::connect(provider).await?; lockdown .start_session(&provider.get_pairing_file().await?) .await?; @@ -36,15 +63,37 @@ impl IdeviceService for MisagentClient { } impl MisagentClient { + /// Creates a new misagent client from an existing device connection + /// + /// # Arguments + /// * `idevice` - Pre-established device connection pub fn new(idevice: Idevice) -> Self { Self { idevice } } + /// Installs a provisioning profile on the device + /// + /// # Arguments + /// * `profile` - The provisioning profile data to install + /// + /// # Returns + /// `Ok(())` on successful installation + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Communication fails + /// - The profile is invalid + /// - Installation is not permitted + /// + /// # Example + /// ```rust + /// let profile_data = std::fs::read("profile.mobileprovision")?; + /// client.install(profile_data).await?; + /// ``` pub async fn install(&mut self, profile: Vec) -> Result<(), IdeviceError> { let mut req = Dictionary::new(); req.insert("MessageType".into(), "Install".into()); req.insert("Profile".into(), plist::Value::Data(profile)); - req.insert("ProfileType".into(), "Provisioning".into()); self.idevice @@ -73,11 +122,28 @@ impl MisagentClient { } } + /// Removes a provisioning profile from the device + /// + /// # Arguments + /// * `id` - The UUID of the profile to remove + /// + /// # Returns + /// `Ok(())` on successful removal + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Communication fails + /// - The profile doesn't exist + /// - Removal is not permitted + /// + /// # Example + /// ```rust + /// client.remove("asdf").await?; + /// ``` pub async fn remove(&mut self, id: &str) -> Result<(), IdeviceError> { let mut req = Dictionary::new(); req.insert("MessageType".into(), "Remove".into()); req.insert("ProfileID".into(), id.into()); - req.insert("ProfileType".into(), "Provisioning".into()); self.idevice @@ -106,10 +172,26 @@ impl MisagentClient { } } + /// Retrieves all provisioning profiles from the device + /// + /// # Returns + /// A vector containing raw profile data for each installed profile + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Communication fails + /// - The response is malformed + /// + /// # Example + /// ```rust + /// let profiles = client.copy_all().await?; + /// for profile in profiles { + /// println!("Profile size: {} bytes", profile.len()); + /// } + /// ``` pub async fn copy_all(&mut self) -> Result>, IdeviceError> { let mut req = Dictionary::new(); req.insert("MessageType".into(), "CopyAll".into()); - req.insert("ProfileType".into(), "Provisioning".into()); self.idevice @@ -137,3 +219,4 @@ impl MisagentClient { } } } + diff --git a/idevice/src/mobile_image_mounter.rs b/idevice/src/mobile_image_mounter.rs index 7209b54..ad9e906 100644 --- a/idevice/src/mobile_image_mounter.rs +++ b/idevice/src/mobile_image_mounter.rs @@ -1,28 +1,58 @@ -// Jackson Coxson +//! iOS Image Mounter Client +//! +//! Provides functionality for mounting disk images on iOS devices, including: +//! - Developer disk images +//! - Personalized images +//! - Cryptex images +//! +//! Handles the complete workflow from uploading images to mounting them with proper signatures. use log::debug; -use crate::{lockdown::LockdowndClient, Idevice, IdeviceError, IdeviceService}; +use crate::{lockdown::LockdownClient, Idevice, IdeviceError, IdeviceService}; #[cfg(feature = "tss")] use crate::tss::TSSRequest; -/// Manages mounted images on the idevice. -/// NOTE: A lockdown client must be established and queried after establishing a mounter client, or -/// the device will stop responding to requests. +/// Client for interacting with the iOS mobile image mounter service +/// +/// Manages mounted images on the device. +/// +/// # Important Note +/// A lockdown client must be established and queried after establishing a mounter client, +/// or the device will stop responding to requests. pub struct ImageMounter { + /// The underlying device connection with established image mounter service idevice: Idevice, } impl IdeviceService for ImageMounter { + /// Returns the image mounter service name as registered with lockdownd fn service_name() -> &'static str { "com.apple.mobile.mobile_image_mounter" } + /// Establishes a connection to the image mounter service + /// + /// # Arguments + /// * `provider` - Device connection provider + /// + /// # Returns + /// A connected `ImageMounter` instance + /// + /// # Errors + /// Returns `IdeviceError` if any step of the connection process fails + /// + /// # Process + /// 1. Connects to lockdownd service + /// 2. Starts a lockdown session + /// 3. Requests the image mounter service port + /// 4. Establishes connection to the service port + /// 5. Optionally starts TLS if required by service async fn connect( provider: &dyn crate::provider::IdeviceProvider, ) -> Result { - let mut lockdown = LockdowndClient::connect(provider).await?; + let mut lockdown = LockdownClient::connect(provider).await?; lockdown .start_session(&provider.get_pairing_file().await?) .await?; @@ -41,10 +71,21 @@ impl IdeviceService for ImageMounter { } impl ImageMounter { + /// Creates a new image mounter client from an existing device connection + /// + /// # Arguments + /// * `idevice` - Pre-established device connection pub fn new(idevice: Idevice) -> Self { Self { idevice } } + /// Retrieves a list of currently mounted devices + /// + /// # Returns + /// A vector of plist values describing mounted devices + /// + /// # Errors + /// Returns `IdeviceError` if communication fails or response is malformed pub async fn copy_devices(&mut self) -> Result, IdeviceError> { let mut req = plist::Dictionary::new(); req.insert("Command".into(), "CopyDevices".into()); @@ -59,7 +100,16 @@ impl ImageMounter { } } - /// Looks up an image and returns the signature + /// Looks up an image by type and returns its signature + /// + /// # Arguments + /// * `image_type` - The type of image to lookup (e.g., "Developer") + /// + /// # Returns + /// The image signature if found + /// + /// # Errors + /// Returns `IdeviceError::NotFound` if image doesn't exist pub async fn lookup_image( &mut self, image_type: impl Into, @@ -79,6 +129,15 @@ impl ImageMounter { } } + /// Uploads an image to the device + /// + /// # Arguments + /// * `image_type` - Type of image being uploaded + /// * `image` - The image data + /// * `signature` - Signature for the image + /// + /// # Errors + /// Returns `IdeviceError` if upload fails pub async fn upload_image( &mut self, image_type: impl Into, @@ -89,6 +148,21 @@ impl ImageMounter { .await } + /// Uploads an image with progress callbacks + /// + /// # Arguments + /// * `image_type` - Type of image being uploaded + /// * `image` - The image data + /// * `signature` - Signature for the image + /// * `callback` - Progress callback + /// * `state` - State to pass to callback + /// + /// # Type Parameters + /// * `Fut` - Future type returned by callback + /// * `S` - Type of state passed to callback + /// + /// # Errors + /// Returns `IdeviceError` if upload fails pub async fn upload_image_with_progress( &mut self, image_type: impl Into, @@ -149,6 +223,16 @@ impl ImageMounter { Ok(()) } + /// Mounts an image on the device + /// + /// # Arguments + /// * `image_type` - Type of image to mount + /// * `signature` - Signature for the image + /// * `trust_cache` - Optional trust cache data + /// * `info_plist` - Optional info plist for the image + /// + /// # Errors + /// Returns `IdeviceError` if mounting fails pub async fn mount_image( &mut self, image_type: impl Into, @@ -187,9 +271,15 @@ impl ImageMounter { Ok(()) } - /// Unmounts an image at a specified path. - /// Use ``/Developer`` for pre-iOS 17 developer images. - /// Use ``/System/Developer`` for personalized images. + /// Unmounts an image at the specified path + /// + /// # Arguments + /// * `mount_path` - Path where image is mounted: + /// - `/Developer` for pre-iOS 17 developer images + /// - `/System/Developer` for personalized images + /// + /// # Errors + /// Returns `IdeviceError` if unmounting fails pub async fn unmount_image( &mut self, mount_path: impl Into, @@ -209,8 +299,20 @@ impl ImageMounter { } } - /// Queries the personalization manifest from the device. + /// Queries the personalization manifest from the device + /// + /// # Important /// On failure, the socket must be closed and reestablished. + /// + /// # Arguments + /// * `image_type` - Type of image to query manifest for + /// * `signature` - Signature of the image + /// + /// # Returns + /// The personalization manifest data + /// + /// # Errors + /// Returns `IdeviceError` if query fails pub async fn query_personalization_manifest( &mut self, image_type: impl Into, @@ -234,6 +336,13 @@ impl ImageMounter { } } + /// Queries the developer mode status of the device + /// + /// # Returns + /// `true` if developer mode is enabled, `false` otherwise + /// + /// # Errors + /// Returns `IdeviceError` if query fails pub async fn query_developer_mode_status(&mut self) -> Result { let mut req = plist::Dictionary::new(); req.insert("Command".into(), "QueryDeveloperModeStatus".into()); @@ -248,6 +357,16 @@ impl ImageMounter { } } + /// Queries the nonce value from the device + /// + /// # Arguments + /// * `personalized_image_type` - Optional image type to get nonce for + /// + /// # Returns + /// The nonce value + /// + /// # Errors + /// Returns `IdeviceError` if query fails pub async fn query_nonce( &mut self, personalized_image_type: Option, @@ -268,6 +387,16 @@ impl ImageMounter { } } + /// Queries personalization identifiers from the device + /// + /// # Arguments + /// * `image_type` - Optional image type to get identifiers for + /// + /// # Returns + /// Dictionary of personalization identifiers + /// + /// # Errors + /// Returns `IdeviceError` if query fails pub async fn query_personalization_identifiers( &mut self, image_type: Option, @@ -288,6 +417,10 @@ impl ImageMounter { } } + /// Rolls the personalization nonce on the device + /// + /// # Errors + /// Returns `IdeviceError` if operation fails pub async fn roll_personalization_nonce(&mut self) -> Result<(), IdeviceError> { let mut req = plist::Dictionary::new(); req.insert("Command".into(), "RollPersonalizationNonce".into()); @@ -298,6 +431,10 @@ impl ImageMounter { Ok(()) } + /// Rolls the cryptex nonce on the device + /// + /// # Errors + /// Returns `IdeviceError` if operation fails pub async fn roll_cryptex_nonce(&mut self) -> Result<(), IdeviceError> { let mut req = plist::Dictionary::new(); req.insert("Command".into(), "RollCryptexNonce".into()); @@ -308,6 +445,14 @@ impl ImageMounter { Ok(()) } + /// Mounts a developer disk image + /// + /// # Arguments + /// * `image` - The developer disk image data + /// * `signature` - Signature for the image + /// + /// # Errors + /// Returns `IdeviceError` if mounting fails pub async fn mount_developer( &mut self, image: &[u8], @@ -321,6 +466,18 @@ impl ImageMounter { } #[cfg(feature = "tss")] + /// Mounts a personalized image with automatic manifest handling + /// + /// # Arguments + /// * `provider` - Device connection provider (used for reconnection if needed) + /// * `image` - The image data + /// * `trust_cache` - Trust cache data + /// * `build_manifest` - Build manifest data + /// * `info_plist` - Optional info plist for the image + /// * `unique_chip_id` - Device's unique chip ID + /// + /// # Errors + /// Returns `IdeviceError` if mounting fails pub async fn mount_personalized( &mut self, provider: &dyn crate::provider::IdeviceProvider, @@ -344,9 +501,28 @@ impl ImageMounter { } #[cfg(feature = "tss")] - /// Calling this has the potential of closing the socket, - /// so a provider is required for this abstraction. - #[allow(clippy::too_many_arguments)] // literally nobody asked + /// Mounts a personalized image with progress callbacks + /// + /// # Important + /// This may close the socket on failure, requiring reconnection. + /// + /// # Arguments + /// * `provider` - Device connection provider + /// * `image` - The image data + /// * `trust_cache` - Trust cache data + /// * `build_manifest` - Build manifest data + /// * `info_plist` - Optional info plist for the image + /// * `unique_chip_id` - Device's unique chip ID + /// * `callback` - Progress callback + /// * `state` - State to pass to callback + /// + /// # Type Parameters + /// * `Fut` - Future type returned by callback + /// * `S` - Type of state passed to callback + /// + /// # Errors + /// Returns `IdeviceError` if mounting fails + #[allow(clippy::too_many_arguments)] pub async fn mount_personalized_with_callback( &mut self, provider: &dyn crate::provider::IdeviceProvider, @@ -385,7 +561,7 @@ impl ImageMounter { } }; - debug!("Uploading imaage"); + debug!("Uploading image"); self.upload_image_with_progress("Personalized", &image, manifest.clone(), callback, state) .await?; @@ -397,6 +573,17 @@ impl ImageMounter { } #[cfg(feature = "tss")] + /// Retrieves a personalization manifest from Apple's TSS server + /// + /// # Arguments + /// * `build_manifest` - Build manifest dictionary + /// * `unique_chip_id` - Device's unique chip ID + /// + /// # Returns + /// The manifest data + /// + /// # Errors + /// Returns `IdeviceError` if manifest retrieval fails pub async fn get_manifest_from_tss( &mut self, build_manifest: &plist::Dictionary, @@ -589,3 +776,4 @@ impl ImageMounter { } } } + diff --git a/idevice/src/pairing_file.rs b/idevice/src/pairing_file.rs index 0f419a4..53903a4 100644 --- a/idevice/src/pairing_file.rs +++ b/idevice/src/pairing_file.rs @@ -1,4 +1,7 @@ -// Jackson Coxson +//! iOS Device Pairing File Handling +//! +//! Provides functionality for reading, writing, and manipulating iOS device pairing files +//! which contain the cryptographic materials needed for secure communication with devices. use std::path::Path; @@ -7,20 +10,35 @@ use plist::Data; use rustls::pki_types::{pem::PemObject, CertificateDer}; use serde::{Deserialize, Serialize}; +/// Represents a complete iOS device pairing record +/// +/// Contains all cryptographic materials and identifiers needed for secure communication +/// with an iOS device, including certificates, private keys, and device identifiers. #[derive(Clone, Debug)] pub struct PairingFile { + /// Device's certificate in DER format pub device_certificate: CertificateDer<'static>, - pub host_private_key: Vec, // the private key doesn't implement clone... + /// Host's private key in DER format + pub host_private_key: Vec, + /// Host's certificate in DER format pub host_certificate: CertificateDer<'static>, + /// Root CA's private key in DER format pub root_private_key: Vec, + /// Root CA's certificate in DER format pub root_certificate: CertificateDer<'static>, + /// System Build Unique Identifier pub system_buid: String, + /// Host identifier pub host_id: String, + /// Escrow bag allowing for access while locked pub escrow_bag: Vec, + /// Device's WiFi MAC address pub wifi_mac_address: String, + /// Device's Unique Device Identifier (optional) pub udid: Option, } +/// Internal representation of a pairing file for serialization/deserialization #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "PascalCase")] struct RawPairingFile { @@ -41,11 +59,37 @@ struct RawPairingFile { } impl PairingFile { + /// Reads a pairing file from disk + /// + /// # Arguments + /// * `path` - Path to the pairing file (typically a .plist file) + /// + /// # Returns + /// A parsed `PairingFile` on success + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - The file cannot be read + /// - The contents are malformed + /// - Cryptographic materials are invalid pub fn read_from_file(path: impl AsRef) -> Result { let f = std::fs::read(path)?; Self::from_bytes(&f) } + /// Parses a pairing file from raw bytes + /// + /// # Arguments + /// * `bytes` - Raw bytes of the pairing file (typically PLIST format) + /// + /// # Returns + /// A parsed `PairingFile` on success + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - The data cannot be parsed as PLIST + /// - Required fields are missing + /// - Cryptographic materials are invalid pub fn from_bytes(bytes: &[u8]) -> Result { let r = match ::plist::from_bytes::(bytes) { Ok(r) => r, @@ -64,12 +108,31 @@ impl PairingFile { } } + /// Creates a pairing file from a plist value + /// + /// # Arguments + /// * `v` - PLIST value containing pairing data + /// + /// # Returns + /// A parsed `PairingFile` on success + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Required fields are missing + /// - Cryptographic materials are invalid pub fn from_value(v: &plist::Value) -> Result { let raw: RawPairingFile = plist::from_value(v)?; let p = raw.try_into()?; Ok(p) } + /// Serializes the pairing file to a PLIST-formatted byte vector + /// + /// # Returns + /// A byte vector containing the serialized pairing file + /// + /// # Errors + /// Returns `IdeviceError` if serialization fails pub fn serialize(self) -> Result, crate::IdeviceError> { let raw = RawPairingFile::from(self); @@ -82,6 +145,9 @@ impl PairingFile { impl TryFrom for PairingFile { type Error = rustls::pki_types::pem::Error; + /// Attempts to convert a raw pairing file into a structured pairing file + /// + /// Performs validation of cryptographic materials during conversion. fn try_from(value: RawPairingFile) -> Result { Ok(Self { device_certificate: CertificateDer::from_pem_slice(&Into::>::into( @@ -105,6 +171,7 @@ impl TryFrom for PairingFile { } impl From for RawPairingFile { + /// Converts a structured pairing file into a raw pairing file for serialization fn from(value: PairingFile) -> Self { Self { device_certificate: Data::new(value.device_certificate.to_vec()), @@ -122,7 +189,7 @@ impl From for RawPairingFile { } #[test] -fn f1() { +fn test_pairing_file_roundtrip() { let f = std::fs::read("/var/lib/lockdown/test.plist").unwrap(); println!("{}", String::from_utf8_lossy(&f)); @@ -133,3 +200,4 @@ fn f1() { assert_eq!(f[..output.len()], output); } + diff --git a/idevice/src/provider.rs b/idevice/src/provider.rs index 4670b08..abd4760 100644 --- a/idevice/src/provider.rs +++ b/idevice/src/provider.rs @@ -1,4 +1,7 @@ -// Jackson Coxson +//! iOS Device Connection Providers +//! +//! Provides abstractions for establishing connections to iOS devices through different +//! transport mechanisms (TCP, USB, etc.). use std::{ future::Future, @@ -14,31 +17,56 @@ use crate::{pairing_file::PairingFile, Idevice, IdeviceError}; #[cfg(feature = "usbmuxd")] use crate::usbmuxd::UsbmuxdAddr; -/// A provider for connecting to the iOS device -/// This is an ugly trait until async traits are stabilized +/// Trait for providers that can establish connections to iOS devices +/// +/// This is an async trait that abstracts over different connection methods +/// (TCP, USB, etc.). pub trait IdeviceProvider: Unpin + Send + Sync + std::fmt::Debug { + /// Establishes a connection to the specified port on the device + /// + /// # Arguments + /// * `port` - The port number to connect to + /// + /// # Returns + /// A future that resolves to an `Idevice` connection handle fn connect( &self, port: u16, ) -> Pin> + Send>>; + /// Returns a label identifying this provider/connection fn label(&self) -> &str; + /// Retrieves the pairing file needed for secure communication + /// + /// # Returns + /// A future that resolves to the device's `PairingFile` fn get_pairing_file( &self, ) -> Pin> + Send>>; } +/// TCP-based device connection provider #[cfg(feature = "tcp")] #[derive(Debug)] pub struct TcpProvider { + /// IP address of the device pub addr: IpAddr, + /// Pairing file for secure communication pub pairing_file: PairingFile, + /// Label identifying this connection pub label: String, } #[cfg(feature = "tcp")] impl IdeviceProvider for TcpProvider { + /// Connects to the device over TCP + /// + /// # Arguments + /// * `port` - The TCP port to connect to + /// + /// # Returns + /// An `Idevice` wrapped in a future fn connect( &self, port: u16, @@ -52,10 +80,12 @@ impl IdeviceProvider for TcpProvider { }) } + /// Returns the connection label fn label(&self) -> &str { &self.label } + /// Returns the pairing file (cloned from the provider) fn get_pairing_file( &self, ) -> Pin> + Send>> { @@ -64,18 +94,31 @@ impl IdeviceProvider for TcpProvider { } } +/// USB-based device connection provider using usbmuxd #[cfg(feature = "usbmuxd")] #[derive(Debug)] pub struct UsbmuxdProvider { + /// USB connection address pub addr: UsbmuxdAddr, + /// Connection tag/identifier pub tag: u32, + /// Device UDID pub udid: String, + /// Device ID pub device_id: u32, + /// Connection label pub label: String, } #[cfg(feature = "usbmuxd")] impl IdeviceProvider for UsbmuxdProvider { + /// Connects to the device over USB via usbmuxd + /// + /// # Arguments + /// * `port` - The port number to connect to on the device + /// + /// # Returns + /// An `Idevice` wrapped in a future fn connect( &self, port: u16, @@ -91,10 +134,12 @@ impl IdeviceProvider for UsbmuxdProvider { }) } + /// Returns the connection label fn label(&self) -> &str { &self.label } + /// Retrieves the pairing record from usbmuxd fn get_pairing_file( &self, ) -> Pin> + Send>> { @@ -108,3 +153,4 @@ impl IdeviceProvider for UsbmuxdProvider { }) } } + diff --git a/idevice/src/springboardservices.rs b/idevice/src/springboardservices.rs index 1f012f7..cb8dd92 100644 --- a/idevice/src/springboardservices.rs +++ b/idevice/src/springboardservices.rs @@ -1,18 +1,46 @@ -use crate::{lockdown::LockdowndClient, Idevice, IdeviceError, IdeviceService}; +//! SpringBoard Services Client +//! +//! Provides functionality for interacting with the SpringBoard services on iOS devices, +//! which manages home screen and app icon related operations. +use crate::{lockdown::LockdownClient, Idevice, IdeviceError, IdeviceService}; + +/// Client for interacting with the iOS SpringBoard services +/// +/// This service provides access to home screen and app icon functionality, +/// such as retrieving app icons. pub struct SpringBoardServicesClient { + /// The underlying device connection with established SpringBoard services pub idevice: Idevice, } impl IdeviceService for SpringBoardServicesClient { + /// Returns the SpringBoard services name as registered with lockdownd fn service_name() -> &'static str { "com.apple.springboardservices" } + /// Establishes a connection to the SpringBoard services + /// + /// # Arguments + /// * `provider` - Device connection provider + /// + /// # Returns + /// A connected `SpringBoardServicesClient` instance + /// + /// # Errors + /// Returns `IdeviceError` if any step of the connection process fails + /// + /// # Process + /// 1. Connects to lockdownd service + /// 2. Starts a lockdown session + /// 3. Requests the SpringBoard services port + /// 4. Establishes connection to the service port + /// 5. Optionally starts TLS if required by service async fn connect( provider: &dyn crate::provider::IdeviceProvider, ) -> Result { - let mut lockdown = LockdowndClient::connect(provider).await?; + let mut lockdown = LockdownClient::connect(provider).await?; lockdown .start_session(&provider.get_pairing_file().await?) .await?; @@ -31,13 +59,33 @@ impl IdeviceService for SpringBoardServicesClient { } impl SpringBoardServicesClient { + /// Creates a new SpringBoard services client from an existing device connection + /// + /// # Arguments + /// * `idevice` - Pre-established device connection pub fn new(idevice: Idevice) -> Self { Self { idevice } } - /// Gets the icon of a spceified app + /// Retrieves the PNG icon data for a specified app + /// /// # Arguments - /// `bundle_identifier` - The identifier of the app to get icon + /// * `bundle_identifier` - The bundle identifier of the app (e.g., "com.apple.Maps") + /// + /// # Returns + /// The raw PNG data of the app icon + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Communication fails + /// - The app doesn't exist + /// - The response is malformed + /// + /// # Example + /// ```rust + /// let icon_data = client.get_icon_pngdata("com.apple.Maps".to_string()).await?; + /// std::fs::write("maps_icon.png", icon_data)?; + /// ``` pub async fn get_icon_pngdata( &mut self, bundle_identifier: String, @@ -56,3 +104,4 @@ impl SpringBoardServicesClient { } } } + diff --git a/idevice/src/tcp/adapter.rs b/idevice/src/tcp/adapter.rs index 4411914..4b0eb8d 100644 --- a/idevice/src/tcp/adapter.rs +++ b/idevice/src/tcp/adapter.rs @@ -1,5 +1,65 @@ -// Jackson Coxson -// Simpler TCP stack +//! A simplified TCP network stack implementation. +//! +//! This module provides a naive TCP stack implementation designed for simple, +//! reliable network environments. It handles basic TCP operations while making +//! significant simplifying assumptions about the underlying transport. +//! +//! # Features +//! - Basic TCP connection establishment (3-way handshake) +//! - Data transmission with PSH flag +//! - Connection teardown +//! - Optional PCAP packet capture +//! - Implements `AsyncRead` and `AsyncWrite` for Tokio compatibility +//! +//! # Limitations +//! - Only supports one connection at a time +//! - No proper sequence number tracking +//! - No retransmission or congestion control +//! - Requires 100% reliable underlying transport +//! - Minimal error handling +//! +//! # Example +//! ```rust,no_run +//! use std::net::{IpAddr, Ipv4Addr}; +//! use tokio::io::{AsyncReadExt, AsyncWriteExt}; +//! use your_crate::tcp::Adapter; +//! use your_crate::ReadWrite; // Assuming you have a ReadWrite trait +//! +//! #[tokio::main] +//! async fn main() -> Result<(), Box> { +//! // Create a transport connection (this would be your actual transport) +//! let transport = /* your transport implementing ReadWrite */; +//! +//! // Create TCP adapter +//! let host_ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2)); +//! let peer_ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)); +//! let mut adapter = Adapter::new(Box::new(transport), host_ip, peer_ip); +//! +//! // Optional: enable packet capture +//! adapter.pcap("capture.pcap").await?; +//! +//! // Connect to remote server +//! adapter.connect(80).await?; +//! +//! // Send HTTP request +//! adapter.write_all(b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n").await?; +//! adapter.flush().await?; +//! +//! // Read response +//! let mut buf = vec![0; 1024]; +//! let n = adapter.read(&mut buf).await?; +//! println!("Received: {}", String::from_utf8_lossy(&buf[..n])); +//! +//! // Close connection +//! adapter.close().await?; +//! +//! Ok(()) +//! } +//! ``` +//! +//! # Warning +//! This implementation makes significant simplifications and should not be used +//! in production environments or with unreliable network transports. use std::{future::Future, net::IpAddr, path::Path, sync::Arc, task::Poll}; @@ -19,32 +79,57 @@ enum AdapterState { None, } -/// An extremely naive, limited and dangerous stack -/// Only one connection can be live at a time -/// Acks aren't tracked, and are silently ignored -/// Only use when the underlying transport is 100% reliable +/// A simplified TCP network stack implementation. +/// +/// This is an extremely naive, limited, and dangerous TCP stack implementation. +/// Key limitations: +/// - Only one connection can be active at a time +/// - ACKs aren't properly tracked and are silently ignored +/// - Should only be used when the underlying transport is 100% reliable +/// +/// The adapter implements `AsyncRead` and `AsyncWrite` for convenient IO operations. #[derive(Debug)] pub struct Adapter { + /// The underlying transport connection peer: Box, + /// The local IP address host_ip: IpAddr, + /// The remote peer's IP address peer_ip: IpAddr, + /// Current connection state state: AdapterState, // TCP state + /// Current sequence number seq: u32, + /// Current acknowledgement number ack: u32, + /// Local port number host_port: u16, + /// Remote port number peer_port: u16, // Read buffer to cache unused bytes + /// Buffer for storing unread received data read_buffer: Vec, + /// Buffer for storing data to be sent write_buffer: Vec, // Logging + /// Optional PCAP file for packet logging pcap: Option>>, } impl Adapter { + /// Creates a new TCP adapter instance. + /// + /// # Arguments + /// * `peer` - The underlying transport connection implementing `ReadWrite` + /// * `host_ip` - The local IP address to use + /// * `peer_ip` - The remote IP address to connect to + /// + /// # Returns + /// A new unconnected `Adapter` instance pub fn new(peer: Box, host_ip: IpAddr, peer_ip: IpAddr) -> Self { Self { peer, @@ -61,6 +146,18 @@ impl Adapter { } } + /// Initiates a TCP connection to the specified port. + /// + /// # Arguments + /// * `port` - The remote port number to connect to + /// + /// # Returns + /// * `Ok(())` if connection was successful + /// * `Err(std::io::Error)` if connection failed + /// + /// # Errors + /// * Returns `InvalidData` if the SYN-ACK response is invalid + /// * Returns other IO errors if underlying transport fails pub async fn connect(&mut self, port: u16) -> Result<(), std::io::Error> { self.read_buffer = Vec::new(); self.write_buffer = Vec::new(); @@ -110,6 +207,14 @@ impl Adapter { Ok(()) } + /// Enables packet capture to a PCAP file. + /// + /// # Arguments + /// * `path` - The filesystem path to write the PCAP data to + /// + /// # Returns + /// * `Ok(())` if PCAP file was successfully created + /// * `Err(std::io::Error)` if file creation failed pub async fn pcap(&mut self, path: impl AsRef) -> Result<(), std::io::Error> { let mut file = tokio::fs::File::create(path).await?; @@ -134,6 +239,14 @@ impl Adapter { Ok(()) } + /// Closes the TCP connection. + /// + /// # Returns + /// * `Ok(())` if connection was closed cleanly + /// * `Err(std::io::Error)` if closing failed + /// + /// # Errors + /// * Returns IO errors if underlying transport fails during close pub async fn close(&mut self) -> Result<(), std::io::Error> { let tcp_packet = TcpPacket::create( self.host_ip, @@ -191,7 +304,17 @@ impl Adapter { Ok(()) } - /// Sends a packet + /// Sends a TCP packet with PSH flag set (pushing data). + /// + /// # Arguments + /// * `data` - The payload data to send + /// + /// # Returns + /// * `Ok(())` if data was sent successfully + /// * `Err(std::io::Error)` if sending failed + /// + /// # Errors + /// * Returns IO errors if underlying transport fails pub async fn psh(&mut self, data: &[u8]) -> Result<(), std::io::Error> { trace!("pshing {} bytes", data.len()); let tcp_packet = TcpPacket::create( @@ -230,6 +353,15 @@ impl Adapter { Ok(()) } + /// Receives data from the connection. + /// + /// # Returns + /// * `Ok(Vec)` containing received data + /// * `Err(std::io::Error)` if receiving failed + /// + /// # Errors + /// * Returns `ConnectionReset` if connection was reset or closed + /// * Returns other IO errors if underlying transport fails pub async fn recv(&mut self) -> Result, std::io::Error> { loop { let res = self.read_tcp_packet().await?; @@ -324,6 +456,18 @@ impl Adapter { } impl AsyncRead for Adapter { + /// Attempts to read from the connection into the provided buffer. + /// + /// Uses an internal read buffer to cache any extra received data. + /// + /// # Returns + /// * `Poll::Ready(Ok(()))` if data was read successfully + /// * `Poll::Ready(Err(e))` if an error occurred + /// * `Poll::Pending` if operation would block + /// + /// # Errors + /// * Returns `NotConnected` if adapter isn't connected + /// * Propagates any underlying transport errors fn poll_read( mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, @@ -377,6 +521,17 @@ impl AsyncRead for Adapter { } impl AsyncWrite for Adapter { + /// Attempts to write data to the connection. + /// + /// Data is buffered internally until flushed. + /// + /// # Returns + /// * `Poll::Ready(Ok(n))` with number of bytes written + /// * `Poll::Ready(Err(e))` if an error occurred + /// * `Poll::Pending` if operation would block + /// + /// # Errors + /// * Returns `NotConnected` if adapter isn't connected fn poll_write( mut self: std::pin::Pin<&mut Self>, _cx: &mut std::task::Context<'_>, diff --git a/idevice/src/tss.rs b/idevice/src/tss.rs index b3eacb4..1a94182 100644 --- a/idevice/src/tss.rs +++ b/idevice/src/tss.rs @@ -1,20 +1,34 @@ -// Jackson Coxson -// Thanks pymobiledevice3 +//! Ticket Signature Server (TSS) Client +//! +//! Provides functionality for interacting with Apple's TSS service to: +//! - Request personalized firmware components +//! - Apply restore request rules for device-specific parameters +//! - Handle cryptographic signing operations use log::{debug, warn}; use plist::Value; use crate::{util::plist_to_xml_bytes, IdeviceError}; +/// TSS client version string sent in requests const TSS_CLIENT_VERSION_STRING: &str = "libauthinstall-1033.0.2"; +/// Apple's TSS endpoint URL const TSS_CONTROLLER_ACTION_URL: &str = "http://gs.apple.com/TSS/controller?action=2"; +/// Represents a TSS request to Apple's signing server #[derive(Debug)] pub struct TSSRequest { + /// The underlying plist dictionary containing request parameters inner: plist::Dictionary, } impl TSSRequest { + /// Creates a new TSS request with default headers + /// + /// Initializes with: + /// - Host platform info + /// - Client version string + /// - Random UUID for request identification pub fn new() -> Self { let mut inner = plist::Dictionary::new(); inner.insert("@HostPlatformInfo".into(), "mac".into()); @@ -26,12 +40,35 @@ impl TSSRequest { Self { inner } } + /// Inserts a key-value pair into the TSS request + /// + /// # Arguments + /// * `key` - The parameter name + /// * `val` - The parameter value (will be converted to plist::Value) pub fn insert(&mut self, key: impl Into, val: impl Into) { let key = key.into(); let val = val.into(); self.inner.insert(key, val); } + /// Sends the TSS request to Apple's servers + /// + /// # Returns + /// The parsed plist response from Apple + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - The request fails + /// - The response is malformed + /// - Apple returns a non-success status + /// + /// # Example + /// ```rust + /// let mut request = TSSRequest::new(); + /// request.insert("ApBoardID", board_id); + /// request.insert("ApChipID", chip_id); + /// let response = request.send().await?; + /// ``` pub async fn send(&self) -> Result { debug!( "Sending TSS request: {}", @@ -51,7 +88,7 @@ impl TSSRequest { .text() .await?; - debug!("Apple responeded with {res}"); + debug!("Apple responded with {res}"); let res = res.trim_start_matches("STATUS=0&"); let res = res.trim_start_matches("MESSAGE="); if !res.starts_with("SUCCESS") { @@ -68,11 +105,24 @@ impl TSSRequest { } impl Default for TSSRequest { + /// Creates a default TSS request (same as `new()`) fn default() -> Self { Self::new() } } +/// Applies restore request rules to modify input parameters +/// +/// # Arguments +/// * `input` - The dictionary to modify based on rules +/// * `parameters` - Device parameters to check conditions against +/// * `rules` - List of rules to apply +/// +/// # Process +/// For each rule: +/// 1. Checks all conditions against the parameters +/// 2. If all conditions are met, applies the rule's actions +/// 3. Actions can add, modify or remove parameters pub fn apply_restore_request_rules( input: &mut plist::Dictionary, parameters: &plist::Dictionary, @@ -122,6 +172,7 @@ pub fn apply_restore_request_rules( }; for (key, value) in actions { + // Skip special values (255 typically means "ignore") if let Some(i) = value.as_unsigned_integer() { if i == 255 { continue; @@ -133,7 +184,7 @@ pub fn apply_restore_request_rules( } } - input.remove(key); // Explicitly remove before inserting, like Python + input.remove(key); // Explicitly remove before inserting input.insert(key.to_owned(), value.to_owned()); } } else { @@ -141,3 +192,4 @@ pub fn apply_restore_request_rules( } } } + diff --git a/idevice/src/tunneld.rs b/idevice/src/tunneld.rs index 9c94a8a..2c4acbd 100644 --- a/idevice/src/tunneld.rs +++ b/idevice/src/tunneld.rs @@ -1,4 +1,7 @@ -// Shim code for using pymobiledevice3's tunneld +//! Tunneld Client Implementation +//! +//! Provides functionality for interacting with pymobiledevice3's tunneld service, +//! which creates network tunnels to iOS devices over USB. use std::{collections::HashMap, net::SocketAddr}; @@ -8,25 +11,55 @@ use serde_json::Value; use crate::IdeviceError; +/// Default port number for the tunneld service pub const DEFAULT_PORT: u16 = 49151; +/// Represents a device connected through tunneld #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TunneldDevice { + /// Network interface name pub interface: String, + /// Tunnel IP address #[serde(rename = "tunnel-address")] pub tunnel_address: String, + /// Tunnel port number #[serde(rename = "tunnel-port")] pub tunnel_port: u16, } +/// Retrieves all devices currently connected through tunneld +/// +/// # Arguments +/// * `socket` - Socket address of the tunneld service (typically localhost with DEFAULT_PORT) +/// +/// # Returns +/// A HashMap mapping device UDIDs to their tunnel information +/// +/// # Errors +/// Returns `IdeviceError` if: +/// - The HTTP request fails +/// - The response format is invalid +/// - JSON parsing fails +/// +/// # Example +/// ```rust +/// let host = SocketAddr::new(IpAddr::from_str("127.0.0.1").unwrap(), DEFAULT_PORT); +/// let devices = get_tunneld_devices(host).await?; +/// for (udid, device) in devices { +/// println!("Device {} is available at {}:{}", +/// udid, device.tunnel_address, device.tunnel_port); +/// } +/// ``` pub async fn get_tunneld_devices( socket: SocketAddr, ) -> Result, IdeviceError> { + // Make HTTP GET request to tunneld endpoint let res: Value = reqwest::get(format!("http://{socket}")) .await? .json() .await?; + // Verify response is a JSON object let res = match res.as_object() { Some(r) => r, None => { @@ -35,6 +68,7 @@ pub async fn get_tunneld_devices( } }; + // Parse each device entry let mut to_return = HashMap::new(); for (udid, v) in res.into_iter() { let mut v: Vec = match serde_json::from_value(v.clone()) { @@ -62,9 +96,14 @@ mod tests { use super::*; + /// Test case for verifying tunneld device listing #[tokio::test] - async fn t1() { + async fn test_get_tunneld_devices() { let host = SocketAddr::new(IpAddr::from_str("127.0.0.1").unwrap(), DEFAULT_PORT); - println!("{:#?}", get_tunneld_devices(host).await); + match get_tunneld_devices(host).await { + Ok(devices) => println!("Found tunneld devices: {:#?}", devices), + Err(e) => println!("Error querying tunneld: {}", e), + } } } + diff --git a/idevice/src/usbmuxd/mod.rs b/idevice/src/usbmuxd/mod.rs index 17e7a53..e90a42c 100644 --- a/idevice/src/usbmuxd/mod.rs +++ b/idevice/src/usbmuxd/mod.rs @@ -1,4 +1,7 @@ -// Jackson Coxson +//! USB Multiplexing Daemon (usbmuxd) Client +//! +//! Provides functionality for interacting with the usbmuxd service which manages +//! connections to iOS devices over USB and network and pairing files use std::{ net::{AddrParseError, IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, @@ -18,36 +21,57 @@ use crate::{ mod des; mod raw_packet; +/// Represents the connection type of a device #[derive(Debug, Clone)] pub enum Connection { + /// Connected via USB Usb, + /// Connected via network with specific IP address Network(IpAddr), + /// Unknown connection type with description Unknown(String), } +/// Represents a device connected through usbmuxd #[derive(Debug, Clone)] pub struct UsbmuxdDevice { + /// How the device is connected pub connection_type: Connection, + /// Unique Device Identifier pub udid: String, + /// usbmuxd-assigned device ID pub device_id: u32, } +/// Active connection to the usbmuxd service pub struct UsbmuxdConnection { socket: Box, tag: u32, } +/// Address of the usbmuxd service #[derive(Clone, Debug)] pub enum UsbmuxdAddr { + /// Unix domain socket path (Unix systems only) #[cfg(unix)] UnixSocket(String), + /// TCP socket address TcpSocket(SocketAddr), } impl UsbmuxdAddr { + /// Default TCP port for usbmuxd pub const DEFAULT_PORT: u16 = 27015; + /// Default Unix socket path for usbmuxd pub const SOCKET_FILE: &'static str = "/var/run/usbmuxd"; + /// Connects to the usbmuxd service + /// + /// # Returns + /// A boxed transport stream + /// + /// # Errors + /// Returns `IdeviceError` if connection fails pub async fn to_socket(&self) -> Result, IdeviceError> { Ok(match self { #[cfg(unix)] @@ -56,11 +80,24 @@ impl UsbmuxdAddr { }) } + /// Creates a new usbmuxd connection + /// + /// # Arguments + /// * `tag` - Connection tag/identifier + /// + /// # Returns + /// A connected `UsbmuxdConnection` pub async fn connect(&self, tag: u32) -> Result { let socket = self.to_socket().await?; Ok(UsbmuxdConnection::new(socket, tag)) } + /// Creates a UsbmuxdAddr from environment variable + /// + /// Checks `USBMUXD_SOCKET_ADDRESS` environment variable, falls back to default + /// + /// # Returns + /// Configured UsbmuxdAddr or parse error pub fn from_env_var() -> Result { Ok(match std::env::var("USBMUXD_SOCKET_ADDRESS") { Ok(var) => { @@ -79,6 +116,9 @@ impl UsbmuxdAddr { } impl Default for UsbmuxdAddr { + /// Creates default usbmuxd address based on platform: + /// - Unix: Uses default socket path + /// - Non-Unix: Uses localhost TCP port fn default() -> Self { #[cfg(not(unix))] { @@ -93,12 +133,22 @@ impl Default for UsbmuxdAddr { } impl UsbmuxdConnection { + /// Binary PLIST protocol version pub const BINARY_PLIST_VERSION: u32 = 0; + /// XML PLIST protocol version pub const XML_PLIST_VERSION: u32 = 1; + /// Result message type pub const RESULT_MESSAGE_TYPE: u32 = 1; + /// PLIST message type pub const PLIST_MESSAGE_TYPE: u32 = 8; + /// Creates a default usbmuxd connection + /// + /// Uses default address based on platform + /// + /// # Returns + /// Connected `UsbmuxdConnection` or error pub async fn default() -> Result { let socket = UsbmuxdAddr::default().to_socket().await?; @@ -108,10 +158,25 @@ impl UsbmuxdConnection { }) } + /// Creates a new usbmuxd connection + /// + /// # Arguments + /// * `socket` - The transport stream + /// * `tag` - Connection tag/identifier pub fn new(socket: Box, tag: u32) -> Self { Self { socket, tag } } + /// Lists all connected devices + /// + /// # Returns + /// Vector of connected devices + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Communication fails + /// - Response is malformed + /// - Device info is incomplete pub async fn get_devices(&mut self) -> Result, IdeviceError> { let mut req = plist::Dictionary::new(); req.insert("MessageType".into(), "ListDevices".into()); @@ -135,13 +200,13 @@ impl UsbmuxdConnection { match addr[0] { 0x02 => { - // ipv4 + // IPv4 Connection::Network(IpAddr::V4(Ipv4Addr::new( addr[4], addr[5], addr[6], addr[7], ))) } 0x1E => { - // ipv6 + // IPv6 if addr.len() < 24 { warn!("IPv6 address is less than 24 bytes"); return Err(IdeviceError::UnexpectedResponse); @@ -182,6 +247,13 @@ impl UsbmuxdConnection { Ok(devs) } + /// Gets a specific device by UDID + /// + /// # Arguments + /// * `udid` - The device UDID to find + /// + /// # Returns + /// The matching device or error if not found pub async fn get_device(&mut self, udid: &str) -> Result { let devices = self.get_devices().await?; match devices.into_iter().find(|x| x.udid == udid) { @@ -190,6 +262,13 @@ impl UsbmuxdConnection { } } + /// Gets the pairing record for a device + /// + /// # Arguments + /// * `udid` - The device UDID + /// + /// # Returns + /// The pairing file or error pub async fn get_pair_record(&mut self, udid: &str) -> Result { debug!("Getting pair record for {udid}"); let mut req = plist::Dictionary::new(); @@ -204,6 +283,10 @@ impl UsbmuxdConnection { } } + /// Gets the BUID + /// + /// # Returns + /// The BUID string or error pub async fn get_buid(&mut self) -> Result { let mut req = plist::Dictionary::new(); req.insert("MessageType".into(), "ReadBUID".into()); @@ -216,6 +299,15 @@ impl UsbmuxdConnection { } } + /// Connects to a service on the device + /// + /// # Arguments + /// * `device_id` - usbmuxd device ID + /// * `port` - TCP port to connect to (host byte order) + /// * `label` - Connection label + /// + /// # Returns + /// An `Idevice` connection or error pub async fn connect_to_device( mut self, device_id: u32, @@ -243,6 +335,7 @@ impl UsbmuxdConnection { } } + /// Writes a PLIST message to usbmuxd async fn write_plist(&mut self, req: plist::Dictionary) -> Result<(), IdeviceError> { let raw = raw_packet::RawPacket::new( req, @@ -257,6 +350,7 @@ impl UsbmuxdConnection { Ok(()) } + /// Reads a PLIST message from usbmuxd async fn read_plist(&mut self) -> Result { let mut header_buffer = [0; 16]; self.socket.read_exact(&mut header_buffer).await?; @@ -276,6 +370,15 @@ impl UsbmuxdConnection { } impl UsbmuxdDevice { + /// Creates a provider for this device + /// + /// # Arguments + /// * `addr` - usbmuxd address + /// * `tag` - Connection tag + /// * `label` - Connection label + /// + /// # Returns + /// Configured `UsbmuxdProvider` pub fn to_provider( &self, addr: UsbmuxdAddr, @@ -293,3 +396,4 @@ impl UsbmuxdDevice { } } } + diff --git a/idevice/src/util.rs b/idevice/src/util.rs index 45e6253..498097b 100644 --- a/idevice/src/util.rs +++ b/idevice/src/util.rs @@ -1,7 +1,27 @@ -// Jackson Coxson +//! Utility Functions +//! +//! Provides helper functions for working with Apple's Property List (PLIST) format, +//! including serialization and pretty-printing utilities. use plist::Value; +/// Converts a PLIST dictionary to XML-formatted bytes +/// +/// # Arguments +/// * `p` - The PLIST dictionary to serialize +/// +/// # Returns +/// A byte vector containing the XML representation +/// +/// # Panics +/// Will panic if serialization fails (should only happen with invalid data) +/// +/// # Example +/// ```rust +/// let mut dict = plist::Dictionary::new(); +/// dict.insert("key".into(), "value".into()); +/// let xml_bytes = plist_to_xml_bytes(&dict); +/// ``` pub fn plist_to_xml_bytes(p: &plist::Dictionary) -> Vec { let buf = Vec::new(); let mut writer = std::io::BufWriter::new(buf); @@ -10,10 +30,32 @@ pub fn plist_to_xml_bytes(p: &plist::Dictionary) -> Vec { writer.into_inner().unwrap() } +/// Pretty-prints a PLIST value with indentation +/// +/// # Arguments +/// * `p` - The PLIST value to format +/// +/// # Returns +/// A formatted string representation pub fn pretty_print_plist(p: &Value) -> String { print_plist(p, 0) } +/// Pretty-prints a PLIST dictionary with key-value pairs +/// +/// # Arguments +/// * `dict` - The dictionary to format +/// +/// # Returns +/// A formatted string representation with newlines and indentation +/// +/// # Example +/// ```rust +/// let mut dict = plist::Dictionary::new(); +/// dict.insert("name".into(), "John".into()); +/// dict.insert("age".into(), 30.into()); +/// println!("{}", pretty_print_dictionary(&dict)); +/// ``` pub fn pretty_print_dictionary(dict: &plist::Dictionary) -> String { let items: Vec = dict .iter() @@ -22,6 +64,14 @@ pub fn pretty_print_dictionary(dict: &plist::Dictionary) -> String { format!("{{\n{}\n}}", items.join(",\n")) } +/// Internal recursive function for printing PLIST values with indentation +/// +/// # Arguments +/// * `p` - The PLIST value to format +/// * `indentation` - Current indentation level +/// +/// # Returns +/// Formatted string representation fn print_plist(p: &Value, indentation: usize) -> String { let indent = " ".repeat(indentation); match p { @@ -75,3 +125,4 @@ fn print_plist(p: &Value, indentation: usize) -> String { _ => "Unknown".to_string(), } } + diff --git a/idevice/src/xpc/cdtunnel.rs b/idevice/src/xpc/cdtunnel.rs deleted file mode 100644 index bd21501..0000000 --- a/idevice/src/xpc/cdtunnel.rs +++ /dev/null @@ -1,28 +0,0 @@ -// DebianArch - -use json::JsonValue; -pub struct CDTunnel {} - -impl CDTunnel { - const MAGIC: &'static [u8; 8] = b"CDTunnel"; - pub fn decode(data: &[u8]) -> Result> { - let magic_len = CDTunnel::MAGIC.len(); - if &data[0..magic_len] != CDTunnel::MAGIC { - Err("Invalid Magic")?; - } - - let size = u16::from_be_bytes(data[magic_len..magic_len + 2].try_into()?) as usize; - let content = &data[magic_len + 2..magic_len + 2 + size]; - Ok(json::parse(&String::from_utf8(content.to_vec())?)?) - } - - pub fn encode(value: JsonValue) -> Result, Box> { - let mut buf = Vec::new(); - let json_str = value.dump(); - - buf.extend_from_slice(CDTunnel::MAGIC); - buf.extend_from_slice(&u16::to_be_bytes(json_str.len().try_into()?)); - buf.extend_from_slice(json_str.as_bytes()); - Ok(buf) - } -} diff --git a/idevice/src/xpc/mod.rs b/idevice/src/xpc/mod.rs index cb04424..ca3c1cc 100644 --- a/idevice/src/xpc/mod.rs +++ b/idevice/src/xpc/mod.rs @@ -1,4 +1,7 @@ -// Thanks DebianArch +//! XPC (Cross-Process Communication) Implementation +//! +//! Provides functionality for interacting with Apple's XPC protocol over HTTP/2, +//! which is used for inter-process communication between iOS/macOS components. use std::collections::HashMap; @@ -14,24 +17,33 @@ use format::{XPCFlag, XPCMessage, XPCObject}; use log::{debug, warn}; use serde::Deserialize; -pub mod cdtunnel; pub mod error; -pub mod format; +mod format; +/// Represents an XPC connection to a device with available services pub struct XPCDevice { + /// The underlying XPC connection pub connection: XPCConnection, + /// Map of available XPC services by name pub services: HashMap, } +/// Describes an available XPC service #[derive(Debug, Clone, Deserialize)] pub struct XPCService { + /// Required entitlement to access this service pub entitlement: String, + /// Port number where the service is available pub port: u16, + /// Whether the service uses remote XPC pub uses_remote_xpc: bool, + /// Optional list of supported features pub features: Option>, + /// Optional service version number pub service_version: Option, } +/// Manages an active XPC connection over HTTP/2 pub struct XPCConnection { pub(crate) inner: http2::Connection, root_message_id: u64, @@ -39,9 +51,22 @@ pub struct XPCConnection { } impl XPCDevice { + /// Creates a new XPC device connection + /// + /// # Arguments + /// * `stream` - The underlying transport stream + /// + /// # Returns + /// A connected XPCDevice instance with discovered services + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - The connection fails + /// - The service discovery response is malformed pub async fn new(stream: R) -> Result { let mut connection = XPCConnection::new(stream).await?; + // Read initial services message let data = connection.read_message(http2::ROOT_CHANNEL).await?; let data = match data.message { @@ -56,6 +81,7 @@ impl XPCDevice { None => return Err(IdeviceError::UnexpectedResponse), }; + // Parse available services let mut services = HashMap::new(); for (name, service) in data.into_iter() { match service.as_dictionary() { @@ -131,18 +157,34 @@ impl XPCDevice { }) } + /// Consumes the device and returns the underlying transport stream pub fn into_inner(self) -> R { self.connection.inner.stream } } impl XPCConnection { + /// Channel ID for root messages pub const ROOT_CHANNEL: u32 = http2::ROOT_CHANNEL; + /// Channel ID for reply messages pub const REPLY_CHANNEL: u32 = http2::REPLY_CHANNEL; + /// Initial stream ID for HTTP/2 connection const INIT_STREAM: u32 = http2::INIT_STREAM; + /// Establishes a new XPC connection + /// + /// # Arguments + /// * `stream` - The underlying transport stream + /// + /// # Returns + /// A connected XPCConnection instance + /// + /// # Errors + /// Returns `XPCError` if the connection handshake fails pub async fn new(stream: R) -> Result { let mut client = http2::Connection::new(stream).await?; + + // Configure HTTP/2 settings client .send_frame(SettingsFrame::new( [ @@ -154,14 +196,19 @@ impl XPCConnection { Default::default(), )) .await?; + + // Update window size client .send_frame(WindowUpdateFrame::new(Self::INIT_STREAM, 983041)) .await?; + let mut xpc_client = Self { inner: client, root_message_id: 1, reply_message_id: 1, }; + + // Perform XPC handshake xpc_client .send_recv_message( Self::ROOT_CHANNEL, @@ -173,7 +220,6 @@ impl XPCConnection { ) .await?; - // we are here. we send data to stream_id 3 yet we get data from stream 1 ??? xpc_client .send_recv_message( Self::REPLY_CHANNEL, @@ -195,6 +241,14 @@ impl XPCConnection { Ok(xpc_client) } + /// Sends a message and waits for the response + /// + /// # Arguments + /// * `stream_id` - The channel/stream to use + /// * `message` - The XPC message to send + /// + /// # Returns + /// The response message pub async fn send_recv_message( &mut self, stream_id: u32, @@ -204,6 +258,7 @@ impl XPCConnection { self.read_message(stream_id).await } + /// Sends an XPC message without waiting for a response pub async fn send_message( &mut self, stream_id: u32, @@ -215,6 +270,7 @@ impl XPCConnection { Ok(()) } + /// Reads an XPC message from the specified stream pub async fn read_message(&mut self, stream_id: u32) -> Result { let mut buf = self.inner.read_streamid(stream_id).await?; loop { @@ -236,3 +292,4 @@ impl XPCConnection { } } } + diff --git a/tools/src/ideviceinfo.rs b/tools/src/ideviceinfo.rs index 583dd72..e47ad1a 100644 --- a/tools/src/ideviceinfo.rs +++ b/tools/src/ideviceinfo.rs @@ -2,7 +2,7 @@ // idevice Rust implementation of libimobiledevice's ideviceinfo use clap::{Arg, Command}; -use idevice::{lockdown::LockdowndClient, IdeviceService}; +use idevice::{lockdown::LockdownClient, IdeviceService}; mod common; @@ -57,7 +57,7 @@ async fn main() { } }; - let mut lockdown_client = match LockdowndClient::connect(&*provider).await { + let mut lockdown_client = match LockdownClient::connect(&*provider).await { Ok(l) => l, Err(e) => { eprintln!("Unable to connect to lockdown: {e:?}"); diff --git a/tools/src/mounter.rs b/tools/src/mounter.rs index 58955d2..56a28a9 100644 --- a/tools/src/mounter.rs +++ b/tools/src/mounter.rs @@ -5,7 +5,7 @@ use std::{io::Write, path::PathBuf}; use clap::{arg, value_parser, Arg, Command}; use idevice::{ - lockdown::LockdowndClient, mobile_image_mounter::ImageMounter, pretty_print_plist, + lockdown::LockdownClient, mobile_image_mounter::ImageMounter, pretty_print_plist, IdeviceService, }; @@ -85,7 +85,7 @@ async fn main() { } }; - let mut lockdown_client = LockdowndClient::connect(&*provider) + let mut lockdown_client = LockdownClient::connect(&*provider) .await .expect("Unable to connect to lockdown");