Document modules and public methods

This commit is contained in:
Jackson Coxson
2025-04-06 10:03:09 -06:00
parent 3cb4f468e6
commit 52b2db404f
26 changed files with 1949 additions and 173 deletions

View File

@@ -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<LockdowndClient, IdeviceError> = RUNTIME.block_on(async move {
let res: Result<LockdownClient, IdeviceError> = 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<LockdowndClient, IdeviceError> = RUNTIME.block_on(async move {
let res: Result<LockdownClient, IdeviceError> = 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

View File

@@ -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<Self, IdeviceError> {
let mut lockdown = LockdowndClient::connect(provider).await?;
let mut lockdown = LockdownClient::connect(provider).await?;
lockdown
.start_session(&provider.get_pairing_file().await?)
.await?;

View File

@@ -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<u8>,
}
@@ -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<Self, IdeviceError> {
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<u8>)` containing the serialized packet.
/// * `Err(io::Error)` if writing to the output buffer fails.
pub fn serialize(&self) -> io::Result<Vec<u8>> {
let mut output = Vec::new();
// Write the magic bytes
output.write_all(Self::MAGIC)?;
// Write the body length
output.write_u16::<BigEndian>(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<Self, IdeviceError> {
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<Self, IdeviceError> {
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<u8>)` containing the received data.
/// * `Err(IdeviceError)` if reading fails.
pub async fn recv(&mut self) -> Result<Vec<u8>, 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<crate::tcp::adapter::Adapter, IdeviceError> {
let our_ip = self

View File

@@ -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<R: ReadWrite> {
/// 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:
/// $<command>[<hex-encoded args>]#<checksum>
pub struct DebugserverCommand {
/// The command name (e.g. "qSupported", "vCont")
pub name: String,
/// Command arguments that will be hex-encoded
pub argv: Vec<String>,
}
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<String>) -> Self {
Self { name, argv }
}
}
impl<R: ReadWrite> DebugProxyClient<R> {
/// 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<R: ReadWrite> DebugProxyClient<R> {
}
}
/// 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:
/// $<command>[<hex-encoded args>]#<checksum>
///
/// # 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<R: ReadWrite> DebugProxyClient<R> {
Ok(response)
}
/// Reads a response packet from debugserver
///
/// Handles the GDB Remote Serial Protocol response format:
/// $<data>#<checksum>
///
/// # 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<Option<String>, IdeviceError> {
let mut buffer = Vec::new();
let mut received_char = [0u8; 1];
@@ -103,11 +152,28 @@ impl<R: ReadWrite> DebugProxyClient<R> {
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<String, IdeviceError> {
let mut buf = vec![0; len];
let r = self.socket.read(&mut buf).await?;
@@ -115,6 +181,19 @@ impl<R: ReadWrite> DebugProxyClient<R> {
Ok(String::from_utf8_lossy(&buf[..r]).to_string())
}
/// Sets program arguments using the 'A' command
///
/// Formats arguments according to GDB protocol:
/// A<arglen>,<argnum>,<argdata>
///
/// # 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<String>) -> Result<String, IdeviceError> {
if argv.is_empty() {
return Err(IdeviceError::InvalidArgument);
@@ -157,26 +236,45 @@ impl<R: ReadWrite> DebugProxyClient<R> {
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<String> 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<String> = 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()
}
}

View File

@@ -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<R>) -> Result<Self, IdeviceError> {
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());

View File

@@ -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<AuxValue>,
}
/// Typed auxiliary value that can be included in messages
#[derive(PartialEq)]
pub enum AuxValue {
String(String), // 0x01
Array(Vec<u8>), // 0x02
U32(u32), // 0x03
I64(i64), // 0x06
/// UTF-8 string value (type 0x01)
String(String),
/// Raw byte array (type 0x02)
Array(Vec<u8>),
/// 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<Aux>,
/// Optional payload data (typically NSKeyedArchive)
pub data: Option<Value>,
}
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<u8>) -> Result<Self, IdeviceError> {
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<AuxValue>) -> 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<u8> {
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<plist::Value>) -> 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<u8> {
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<u8> {
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<R: AsyncRead + Unpin>(reader: &mut R) -> Result<Self, IdeviceError> {
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<u8> {
let aux = match &self.aux {
Some(a) => a.serialize(),

View File

@@ -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<R>) -> Result<Self, IdeviceError> {
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<String>,
@@ -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(

View File

@@ -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<R: ReadWrite> {
/// 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<u32, VecDeque<Message>>,
}
/// 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<R>,
/// Channel number this handle operates on
channel: u32,
}
impl<R: ReadWrite> RemoteServerClient<R> {
/// 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<R: ReadWrite> RemoteServerClient<R> {
}
}
/// 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<R> {
Channel {
client: self,
@@ -49,6 +119,18 @@ impl<R: ReadWrite> RemoteServerClient<R> {
}
}
/// 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<String>,
@@ -89,6 +171,20 @@ impl<R: ReadWrite> RemoteServerClient<R> {
})
}
/// 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<R: ReadWrite> RemoteServerClient<R> {
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<Message, IdeviceError> {
// Determine if we already have a message cached
let cache = match self.channels.get_mut(&channel) {
@@ -140,10 +250,32 @@ impl<R: ReadWrite> RemoteServerClient<R> {
}
impl<R: ReadWrite> 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<Message, IdeviceError> {
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<impl Into<plist::Value>>,

View File

@@ -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<Self, IdeviceError> {
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<u64, IdeviceError> {
// 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());

View File

@@ -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<Self, IdeviceError> {
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<String>,
@@ -55,7 +105,7 @@ impl InstallationProxyClient {
.into_iter()
.map(plist::Value::String)
.collect::<Vec<plist::Value>>();
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 {
}
}
}

View File

@@ -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<T: AsyncRead + AsyncWrite + Unpin + Send + Sync + std::fmt::Debug> 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<Output = Result<Self, IdeviceError>> + 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<dyn ReadWrite>;
/// 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<Box<dyn ReadWrite>>, // in a box for now to use the ReadWrite trait for further uses
/// The underlying connection socket, boxed for dynamic dispatch
socket: Option<Box<dyn ReadWrite>>,
/// 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<dyn ReadWrite>, label: impl Into<String>) -> 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<String, IdeviceError> {
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<Fut, S>(
&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<Vec<u8>, 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<Vec<u8>, 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<plist::Dictionary, IdeviceError> {
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<Self> {
match e {
"GetProhibited" => Some(Self::GetProhibited),

View File

@@ -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<Self, IdeviceError> {
@@ -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<String>,
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<String>) -> Result<Value, IdeviceError> {
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<plist::Dictionary, IdeviceError> {
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<String>,
@@ -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<Idevice> for LockdowndClient {
impl From<Idevice> for LockdownClient {
/// Converts an existing device connection into a lockdown client
fn from(value: Idevice) -> Self {
Self::new(value)
}

View File

@@ -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<Self, IdeviceError> {
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<u8>) -> 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<Vec<Vec<u8>>, 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 {
}
}
}

View File

@@ -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<Self, IdeviceError> {
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<Vec<plist::Value>, 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<String>,
@@ -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<String>,
@@ -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<Fut, S>(
&mut self,
image_type: impl Into<String>,
@@ -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<String>,
@@ -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<String>,
@@ -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<String>,
@@ -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<bool, IdeviceError> {
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<String>,
@@ -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<String>,
@@ -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<Fut, S>(
&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 {
}
}
}

View File

@@ -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<u8>, // the private key doesn't implement clone...
/// Host's private key in DER format
pub host_private_key: Vec<u8>,
/// Host's certificate in DER format
pub host_certificate: CertificateDer<'static>,
/// Root CA's private key in DER format
pub root_private_key: Vec<u8>,
/// 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<u8>,
/// Device's WiFi MAC address
pub wifi_mac_address: String,
/// Device's Unique Device Identifier (optional)
pub udid: Option<String>,
}
/// 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<Path>) -> Result<Self, crate::IdeviceError> {
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<Self, crate::IdeviceError> {
let r = match ::plist::from_bytes::<RawPairingFile>(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<Self, crate::IdeviceError> {
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<Vec<u8>, crate::IdeviceError> {
let raw = RawPairingFile::from(self);
@@ -82,6 +145,9 @@ impl PairingFile {
impl TryFrom<RawPairingFile> 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<Self, Self::Error> {
Ok(Self {
device_certificate: CertificateDer::from_pem_slice(&Into::<Vec<u8>>::into(
@@ -105,6 +171,7 @@ impl TryFrom<RawPairingFile> for PairingFile {
}
impl From<PairingFile> 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<PairingFile> 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);
}

View File

@@ -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<Box<dyn Future<Output = Result<Idevice, IdeviceError>> + 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<Box<dyn Future<Output = Result<PairingFile, IdeviceError>> + 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<Box<dyn Future<Output = Result<PairingFile, IdeviceError>> + 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<Box<dyn Future<Output = Result<PairingFile, IdeviceError>> + Send>> {
@@ -108,3 +153,4 @@ impl IdeviceProvider for UsbmuxdProvider {
})
}
}

View File

@@ -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<Self, IdeviceError> {
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 {
}
}
}

View File

@@ -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<dyn std::error::Error>> {
//! // 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<dyn ReadWrite>,
/// 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<u8>,
/// Buffer for storing data to be sent
write_buffer: Vec<u8>,
// Logging
/// Optional PCAP file for packet logging
pcap: Option<Arc<Mutex<tokio::fs::File>>>,
}
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<dyn ReadWrite>, 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<Path>) -> 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<u8>)` 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<Vec<u8>, 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<'_>,

View File

@@ -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<String>, val: impl Into<Value>) {
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<plist::Value, IdeviceError> {
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(
}
}
}

View File

@@ -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<HashMap<String, TunneldDevice>, 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<TunneldDevice> = 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),
}
}
}

View File

@@ -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<dyn ReadWrite>,
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<Box<dyn ReadWrite>, 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<UsbmuxdConnection, IdeviceError> {
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<Self, AddrParseError> {
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<Self, IdeviceError> {
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<dyn ReadWrite>, 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<Vec<UsbmuxdDevice>, 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<UsbmuxdDevice, IdeviceError> {
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<PairingFile, IdeviceError> {
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<String, IdeviceError> {
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<plist::Dictionary, IdeviceError> {
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 {
}
}
}

View File

@@ -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<u8> {
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<u8> {
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<String> = 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(),
}
}

View File

@@ -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<JsonValue, Box<dyn std::error::Error>> {
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<Vec<u8>, Box<dyn std::error::Error>> {
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)
}
}

View File

@@ -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<R: ReadWrite> {
/// The underlying XPC connection
pub connection: XPCConnection<R>,
/// Map of available XPC services by name
pub services: HashMap<String, XPCService>,
}
/// 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<Vec<String>>,
/// Optional service version number
pub service_version: Option<i64>,
}
/// Manages an active XPC connection over HTTP/2
pub struct XPCConnection<R: ReadWrite> {
pub(crate) inner: http2::Connection<R>,
root_message_id: u64,
@@ -39,9 +51,22 @@ pub struct XPCConnection<R: ReadWrite> {
}
impl<R: ReadWrite> XPCDevice<R> {
/// 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<Self, IdeviceError> {
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<R: ReadWrite> XPCDevice<R> {
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<R: ReadWrite> XPCDevice<R> {
})
}
/// Consumes the device and returns the underlying transport stream
pub fn into_inner(self) -> R {
self.connection.inner.stream
}
}
impl<R: ReadWrite> XPCConnection<R> {
/// 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<Self, XPCError> {
let mut client = http2::Connection::new(stream).await?;
// Configure HTTP/2 settings
client
.send_frame(SettingsFrame::new(
[
@@ -154,14 +196,19 @@ impl<R: ReadWrite> XPCConnection<R> {
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<R: ReadWrite> XPCConnection<R> {
)
.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<R: ReadWrite> XPCConnection<R> {
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<R: ReadWrite> XPCConnection<R> {
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<R: ReadWrite> XPCConnection<R> {
Ok(())
}
/// Reads an XPC message from the specified stream
pub async fn read_message(&mut self, stream_id: u32) -> Result<XPCMessage, XPCError> {
let mut buf = self.inner.read_streamid(stream_id).await?;
loop {
@@ -236,3 +292,4 @@ impl<R: ReadWrite> XPCConnection<R> {
}
}
}

View File

@@ -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:?}");

View File

@@ -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");