diff --git a/ffi/src/amfi.rs b/ffi/src/amfi.rs new file mode 100644 index 0000000..56daf7e --- /dev/null +++ b/ffi/src/amfi.rs @@ -0,0 +1,245 @@ +// Jackson Coxson + +use idevice::{IdeviceError, IdeviceService, amfi::AmfiClient}; + +use crate::{ + IdeviceErrorCode, IdeviceHandle, RUNTIME, + provider::{TcpProviderHandle, UsbmuxdProviderHandle}, +}; + +pub struct AmfiClientHandle(pub AmfiClient); + +/// Automatically creates and connects to AMFI service, returning a client handle +/// +/// # Arguments +/// * [`provider`] - A TcpProvider +/// * [`client`] - On success, will be set to point to a newly allocated AmfiClient handle +/// +/// # Returns +/// An error code indicating success or failure +/// +/// # Safety +/// `provider` must be a valid pointer to a handle allocated by this library +/// `client` must be a valid, non-null pointer to a location where the handle will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn amfi_connect_tcp( + provider: *mut TcpProviderHandle, + client: *mut *mut AmfiClientHandle, +) -> IdeviceErrorCode { + if provider.is_null() || client.is_null() { + log::error!("Null pointer provided"); + return IdeviceErrorCode::InvalidArg; + } + + let res: Result = RUNTIME.block_on(async move { + // Take ownership of the provider (without immediately dropping it) + let provider_box = unsafe { Box::from_raw(provider) }; + + // Get a reference to the inner value + let provider_ref = &provider_box.0; + + // Connect using the reference + let result = AmfiClient::connect(provider_ref).await; + + // Explicitly keep the provider_box alive until after connect completes + std::mem::forget(provider_box); + result + }); + + match res { + Ok(r) => { + let boxed = Box::new(AmfiClientHandle(r)); + unsafe { *client = Box::into_raw(boxed) }; + IdeviceErrorCode::IdeviceSuccess + } + Err(e) => { + // If connection failed, the provider_box was already forgotten, + // so we need to reconstruct it to avoid leak + let _ = unsafe { Box::from_raw(provider) }; + e.into() + } + } +} + +/// Automatically creates and connects to AMFI service, returning a client handle +/// +/// # Arguments +/// * [`provider`] - A UsbmuxdProvider +/// * [`client`] - On success, will be set to point to a newly allocated AmfiClient handle +/// +/// # Returns +/// An error code indicating success or failure +/// +/// # Safety +/// `provider` must be a valid pointer to a handle allocated by this library +/// `client` must be a valid, non-null pointer to a location where the handle will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn amfi_connect_usbmuxd( + provider: *mut UsbmuxdProviderHandle, + client: *mut *mut AmfiClientHandle, +) -> IdeviceErrorCode { + if provider.is_null() { + log::error!("Provider is null"); + return IdeviceErrorCode::InvalidArg; + } + + let res: Result = RUNTIME.block_on(async move { + // Take ownership of the provider (without immediately dropping it) + let provider_box = unsafe { Box::from_raw(provider) }; + + // Get a reference to the inner value + let provider_ref = &provider_box.0; + + // Connect using the reference + let result = AmfiClient::connect(provider_ref).await; + + // Explicitly keep the provider_box alive until after connect completes + std::mem::forget(provider_box); + result + }); + + match res { + Ok(r) => { + let boxed = Box::new(AmfiClientHandle(r)); + unsafe { *client = Box::into_raw(boxed) }; + IdeviceErrorCode::IdeviceSuccess + } + Err(e) => e.into(), + } +} + +/// Automatically creates and connects to AMFI service, returning a client handle +/// +/// # Arguments +/// * [`socket`] - An IdeviceSocket handle +/// * [`client`] - On success, will be set to point to a newly allocated AmfiClient handle +/// +/// # Returns +/// An error code indicating success or failure +/// +/// # Safety +/// `socket` must be a valid pointer to a handle allocated by this library +/// `client` must be a valid, non-null pointer to a location where the handle will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn amfi_new( + socket: *mut IdeviceHandle, + client: *mut *mut AmfiClientHandle, +) -> IdeviceErrorCode { + if socket.is_null() { + return IdeviceErrorCode::InvalidArg; + } + let socket = unsafe { Box::from_raw(socket) }.0; + let r = AmfiClient::new(socket); + let boxed = Box::new(AmfiClientHandle(r)); + unsafe { *client = Box::into_raw(boxed) }; + IdeviceErrorCode::IdeviceSuccess +} + +/// Shows the option in the settings UI +/// +/// # Arguments +/// * `client` - A valid AmfiClient handle +/// +/// # Returns +/// An error code indicating success or failure +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +#[unsafe(no_mangle)] +pub unsafe extern "C" fn amfi_reveal_developer_mode_option_in_ui( + client: *mut AmfiClientHandle, +) -> IdeviceErrorCode { + let res: Result<(), IdeviceError> = RUNTIME.block_on(async move { + // Take ownership of the client + let mut client_box = unsafe { Box::from_raw(client) }; + + // Get a reference to the inner value + let client_ref = &mut client_box.0; + let res = client_ref.reveal_developer_mode_option_in_ui().await; + + std::mem::forget(client_box); + res + }); + match res { + Ok(_) => IdeviceErrorCode::IdeviceSuccess, + Err(e) => e.into(), + } +} + +/// Enables developer mode on the device +/// +/// # Arguments +/// * `client` - A valid AmfiClient handle +/// +/// # Returns +/// An error code indicating success or failure +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +#[unsafe(no_mangle)] +pub unsafe extern "C" fn amfi_enable_developer_mode( + client: *mut AmfiClientHandle, +) -> IdeviceErrorCode { + let res: Result<(), IdeviceError> = RUNTIME.block_on(async move { + // Take ownership of the client + let mut client_box = unsafe { Box::from_raw(client) }; + + // Get a reference to the inner value + let client_ref = &mut client_box.0; + let res = client_ref.enable_developer_mode().await; + + std::mem::forget(client_box); + res + }); + match res { + Ok(_) => IdeviceErrorCode::IdeviceSuccess, + Err(e) => e.into(), + } +} + +/// Accepts developer mode on the device +/// +/// # Arguments +/// * `client` - A valid AmfiClient handle +/// +/// # Returns +/// An error code indicating success or failure +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +#[unsafe(no_mangle)] +pub unsafe extern "C" fn amfi_accept_developer_mode( + client: *mut AmfiClientHandle, +) -> IdeviceErrorCode { + let res: Result<(), IdeviceError> = RUNTIME.block_on(async move { + // Take ownership of the client + let mut client_box = unsafe { Box::from_raw(client) }; + + // Get a reference to the inner value + let client_ref = &mut client_box.0; + let res = client_ref.accept_developer_mode().await; + + std::mem::forget(client_box); + res + }); + match res { + Ok(_) => IdeviceErrorCode::IdeviceSuccess, + Err(e) => e.into(), + } +} + +/// Frees a handle +/// +/// # Arguments +/// * [`handle`] - The handle to free +/// +/// # Safety +/// `handle` must be a valid pointer to the handle that was allocated by this library, +/// or NULL (in which case this function does nothing) +#[unsafe(no_mangle)] +pub unsafe extern "C" fn amfi_client_free(handle: *mut AmfiClientHandle) { + if !handle.is_null() { + log::debug!("Freeing AmfiClient handle"); + let _ = unsafe { Box::from_raw(handle) }; + } +} diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index 8714d63..d41db13 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -1,6 +1,7 @@ // Jackson Coxson pub mod adapter; +pub mod amfi; pub mod core_device_proxy; pub mod debug_proxy; mod errors; diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 63f8461..7c760ad 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -49,6 +49,7 @@ bytes = "1.10.1" [features] afc = ["dep:chrono"] +amfi = [] core_device_proxy = ["dep:serde_json", "dep:json", "dep:byteorder"] debug_proxy = [] dvt = ["dep:byteorder", "dep:ns-keyed-archive"] @@ -73,6 +74,7 @@ xpc = [ ] full = [ "afc", + "amfi", "core_device_proxy", "debug_proxy", "dvt", diff --git a/idevice/src/amfi.rs b/idevice/src/amfi.rs new file mode 100644 index 0000000..f4be150 --- /dev/null +++ b/idevice/src/amfi.rs @@ -0,0 +1,114 @@ +//! Abstraction for Apple Mobile File Integrity + +use plist::Dictionary; + +use crate::{lockdown::LockdownClient, Idevice, IdeviceError, IdeviceService}; + +/// Client for interacting with the AMFI service on the device +pub struct AmfiClient { + /// The underlying device connection with established amfi service + pub idevice: Idevice, +} + +impl IdeviceService for AmfiClient { + /// Returns the amfi service name as registered with lockdownd + fn service_name() -> &'static str { + "com.apple.amfi.lockdown" + } + + /// Establishes a connection to the amfi service + /// + /// # Arguments + /// * `provider` - Device connection provider + /// + /// # Returns + /// A connected `AmfiClient` 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 amfi service port + /// 4. Establishes connection to the amfi port + /// 5. Optionally starts TLS if required by service + async fn connect( + provider: &dyn crate::provider::IdeviceProvider, + ) -> Result { + let mut lockdown = LockdownClient::connect(provider).await?; + lockdown + .start_session(&provider.get_pairing_file().await?) + .await?; + + let (port, ssl) = lockdown.start_service(Self::service_name()).await?; + + let mut idevice = provider.connect(port).await?; + if ssl { + idevice + .start_session(&provider.get_pairing_file().await?) + .await?; + } + + Ok(Self { idevice }) + } +} + +impl AmfiClient { + /// Creates a new amfi client from an existing device connection + /// + /// # Arguments + /// * `idevice` - Pre-established device connection + pub fn new(idevice: Idevice) -> Self { + Self { idevice } + } + + /// Shows the developer mode option in settings in iOS 18+ + /// Settings -> Privacy & Security -> Developer Mode + pub async fn reveal_developer_mode_option_in_ui(&mut self) -> Result<(), IdeviceError> { + let mut request = Dictionary::new(); + request.insert("action".into(), 0.into()); + self.idevice + .send_plist(plist::Value::Dictionary(request)) + .await?; + + let res = self.idevice.read_plist().await?; + if res.get("success").is_some() { + Ok(()) + } else { + Err(IdeviceError::UnexpectedResponse) + } + } + + /// Enables developer mode, triggering a reboot on iOS 18+ + pub async fn enable_developer_mode(&mut self) -> Result<(), IdeviceError> { + let mut request = Dictionary::new(); + request.insert("action".into(), 1.into()); + self.idevice + .send_plist(plist::Value::Dictionary(request)) + .await?; + + let res = self.idevice.read_plist().await?; + if res.get("success").is_some() { + Ok(()) + } else { + Err(IdeviceError::UnexpectedResponse) + } + } + + /// Shows the accept dialogue for enabling developer mode + pub async fn accept_developer_mode(&mut self) -> Result<(), IdeviceError> { + let mut request = Dictionary::new(); + request.insert("action".into(), 2.into()); + self.idevice + .send_plist(plist::Value::Dictionary(request)) + .await?; + + let res = self.idevice.read_plist().await?; + if res.get("success").is_some() { + Ok(()) + } else { + Err(IdeviceError::UnexpectedResponse) + } + } +} diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index f82f1d0..26704fd 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -3,6 +3,8 @@ #[cfg(feature = "afc")] pub mod afc; +#[cfg(feature = "amfi")] +pub mod amfi; #[cfg(feature = "core_device_proxy")] pub mod core_device_proxy; #[cfg(feature = "debug_proxy")] diff --git a/tools/Cargo.toml b/tools/Cargo.toml index d1c53fe..9879f8a 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -57,11 +57,14 @@ path = "src/misagent.rs" name = "location_simulation" path = "src/location_simulation.rs" - [[bin]] name = "afc" path = "src/afc.rs" +[[bin]] +name = "amfi" +path = "src/amfi.rs" + [dependencies] idevice = { path = "../idevice", features = ["full"] } tokio = { version = "1.43", features = ["io-util", "macros", "time", "full"] } diff --git a/tools/src/amfi.rs b/tools/src/amfi.rs new file mode 100644 index 0000000..be1d594 --- /dev/null +++ b/tools/src/amfi.rs @@ -0,0 +1,84 @@ +// Jackson Coxson + +use clap::{Arg, Command}; +use idevice::{amfi::AmfiClient, IdeviceService}; + +mod common; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let matches = Command::new("amfi") + .about("Mess with developer mode") + .arg( + Arg::new("host") + .long("host") + .value_name("HOST") + .help("IP address of the device"), + ) + .arg( + Arg::new("pairing_file") + .long("pairing-file") + .value_name("PATH") + .help("Path to the pairing file"), + ) + .arg( + Arg::new("udid") + .value_name("UDID") + .help("UDID of the device (overrides host/pairing file)") + .index(1), + ) + .arg( + Arg::new("about") + .long("about") + .help("Show about information") + .action(clap::ArgAction::SetTrue), + ) + .subcommand(Command::new("show").about("Shows the developer mode option in settings")) + .subcommand(Command::new("enable").about("Enables developer mode")) + .subcommand(Command::new("accept").about("Shows the accept dialogue for developer mode")) + .get_matches(); + + if matches.get_flag("about") { + println!("amfi - manage developer mode"); + println!("Copyright (c) 2025 Jackson Coxson"); + return; + } + + let udid = matches.get_one::("udid"); + let host = matches.get_one::("host"); + let pairing_file = matches.get_one::("pairing_file"); + + let provider = match common::get_provider(udid, host, pairing_file, "amfi-jkcoxson").await { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + + let mut amfi_client = AmfiClient::connect(&*provider) + .await + .expect("Failed to connect to amfi"); + + if matches.subcommand_matches("show").is_some() { + amfi_client + .reveal_developer_mode_option_in_ui() + .await + .expect("Failed to show"); + } else if matches.subcommand_matches("enable").is_some() { + amfi_client + .enable_developer_mode() + .await + .expect("Failed to show"); + } else if matches.subcommand_matches("accept").is_some() { + amfi_client + .accept_developer_mode() + .await + .expect("Failed to show"); + } else { + eprintln!("Invalid usage, pass -h for help"); + } + return; +}