From 88d031edd70d7feeae0f2df98a0495de470959a3 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Sat, 5 Apr 2025 01:25:56 -0600 Subject: [PATCH] Initial afc support --- idevice/Cargo.toml | 4 +- idevice/src/afc/errors.rs | 107 ++++++++++++++++++++++++++++++++++++++ idevice/src/afc/mod.rs | 107 ++++++++++++++++++++++++++++++++++++++ idevice/src/afc/opcode.rs | 83 +++++++++++++++++++++++++++++ idevice/src/afc/packet.rs | 98 ++++++++++++++++++++++++++++++++++ idevice/src/lib.rs | 14 +++++ tools/Cargo.toml | 5 ++ tools/src/afc.rs | 81 +++++++++++++++++++++++++++++ 8 files changed, 498 insertions(+), 1 deletion(-) create mode 100644 idevice/src/afc/errors.rs create mode 100644 idevice/src/afc/mod.rs create mode 100644 idevice/src/afc/opcode.rs create mode 100644 idevice/src/afc/packet.rs create mode 100644 tools/src/afc.rs diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 18d9efc..707056f 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -43,6 +43,7 @@ tun-rs = { version = "2.0.8", features = ["async_tokio"] } bytes = "1.10.1" [features] +afc = [] core_device_proxy = ["dep:serde_json", "dep:json", "dep:byteorder"] debug_proxy = [] dvt = ["dep:byteorder", "dep:ns-keyed-archive"] @@ -66,6 +67,7 @@ xpc = [ "dep:json", ] full = [ + "afc", "core_device_proxy", "debug_proxy", "dvt", @@ -80,7 +82,7 @@ full = [ "tunnel_tcp_stack", "tss", "tunneld", - "sbservices" + "sbservices", ] # Why: https://github.com/rust-lang/cargo/issues/1197 diff --git a/idevice/src/afc/errors.rs b/idevice/src/afc/errors.rs new file mode 100644 index 0000000..41c4037 --- /dev/null +++ b/idevice/src/afc/errors.rs @@ -0,0 +1,107 @@ +// Jackson Coxson + +#[derive(thiserror::Error, Debug)] +#[non_exhaustive] +#[repr(C)] +pub enum AfcError { + Success = 0, + UnknownError = 1, + OpHeaderInvalid = 2, + NoResources = 3, + ReadError = 4, + WriteError = 5, + UnknownPacketType = 6, + InvalidArg = 7, + ObjectNotFound = 8, + ObjectIsDir = 9, + PermDenied = 10, + ServiceNotConnected = 11, + OpTimeout = 12, + TooMuchData = 13, + EndOfData = 14, + OpNotSupported = 15, + ObjectExists = 16, + ObjectBusy = 17, + NoSpaceLeft = 18, + OpWouldBlock = 19, + IoError = 20, + OpInterrupted = 21, + OpInProgress = 22, + InternalError = 23, + MuxError = 30, + NoMem = 31, + NotEnoughData = 32, + DirNotEmpty = 33, +} + +impl std::fmt::Display for AfcError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let description = match self { + AfcError::Success => "Success", + AfcError::UnknownError => "Unknown error", + AfcError::OpHeaderInvalid => "Operation header invalid", + AfcError::NoResources => "No resources available", + AfcError::ReadError => "Read error", + AfcError::WriteError => "Write error", + AfcError::UnknownPacketType => "Unknown packet type", + AfcError::InvalidArg => "Invalid argument", + AfcError::ObjectNotFound => "Object not found", + AfcError::ObjectIsDir => "Object is a directory", + AfcError::PermDenied => "Permission denied", + AfcError::ServiceNotConnected => "Service not connected", + AfcError::OpTimeout => "Operation timed out", + AfcError::TooMuchData => "Too much data", + AfcError::EndOfData => "End of data", + AfcError::OpNotSupported => "Operation not supported", + AfcError::ObjectExists => "Object already exists", + AfcError::ObjectBusy => "Object is busy", + AfcError::NoSpaceLeft => "No space left", + AfcError::OpWouldBlock => "Operation would block", + AfcError::IoError => "I/O error", + AfcError::OpInterrupted => "Operation interrupted", + AfcError::OpInProgress => "Operation in progress", + AfcError::InternalError => "Internal error", + AfcError::MuxError => "Multiplexer error", + AfcError::NoMem => "Out of memory", + AfcError::NotEnoughData => "Not enough data", + AfcError::DirNotEmpty => "Directory not empty", + }; + write!(f, "{}", description) + } +} + +impl From for AfcError { + fn from(value: u64) -> Self { + match value { + 0 => Self::Success, + 1 => Self::UnknownError, + 2 => Self::OpHeaderInvalid, + 3 => Self::NoResources, + 4 => Self::ReadError, + 5 => Self::WriteError, + 6 => Self::UnknownPacketType, + 7 => Self::InvalidArg, + 8 => Self::ObjectNotFound, + 9 => Self::ObjectIsDir, + 10 => Self::PermDenied, + 11 => Self::ServiceNotConnected, + 12 => Self::OpTimeout, + 13 => Self::TooMuchData, + 14 => Self::EndOfData, + 15 => Self::OpNotSupported, + 16 => Self::ObjectExists, + 17 => Self::ObjectBusy, + 18 => Self::NoSpaceLeft, + 19 => Self::OpWouldBlock, + 20 => Self::IoError, + 21 => Self::OpInterrupted, + 22 => Self::OpInProgress, + 23 => Self::InternalError, + 30 => Self::MuxError, + 31 => Self::NoMem, + 32 => Self::NotEnoughData, + 33 => Self::DirNotEmpty, + _ => Self::UnknownError, // fallback for unknown codes + } + } +} diff --git a/idevice/src/afc/mod.rs b/idevice/src/afc/mod.rs new file mode 100644 index 0000000..354ab43 --- /dev/null +++ b/idevice/src/afc/mod.rs @@ -0,0 +1,107 @@ +// Jackson Coxson + +use errors::AfcError; +use opcode::AfcOpcode; +use packet::{AfcPacket, AfcPacketHeader}; + +use crate::{lockdownd::LockdowndClient, Idevice, IdeviceError, IdeviceService}; + +pub mod errors; +pub mod opcode; +pub mod packet; + +pub const MAGIC: u64 = 0x4141504c36414643; + +pub struct AfcClient { + pub idevice: Idevice, + package_number: u64, +} + +impl IdeviceService for AfcClient { + fn service_name() -> &'static str { + "com.apple.afc" + } + + async fn connect( + provider: &dyn crate::provider::IdeviceProvider, + ) -> Result { + let mut lockdown = LockdowndClient::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, + package_number: 0, + }) + } +} + +impl AfcClient { + pub fn new(idevice: Idevice) -> Self { + Self { + idevice, + package_number: 0, + } + } + + pub async fn list(&mut self, path: impl Into) -> Result, IdeviceError> { + let path = path.into(); + let header_payload = path.as_bytes().to_vec(); + let header_len = header_payload.len() as u64 + AfcPacketHeader::LEN; + + let header = AfcPacketHeader { + magic: MAGIC, + entire_len: header_len, // it's the same since the payload is empty for this + header_payload_len: header_len, + packet_num: self.package_number, + operation: AfcOpcode::ReadDir, + }; + self.package_number += 1; + + let packet = AfcPacket { + header, + header_payload, + payload: Vec::new(), + }; + + self.send(packet).await?; + let res = self.read().await?; + + let strings: Vec = res + .payload + .split(|b| *b == 0) + .filter(|s| !s.is_empty()) + .map(|s| String::from_utf8_lossy(s).into_owned()) + .collect(); + Ok(strings) + } + + pub async fn read(&mut self) -> Result { + let res = AfcPacket::read(&mut self.idevice).await?; + if res.header.operation == AfcOpcode::Status { + if res.header_payload.len() < 8 { + log::error!("AFC returned error opcode, but not a code"); + return Err(IdeviceError::UnexpectedResponse); + } + let code = u64::from_le_bytes(res.header_payload[..8].try_into().unwrap()); + return Err(IdeviceError::Afc(AfcError::from(code))); + } + Ok(res) + } + + pub async fn send(&mut self, packet: AfcPacket) -> Result<(), IdeviceError> { + let packet = packet.serialize(); + self.idevice.send_raw(&packet).await?; + Ok(()) + } +} diff --git a/idevice/src/afc/opcode.rs b/idevice/src/afc/opcode.rs new file mode 100644 index 0000000..6f5f88c --- /dev/null +++ b/idevice/src/afc/opcode.rs @@ -0,0 +1,83 @@ +// Jackson Coxson + +#[derive(Clone, Debug, PartialEq, Eq)] +#[repr(u64)] +pub enum AfcOpcode { + Status = 0x00000001, + Data = 0x00000002, // Data + ReadDir = 0x00000003, // ReadDir + ReadFile = 0x00000004, // ReadFile + WriteFile = 0x00000005, // WriteFile + WritePart = 0x00000006, // WritePart + Truncate = 0x00000007, // TruncateFile + RemovePath = 0x00000008, // RemovePath + MakeDir = 0x00000009, // MakeDir + GetFileInfo = 0x0000000a, // GetFileInfo + GetDevInfo = 0x0000000b, // GetDeviceInfo + WriteFileAtom = 0x0000000c, // WriteFileAtomic (tmp file+rename) + FileOpen = 0x0000000d, // FileRefOpen + FileOpenRes = 0x0000000e, // FileRefOpenResult + Read = 0x0000000f, // FileRefRead + Write = 0x00000010, // FileRefWrite + FileSeek = 0x00000011, // FileRefSeek + FileTell = 0x00000012, // FileRefTell + FileTellRes = 0x00000013, // FileRefTellResult + FileClose = 0x00000014, // FileRefClose + FileSetSize = 0x00000015, // FileRefSetFileSize (ftruncate) + GetConInfo = 0x00000016, // GetConnectionInfo + SetConOptions = 0x00000017, // SetConnectionOptions + RenamePath = 0x00000018, // RenamePath + SetFsBs = 0x00000019, // SetFSBlockSize (0x800000) + SetSocketBs = 0x0000001A, // SetSocketBlockSize (0x800000) + FileLock = 0x0000001B, // FileRefLock + MakeLink = 0x0000001C, // MakeLink + SetFileTime = 0x0000001E, // Set st_mtime +} + +pub enum AfcFopenMode { + RdOnly = 0x00000001, // r O_RDONLY + Rw = 0x00000002, // r+ O_RDWR | O_CREAT + WrOnly = 0x00000003, // w O_WRONLY | O_CREAT | O_TRUNC + Wr = 0x00000004, // w+ O_RDWR | O_CREAT | O_TRUNC + Append = 0x00000005, // a O_WRONLY | O_APPEND | O_CREAT + RdAppend = 0x00000006, // a+ O_RDWR | O_APPEND | O_CREAT +} + +impl TryFrom for AfcOpcode { + type Error = (); + + fn try_from(value: u64) -> Result { + match value { + 0x00000001 => Ok(Self::Status), + 0x00000002 => Ok(Self::Data), + 0x00000003 => Ok(Self::ReadDir), + 0x00000004 => Ok(Self::ReadFile), + 0x00000005 => Ok(Self::WriteFile), + 0x00000006 => Ok(Self::WritePart), + 0x00000007 => Ok(Self::Truncate), + 0x00000008 => Ok(Self::RemovePath), + 0x00000009 => Ok(Self::MakeDir), + 0x0000000a => Ok(Self::GetFileInfo), + 0x0000000b => Ok(Self::GetDevInfo), + 0x0000000c => Ok(Self::WriteFileAtom), + 0x0000000d => Ok(Self::FileOpen), + 0x0000000e => Ok(Self::FileOpenRes), + 0x0000000f => Ok(Self::Read), + 0x00000010 => Ok(Self::Write), + 0x00000011 => Ok(Self::FileSeek), + 0x00000012 => Ok(Self::FileTell), + 0x00000013 => Ok(Self::FileTellRes), + 0x00000014 => Ok(Self::FileClose), + 0x00000015 => Ok(Self::FileSetSize), + 0x00000016 => Ok(Self::GetConInfo), + 0x00000017 => Ok(Self::SetConOptions), + 0x00000018 => Ok(Self::RenamePath), + 0x00000019 => Ok(Self::SetFsBs), + 0x0000001A => Ok(Self::SetSocketBs), + 0x0000001B => Ok(Self::FileLock), + 0x0000001C => Ok(Self::MakeLink), + 0x0000001E => Ok(Self::SetFileTime), + _ => Err(()), + } + } +} diff --git a/idevice/src/afc/packet.rs b/idevice/src/afc/packet.rs new file mode 100644 index 0000000..02efefe --- /dev/null +++ b/idevice/src/afc/packet.rs @@ -0,0 +1,98 @@ +// Jackson Coxson + +use log::debug; + +use crate::{Idevice, IdeviceError}; + +use super::opcode::AfcOpcode; + +#[derive(Clone, Debug)] +pub struct AfcPacketHeader { + pub magic: u64, + pub entire_len: u64, + pub header_payload_len: u64, + pub packet_num: u64, + pub operation: AfcOpcode, +} + +#[derive(Clone, Debug)] +pub struct AfcPacket { + pub header: AfcPacketHeader, + pub header_payload: Vec, + pub payload: Vec, +} + +impl AfcPacketHeader { + pub const LEN: u64 = 40; + + pub fn serialize(&self) -> Vec { + let mut res = Vec::with_capacity(Self::LEN as usize); + + res.extend_from_slice(&self.magic.to_le_bytes()); + res.extend_from_slice(&self.entire_len.to_le_bytes()); + res.extend_from_slice(&self.header_payload_len.to_le_bytes()); + res.extend_from_slice(&self.packet_num.to_le_bytes()); + res.extend_from_slice(&(self.operation.clone() as u64).to_le_bytes()); + + res + } + + pub async fn read(reader: &mut Idevice) -> Result { + let header_bytes = reader.read_raw(Self::LEN as usize).await?; + let mut chunks = header_bytes.chunks_exact(8); + let res = Self { + magic: u64::from_le_bytes(chunks.next().unwrap().try_into().unwrap()), + entire_len: u64::from_le_bytes(chunks.next().unwrap().try_into().unwrap()), + header_payload_len: u64::from_le_bytes(chunks.next().unwrap().try_into().unwrap()), + packet_num: u64::from_le_bytes(chunks.next().unwrap().try_into().unwrap()), + operation: match AfcOpcode::try_from(u64::from_le_bytes( + chunks.next().unwrap().try_into().unwrap(), + )) { + Ok(o) => o, + Err(_) => { + return Err(IdeviceError::UnknownAfcOpcode); + } + }, + }; + if res.magic != super::MAGIC { + return Err(IdeviceError::InvalidAfcMagic); + } + Ok(res) + } +} + +impl AfcPacket { + pub fn serialize(&self) -> Vec { + let mut res = Vec::new(); + + res.extend_from_slice(&self.header.serialize()); + res.extend_from_slice(&self.header_payload); + res.extend_from_slice(&self.payload); + + res + } + + pub async fn read(reader: &mut Idevice) -> Result { + let header = AfcPacketHeader::read(reader).await?; + debug!("afc header: {header:?}"); + let header_payload = reader + .read_raw((header.header_payload_len - AfcPacketHeader::LEN) as usize) + .await?; + + let payload = if header.header_payload_len == header.entire_len { + Vec::new() // no payload + } else { + reader + .read_raw((header.entire_len - header.header_payload_len) as usize) + .await? + }; + + let res = Self { + header, + header_payload, + payload, + }; + debug!("Recv afc: {res:?}"); + Ok(res) + } +} diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 26175f2..7eca65c 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -1,5 +1,7 @@ // Jackson Coxson +#[cfg(feature = "afc")] +pub mod afc; #[cfg(feature = "core_device_proxy")] pub mod core_device_proxy; #[cfg(feature = "debug_proxy")] @@ -298,6 +300,18 @@ pub enum IdeviceError { #[error("misagent operation failed")] MisagentFailure, + #[cfg(feature = "afc")] + #[error("afc error")] + Afc(#[from] afc::errors::AfcError), + + #[cfg(feature = "afc")] + #[error("unknown afc opcode")] + UnknownAfcOpcode, + + #[cfg(feature = "afc")] + #[error("invalid afc magic")] + InvalidAfcMagic, + #[cfg(any(feature = "tss", feature = "tunneld"))] #[error("http reqwest error")] Reqwest(#[from] reqwest::Error), diff --git a/tools/Cargo.toml b/tools/Cargo.toml index fc9455d..d1c53fe 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -57,6 +57,11 @@ path = "src/misagent.rs" name = "location_simulation" path = "src/location_simulation.rs" + +[[bin]] +name = "afc" +path = "src/afc.rs" + [dependencies] idevice = { path = "../idevice", features = ["full"] } tokio = { version = "1.43", features = ["io-util", "macros", "time", "full"] } diff --git a/tools/src/afc.rs b/tools/src/afc.rs new file mode 100644 index 0000000..14c8a16 --- /dev/null +++ b/tools/src/afc.rs @@ -0,0 +1,81 @@ +// Jackson Coxson + +use std::path::PathBuf; + +use clap::{arg, value_parser, Arg, Command}; +use idevice::{afc::AfcClient, misagent::MisagentClient, IdeviceService}; + +mod common; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let matches = Command::new("afc") + .about("Start a tunnel") + .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)"), + ) + .arg( + Arg::new("about") + .long("about") + .help("Show about information") + .action(clap::ArgAction::SetTrue), + ) + .subcommand( + Command::new("list") + .about("Lists the items in the directory") + .arg(Arg::new("path").required(true).index(1)), + ) + .subcommand( + Command::new("remove") + .about("Remove a provisioning profile") + .arg(Arg::new("path").required(true).index(1)), + ) + .get_matches(); + + if matches.get_flag("about") { + println!("afc"); + 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, "afc-jkcoxson").await { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + let mut afc_client = AfcClient::connect(&*provider) + .await + .expect("Unable to connect to misagent"); + + if let Some(matches) = matches.subcommand_matches("list") { + let path = matches.get_one::("path").expect("No path passed"); + let res = afc_client.list(path).await.expect("Failed to read dir"); + println!("{path}\n{res:#?}"); + } else if let Some(matches) = matches.subcommand_matches("remove") { + let path = matches.get_one::("id").expect("No path passed"); + } else { + eprintln!("Invalid usage, pass -h for help"); + } +}