Merge master into rppairing-try2

This commit is contained in:
Jackson Coxson
2026-02-14 13:32:14 -07:00
75 changed files with 4831 additions and 7577 deletions

View File

@@ -30,8 +30,14 @@ jobs:
- name: Install rustup targets - name: Install rustup targets
run: | run: |
rustup target add aarch64-apple-ios && \
rustup target add x86_64-apple-ios && \
rustup target add aarch64-apple-ios-sim && \
rustup target add aarch64-apple-darwin && \ rustup target add aarch64-apple-darwin && \
rustup target add x86_64-apple-darwin && cargo install --force --locked bindgen-cli rustup target add x86_64-apple-darwin && \
rustup target add aarch64-apple-ios-macabi && \
rustup target add x86_64-apple-ios-macabi && \
cargo install --force --locked bindgen-cli
- name: Build all Apple targets and examples/tools - name: Build all Apple targets and examples/tools
run: | run: |
@@ -44,6 +50,12 @@ jobs:
path: | path: |
target/*apple*/release/libidevice_ffi.a target/*apple*/release/libidevice_ffi.a
- name: Upload macOS+iOS XCFramework
uses: actions/upload-artifact@v4
with:
name: idevice-xcframework
path: swift/bundle.zip
- name: Upload C examples/tools - name: Upload C examples/tools
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:

3870
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -47,6 +47,8 @@ To keep dependency bloat and compile time down, everything is contained in featu
|------------------------|-----------------------------------------------------------------------------| |------------------------|-----------------------------------------------------------------------------|
| `afc` | Apple File Conduit for file system access.| | `afc` | Apple File Conduit for file system access.|
| `amfi` | Apple mobile file integrity service | | `amfi` | Apple mobile file integrity service |
| `bt_packet_logger` | Capture Bluetooth packets. |
| `companion_proxy` | Manage paired Apple Watches. |
| `core_device_proxy` | Start a secure tunnel to access protected services. | | `core_device_proxy` | Start a secure tunnel to access protected services. |
| `crashreportcopymobile`| Copy crash reports.| | `crashreportcopymobile`| Copy crash reports.|
| `debug_proxy` | Send GDB commands to the device.| | `debug_proxy` | Send GDB commands to the device.|
@@ -55,13 +57,19 @@ To keep dependency bloat and compile time down, everything is contained in featu
| `heartbeat` | Maintain a heartbeat connection.| | `heartbeat` | Maintain a heartbeat connection.|
| `house_arrest` | Manage files in app containers | | `house_arrest` | Manage files in app containers |
| `installation_proxy` | Manage app installation and uninstallation.| | `installation_proxy` | Manage app installation and uninstallation.|
| `springboardservices` | Control SpringBoard (e.g. UI interactions). Partial support.| | `installcoordination_proxy` | Manage app installation coordination.|
| `misagent` | Manage provisioning profiles on the device.|
| `mobilebackup2` | Manage backups.|
| `mobile_image_mounter` | Manage DDI images.|
| `location_simulation` | Simulate GPS locations on the device.| | `location_simulation` | Simulate GPS locations on the device.|
| `misagent` | Manage provisioning profiles on the device.|
| `mobile_image_mounter` | Manage DDI images.|
| `mobileactivationd` | Activate/Deactivate device.|
| `mobilebackup2` | Manage backups.|
| `pair` | Pair the device.| | `pair` | Pair the device.|
| `syslog_relay` | Relay system logs from the device | | `pcapd` | Capture network packets.|
| `preboard_service` | Interface with Preboard.|
| `restore_service` | Restore service (recovery/reboot).|
| `screenshotr` | Take screenshots.|
| `springboardservices` | Control SpringBoard (icons, wallpaper, orientation, etc.).|
| `syslog_relay` | Relay system logs and OS trace logs from the device. |
| `tcp` | Connect to devices over TCP.| | `tcp` | Connect to devices over TCP.|
| `tunnel_tcp_stack` | Naive in-process TCP stack for `core_device_proxy`.| | `tunnel_tcp_stack` | Naive in-process TCP stack for `core_device_proxy`.|
| `tss` | Make requests to Apple's TSS servers. Partial support.| | `tss` | Make requests to Apple's TSS servers. Partial support.|
@@ -73,16 +81,11 @@ To keep dependency bloat and compile time down, everything is contained in featu
Finish the following: Finish the following:
- springboard - webinspector
Implement the following: Implement the following:
- companion_proxy
- diagnostics
- mobilebackup2
- notification_proxy - notification_proxy
- screenshot
- webinspector
As this project is done in my free time within my busy schedule, there As this project is done in my free time within my busy schedule, there
is no ETA for any of these. Feel free to contribute or donate! is no ETA for any of these. Feel free to contribute or donate!

View File

@@ -0,0 +1,60 @@
// Jackson Coxson
#pragma once
#include <idevice++/bindings.hpp>
#include <idevice++/ffi.hpp>
#include <idevice++/provider.hpp>
#include <idevice++/result.hpp>
#include <string>
#include <vector>
namespace IdeviceFFI {
using DiagnosticsRelayPtr =
std::unique_ptr<DiagnosticsRelayClientHandle,
FnDeleter<DiagnosticsRelayClientHandle, diagnostics_relay_client_free>>;
class DiagnosticsRelay {
public:
// Factory: connect via Provider
static Result<DiagnosticsRelay, FfiError> connect(Provider& provider);
// Factory: wrap an existing Idevice socket (consumes it on success)
static Result<DiagnosticsRelay, FfiError> from_socket(Idevice&& socket);
// API Methods - queries returning optional plist
Result<Option<plist_t>, FfiError> ioregistry(Option<std::string> current_plane,
Option<std::string> entry_name,
Option<std::string> entry_class) const;
Result<Option<plist_t>, FfiError> mobilegestalt(Option<std::vector<char*>> keys) const;
Result<Option<plist_t>, FfiError> gasguage() const;
Result<Option<plist_t>, FfiError> nand() const;
Result<Option<plist_t>, FfiError> all() const;
Result<Option<plist_t>, FfiError> wifi() const;
// API Methods - actions
Result<void, FfiError> restart();
Result<void, FfiError> shutdown();
Result<void, FfiError> sleep();
Result<void, FfiError> goodbye();
// RAII / moves
~DiagnosticsRelay() noexcept = default;
DiagnosticsRelay(DiagnosticsRelay&&) noexcept = default;
DiagnosticsRelay& operator=(DiagnosticsRelay&&) noexcept = default;
DiagnosticsRelay(const DiagnosticsRelay&) = delete;
DiagnosticsRelay& operator=(const DiagnosticsRelay&) = delete;
DiagnosticsRelayClientHandle* raw() const noexcept { return handle_.get(); }
static DiagnosticsRelay adopt(DiagnosticsRelayClientHandle* h) noexcept {
return DiagnosticsRelay(h);
}
private:
explicit DiagnosticsRelay(DiagnosticsRelayClientHandle* h) noexcept : handle_(h) {}
DiagnosticsRelayPtr handle_{};
};
} // namespace IdeviceFFI

View File

@@ -0,0 +1,46 @@
#pragma once
#include <idevice++/bindings.hpp>
#include <idevice++/ffi.hpp>
#include <idevice++/provider.hpp>
#include <memory>
#include <string>
#include <vector>
namespace IdeviceFFI {
using NotificationProxyPtr = std::unique_ptr<NotificationProxyClientHandle,
FnDeleter<NotificationProxyClientHandle, notification_proxy_client_free>>;
class NotificationProxy {
public:
// Factory: connect via Provider
static Result<NotificationProxy, FfiError> connect(Provider& provider);
// Factory: wrap an existing Idevice socket (consumes it on success)
static Result<NotificationProxy, FfiError> from_socket(Idevice&& socket);
// Ops
Result<void, FfiError> post_notification(const std::string& name);
Result<void, FfiError> observe_notification(const std::string& name);
Result<void, FfiError> observe_notifications(const std::vector<std::string>& names);
Result<std::string, FfiError> receive_notification();
Result<std::string, FfiError> receive_notification_with_timeout(u_int64_t interval);
// RAII / moves
~NotificationProxy() noexcept = default;
NotificationProxy(NotificationProxy&&) noexcept = default;
NotificationProxy& operator=(NotificationProxy&&) noexcept = default;
NotificationProxy(const NotificationProxy&) = delete;
NotificationProxy& operator=(const NotificationProxy&) = delete;
NotificationProxyClientHandle* raw() const noexcept { return handle_.get(); }
static NotificationProxy adopt(NotificationProxyClientHandle* h) noexcept {
return NotificationProxy(h);
}
private:
explicit NotificationProxy(NotificationProxyClientHandle* h) noexcept : handle_(h) {}
NotificationProxyPtr handle_{};
};
} // namespace IdeviceFFI

View File

@@ -0,0 +1,159 @@
// Jackson Coxson
#include <idevice++/bindings.hpp>
#include <idevice++/diagnostics_relay.hpp>
#include <idevice++/ffi.hpp>
#include <idevice++/provider.hpp>
namespace IdeviceFFI {
// -------- Factory Methods --------
Result<DiagnosticsRelay, FfiError> DiagnosticsRelay::connect(Provider& provider) {
DiagnosticsRelayClientHandle* out = nullptr;
FfiError e(::diagnostics_relay_client_connect(provider.raw(), &out));
if (e) {
return Err(e);
}
return Ok(DiagnosticsRelay::adopt(out));
}
Result<DiagnosticsRelay, FfiError> DiagnosticsRelay::from_socket(Idevice&& socket) {
DiagnosticsRelayClientHandle* out = nullptr;
FfiError e(::diagnostics_relay_client_new(socket.raw(), &out));
if (e) {
return Err(e);
}
socket.release();
return Ok(DiagnosticsRelay::adopt(out));
}
// -------- API Methods --------
Result<Option<plist_t>, FfiError>
DiagnosticsRelay::ioregistry(Option<std::string> current_plane,
Option<std::string> entry_name,
Option<std::string> entry_class) const {
plist_t res = nullptr;
const char* plane_ptr = current_plane.is_some() ? current_plane.unwrap().c_str() : nullptr;
const char* name_ptr = entry_name.is_some() ? entry_name.unwrap().c_str() : nullptr;
const char* class_ptr = entry_class.is_some() ? entry_class.unwrap().c_str() : nullptr;
FfiError e(
::diagnostics_relay_client_ioregistry(handle_.get(), plane_ptr, name_ptr, class_ptr, &res));
if (e) {
return Err(e);
}
if (res == nullptr) {
return Ok(Option<plist_t>(None));
}
return Ok(Some(res));
}
Result<Option<plist_t>, FfiError>
DiagnosticsRelay::mobilegestalt(Option<std::vector<char*>> keys) const {
plist_t res = nullptr;
if (!keys.is_some() || keys.unwrap().empty()) {
return Err(FfiError::InvalidArgument());
}
FfiError e(::diagnostics_relay_client_mobilegestalt(
handle_.get(), keys.unwrap().data(), keys.unwrap().size(), &res));
if (e) {
return Err(e);
}
if (res == nullptr) {
return Ok(Option<plist_t>(None));
}
return Ok(Some(res));
}
Result<Option<plist_t>, FfiError> DiagnosticsRelay::gasguage() const {
plist_t res = nullptr;
FfiError e(::diagnostics_relay_client_gasguage(handle_.get(), &res));
if (e) {
return Err(e);
}
if (res == nullptr) {
return Ok(Option<plist_t>(None));
}
return Ok(Some(res));
}
Result<Option<plist_t>, FfiError> DiagnosticsRelay::nand() const {
plist_t res = nullptr;
FfiError e(::diagnostics_relay_client_nand(handle_.get(), &res));
if (e) {
return Err(e);
}
if (res == nullptr) {
return Ok(Option<plist_t>(None));
}
return Ok(Some(res));
}
Result<Option<plist_t>, FfiError> DiagnosticsRelay::all() const {
plist_t res = nullptr;
FfiError e(::diagnostics_relay_client_all(handle_.get(), &res));
if (e) {
return Err(e);
}
if (res == nullptr) {
return Ok(Option<plist_t>(None));
}
return Ok(Some(res));
}
Result<Option<plist_t>, FfiError> DiagnosticsRelay::wifi() const {
plist_t res = nullptr;
FfiError e(::diagnostics_relay_client_wifi(handle_.get(), &res));
if (e) {
return Err(e);
}
if (res == nullptr) {
return Ok(Option<plist_t>(None));
}
return Ok(Some(res));
}
Result<void, FfiError> DiagnosticsRelay::restart() {
FfiError e(::diagnostics_relay_client_restart(handle_.get()));
if (e) {
return Err(e);
}
return Ok();
}
Result<void, FfiError> DiagnosticsRelay::shutdown() {
FfiError e(::diagnostics_relay_client_shutdown(handle_.get()));
if (e) {
return Err(e);
}
return Ok();
}
Result<void, FfiError> DiagnosticsRelay::sleep() {
FfiError e(::diagnostics_relay_client_sleep(handle_.get()));
if (e) {
return Err(e);
}
return Ok();
}
Result<void, FfiError> DiagnosticsRelay::goodbye() {
FfiError e(::diagnostics_relay_client_goodbye(handle_.get()));
if (e) {
return Err(e);
}
return Ok();
}
} // namespace IdeviceFFI

View File

@@ -0,0 +1,82 @@
// Jackson Coxson
#include <idevice++/bindings.hpp>
#include <idevice++/ffi.hpp>
#include <idevice++/notification_proxy.hpp>
#include <idevice++/provider.hpp>
namespace IdeviceFFI {
Result<NotificationProxy, FfiError> NotificationProxy::connect(Provider& provider) {
NotificationProxyClientHandle* out = nullptr;
FfiError e(::notification_proxy_connect(provider.raw(), &out));
if (e) {
provider.release();
return Err(e);
}
return Ok(NotificationProxy::adopt(out));
}
Result<NotificationProxy, FfiError> NotificationProxy::from_socket(Idevice&& socket) {
NotificationProxyClientHandle* out = nullptr;
FfiError e(::notification_proxy_new(socket.raw(), &out));
if (e) {
return Err(e);
}
socket.release();
return Ok(NotificationProxy::adopt(out));
}
Result<void, FfiError> NotificationProxy::post_notification(const std::string& name) {
FfiError e(::notification_proxy_post(handle_.get(), name.c_str()));
if (e) {
return Err(e);
}
return Ok();
}
Result<void, FfiError> NotificationProxy::observe_notification(const std::string& name) {
FfiError e(::notification_proxy_observe(handle_.get(), name.c_str()));
if (e) {
return Err(e);
}
return Ok();
}
Result<void, FfiError> NotificationProxy::observe_notifications(const std::vector<std::string>& names) {
std::vector<const char*> ptrs;
ptrs.reserve(names.size() + 1);
for (const auto& n : names) {
ptrs.push_back(n.c_str());
}
ptrs.push_back(nullptr);
FfiError e(::notification_proxy_observe_multiple(handle_.get(), ptrs.data()));
if (e) {
return Err(e);
}
return Ok();
}
Result<std::string, FfiError> NotificationProxy::receive_notification() {
char* name_ptr = nullptr;
FfiError e(::notification_proxy_receive(handle_.get(), &name_ptr));
if (e) {
return Err(e);
}
std::string name(name_ptr);
::notification_proxy_free_string(name_ptr);
return Ok(std::move(name));
}
Result<std::string, FfiError> NotificationProxy::receive_notification_with_timeout(u_int64_t interval) {
char* name_ptr = nullptr;
FfiError e(::notification_proxy_receive_with_timeout(handle_.get(), interval, &name_ptr));
if (e) {
return Err(e);
}
std::string name(name_ptr);
::notification_proxy_free_string(name_ptr);
return Ok(std::move(name));
}
} // namespace IdeviceFFI

View File

@@ -34,6 +34,7 @@ debug_proxy = ["idevice/debug_proxy"]
diagnostics_relay = ["idevice/diagnostics_relay"] diagnostics_relay = ["idevice/diagnostics_relay"]
dvt = ["idevice/dvt"] dvt = ["idevice/dvt"]
heartbeat = ["idevice/heartbeat"] heartbeat = ["idevice/heartbeat"]
notification_proxy = ["idevice/notification_proxy"]
house_arrest = ["idevice/house_arrest"] house_arrest = ["idevice/house_arrest"]
installation_proxy = ["idevice/installation_proxy"] installation_proxy = ["idevice/installation_proxy"]
springboardservices = ["idevice/springboardservices"] springboardservices = ["idevice/springboardservices"]
@@ -50,6 +51,7 @@ tss = ["idevice/tss"]
tunneld = ["idevice/tunneld"] tunneld = ["idevice/tunneld"]
usbmuxd = ["idevice/usbmuxd"] usbmuxd = ["idevice/usbmuxd"]
xpc = ["idevice/xpc"] xpc = ["idevice/xpc"]
screenshotr = ["idevice/screenshotr"]
full = [ full = [
"afc", "afc",
"amfi", "amfi",
@@ -60,6 +62,7 @@ full = [
"diagnostics_relay", "diagnostics_relay",
"dvt", "dvt",
"heartbeat", "heartbeat",
"notification_proxy",
"house_arrest", "house_arrest",
"installation_proxy", "installation_proxy",
"misagent", "misagent",
@@ -75,6 +78,7 @@ full = [
"tunneld", "tunneld",
"springboardservices", "springboardservices",
"syslog_relay", "syslog_relay",
"screenshotr",
] ]
default = ["full", "aws-lc"] default = ["full", "aws-lc"]

View File

@@ -35,14 +35,20 @@ fn main() {
.expect("Unable to generate bindings") .expect("Unable to generate bindings")
.write_to_file("idevice.h"); .write_to_file("idevice.h");
// download plist.h // Check if plist.h exists locally first, otherwise download
let h = ureq::get("https://raw.githubusercontent.com/libimobiledevice/libplist/refs/heads/master/include/plist/plist.h") let plist_h_path = "plist.h";
.call() let h = if std::path::Path::new(plist_h_path).exists() {
.expect("failed to download plist.h"); std::fs::read_to_string(plist_h_path).expect("failed to read plist.h")
let h = h } else {
.into_body() // download plist.h
.read_to_string() let h = ureq::get("https://raw.githubusercontent.com/libimobiledevice/libplist/refs/heads/master/include/plist/plist.h")
.expect("failed to get string content"); .call()
.expect("failed to download plist.h");
h.into_body()
.read_to_string()
.expect("failed to get string content")
};
let mut f = OpenOptions::new().append(true).open("idevice.h").unwrap(); let mut f = OpenOptions::new().append(true).open("idevice.h").unwrap();
f.write_all(b"\n\n\n").unwrap(); f.write_all(b"\n\n\n").unwrap();
f.write_all(&h.into_bytes()) f.write_all(&h.into_bytes())

View File

@@ -254,7 +254,7 @@ int main(int argc, char **argv) {
} else { } else {
uint8_t *data = NULL; uint8_t *data = NULL;
size_t length = 0; size_t length = 0;
err = afc_file_read(file, &data, &length); err = afc_file_read_entire(file, &data, &length);
if (err == NULL) { if (err == NULL) {
if (write_file(dest_path, data, length)) { if (write_file(dest_path, data, length)) {
printf("File downloaded successfully\n"); printf("File downloaded successfully\n");

View File

@@ -1,12 +1,13 @@
// Jackson Coxson // Jackson Coxson
use std::ptr::null_mut; use std::{io::SeekFrom, ptr::null_mut};
use idevice::{ use idevice::{
IdeviceError, IdeviceService, IdeviceError, IdeviceService,
afc::{AfcClient, DeviceInfo, FileInfo}, afc::{AfcClient, DeviceInfo, FileInfo, file::FileDescriptor},
provider::IdeviceProvider, provider::IdeviceProvider,
}; };
use tokio::io::{AsyncReadExt, AsyncSeekExt};
use crate::{ use crate::{
IdeviceFfiError, IdeviceHandle, LOCAL_RUNTIME, ffi_err, provider::IdeviceProviderHandle, IdeviceFfiError, IdeviceHandle, LOCAL_RUNTIME, ffi_err, provider::IdeviceProviderHandle,
@@ -53,6 +54,44 @@ pub unsafe extern "C" fn afc_client_connect(
} }
} }
/// Connects to the AFC2 service using a TCP provider
///
/// # Arguments
/// * [`provider`] - An IdeviceProvider
/// * [`client`] - On success, will be set to point to a newly allocated AfcClient handle
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # 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 afc2_client_connect(
provider: *mut IdeviceProviderHandle,
client: *mut *mut AfcClientHandle,
) -> *mut IdeviceFfiError {
if provider.is_null() || client.is_null() {
tracing::error!("Null pointer provided");
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let res = run_sync_local(async {
let provider_ref: &dyn IdeviceProvider = unsafe { &*(*provider).0 };
AfcClient::new_afc2(provider_ref).await
});
match res {
Ok(r) => {
let boxed = Box::new(AfcClientHandle(r));
unsafe { *client = Box::into_raw(boxed) };
null_mut()
}
Err(e) => ffi_err!(e),
}
}
/// Creates a new AfcClient from an existing Idevice connection /// Creates a new AfcClient from an existing Idevice connection
/// ///
/// # Arguments /// # Arguments
@@ -555,12 +594,13 @@ pub unsafe extern "C" fn afc_file_close(handle: *mut AfcFileHandle) -> *mut Idev
} }
} }
/// Reads data from an open file /// Reads data from an open file. This advances the cursor of the file.
/// ///
/// # Arguments /// # Arguments
/// * [`handle`] - File handle to read from /// * [`handle`] - File handle to read from
/// * [`data`] - Will be set to point to the read data /// * [`data`] - Will be set to point to the read data
/// * [`length`] - Will be set to the length of the read data /// * [`len`] - Number of bytes to read from the file
/// * [`bytes_read`] - The number of bytes read from the file
/// ///
/// # Returns /// # Returns
/// An IdeviceFfiError on error, null on success /// An IdeviceFfiError on error, null on success
@@ -569,6 +609,53 @@ pub unsafe extern "C" fn afc_file_close(handle: *mut AfcFileHandle) -> *mut Idev
/// All pointers must be valid and non-null /// All pointers must be valid and non-null
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub unsafe extern "C" fn afc_file_read( pub unsafe extern "C" fn afc_file_read(
handle: *mut AfcFileHandle,
data: *mut *mut u8,
len: usize,
bytes_read: *mut libc::size_t,
) -> *mut IdeviceFfiError {
if handle.is_null() || data.is_null() || bytes_read.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let fd = unsafe { &mut *(handle as *mut FileDescriptor) };
let res: Result<Vec<u8>, IdeviceError> = run_sync({
let mut buf = vec![0u8; len];
async move {
let r = fd.read(&mut buf).await?;
buf.truncate(r);
Ok(buf)
}
});
match res {
Ok(bytes) => {
let mut boxed = bytes.into_boxed_slice();
unsafe {
*data = boxed.as_mut_ptr();
*bytes_read = boxed.len();
}
std::mem::forget(boxed);
null_mut()
}
Err(e) => ffi_err!(e),
}
}
/// Reads all data from an open file.
///
/// # Arguments
/// * [`handle`] - File handle to read from
/// * [`data`] - Will be set to point to the read data
/// * [`length`] - The number of bytes read from the file
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// All pointers must be valid and non-null
#[unsafe(no_mangle)]
pub unsafe extern "C" fn afc_file_read_entire(
handle: *mut AfcFileHandle, handle: *mut AfcFileHandle,
data: *mut *mut u8, data: *mut *mut u8,
length: *mut libc::size_t, length: *mut libc::size_t,
@@ -594,6 +681,100 @@ pub unsafe extern "C" fn afc_file_read(
} }
} }
/// Moves the read/write cursor in an open file.
///
/// # Arguments
/// * [`handle`] - File handle whose cursor should be moved
/// * [`offset`] - Distance to move the cursor, interpreted based on `whence`
/// * [`whence`] - Origin used for the seek operation:
/// * `0` — Seek from the start of the file (`SeekFrom::Start`)
/// * `1` — Seek from the current cursor position (`SeekFrom::Current`)
/// * `2` — Seek from the end of the file (`SeekFrom::End`)
/// * [`new_pos`] - Output parameter; will be set to the new absolute cursor position
///
/// # Returns
/// An [`IdeviceFfiError`] on error, or null on success.
///
/// # Safety
/// All pointers must be valid and non-null.
///
/// # Notes
/// * If `whence` is invalid, this function returns `FfiInvalidArg`.
/// * The AFC protocol may restrict seeking beyond certain bounds; such errors
/// are reported through the returned [`IdeviceFfiError`].
#[unsafe(no_mangle)]
pub unsafe extern "C" fn afc_file_seek(
handle: *mut AfcFileHandle,
offset: i64,
whence: libc::c_int,
new_pos: *mut i64,
) -> *mut IdeviceFfiError {
if handle.is_null() || new_pos.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let fd = unsafe { &mut *(handle as *mut FileDescriptor) };
let seek_from = match whence {
0 => SeekFrom::Start(offset as u64),
1 => SeekFrom::Current(offset),
2 => SeekFrom::End(offset),
_ => return ffi_err!(IdeviceError::FfiInvalidArg),
};
let res: Result<u64, IdeviceError> = run_sync(async move { Ok(fd.seek(seek_from).await?) });
match res {
Ok(pos) => {
unsafe {
*new_pos = pos as i64;
}
null_mut()
}
Err(e) => ffi_err!(e),
}
}
/// Returns the current read/write cursor position of an open file.
///
/// # Arguments
/// * [`handle`] - File handle whose cursor should be queried
/// * [`pos`] - Output parameter; will be set to the current absolute cursor position
///
/// # Returns
/// An [`IdeviceFfiError`] on error, or null on success.
///
/// # Safety
/// All pointers must be valid and non-null.
///
/// # Notes
/// This function is equivalent to performing a seek operation with
/// `SeekFrom::Current(0)` internally.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn afc_file_tell(
handle: *mut AfcFileHandle,
pos: *mut i64,
) -> *mut IdeviceFfiError {
if handle.is_null() || pos.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let fd = unsafe { &mut *(handle as *mut FileDescriptor) };
let res: Result<u64, IdeviceError> =
run_sync(async { Ok(fd.seek(SeekFrom::Current(0)).await?) });
match res {
Ok(cur) => {
unsafe {
*pos = cur as i64;
}
null_mut()
}
Err(e) => ffi_err!(e),
}
}
/// Writes data to an open file /// Writes data to an open file
/// ///
/// # Arguments /// # Arguments

View File

@@ -190,7 +190,7 @@ pub unsafe extern "C" fn diagnostics_relay_client_mobilegestalt(
return ffi_err!(IdeviceError::FfiInvalidArg); return ffi_err!(IdeviceError::FfiInvalidArg);
} }
let keys = if keys.is_null() { let keys = if !keys.is_null() {
let keys = unsafe { std::slice::from_raw_parts(keys, keys_len) }; let keys = unsafe { std::slice::from_raw_parts(keys, keys_len) };
Some( Some(
keys.iter() keys.iter()

183
ffi/src/house_arrest.rs Normal file
View File

@@ -0,0 +1,183 @@
// Jackson Coxson
use std::{
ffi::{CStr, c_char},
ptr::null_mut,
};
use idevice::{
IdeviceError, IdeviceService, afc::AfcClient, house_arrest::HouseArrestClient,
provider::IdeviceProvider,
};
use crate::{
IdeviceFfiError, IdeviceHandle, afc::AfcClientHandle, ffi_err, provider::IdeviceProviderHandle,
run_sync_local,
};
pub struct HouseArrestClientHandle(pub HouseArrestClient);
/// Connects to the House Arrest service using a TCP provider
///
/// # Arguments
/// * [`provider`] - An IdeviceProvider
/// * [`client`] - On success, will be set to point to a newly allocated HouseArrestClient handle
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # 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 house_arrest_client_connect(
provider: *mut IdeviceProviderHandle,
client: *mut *mut HouseArrestClientHandle,
) -> *mut IdeviceFfiError {
if provider.is_null() || client.is_null() {
tracing::error!("Null pointer provided");
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let res = run_sync_local(async {
let provider_ref: &dyn IdeviceProvider = unsafe { &*(*provider).0 };
HouseArrestClient::connect(provider_ref).await
});
match res {
Ok(r) => {
let boxed = Box::new(HouseArrestClientHandle(r));
unsafe { *client = Box::into_raw(boxed) };
null_mut()
}
Err(e) => ffi_err!(e),
}
}
/// Creates a new HouseArrestClient from an existing Idevice connection
///
/// # Arguments
/// * [`socket`] - An IdeviceSocket handle
/// * [`client`] - On success, will be set to point to a newly allocated HouseArrestClient handle
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # 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 house_arrest_client_new(
socket: *mut IdeviceHandle,
client: *mut *mut HouseArrestClientHandle,
) -> *mut IdeviceFfiError {
if socket.is_null() || client.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let socket = unsafe { Box::from_raw(socket) }.0;
let r = HouseArrestClient::new(socket);
let boxed = Box::new(HouseArrestClientHandle(r));
unsafe { *client = Box::into_raw(boxed) };
null_mut()
}
/// Vends a container for an app
///
/// # Arguments
/// * [`client`] - The House Arrest client
/// * [`bundle_id`] - The bundle ID to vend for
/// * [`afc_client`] - The new AFC client for the underlying connection
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `client` must be a allocated by this library
/// `bundle_id` must be a NULL-terminated string
/// `afc_client` must be a valid, non-null pointer where the new AFC client will be stored
#[unsafe(no_mangle)]
pub unsafe extern "C" fn house_arrest_vend_container(
client: *mut HouseArrestClientHandle,
bundle_id: *const c_char,
afc_client: *mut *mut AfcClientHandle,
) -> *mut IdeviceFfiError {
if client.is_null() || bundle_id.is_null() || afc_client.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let bundle_id = unsafe { CStr::from_ptr(bundle_id) }.to_string_lossy();
let client_ref = unsafe { Box::from_raw(client) }.0; // take ownership and drop
let res: Result<AfcClient, IdeviceError> =
run_sync_local(async move { client_ref.vend_container(bundle_id).await });
match res {
Ok(a) => {
let a = Box::into_raw(Box::new(AfcClientHandle(a)));
unsafe { *afc_client = a };
null_mut()
}
Err(e) => {
ffi_err!(e)
}
}
}
/// Vends documents for an app
///
/// # Arguments
/// * [`client`] - The House Arrest client
/// * [`bundle_id`] - The bundle ID to vend for
/// * [`afc_client`] - The new AFC client for the underlying connection
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `client` must be a allocated by this library
/// `bundle_id` must be a NULL-terminated string
/// `afc_client` must be a valid, non-null pointer where the new AFC client will be stored
#[unsafe(no_mangle)]
pub unsafe extern "C" fn house_arrest_vend_documents(
client: *mut HouseArrestClientHandle,
bundle_id: *const c_char,
afc_client: *mut *mut AfcClientHandle,
) -> *mut IdeviceFfiError {
if client.is_null() || bundle_id.is_null() || afc_client.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let bundle_id = unsafe { CStr::from_ptr(bundle_id) }.to_string_lossy();
let client_ref = unsafe { Box::from_raw(client) }.0; // take ownership and drop
let res: Result<AfcClient, IdeviceError> =
run_sync_local(async move { client_ref.vend_documents(bundle_id).await });
match res {
Ok(a) => {
let a = Box::into_raw(Box::new(AfcClientHandle(a)));
unsafe { *afc_client = a };
null_mut()
}
Err(e) => {
ffi_err!(e)
}
}
}
/// Frees an HouseArrestClient 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 house_arrest_client_free(handle: *mut HouseArrestClientHandle) {
if !handle.is_null() {
tracing::debug!("Freeing house_arrest_client");
let _ = unsafe { Box::from_raw(handle) };
}
}

View File

@@ -19,6 +19,8 @@ pub mod dvt;
mod errors; mod errors;
#[cfg(feature = "heartbeat")] #[cfg(feature = "heartbeat")]
pub mod heartbeat; pub mod heartbeat;
#[cfg(feature = "house_arrest")]
pub mod house_arrest;
#[cfg(feature = "installation_proxy")] #[cfg(feature = "installation_proxy")]
pub mod installation_proxy; pub mod installation_proxy;
pub mod lockdown; pub mod lockdown;
@@ -27,12 +29,16 @@ pub mod logging;
pub mod misagent; pub mod misagent;
#[cfg(feature = "mobile_image_mounter")] #[cfg(feature = "mobile_image_mounter")]
pub mod mobile_image_mounter; pub mod mobile_image_mounter;
#[cfg(feature = "notification_proxy")]
pub mod notification_proxy;
#[cfg(feature = "syslog_relay")] #[cfg(feature = "syslog_relay")]
pub mod os_trace_relay; pub mod os_trace_relay;
mod pairing_file; mod pairing_file;
pub mod provider; pub mod provider;
#[cfg(feature = "xpc")] #[cfg(feature = "xpc")]
pub mod rsd; pub mod rsd;
#[cfg(feature = "screenshotr")]
pub mod screenshotr;
#[cfg(feature = "springboardservices")] #[cfg(feature = "springboardservices")]
pub mod springboardservices; pub mod springboardservices;
#[cfg(feature = "syslog_relay")] #[cfg(feature = "syslog_relay")]

View File

@@ -179,7 +179,7 @@ pub unsafe extern "C" fn lockdownd_get_value(
domain: *const libc::c_char, domain: *const libc::c_char,
out_plist: *mut plist_t, out_plist: *mut plist_t,
) -> *mut IdeviceFfiError { ) -> *mut IdeviceFfiError {
if out_plist.is_null() { if client.is_null() || out_plist.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg); return ffi_err!(IdeviceError::FfiInvalidArg);
} }
@@ -221,6 +221,89 @@ pub unsafe extern "C" fn lockdownd_get_value(
} }
} }
/// Tells the device to enter recovery mode
///
/// # Arguments
/// * `client` - A valid LockdowndClient handle
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `client` must be a valid pointer to a handle allocated by this library
#[unsafe(no_mangle)]
pub unsafe extern "C" fn lockdownd_enter_recovery(
client: *mut LockdowndClientHandle,
) -> *mut IdeviceFfiError {
if client.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let res: Result<(), IdeviceError> = run_sync_local(async move {
let client_ref = unsafe { &mut (*client).0 };
client_ref.enter_recovery().await
});
match res {
Ok(_) => null_mut(),
Err(e) => ffi_err!(e),
}
}
/// Sets a value in lockdownd
///
/// # Arguments
/// * `client` - A valid LockdowndClient handle
/// * `key` - The key to set (null-terminated string)
/// * `value` - The value to set as a plist
/// * `domain` - The domain to set in (null-terminated string, optional)
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `client` must be a valid pointer to a handle allocated by this library
/// `key` must be a valid null-terminated string
/// `value` must be a valid plist
/// `domain` must be a valid null-terminated string or NULL
#[unsafe(no_mangle)]
pub unsafe extern "C" fn lockdownd_set_value(
client: *mut LockdowndClientHandle,
key: *const libc::c_char,
value: plist_t,
domain: *const libc::c_char,
) -> *mut IdeviceFfiError {
if client.is_null() || key.is_null() || value.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let key = match unsafe { std::ffi::CStr::from_ptr(key) }.to_str() {
Ok(k) => k,
Err(_) => return ffi_err!(IdeviceError::InvalidCString),
};
let domain = if domain.is_null() {
None
} else {
Some(match unsafe { std::ffi::CStr::from_ptr(domain) }.to_str() {
Ok(d) => d,
Err(_) => return ffi_err!(IdeviceError::InvalidCString),
})
};
let value = unsafe { &mut *value }.borrow_self().clone();
let res: Result<(), IdeviceError> = run_sync_local(async move {
let client_ref = unsafe { &mut (*client).0 };
client_ref.set_value(key, value, domain).await
});
match res {
Ok(_) => null_mut(),
Err(e) => ffi_err!(e),
}
}
/// Frees a LockdowndClient handle /// Frees a LockdowndClient handle
/// ///
/// # Arguments /// # Arguments

View File

@@ -0,0 +1,311 @@
// Jackson Coxson
use std::ffi::{CStr, CString, c_char};
use std::ptr::null_mut;
use idevice::{
IdeviceError, IdeviceService, notification_proxy::NotificationProxyClient,
provider::IdeviceProvider,
};
use crate::{
IdeviceFfiError, IdeviceHandle, ffi_err, provider::IdeviceProviderHandle, run_sync_local,
};
pub struct NotificationProxyClientHandle(pub NotificationProxyClient);
/// Automatically creates and connects to Notification Proxy, returning a client handle
///
/// # Arguments
/// * [`provider`] - An IdeviceProvider
/// * [`client`] - On success, will be set to point to a newly allocated NotificationProxyClient handle
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # 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 notification_proxy_connect(
provider: *mut IdeviceProviderHandle,
client: *mut *mut NotificationProxyClientHandle,
) -> *mut IdeviceFfiError {
if provider.is_null() || client.is_null() {
tracing::error!("Null pointer provided");
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let res: Result<NotificationProxyClient, IdeviceError> = run_sync_local(async move {
let provider_ref: &dyn IdeviceProvider = unsafe { &*(*provider).0 };
NotificationProxyClient::connect(provider_ref).await
});
match res {
Ok(r) => {
let boxed = Box::new(NotificationProxyClientHandle(r));
unsafe { *client = Box::into_raw(boxed) };
null_mut()
}
Err(e) => {
ffi_err!(e)
}
}
}
/// Creates a new NotificationProxyClient from an existing Idevice connection
///
/// # Arguments
/// * [`socket`] - An IdeviceSocket handle
/// * [`client`] - On success, will be set to point to a newly allocated NotificationProxyClient handle
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `socket` must be a valid pointer to a handle allocated by this library. The socket is consumed,
/// and may not be used again.
/// `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 notification_proxy_new(
socket: *mut IdeviceHandle,
client: *mut *mut NotificationProxyClientHandle,
) -> *mut IdeviceFfiError {
if socket.is_null() || client.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let socket = unsafe { Box::from_raw(socket) }.0;
let r = NotificationProxyClient::new(socket);
let boxed = Box::new(NotificationProxyClientHandle(r));
unsafe { *client = Box::into_raw(boxed) };
null_mut()
}
/// Posts a notification to the device
///
/// # Arguments
/// * `client` - A valid NotificationProxyClient handle
/// * `name` - C string containing the notification name
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `client` must be a valid pointer to a handle allocated by this library
/// `name` must be a valid null-terminated C string
#[unsafe(no_mangle)]
pub unsafe extern "C" fn notification_proxy_post(
client: *mut NotificationProxyClientHandle,
name: *const c_char,
) -> *mut IdeviceFfiError {
if client.is_null() || name.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let name_str = match unsafe { CStr::from_ptr(name) }.to_str() {
Ok(s) => s.to_string(),
Err(_) => return ffi_err!(IdeviceError::FfiInvalidString),
};
let res: Result<(), IdeviceError> = run_sync_local(async move {
let client_ref = unsafe { &mut (*client).0 };
client_ref.post_notification(name_str).await
});
match res {
Ok(_) => null_mut(),
Err(e) => ffi_err!(e),
}
}
/// Observes a specific notification
///
/// # Arguments
/// * `client` - A valid NotificationProxyClient handle
/// * `name` - C string containing the notification name to observe
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `client` must be a valid pointer to a handle allocated by this library
/// `name` must be a valid null-terminated C string
#[unsafe(no_mangle)]
pub unsafe extern "C" fn notification_proxy_observe(
client: *mut NotificationProxyClientHandle,
name: *const c_char,
) -> *mut IdeviceFfiError {
if client.is_null() || name.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let name_str = match unsafe { CStr::from_ptr(name) }.to_str() {
Ok(s) => s.to_string(),
Err(_) => return ffi_err!(IdeviceError::FfiInvalidString),
};
let res: Result<(), IdeviceError> = run_sync_local(async move {
let client_ref = unsafe { &mut (*client).0 };
client_ref.observe_notification(name_str).await
});
match res {
Ok(_) => null_mut(),
Err(e) => ffi_err!(e),
}
}
/// Observes multiple notifications at once
///
/// # Arguments
/// * `client` - A valid NotificationProxyClient handle
/// * `names` - A null-terminated array of C strings containing notification names
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `client` must be a valid pointer to a handle allocated by this library
/// `names` must be a valid pointer to a null-terminated array of null-terminated C strings
#[unsafe(no_mangle)]
pub unsafe extern "C" fn notification_proxy_observe_multiple(
client: *mut NotificationProxyClientHandle,
names: *const *const c_char,
) -> *mut IdeviceFfiError {
if client.is_null() || names.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let mut notification_names: Vec<String> = Vec::new();
let mut i = 0;
loop {
let ptr = unsafe { *names.add(i) };
if ptr.is_null() {
break;
}
match unsafe { CStr::from_ptr(ptr) }.to_str() {
Ok(s) => notification_names.push(s.to_string()),
Err(_) => return ffi_err!(IdeviceError::FfiInvalidString),
}
i += 1;
}
let refs: Vec<&str> = notification_names.iter().map(|s| s.as_str()).collect();
let res: Result<(), IdeviceError> = run_sync_local(async move {
let client_ref = unsafe { &mut (*client).0 };
client_ref.observe_notifications(&refs).await
});
match res {
Ok(_) => null_mut(),
Err(e) => ffi_err!(e),
}
}
/// Receives the next notification from the device
///
/// # Arguments
/// * `client` - A valid NotificationProxyClient handle
/// * `name_out` - On success, will be set to a newly allocated C string containing the notification name
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `client` must be a valid pointer to a handle allocated by this library
/// `name_out` must be a valid pointer. The returned string must be freed with `notification_proxy_free_string`
#[unsafe(no_mangle)]
pub unsafe extern "C" fn notification_proxy_receive(
client: *mut NotificationProxyClientHandle,
name_out: *mut *mut c_char,
) -> *mut IdeviceFfiError {
if client.is_null() || name_out.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let res: Result<String, IdeviceError> = run_sync_local(async move {
let client_ref = unsafe { &mut (*client).0 };
client_ref.receive_notification().await
});
match res {
Ok(name) => match CString::new(name) {
Ok(c_string) => {
unsafe { *name_out = c_string.into_raw() };
null_mut()
}
Err(_) => ffi_err!(IdeviceError::FfiInvalidString),
},
Err(e) => ffi_err!(e),
}
}
/// Receives the next notification with a timeout
///
/// # Arguments
/// * `client` - A valid NotificationProxyClient handle
/// * `interval` - Timeout in seconds to wait for a notification
/// * `name_out` - On success, will be set to a newly allocated C string containing the notification name
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `client` must be a valid pointer to a handle allocated by this library
/// `name_out` must be a valid pointer. The returned string must be freed with `notification_proxy_free_string`
#[unsafe(no_mangle)]
pub unsafe extern "C" fn notification_proxy_receive_with_timeout(
client: *mut NotificationProxyClientHandle,
interval: u64,
name_out: *mut *mut c_char,
) -> *mut IdeviceFfiError {
if client.is_null() || name_out.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let res: Result<String, IdeviceError> = run_sync_local(async move {
let client_ref = unsafe { &mut (*client).0 };
client_ref.receive_notification_with_timeout(interval).await
});
match res {
Ok(name) => match CString::new(name) {
Ok(c_string) => {
unsafe { *name_out = c_string.into_raw() };
null_mut()
}
Err(_) => ffi_err!(IdeviceError::FfiInvalidString),
},
Err(e) => ffi_err!(e),
}
}
/// Frees a string returned by notification_proxy_receive
///
/// # Safety
/// `s` must be a valid pointer returned from `notification_proxy_receive`
#[unsafe(no_mangle)]
pub unsafe extern "C" fn notification_proxy_free_string(s: *mut c_char) {
if !s.is_null() {
let _ = unsafe { CString::from_raw(s) };
}
}
/// 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 notification_proxy_client_free(
handle: *mut NotificationProxyClientHandle,
) {
if !handle.is_null() {
tracing::debug!("Freeing notification_proxy_client");
let _ = unsafe { Box::from_raw(handle) };
}
}

132
ffi/src/screenshotr.rs Normal file
View File

@@ -0,0 +1,132 @@
// Jackson Coxson
use std::ptr::null_mut;
use idevice::{
IdeviceError, IdeviceService, provider::IdeviceProvider,
services::screenshotr::ScreenshotService,
};
use crate::{IdeviceFfiError, ffi_err, provider::IdeviceProviderHandle, run_sync_local};
pub struct ScreenshotrClientHandle(pub ScreenshotService);
/// Represents a screenshot data buffer
#[repr(C)]
pub struct ScreenshotData {
pub data: *mut u8,
pub length: usize,
}
/// Connects to screenshotr service using provider
///
/// # Arguments
/// * [`provider`] - An IdeviceProvider
/// * [`client`] - On success, will be set to point to a newly allocated ScreenshotrClient handle
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # 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 screenshotr_connect(
provider: *mut IdeviceProviderHandle,
client: *mut *mut ScreenshotrClientHandle,
) -> *mut IdeviceFfiError {
if provider.is_null() || client.is_null() {
tracing::error!("Null pointer provided");
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let res: Result<ScreenshotService, IdeviceError> = run_sync_local(async move {
let provider_ref: &dyn IdeviceProvider = unsafe { &*(*provider).0 };
ScreenshotService::connect(provider_ref).await
});
match res {
Ok(r) => {
let boxed = Box::new(ScreenshotrClientHandle(r));
unsafe { *client = Box::into_raw(boxed) };
null_mut()
}
Err(e) => ffi_err!(e),
}
}
/// Takes a screenshot from the device
///
/// # Arguments
/// * `client` - A valid ScreenshotrClient handle
/// * `screenshot` - Pointer to store the screenshot data
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `client` must be a valid pointer to a handle allocated by this library
/// `screenshot` must be a valid pointer to store the screenshot data
/// The caller is responsible for freeing the screenshot data using screenshotr_screenshot_free
#[unsafe(no_mangle)]
pub unsafe extern "C" fn screenshotr_take_screenshot(
client: *mut ScreenshotrClientHandle,
screenshot: *mut ScreenshotData,
) -> *mut IdeviceFfiError {
if client.is_null() || screenshot.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let res: Result<Vec<u8>, IdeviceError> = run_sync_local(async move {
let client_ref = unsafe { &mut (*client).0 };
client_ref.take_screenshot().await
});
match res {
Ok(data) => {
let len = data.len();
let boxed = data.into_boxed_slice();
let ptr = Box::into_raw(boxed) as *mut u8;
unsafe {
(*screenshot).data = ptr;
(*screenshot).length = len;
}
null_mut()
}
Err(e) => ffi_err!(e),
}
}
/// Frees screenshot data
///
/// # Arguments
/// * `screenshot` - The screenshot data to free
///
/// # Safety
/// `screenshot` must be a valid ScreenshotData that was allocated by screenshotr_take_screenshot
/// or NULL (in which case this function does nothing)
#[unsafe(no_mangle)]
pub unsafe extern "C" fn screenshotr_screenshot_free(screenshot: ScreenshotData) {
if !screenshot.data.is_null() && screenshot.length > 0 {
tracing::debug!("Freeing screenshot data");
let _ =
unsafe { Vec::from_raw_parts(screenshot.data, screenshot.length, screenshot.length) };
}
}
/// Frees a ScreenshotrClient 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 screenshotr_client_free(handle: *mut ScreenshotrClientHandle) {
if !handle.is_null() {
tracing::debug!("Freeing screenshotr_client");
let _ = unsafe { Box::from_raw(handle) };
}
}

View File

@@ -7,6 +7,7 @@ use idevice::{
IdeviceError, IdeviceService, provider::IdeviceProvider, IdeviceError, IdeviceService, provider::IdeviceProvider,
springboardservices::SpringBoardServicesClient, springboardservices::SpringBoardServicesClient,
}; };
use plist_ffi::plist_t;
use crate::{ use crate::{
IdeviceFfiError, IdeviceHandle, ffi_err, provider::IdeviceProviderHandle, run_sync, IdeviceFfiError, IdeviceHandle, ffi_err, provider::IdeviceProviderHandle, run_sync,
@@ -137,6 +138,169 @@ pub unsafe extern "C" fn springboard_services_get_icon(
} }
} }
/// Gets the home screen wallpaper preview as PNG image
///
/// # Arguments
/// * `client` - A valid SpringBoardServicesClient handle
/// * `out_result` - On success, will be set to point to newly allocated png image
/// * `out_result_len` - On success, will contain the size of the data in bytes
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `client` must be a valid pointer to a handle allocated by this library
/// `out_result` and `out_result_len` must be valid, non-null pointers
#[unsafe(no_mangle)]
pub unsafe extern "C" fn springboard_services_get_home_screen_wallpaper_preview(
client: *mut SpringBoardServicesClientHandle,
out_result: *mut *mut c_void,
out_result_len: *mut libc::size_t,
) -> *mut IdeviceFfiError {
if client.is_null() || out_result.is_null() || out_result_len.is_null() {
tracing::error!("Invalid arguments: {client:?}, {out_result:?}");
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let client = unsafe { &mut *client };
let res: Result<Vec<u8>, IdeviceError> =
run_sync(async { client.0.get_home_screen_wallpaper_preview_pngdata().await });
match res {
Ok(r) => {
let len = r.len();
let boxed_slice = r.into_boxed_slice();
let ptr = boxed_slice.as_ptr();
std::mem::forget(boxed_slice);
unsafe {
*out_result = ptr as *mut c_void;
*out_result_len = len;
}
null_mut()
}
Err(e) => ffi_err!(e),
}
}
/// Gets the lock screen wallpaper preview as PNG image
///
/// # Arguments
/// * `client` - A valid SpringBoardServicesClient handle
/// * `out_result` - On success, will be set to point to newly allocated png image
/// * `out_result_len` - On success, will contain the size of the data in bytes
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `client` must be a valid pointer to a handle allocated by this library
/// `out_result` and `out_result_len` must be valid, non-null pointers
#[unsafe(no_mangle)]
pub unsafe extern "C" fn springboard_services_get_lock_screen_wallpaper_preview(
client: *mut SpringBoardServicesClientHandle,
out_result: *mut *mut c_void,
out_result_len: *mut libc::size_t,
) -> *mut IdeviceFfiError {
if client.is_null() || out_result.is_null() || out_result_len.is_null() {
tracing::error!("Invalid arguments: {client:?}, {out_result:?}");
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let client = unsafe { &mut *client };
let res: Result<Vec<u8>, IdeviceError> =
run_sync(async { client.0.get_lock_screen_wallpaper_preview_pngdata().await });
match res {
Ok(r) => {
let len = r.len();
let boxed_slice = r.into_boxed_slice();
let ptr = boxed_slice.as_ptr();
std::mem::forget(boxed_slice);
unsafe {
*out_result = ptr as *mut c_void;
*out_result_len = len;
}
null_mut()
}
Err(e) => ffi_err!(e),
}
}
/// Gets the current interface orientation of the device
///
/// # Arguments
/// * `client` - A valid SpringBoardServicesClient handle
/// * `out_orientation` - On success, will contain the orientation value (0-4)
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `client` must be a valid pointer to a handle allocated by this library
/// `out_orientation` must be a valid, non-null pointer
#[unsafe(no_mangle)]
pub unsafe extern "C" fn springboard_services_get_interface_orientation(
client: *mut SpringBoardServicesClientHandle,
out_orientation: *mut u8,
) -> *mut IdeviceFfiError {
if client.is_null() || out_orientation.is_null() {
tracing::error!("Invalid arguments: {client:?}, {out_orientation:?}");
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let client = unsafe { &mut *client };
let res = run_sync(async { client.0.get_interface_orientation().await });
match res {
Ok(orientation) => {
unsafe {
*out_orientation = orientation as u8;
}
null_mut()
}
Err(e) => ffi_err!(e),
}
}
/// Gets the home screen icon layout metrics
///
/// # Arguments
/// * `client` - A valid SpringBoardServicesClient handle
/// * `res` - On success, will point to a plist dictionary node containing the metrics
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `client` must be a valid pointer to a handle allocated by this library
/// `res` must be a valid, non-null pointer
#[unsafe(no_mangle)]
pub unsafe extern "C" fn springboard_services_get_homescreen_icon_metrics(
client: *mut SpringBoardServicesClientHandle,
res: *mut plist_t,
) -> *mut IdeviceFfiError {
if client.is_null() || res.is_null() {
tracing::error!("Invalid arguments: {client:?}, {res:?}");
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let client = unsafe { &mut *client };
let output = run_sync(async { client.0.get_homescreen_icon_metrics().await });
match output {
Ok(metrics) => {
unsafe {
*res =
plist_ffi::PlistWrapper::new_node(plist::Value::Dictionary(metrics)).into_ptr();
}
null_mut()
}
Err(e) => ffi_err!(e),
}
}
/// Frees an SpringBoardServicesClient handle /// Frees an SpringBoardServicesClient handle
/// ///
/// # Arguments /// # Arguments

View File

@@ -2,7 +2,7 @@
name = "idevice" name = "idevice"
description = "A Rust library to interact with services on iOS devices." description = "A Rust library to interact with services on iOS devices."
authors = ["Jackson Coxson"] authors = ["Jackson Coxson"]
version = "0.1.50" version = "0.1.53"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
documentation = "https://docs.rs/idevice" documentation = "https://docs.rs/idevice"
@@ -21,6 +21,7 @@ tokio-openssl = { version = "0.6", optional = true }
openssl = { version = "0.10", optional = true } openssl = { version = "0.10", optional = true }
plist = { version = "1.8" } plist = { version = "1.8" }
plist-macro = { version = "0.1.3" }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
ns-keyed-archive = { version = "0.1.4", optional = true } ns-keyed-archive = { version = "0.1.4", optional = true }
crossfire = { version = "2.1", optional = true } crossfire = { version = "2.1", optional = true }
@@ -40,7 +41,7 @@ json = { version = "0.12", optional = true }
byteorder = { version = "1.5", optional = true } byteorder = { version = "1.5", optional = true }
bytes = { version = "1.10", optional = true } bytes = { version = "1.10", optional = true }
reqwest = { version = "0.12", features = [ reqwest = { version = "0.13", features = [
"json", "json",
], optional = true, default-features = false } ], optional = true, default-features = false }
rand = { version = "0.9", optional = true } rand = { version = "0.9", optional = true }
@@ -76,7 +77,7 @@ ring = ["rustls", "rustls/ring", "tokio-rustls/ring"]
rustls = ["dep:rustls", "dep:tokio-rustls"] rustls = ["dep:rustls", "dep:tokio-rustls"]
openssl = ["dep:openssl", "dep:tokio-openssl"] openssl = ["dep:openssl", "dep:tokio-openssl"]
afc = ["dep:chrono"] afc = ["dep:chrono", "dep:futures"]
amfi = [] amfi = []
bt_packet_logger = [] bt_packet_logger = []
companion_proxy = [] companion_proxy = []
@@ -101,6 +102,7 @@ misagent = []
mobile_image_mounter = ["dep:sha2"] mobile_image_mounter = ["dep:sha2"]
mobileactivationd = ["dep:reqwest"] mobileactivationd = ["dep:reqwest"]
mobilebackup2 = [] mobilebackup2 = []
notification_proxy = ["tokio/macros", "tokio/time", "dep:async-stream", "dep:futures"]
location_simulation = [] location_simulation = []
pair = ["chrono/default", "tokio/time", "dep:sha2", "dep:rsa", "dep:x509-cert"] pair = ["chrono/default", "tokio/time", "dep:sha2", "dep:rsa", "dep:x509-cert"]
pcapd = [] pcapd = []
@@ -152,6 +154,7 @@ full = [
"mobile_image_mounter", "mobile_image_mounter",
"mobileactivationd", "mobileactivationd",
"mobilebackup2", "mobilebackup2",
"notification_proxy",
"pair", "pair",
"pcapd", "pcapd",
"preboard_service", "preboard_service",

334
idevice/src/cursor.rs Normal file
View File

@@ -0,0 +1,334 @@
// Jackson Coxson
#[derive(Clone, Debug)]
pub struct Cursor<'a> {
inner: &'a [u8],
pos: usize,
}
impl<'a> Cursor<'a> {
/// Creates a new cursor
pub fn new(inner: &'a [u8]) -> Self {
Self { inner, pos: 0 }
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
pub fn at_end(&self) -> bool {
self.pos == self.inner.len()
}
pub fn read(&mut self, to_read: usize) -> Option<&'a [u8]> {
// Check if the end of the slice (self.pos + to_read) is beyond the buffer length
if self
.pos
.checked_add(to_read)
.is_none_or(|end_pos| end_pos > self.inner.len())
{
return None;
}
// The end of the slice is self.pos + to_read
let end_pos = self.pos + to_read;
let res = Some(&self.inner[self.pos..end_pos]);
self.pos = end_pos;
res
}
pub fn back(&mut self, to_back: usize) {
let to_back = if to_back > self.pos {
self.pos
} else {
to_back
};
self.pos -= to_back;
}
/// True if actually all zeroes
pub fn read_assert_zero(&mut self, to_read: usize) -> Option<()> {
let bytes = self.read(to_read)?;
#[cfg(debug_assertions)]
for b in bytes.iter() {
if *b > 0 {
eprintln!("Zero read contained non-zero values!");
eprintln!("{bytes:02X?}");
return None;
}
}
Some(())
}
pub fn read_to(&mut self, end: usize) -> Option<&'a [u8]> {
if end > self.inner.len() {
return None;
}
let res = Some(&self.inner[self.pos..end]);
self.pos = end;
res
}
pub fn peek_to(&mut self, end: usize) -> Option<&'a [u8]> {
if end > self.inner.len() {
return None;
}
Some(&self.inner[self.pos..end])
}
pub fn peek(&self, to_read: usize) -> Option<&'a [u8]> {
if self
.pos
.checked_add(to_read)
.is_none_or(|end_pos| end_pos > self.inner.len())
{
return None;
}
let end_pos = self.pos + to_read;
Some(&self.inner[self.pos..end_pos])
}
pub fn reveal(&self, surrounding: usize) {
let len = self.inner.len();
if self.pos > len {
println!("Cursor is past end of buffer");
return;
}
let start = self.pos.saturating_sub(surrounding);
let end = (self.pos + surrounding + 1).min(len);
// HEADER
println!("Reveal around pos {} ({} bytes):", self.pos, surrounding);
// --- HEX LINE ---
print!("Hex: ");
for i in start..end {
if i == self.pos {
print!("[{:02X}] ", self.inner[i]);
} else {
print!("{:02X} ", self.inner[i]);
}
}
println!();
// --- ASCII LINE ---
print!("Ascii: ");
for i in start..end {
let b = self.inner[i];
let c = if b.is_ascii_graphic() || b == b' ' {
b as char
} else {
'.'
};
if i == self.pos {
print!("[{}] ", c);
} else {
print!("{} ", c);
}
}
println!();
// --- OFFSET LINE ---
print!("Offset: ");
for i in start..end {
let off = i as isize - self.pos as isize;
if i == self.pos {
print!("[{}] ", off);
} else {
print!("{:<3} ", off);
}
}
println!();
}
pub fn remaining(&mut self) -> &'a [u8] {
let res = &self.inner[self.pos..];
self.pos = self.inner.len();
res
}
pub fn read_u8(&mut self) -> Option<u8> {
if self.pos == self.inner.len() {
return None;
}
let res = Some(self.inner[self.pos]);
self.pos += 1;
res
}
pub fn read_le_u16(&mut self) -> Option<u16> {
const SIZE: usize = 2;
let bytes = self.read(SIZE)?;
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
Some(u16::from_le_bytes(bytes))
}
pub fn read_be_u16(&mut self) -> Option<u16> {
const SIZE: usize = 2;
let bytes = self.read(SIZE)?;
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
Some(u16::from_be_bytes(bytes))
}
pub fn read_le_u32(&mut self) -> Option<u32> {
const SIZE: usize = 4;
let bytes = self.read(SIZE)?;
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
Some(u32::from_le_bytes(bytes))
}
pub fn read_be_u32(&mut self) -> Option<u32> {
const SIZE: usize = 4;
let bytes = self.read(SIZE)?;
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
Some(u32::from_be_bytes(bytes))
}
pub fn read_le_u64(&mut self) -> Option<u64> {
const SIZE: usize = 8;
let bytes = self.read(SIZE)?;
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
Some(u64::from_le_bytes(bytes))
}
pub fn read_be_u64(&mut self) -> Option<u64> {
const SIZE: usize = 8;
let bytes = self.read(SIZE)?;
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
Some(u64::from_be_bytes(bytes))
}
pub fn read_le_u128(&mut self) -> Option<u128> {
const SIZE: usize = 16;
let bytes = self.read(SIZE)?;
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
Some(u128::from_le_bytes(bytes))
}
pub fn read_be_u128(&mut self) -> Option<u128> {
const SIZE: usize = 16;
let bytes = self.read(SIZE)?;
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
Some(u128::from_be_bytes(bytes))
}
pub fn read_le_f32(&mut self) -> Option<f32> {
const SIZE: usize = 4;
let bytes = self.read(SIZE)?;
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
Some(f32::from_le_bytes(bytes))
}
pub fn read_be_f32(&mut self) -> Option<f32> {
const SIZE: usize = 4;
let bytes = self.read(SIZE)?;
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
Some(f32::from_be_bytes(bytes))
}
pub fn read_i8(&mut self) -> Option<i8> {
if self.pos == self.inner.len() {
return None;
}
let res = Some(self.inner[self.pos]).map(|x| x as i8);
self.pos += 1;
res
}
pub fn read_le_i16(&mut self) -> Option<i16> {
const SIZE: usize = 2;
let bytes = self.read(SIZE)?;
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
Some(i16::from_le_bytes(bytes))
}
pub fn read_be_i16(&mut self) -> Option<i16> {
const SIZE: usize = 2;
let bytes = self.read(SIZE)?;
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
Some(i16::from_be_bytes(bytes))
}
pub fn read_le_i32(&mut self) -> Option<i32> {
const SIZE: usize = 4;
let bytes = self.read(SIZE)?;
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
Some(i32::from_le_bytes(bytes))
}
pub fn read_be_i32(&mut self) -> Option<i32> {
const SIZE: usize = 4;
let bytes = self.read(SIZE)?;
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
Some(i32::from_be_bytes(bytes))
}
pub fn read_le_i64(&mut self) -> Option<i64> {
const SIZE: usize = 8;
let bytes = self.read(SIZE)?;
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
Some(i64::from_le_bytes(bytes))
}
pub fn read_be_i64(&mut self) -> Option<i64> {
const SIZE: usize = 8;
let bytes = self.read(SIZE)?;
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
Some(i64::from_be_bytes(bytes))
}
pub fn read_le_i128(&mut self) -> Option<i128> {
const SIZE: usize = 16;
let bytes = self.read(SIZE)?;
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
Some(i128::from_le_bytes(bytes))
}
pub fn read_be_i128(&mut self) -> Option<i128> {
const SIZE: usize = 16;
let bytes = self.read(SIZE)?;
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
Some(i128::from_be_bytes(bytes))
}
pub fn take_2(&mut self) -> Option<[u8; 2]> {
let bytes = self.read(2)?;
Some(bytes.to_owned().try_into().unwrap())
}
pub fn take_3(&mut self) -> Option<[u8; 3]> {
let bytes = self.read(3)?;
Some(bytes.to_owned().try_into().unwrap())
}
pub fn take_4(&mut self) -> Option<[u8; 4]> {
let bytes = self.read(4)?;
Some(bytes.to_owned().try_into().unwrap())
}
pub fn take_8(&mut self) -> Option<[u8; 8]> {
let bytes = self.read(8)?;
Some(bytes.to_owned().try_into().unwrap())
}
pub fn take_20(&mut self) -> Option<[u8; 20]> {
let bytes = self.read(20)?;
Some(bytes.to_owned().try_into().unwrap())
}
pub fn take_32(&mut self) -> Option<[u8; 32]> {
let bytes = self.read(32)?;
Some(bytes.to_owned().try_into().unwrap())
}
}

View File

@@ -5,8 +5,9 @@
#[cfg(all(feature = "pair", feature = "rustls"))] #[cfg(all(feature = "pair", feature = "rustls"))]
mod ca; mod ca;
pub mod cursor;
mod obfuscation;
pub mod pairing_file; pub mod pairing_file;
pub mod plist_macro;
pub mod provider; pub mod provider;
#[cfg(feature = "remote_pairing")] #[cfg(feature = "remote_pairing")]
pub mod remote_pairing; pub mod remote_pairing;
@@ -20,7 +21,6 @@ pub mod tss;
pub mod tunneld; pub mod tunneld;
#[cfg(feature = "usbmuxd")] #[cfg(feature = "usbmuxd")]
pub mod usbmuxd; pub mod usbmuxd;
mod util;
pub mod utils; pub mod utils;
#[cfg(feature = "xpc")] #[cfg(feature = "xpc")]
pub mod xpc; pub mod xpc;
@@ -31,6 +31,7 @@ pub use services::*;
#[cfg(feature = "xpc")] #[cfg(feature = "xpc")]
pub use xpc::RemoteXpcClient; pub use xpc::RemoteXpcClient;
use plist_macro::{plist, pretty_print_dictionary, pretty_print_plist};
use provider::{IdeviceProvider, RsdProvider}; use provider::{IdeviceProvider, RsdProvider};
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
use rustls::{crypto::CryptoProvider, pki_types::ServerName}; use rustls::{crypto::CryptoProvider, pki_types::ServerName};
@@ -40,9 +41,7 @@ use std::{
}; };
use thiserror::Error; use thiserror::Error;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tracing::{debug, error, trace}; use tracing::{debug, trace};
pub use util::{pretty_print_dictionary, pretty_print_plist};
use crate::services::lockdown::LockdownClient; use crate::services::lockdown::LockdownClient;
@@ -79,6 +78,7 @@ pub trait IdeviceService: Sized {
async fn connect(provider: &dyn IdeviceProvider) -> Result<Self, IdeviceError> { async fn connect(provider: &dyn IdeviceProvider) -> Result<Self, IdeviceError> {
let mut lockdown = LockdownClient::connect(provider).await?; let mut lockdown = LockdownClient::connect(provider).await?;
#[cfg(feature = "openssl")]
let legacy = lockdown let legacy = lockdown
.get_value(Some("ProductVersion"), None) .get_value(Some("ProductVersion"), None)
.await .await
@@ -90,6 +90,9 @@ pub trait IdeviceService: Sized {
.map(|x| x < 5) .map(|x| x < 5)
.unwrap_or(false); .unwrap_or(false);
#[cfg(not(feature = "openssl"))]
let legacy = false;
lockdown lockdown
.start_session(&provider.get_pairing_file().await?) .start_session(&provider.get_pairing_file().await?)
.await?; .await?;
@@ -194,7 +197,7 @@ impl Idevice {
/// # Errors /// # Errors
/// Returns `IdeviceError` if communication fails or response is invalid /// Returns `IdeviceError` if communication fails or response is invalid
pub async fn get_type(&mut self) -> Result<String, IdeviceError> { pub async fn get_type(&mut self) -> Result<String, IdeviceError> {
let req = crate::plist!({ let req = plist!({
"Label": self.label.clone(), "Label": self.label.clone(),
"Request": "QueryType", "Request": "QueryType",
}); });
@@ -214,7 +217,7 @@ impl Idevice {
/// # Errors /// # Errors
/// Returns `IdeviceError` if the protocol sequence isn't followed correctly /// Returns `IdeviceError` if the protocol sequence isn't followed correctly
pub async fn rsd_checkin(&mut self) -> Result<(), IdeviceError> { pub async fn rsd_checkin(&mut self) -> Result<(), IdeviceError> {
let req = crate::plist!({ let req = plist!({
"Label": self.label.clone(), "Label": self.label.clone(),
"ProtocolVersion": "2", "ProtocolVersion": "2",
"Request": "RSDCheckin", "Request": "RSDCheckin",
@@ -484,7 +487,13 @@ impl Idevice {
if let Some(e) = IdeviceError::from_device_error_type(e.as_str(), &res) { if let Some(e) = IdeviceError::from_device_error_type(e.as_str(), &res) {
return Err(e); return Err(e);
} else { } else {
return Err(IdeviceError::UnknownErrorType(e)); let msg =
if let Some(desc) = res.get("ErrorDescription").and_then(|x| x.as_string()) {
format!("{} ({})", e, desc)
} else {
e
};
return Err(IdeviceError::UnknownErrorType(msg));
} }
} }
Ok(res) Ok(res)
@@ -875,6 +884,9 @@ pub enum IdeviceError {
#[cfg(feature = "remote_pairing")] #[cfg(feature = "remote_pairing")]
#[error("Chacha encryption error")] #[error("Chacha encryption error")]
ChachaEncryption(chacha20poly1305::Error) = -75, ChachaEncryption(chacha20poly1305::Error) = -75,
#[cfg(feature = "notification_proxy")]
#[error("notification proxy died")]
NotificationProxyDeath = -76,
} }
impl IdeviceError { impl IdeviceError {
@@ -1048,6 +1060,9 @@ impl IdeviceError {
IdeviceError::PairVerifyFailed => -73, IdeviceError::PairVerifyFailed => -73,
IdeviceError::SrpAuthFailed => -74, IdeviceError::SrpAuthFailed => -74,
IdeviceError::ChachaEncryption(_) => -75, IdeviceError::ChachaEncryption(_) => -75,
#[cfg(feature = "notification_proxy")]
IdeviceError::NotificationProxyDeath => -76,
} }
} }
} }

View File

@@ -0,0 +1,15 @@
// Jackson Coxson
#[macro_export]
macro_rules! obf {
($lit:literal) => {{
#[cfg(feature = "obfuscate")]
{
std::borrow::Cow::Owned(obfstr::obfstr!($lit).to_string())
}
#[cfg(not(feature = "obfuscate"))]
{
std::borrow::Cow::Borrowed($lit)
}
}};
}

View File

@@ -38,7 +38,7 @@ pub struct PairingFile {
/// Host identifier /// Host identifier
pub host_id: String, pub host_id: String,
/// Escrow bag allowing for access while locked /// Escrow bag allowing for access while locked
pub escrow_bag: Vec<u8>, pub escrow_bag: Option<Vec<u8>>,
/// Device's WiFi MAC address /// Device's WiFi MAC address
pub wifi_mac_address: String, pub wifi_mac_address: String,
/// Device's Unique Device Identifier (optional) /// Device's Unique Device Identifier (optional)
@@ -73,7 +73,7 @@ struct RawPairingFile {
system_buid: String, system_buid: String,
#[serde(rename = "HostID")] #[serde(rename = "HostID")]
host_id: String, host_id: String,
escrow_bag: Data, escrow_bag: Option<Data>, // None on Apple Watch
#[serde(rename = "WiFiMACAddress")] #[serde(rename = "WiFiMACAddress")]
wifi_mac_address: String, wifi_mac_address: String,
#[serde(rename = "UDID")] #[serde(rename = "UDID")]
@@ -206,7 +206,7 @@ impl TryFrom<RawPairingFile> for PairingFile {
root_certificate: CertificateDer::from_pem_slice(&root_certificate_pem)?, root_certificate: CertificateDer::from_pem_slice(&root_certificate_pem)?,
system_buid: value.system_buid, system_buid: value.system_buid,
host_id: value.host_id, host_id: value.host_id,
escrow_bag: value.escrow_bag.into(), escrow_bag: value.escrow_bag.map(|x| x.into()),
wifi_mac_address: value.wifi_mac_address, wifi_mac_address: value.wifi_mac_address,
udid: value.udid, udid: value.udid,
}) })
@@ -230,7 +230,7 @@ impl TryFrom<RawPairingFile> for PairingFile {
root_certificate: X509::from_pem(&Into::<Vec<u8>>::into(value.root_certificate))?, root_certificate: X509::from_pem(&Into::<Vec<u8>>::into(value.root_certificate))?,
system_buid: value.system_buid, system_buid: value.system_buid,
host_id: value.host_id, host_id: value.host_id,
escrow_bag: value.escrow_bag.into(), escrow_bag: value.escrow_bag.map(|x| x.into()),
wifi_mac_address: value.wifi_mac_address, wifi_mac_address: value.wifi_mac_address,
udid: value.udid, udid: value.udid,
}) })
@@ -258,7 +258,7 @@ impl From<PairingFile> for RawPairingFile {
root_certificate: Data::new(root_cert_data), root_certificate: Data::new(root_cert_data),
system_buid: value.system_buid, system_buid: value.system_buid,
host_id: value.host_id.clone(), host_id: value.host_id.clone(),
escrow_bag: Data::new(value.escrow_bag), escrow_bag: value.escrow_bag.map(Data::new),
wifi_mac_address: value.wifi_mac_address, wifi_mac_address: value.wifi_mac_address,
udid: value.udid, udid: value.udid,
} }
@@ -278,7 +278,7 @@ impl TryFrom<PairingFile> for RawPairingFile {
root_certificate: Data::new(value.root_certificate.to_pem()?), root_certificate: Data::new(value.root_certificate.to_pem()?),
system_buid: value.system_buid, system_buid: value.system_buid,
host_id: value.host_id.clone(), host_id: value.host_id.clone(),
escrow_bag: Data::new(value.escrow_bag), escrow_bag: value.escrow_bag.map(Data::new),
wifi_mac_address: value.wifi_mac_address, wifi_mac_address: value.wifi_mac_address,
udid: value.udid, udid: value.udid,
}) })

View File

@@ -1,714 +0,0 @@
// Jackson Coxson
// Ported from serde's json!
/// Construct a `plist::Value` from a JSON-like literal.
///
/// ```
/// # use idevice::plist;
/// #
/// let value = plist!({
/// "code": 200,
/// "success": true,
/// "payload": {
/// "features": [
/// "serde",
/// "plist"
/// ],
/// "homepage": null
/// }
/// });
/// ```
///
/// Variables or expressions can be interpolated into the plist literal. Any type
/// interpolated into an array element or object value must implement `Into<plist::Value>`.
/// If the conversion fails, the `plist!` macro will panic.
///
/// ```
/// # use idevice::plist;
/// #
/// let code = 200;
/// let features = vec!["serde", "plist"];
///
/// let value = plist!({
/// "code": code,
/// "success": code == 200,
/// "payload": {
/// features[0]: features[1]
/// }
/// });
/// ```
///
/// Trailing commas are allowed inside both arrays and objects.
///
/// ```
/// # use idevice::plist;
/// #
/// let value = plist!([
/// "notice",
/// "the",
/// "trailing",
/// "comma -->",
/// ]);
/// ```
#[macro_export]
macro_rules! plist {
// Force: dictionary out
(dict { $($tt:tt)+ }) => {{
let mut object = plist::Dictionary::new();
$crate::plist_internal!(@object object () ($($tt)+) ($($tt)+));
object
}};
// Force: value out (explicit, though default already does this)
(value { $($tt:tt)+ }) => {
$crate::plist_internal!({ $($tt)+ })
};
// Force: raw vec of plist::Value out
(array [ $($tt:tt)+ ]) => {
$crate::plist_internal!(@array [] $($tt)+)
};
// Hide distracting implementation details from the generated rustdoc.
($($plist:tt)+) => {
$crate::plist_internal!($($plist)+)
};
}
#[macro_export]
#[doc(hidden)]
macro_rules! plist_internal {
//////////////////////////////////////////////////////////////////////////
// TT muncher for parsing the inside of an array [...]. Produces a vec![...]
// of the elements.
//
// Must be invoked as: plist_internal!(@array [] $($tt)*)
//////////////////////////////////////////////////////////////////////////
// Done with trailing comma.
(@array [$($elems:expr,)*]) => {
vec![$($elems,)*]
};
// Done without trailing comma.
(@array [$($elems:expr),*]) => {
vec![$($elems),*]
};
// Next element is `null`.
(@array [$($elems:expr,)*] null $($rest:tt)*) => {
$crate::plist_internal!(@array [$($elems,)* $crate::plist_internal!(null)] $($rest)*)
};
// Next element is `true`.
(@array [$($elems:expr,)*] true $($rest:tt)*) => {
$crate::plist_internal!(@array [$($elems,)* $crate::plist_internal!(true)] $($rest)*)
};
// Next element is `false`.
(@array [$($elems:expr,)*] false $($rest:tt)*) => {
$crate::plist_internal!(@array [$($elems,)* $crate::plist_internal!(false)] $($rest)*)
};
// Next element is an array.
(@array [$($elems:expr,)*] [$($array:tt)*] $($rest:tt)*) => {
$crate::plist_internal!(@array [$($elems,)* $crate::plist_internal!([$($array)*])] $($rest)*)
};
// Next element is a map.
(@array [$($elems:expr,)*] {$($map:tt)*} $($rest:tt)*) => {
$crate::plist_internal!(@array [$($elems,)* $crate::plist_internal!({$($map)*})] $($rest)*)
};
// Next element is an expression followed by comma.
(@array [$($elems:expr,)*] $next:expr, $($rest:tt)*) => {
$crate::plist_internal!(@array [$($elems,)* $crate::plist_internal!($next),] $($rest)*)
};
// Last element is an expression with no trailing comma.
(@array [$($elems:expr,)*] $last:expr) => {
$crate::plist_internal!(@array [$($elems,)* $crate::plist_internal!($last)])
};
// Comma after the most recent element.
(@array [$($elems:expr),*] , $($rest:tt)*) => {
$crate::plist_internal!(@array [$($elems,)*] $($rest)*)
};
// Unexpected token after most recent element.
(@array [$($elems:expr),*] $unexpected:tt $($rest:tt)*) => {
$crate::plist_unexpected!($unexpected)
};
(@array [$($elems:expr,)*] ? $maybe:expr , $($rest:tt)*) => {
if let Some(__v) = $crate::plist_macro::plist_maybe($maybe) {
$crate::plist_internal!(@array [$($elems,)* __v,] $($rest)*)
} else {
$crate::plist_internal!(@array [$($elems,)*] $($rest)*)
}
};
(@array [$($elems:expr,)*] ? $maybe:expr) => {
if let Some(__v) = $crate::plist_macro::plist_maybe($maybe) {
$crate::plist_internal!(@array [$($elems,)* __v])
} else {
$crate::plist_internal!(@array [$($elems,)*])
}
};
//////////////////////////////////////////////////////////////////////////
// TT muncher for parsing the inside of an object {...}. Each entry is
// inserted into the given map variable.
//
// Must be invoked as: plist_internal!(@object $map () ($($tt)*) ($($tt)*))
//
// We require two copies of the input tokens so that we can match on one
// copy and trigger errors on the other copy.
//////////////////////////////////////////////////////////////////////////
// Done.
(@object $object:ident () () ()) => {};
// Insert the current entry followed by trailing comma.
(@object $object:ident [$($key:tt)+] ($value:expr) , $($rest:tt)*) => {
let _ = $object.insert(($($key)+).into(), $value);
$crate::plist_internal!(@object $object () ($($rest)*) ($($rest)*));
};
// Current entry followed by unexpected token.
(@object $object:ident [$($key:tt)+] ($value:expr) $unexpected:tt $($rest:tt)*) => {
$crate::plist_unexpected!($unexpected);
};
// Insert the last entry without trailing comma.
(@object $object:ident [$($key:tt)+] ($value:expr)) => {
let _ = $object.insert(($($key)+).into(), $value);
};
// Next value is `null`.
(@object $object:ident ($($key:tt)+) (: null $($rest:tt)*) $copy:tt) => {
$crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!(null)) $($rest)*);
};
// Next value is `true`.
(@object $object:ident ($($key:tt)+) (: true $($rest:tt)*) $copy:tt) => {
$crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!(true)) $($rest)*);
};
// Next value is `false`.
(@object $object:ident ($($key:tt)+) (: false $($rest:tt)*) $copy:tt) => {
$crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!(false)) $($rest)*);
};
// Next value is an array.
(@object $object:ident ($($key:tt)+) (: [$($array:tt)*] $($rest:tt)*) $copy:tt) => {
$crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!([$($array)*])) $($rest)*);
};
// Next value is a map.
(@object $object:ident ($($key:tt)+) (: {$($map:tt)*} $($rest:tt)*) $copy:tt) => {
$crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!({$($map)*})) $($rest)*);
};
// Optional insert with trailing comma: key?: expr,
(@object $object:ident ($($key:tt)+) (:? $value:expr , $($rest:tt)*) $copy:tt) => {
if let Some(__v) = $crate::plist_macro::plist_maybe($value) {
let _ = $object.insert(($($key)+).into(), __v);
}
$crate::plist_internal!(@object $object () ($($rest)*) ($($rest)*));
};
// Optional insert, last entry: key?: expr
(@object $object:ident ($($key:tt)+) (:? $value:expr) $copy:tt) => {
if let Some(__v) = $crate::plist_macro::plist_maybe($value) {
let _ = $object.insert(($($key)+).into(), __v);
}
};
(@object $object:ident () ( :< $value:expr , $($rest:tt)*) $copy:tt) => {
{
let __v = $crate::plist_internal!($value);
let __dict = $crate::plist_macro::IntoPlistDict::into_plist_dict(__v);
for (__k, __val) in __dict {
let _ = $object.insert(__k, __val);
}
}
$crate::plist_internal!(@object $object () ($($rest)*) ($($rest)*));
};
// Merge: last entry `:< expr`
(@object $object:ident () ( :< $value:expr ) $copy:tt) => {
{
let __v = $crate::plist_internal!($value);
let __dict = $crate::plist_macro::IntoPlistDict::into_plist_dict(__v);
for (__k, __val) in __dict {
let _ = $object.insert(__k, __val);
}
}
};
// Optional merge: `:< ? expr,` — only merge if Some(...)
(@object $object:ident () ( :< ? $value:expr , $($rest:tt)*) $copy:tt) => {
if let Some(__dict) = $crate::plist_macro::maybe_into_dict($value) {
for (__k, __val) in __dict {
let _ = $object.insert(__k, __val);
}
}
$crate::plist_internal!(@object $object () ($($rest)*) ($($rest)*));
};
// Optional merge: last entry `:< ? expr`
(@object $object:ident () ( :< ? $value:expr ) $copy:tt) => {
if let Some(__dict) = $crate::plist_macro::maybe_into_dict($value) {
for (__k, __val) in __dict {
let _ = $object.insert(__k, __val);
}
}
};
// Next value is an expression followed by comma.
(@object $object:ident ($($key:tt)+) (: $value:expr , $($rest:tt)*) $copy:tt) => {
$crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!($value)) , $($rest)*);
};
// Last value is an expression with no trailing comma.
(@object $object:ident ($($key:tt)+) (: $value:expr) $copy:tt) => {
$crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!($value)));
};
// Missing value for last entry. Trigger a reasonable error message.
(@object $object:ident ($($key:tt)+) (:) $copy:tt) => {
// "unexpected end of macro invocation"
$crate::plist_internal!();
};
// Missing colon and value for last entry. Trigger a reasonable error
// message.
(@object $object:ident ($($key:tt)+) () $copy:tt) => {
// "unexpected end of macro invocation"
$crate::plist_internal!();
};
// Misplaced colon. Trigger a reasonable error message.
(@object $object:ident () (: $($rest:tt)*) ($colon:tt $($copy:tt)*)) => {
// Takes no arguments so "no rules expected the token `:`".
$crate::plist_unexpected!($colon);
};
// Found a comma inside a key. Trigger a reasonable error message.
(@object $object:ident ($($key:tt)*) (, $($rest:tt)*) ($comma:tt $($copy:tt)*)) => {
// Takes no arguments so "no rules expected the token `,`".
$crate::plist_unexpected!($comma);
};
// Key is fully parenthesized. This avoids clippy double_parens false
// positives because the parenthesization may be necessary here.
(@object $object:ident () (($key:expr) : $($rest:tt)*) $copy:tt) => {
$crate::plist_internal!(@object $object ($key) (: $($rest)*) (: $($rest)*));
};
// Refuse to absorb colon token into key expression.
(@object $object:ident ($($key:tt)*) (: $($unexpected:tt)+) $copy:tt) => {
$crate::plist_expect_expr_comma!($($unexpected)+);
};
// Munch a token into the current key.
(@object $object:ident ($($key:tt)*) ($tt:tt $($rest:tt)*) $copy:tt) => {
$crate::plist_internal!(@object $object ($($key)* $tt) ($($rest)*) ($($rest)*));
};
//////////////////////////////////////////////////////////////////////////
// The main implementation.
//
// Must be invoked as: plist_internal!($($plist)+)
//////////////////////////////////////////////////////////////////////////
(null) => {
plist::Value::String("".to_string()) // plist doesn't have null, use empty string or consider other representation
};
(true) => {
plist::Value::Boolean(true)
};
(false) => {
plist::Value::Boolean(false)
};
([]) => {
plist::Value::Array(vec![])
};
([ $($tt:tt)+ ]) => {
plist::Value::Array($crate::plist_internal!(@array [] $($tt)+))
};
({}) => {
plist::Value::Dictionary(plist::Dictionary::new())
};
({ $($tt:tt)+ }) => {
plist::Value::Dictionary({
let mut object = plist::Dictionary::new();
$crate::plist_internal!(@object object () ($($tt)+) ($($tt)+));
object
})
};
// Any type that can be converted to plist::Value: numbers, strings, variables etc.
// Must be below every other rule.
($other:expr) => {
$crate::plist_macro::plist_to_value($other)
};
}
#[macro_export]
#[doc(hidden)]
macro_rules! plist_unexpected {
() => {};
}
#[macro_export]
#[doc(hidden)]
macro_rules! plist_expect_expr_comma {
($e:expr , $($tt:tt)*) => {};
}
// Helper function to convert various types to plist::Value
#[doc(hidden)]
pub fn plist_to_value<T: PlistConvertible>(value: T) -> plist::Value {
value.to_plist_value()
}
// Trait for types that can be converted to plist::Value
pub trait PlistConvertible {
fn to_plist_value(self) -> plist::Value;
}
// Implementations for common types
impl PlistConvertible for plist::Value {
fn to_plist_value(self) -> plist::Value {
self
}
}
impl PlistConvertible for String {
fn to_plist_value(self) -> plist::Value {
plist::Value::String(self)
}
}
impl PlistConvertible for &str {
fn to_plist_value(self) -> plist::Value {
plist::Value::String(self.to_string())
}
}
impl PlistConvertible for i16 {
fn to_plist_value(self) -> plist::Value {
plist::Value::Integer(self.into())
}
}
impl PlistConvertible for i32 {
fn to_plist_value(self) -> plist::Value {
plist::Value::Integer(self.into())
}
}
impl PlistConvertible for i64 {
fn to_plist_value(self) -> plist::Value {
plist::Value::Integer(self.into())
}
}
impl PlistConvertible for u16 {
fn to_plist_value(self) -> plist::Value {
plist::Value::Integer((self as i64).into())
}
}
impl PlistConvertible for u32 {
fn to_plist_value(self) -> plist::Value {
plist::Value::Integer((self as i64).into())
}
}
impl PlistConvertible for u64 {
fn to_plist_value(self) -> plist::Value {
plist::Value::Integer((self as i64).into())
}
}
impl PlistConvertible for f32 {
fn to_plist_value(self) -> plist::Value {
plist::Value::Real(self as f64)
}
}
impl PlistConvertible for f64 {
fn to_plist_value(self) -> plist::Value {
plist::Value::Real(self)
}
}
impl PlistConvertible for bool {
fn to_plist_value(self) -> plist::Value {
plist::Value::Boolean(self)
}
}
impl<'a> PlistConvertible for std::borrow::Cow<'a, str> {
fn to_plist_value(self) -> plist::Value {
plist::Value::String(self.into_owned())
}
}
impl PlistConvertible for Vec<u8> {
fn to_plist_value(self) -> plist::Value {
plist::Value::Data(self)
}
}
impl PlistConvertible for &[u8] {
fn to_plist_value(self) -> plist::Value {
plist::Value::Data(self.to_vec())
}
}
impl PlistConvertible for std::time::SystemTime {
fn to_plist_value(self) -> plist::Value {
plist::Value::Date(self.into())
}
}
impl<T: PlistConvertible> PlistConvertible for Vec<T> {
fn to_plist_value(self) -> plist::Value {
plist::Value::Array(self.into_iter().map(|item| item.to_plist_value()).collect())
}
}
impl<T: PlistConvertible + Clone> PlistConvertible for &[T] {
fn to_plist_value(self) -> plist::Value {
plist::Value::Array(
self.iter()
.map(|item| item.clone().to_plist_value())
.collect(),
)
}
}
impl<T: PlistConvertible + Clone, const N: usize> PlistConvertible for [T; N] {
fn to_plist_value(self) -> plist::Value {
plist::Value::Array(self.into_iter().map(|item| item.to_plist_value()).collect())
}
}
impl<T: PlistConvertible + Clone, const N: usize> PlistConvertible for &[T; N] {
fn to_plist_value(self) -> plist::Value {
plist::Value::Array(
self.iter()
.map(|item| item.clone().to_plist_value())
.collect(),
)
}
}
impl PlistConvertible for plist::Dictionary {
fn to_plist_value(self) -> plist::Value {
plist::Value::Dictionary(self)
}
}
impl<K, V> PlistConvertible for std::collections::HashMap<K, V>
where
K: Into<String>,
V: PlistConvertible,
{
fn to_plist_value(self) -> plist::Value {
let mut dict = plist::Dictionary::new();
for (key, value) in self {
dict.insert(key.into(), value.to_plist_value());
}
plist::Value::Dictionary(dict)
}
}
impl<K, V> PlistConvertible for std::collections::BTreeMap<K, V>
where
K: Into<String>,
V: PlistConvertible,
{
fn to_plist_value(self) -> plist::Value {
let mut dict = plist::Dictionary::new();
for (key, value) in self {
dict.insert(key.into(), value.to_plist_value());
}
plist::Value::Dictionary(dict)
}
}
// Treat plain T as Some(T) and Option<T> as-is.
pub trait MaybePlist {
fn into_option_value(self) -> Option<plist::Value>;
}
impl<T: PlistConvertible> MaybePlist for T {
fn into_option_value(self) -> Option<plist::Value> {
Some(self.to_plist_value())
}
}
impl<T: PlistConvertible> MaybePlist for Option<T> {
fn into_option_value(self) -> Option<plist::Value> {
self.map(|v| v.to_plist_value())
}
}
#[doc(hidden)]
pub fn plist_maybe<T: MaybePlist>(v: T) -> Option<plist::Value> {
v.into_option_value()
}
// Convert things into a Dictionary we can merge.
pub trait IntoPlistDict {
fn into_plist_dict(self) -> plist::Dictionary;
}
impl IntoPlistDict for plist::Dictionary {
fn into_plist_dict(self) -> plist::Dictionary {
self
}
}
impl IntoPlistDict for plist::Value {
fn into_plist_dict(self) -> plist::Dictionary {
match self {
plist::Value::Dictionary(d) => d,
other => panic!("plist :< expects a dictionary, got {other:?}"),
}
}
}
impl<K, V> IntoPlistDict for std::collections::HashMap<K, V>
where
K: Into<String>,
V: PlistConvertible,
{
fn into_plist_dict(self) -> plist::Dictionary {
let mut d = plist::Dictionary::new();
for (k, v) in self {
d.insert(k.into(), v.to_plist_value());
}
d
}
}
impl<K, V> IntoPlistDict for std::collections::BTreeMap<K, V>
where
K: Into<String>,
V: PlistConvertible,
{
fn into_plist_dict(self) -> plist::Dictionary {
let mut d = plist::Dictionary::new();
for (k, v) in self {
d.insert(k.into(), v.to_plist_value());
}
d
}
}
// Optional version: T or Option<T>.
pub trait MaybeIntoPlistDict {
fn into_option_plist_dict(self) -> Option<plist::Dictionary>;
}
impl<T: IntoPlistDict> MaybeIntoPlistDict for T {
fn into_option_plist_dict(self) -> Option<plist::Dictionary> {
Some(self.into_plist_dict())
}
}
impl<T: IntoPlistDict> MaybeIntoPlistDict for Option<T> {
fn into_option_plist_dict(self) -> Option<plist::Dictionary> {
self.map(|t| t.into_plist_dict())
}
}
#[doc(hidden)]
pub fn maybe_into_dict<T: MaybeIntoPlistDict>(v: T) -> Option<plist::Dictionary> {
v.into_option_plist_dict()
}
#[cfg(test)]
mod tests {
#[test]
fn test_plist_macro_basic() {
let value = plist!({
"name": "test",
"count": 42,
"active": true,
"items": ["a", ?"b", "c"]
});
if let plist::Value::Dictionary(dict) = value {
assert_eq!(
dict.get("name"),
Some(&plist::Value::String("test".to_string()))
);
assert_eq!(dict.get("count"), Some(&plist::Value::Integer(42.into())));
assert_eq!(dict.get("active"), Some(&plist::Value::Boolean(true)));
} else {
panic!("Expected dictionary");
}
}
#[test]
fn test_plist_macro_with_variables() {
let name = "dynamic";
let count = 100;
let items = vec!["x", "y"];
let none: Option<u64> = None;
let to_merge = plist!({
"reee": "cool beans"
});
let maybe_merge = Some(plist!({
"yeppers": "what did I say about yeppers",
"replace me": 2,
}));
let value = plist!({
"name": name,
"count": count,
"items": items,
"omit me":? none,
"keep me":? Some(123),
"replace me": 1,
:< to_merge,
:<? maybe_merge
});
if let plist::Value::Dictionary(dict) = value {
assert_eq!(
dict.get("name"),
Some(&plist::Value::String("dynamic".to_string()))
);
assert_eq!(dict.get("count"), Some(&plist::Value::Integer(100.into())));
assert!(dict.get("omit me").is_none());
assert_eq!(
dict.get("keep me"),
Some(&plist::Value::Integer(123.into()))
);
assert_eq!(
dict.get("reee"),
Some(&plist::Value::String("cool beans".to_string()))
);
assert_eq!(
dict.get("yeppers"),
Some(&plist::Value::String(
"what did I say about yeppers".to_string()
))
);
assert_eq!(
dict.get("replace me"),
Some(&plist::Value::Integer(2.into()))
);
} else {
panic!("Expected dictionary");
}
}
}

View File

@@ -140,7 +140,7 @@ crate::impl_to_structs!(InnerFileDescriptor<'_>, OwnedInnerFileDescriptor; {
let mut collected_bytes = Vec::with_capacity(n); let mut collected_bytes = Vec::with_capacity(n);
for chunk in chunk_number(n, MAX_TRANSFER as usize) { for chunk in chunk_number(n, MAX_TRANSFER as usize) {
let header_payload = [self.fd.to_le_bytes(), chunk.to_le_bytes()].concat(); let header_payload = [self.fd.to_le_bytes(), (chunk as u64).to_le_bytes()].concat();
let res = self let res = self
.as_mut() .as_mut()
.send_packet(AfcOpcode::Read, header_payload, Vec::new()) .send_packet(AfcOpcode::Read, header_payload, Vec::new())

View File

@@ -13,6 +13,7 @@ use tracing::warn;
use crate::{ use crate::{
Idevice, IdeviceError, IdeviceService, Idevice, IdeviceError, IdeviceService,
afc::file::{FileDescriptor, OwnedFileDescriptor}, afc::file::{FileDescriptor, OwnedFileDescriptor},
lockdown::LockdownClient,
obf, obf,
}; };
@@ -91,6 +92,43 @@ impl AfcClient {
} }
} }
/// Connects to afc2 from a provider
pub async fn new_afc2(
provider: &dyn crate::provider::IdeviceProvider,
) -> Result<Self, IdeviceError> {
let mut lockdown = LockdownClient::connect(provider).await?;
#[cfg(feature = "openssl")]
let legacy = lockdown
.get_value(Some("ProductVersion"), None)
.await
.ok()
.as_ref()
.and_then(|x| x.as_string())
.and_then(|x| x.split(".").next())
.and_then(|x| x.parse::<u8>().ok())
.map(|x| x < 5)
.unwrap_or(false);
#[cfg(not(feature = "openssl"))]
let legacy = false;
lockdown
.start_session(&provider.get_pairing_file().await?)
.await?;
let (port, ssl) = lockdown.start_service(obf!("com.apple.afc2")).await?;
let mut idevice = provider.connect(port).await?;
if ssl {
idevice
.start_session(&provider.get_pairing_file().await?, legacy)
.await?;
}
Self::from_stream(idevice).await
}
/// Lists the contents of a directory on the device /// Lists the contents of a directory on the device
/// ///
/// # Arguments /// # Arguments

View File

@@ -1,5 +1,6 @@
// Jackson Coxson // Jackson Coxson
use plist_macro::plist_to_xml_bytes;
use serde::Deserialize; use serde::Deserialize;
use tracing::warn; use tracing::warn;
@@ -216,7 +217,7 @@ impl<R: ReadWrite> AppServiceClient<R> {
"user": { "user": {
"active": true, "active": true,
}, },
"platformSpecificOptions": plist::Value::Data(crate::util::plist_to_xml_bytes(&platform_options.unwrap_or_default())), "platformSpecificOptions": plist::Value::Data(plist_to_xml_bytes(&platform_options.unwrap_or_default())),
}, },
}); });

View File

@@ -260,6 +260,7 @@ impl LockdownClient {
&mut self, &mut self,
host_id: impl Into<String>, host_id: impl Into<String>,
system_buid: impl Into<String>, system_buid: impl Into<String>,
host_name: Option<&str>,
) -> Result<crate::pairing_file::PairingFile, IdeviceError> { ) -> Result<crate::pairing_file::PairingFile, IdeviceError> {
let host_id = host_id.into(); let host_id = host_id.into();
let system_buid = system_buid.into(); let system_buid = system_buid.into();
@@ -297,6 +298,7 @@ impl LockdownClient {
let req = crate::plist!({ let req = crate::plist!({
"Label": self.idevice.label.clone(), "Label": self.idevice.label.clone(),
"Request": "Pair", "Request": "Pair",
"HostName":? host_name,
"PairRecord": pair_record.clone(), "PairRecord": pair_record.clone(),
"ProtocolVersion": "2", "ProtocolVersion": "2",
"PairingOptions": { "PairingOptions": {
@@ -326,6 +328,23 @@ impl LockdownClient {
} }
} }
} }
/// Tell the device to enter recovery mode
pub async fn enter_recovery(&mut self) -> Result<(), IdeviceError> {
self.idevice
.send_plist(crate::plist!({
"Request": "EnterRecovery"
}))
.await?;
let res = self.idevice.read_plist().await?;
if res.get("Request").and_then(|x| x.as_string()) == Some("EnterRecovery") {
Ok(())
} else {
Err(IdeviceError::UnexpectedResponse)
}
}
} }
impl From<Idevice> for LockdownClient { impl From<Idevice> for LockdownClient {

View File

@@ -90,7 +90,11 @@ impl ImageMounter {
self.idevice.send_plist(req).await?; self.idevice.send_plist(req).await?;
let res = self.idevice.read_plist().await?; let res = self.idevice.read_plist().await?;
match res.get("ImageSignature") { match res
.get("ImageSignature")
.and_then(|x| x.as_array())
.and_then(|x| x.first())
{
Some(plist::Value::Data(signature)) => Ok(signature.clone()), Some(plist::Value::Data(signature)) => Ok(signature.clone()),
_ => Err(IdeviceError::NotFound), _ => Err(IdeviceError::NotFound),
} }

View File

@@ -35,6 +35,8 @@ pub mod mobile_image_mounter;
pub mod mobileactivationd; pub mod mobileactivationd;
#[cfg(feature = "mobilebackup2")] #[cfg(feature = "mobilebackup2")]
pub mod mobilebackup2; pub mod mobilebackup2;
#[cfg(feature = "notification_proxy")]
pub mod notification_proxy;
#[cfg(feature = "syslog_relay")] #[cfg(feature = "syslog_relay")]
pub mod os_trace_relay; pub mod os_trace_relay;
#[cfg(feature = "pcapd")] #[cfg(feature = "pcapd")]

View File

@@ -0,0 +1,212 @@
//! iOS Device Notification Proxy Service
//!
//! Based on libimobiledevice's notification_proxy implementation
//!
//! Common notification identifiers:
//! Full list: include/libimobiledevice/notification_proxy.h
//!
//! - Notifications that can be sent (PostNotification):
//! - `com.apple.itunes-mobdev.syncWillStart` - Sync will start
//! - `com.apple.itunes-mobdev.syncDidStart` - Sync started
//! - `com.apple.itunes-mobdev.syncDidFinish` - Sync finished
//! - `com.apple.itunes-mobdev.syncLockRequest` - Request sync lock
//!
//! - Notifications that can be observed (ObserveNotification):
//! - `com.apple.itunes-client.syncCancelRequest` - Cancel sync request
//! - `com.apple.itunes-client.syncSuspendRequest` - Suspend sync
//! - `com.apple.itunes-client.syncResumeRequest` - Resume sync
//! - `com.apple.mobile.lockdown.phone_number_changed` - Phone number changed
//! - `com.apple.mobile.lockdown.device_name_changed` - Device name changed
//! - `com.apple.mobile.lockdown.timezone_changed` - Timezone changed
//! - `com.apple.mobile.lockdown.trusted_host_attached` - Trusted host attached
//! - `com.apple.mobile.lockdown.host_detached` - Host detached
//! - `com.apple.mobile.lockdown.host_attached` - Host attached
//! - `com.apple.mobile.lockdown.registration_failed` - Registration failed
//! - `com.apple.mobile.lockdown.activation_state` - Activation state
//! - `com.apple.mobile.lockdown.brick_state` - Brick state
//! - `com.apple.mobile.lockdown.disk_usage_changed` - Disk usage (iOS 4.0+)
//! - `com.apple.mobile.data_sync.domain_changed` - Data sync domain changed
//! - `com.apple.mobile.application_installed` - App installed
//! - `com.apple.mobile.application_uninstalled` - App uninstalled
use std::pin::Pin;
use futures::Stream;
use tracing::warn;
use crate::{Idevice, IdeviceError, IdeviceService, obf};
/// Client for interacting with the iOS notification proxy service
///
/// The notification proxy service provides a mechanism to observe and post
/// system notifications.
///
/// Use `observe_notification` to register for events, then `receive_notification`
/// to wait for them.
#[derive(Debug)]
pub struct NotificationProxyClient {
/// The underlying device connection with established notification_proxy service
pub idevice: Idevice,
}
impl IdeviceService for NotificationProxyClient {
/// Returns the notification proxy service name as registered with lockdownd
fn service_name() -> std::borrow::Cow<'static, str> {
obf!("com.apple.mobile.notification_proxy")
}
async fn from_stream(idevice: Idevice) -> Result<Self, crate::IdeviceError> {
Ok(Self::new(idevice))
}
}
impl NotificationProxyClient {
/// Creates a new notification proxy client from an existing device connection
///
/// # Arguments
/// * `idevice` - Pre-established device connection
pub fn new(idevice: Idevice) -> Self {
Self { idevice }
}
/// Posts a notification to the device
///
/// # Arguments
/// * `notification_name` - Name of the notification to post
///
/// # Errors
/// Returns `IdeviceError` if the notification fails to send
pub async fn post_notification(
&mut self,
notification_name: impl Into<String>,
) -> Result<(), IdeviceError> {
let request = crate::plist!({
"Command": "PostNotification",
"Name": notification_name.into()
});
self.idevice.send_plist(request).await
}
/// Registers to observe a specific notification
///
/// After calling this, use `receive_notification` to wait for events.
///
/// # Arguments
/// * `notification_name` - Name of the notification to observe
///
/// # Errors
/// Returns `IdeviceError` if the registration fails
pub async fn observe_notification(
&mut self,
notification_name: impl Into<String>,
) -> Result<(), IdeviceError> {
let request = crate::plist!({
"Command": "ObserveNotification",
"Name": notification_name.into()
});
self.idevice.send_plist(request).await
}
/// Registers to observe multiple notifications at once
///
/// # Arguments
/// * `notification_names` - Slice of notification names to observe
///
/// # Errors
/// Returns `IdeviceError` if any registration fails
pub async fn observe_notifications(
&mut self,
notification_names: &[&str],
) -> Result<(), IdeviceError> {
for name in notification_names {
self.observe_notification(*name).await?;
}
Ok(())
}
/// Waits for and receives the next notification from the device
///
/// # Returns
/// The name of the received notification
///
/// # Errors
/// - `NotificationProxyDeath` if the proxy connection died
/// - `UnexpectedResponse` if the response format is invalid
pub async fn receive_notification(&mut self) -> Result<String, IdeviceError> {
let response = self.idevice.read_plist().await?;
match response.get("Command").and_then(|c| c.as_string()) {
Some("RelayNotification") => match response.get("Name").and_then(|n| n.as_string()) {
Some(name) => Ok(name.to_string()),
None => Err(IdeviceError::UnexpectedResponse),
},
Some("ProxyDeath") => {
warn!("NotificationProxy died!");
Err(IdeviceError::NotificationProxyDeath)
}
_ => Err(IdeviceError::UnexpectedResponse),
}
}
/// Waits for a notification with a timeout
///
/// # Arguments
/// * `interval` - Timeout in seconds to wait for a notification
///
/// # Returns
/// The name of the received notification
///
/// # Errors
/// - `NotificationProxyDeath` if the proxy connection died
/// - `UnexpectedResponse` if the response format is invalid
/// - `HeartbeatTimeout` if no notification received before interval
pub async fn receive_notification_with_timeout(
&mut self,
interval: u64,
) -> Result<String, IdeviceError> {
tokio::select! {
result = self.receive_notification() => result,
_ = tokio::time::sleep(tokio::time::Duration::from_secs(interval)) => {
Err(IdeviceError::HeartbeatTimeout)
}
}
}
/// Continuous stream of notifications.
pub fn into_stream(
mut self,
) -> Pin<Box<dyn Stream<Item = Result<String, IdeviceError>> + Send>> {
Box::pin(async_stream::try_stream! {
loop {
let response = self.idevice.read_plist().await?;
match response.get("Command").and_then(|c| c.as_string()) {
Some("RelayNotification") => {
match response.get("Name").and_then(|n| n.as_string()) {
Some(name) => yield name.to_string(),
None => Err(IdeviceError::UnexpectedResponse)?,
}
}
Some("ProxyDeath") => {
warn!("NotificationProxy died!");
Err(IdeviceError::NotificationProxyDeath)?;
}
_ => Err(IdeviceError::UnexpectedResponse)?,
}
}
})
}
/// Shuts down the notification proxy connection
///
/// # Errors
/// Returns `IdeviceError` if the shutdown command fails to send
pub async fn shutdown(&mut self) -> Result<(), IdeviceError> {
let request = crate::plist!({
"Command": "Shutdown"
});
self.idevice.send_plist(request).await?;
// Best-effort: wait for ProxyDeath ack
let _ = self.idevice.read_plist().await;
Ok(())
}
}

View File

@@ -3,7 +3,23 @@
//! Provides functionality for interacting with the SpringBoard services on iOS devices, //! Provides functionality for interacting with the SpringBoard services on iOS devices,
//! which manages home screen and app icon related operations. //! which manages home screen and app icon related operations.
use crate::{Idevice, IdeviceError, IdeviceService, obf}; use crate::{Idevice, IdeviceError, IdeviceService, obf, utils::plist::truncate_dates_to_seconds};
/// Orientation of the device
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum InterfaceOrientation {
/// Orientation is unknown or cannot be determined
Unknown = 0,
/// Portrait mode (normal vertical)
Portrait = 1,
/// Portrait mode upside down
PortraitUpsideDown = 2,
/// Landscape with home button on the right (notch to the left)
LandscapeRight = 3,
/// Landscape with home button on the left (notch to the right)
LandscapeLeft = 4,
}
/// Client for interacting with the iOS SpringBoard services /// Client for interacting with the iOS SpringBoard services
/// ///
@@ -70,4 +86,270 @@ impl SpringBoardServicesClient {
_ => Err(IdeviceError::UnexpectedResponse), _ => Err(IdeviceError::UnexpectedResponse),
} }
} }
/// Retrieves the current icon state from the device
///
/// The icon state contains the layout and organization of all apps on the home screen,
/// including folder structures and icon positions. This is a read-only operation.
///
/// # Arguments
/// * `format_version` - Optional format version string for the icon state format
///
/// # Returns
/// A plist Value containing the complete icon state structure
///
/// # Errors
/// Returns `IdeviceError` if:
/// - Communication fails
/// - The response is malformed
///
/// # Example
/// ```rust
/// use idevice::services::springboardservices::SpringBoardServicesClient;
///
/// let mut client = SpringBoardServicesClient::connect(&provider).await?;
/// let icon_state = client.get_icon_state(None).await?;
/// println!("Icon state: {:?}", icon_state);
/// ```
///
/// # Notes
/// This method successfully reads the home screen layout on all iOS versions.
pub async fn get_icon_state(
&mut self,
format_version: Option<&str>,
) -> Result<plist::Value, IdeviceError> {
let req = crate::plist!({
"command": "getIconState",
"formatVersion":? format_version,
});
self.idevice.send_plist(req).await?;
let mut res = self.idevice.read_plist_value().await?;
// Some devices may return an error dictionary instead of icon state.
// Detect this and surface it as an UnexpectedResponse, similar to get_icon_pngdata.
if let plist::Value::Dictionary(ref dict) = res
&& (dict.contains_key("error") || dict.contains_key("Error"))
{
return Err(IdeviceError::UnexpectedResponse);
}
truncate_dates_to_seconds(&mut res);
Ok(res)
}
/// Sets the icon state on the device
///
/// This method allows you to modify the home screen layout by providing a new icon state.
/// The icon state structure should match the format returned by `get_icon_state`.
///
/// # Arguments
/// * `icon_state` - A plist Value containing the complete icon state structure
///
/// # Returns
/// Ok(()) if the icon state was successfully set
///
/// # Errors
/// Returns `IdeviceError` if:
/// - Communication fails
/// - The icon state format is invalid
/// - The device rejects the new layout
///
/// # Example
/// ```rust
/// use idevice::services::springboardservices::SpringBoardServicesClient;
///
/// let mut client = SpringBoardServicesClient::connect(&provider).await?;
/// let mut icon_state = client.get_icon_state(None).await?;
///
/// // Modify the icon state (e.g., swap two icons)
/// // ... modify icon_state ...
///
/// client.set_icon_state(icon_state).await?;
/// println!("Icon state updated successfully");
/// ```
///
/// # Notes
/// - Changes take effect immediately
/// - The device may validate the icon state structure before applying
/// - Invalid icon states will be rejected by the device
pub async fn set_icon_state(&mut self, icon_state: plist::Value) -> Result<(), IdeviceError> {
let req = crate::plist!({
"command": "setIconState",
"iconState": icon_state,
});
self.idevice.send_plist(req).await?;
Ok(())
}
/// Sets the icon state with a specific format version
///
/// This is similar to `set_icon_state` but allows specifying a format version.
///
/// # Arguments
/// * `icon_state` - A plist Value containing the complete icon state structure
/// * `format_version` - Optional format version string
///
/// # Returns
/// Ok(()) if the icon state was successfully set
///
/// # Errors
/// Returns `IdeviceError` if:
/// - Communication fails
/// - The icon state format is invalid
/// - The device rejects the new layout
pub async fn set_icon_state_with_version(
&mut self,
icon_state: plist::Value,
format_version: Option<&str>,
) -> Result<(), IdeviceError> {
let req = crate::plist!({
"command": "setIconState",
"iconState": icon_state,
"formatVersion":? format_version,
});
self.idevice.send_plist(req).await?;
Ok(())
}
/// Gets the home screen wallpaper preview as PNG data
///
/// This gets a rendered preview of the home screen wallpaper.
///
/// # Returns
/// The raw PNG data of the home screen wallpaper preview
///
/// # Errors
/// Returns `IdeviceError` if:
/// - Communication fails
/// - The device rejects the request
/// - The image is malformed/corupted
///
/// # Example
/// ```rust
/// let wallpaper = client.get_home_screen_wallpaper_preview_pngdata().await?;
/// std::fs::write("home.png", wallpaper)?;
/// ```
pub async fn get_home_screen_wallpaper_preview_pngdata(
&mut self,
) -> Result<Vec<u8>, IdeviceError> {
let req = crate::plist!({
"command": "getWallpaperPreviewImage",
"wallpaperName": "homescreen",
});
self.idevice.send_plist(req).await?;
let mut res = self.idevice.read_plist().await?;
match res.remove("pngData") {
Some(plist::Value::Data(res)) => Ok(res),
_ => Err(IdeviceError::UnexpectedResponse),
}
}
/// Gets the lock screen wallpaper preview as PNG data
///
/// This gets a rendered preview of the lock screen wallpaper.
///
/// # Returns
/// The raw PNG data of the lock screen wallpaper preview
///
/// # Errors
/// Returns `IdeviceError` if:
/// - Communication fails
/// - The device rejects the request
/// - The image is malformed/corupted
///
/// # Example
/// ```rust
/// let wallpaper = client.get_lock_screen_wallpaper_preview_pngdata().await?;
/// std::fs::write("lock.png", wallpaper)?;
/// ```
pub async fn get_lock_screen_wallpaper_preview_pngdata(
&mut self,
) -> Result<Vec<u8>, IdeviceError> {
let req = crate::plist!({
"command": "getWallpaperPreviewImage",
"wallpaperName": "lockscreen",
});
self.idevice.send_plist(req).await?;
let mut res = self.idevice.read_plist().await?;
match res.remove("pngData") {
Some(plist::Value::Data(res)) => Ok(res),
_ => Err(IdeviceError::UnexpectedResponse),
}
}
/// Gets the current interface orientation of the device
///
/// This gets which way the device is currently facing
///
/// # Returns
/// The current `InterfaceOrientation` of the device
///
/// # Errors
/// Returns `IdeviceError` if:
/// - Communication fails
/// - The device doesn't support this command
/// - The response format is unexpected
///
/// # Example
/// ```rust
/// let orientation = client.get_interface_orientation().await?;
/// println!("Device orientation: {:?}", orientation);
/// ```
pub async fn get_interface_orientation(
&mut self,
) -> Result<InterfaceOrientation, IdeviceError> {
let req = crate::plist!({
"command": "getInterfaceOrientation",
});
self.idevice.send_plist(req).await?;
let res = self.idevice.read_plist().await?;
let orientation_value = res
.get("interfaceOrientation")
.and_then(|v| v.as_unsigned_integer())
.ok_or(IdeviceError::UnexpectedResponse)?;
let orientation = match orientation_value {
1 => InterfaceOrientation::Portrait,
2 => InterfaceOrientation::PortraitUpsideDown,
3 => InterfaceOrientation::LandscapeRight,
4 => InterfaceOrientation::LandscapeLeft,
_ => InterfaceOrientation::Unknown,
};
Ok(orientation)
}
/// Gets the home screen icon layout metrics
///
/// Returns icon spacing, size, and positioning information
///
/// # Returns
/// A `plist::Dictionary` containing the icon layout metrics
///
/// # Errors
/// Returns `IdeviceError` if:
/// - Communication fails
/// - The response is malformed
///
/// # Example
/// ```rust
/// let metrics = client.get_homescreen_icon_metrics().await?;
/// println!("{:?}", metrics);
/// ```
pub async fn get_homescreen_icon_metrics(&mut self) -> Result<plist::Dictionary, IdeviceError> {
let req = crate::plist!({
"command": "getHomeScreenIconMetrics",
});
self.idevice.send_plist(req).await?;
let res = self.idevice.read_plist().await?;
Ok(res)
}
} }

View File

@@ -6,9 +6,10 @@
//! - Handle cryptographic signing operations //! - Handle cryptographic signing operations
use plist::Value; use plist::Value;
use plist_macro::plist_to_xml_bytes;
use tracing::{debug, warn}; use tracing::{debug, warn};
use crate::{IdeviceError, util::plist_to_xml_bytes}; use crate::IdeviceError;
/// TSS client version string sent in requests /// TSS client version string sent in requests
const TSS_CLIENT_VERSION_STRING: &str = "libauthinstall-1033.0.2"; const TSS_CLIENT_VERSION_STRING: &str = "libauthinstall-1033.0.2";
@@ -30,7 +31,7 @@ impl TSSRequest {
/// - Client version string /// - Client version string
/// - Random UUID for request identification /// - Random UUID for request identification
pub fn new() -> Self { pub fn new() -> Self {
let inner = crate::plist!(dict { let inner = plist_macro::plist!(dict {
"@HostPlatformInfo": "mac", "@HostPlatformInfo": "mac",
"@VersionInfo": TSS_CLIENT_VERSION_STRING, "@VersionInfo": TSS_CLIENT_VERSION_STRING,
"@UUID": uuid::Uuid::new_v4().to_string().to_uppercase() "@UUID": uuid::Uuid::new_v4().to_string().to_uppercase()

View File

@@ -1,6 +1,6 @@
// Jackson Coxson // Jackson Coxson
use crate::util::plist_to_xml_bytes; use plist_macro::plist_to_xml_bytes;
use tracing::warn; use tracing::warn;
#[derive(Debug)] #[derive(Debug)]

View File

@@ -1,142 +0,0 @@
//! Utility Functions
//!
//! Provides helper functions for working with Apple's Property List (PLIST) format,
//! including serialization and pretty-printing utilities.
#![allow(dead_code)] // functions might not be used by all features
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);
plist::to_writer_xml(&mut writer, &p).unwrap();
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()
.map(|(k, v)| format!("{}: {}", k, print_plist(v, 2)))
.collect();
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 {
Value::Array(vec) => {
let items: Vec<String> = vec
.iter()
.map(|v| {
format!(
"{}{}",
" ".repeat(indentation + 2),
print_plist(v, indentation + 2)
)
})
.collect();
format!("[\n{}\n{}]", items.join(",\n"), indent)
}
Value::Dictionary(dict) => {
let items: Vec<String> = dict
.iter()
.map(|(k, v)| {
format!(
"{}{}: {}",
" ".repeat(indentation + 2),
k,
print_plist(v, indentation + 2)
)
})
.collect();
format!("{{\n{}\n{}}}", items.join(",\n"), indent)
}
Value::Boolean(b) => format!("{b}"),
Value::Data(vec) => {
let len = vec.len();
let preview: String = vec
.iter()
.take(20)
.map(|b| format!("{b:02X}"))
.collect::<Vec<String>>()
.join(" ");
if len > 20 {
format!("Data({preview}... Len: {len})")
} else {
format!("Data({preview} Len: {len})")
}
}
Value::Date(date) => format!("Date({})", date.to_xml_format()),
Value::Real(f) => format!("{f}"),
Value::Integer(i) => format!("{i}"),
Value::String(s) => format!("\"{s}\""),
Value::Uid(_uid) => "Uid(?)".to_string(),
_ => "Unknown".to_string(),
}
}
#[macro_export]
macro_rules! obf {
($lit:literal) => {{
#[cfg(feature = "obfuscate")]
{
std::borrow::Cow::Owned(obfstr::obfstr!($lit).to_string())
}
#[cfg(not(feature = "obfuscate"))]
{
std::borrow::Cow::Borrowed($lit)
}
}};
}

View File

@@ -1,13 +1,12 @@
use std::{io::Cursor, path::Path};
use async_zip::base::read::seek::ZipFileReader; use async_zip::base::read::seek::ZipFileReader;
use futures::AsyncReadExt as _; use futures::AsyncReadExt as _;
use plist_macro::plist;
use std::{io::Cursor, path::Path};
use tokio::io::{AsyncBufRead, AsyncSeek, BufReader}; use tokio::io::{AsyncBufRead, AsyncSeek, BufReader};
use crate::{ use crate::{
IdeviceError, IdeviceService, IdeviceError, IdeviceService,
afc::{AfcClient, opcode::AfcFopenMode}, afc::{AfcClient, opcode::AfcFopenMode},
plist,
provider::IdeviceProvider, provider::IdeviceProvider,
}; };

View File

@@ -2,3 +2,5 @@
#[cfg(all(feature = "afc", feature = "installation_proxy"))] #[cfg(all(feature = "afc", feature = "installation_proxy"))]
pub mod installation; pub mod installation;
pub mod plist;

155
idevice/src/utils/plist.rs Normal file
View File

@@ -0,0 +1,155 @@
/// Utilities for working with plist values
///
/// Truncates all Date values in a plist structure to second precision.
///
/// This function recursively walks through a plist Value and truncates any Date values
/// from nanosecond precision to second precision. This is necessary for compatibility
/// with iOS devices that reject high-precision date formats.
///
/// # Arguments
/// * `value` - The plist Value to normalize (modified in place)
///
/// # Example
/// ```rust,no_run
/// use idevice::utils::plist::truncate_dates_to_seconds;
/// use plist::Value;
///
/// let mut icon_state = Value::Array(vec![]);
/// truncate_dates_to_seconds(&mut icon_state);
/// ```
///
/// # Details
/// - Converts dates from format: `2026-01-17T03:09:58.332738876Z` (nanosecond precision)
/// - To format: `2026-01-17T03:09:58Z` (second precision)
/// - Recursively processes Arrays and Dictionaries
/// - Other value types are left unchanged
pub fn truncate_dates_to_seconds(value: &mut plist::Value) {
match value {
plist::Value::Date(date) => {
let xml_string = date.to_xml_format();
if let Some(dot_pos) = xml_string.find('.')
&& xml_string[dot_pos..].contains('Z')
{
let truncated_string = format!("{}Z", &xml_string[..dot_pos]);
if let Ok(new_date) = plist::Date::from_xml_format(&truncated_string) {
*date = new_date;
}
}
}
plist::Value::Array(arr) => {
for item in arr.iter_mut() {
truncate_dates_to_seconds(item);
}
}
plist::Value::Dictionary(dict) => {
for (_, v) in dict.iter_mut() {
truncate_dates_to_seconds(v);
}
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_truncate_date_with_nanoseconds() {
let date_str = "2026-01-17T03:09:58.332738876Z";
let date = plist::Date::from_xml_format(date_str).unwrap();
let mut value = plist::Value::Date(date);
truncate_dates_to_seconds(&mut value);
if let plist::Value::Date(truncated_date) = value {
let result = truncated_date.to_xml_format();
assert!(
!result.contains('.'),
"Date should not contain fractional seconds"
);
assert!(result.ends_with('Z'), "Date should end with Z");
assert!(
result.starts_with("2026-01-17T03:09:58"),
"Date should preserve main timestamp"
);
} else {
panic!("Value should still be a Date");
}
}
#[test]
fn test_truncate_date_already_truncated() {
let date_str = "2026-01-17T03:09:58Z";
let date = plist::Date::from_xml_format(date_str).unwrap();
let original_format = date.to_xml_format();
let mut value = plist::Value::Date(date);
truncate_dates_to_seconds(&mut value);
if let plist::Value::Date(truncated_date) = value {
let result = truncated_date.to_xml_format();
assert_eq!(
result, original_format,
"Already truncated date should remain unchanged"
);
}
}
#[test]
fn test_truncate_dates_in_array() {
let date1 = plist::Date::from_xml_format("2026-01-17T03:09:58.123456Z").unwrap();
let date2 = plist::Date::from_xml_format("2026-01-18T04:10:59.987654Z").unwrap();
let mut value =
plist::Value::Array(vec![plist::Value::Date(date1), plist::Value::Date(date2)]);
truncate_dates_to_seconds(&mut value);
if let plist::Value::Array(arr) = value {
for item in arr {
if let plist::Value::Date(date) = item {
let formatted = date.to_xml_format();
assert!(
!formatted.contains('.'),
"Dates in array should be truncated"
);
}
}
}
}
#[test]
fn test_truncate_dates_in_dictionary() {
let date = plist::Date::from_xml_format("2026-01-17T03:09:58.999999Z").unwrap();
let mut dict = plist::Dictionary::new();
dict.insert("timestamp".to_string(), plist::Value::Date(date));
let mut value = plist::Value::Dictionary(dict);
truncate_dates_to_seconds(&mut value);
if let plist::Value::Dictionary(dict) = value
&& let Some(plist::Value::Date(date)) = dict.get("timestamp")
{
let formatted = date.to_xml_format();
assert!(
!formatted.contains('.'),
"Date in dictionary should be truncated"
);
}
}
#[test]
fn test_other_value_types_unchanged() {
let mut string_val = plist::Value::String("test".to_string());
let mut int_val = plist::Value::Integer(42.into());
let mut bool_val = plist::Value::Boolean(true);
truncate_dates_to_seconds(&mut string_val);
truncate_dates_to_seconds(&mut int_val);
truncate_dates_to_seconds(&mut bool_val);
assert!(matches!(string_val, plist::Value::String(_)));
assert!(matches!(int_val, plist::Value::Integer(_)));
assert!(matches!(bool_val, plist::Value::Boolean(_)));
}
}

View File

@@ -1,3 +1,4 @@
use plist_macro::plist;
use std::{ use std::{
ffi::CString, ffi::CString,
io::{BufRead, Cursor, Read}, io::{BufRead, Cursor, Read},
@@ -169,7 +170,7 @@ impl XPCObject {
plist::Value::Dictionary(dict) plist::Value::Dictionary(dict)
} }
Self::FileTransfer { msg_id, data } => { Self::FileTransfer { msg_id, data } => {
crate::plist!({ plist!({
"msg_id": *msg_id, "msg_id": *msg_id,
"data": data.to_plist(), "data": data.to_plist(),
}) })

View File

@@ -6,7 +6,7 @@ check-features:
ci-check: build-ffi-native build-tools-native build-cpp build-c ci-check: build-ffi-native build-tools-native build-cpp build-c
cargo clippy --all-targets --all-features -- -D warnings cargo clippy --all-targets --all-features -- -D warnings
cargo fmt -- --check cargo fmt -- --check
macos-ci-check: ci-check macos-ci-check: ci-check xcframework
cd tools && cargo build --release --target x86_64-apple-darwin cd tools && cargo build --release --target x86_64-apple-darwin
windows-ci-check: build-ffi-native build-tools-native build-cpp windows-ci-check: build-ffi-native build-tools-native build-cpp
@@ -40,6 +40,9 @@ xcframework: apple-build
lipo -create -output swift/libs/idevice-ios-sim.a \ lipo -create -output swift/libs/idevice-ios-sim.a \
target/aarch64-apple-ios-sim/release/libidevice_ffi.a \ target/aarch64-apple-ios-sim/release/libidevice_ffi.a \
target/x86_64-apple-ios/release/libidevice_ffi.a target/x86_64-apple-ios/release/libidevice_ffi.a
lipo -create -output swift/libs/idevice-maccatalyst.a \
target/aarch64-apple-ios-macabi/release/libidevice_ffi.a \
target/x86_64-apple-ios-macabi/release/libidevice_ffi.a
lipo -create -output swift/libs/idevice-macos.a \ lipo -create -output swift/libs/idevice-macos.a \
target/aarch64-apple-darwin/release/libidevice_ffi.a \ target/aarch64-apple-darwin/release/libidevice_ffi.a \
target/x86_64-apple-darwin/release/libidevice_ffi.a target/x86_64-apple-darwin/release/libidevice_ffi.a
@@ -48,8 +51,9 @@ xcframework: apple-build
-library target/aarch64-apple-ios/release/libidevice_ffi.a -headers swift/include \ -library target/aarch64-apple-ios/release/libidevice_ffi.a -headers swift/include \
-library swift/libs/idevice-ios-sim.a -headers swift/include \ -library swift/libs/idevice-ios-sim.a -headers swift/include \
-library swift/libs/idevice-macos.a -headers swift/include \ -library swift/libs/idevice-macos.a -headers swift/include \
-library swift/libs/idevice-maccatalyst.a -headers swift/include \
-output swift/IDevice.xcframework -output swift/IDevice.xcframework
zip -r swift/bundle.zip swift/IDevice.xcframework zip -r swift/bundle.zip swift/IDevice.xcframework
openssl dgst -sha256 swift/bundle.zip openssl dgst -sha256 swift/bundle.zip
@@ -57,6 +61,7 @@ xcframework: apple-build
apple-build: # requires a Mac apple-build: # requires a Mac
# iOS device build # iOS device build
BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$(xcrun --sdk iphoneos --show-sdk-path)" \ BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$(xcrun --sdk iphoneos --show-sdk-path)" \
IPHONEOS_DEPLOYMENT_TARGET=17.0 \
cargo build --release --target aarch64-apple-ios --features obfuscate cargo build --release --target aarch64-apple-ios --features obfuscate
# iOS Simulator (arm64) # iOS Simulator (arm64)
@@ -67,7 +72,16 @@ apple-build: # requires a Mac
BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$(xcrun --sdk iphonesimulator --show-sdk-path)" \ BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$(xcrun --sdk iphonesimulator --show-sdk-path)" \
cargo build --release --target x86_64-apple-ios cargo build --release --target x86_64-apple-ios
# Mac Catalyst (arm64)
# AWS-LC has an a hard time compiling for an iOS with macabi target, so we switch to ring.
BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$(xcrun --sdk macosx --show-sdk-path)" \
cargo build --release --target aarch64-apple-ios-macabi --no-default-features --features "ring full"
# Mac Catalyst (x86_64)
# AWS-LC has an a hard time compiling for an iOS with macabi target, so we switch to ring.
BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$(xcrun --sdk macosx --show-sdk-path)" \
cargo build --release --target x86_64-apple-ios-macabi --no-default-features --features "ring full"
# macOS (native) no special env needed # macOS (native) no special env needed
cargo build --release --target aarch64-apple-darwin cargo build --release --target aarch64-apple-darwin
cargo build --release --target x86_64-apple-darwin cargo build --release --target x86_64-apple-darwin

View File

@@ -2,80 +2,13 @@
name = "idevice-tools" name = "idevice-tools"
description = "Rust binary tools to interact with services on iOS devices." description = "Rust binary tools to interact with services on iOS devices."
authors = ["Jackson Coxson"] authors = ["Jackson Coxson"]
version = "0.1.0" version = "0.1.53"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
documentation = "https://docs.rs/idevice" documentation = "https://docs.rs/idevice"
repository = "https://github.com/jkcoxson/idevice" repository = "https://github.com/jkcoxson/idevice"
keywords = ["lockdownd", "ios"] keywords = ["lockdownd", "ios"]
default-run = "idevice-tools"
# [[bin]]
# name = "ideviceinfo"
# path = "src/ideviceinfo.rs"
#
# [[bin]]
# name = "heartbeat_client"
# path = "src/heartbeat_client.rs"
#
# [[bin]]
# name = "instproxy"
# path = "src/instproxy.rs"
#
# [[bin]]
# name = "ideviceinstaller"
# path = "src/ideviceinstaller.rs"
#
# [[bin]]
# name = "mounter"
# path = "src/mounter.rs"
# [[bin]]
# name = "core_device_proxy_tun"
# path = "src/core_device_proxy_tun.rs"
# [[bin]]
# name = "idevice_id"
# path = "src/idevice_id.rs"
#
# [[bin]]
# name = "process_control"
# path = "src/process_control.rs"
#
# [[bin]]
# name = "dvt_packet_parser"
# path = "src/dvt_packet_parser.rs"
#
# [[bin]]
# name = "remotexpc"
# path = "src/remotexpc.rs"
#
# [[bin]]
# name = "debug_proxy"
# path = "src/debug_proxy.rs"
#
# [[bin]]
# name = "misagent"
# path = "src/misagent.rs"
#
# [[bin]]
# name = "location_simulation"
# path = "src/location_simulation.rs"
#
# [[bin]]
# name = "afc"
# path = "src/afc.rs"
#
# [[bin]]
# name = "crash_logs"
# path = "src/crash_logs.rs"
#
# [[bin]]
# name = "amfi"
# path = "src/amfi.rs"
#
# [[bin]]
# name = "pair"
# path = "src/pair.rs"
[[bin]] [[bin]]
name = "pair_apple_tv" name = "pair_apple_tv"
@@ -86,74 +19,16 @@ name = "pair_rsd_ios"
path = "src/pair_rsd_ios.rs" path = "src/pair_rsd_ios.rs"
# [[bin]] # [[bin]]
# name = "syslog_relay" # name = "core_device_proxy_tun"
# path = "src/syslog_relay.rs" # path = "src/core_device_proxy_tun.rs"
#
# [[bin]] [[bin]]
# name = "os_trace_relay" name = "idevice_id"
# path = "src/os_trace_relay.rs" path = "src/idevice_id.rs"
#
# [[bin]] [[bin]]
# name = "app_service" name = "iproxy"
# path = "src/app_service.rs" path = "src/iproxy.rs"
#
# [[bin]]
# name = "lockdown"
# path = "src/lockdown.rs"
#
# [[bin]]
# name = "restore_service"
# path = "src/restore_service.rs"
#
# [[bin]]
# name = "companion_proxy"
# path = "src/companion_proxy.rs"
#
# [[bin]]
# name = "diagnostics"
# path = "src/diagnostics.rs"
#
# [[bin]]
# name = "mobilebackup2"
# path = "src/mobilebackup2.rs"
#
# [[bin]]
# name = "diagnosticsservice"
# path = "src/diagnosticsservice.rs"
#
# [[bin]]
# name = "bt_packet_logger"
# path = "src/bt_packet_logger.rs"
#
# [[bin]]
# name = "pcapd"
# path = "src/pcapd.rs"
#
# [[bin]]
# name = "preboard"
# path = "src/preboard.rs"
#
#
# [[bin]]
# name = "screenshot"
# path = "src/screenshot.rs"
#
# [[bin]]
# name = "activation"
# path = "src/activation.rs"
#
# [[bin]]
# name = "notifications"
# path = "src/notifications.rs"
#
#
# [[bin]]
# name = "installcoordination_proxy"
# path = "src/installcoordination_proxy.rs"
#
# [[bin]]
# name = "iproxy"
# path = "src/iproxy.rs"
[dependencies] [dependencies]
idevice = { path = "../idevice", features = ["full"], default-features = false } idevice = { path = "../idevice", features = ["full"], default-features = false }
@@ -164,7 +39,9 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
sha2 = { version = "0.10" } sha2 = { version = "0.10" }
ureq = { version = "3" } ureq = { version = "3" }
clap = { version = "4.5" } clap = { version = "4.5" }
jkcli = { version = "0.1" }
plist = { version = "1.7" } plist = { version = "1.7" }
plist-macro = { version = "0.1.3" }
ns-keyed-archive = "0.1.2" ns-keyed-archive = "0.1.2"
uuid = "1.16" uuid = "1.16"
futures-util = { version = "0.3" } futures-util = { version = "0.3" }

View File

@@ -1,65 +1,23 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command};
use idevice::{ use idevice::{
IdeviceService, lockdown::LockdownClient, mobileactivationd::MobileActivationdClient, IdeviceService, lockdown::LockdownClient, mobileactivationd::MobileActivationdClient,
provider::IdeviceProvider,
}; };
use jkcli::{CollectedArguments, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new()
#[tokio::main] .help("Manage activation status on an iOS device")
async fn main() { .with_subcommand("state", JkCommand::new().help("Gets the activation state"))
tracing_subscriber::fmt::init(); .with_subcommand(
"deactivate",
let matches = Command::new("activation") JkCommand::new().help("Deactivates the device"),
.about("mobileactivationd")
.arg(
Arg::new("host")
.long("host")
.value_name("HOST")
.help("IP address of the device"),
) )
.arg( .subcommand_required(true)
Arg::new("pairing_file") }
.long("pairing-file")
.value_name("PATH")
.help("Path to the pairing file"),
)
.arg(
Arg::new("udid")
.value_name("UDID")
.help("UDID of the device (overrides host/pairing file)")
.index(1),
)
.arg(
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.subcommand(Command::new("state").about("Gets the activation state"))
.subcommand(Command::new("deactivate").about("Deactivates the device"))
.get_matches();
if matches.get_flag("about") {
println!("activation - activate the device");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider = match common::get_provider(udid, host, pairing_file, "activation-jkcoxson").await
{
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
let activation_client = MobileActivationdClient::new(&*provider); let activation_client = MobileActivationdClient::new(&*provider);
let mut lc = LockdownClient::connect(&*provider) let mut lc = LockdownClient::connect(&*provider)
.await .await
@@ -74,40 +32,19 @@ async fn main() {
.into_string() .into_string()
.unwrap(); .unwrap();
if matches.subcommand_matches("state").is_some() { let (sub_name, _sub_args) = arguments.first_subcommand().expect("no subarg passed");
let s = activation_client.state().await.expect("no state");
println!("Activation State: {s}"); match sub_name.as_str() {
} else if matches.subcommand_matches("deactivate").is_some() { "state" => {
println!("CAUTION: You are deactivating {udid}, press enter to continue."); let s = activation_client.state().await.expect("no state");
let mut input = String::new(); println!("Activation State: {s}");
std::io::stdin().read_line(&mut input).ok(); }
activation_client.deactivate().await.expect("no deactivate"); "deactivate" => {
// } else if matches.subcommand_matches("accept").is_some() { println!("CAUTION: You are deactivating {udid}, press enter to continue.");
// amfi_client let mut input = String::new();
// .accept_developer_mode() std::io::stdin().read_line(&mut input).ok();
// .await activation_client.deactivate().await.expect("no deactivate");
// .expect("Failed to show"); }
// } else if matches.subcommand_matches("status").is_some() { _ => unreachable!(),
// let status = amfi_client
// .get_developer_mode_status()
// .await
// .expect("Failed to get status");
// println!("Enabled: {status}");
// } else if let Some(matches) = matches.subcommand_matches("state") {
// let uuid: &String = match matches.get_one("uuid") {
// Some(u) => u,
// None => {
// eprintln!("No UUID passed. Invalid usage, pass -h for help");
// return;
// }
// };
// let status = amfi_client
// .trust_app_signer(uuid)
// .await
// .expect("Failed to get state");
// println!("Enabled: {status}");
} else {
eprintln!("Invalid usage, pass -h for help");
} }
return;
} }

View File

@@ -2,130 +2,119 @@
use std::path::PathBuf; use std::path::PathBuf;
use clap::{Arg, Command, value_parser};
use idevice::{ use idevice::{
IdeviceService, IdeviceService,
afc::{AfcClient, opcode::AfcFopenMode}, afc::{AfcClient, opcode::AfcFopenMode},
house_arrest::HouseArrestClient, house_arrest::HouseArrestClient,
provider::IdeviceProvider,
}; };
use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag};
mod common; const DOCS_HELP: &str = "Read the documents from a bundle. Note that when vending documents, you can only access files in /Documents";
#[tokio::main] pub fn register() -> JkCommand {
async fn main() { JkCommand::new()
tracing_subscriber::fmt::init(); .help("Manage files in the AFC jail of a device")
.with_flag(
let matches = Command::new("afc") JkFlag::new("documents")
.about("Manage files on the device") .with_help(DOCS_HELP)
.arg( .with_argument(JkArgument::new().required(true)),
Arg::new("host")
.long("host")
.value_name("HOST")
.help("IP address of the device"),
) )
.arg( .with_flag(
Arg::new("pairing_file") JkFlag::new("container")
.long("pairing-file") .with_help("Read the container contents of a bundle")
.value_name("PATH") .with_argument(JkArgument::new().required(true)),
.help("Path to the pairing file"),
) )
.arg( .with_subcommand(
Arg::new("udid") "list",
.long("udid") JkCommand::new()
.value_name("UDID") .help("Lists the items in the directory")
.help("UDID of the device (overrides host/pairing file)"), .with_argument(
) JkArgument::new()
.arg(
Arg::new("documents")
.long("documents")
.value_name("BUNDLE_ID")
.help("Read the documents from a bundle. Note that when vending documents, you can only access files in /Documents")
.global(true),
)
.arg(
Arg::new("container")
.long("container")
.value_name("BUNDLE_ID")
.help("Read the container contents of a bundle")
.global(true),
)
.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("download")
.about("Downloads a file")
.arg(Arg::new("path").required(true).index(1))
.arg(Arg::new("save").required(true).index(2)),
)
.subcommand(
Command::new("upload")
.about("Creates a directory")
.arg(
Arg::new("file")
.required(true) .required(true)
.index(1) .with_help("The directory to list in"),
.value_parser(value_parser!(PathBuf)), ),
)
.with_subcommand(
"download",
JkCommand::new()
.help("Download a file")
.with_argument(
JkArgument::new()
.required(true)
.with_help("Path in the AFC jail"),
) )
.arg(Arg::new("path").required(true).index(2)), .with_argument(
JkArgument::new()
.required(true)
.with_help("Path to save file to"),
),
) )
.subcommand( .with_subcommand(
Command::new("mkdir") "upload",
.about("Creates a directory") JkCommand::new()
.arg(Arg::new("path").required(true).index(1)), .help("Upload a file")
.with_argument(
JkArgument::new()
.required(true)
.with_help("Path to the file to upload"),
)
.with_argument(
JkArgument::new()
.required(true)
.with_help("Path to save file to in the AFC jail"),
),
) )
.subcommand( .with_subcommand(
Command::new("remove") "mkdir",
.about("Remove a provisioning profile") JkCommand::new().help("Create a folder").with_argument(
.arg(Arg::new("path").required(true).index(1)), JkArgument::new()
.required(true)
.with_help("Path to the folder to create in the AFC jail"),
),
) )
.subcommand( .with_subcommand(
Command::new("remove_all") "remove",
.about("Remove a provisioning profile") JkCommand::new().help("Remove a file").with_argument(
.arg(Arg::new("path").required(true).index(1)), JkArgument::new()
.required(true)
.with_help("Path to the file to remove"),
),
) )
.subcommand( .with_subcommand(
Command::new("info") "remove_all",
.about("Get info about a file") JkCommand::new().help("Remove a folder").with_argument(
.arg(Arg::new("path").required(true).index(1)), JkArgument::new()
.required(true)
.with_help("Path to the folder to remove"),
),
) )
.subcommand(Command::new("device_info").about("Get info about the device")) .with_subcommand(
.get_matches(); "info",
JkCommand::new()
.help("Get info about a file")
.with_argument(
JkArgument::new()
.required(true)
.with_help("Path to the file to get info for"),
),
)
.with_subcommand(
"device_info",
JkCommand::new().help("Get info about the device"),
)
.subcommand_required(true)
}
if matches.get_flag("about") { pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
println!("afc"); let mut afc_client = if let Some(bundle_id) = arguments.get_flag::<String>("container") {
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("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 = if let Some(bundle_id) = matches.get_one::<String>("container") {
let h = HouseArrestClient::connect(&*provider) let h = HouseArrestClient::connect(&*provider)
.await .await
.expect("Failed to connect to house arrest"); .expect("Failed to connect to house arrest");
h.vend_container(bundle_id) h.vend_container(bundle_id)
.await .await
.expect("Failed to vend container") .expect("Failed to vend container")
} else if let Some(bundle_id) = matches.get_one::<String>("documents") { } else if let Some(bundle_id) = arguments.get_flag::<String>("documents") {
let h = HouseArrestClient::connect(&*provider) let h = HouseArrestClient::connect(&*provider)
.await .await
.expect("Failed to connect to house arrest"); .expect("Failed to connect to house arrest");
@@ -138,59 +127,72 @@ async fn main() {
.expect("Unable to connect to misagent") .expect("Unable to connect to misagent")
}; };
if let Some(matches) = matches.subcommand_matches("list") { let (sub_name, sub_args) = arguments.first_subcommand().unwrap();
let path = matches.get_one::<String>("path").expect("No path passed"); let mut sub_args = sub_args.clone();
let res = afc_client.list_dir(path).await.expect("Failed to read dir"); match sub_name.as_str() {
println!("{path}\n{res:#?}"); "list" => {
} else if let Some(matches) = matches.subcommand_matches("mkdir") { let path = sub_args.next_argument::<String>().expect("No path passed");
let path = matches.get_one::<String>("path").expect("No path passed"); let res = afc_client
afc_client.mk_dir(path).await.expect("Failed to mkdir"); .list_dir(&path)
} else if let Some(matches) = matches.subcommand_matches("download") { .await
let path = matches.get_one::<String>("path").expect("No path passed"); .expect("Failed to read dir");
let save = matches.get_one::<String>("save").expect("No path passed"); println!("{path}\n{res:#?}");
}
"mkdir" => {
let path = sub_args.next_argument::<String>().expect("No path passed");
afc_client.mk_dir(path).await.expect("Failed to mkdir");
}
"download" => {
let path = sub_args.next_argument::<String>().expect("No path passed");
let save = sub_args.next_argument::<String>().expect("No path passed");
let mut file = afc_client let mut file = afc_client
.open(path, AfcFopenMode::RdOnly) .open(path, AfcFopenMode::RdOnly)
.await .await
.expect("Failed to open"); .expect("Failed to open");
let res = file.read_entire().await.expect("Failed to read"); let res = file.read_entire().await.expect("Failed to read");
tokio::fs::write(save, res) tokio::fs::write(save, res)
.await .await
.expect("Failed to write to file"); .expect("Failed to write to file");
} else if let Some(matches) = matches.subcommand_matches("upload") { }
let file = matches.get_one::<PathBuf>("file").expect("No path passed"); "upload" => {
let path = matches.get_one::<String>("path").expect("No path passed"); let file = sub_args.next_argument::<PathBuf>().expect("No path passed");
let path = sub_args.next_argument::<String>().expect("No path passed");
let bytes = tokio::fs::read(file).await.expect("Failed to read file"); let bytes = tokio::fs::read(file).await.expect("Failed to read file");
let mut file = afc_client let mut file = afc_client
.open(path, AfcFopenMode::WrOnly) .open(path, AfcFopenMode::WrOnly)
.await .await
.expect("Failed to open"); .expect("Failed to open");
file.write_entire(&bytes) file.write_entire(&bytes)
.await .await
.expect("Failed to upload bytes"); .expect("Failed to upload bytes");
} else if let Some(matches) = matches.subcommand_matches("remove") { }
let path = matches.get_one::<String>("path").expect("No path passed"); "remove" => {
afc_client.remove(path).await.expect("Failed to remove"); let path = sub_args.next_argument::<String>().expect("No path passed");
} else if let Some(matches) = matches.subcommand_matches("remove_all") { afc_client.remove(path).await.expect("Failed to remove");
let path = matches.get_one::<String>("path").expect("No path passed"); }
afc_client.remove_all(path).await.expect("Failed to remove"); "remove_all" => {
} else if let Some(matches) = matches.subcommand_matches("info") { let path = sub_args.next_argument::<String>().expect("No path passed");
let path = matches.get_one::<String>("path").expect("No path passed"); afc_client.remove_all(path).await.expect("Failed to remove");
let res = afc_client }
.get_file_info(path) "info" => {
.await let path = sub_args.next_argument::<String>().expect("No path passed");
.expect("Failed to get file info"); let res = afc_client
println!("{res:#?}"); .get_file_info(path)
} else if matches.subcommand_matches("device_info").is_some() { .await
let res = afc_client .expect("Failed to get file info");
.get_device_info() println!("{res:#?}");
.await }
.expect("Failed to get file info"); "device_info" => {
println!("{res:#?}"); let res = afc_client
} else { .get_device_info()
eprintln!("Invalid usage, pass -h for help"); .await
.expect("Failed to get file info");
println!("{res:#?}");
}
_ => unreachable!(),
} }
} }

View File

@@ -1,109 +1,81 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command}; use idevice::{IdeviceService, amfi::AmfiClient, provider::IdeviceProvider};
use idevice::{IdeviceService, amfi::AmfiClient}; use jkcli::{CollectedArguments, JkArgument, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new()
#[tokio::main] .help("Mess with devleoper mode")
async fn main() { .with_subcommand(
tracing_subscriber::fmt::init(); "show",
JkCommand::new().help("Shows the developer mode option in settings"),
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( .with_subcommand("enable", JkCommand::new().help("Enables developer mode"))
Arg::new("pairing_file") .with_subcommand(
.long("pairing-file") "accept",
.value_name("PATH") JkCommand::new().help("Shows the accept dialogue for developer mode"),
.help("Path to the pairing file"),
) )
.arg( .with_subcommand(
Arg::new("udid") "status",
.value_name("UDID") JkCommand::new().help("Gets the developer mode status"),
.help("UDID of the device (overrides host/pairing file)")
.index(1),
) )
.arg( .with_subcommand(
Arg::new("about") "trust",
.long("about") JkCommand::new()
.help("Show about information") .help("Trusts an app signer")
.action(clap::ArgAction::SetTrue), .with_argument(JkArgument::new().with_help("UUID").required(true)),
) )
.subcommand(Command::new("show").about("Shows the developer mode option in settings")) .subcommand_required(true)
.subcommand(Command::new("enable").about("Enables developer mode")) }
.subcommand(Command::new("accept").about("Shows the accept dialogue for developer mode"))
.subcommand(Command::new("status").about("Gets the developer mode status"))
.subcommand(
Command::new("trust")
.about("Trusts an app signer")
.arg(Arg::new("uuid").required(true)),
)
.get_matches();
if matches.get_flag("about") {
println!("amfi - manage developer mode");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider = match common::get_provider(udid, host, pairing_file, "amfi-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
let mut amfi_client = AmfiClient::connect(&*provider) let mut amfi_client = AmfiClient::connect(&*provider)
.await .await
.expect("Failed to connect to amfi"); .expect("Failed to connect to amfi");
if matches.subcommand_matches("show").is_some() { let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand passed");
amfi_client let mut sub_args = sub_args.clone();
.reveal_developer_mode_option_in_ui()
.await match sub_name.as_str() {
.expect("Failed to show"); "show" => {
} else if matches.subcommand_matches("enable").is_some() { amfi_client
amfi_client .reveal_developer_mode_option_in_ui()
.enable_developer_mode() .await
.await .expect("Failed to show");
.expect("Failed to show"); }
} else if matches.subcommand_matches("accept").is_some() { "enable" => {
amfi_client amfi_client
.accept_developer_mode() .enable_developer_mode()
.await .await
.expect("Failed to show"); .expect("Failed to show");
} else if matches.subcommand_matches("status").is_some() { }
let status = amfi_client "accept" => {
.get_developer_mode_status() amfi_client
.await .accept_developer_mode()
.expect("Failed to get status"); .await
println!("Enabled: {status}"); .expect("Failed to show");
} else if let Some(matches) = matches.subcommand_matches("state") { }
let uuid: &String = match matches.get_one("uuid") { "status" => {
Some(u) => u, let status = amfi_client
None => { .get_developer_mode_status()
eprintln!("No UUID passed. Invalid usage, pass -h for help"); .await
return; .expect("Failed to get status");
} println!("Enabled: {status}");
}; }
let status = amfi_client "trust" => {
.trust_app_signer(uuid) let uuid: String = match sub_args.next_argument() {
.await Some(u) => u,
.expect("Failed to get state"); None => {
println!("Enabled: {status}"); eprintln!("No UUID passed. Invalid usage, pass -h for help");
} else { return;
eprintln!("Invalid usage, pass -h for help"); }
};
let status = amfi_client
.trust_app_signer(uuid)
.await
.expect("Failed to get state");
println!("Enabled: {status}");
}
_ => unreachable!(),
} }
return;
} }

View File

@@ -1,111 +1,72 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command};
use idevice::{ use idevice::{
IdeviceService, RsdService, IdeviceService, RsdService,
core_device::{AppServiceClient, OpenStdioSocketClient}, core_device::{AppServiceClient, OpenStdioSocketClient},
core_device_proxy::CoreDeviceProxy, core_device_proxy::CoreDeviceProxy,
provider::IdeviceProvider,
rsd::RsdHandshake, rsd::RsdHandshake,
}; };
use jkcli::{CollectedArguments, JkArgument, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new()
#[tokio::main] .help("Interact with the RemoteXPC app service on the device")
async fn main() { .with_subcommand("list", JkCommand::new().help("List apps on the device"))
tracing_subscriber::fmt::init(); .with_subcommand(
"launch",
let matches = Command::new("remotexpc") JkCommand::new()
.about("Get services from RemoteXPC") .help("Launch an app on the device")
.arg( .with_argument(
Arg::new("host") JkArgument::new()
.long("host") .with_help("Bundle ID to launch")
.value_name("HOST") .required(true),
.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("tunneld")
.long("tunneld")
.help("Use tunneld")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.subcommand(Command::new("list").about("Lists the images mounted on the device"))
.subcommand(
Command::new("launch")
.about("Launch the app on the device")
.arg(
Arg::new("bundle_id")
.required(true)
.help("The bundle ID to launch"),
), ),
) )
.subcommand(Command::new("processes").about("List the processes running")) .with_subcommand(
.subcommand( "processes",
Command::new("uninstall").about("Uninstall an app").arg( JkCommand::new().help("List the processes running"),
Arg::new("bundle_id") )
.required(true) .with_subcommand(
.help("The bundle ID to uninstall"), "uninstall",
JkCommand::new().help("Uninstall an app").with_argument(
JkArgument::new()
.with_help("Bundle ID to uninstall")
.required(true),
), ),
) )
.subcommand( .with_subcommand(
Command::new("signal") "signal",
.about("Send a signal to an app") JkCommand::new()
.arg(Arg::new("pid").required(true).help("PID to send to")) .help("Uninstall an app")
.arg(Arg::new("signal").required(true).help("Signal to send")), .with_argument(JkArgument::new().with_help("PID to signal").required(true))
.with_argument(JkArgument::new().with_help("Signal to send").required(true)),
) )
.subcommand( .with_subcommand(
Command::new("icon") "icon",
.about("Send a signal to an app") JkCommand::new()
.arg( .help("Fetch an icon for an app")
Arg::new("bundle_id") .with_argument(
.required(true) JkArgument::new()
.help("The bundle ID to fetch"), .with_help("Bundle ID for the app")
.required(true),
) )
.arg( .with_argument(
Arg::new("path") JkArgument::new()
.required(true) .with_help("Path to save it to")
.help("The path to save the icon to"), .required(true),
) )
.arg(Arg::new("hw").required(false).help("The height and width")) .with_argument(
.arg(Arg::new("scale").required(false).help("The scale")), JkArgument::new()
.with_help("Height and width")
.required(true),
)
.with_argument(JkArgument::new().with_help("Scale").required(true)),
) )
.get_matches(); .subcommand_required(true)
}
if matches.get_flag("about") { pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
println!("debug_proxy - connect to the debug proxy and run commands");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let pairing_file = matches.get_one::<String>("pairing_file");
let host = matches.get_one::<String>("host");
let provider =
match common::get_provider(udid, host, pairing_file, "app_service-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let proxy = CoreDeviceProxy::connect(&*provider) let proxy = CoreDeviceProxy::connect(&*provider)
.await .await
.expect("no core proxy"); .expect("no core proxy");
@@ -123,121 +84,122 @@ async fn main() {
.await .await
.expect("no connect"); .expect("no connect");
if matches.subcommand_matches("list").is_some() { let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand");
let apps = asc let mut sub_args = sub_args.clone();
.list_apps(true, true, true, true, true)
.await
.expect("Failed to get apps");
println!("{apps:#?}");
} else if let Some(matches) = matches.subcommand_matches("launch") {
let bundle_id: &String = match matches.get_one("bundle_id") {
Some(b) => b,
None => {
eprintln!("No bundle ID passed");
return;
}
};
let mut stdio_conn = OpenStdioSocketClient::connect_rsd(&mut adapter, &mut handshake) match sub_name.as_str() {
.await "list" => {
.expect("no stdio"); let apps = asc
.list_apps(true, true, true, true, true)
let stdio_uuid = stdio_conn.read_uuid().await.expect("no uuid"); .await
println!("stdio uuid: {stdio_uuid:?}"); .expect("Failed to get apps");
println!("{apps:#?}");
let res = asc }
.launch_application(bundle_id, &[], true, false, None, None, Some(stdio_uuid)) "launch" => {
.await let bundle_id: String = match sub_args.next_argument() {
.expect("no launch"); Some(b) => b,
None => {
println!("Launch response {res:#?}"); eprintln!("No bundle ID passed");
return;
let (mut remote_reader, mut remote_writer) = tokio::io::split(stdio_conn.inner);
let mut local_stdin = tokio::io::stdin();
let mut local_stdout = tokio::io::stdout();
tokio::select! {
// Task 1: Copy data from the remote process to local stdout
res = tokio::io::copy(&mut remote_reader, &mut local_stdout) => {
if let Err(e) = res {
eprintln!("Error copying from remote to local: {}", e);
} }
println!("\nRemote connection closed."); };
return;
} let mut stdio_conn = OpenStdioSocketClient::connect_rsd(&mut adapter, &mut handshake)
// Task 2: Copy data from local stdin to the remote process .await
res = tokio::io::copy(&mut local_stdin, &mut remote_writer) => { .expect("no stdio");
if let Err(e) = res {
eprintln!("Error copying from local to remote: {}", e); let stdio_uuid = stdio_conn.read_uuid().await.expect("no uuid");
println!("stdio uuid: {stdio_uuid:?}");
let res = asc
.launch_application(bundle_id, &[], true, false, None, None, Some(stdio_uuid))
.await
.expect("no launch");
println!("Launch response {res:#?}");
let (mut remote_reader, mut remote_writer) = tokio::io::split(stdio_conn.inner);
let mut local_stdin = tokio::io::stdin();
let mut local_stdout = tokio::io::stdout();
tokio::select! {
// Task 1: Copy data from the remote process to local stdout
res = tokio::io::copy(&mut remote_reader, &mut local_stdout) => {
if let Err(e) = res {
eprintln!("Error copying from remote to local: {}", e);
}
println!("\nRemote connection closed.");
}
// Task 2: Copy data from local stdin to the remote process
res = tokio::io::copy(&mut local_stdin, &mut remote_writer) => {
if let Err(e) = res {
eprintln!("Error copying from local to remote: {}", e);
}
println!("\nLocal stdin closed.");
} }
println!("\nLocal stdin closed.");
return;
} }
} }
} else if matches.subcommand_matches("processes").is_some() { "processes" => {
let p = asc.list_processes().await.expect("no processes?"); let p = asc.list_processes().await.expect("no processes?");
println!("{p:#?}"); println!("{p:#?}");
} else if let Some(matches) = matches.subcommand_matches("uninstall") { }
let bundle_id: &String = match matches.get_one("bundle_id") { "uninstall" => {
Some(b) => b, let bundle_id: String = match sub_args.next_argument() {
None => { Some(b) => b,
eprintln!("No bundle ID passed"); None => {
return; eprintln!("No bundle ID passed");
} return;
}; }
};
asc.uninstall_app(bundle_id).await.expect("no launch") asc.uninstall_app(bundle_id).await.expect("no launch")
} else if let Some(matches) = matches.subcommand_matches("signal") { }
let pid: u32 = match matches.get_one::<String>("pid") { "signal" => {
Some(b) => b.parse().expect("failed to parse PID as u32"), let pid: u32 = match sub_args.next_argument() {
None => { Some(b) => b,
eprintln!("No bundle PID passed"); None => {
return; eprintln!("No bundle PID passed");
} return;
}; }
let signal: u32 = match matches.get_one::<String>("signal") { };
Some(b) => b.parse().expect("failed to parse signal as u32"), let signal: u32 = match sub_args.next_argument() {
None => { Some(b) => b,
eprintln!("No bundle signal passed"); None => {
return; eprintln!("No bundle signal passed");
} return;
}; }
};
let res = asc.send_signal(pid, signal).await.expect("no signal"); let res = asc.send_signal(pid, signal).await.expect("no signal");
println!("{res:#?}"); println!("{res:#?}");
} else if let Some(matches) = matches.subcommand_matches("icon") { }
let bundle_id: &String = match matches.get_one("bundle_id") { "icon" => {
Some(b) => b, let bundle_id: String = match sub_args.next_argument() {
None => { Some(b) => b,
eprintln!("No bundle ID passed"); None => {
return; eprintln!("No bundle ID passed");
} return;
}; }
let save_path: &String = match matches.get_one("path") { };
Some(b) => b, let save_path: String = match sub_args.next_argument() {
None => { Some(b) => b,
eprintln!("No bundle ID passed"); None => {
return; eprintln!("No bundle ID passed");
} return;
}; }
let hw: f32 = match matches.get_one::<String>("hw") { };
Some(b) => b.parse().expect("failed to parse PID as f32"), let hw: f32 = sub_args.next_argument().unwrap_or(1.0);
None => 1.0, let scale: f32 = sub_args.next_argument().unwrap_or(1.0);
};
let scale: f32 = match matches.get_one::<String>("scale") {
Some(b) => b.parse().expect("failed to parse signal as f32"),
None => 1.0,
};
let res = asc let res = asc
.fetch_app_icon(bundle_id, hw, hw, scale, true) .fetch_app_icon(bundle_id, hw, hw, scale, true)
.await .await
.expect("no signal"); .expect("no signal");
println!("{res:?}"); println!("{res:?}");
tokio::fs::write(save_path, res.data) tokio::fs::write(save_path, res.data)
.await .await
.expect("failed to save"); .expect("failed to save");
} else { }
eprintln!("Invalid usage, pass -h for help"); _ => unreachable!(),
} }
} }

View File

@@ -1,71 +1,20 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command};
use futures_util::StreamExt; use futures_util::StreamExt;
use idevice::{IdeviceService, bt_packet_logger::BtPacketLoggerClient}; use idevice::{IdeviceService, bt_packet_logger::BtPacketLoggerClient, provider::IdeviceProvider};
use jkcli::{CollectedArguments, JkArgument, JkCommand};
use tokio::io::AsyncWrite; use tokio::io::AsyncWrite;
use crate::pcap::{write_pcap_header, write_pcap_record}; use crate::pcap::{write_pcap_header, write_pcap_record};
mod common; pub fn register() -> JkCommand {
mod pcap; JkCommand::new()
.help("Writes Bluetooth pcap data")
.with_argument(JkArgument::new().with_help("Write PCAP to this file (use '-' for stdout)"))
}
#[tokio::main] pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
async fn main() { let out: Option<String> = arguments.clone().next_argument();
tracing_subscriber::fmt::init();
let matches = Command::new("amfi")
.about("Capture Bluetooth packets")
.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),
)
.arg(
Arg::new("out")
.long("out")
.value_name("PCAP")
.help("Write PCAP to this file (use '-' for stdout)"),
)
.get_matches();
if matches.get_flag("about") {
println!("bt_packet_logger - capture bluetooth packets");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let out = matches.get_one::<String>("out").map(String::to_owned);
let provider = match common::get_provider(udid, host, pairing_file, "amfi-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let logger_client = BtPacketLoggerClient::connect(&*provider) let logger_client = BtPacketLoggerClient::connect(&*provider)
.await .await

View File

@@ -1,83 +1,60 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command, arg};
use idevice::{ use idevice::{
IdeviceService, RsdService, companion_proxy::CompanionProxy, IdeviceService, RsdService, companion_proxy::CompanionProxy,
core_device_proxy::CoreDeviceProxy, pretty_print_dictionary, pretty_print_plist, core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider, rsd::RsdHandshake,
rsd::RsdHandshake,
}; };
use jkcli::{CollectedArguments, JkArgument, JkCommand};
use plist_macro::{pretty_print_dictionary, pretty_print_plist};
mod common; pub fn register() -> JkCommand {
JkCommand::new()
#[tokio::main] .help("Apple Watch proxy")
async fn main() { .with_subcommand(
tracing_subscriber::fmt::init(); "list",
JkCommand::new().help("List the companions on the device"),
let matches = Command::new("companion_proxy") )
.about("Apple Watch things") .with_subcommand("listen", JkCommand::new().help("Listen for devices"))
.arg( .with_subcommand(
Arg::new("host") "get",
.long("host") JkCommand::new()
.value_name("HOST") .help("Gets a value from an AW")
.help("IP address of the device"), .with_argument(
) JkArgument::new()
.arg( .with_help("The AW UDID to get from")
Arg::new("pairing_file") .required(true),
.long("pairing-file") )
.value_name("PATH") .with_argument(
.help("Path to the pairing file"), JkArgument::new()
) .with_help("The value to get")
.arg( .required(true),
Arg::new("udid") ),
.value_name("UDID") )
.help("UDID of the device (overrides host/pairing file)") .with_subcommand(
.index(1), "start",
) JkCommand::new()
.arg( .help("Starts a service on the Apple Watch")
Arg::new("about") .with_argument(
.long("about") JkArgument::new()
.help("Show about information") .with_help("The port to listen on")
.action(clap::ArgAction::SetTrue), .required(true),
) )
.subcommand(Command::new("list").about("List the companions on the device")) .with_argument(JkArgument::new().with_help("The service name")),
.subcommand(Command::new("listen").about("Listen for devices")) )
.subcommand( .with_subcommand(
Command::new("get") "stop",
.about("Gets a value") JkCommand::new()
.arg(arg!(-d --device_udid <STRING> "the device udid to get from").required(true)) .help("Stops a service on the Apple Watch")
.arg(arg!(-v --value <STRING> "the value to get").required(true)), .with_argument(
) JkArgument::new()
.subcommand( .with_help("The port to stop")
Command::new("start") .required(true),
.about("Starts a service") ),
.arg(arg!(-p --port <PORT> "the port").required(true)) )
.arg(arg!(-n --name <STRING> "the optional service name").required(false)), .subcommand_required(true)
) }
.subcommand(
Command::new("stop")
.about("Starts a service")
.arg(arg!(-p --port <PORT> "the port").required(true)),
)
.get_matches();
if matches.get_flag("about") {
println!("companion_proxy");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider = match common::get_provider(udid, host, pairing_file, "amfi-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
let proxy = CoreDeviceProxy::connect(&*provider) let proxy = CoreDeviceProxy::connect(&*provider)
.await .await
.expect("no core_device_proxy"); .expect("no core_device_proxy");
@@ -97,55 +74,72 @@ async fn main() {
// .await // .await
// .expect("Failed to connect to companion proxy"); // .expect("Failed to connect to companion proxy");
if matches.subcommand_matches("list").is_some() { let (sub_name, sub_args) = arguments.first_subcommand().unwrap();
proxy.get_device_registry().await.expect("Failed to show"); let mut sub_args = sub_args.clone();
} else if matches.subcommand_matches("listen").is_some() {
let mut stream = proxy.listen_for_devices().await.expect("Failed to show");
while let Ok(v) = stream.next().await {
println!("{}", pretty_print_dictionary(&v));
}
} else if let Some(matches) = matches.subcommand_matches("get") {
let key = matches.get_one::<String>("value").expect("no value passed");
let udid = matches
.get_one::<String>("device_udid")
.expect("no AW udid passed");
match proxy.get_value(udid, key).await { match sub_name.as_str() {
Ok(value) => { "list" => {
println!("{}", pretty_print_plist(&value)); proxy.get_device_registry().await.expect("Failed to show");
} }
Err(e) => { "listen" => {
eprintln!("Error getting value: {e}"); let mut stream = proxy.listen_for_devices().await.expect("Failed to show");
while let Ok(v) = stream.next().await {
println!("{}", pretty_print_dictionary(&v));
} }
} }
} else if let Some(matches) = matches.subcommand_matches("start") { "get" => {
let port: u16 = matches let key: String = sub_args.next_argument::<String>().expect("no value passed");
.get_one::<String>("port") let udid = sub_args
.expect("no port passed") .next_argument::<String>()
.parse() .expect("no AW udid passed");
.expect("not a number");
let name = matches.get_one::<String>("name").map(|x| x.as_str());
match proxy.start_forwarding_service_port(port, name, None).await { match proxy.get_value(udid, key).await {
Ok(value) => { Ok(value) => {
println!("started on port {value}"); println!("{}", pretty_print_plist(&value));
}
Err(e) => {
eprintln!("Error getting value: {e}");
}
} }
Err(e) => { }
"start" => {
let port: u16 = sub_args
.next_argument::<String>()
.expect("no port passed")
.parse()
.expect("not a number");
let name = sub_args.next_argument::<String>();
match proxy
.start_forwarding_service_port(
port,
match &name {
Some(n) => Some(n.as_str()),
None => None,
},
None,
)
.await
{
Ok(value) => {
println!("started on port {value}");
}
Err(e) => {
eprintln!("Error starting: {e}");
}
}
}
"stop" => {
let port: u16 = sub_args
.next_argument::<String>()
.expect("no port passed")
.parse()
.expect("not a number");
if let Err(e) = proxy.stop_forwarding_service_port(port).await {
eprintln!("Error starting: {e}"); eprintln!("Error starting: {e}");
} }
} }
} else if let Some(matches) = matches.subcommand_matches("stop") { _ => unreachable!(),
let port: u16 = matches
.get_one::<String>("port")
.expect("no port passed")
.parse()
.expect("not a number");
if let Err(e) = proxy.stop_forwarding_service_port(port).await {
eprintln!("Error starting: {e}");
}
} else {
eprintln!("Invalid usage, pass -h for help");
} }
return;
} }

View File

@@ -1,96 +1,79 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command};
use idevice::{ use idevice::{
IdeviceService, IdeviceService,
crashreportcopymobile::{CrashReportCopyMobileClient, flush_reports}, crashreportcopymobile::{CrashReportCopyMobileClient, flush_reports},
provider::IdeviceProvider,
}; };
use jkcli::{CollectedArguments, JkArgument, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new()
.help("Manage crash logs")
.with_subcommand(
"list",
JkCommand::new()
.help("List crash logs in the directory")
.with_argument(
JkArgument::new()
.with_help("Path to list in")
.required(true),
),
)
.with_subcommand(
"flush",
JkCommand::new().help("Flushes reports to the directory"),
)
.with_subcommand(
"pull",
JkCommand::new()
.help("Check the capabilities")
.with_argument(
JkArgument::new()
.with_help("Path to the log to pull")
.required(true),
)
.with_argument(
JkArgument::new()
.with_help("Path to save the log to")
.required(true),
),
)
.subcommand_required(true)
}
#[tokio::main] pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
async fn main() {
tracing_subscriber::fmt::init();
let matches = Command::new("crash_logs")
.about("Manage crash logs")
.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("dir").required(false).index(1)),
)
.subcommand(Command::new("flush").about("Flushes reports to the directory"))
.subcommand(
Command::new("pull")
.about("Pulls a log")
.arg(Arg::new("path").required(true).index(1))
.arg(Arg::new("save").required(true).index(2))
.arg(Arg::new("dir").required(false).index(3)),
)
.get_matches();
if matches.get_flag("about") {
println!("crash_logs - manage crash logs on the device");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("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 crash_client = CrashReportCopyMobileClient::connect(&*provider) let mut crash_client = CrashReportCopyMobileClient::connect(&*provider)
.await .await
.expect("Unable to connect to misagent"); .expect("Unable to connect to misagent");
if let Some(matches) = matches.subcommand_matches("list") { let (sub_name, sub_args) = arguments.first_subcommand().expect("No sub command passed");
let dir_path: Option<&String> = matches.get_one("dir"); let mut sub_args = sub_args.clone();
let res = crash_client
.ls(dir_path.map(|x| x.as_str()))
.await
.expect("Failed to read dir");
println!("{res:#?}");
} else if matches.subcommand_matches("flush").is_some() {
flush_reports(&*provider).await.expect("Failed to flush");
} else if let Some(matches) = matches.subcommand_matches("pull") {
let path = matches.get_one::<String>("path").expect("No path passed");
let save = matches.get_one::<String>("save").expect("No path passed");
let res = crash_client.pull(path).await.expect("Failed to pull log"); match sub_name.as_str() {
tokio::fs::write(save, res) "list" => {
.await let dir_path: Option<String> = sub_args.next_argument();
.expect("Failed to write to file"); let res = crash_client
} else { .ls(match &dir_path {
eprintln!("Invalid usage, pass -h for help"); Some(d) => Some(d.as_str()),
None => None,
})
.await
.expect("Failed to read dir");
println!("{res:#?}");
}
"flush" => {
flush_reports(&*provider).await.expect("Failed to flush");
}
"pull" => {
let path = sub_args.next_argument::<String>().expect("No path passed");
let save = sub_args.next_argument::<String>().expect("No path passed");
let res = crash_client.pull(path).await.expect("Failed to pull log");
tokio::fs::write(save, res)
.await
.expect("Failed to write to file");
}
_ => unreachable!(),
} }
} }

View File

@@ -2,70 +2,17 @@
use std::io::Write; use std::io::Write;
use clap::{Arg, Command};
use idevice::{ use idevice::{
IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, debug_proxy::DebugProxyClient, IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, debug_proxy::DebugProxyClient,
rsd::RsdHandshake, provider::IdeviceProvider, rsd::RsdHandshake,
}; };
use jkcli::{CollectedArguments, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new().help("Start a debug proxy shell")
}
#[tokio::main] pub async fn main(_arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
async fn main() {
tracing_subscriber::fmt::init();
let matches = Command::new("remotexpc")
.about("Get services from RemoteXPC")
.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("tunneld")
.long("tunneld")
.help("Use tunneld")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.get_matches();
if matches.get_flag("about") {
println!("debug_proxy - connect to the debug proxy and run commands");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let pairing_file = matches.get_one::<String>("pairing_file");
let host = matches.get_one::<String>("host");
let provider =
match common::get_provider(udid, host, pairing_file, "debug-proxy-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let proxy = CoreDeviceProxy::connect(&*provider) let proxy = CoreDeviceProxy::connect(&*provider)
.await .await
.expect("no core proxy"); .expect("no core proxy");

View File

@@ -1,106 +1,71 @@
// Jackson Coxson // Jackson Coxson
// idevice Rust implementation of libimobiledevice's idevicediagnostics // idevice Rust implementation of libimobiledevice's idevicediagnostics
use clap::{Arg, ArgMatches, Command}; use idevice::{
use idevice::{IdeviceService, services::diagnostics_relay::DiagnosticsRelayClient}; IdeviceService, provider::IdeviceProvider, services::diagnostics_relay::DiagnosticsRelayClient,
};
use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag};
mod common; pub fn register() -> JkCommand {
JkCommand::new()
#[tokio::main] .help("Interact with the diagnostics interface of a device")
async fn main() { .with_subcommand(
tracing_subscriber::fmt::init(); "ioregistry",
JkCommand::new()
let matches = Command::new("idevicediagnostics") .help("Print IORegistry information")
.about("Interact with the diagnostics interface of a device") .with_flag(
.arg( JkFlag::new("plane")
Arg::new("host") .with_help("IORegistry plane to query (e.g., IODeviceTree, IOService)")
.long("host") .with_argument(JkArgument::new().required(true)),
.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("ioregistry")
.about("Print IORegistry information")
.arg(
Arg::new("plane")
.long("plane")
.value_name("PLANE")
.help("IORegistry plane to query (e.g., IODeviceTree, IOService)"),
) )
.arg( .with_flag(
Arg::new("name") JkFlag::new("name")
.long("name") .with_help("Entry name to filter by")
.value_name("NAME") .with_argument(JkArgument::new().required(true)),
.help("Entry name to filter by"),
) )
.arg( .with_flag(
Arg::new("class") JkFlag::new("class")
.long("class") .with_help("Entry class to filter by")
.value_name("CLASS") .with_argument(JkArgument::new().required(true)),
.help("Entry class to filter by"),
), ),
) )
.subcommand( .with_subcommand(
Command::new("mobilegestalt") "mobilegestalt",
.about("Print MobileGestalt information") JkCommand::new()
.arg( .help("Print MobileGestalt information")
Arg::new("keys") .with_argument(
.long("keys") JkArgument::new()
.value_name("KEYS") .with_help("Comma-separated list of keys to query")
.help("Comma-separated list of keys to query") .required(true),
.value_delimiter(',')
.num_args(1..),
), ),
) )
.subcommand(Command::new("gasguage").about("Print gas gauge (battery) information")) .with_subcommand(
.subcommand(Command::new("nand").about("Print NAND flash information")) "gasguage",
.subcommand(Command::new("all").about("Print all available diagnostics information")) JkCommand::new().help("Print gas gauge (battery) information"),
.subcommand(Command::new("wifi").about("Print WiFi diagnostics information")) )
.subcommand(Command::new("goodbye").about("Send Goodbye to diagnostics relay")) .with_subcommand(
.subcommand(Command::new("restart").about("Restart the device")) "nand",
.subcommand(Command::new("shutdown").about("Shutdown the device")) JkCommand::new().help("Print NAND flash information"),
.subcommand(Command::new("sleep").about("Put the device to sleep")) )
.get_matches(); .with_subcommand(
"all",
if matches.get_flag("about") { JkCommand::new().help("Print all available diagnostics information"),
println!( )
"idevicediagnostics - interact with the diagnostics interface of a device. Reimplementation of libimobiledevice's binary." .with_subcommand(
); "wifi",
println!("Copyright (c) 2025 Jackson Coxson"); JkCommand::new().help("Print WiFi diagnostics information"),
return; )
} .with_subcommand(
"goodbye",
let udid = matches.get_one::<String>("udid"); JkCommand::new().help("Send Goodbye to diagnostics relay"),
let host = matches.get_one::<String>("host"); )
let pairing_file = matches.get_one::<String>("pairing_file"); .with_subcommand("restart", JkCommand::new().help("Restart the device"))
.with_subcommand("shutdown", JkCommand::new().help("Shutdown the device"))
let provider = .with_subcommand("sleep", JkCommand::new().help("Put the device to sleep"))
match common::get_provider(udid, host, pairing_file, "idevicediagnostics-jkcoxson").await { .subcommand_required(true)
Ok(p) => p, }
Err(e) => {
eprintln!("{e}");
return;
}
};
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
let mut diagnostics_client = match DiagnosticsRelayClient::connect(&*provider).await { let mut diagnostics_client = match DiagnosticsRelayClient::connect(&*provider).await {
Ok(client) => client, Ok(client) => client,
Err(e) => { Err(e) => {
@@ -109,47 +74,52 @@ async fn main() {
} }
}; };
match matches.subcommand() { let (sub_name, sub_args) = arguments.first_subcommand().unwrap();
Some(("ioregistry", sub_matches)) => { let mut sub_matches = sub_args.clone();
handle_ioregistry(&mut diagnostics_client, sub_matches).await;
match sub_name.as_str() {
"ioregistry" => {
handle_ioregistry(&mut diagnostics_client, &sub_matches).await;
} }
Some(("mobilegestalt", sub_matches)) => { "mobilegestalt" => {
handle_mobilegestalt(&mut diagnostics_client, sub_matches).await; handle_mobilegestalt(&mut diagnostics_client, &mut sub_matches).await;
} }
Some(("gasguage", _)) => { "gasguage" => {
handle_gasguage(&mut diagnostics_client).await; handle_gasguage(&mut diagnostics_client).await;
} }
Some(("nand", _)) => { "nand" => {
handle_nand(&mut diagnostics_client).await; handle_nand(&mut diagnostics_client).await;
} }
Some(("all", _)) => { "all" => {
handle_all(&mut diagnostics_client).await; handle_all(&mut diagnostics_client).await;
} }
Some(("wifi", _)) => { "wifi" => {
handle_wifi(&mut diagnostics_client).await; handle_wifi(&mut diagnostics_client).await;
} }
Some(("restart", _)) => { "restart" => {
handle_restart(&mut diagnostics_client).await; handle_restart(&mut diagnostics_client).await;
} }
Some(("shutdown", _)) => { "shutdown" => {
handle_shutdown(&mut diagnostics_client).await; handle_shutdown(&mut diagnostics_client).await;
} }
Some(("sleep", _)) => { "sleep" => {
handle_sleep(&mut diagnostics_client).await; handle_sleep(&mut diagnostics_client).await;
} }
Some(("goodbye", _)) => { "goodbye" => {
handle_goodbye(&mut diagnostics_client).await; handle_goodbye(&mut diagnostics_client).await;
} }
_ => { _ => unreachable!(),
eprintln!("No subcommand specified. Use --help for usage information.");
}
} }
} }
async fn handle_ioregistry(client: &mut DiagnosticsRelayClient, matches: &ArgMatches) { async fn handle_ioregistry(client: &mut DiagnosticsRelayClient, matches: &CollectedArguments) {
let plane = matches.get_one::<String>("plane").map(|s| s.as_str()); let plane = matches.get_flag::<String>("plane");
let name = matches.get_one::<String>("name").map(|s| s.as_str()); let name = matches.get_flag::<String>("name");
let class = matches.get_one::<String>("class").map(|s| s.as_str()); let class = matches.get_flag::<String>("class");
let plane = plane.as_deref();
let name = name.as_deref();
let class = class.as_deref();
match client.ioregistry(plane, name, class).await { match client.ioregistry(plane, name, class).await {
Ok(Some(data)) => { Ok(Some(data)) => {
@@ -164,12 +134,14 @@ async fn handle_ioregistry(client: &mut DiagnosticsRelayClient, matches: &ArgMat
} }
} }
async fn handle_mobilegestalt(client: &mut DiagnosticsRelayClient, matches: &ArgMatches) { async fn handle_mobilegestalt(
let keys = matches client: &mut DiagnosticsRelayClient,
.get_many::<String>("keys") matches: &mut CollectedArguments,
.map(|values| values.map(|s| s.to_string()).collect::<Vec<_>>()); ) {
let keys = matches.next_argument::<String>().unwrap();
let keys = keys.split(',').map(|x| x.to_string()).collect();
match client.mobilegestalt(keys).await { match client.mobilegestalt(Some(keys)).await {
Ok(Some(data)) => { Ok(Some(data)) => {
println!("{data:#?}"); println!("{data:#?}");
} }

View File

@@ -1,71 +1,18 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command};
use futures_util::StreamExt; use futures_util::StreamExt;
use idevice::{ use idevice::{
IdeviceService, RsdService, core_device::DiagnostisServiceClient, IdeviceService, RsdService, core_device::DiagnostisServiceClient,
core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider, rsd::RsdHandshake,
}; };
use jkcli::{CollectedArguments, JkCommand};
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
mod common; pub fn register() -> JkCommand {
JkCommand::new().help("Retrieve a sysdiagnose")
}
#[tokio::main] pub async fn main(_arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
async fn main() {
tracing_subscriber::fmt::init();
let matches = Command::new("remotexpc")
.about("Gets a sysdiagnose")
.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("tunneld")
.long("tunneld")
.help("Use tunneld")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.get_matches();
if matches.get_flag("about") {
println!("debug_proxy - connect to the debug proxy and run commands");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let pairing_file = matches.get_one::<String>("pairing_file");
let host = matches.get_one::<String>("host");
let provider =
match common::get_provider(udid, host, pairing_file, "diagnosticsservice-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let proxy = CoreDeviceProxy::connect(&*provider) let proxy = CoreDeviceProxy::connect(&*provider)
.await .await
.expect("no core proxy"); .expect("no core proxy");

View File

@@ -1,10 +1,22 @@
// Jackson Coxson // Jackson Coxson
use idevice::dvt::message::Message; use idevice::{dvt::message::Message, provider::IdeviceProvider};
use jkcli::{CollectedArguments, JkArgument, JkCommand};
#[tokio::main] pub fn register() -> JkCommand {
async fn main() { JkCommand::new()
let file = std::env::args().nth(1).expect("No file passed"); .help("Parse a DVT packet from a file")
.with_argument(
JkArgument::new()
.required(true)
.with_help("Path the the packet file"),
)
}
pub async fn main(arguments: &CollectedArguments, _provider: Box<dyn IdeviceProvider>) {
let mut arguments = arguments.clone();
let file: String = arguments.next_argument().expect("No file passed");
let mut bytes = tokio::fs::File::open(file).await.unwrap(); let mut bytes = tokio::fs::File::open(file).await.unwrap();
let message = Message::from_reader(&mut bytes).await.unwrap(); let message = Message::from_reader(&mut bytes).await.unwrap();

View File

@@ -1,60 +1,14 @@
// Jackson Coxson // Jackson Coxson
// Heartbeat client // Heartbeat client
use clap::{Arg, Command}; use idevice::{IdeviceService, heartbeat::HeartbeatClient, provider::IdeviceProvider};
use idevice::{IdeviceService, heartbeat::HeartbeatClient}; use jkcli::{CollectedArguments, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new().help("heartbeat a device")
}
#[tokio::main] pub async fn main(_arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
async fn main() {
tracing_subscriber::fmt::init();
let matches = Command::new("core_device_proxy_tun")
.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)")
.index(1),
)
.arg(
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.get_matches();
if matches.get_flag("about") {
println!("heartbeat_client - heartbeat a device");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider =
match common::get_provider(udid, host, pairing_file, "heartbeat_client-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let mut heartbeat_client = HeartbeatClient::connect(&*provider) let mut heartbeat_client = HeartbeatClient::connect(&*provider)
.await .await
.expect("Unable to connect to heartbeat"); .expect("Unable to connect to heartbeat");

View File

@@ -1,64 +1,14 @@
// Jackson Coxson // Jackson Coxson
// idevice Rust implementation of libimobiledevice's ideviceinfo // idevice Rust implementation of libimobiledevice's ideviceinfo
use clap::{Arg, Command}; use idevice::{IdeviceService, lockdown::LockdownClient, provider::IdeviceProvider};
use idevice::{IdeviceService, lockdown::LockdownClient}; use jkcli::{CollectedArguments, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new().help("ideviceinfo - get information from the idevice. Reimplementation of libimobiledevice's binary.")
#[tokio::main] }
async fn main() {
tracing_subscriber::fmt::init();
let matches = Command::new("core_device_proxy_tun")
.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)")
.index(1),
)
.arg(
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.get_matches();
if matches.get_flag("about") {
println!(
"ideviceinfo - get information from the idevice. Reimplementation of libimobiledevice's binary."
);
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider =
match common::get_provider(udid, host, pairing_file, "ideviceinfo-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
pub async fn main(_arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
let mut lockdown_client = match LockdownClient::connect(&*provider).await { let mut lockdown_client = match LockdownClient::connect(&*provider).await {
Ok(l) => l, Ok(l) => l,
Err(e) => { Err(e) => {

View File

@@ -1,103 +1,73 @@
// A minimal ideviceinstaller-like CLI to install/upgrade apps // A minimal ideviceinstaller-like CLI to install/upgrade apps
use clap::{Arg, ArgAction, Command}; use idevice::{provider::IdeviceProvider, utils::installation};
use idevice::utils::installation; use jkcli::{CollectedArguments, JkArgument, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new()
.help("Manage files in the AFC jail of a device")
.with_subcommand(
"install",
JkCommand::new()
.help("Install a local .ipa or directory")
.with_argument(
JkArgument::new()
.required(true)
.with_help("Path to the .ipa or directory containing the app"),
),
)
.with_subcommand(
"upgrade",
JkCommand::new()
.help("Install a local .ipa or directory")
.with_argument(
JkArgument::new()
.required(true)
.with_help("Path to the .ipa or directory containing the app"),
),
)
.subcommand_required(true)
}
#[tokio::main] pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
async fn main() { let (sub_name, sub_args) = arguments.first_subcommand().expect("no sub arg");
tracing_subscriber::fmt::init(); let mut sub_args = sub_args.clone();
let matches = Command::new("ideviceinstaller") match sub_name.as_str() {
.about("Install/upgrade apps on an iOS device (AFC + InstallationProxy)") "install" => {
.arg( let path: String = sub_args.next_argument().expect("required");
Arg::new("host") match installation::install_package_with_callback(
.long("host") &*provider,
.value_name("HOST") path,
.help("IP address of the device"), None,
) |(percentage, _)| async move {
.arg( println!("Installing: {percentage}%");
Arg::new("pairing_file") },
.long("pairing-file") (),
.value_name("PATH") )
.help("Path to the pairing file"), .await
) {
.arg( Ok(()) => println!("install success"),
Arg::new("udid") Err(e) => eprintln!("Install failed: {e}"),
.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(ArgAction::SetTrue),
)
.subcommand(
Command::new("install")
.about("Install a local .ipa or directory")
.arg(Arg::new("path").required(true).value_name("PATH")),
)
.subcommand(
Command::new("upgrade")
.about("Upgrade from a local .ipa or directory")
.arg(Arg::new("path").required(true).value_name("PATH")),
)
.get_matches();
if matches.get_flag("about") {
println!("ideviceinstaller - install/upgrade apps using AFC + InstallationProxy (Rust)");
println!("Copyright (c) 2025");
return;
}
let udid = matches.get_one::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider = match common::get_provider(udid, host, pairing_file, "ideviceinstaller").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
} }
}; "upgrade" => {
let path: String = sub_args.next_argument().expect("required");
if let Some(matches) = matches.subcommand_matches("install") { match installation::upgrade_package_with_callback(
let path: &String = matches.get_one("path").expect("required"); &*provider,
match installation::install_package_with_callback( path,
&*provider, None,
path, |(percentage, _)| async move {
None, println!("Upgrading: {percentage}%");
|(percentage, _)| async move { },
println!("Installing: {percentage}%"); (),
}, )
(), .await
) {
.await Ok(()) => println!("upgrade success"),
{ Err(e) => eprintln!("Upgrade failed: {e}"),
Ok(()) => println!("install success"), }
Err(e) => eprintln!("Install failed: {e}"),
} }
} else if let Some(matches) = matches.subcommand_matches("upgrade") { _ => unreachable!(),
let path: &String = matches.get_one("path").expect("required");
match installation::upgrade_package_with_callback(
&*provider,
path,
None,
|(percentage, _)| async move {
println!("Upgrading: {percentage}%");
},
(),
)
.await
{
Ok(()) => println!("upgrade success"),
Err(e) => eprintln!("Upgrade failed: {e}"),
}
} else {
eprintln!("Invalid usage, pass -h for help");
} }
} }

View File

@@ -1,87 +1,39 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command};
use idevice::{ use idevice::{
IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy,
installcoordination_proxy::InstallcoordinationProxy, rsd::RsdHandshake, installcoordination_proxy::InstallcoordinationProxy, provider::IdeviceProvider,
rsd::RsdHandshake,
}; };
use jkcli::{CollectedArguments, JkArgument, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new()
#[tokio::main] .help("Interact with the RemoteXPC installation coordination proxy")
async fn main() { .with_subcommand(
tracing_subscriber::fmt::init(); "info",
JkCommand::new()
let matches = Command::new("installationcoordination_proxy") .help("Get info about an app on the device")
.about("") .with_argument(
.arg( JkArgument::new()
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("tunneld")
.long("tunneld")
.help("Use tunneld")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.subcommand(
Command::new("info")
.about("Get info about an app on the device")
.arg(
Arg::new("bundle_id")
.required(true) .required(true)
.help("The bundle ID to query"), .with_help("The bundle ID to query"),
), ),
) )
.subcommand( .with_subcommand(
Command::new("uninstall") "uninstall",
.about("Get info about an app on the device") JkCommand::new()
.arg( .help("Uninstalls an app on the device")
Arg::new("bundle_id") .with_argument(
JkArgument::new()
.required(true) .required(true)
.help("The bundle ID to query"), .with_help("The bundle ID to delete"),
), ),
) )
.get_matches(); .subcommand_required(true)
}
if matches.get_flag("about") { pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
println!("debug_proxy - connect to the debug proxy and run commands");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let pairing_file = matches.get_one::<String>("pairing_file");
let host = matches.get_one::<String>("host");
let provider =
match common::get_provider(udid, host, pairing_file, "app_service-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let proxy = CoreDeviceProxy::connect(&*provider) let proxy = CoreDeviceProxy::connect(&*provider)
.await .await
.expect("no core proxy"); .expect("no core proxy");
@@ -103,30 +55,38 @@ async fn main() {
.await .await
.expect("no connect"); .expect("no connect");
if let Some(matches) = matches.subcommand_matches("info") { let (sub_name, sub_args) = arguments.first_subcommand().unwrap();
let bundle_id: &String = match matches.get_one("bundle_id") { let mut sub_args = sub_args.clone();
Some(b) => b,
None => {
eprintln!("No bundle ID passed");
return;
}
};
let res = icp.query_app_path(bundle_id).await.expect("no info"); match sub_name.as_str() {
println!("Path: {res}"); "info" => {
} else if let Some(matches) = matches.subcommand_matches("uninstall") { let bundle_id: String = match sub_args.next_argument() {
let bundle_id: &String = match matches.get_one("bundle_id") { Some(b) => b,
Some(b) => b, None => {
None => { eprintln!("No bundle ID passed");
eprintln!("No bundle ID passed"); return;
return; }
} };
};
icp.uninstall_app(bundle_id) let res = icp
.await .query_app_path(bundle_id.as_str())
.expect("uninstall failed"); .await
} else { .expect("no info");
eprintln!("Invalid usage, pass -h for help"); println!("Path: {res}");
}
"uninstall" => {
let bundle_id: String = match sub_args.next_argument() {
Some(b) => b,
None => {
eprintln!("No bundle ID passed");
return;
}
};
icp.uninstall_app(bundle_id.as_str())
.await
.expect("uninstall failed");
}
_ => unreachable!(),
} }
} }

View File

@@ -1,108 +1,84 @@
// Jackson Coxson // Jackson Coxson
// Just lists apps for now // Just lists apps for now
use clap::{Arg, Command}; use idevice::{
use idevice::{IdeviceService, installation_proxy::InstallationProxyClient}; IdeviceService, installation_proxy::InstallationProxyClient, provider::IdeviceProvider,
};
use jkcli::{CollectedArguments, JkArgument, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new()
#[tokio::main] .help("Manage files in the AFC jail of a device")
async fn main() { .with_subcommand(
tracing_subscriber::fmt::init(); "lookup",
JkCommand::new().help("Gets the apps on the device"),
let matches = Command::new("core_device_proxy_tun")
.about("Start a tunnel")
.arg(
Arg::new("host")
.long("host")
.value_name("HOST")
.help("IP address of the device"),
) )
.arg( .with_subcommand(
Arg::new("pairing_file") "browse",
.long("pairing-file") JkCommand::new().help("Browses the apps on the device"),
.value_name("PATH")
.help("Path to the pairing file"),
) )
.arg( .with_subcommand(
Arg::new("udid") "check_capabilities",
.value_name("UDID") JkCommand::new().help("Check the capabilities"),
.help("UDID of the device (overrides host/pairing file)")
.index(1),
) )
.arg( .with_subcommand(
Arg::new("about") "install",
.long("about") JkCommand::new()
.help("Show about information") .help("Install an app in the AFC jail")
.action(clap::ArgAction::SetTrue), .with_argument(
JkArgument::new()
.required(true)
.with_help("Path in the AFC jail"),
),
) )
.subcommand(Command::new("lookup").about("Gets the apps on the device")) .subcommand_required(true)
.subcommand(Command::new("browse").about("Browses the apps on the device")) }
.subcommand(Command::new("check_capabilities").about("Check the capabilities"))
.subcommand(
Command::new("install")
.about("Install an app in the AFC jail")
.arg(Arg::new("path")),
)
.get_matches();
if matches.get_flag("about") {
println!(
"instproxy - query and manage apps installed on a device. Reimplementation of libimobiledevice's binary."
);
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider = match common::get_provider(udid, host, pairing_file, "instproxy-jkcoxson").await
{
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
let mut instproxy_client = InstallationProxyClient::connect(&*provider) let mut instproxy_client = InstallationProxyClient::connect(&*provider)
.await .await
.expect("Unable to connect to instproxy"); .expect("Unable to connect to instproxy");
if matches.subcommand_matches("lookup").is_some() {
let apps = instproxy_client.get_apps(Some("User"), None).await.unwrap();
for app in apps.keys() {
println!("{app}");
}
} else if matches.subcommand_matches("browse").is_some() {
instproxy_client.browse(None).await.expect("browse failed");
} else if matches.subcommand_matches("check_capabilities").is_some() {
instproxy_client
.check_capabilities_match(Vec::new(), None)
.await
.expect("check failed");
} else if let Some(matches) = matches.subcommand_matches("install") {
let path: &String = match matches.get_one("path") {
Some(p) => p,
None => {
eprintln!("No path passed, pass -h for help");
return;
}
};
instproxy_client let (sub_name, sub_args) = arguments.first_subcommand().expect("no sub arg");
.install_with_callback( let mut sub_args = sub_args.clone();
path,
None, match sub_name.as_str() {
async |(percentage, _)| { "lookup" => {
println!("Installing: {percentage}"); let apps = instproxy_client.get_apps(Some("User"), None).await.unwrap();
}, for app in apps.keys() {
(), println!("{app}");
) }
.await }
.expect("Failed to install") "browse" => {
} else { instproxy_client.browse(None).await.expect("browse failed");
eprintln!("Invalid usage, pass -h for help"); }
"check_capabilities" => {
instproxy_client
.check_capabilities_match(Vec::new(), None)
.await
.expect("check failed");
}
"install" => {
let path: String = match sub_args.next_argument() {
Some(p) => p,
None => {
eprintln!("No path passed, pass -h for help");
return;
}
};
instproxy_client
.install_with_callback(
path,
None,
async |(percentage, _)| {
println!("Installing: {percentage}");
},
(),
)
.await
.expect("Failed to install")
}
_ => unreachable!(),
} }
} }

View File

@@ -1,70 +1,33 @@
// Jackson Coxson // Jackson Coxson
// Just lists apps for now // Just lists apps for now
use clap::{Arg, Command}; use idevice::provider::IdeviceProvider;
use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake};
use idevice::dvt::location_simulation::LocationSimulationClient; use idevice::dvt::location_simulation::LocationSimulationClient;
use idevice::services::simulate_location::LocationSimulationService; use idevice::services::simulate_location::LocationSimulationService;
mod common; use jkcli::{CollectedArguments, JkArgument, JkCommand};
#[tokio::main] pub fn register() -> JkCommand {
async fn main() { JkCommand::new()
tracing_subscriber::fmt::init(); .help("Simulate device location")
.with_subcommand(
"clear",
JkCommand::new().help("Clears the location set on the device"),
)
.with_subcommand(
"set",
JkCommand::new()
.help("Set the location on the device")
.with_argument(JkArgument::new().with_help("latitude").required(true))
.with_argument(JkArgument::new().with_help("longitutde").required(true)),
)
.subcommand_required(true)
}
let matches = Command::new("simulate_location") pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
.about("Simulate device location") let (sub_name, sub_args) = arguments.first_subcommand().expect("No sub arg passed");
.arg( let mut sub_args = sub_args.clone();
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("clear").about("Clears the location set on the device"))
.subcommand(
Command::new("set")
.about("Set the location on the device")
.arg(Arg::new("latitude").required(true))
.arg(Arg::new("longitude").required(true)),
)
.get_matches();
if matches.get_flag("about") {
println!("simulate_location - Sets the simulated location on an iOS device");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider =
match common::get_provider(udid, host, pairing_file, "simulate_location-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await { if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await {
let rsd_port = proxy.handshake.server_rsd_port; let rsd_port = proxy.handshake.server_rsd_port;
@@ -86,42 +49,44 @@ async fn main() {
let mut ls_client = LocationSimulationClient::new(&mut ls_client) let mut ls_client = LocationSimulationClient::new(&mut ls_client)
.await .await
.expect("Unable to get channel for location simulation"); .expect("Unable to get channel for location simulation");
if matches.subcommand_matches("clear").is_some() { match sub_name.as_str() {
ls_client.clear().await.expect("Unable to clear"); "clear" => {
println!("Location cleared!"); ls_client.clear().await.expect("Unable to clear");
} else if let Some(matches) = matches.subcommand_matches("set") { println!("Location cleared!");
let latitude: &String = match matches.get_one("latitude") { }
Some(l) => l, "set" => {
None => { let latitude: String = match sub_args.next_argument() {
eprintln!("No latitude passed! Pass -h for help"); Some(l) => l,
return; None => {
} eprintln!("No latitude passed! Pass -h for help");
}; return;
let latitude: f64 = latitude.parse().expect("Failed to parse as float"); }
let longitude: &String = match matches.get_one("longitude") { };
Some(l) => l, let latitude: f64 = latitude.parse().expect("Failed to parse as float");
None => { let longitude: String = match sub_args.next_argument() {
eprintln!("No longitude passed! Pass -h for help"); Some(l) => l,
return; None => {
} eprintln!("No longitude passed! Pass -h for help");
}; return;
let longitude: f64 = longitude.parse().expect("Failed to parse as float"); }
ls_client };
.set(latitude, longitude) let longitude: f64 = longitude.parse().expect("Failed to parse as float");
.await
.expect("Failed to set location");
println!("Location set!");
println!("Press ctrl-c to stop");
loop {
ls_client ls_client
.set(latitude, longitude) .set(latitude, longitude)
.await .await
.expect("Failed to set location"); .expect("Failed to set location");
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
println!("Location set!");
println!("Press ctrl-c to stop");
loop {
ls_client
.set(latitude, longitude)
.await
.expect("Failed to set location");
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
}
} }
} else { _ => unreachable!(),
eprintln!("Invalid usage, pass -h for help");
} }
} else { } else {
let mut location_client = match LocationSimulationService::connect(&*provider).await { let mut location_client = match LocationSimulationService::connect(&*provider).await {
@@ -133,35 +98,36 @@ async fn main() {
return; return;
} }
}; };
if matches.subcommand_matches("clear").is_some() {
location_client.clear().await.expect("Unable to clear");
println!("Location cleared!");
} else if let Some(matches) = matches.subcommand_matches("set") {
let latitude: &String = match matches.get_one("latitude") {
Some(l) => l,
None => {
eprintln!("No latitude passed! Pass -h for help");
return;
}
};
let longitude: &String = match matches.get_one("longitude") { match sub_name.as_str() {
Some(l) => l, "clear" => {
None => { location_client.clear().await.expect("Unable to clear");
eprintln!("No longitude passed! Pass -h for help"); println!("Location cleared!");
return; }
} "set" => {
}; let latitude: String = match sub_args.next_argument() {
location_client Some(l) => l,
.set(latitude, longitude) None => {
.await eprintln!("No latitude passed! Pass -h for help");
.expect("Failed to set location"); return;
}
};
println!("Location set!"); let longitude: String = match sub_args.next_argument() {
} else { Some(l) => l,
eprintln!("Invalid usage, pass -h for help"); None => {
eprintln!("No longitude passed! Pass -h for help");
return;
}
};
location_client
.set(latitude.as_str(), longitude.as_str())
.await
.expect("Failed to set location");
println!("Location set!");
}
_ => unreachable!(),
} }
}; };
return;
} }

View File

@@ -1,92 +1,79 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command, arg}; use idevice::{IdeviceService, lockdown::LockdownClient, provider::IdeviceProvider};
use idevice::{IdeviceService, lockdown::LockdownClient, pretty_print_plist}; use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag};
use plist::Value; use plist::Value;
use plist_macro::pretty_print_plist;
mod common; pub fn register() -> JkCommand {
JkCommand::new()
#[tokio::main] .help("Interact with lockdown")
async fn main() { .with_subcommand(
tracing_subscriber::fmt::init(); "get",
JkCommand::new()
let matches = Command::new("lockdown") .help("Gets a value from lockdown")
.about("Start a tunnel") .with_argument(JkArgument::new().with_help("The value to get")),
.arg(
Arg::new("host")
.long("host")
.value_name("HOST")
.help("IP address of the device"),
) )
.arg( .with_subcommand(
Arg::new("pairing_file") "set",
.long("pairing-file") JkCommand::new()
.value_name("PATH") .help("Gets a value from lockdown")
.help("Path to the pairing file"), .with_argument(
JkArgument::new()
.with_help("The value to set")
.required(true),
)
.with_argument(
JkArgument::new()
.with_help("The value key to set")
.required(true),
),
) )
.arg( .with_subcommand(
Arg::new("udid") "recovery",
.value_name("UDID") JkCommand::new().help("Tell the device to enter recovery mode"),
.help("UDID of the device (overrides host/pairing file)")
.index(1),
) )
.arg( .with_flag(
Arg::new("about") JkFlag::new("domain")
.long("about") .with_help("The domain to set/get in")
.help("Show about information") .with_argument(JkArgument::new().required(true)),
.action(clap::ArgAction::SetTrue),
) )
.subcommand( .with_flag(JkFlag::new("no-session").with_help("Don't start a TLS session"))
Command::new("get") .subcommand_required(true)
.about("Gets a value") }
.arg(arg!(-v --value <STRING> "the value to get").required(false))
.arg(arg!(-d --domain <STRING> "the domain to get in").required(false)),
)
.subcommand(
Command::new("set")
.about("Sets a lockdown value")
.arg(arg!(-k --key <STRING> "the key to set").required(true))
.arg(arg!(-v --value <STRING> "the value to set the key to").required(true))
.arg(arg!(-d --domain <STRING> "the domain to get in").required(false)),
)
.get_matches();
if matches.get_flag("about") {
println!(
"lockdown - query and manage values on a device. Reimplementation of libimobiledevice's binary."
);
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider =
match common::get_provider(udid, host, pairing_file, "ideviceinfo-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
let mut lockdown_client = LockdownClient::connect(&*provider) let mut lockdown_client = LockdownClient::connect(&*provider)
.await .await
.expect("Unable to connect to lockdown"); .expect("Unable to connect to lockdown");
lockdown_client if !arguments.has_flag("no-session") {
.start_session(&provider.get_pairing_file().await.expect("no pairing file")) lockdown_client
.await .start_session(&provider.get_pairing_file().await.expect("no pairing file"))
.expect("no session"); .await
.expect("no session");
}
match matches.subcommand() { let domain: Option<String> = arguments.get_flag("domain");
Some(("get", sub_m)) => { let domain = domain.as_deref();
let key = sub_m.get_one::<String>("value").map(|x| x.as_str());
let domain = sub_m.get_one::<String>("domain").map(|x| x.as_str());
match lockdown_client.get_value(key, domain).await { let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand");
let mut sub_args = sub_args.clone();
match sub_name.as_str() {
"get" => {
let key: Option<String> = sub_args.next_argument();
match lockdown_client
.get_value(
match &key {
Some(k) => Some(k.as_str()),
None => None,
},
domain,
)
.await
{
Ok(value) => { Ok(value) => {
println!("{}", pretty_print_plist(&value)); println!("{}", pretty_print_plist(&value));
} }
@@ -95,25 +82,21 @@ async fn main() {
} }
} }
} }
"set" => {
Some(("set", sub_m)) => { let value_str: String = sub_args.next_argument().unwrap();
let key = sub_m.get_one::<String>("key").unwrap(); let key: String = sub_args.next_argument().unwrap();
let value_str = sub_m.get_one::<String>("value").unwrap();
let domain = sub_m.get_one::<String>("domain");
let value = Value::String(value_str.clone()); let value = Value::String(value_str.clone());
match lockdown_client match lockdown_client.set_value(key, value, domain).await {
.set_value(key, value, domain.map(|x| x.as_str()))
.await
{
Ok(()) => println!("Successfully set"), Ok(()) => println!("Successfully set"),
Err(e) => eprintln!("Error setting value: {e}"), Err(e) => eprintln!("Error setting value: {e}"),
} }
} }
"recovery" => lockdown_client
_ => { .enter_recovery()
eprintln!("No subcommand provided. Try `--help` for usage."); .await
} .expect("Failed to enter recovery"),
_ => unreachable!(),
} }
} }

338
tools/src/main.rs Normal file
View File

@@ -0,0 +1,338 @@
// Jackson Coxson
use std::{
net::{IpAddr, SocketAddr},
str::FromStr,
};
use idevice::{
pairing_file::PairingFile,
provider::{IdeviceProvider, TcpProvider},
usbmuxd::{Connection, UsbmuxdAddr, UsbmuxdConnection, UsbmuxdDevice},
};
use jkcli::{JkArgument, JkCommand, JkFlag};
mod activation;
mod afc;
mod amfi;
mod app_service;
mod bt_packet_logger;
mod companion_proxy;
mod crash_logs;
mod debug_proxy;
mod diagnostics;
mod diagnosticsservice;
mod dvt_packet_parser;
mod heartbeat_client;
mod ideviceinfo;
mod ideviceinstaller;
mod installcoordination_proxy;
mod instproxy;
mod location_simulation;
mod lockdown;
mod misagent;
mod mobilebackup2;
mod mounter;
mod notification_proxy_client;
mod notifications;
mod os_trace_relay;
mod pair;
mod pcapd;
mod preboard;
mod process_control;
mod remotexpc;
mod restore_service;
mod screenshot;
mod springboardservices;
mod syslog_relay;
mod pcap;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
// Set the base CLI
let arguments = JkCommand::new()
.with_flag(
JkFlag::new("about")
.with_help("Prints the about message")
.with_short_curcuit(|| {
eprintln!("idevice-rs-tools - Jackson Coxson\n");
eprintln!("Tools to manage and manipulate iOS devices");
eprintln!("Version {}", env!("CARGO_PKG_VERSION"));
eprintln!("https://github.com/jkcoxson/idevice");
eprintln!("\nOn to eternal perfection!");
std::process::exit(0);
}),
)
.with_flag(
JkFlag::new("version")
.with_help("Prints the version")
.with_short_curcuit(|| {
println!("{}", env!("CARGO_PKG_VERSION"));
std::process::exit(0);
}),
)
.with_flag(
JkFlag::new("pairing-file")
.with_argument(JkArgument::new().required(true))
.with_help("The path to the pairing file to use"),
)
.with_flag(
JkFlag::new("host")
.with_argument(JkArgument::new().required(true))
.with_help("The host to connect to"),
)
.with_flag(
JkFlag::new("udid")
.with_argument(JkArgument::new().required(true))
.with_help("The UDID to use"),
)
.with_subcommand("activation", activation::register())
.with_subcommand("afc", afc::register())
.with_subcommand("amfi", amfi::register())
.with_subcommand("app_service", app_service::register())
.with_subcommand("bt_packet_logger", bt_packet_logger::register())
.with_subcommand("companion_proxy", companion_proxy::register())
.with_subcommand("crash_logs", crash_logs::register())
.with_subcommand("debug_proxy", debug_proxy::register())
.with_subcommand("diagnostics", diagnostics::register())
.with_subcommand("diagnosticsservice", diagnosticsservice::register())
.with_subcommand("dvt_packet_parser", dvt_packet_parser::register())
.with_subcommand("heartbeat_client", heartbeat_client::register())
.with_subcommand("ideviceinfo", ideviceinfo::register())
.with_subcommand("ideviceinstaller", ideviceinstaller::register())
.with_subcommand(
"installcoordination_proxy",
installcoordination_proxy::register(),
)
.with_subcommand("instproxy", instproxy::register())
.with_subcommand("location_simulation", location_simulation::register())
.with_subcommand("lockdown", lockdown::register())
.with_subcommand("misagent", misagent::register())
.with_subcommand("mobilebackup2", mobilebackup2::register())
.with_subcommand("mounter", mounter::register())
.with_subcommand("notifications", notifications::register())
.with_subcommand("notification_proxy", notification_proxy_client::register())
.with_subcommand("os_trace_relay", os_trace_relay::register())
.with_subcommand("pair", pair::register())
.with_subcommand("pcapd", pcapd::register())
.with_subcommand("preboard", preboard::register())
.with_subcommand("process_control", process_control::register())
.with_subcommand("remotexpc", remotexpc::register())
.with_subcommand("restore_service", restore_service::register())
.with_subcommand("screenshot", screenshot::register())
.with_subcommand("springboard", springboardservices::register())
.with_subcommand("syslog_relay", syslog_relay::register())
.subcommand_required(true)
.collect()
.expect("Failed to collect CLI args");
let udid = arguments.get_flag::<String>("udid");
let host = arguments.get_flag::<String>("host");
let pairing_file = arguments.get_flag::<String>("pairing-file");
let provider = match get_provider(udid, host, pairing_file, "idevice-rs-tools").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let (subcommand, sub_args) = match arguments.first_subcommand() {
Some(s) => s,
None => {
eprintln!("No subcommand passed, pass -h for help");
return;
}
};
match subcommand.as_str() {
"activation" => {
activation::main(sub_args, provider).await;
}
"afc" => {
afc::main(sub_args, provider).await;
}
"amfi" => {
amfi::main(sub_args, provider).await;
}
"app_service" => {
app_service::main(sub_args, provider).await;
}
"bt_packet_logger" => {
bt_packet_logger::main(sub_args, provider).await;
}
"companion_proxy" => {
companion_proxy::main(sub_args, provider).await;
}
"crash_logs" => {
crash_logs::main(sub_args, provider).await;
}
"debug_proxy" => {
debug_proxy::main(sub_args, provider).await;
}
"diagnostics" => {
diagnostics::main(sub_args, provider).await;
}
"diagnosticsservice" => {
diagnosticsservice::main(sub_args, provider).await;
}
"dvt_packet_parser" => {
dvt_packet_parser::main(sub_args, provider).await;
}
"heartbeat_client" => {
heartbeat_client::main(sub_args, provider).await;
}
"ideviceinfo" => {
ideviceinfo::main(sub_args, provider).await;
}
"ideviceinstaller" => {
ideviceinstaller::main(sub_args, provider).await;
}
"installcoordination_proxy" => {
installcoordination_proxy::main(sub_args, provider).await;
}
"instproxy" => {
instproxy::main(sub_args, provider).await;
}
"location_simulation" => {
location_simulation::main(sub_args, provider).await;
}
"lockdown" => {
lockdown::main(sub_args, provider).await;
}
"misagent" => {
misagent::main(sub_args, provider).await;
}
"mobilebackup2" => {
mobilebackup2::main(sub_args, provider).await;
}
"mounter" => {
mounter::main(sub_args, provider).await;
}
"notifications" => {
notifications::main(sub_args, provider).await;
}
"notification_proxy" => {
notification_proxy_client::main(sub_args, provider).await;
}
"os_trace_relay" => {
os_trace_relay::main(sub_args, provider).await;
}
"pair" => {
pair::main(sub_args, provider).await;
}
"pcapd" => {
pcapd::main(sub_args, provider).await;
}
"preboard" => {
preboard::main(sub_args, provider).await;
}
"process_control" => {
process_control::main(sub_args, provider).await;
}
"remotexpc" => {
remotexpc::main(sub_args, provider).await;
}
"restore_service" => {
restore_service::main(sub_args, provider).await;
}
"screenshot" => {
screenshot::main(sub_args, provider).await;
}
"springboard" => {
springboardservices::main(sub_args, provider).await;
}
"syslog_relay" => {
syslog_relay::main(sub_args, provider).await;
}
_ => unreachable!(),
}
}
async fn get_provider(
udid: Option<String>,
host: Option<String>,
pairing_file: Option<String>,
label: &str,
) -> Result<Box<dyn IdeviceProvider>, String> {
let provider: Box<dyn IdeviceProvider> = if let Some(udid) = udid {
let mut usbmuxd = if let Ok(var) = std::env::var("USBMUXD_SOCKET_ADDRESS") {
let socket = SocketAddr::from_str(&var).expect("Bad USBMUXD_SOCKET_ADDRESS");
let socket = tokio::net::TcpStream::connect(socket)
.await
.expect("unable to connect to socket address");
UsbmuxdConnection::new(Box::new(socket), 1)
} else {
UsbmuxdConnection::default()
.await
.expect("Unable to connect to usbmxud")
};
let dev = match usbmuxd.get_device(udid.as_str()).await {
Ok(d) => d,
Err(e) => {
return Err(format!("Device not found: {e:?}"));
}
};
Box::new(dev.to_provider(UsbmuxdAddr::from_env_var().unwrap(), label))
} else if let Some(host) = host
&& let Some(pairing_file) = pairing_file
{
let host = match IpAddr::from_str(host.as_str()) {
Ok(h) => h,
Err(e) => {
return Err(format!("Invalid host: {e:?}"));
}
};
let pairing_file = match PairingFile::read_from_file(pairing_file) {
Ok(p) => p,
Err(e) => {
return Err(format!("Unable to read pairing file: {e:?}"));
}
};
Box::new(TcpProvider {
addr: host,
pairing_file,
label: label.to_string(),
})
} else {
let mut usbmuxd = if let Ok(var) = std::env::var("USBMUXD_SOCKET_ADDRESS") {
let socket = SocketAddr::from_str(&var).expect("Bad USBMUXD_SOCKET_ADDRESS");
let socket = tokio::net::TcpStream::connect(socket)
.await
.expect("unable to connect to socket address");
UsbmuxdConnection::new(Box::new(socket), 1)
} else {
UsbmuxdConnection::default()
.await
.expect("Unable to connect to usbmxud")
};
let devs = match usbmuxd.get_devices().await {
Ok(d) => d,
Err(e) => {
return Err(format!("Unable to get devices from usbmuxd: {e:?}"));
}
};
let usb_devs: Vec<&UsbmuxdDevice> = devs
.iter()
.filter(|x| x.connection_type == Connection::Usb)
.collect();
if devs.is_empty() {
return Err("No devices connected!".to_string());
}
let chosen_dev = if !usb_devs.is_empty() {
usb_devs[0]
} else {
&devs[0]
};
Box::new(chosen_dev.to_provider(UsbmuxdAddr::from_env_var().unwrap(), label))
};
Ok(provider)
}

View File

@@ -2,99 +2,89 @@
use std::path::PathBuf; use std::path::PathBuf;
use clap::{Arg, Command, arg, value_parser}; use idevice::{IdeviceService, misagent::MisagentClient, provider::IdeviceProvider};
use idevice::{IdeviceService, misagent::MisagentClient}; use jkcli::{CollectedArguments, JkArgument, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new()
#[tokio::main] .help("Manage provisioning profiles on the device")
async fn main() { .with_subcommand(
tracing_subscriber::fmt::init(); "list",
JkCommand::new()
let matches = Command::new("core_device_proxy_tun") .help("List profiles installed on the device")
.about("Start a tunnel") .with_argument(
.arg( JkArgument::new()
Arg::new("host") .with_help("Path to save profiles from the device")
.long("host") .required(false),
.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 images mounted on the device")
.arg(
arg!(-s --save <FOLDER> "the folder to save the profiles to")
.value_parser(value_parser!(PathBuf)),
), ),
) )
.subcommand( .with_subcommand(
Command::new("remove") "remove",
.about("Remove a provisioning profile") JkCommand::new()
.arg(Arg::new("id").required(true).index(1)), .help("Remove a profile installed on the device")
.with_argument(
JkArgument::new()
.with_help("ID of the profile to remove")
.required(true),
),
) )
.get_matches(); .with_subcommand(
"install",
JkCommand::new()
.help("Install a provisioning profile on the device")
.with_argument(
JkArgument::new()
.with_help("Path to the provisioning profile to install")
.required(true),
),
)
.subcommand_required(true)
}
if matches.get_flag("about") { pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
println!(
"mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary."
);
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider = match common::get_provider(udid, host, pairing_file, "misagent-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let mut misagent_client = MisagentClient::connect(&*provider) let mut misagent_client = MisagentClient::connect(&*provider)
.await .await
.expect("Unable to connect to misagent"); .expect("Unable to connect to misagent");
if let Some(matches) = matches.subcommand_matches("list") { let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand passed");
let images = misagent_client let mut sub_args = sub_args.clone();
.copy_all()
.await
.expect("Unable to get images");
if let Some(path) = matches.get_one::<PathBuf>("save") {
tokio::fs::create_dir_all(path)
.await
.expect("Unable to create save DIR");
for (index, image) in images.iter().enumerate() { match sub_name.as_str() {
let f = path.join(format!("{index}.pem")); "list" => {
tokio::fs::write(f, image) let images = misagent_client
.copy_all()
.await
.expect("Unable to get images");
if let Some(path) = sub_args.next_argument::<PathBuf>() {
tokio::fs::create_dir_all(&path)
.await .await
.expect("Failed to write image"); .expect("Unable to create save DIR");
for (index, image) in images.iter().enumerate() {
let f = path.join(format!("{index}.pem"));
tokio::fs::write(f, image)
.await
.expect("Failed to write image");
}
} }
} }
} else if let Some(matches) = matches.subcommand_matches("remove") { "remove" => {
let id = matches.get_one::<String>("id").expect("No ID passed"); let id = sub_args.next_argument::<String>().expect("No ID passed");
misagent_client.remove(id).await.expect("Failed to remove"); misagent_client
} else { .remove(id.as_str())
eprintln!("Invalid usage, pass -h for help"); .await
.expect("Failed to remove");
}
"install" => {
let path = sub_args
.next_argument::<PathBuf>()
.expect("No profile path passed");
let profile = tokio::fs::read(path).await.expect("Unable to read profile");
misagent_client
.install(profile)
.await
.expect("Failed to install profile");
}
_ => unreachable!(),
} }
} }

View File

@@ -1,191 +1,135 @@
// Jackson Coxson // Jackson Coxson
// Mobile Backup 2 tool for iOS devices // Mobile Backup 2 tool for iOS devices
use clap::{Arg, Command};
use idevice::{ use idevice::{
IdeviceService, IdeviceService,
mobilebackup2::{MobileBackup2Client, RestoreOptions}, mobilebackup2::{MobileBackup2Client, RestoreOptions},
provider::IdeviceProvider,
}; };
use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag};
use plist::Dictionary; use plist::Dictionary;
use std::fs; use std::fs;
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::path::Path; use std::path::Path;
mod common; pub fn register() -> JkCommand {
JkCommand::new()
#[tokio::main] .help("Mobile Backup 2 tool for iOS devices")
async fn main() { .with_subcommand(
tracing_subscriber::fmt::init(); "info",
JkCommand::new()
let matches = Command::new("mobilebackup2") .help("Get backup information from a local backup directory")
.about("Mobile Backup 2 tool for iOS devices") .with_argument(
.arg( JkArgument::new()
Arg::new("host") .with_help("Backup DIR to read from")
.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("info")
.about("Get backup information from a local backup directory")
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true))
.arg(
Arg::new("source")
.long("source")
.value_name("SOURCE")
.help("Source identifier (defaults to current UDID)"),
),
)
.subcommand(
Command::new("list")
.about("List files of the last backup from a local backup directory")
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true))
.arg(Arg::new("source").long("source").value_name("SOURCE")),
)
.subcommand(
Command::new("backup")
.about("Start a backup operation")
.arg(
Arg::new("dir")
.long("dir")
.value_name("DIR")
.help("Backup directory on host")
.required(true), .required(true),
) )
.arg( .with_argument(
Arg::new("target") JkArgument::new()
.long("target") .with_help("Source identifier (defaults to current UDID)")
.value_name("TARGET") .required(true),
.help("Target identifier for the backup"),
)
.arg(
Arg::new("source")
.long("source")
.value_name("SOURCE")
.help("Source identifier for the backup"),
), ),
) )
.subcommand( .with_subcommand(
Command::new("restore") "list",
.about("Restore from a local backup directory (DeviceLink)") JkCommand::new()
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) .help("List files of the last backup from a local backup directory")
.arg( .with_argument(
Arg::new("source") JkArgument::new()
.long("source") .with_help("Backup DIR to read from")
.value_name("SOURCE") .required(true),
.help("Source UDID; defaults to current device UDID"),
) )
.arg( .with_argument(
Arg::new("password") JkArgument::new()
.long("password") .with_help("Source identifier (defaults to current UDID)")
.value_name("PWD") .required(true),
.help("Backup password if encrypted"),
)
.arg(
Arg::new("no-reboot")
.long("no-reboot")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("no-copy")
.long("no-copy")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("no-settings")
.long("no-settings")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("system")
.long("system")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("remove")
.long("remove")
.action(clap::ArgAction::SetTrue),
), ),
) )
.subcommand( .with_subcommand(
Command::new("unback") "backup",
.about("Unpack a complete backup to device hierarchy") JkCommand::new()
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) .help("Start a backup operation")
.arg(Arg::new("source").long("source").value_name("SOURCE")) .with_argument(
.arg(Arg::new("password").long("password").value_name("PWD")), JkArgument::new()
) .with_help("Backup directory on host")
.subcommand(
Command::new("extract")
.about("Extract a file from a previous backup")
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true))
.arg(Arg::new("source").long("source").value_name("SOURCE"))
.arg(
Arg::new("domain")
.long("domain")
.value_name("DOMAIN")
.required(true), .required(true),
) )
.arg( .with_argument(
Arg::new("path") JkArgument::new()
.long("path") .with_help("Target identifier for the backup")
.value_name("REL_PATH")
.required(true), .required(true),
) )
.arg(Arg::new("password").long("password").value_name("PWD")), .with_argument(
JkArgument::new()
.with_help("Source identifier for the backup")
.required(true),
),
) )
.subcommand( .with_subcommand(
Command::new("change-password") "restore",
.about("Change backup password") JkCommand::new()
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) .help("Restore from a local backup directory (DeviceLink)")
.arg(Arg::new("old").long("old").value_name("OLD")) .with_argument(JkArgument::new().with_help("DIR").required(true))
.arg(Arg::new("new").long("new").value_name("NEW")), .with_argument(
JkArgument::new()
.with_help("Source UDID; defaults to current device UDID")
.required(true),
)
.with_argument(
JkArgument::new()
.with_help("Backup password if encrypted")
.required(true),
)
.with_flag(JkFlag::new("no-reboot"))
.with_flag(JkFlag::new("no-copy"))
.with_flag(JkFlag::new("no-settings"))
.with_flag(JkFlag::new("system"))
.with_flag(JkFlag::new("remove")),
) )
.subcommand( .with_subcommand(
Command::new("erase-device") "unback",
.about("Erase the device via mobilebackup2") JkCommand::new()
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true)), .help("Unpack a complete backup to device hierarchy")
.with_argument(JkArgument::new().with_help("DIR").required(true))
.with_argument(JkArgument::new().with_help("Source"))
.with_argument(JkArgument::new().with_help("Password")),
) )
.subcommand(Command::new("freespace").about("Get free space information")) .with_subcommand(
.subcommand(Command::new("encryption").about("Check backup encryption status")) "extract",
.get_matches(); JkCommand::new()
.help("Extract a file from a previous backup")
if matches.get_flag("about") { .with_argument(JkArgument::new().with_help("DIR").required(true))
println!("mobilebackup2 - manage device backups using Mobile Backup 2 service"); .with_argument(JkArgument::new().with_help("Source").required(true))
println!("Copyright (c) 2025 Jackson Coxson"); .with_argument(JkArgument::new().with_help("Domain").required(true))
return; .with_argument(JkArgument::new().with_help("Path").required(true))
} .with_argument(JkArgument::new().with_help("Password").required(true)),
)
let udid = matches.get_one::<String>("udid"); .with_subcommand(
let host = matches.get_one::<String>("host"); "change-password",
let pairing_file = matches.get_one::<String>("pairing_file"); JkCommand::new()
.help("Change backup password")
let provider = .with_argument(JkArgument::new().with_help("DIR").required(true))
match common::get_provider(udid, host, pairing_file, "mobilebackup2-jkcoxson").await { .with_argument(JkArgument::new().with_help("Old password").required(true))
Ok(p) => p, .with_argument(JkArgument::new().with_help("New password").required(true)),
Err(e) => { )
eprintln!("Error creating provider: {e}"); .with_subcommand(
return; "erase-device",
} JkCommand::new()
}; .help("Erase the device via mobilebackup2")
.with_argument(JkArgument::new().with_help("DIR").required(true)),
)
.with_subcommand(
"freespace",
JkCommand::new().help("Get free space information"),
)
.with_subcommand(
"encryption",
JkCommand::new().help("Check backup encryption status"),
)
.subcommand_required(true)
}
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
let mut backup_client = match MobileBackup2Client::connect(&*provider).await { let mut backup_client = match MobileBackup2Client::connect(&*provider).await {
Ok(client) => client, Ok(client) => client,
Err(e) => { Err(e) => {
@@ -194,11 +138,16 @@ async fn main() {
} }
}; };
match matches.subcommand() { let (sub_name, sub_args) = arguments.first_subcommand().unwrap();
Some(("info", sub)) => { let mut sub_args = sub_args.clone();
let dir = sub.get_one::<String>("dir").unwrap();
let source = sub.get_one::<String>("source").map(|s| s.as_str()); match sub_name.as_str() {
match backup_client.info_from_path(Path::new(dir), source).await { "info" => {
let dir = sub_args.next_argument::<String>().unwrap();
let source = sub_args.next_argument::<String>();
let source = source.as_deref();
match backup_client.info_from_path(Path::new(&dir), source).await {
Ok(dict) => { Ok(dict) => {
println!("Backup Information:"); println!("Backup Information:");
for (k, v) in dict { for (k, v) in dict {
@@ -208,10 +157,12 @@ async fn main() {
Err(e) => eprintln!("Failed to get info: {e}"), Err(e) => eprintln!("Failed to get info: {e}"),
} }
} }
Some(("list", sub)) => { "list" => {
let dir = sub.get_one::<String>("dir").unwrap(); let dir = sub_args.next_argument::<String>().unwrap();
let source = sub.get_one::<String>("source").map(|s| s.as_str()); let source = sub_args.next_argument::<String>();
match backup_client.list_from_path(Path::new(dir), source).await { let source = source.as_deref();
match backup_client.list_from_path(Path::new(&dir), source).await {
Ok(dict) => { Ok(dict) => {
println!("List Response:"); println!("List Response:");
for (k, v) in dict { for (k, v) in dict {
@@ -221,12 +172,12 @@ async fn main() {
Err(e) => eprintln!("Failed to list: {e}"), Err(e) => eprintln!("Failed to list: {e}"),
} }
} }
Some(("backup", sub_matches)) => { "backup" => {
let target = sub_matches.get_one::<String>("target").map(|s| s.as_str()); let target = sub_args.next_argument::<String>();
let source = sub_matches.get_one::<String>("source").map(|s| s.as_str()); let target = target.as_deref();
let dir = sub_matches let source = sub_args.next_argument::<String>();
.get_one::<String>("dir") let source = source.as_deref();
.expect("dir is required"); let dir = sub_args.next_argument::<String>().expect("dir is required");
println!("Starting backup operation..."); println!("Starting backup operation...");
let res = backup_client let res = backup_client
@@ -234,95 +185,112 @@ async fn main() {
.await; .await;
if let Err(e) = res { if let Err(e) = res {
eprintln!("Failed to send backup request: {e}"); eprintln!("Failed to send backup request: {e}");
} else if let Err(e) = process_dl_loop(&mut backup_client, Path::new(dir)).await { } else if let Err(e) = process_dl_loop(&mut backup_client, Path::new(&dir)).await {
eprintln!("Backup failed during DL loop: {e}"); eprintln!("Backup failed during DL loop: {e}");
} else { } else {
println!("Backup flow finished"); println!("Backup flow finished");
} }
} }
Some(("restore", sub)) => { "restore" => {
let dir = sub.get_one::<String>("dir").unwrap(); let dir = sub_args.next_argument::<String>().unwrap();
let source = sub.get_one::<String>("source").map(|s| s.as_str()); let source = sub_args.next_argument::<String>();
let source = source.as_deref();
let mut ropts = RestoreOptions::new(); let mut ropts = RestoreOptions::new();
if sub.get_flag("no-reboot") { if sub_args.has_flag("no-reboot") {
ropts = ropts.with_reboot(false); ropts = ropts.with_reboot(false);
} }
if sub.get_flag("no-copy") { if sub_args.has_flag("no-copy") {
ropts = ropts.with_copy(false); ropts = ropts.with_copy(false);
} }
if sub.get_flag("no-settings") { if sub_args.has_flag("no-settings") {
ropts = ropts.with_preserve_settings(false); ropts = ropts.with_preserve_settings(false);
} }
if sub.get_flag("system") { if sub_args.has_flag("system") {
ropts = ropts.with_system_files(true); ropts = ropts.with_system_files(true);
} }
if sub.get_flag("remove") { if sub_args.has_flag("remove") {
ropts = ropts.with_remove_items_not_restored(true); ropts = ropts.with_remove_items_not_restored(true);
} }
if let Some(pw) = sub.get_one::<String>("password") { if let Some(pw) = sub_args.next_argument::<String>() {
ropts = ropts.with_password(pw); ropts = ropts.with_password(pw);
} }
match backup_client match backup_client
.restore_from_path(Path::new(dir), source, Some(ropts)) .restore_from_path(Path::new(&dir), source, Some(ropts))
.await .await
{ {
Ok(_) => println!("Restore flow finished"), Ok(_) => println!("Restore flow finished"),
Err(e) => eprintln!("Restore failed: {e}"), Err(e) => eprintln!("Restore failed: {e}"),
} }
} }
Some(("unback", sub)) => { "unback" => {
let dir = sub.get_one::<String>("dir").unwrap(); let dir = sub_args.next_argument::<String>().unwrap();
let source = sub.get_one::<String>("source").map(|s| s.as_str()); let source = sub_args.next_argument::<String>();
let password = sub.get_one::<String>("password").map(|s| s.as_str()); let source = source.as_deref();
let password = sub_args.next_argument::<String>();
let password = password.as_deref();
match backup_client match backup_client
.unback_from_path(Path::new(dir), password, source) .unback_from_path(Path::new(&dir), password, source)
.await .await
{ {
Ok(_) => println!("Unback finished"), Ok(_) => println!("Unback finished"),
Err(e) => eprintln!("Unback failed: {e}"), Err(e) => eprintln!("Unback failed: {e}"),
} }
} }
Some(("extract", sub)) => { "extract" => {
let dir = sub.get_one::<String>("dir").unwrap(); let dir = sub_args.next_argument::<String>().unwrap();
let source = sub.get_one::<String>("source").map(|s| s.as_str()); let source = sub_args.next_argument::<String>();
let domain = sub.get_one::<String>("domain").unwrap(); let source = source.as_deref();
let rel = sub.get_one::<String>("path").unwrap(); let domain = sub_args.next_argument::<String>().unwrap();
let password = sub.get_one::<String>("password").map(|s| s.as_str()); let rel = sub_args.next_argument::<String>().unwrap();
let password = sub_args.next_argument::<String>();
let password = password.as_deref();
match backup_client match backup_client
.extract_from_path(domain, rel, Path::new(dir), password, source) .extract_from_path(
domain.as_str(),
rel.as_str(),
Path::new(&dir),
password,
source,
)
.await .await
{ {
Ok(_) => println!("Extract finished"), Ok(_) => println!("Extract finished"),
Err(e) => eprintln!("Extract failed: {e}"), Err(e) => eprintln!("Extract failed: {e}"),
} }
} }
Some(("change-password", sub)) => { "change-password" => {
let dir = sub.get_one::<String>("dir").unwrap(); let dir = sub_args.next_argument::<String>().unwrap();
let old = sub.get_one::<String>("old").map(|s| s.as_str()); let old = sub_args.next_argument::<String>();
let newv = sub.get_one::<String>("new").map(|s| s.as_str()); let old = old.as_deref();
let newv = sub_args.next_argument::<String>();
let newv = newv.as_deref();
match backup_client match backup_client
.change_password_from_path(Path::new(dir), old, newv) .change_password_from_path(Path::new(&dir), old, newv)
.await .await
{ {
Ok(_) => println!("Change password finished"), Ok(_) => println!("Change password finished"),
Err(e) => eprintln!("Change password failed: {e}"), Err(e) => eprintln!("Change password failed: {e}"),
} }
} }
Some(("erase-device", sub)) => { "erase-device" => {
let dir = sub.get_one::<String>("dir").unwrap(); let dir = sub_args.next_argument::<String>().unwrap();
match backup_client.erase_device_from_path(Path::new(dir)).await { match backup_client.erase_device_from_path(Path::new(&dir)).await {
Ok(_) => println!("Erase device command sent"), Ok(_) => println!("Erase device command sent"),
Err(e) => eprintln!("Erase device failed: {e}"), Err(e) => eprintln!("Erase device failed: {e}"),
} }
} }
Some(("freespace", _)) => match backup_client.get_freespace().await { "freespace" => match backup_client.get_freespace().await {
Ok(freespace) => { Ok(freespace) => {
let freespace_gb = freespace as f64 / (1024.0 * 1024.0 * 1024.0); let freespace_gb = freespace as f64 / (1024.0 * 1024.0 * 1024.0);
println!("Free space: {freespace} bytes ({freespace_gb:.2} GB)"); println!("Free space: {freespace} bytes ({freespace_gb:.2} GB)");
} }
Err(e) => eprintln!("Failed to get free space: {e}"), Err(e) => eprintln!("Failed to get free space: {e}"),
}, },
Some(("encryption", _)) => match backup_client.check_backup_encryption().await { "encryption" => match backup_client.check_backup_encryption().await {
Ok(is_encrypted) => { Ok(is_encrypted) => {
println!( println!(
"Backup encryption: {}", "Backup encryption: {}",

View File

@@ -3,90 +3,62 @@
use std::{io::Write, path::PathBuf}; use std::{io::Write, path::PathBuf};
use clap::{Arg, Command, arg, value_parser};
use idevice::{ use idevice::{
IdeviceService, lockdown::LockdownClient, mobile_image_mounter::ImageMounter, IdeviceService, lockdown::LockdownClient, mobile_image_mounter::ImageMounter,
pretty_print_plist, provider::IdeviceProvider,
}; };
use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag};
use plist_macro::pretty_print_plist;
mod common; pub fn register() -> JkCommand {
JkCommand::new()
#[tokio::main] .help("Manage mounts on an iOS device")
async fn main() { .with_subcommand(
tracing_subscriber::fmt::init(); "list",
JkCommand::new().help("Lists the images mounted on the device"),
let matches = Command::new("core_device_proxy_tun")
.about("Start a tunnel")
.arg(
Arg::new("host")
.long("host")
.value_name("HOST")
.help("IP address of the device"),
) )
.arg( .with_subcommand(
Arg::new("pairing_file") "lookup",
.long("pairing-file") JkCommand::new().help("Lookup the image signature on the device"),
.value_name("PATH")
.help("Path to the pairing file"),
) )
.arg( .with_subcommand(
Arg::new("udid") "unmount",
.value_name("UDID") JkCommand::new().help("Unmounts the developer disk image"),
.help("UDID of the device (overrides host/pairing file)")
.index(1),
) )
.arg( .with_subcommand(
Arg::new("about") "mount",
.long("about") JkCommand::new()
.help("Show about information") .help("Mounts the developer disk image")
.action(clap::ArgAction::SetTrue), .with_flag(
) JkFlag::new("image")
.subcommand(Command::new("list").about("Lists the images mounted on the device")) .with_short("i")
.subcommand(Command::new("unmount").about("Unmounts the developer disk image")) .with_argument(JkArgument::new().required(true))
.subcommand( .with_help("A path to the image to mount")
Command::new("mount")
.about("Mounts the developer disk image")
.arg(
arg!(-i --image <FILE> "the developer disk image to mount")
.value_parser(value_parser!(PathBuf))
.required(true), .required(true),
) )
.arg( .with_flag(
arg!(-b --manifest <FILE> "the build manifest (iOS 17+)") JkFlag::new("manifest")
.value_parser(value_parser!(PathBuf)), .with_short("b")
.with_argument(JkArgument::new())
.with_help("the build manifest (iOS 17+)"),
) )
.arg( .with_flag(
arg!(-t --trustcache <FILE> "the trust cache (iOS 17+)") JkFlag::new("trustcache")
.value_parser(value_parser!(PathBuf)), .with_short("t")
.with_argument(JkArgument::new())
.with_help("the trust cache (iOS 17+)"),
) )
.arg( .with_flag(
arg!(-s --signature <FILE> "the image signature (iOS < 17.0") JkFlag::new("signature")
.value_parser(value_parser!(PathBuf)), .with_short("s")
.with_argument(JkArgument::new())
.with_help("the image signature (iOS < 17.0"),
), ),
) )
.get_matches(); .subcommand_required(true)
}
if matches.get_flag("about") {
println!(
"mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary."
);
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider =
match common::get_provider(udid, host, pairing_file, "ideviceinfo-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
let mut lockdown_client = LockdownClient::connect(&*provider) let mut lockdown_client = LockdownClient::connect(&*provider)
.await .await
.expect("Unable to connect to lockdown"); .expect("Unable to connect to lockdown");
@@ -119,114 +91,131 @@ async fn main() {
.await .await
.expect("Unable to connect to image mounter"); .expect("Unable to connect to image mounter");
if matches.subcommand_matches("list").is_some() { let (subcommand, sub_args) = arguments
let images = mounter_client .first_subcommand()
.copy_devices() .expect("No subcommand passed! Pass -h for help");
.await
.expect("Unable to get images"); match subcommand.as_str() {
for i in images { "list" => {
println!("{}", pretty_print_plist(&i)); let images = mounter_client
} .copy_devices()
} else if matches.subcommand_matches("unmount").is_some() {
if product_version < 17 {
mounter_client
.unmount_image("/Developer")
.await .await
.expect("Failed to unmount"); .expect("Unable to get images");
} else { for i in images {
mounter_client println!("{}", pretty_print_plist(&i));
.unmount_image("/System/Developer")
.await
.expect("Failed to unmount");
}
} else if let Some(matches) = matches.subcommand_matches("mount") {
let image: &PathBuf = match matches.get_one("image") {
Some(i) => i,
None => {
eprintln!("No image was passed! Pass -h for help");
return;
} }
};
let image = tokio::fs::read(image).await.expect("Unable to read image");
if product_version < 17 {
let signature: &PathBuf = match matches.get_one("signature") {
Some(s) => s,
None => {
eprintln!("No signature was passed! Pass -h for help");
return;
}
};
let signature = tokio::fs::read(signature)
.await
.expect("Unable to read signature");
mounter_client
.mount_developer(&image, signature)
.await
.expect("Unable to mount");
} else {
let manifest: &PathBuf = match matches.get_one("manifest") {
Some(s) => s,
None => {
eprintln!("No build manifest was passed! Pass -h for help");
return;
}
};
let build_manifest = &tokio::fs::read(manifest)
.await
.expect("Unable to read signature");
let trust_cache: &PathBuf = match matches.get_one("trustcache") {
Some(s) => s,
None => {
eprintln!("No trust cache was passed! Pass -h for help");
return;
}
};
let trust_cache = tokio::fs::read(trust_cache)
.await
.expect("Unable to read signature");
let unique_chip_id =
match lockdown_client.get_value(Some("UniqueChipID"), None).await {
Ok(u) => u,
Err(_) => {
lockdown_client
.start_session(&provider.get_pairing_file().await.unwrap())
.await
.expect("Unable to start session");
lockdown_client
.get_value(Some("UniqueChipID"), None)
.await
.expect("Unable to get UniqueChipID")
}
}
.as_unsigned_integer()
.expect("Unexpected value for chip IP");
mounter_client
.mount_personalized_with_callback(
&*provider,
image,
trust_cache,
build_manifest,
None,
unique_chip_id,
async |((n, d), _)| {
let percent = (n as f64 / d as f64) * 100.0;
print!("\rProgress: {percent:.2}%");
std::io::stdout().flush().unwrap(); // Make sure it prints immediately
if n == d {
println!();
}
},
(),
)
.await
.expect("Unable to mount");
} }
} else { "lookup" => {
eprintln!("Invalid usage, pass -h for help"); let sig = mounter_client
.lookup_image(if product_version < 17 {
"Developer"
} else {
"Personalized"
})
.await
.expect("Failed to lookup images");
println!("Image signature: {sig:02X?}");
}
"unmount" => {
if product_version < 17 {
mounter_client
.unmount_image("/Developer")
.await
.expect("Failed to unmount");
} else {
mounter_client
.unmount_image("/System/Developer")
.await
.expect("Failed to unmount");
}
}
"mount" => {
let image: PathBuf = match sub_args.get_flag("image") {
Some(i) => i,
None => {
eprintln!("No image was passed! Pass -h for help");
return;
}
};
let image = tokio::fs::read(image).await.expect("Unable to read image");
if product_version < 17 {
let signature: PathBuf = match sub_args.get_flag("signature") {
Some(s) => s,
None => {
eprintln!("No signature was passed! Pass -h for help");
return;
}
};
let signature = tokio::fs::read(signature)
.await
.expect("Unable to read signature");
mounter_client
.mount_developer(&image, signature)
.await
.expect("Unable to mount");
} else {
let manifest: PathBuf = match sub_args.get_flag("manifest") {
Some(s) => s,
None => {
eprintln!("No build manifest was passed! Pass -h for help");
return;
}
};
let build_manifest = &tokio::fs::read(manifest)
.await
.expect("Unable to read signature");
let trust_cache: PathBuf = match sub_args.get_flag("trustcache") {
Some(s) => s,
None => {
eprintln!("No trust cache was passed! Pass -h for help");
return;
}
};
let trust_cache = tokio::fs::read(trust_cache)
.await
.expect("Unable to read signature");
let unique_chip_id =
match lockdown_client.get_value(Some("UniqueChipID"), None).await {
Ok(u) => u,
Err(_) => {
lockdown_client
.start_session(&provider.get_pairing_file().await.unwrap())
.await
.expect("Unable to start session");
lockdown_client
.get_value(Some("UniqueChipID"), None)
.await
.expect("Unable to get UniqueChipID")
}
}
.as_unsigned_integer()
.expect("Unexpected value for chip IP");
mounter_client
.mount_personalized_with_callback(
&*provider,
image,
trust_cache,
build_manifest,
None,
unique_chip_id,
async |((n, d), _)| {
let percent = (n as f64 / d as f64) * 100.0;
print!("\rProgress: {percent:.2}%");
std::io::stdout().flush().unwrap(); // Make sure it prints immediately
if n == d {
println!();
}
},
(),
)
.await
.expect("Unable to mount");
}
}
_ => unreachable!(),
} }
return;
} }

View File

@@ -0,0 +1,85 @@
// Jackson Coxson
use idevice::{
IdeviceService, notification_proxy::NotificationProxyClient, provider::IdeviceProvider,
};
use jkcli::{CollectedArguments, JkArgument, JkCommand};
pub fn register() -> JkCommand {
JkCommand::new()
.help("Notification proxy")
.with_subcommand(
"observe",
JkCommand::new()
.help("Observe notifications from the device")
.with_argument(
JkArgument::new()
.with_help("The notification ID to observe")
.required(true),
),
)
.with_subcommand(
"post",
JkCommand::new()
.help("Post a notification to the device")
.with_argument(
JkArgument::new()
.with_help("The notification ID to post")
.required(true),
),
)
.subcommand_required(true)
}
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
let mut client = NotificationProxyClient::connect(&*provider)
.await
.expect("Unable to connect to notification proxy");
let (subcommand, sub_args) = arguments.first_subcommand().unwrap();
let mut sub_args = sub_args.clone();
match subcommand.as_str() {
"observe" => {
let input: String = sub_args
.next_argument::<String>()
.expect("No notification ID passed");
let notifications: Vec<&str> = input.split_whitespace().collect();
client
.observe_notifications(&notifications)
.await
.expect("Failed to observe notifications");
loop {
tokio::select! {
_ = tokio::signal::ctrl_c() => {
println!("\nShutdown signal received, exiting.");
break;
}
result = client.receive_notification() => {
match result {
Ok(notif) => println!("Received notification: {}", notif),
Err(e) => {
eprintln!("Failed to receive notification: {}", e);
break;
}
}
}
}
}
}
"post" => {
let notification: String = sub_args
.next_argument::<String>()
.expect("No notification ID passed");
client
.post_notification(&notification)
.await
.expect("Failed to post notification");
}
_ => unreachable!(),
}
}

View File

@@ -1,59 +1,16 @@
// Monitor memory and app notifications // Monitor memory and app notifications
use clap::{Arg, Command}; use idevice::{
use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider,
mod common; rsd::RsdHandshake,
};
use jkcli::{CollectedArguments, JkCommand};
#[tokio::main] pub fn register() -> JkCommand {
async fn main() { JkCommand::new().help("Notification proxy")
tracing_subscriber::fmt::init(); }
let matches = Command::new("notifications")
.about("start notifications")
.arg(
Arg::new("host")
.long("host")
.value_name("HOST")
.help("IP address of the device"),
)
.arg(
Arg::new("pairing_file")
.long("pairing-file")
.value_name("PATH")
.help("Path to the pairing file"),
)
.arg(
Arg::new("udid")
.value_name("UDID")
.help("UDID of the device (overrides host/pairing file)")
.index(1),
)
.arg(
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.get_matches();
if matches.get_flag("about") {
print!("notifications - start notifications to ios device");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider =
match common::get_provider(udid, host, pairing_file, "notifications-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
pub async fn main(_arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
let proxy = CoreDeviceProxy::connect(&*provider) let proxy = CoreDeviceProxy::connect(&*provider)
.await .await
.expect("no core proxy"); .expect("no core proxy");
@@ -80,7 +37,6 @@ async fn main() {
.await .await
.expect("Failed to start notifications"); .expect("Failed to start notifications");
// Handle Ctrl+C gracefully
loop { loop {
tokio::select! { tokio::select! {
_ = tokio::signal::ctrl_c() => { _ = tokio::signal::ctrl_c() => {
@@ -88,7 +44,6 @@ async fn main() {
break; break;
} }
// Branch 2: Wait for the next batch of notifications.
result = notification_client.get_notification() => { result = notification_client.get_notification() => {
if let Err(e) = result { if let Err(e) = result {
eprintln!("Failed to get notifications: {}", e); eprintln!("Failed to get notifications: {}", e);

View File

@@ -1,58 +1,13 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command}; use idevice::{IdeviceService, os_trace_relay::OsTraceRelayClient, provider::IdeviceProvider};
use idevice::{IdeviceService, os_trace_relay::OsTraceRelayClient}; use jkcli::{CollectedArguments, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new().help("Relay OS logs")
}
#[tokio::main] pub async fn main(_arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
async fn main() {
tracing_subscriber::fmt::init();
let matches = Command::new("os_trace_relay")
.about("Relay system logs")
.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),
)
.get_matches();
if matches.get_flag("about") {
println!("Relay logs on the device");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider = match common::get_provider(udid, host, pairing_file, "misagent-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let log_client = OsTraceRelayClient::connect(&*provider) let log_client = OsTraceRelayClient::connect(&*provider)
.await .await
.expect("Unable to connect to misagent"); .expect("Unable to connect to misagent");

View File

@@ -1,46 +1,35 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command};
use idevice::{ use idevice::{
IdeviceService, IdeviceService,
lockdown::LockdownClient, lockdown::LockdownClient,
provider::IdeviceProvider,
usbmuxd::{Connection, UsbmuxdAddr, UsbmuxdConnection}, usbmuxd::{Connection, UsbmuxdAddr, UsbmuxdConnection},
}; };
use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag};
#[tokio::main] pub fn register() -> JkCommand {
async fn main() { JkCommand::new()
tracing_subscriber::fmt::init(); .help("Manage files in the AFC jail of a device")
.with_argument(JkArgument::new().with_help("A UDID to override and pair with"))
let matches = Command::new("pair") .with_flag(
.about("Pair with the device") JkFlag::new("name")
.arg( .with_help("The host name to report to the device")
Arg::new("udid") .with_argument(JkArgument::new().required(true))
.value_name("UDID") .with_short("n"),
.help("UDID of the device (overrides host/pairing file)")
.index(1),
) )
.arg( }
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.get_matches();
if matches.get_flag("about") { pub async fn main(arguments: &CollectedArguments, _provider: Box<dyn IdeviceProvider>) {
println!("pair - pair with the device"); let mut arguments = arguments.clone();
println!("Copyright (c) 2025 Jackson Coxson"); let udid: Option<String> = arguments.next_argument();
return;
}
let udid = matches.get_one::<String>("udid");
let mut u = UsbmuxdConnection::default() let mut u = UsbmuxdConnection::default()
.await .await
.expect("Failed to connect to usbmuxd"); .expect("Failed to connect to usbmuxd");
let dev = match udid { let dev = match udid {
Some(udid) => u Some(udid) => u
.get_device(udid) .get_device(udid.as_str())
.await .await
.expect("Failed to get device with specific udid"), .expect("Failed to get device with specific udid"),
None => u None => u
@@ -62,8 +51,11 @@ async fn main() {
}; };
let id = uuid::Uuid::new_v4().to_string().to_uppercase(); let id = uuid::Uuid::new_v4().to_string().to_uppercase();
let name = arguments.get_flag::<String>("name");
let name = name.as_deref();
let mut pairing_file = lockdown_client let mut pairing_file = lockdown_client
.pair(id, u.get_buid().await.unwrap()) .pair(id, u.get_buid().await.unwrap(), name)
.await .await
.expect("Failed to pair"); .expect("Failed to pair");

View File

@@ -1,55 +1,20 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command};
use idevice::{ use idevice::{
IdeviceService, IdeviceService,
pcapd::{PcapFileWriter, PcapdClient}, pcapd::{PcapFileWriter, PcapdClient},
provider::IdeviceProvider,
}; };
use jkcli::{CollectedArguments, JkArgument, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new()
.help("Writes pcap network data")
.with_argument(JkArgument::new().with_help("Write PCAP to this file (use '-' for stdout)"))
}
#[tokio::main] pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
async fn main() { let out = arguments.clone().next_argument::<String>();
tracing_subscriber::fmt::init();
let matches = Command::new("pcapd")
.about("Capture IP packets")
.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),
)
.arg(
Arg::new("out")
.long("out")
.value_name("PCAP")
.help("Write PCAP to this file (use '-' for stdout)"),
)
.get_matches();
if matches.get_flag("about") {
println!("bt_packet_logger - capture bluetooth packets");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let out = matches.get_one::<String>("out").map(String::to_owned);
let provider = match common::get_provider(udid, None, None, "pcapd-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let mut logger_client = PcapdClient::connect(&*provider) let mut logger_client = PcapdClient::connect(&*provider)
.await .await

View File

@@ -1,76 +1,34 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command}; use idevice::{IdeviceService, preboard_service::PreboardServiceClient, provider::IdeviceProvider};
use idevice::{IdeviceService, preboard_service::PreboardServiceClient}; use jkcli::{CollectedArguments, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new()
#[tokio::main] .help("Interact with the preboard service")
async fn main() { .with_subcommand("create", JkCommand::new().help("Create a stashbag??"))
tracing_subscriber::fmt::init(); .with_subcommand("commit", JkCommand::new().help("Commit a stashbag??"))
.subcommand_required(true)
let matches = Command::new("preboard") }
.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("create").about("Create a stashbag??"))
.subcommand(Command::new("commit").about("Commit a stashbag??"))
.get_matches();
if matches.get_flag("about") {
println!("preboard - no idea what this does");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider = match common::get_provider(udid, host, pairing_file, "amfi-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
let mut pc = PreboardServiceClient::connect(&*provider) let mut pc = PreboardServiceClient::connect(&*provider)
.await .await
.expect("Failed to connect to Preboard"); .expect("Failed to connect to Preboard");
if matches.subcommand_matches("create").is_some() { let (sub_name, _) = arguments.first_subcommand().unwrap();
pc.create_stashbag(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 0])
.await match sub_name.as_str() {
.expect("Failed to create"); "create" => {
} else if matches.subcommand_matches("commit").is_some() { pc.create_stashbag(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 0])
pc.commit_stashbag(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]) .await
.await .expect("Failed to create");
.expect("Failed to create"); }
} else { "commit" => {
eprintln!("Invalid usage, pass -h for help"); pc.commit_stashbag(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 0])
.await
.expect("Failed to create");
}
_ => unreachable!(),
} }
return;
} }

View File

@@ -1,76 +1,26 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command}; use idevice::provider::IdeviceProvider;
use idevice::services::lockdown::LockdownClient; use idevice::services::lockdown::LockdownClient;
use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake};
use jkcli::{CollectedArguments, JkArgument, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new()
.help("Launch an app with process control")
.with_argument(
JkArgument::new()
.required(true)
.with_help("The bundle ID to launch"),
)
}
#[tokio::main] pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
async fn main() {
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
let matches = Command::new("process_control") let mut arguments = arguments.clone();
.about("Query process control")
.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(2),
)
.arg(
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("tunneld")
.long("tunneld")
.help("Use tunneld for connection")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("bundle_id")
.value_name("Bundle ID")
.help("Bundle ID of the app to launch")
.index(1),
)
.get_matches();
if matches.get_flag("about") { let bundle_id: String = arguments.next_argument().expect("No bundle ID specified");
println!("process_control - launch and manage processes on the device");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let pairing_file = matches.get_one::<String>("pairing_file");
let host = matches.get_one::<String>("host");
let bundle_id = matches
.get_one::<String>("bundle_id")
.expect("No bundle ID specified");
let provider =
match common::get_provider(udid, host, pairing_file, "process_control-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let mut rs_client_opt: Option< let mut rs_client_opt: Option<
idevice::dvt::remote_server::RemoteServerClient<Box<dyn idevice::ReadWrite>>, idevice::dvt::remote_server::RemoteServerClient<Box<dyn idevice::ReadWrite>>,

View File

@@ -1,65 +1,17 @@
// Jackson Coxson // Jackson Coxson
// Print out all the RemoteXPC services // Print out all the RemoteXPC services
use clap::{Arg, Command};
use idevice::{ use idevice::{
IdeviceService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, IdeviceService, core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider,
tcp::stream::AdapterStream, rsd::RsdHandshake, tcp::stream::AdapterStream,
}; };
use jkcli::{CollectedArguments, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new().help("Get services from RemoteXPC")
#[tokio::main] }
async fn main() {
tracing_subscriber::fmt::init();
let matches = Command::new("remotexpc")
.about("Get services from RemoteXPC")
.arg(
Arg::new("host")
.long("host")
.value_name("HOST")
.help("IP address of the device"),
)
.arg(
Arg::new("pairing_file")
.long("pairing-file")
.value_name("PATH")
.help("Path to the pairing file"),
)
.arg(
Arg::new("udid")
.value_name("UDID")
.help("UDID of the device (overrides host/pairing file)")
.index(1),
)
.arg(
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.get_matches();
if matches.get_flag("about") {
println!("remotexpc - get info from RemoteXPC");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let pairing_file = matches.get_one::<String>("pairing_file");
let host = matches.get_one::<String>("host");
let provider = match common::get_provider(udid, host, pairing_file, "remotexpc-jkcoxson").await
{
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
pub async fn main(_arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
let proxy = CoreDeviceProxy::connect(&*provider) let proxy = CoreDeviceProxy::connect(&*provider)
.await .await
.expect("no core proxy"); .expect("no core proxy");

View File

@@ -1,76 +1,41 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command};
use idevice::{ use idevice::{
IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, pretty_print_dictionary, IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider,
restore_service::RestoreServiceClient, rsd::RsdHandshake, restore_service::RestoreServiceClient, rsd::RsdHandshake,
}; };
use jkcli::{CollectedArguments, JkArgument, JkCommand};
use plist_macro::pretty_print_dictionary;
mod common; pub fn register() -> JkCommand {
JkCommand::new()
#[tokio::main] .help("Interact with the Restore Service service")
async fn main() { .with_subcommand("delay", JkCommand::new().help("Delay recovery image"))
tracing_subscriber::fmt::init(); .with_subcommand("recovery", JkCommand::new().help("Enter recovery mode"))
.with_subcommand("reboot", JkCommand::new().help("Reboots the device"))
let matches = Command::new("restore_service") .with_subcommand(
.about("Interact with the Restore Service service") "preflightinfo",
.arg( JkCommand::new().help("Gets the preflight info"),
Arg::new("host")
.long("host")
.value_name("HOST")
.help("IP address of the device"),
) )
.arg( .with_subcommand("nonces", JkCommand::new().help("Gets the nonces"))
Arg::new("pairing_file") .with_subcommand(
.long("pairing-file") "app_parameters",
.value_name("PATH") JkCommand::new().help("Gets the app parameters"),
.help("Path to the pairing file"),
) )
.arg( .with_subcommand(
Arg::new("udid") "restore_lang",
.value_name("UDID") JkCommand::new()
.help("UDID of the device (overrides host/pairing file)"), .help("Restores the language")
.with_argument(
JkArgument::new()
.required(true)
.with_help("Language to restore"),
),
) )
.arg( .subcommand_required(true)
Arg::new("about") }
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.subcommand(Command::new("delay").about("Delay recovery image"))
.subcommand(Command::new("recovery").about("Enter recovery mode"))
.subcommand(Command::new("reboot").about("Reboots the device"))
.subcommand(Command::new("preflightinfo").about("Gets the preflight info"))
.subcommand(Command::new("nonces").about("Gets the nonces"))
.subcommand(Command::new("app_parameters").about("Gets the app parameters"))
.subcommand(
Command::new("restore_lang")
.about("Restores the language")
.arg(Arg::new("language").required(true).index(1)),
)
.get_matches();
if matches.get_flag("about") {
println!(
"mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary."
);
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider =
match common::get_provider(udid, host, pairing_file, "restore_service-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
let proxy = CoreDeviceProxy::connect(&*provider) let proxy = CoreDeviceProxy::connect(&*provider)
.await .await
.expect("no core proxy"); .expect("no core proxy");
@@ -88,37 +53,46 @@ async fn main() {
.await .await
.expect("Unable to connect to service"); .expect("Unable to connect to service");
if matches.subcommand_matches("recovery").is_some() { let (sub_name, sub_args) = arguments.first_subcommand().unwrap();
restore_client let mut sub_args = sub_args.clone();
.enter_recovery()
.await match sub_name.as_str() {
.expect("command failed"); "recovery" => {
} else if matches.subcommand_matches("reboot").is_some() { restore_client
restore_client.reboot().await.expect("command failed"); .enter_recovery()
} else if matches.subcommand_matches("preflightinfo").is_some() { .await
let info = restore_client .expect("command failed");
.get_preflightinfo() }
.await "reboot" => {
.expect("command failed"); restore_client.reboot().await.expect("command failed");
pretty_print_dictionary(&info); }
} else if matches.subcommand_matches("nonces").is_some() { "preflightinfo" => {
let nonces = restore_client.get_nonces().await.expect("command failed"); let info = restore_client
pretty_print_dictionary(&nonces); .get_preflightinfo()
} else if matches.subcommand_matches("app_parameters").is_some() { .await
let params = restore_client .expect("command failed");
.get_app_parameters() println!("{}", pretty_print_dictionary(&info));
.await }
.expect("command failed"); "nonces" => {
pretty_print_dictionary(&params); let nonces = restore_client.get_nonces().await.expect("command failed");
} else if let Some(matches) = matches.subcommand_matches("restore_lang") { println!("{}", pretty_print_dictionary(&nonces));
let lang = matches }
.get_one::<String>("language") "app_parameters" => {
.expect("No language passed"); let params = restore_client
restore_client .get_app_parameters()
.restore_lang(lang) .await
.await .expect("command failed");
.expect("failed to restore lang"); println!("{}", pretty_print_dictionary(&params));
} else { }
eprintln!("Invalid usage, pass -h for help"); "restore_lang" => {
let lang: String = sub_args
.next_argument::<String>()
.expect("No language passed");
restore_client
.restore_lang(lang)
.await
.expect("failed to restore lang");
}
_ => unreachable!(),
} }
} }

View File

@@ -1,69 +1,20 @@
use clap::{Arg, Command}; use idevice::{
use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider,
rsd::RsdHandshake,
};
use jkcli::{CollectedArguments, JkArgument, JkCommand};
use std::fs; use std::fs;
use idevice::screenshotr::ScreenshotService; use idevice::screenshotr::ScreenshotService;
mod common; pub fn register() -> JkCommand {
JkCommand::new()
.help("Take a screenshot")
.with_argument(JkArgument::new().with_help("Output path").required(true))
}
#[tokio::main] pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
async fn main() { let output_path = arguments.clone().next_argument::<String>().unwrap();
tracing_subscriber::fmt::init();
let matches = Command::new("screen_shot")
.about("take screenshot")
.arg(
Arg::new("host")
.long("host")
.value_name("HOST")
.help("IP address of the device"),
)
.arg(
Arg::new("pairing_file")
.long("pairing-file")
.value_name("PATH")
.help("Path to the pairing file"),
)
.arg(
Arg::new("udid")
.value_name("UDID")
.help("UDID of the device (overrides host/pairing file)")
.index(1),
)
.arg(
Arg::new("output")
.short('o')
.long("output")
.value_name("FILE")
.help("Output file path for the screenshot (default: ./screenshot.png)")
.default_value("screenshot.png"),
)
.arg(
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.get_matches();
if matches.get_flag("about") {
print!("screen_shot - take screenshot from ios device");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let output_path = matches.get_one::<String>("output").unwrap();
let provider =
match common::get_provider(udid, host, pairing_file, "take_screenshot-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let res = if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await { let res = if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await {
println!("Using DVT over CoreDeviceProxy"); println!("Using DVT over CoreDeviceProxy");
@@ -104,7 +55,7 @@ async fn main() {
screenshot_client.take_screenshot().await.unwrap() screenshot_client.take_screenshot().await.unwrap()
}; };
match fs::write(output_path, res) { match fs::write(&output_path, res) {
Ok(_) => println!("Screenshot saved to: {}", output_path), Ok(_) => println!("Screenshot saved to: {}", output_path),
Err(e) => eprintln!("Failed to write screenshot to file: {}", e), Err(e) => eprintln!("Failed to write screenshot to file: {}", e),
} }

View File

@@ -0,0 +1,161 @@
// Jackson Coxson
use idevice::{
IdeviceService, provider::IdeviceProvider, springboardservices::SpringBoardServicesClient,
};
use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag};
use plist_macro::{plist_value_to_xml_bytes, pretty_print_plist};
pub fn register() -> JkCommand {
JkCommand::new()
.help("Manage the springboard service")
.with_subcommand(
"get_icon_state",
JkCommand::new()
.help("Gets the icon state from the device")
.with_argument(
JkArgument::new()
.with_help("Version to query by")
.required(false),
)
.with_flag(
JkFlag::new("save")
.with_help("Path to save to")
.with_argument(JkArgument::new().required(true)),
),
)
.with_subcommand(
"set_icon_state",
JkCommand::new().help("Sets the icon state").with_argument(
JkArgument::new()
.with_help("plist to set based on")
.required(true),
),
)
.with_subcommand(
"get_wallpaper_preview",
JkCommand::new()
.help("Gets wallpaper preview")
.with_subcommand("homescreen", JkCommand::new())
.with_subcommand("lockscreen", JkCommand::new())
.subcommand_required(true)
.with_flag(
JkFlag::new("save")
.with_help("Path to save the wallpaper PNG file, or preview.png by default")
.with_argument(JkArgument::new().required(true)),
),
)
.with_subcommand(
"get_interface_orientation",
JkCommand::new().help("Gets the device's current screen orientation"),
)
.with_subcommand(
"get_homescreen_icon_metrics",
JkCommand::new().help("Gets home screen icon layout metrics"),
)
.with_subcommand(
"get_icon",
JkCommand::new()
.help("Gets an app's icon as PNG")
.with_argument(
JkArgument::new()
.with_help("Bundle identifier (e.g. com.apple.Maps)")
.required(true),
)
.with_flag(
JkFlag::new("save")
.with_help("Path to save the icon PNG file, or icon.png by default")
.with_argument(JkArgument::new().required(true)),
),
)
.subcommand_required(true)
}
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
let mut sbc = SpringBoardServicesClient::connect(&*provider)
.await
.expect("Failed to connect to springboardservices");
let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand passed");
let mut sub_args = sub_args.clone();
match sub_name.as_str() {
"get_icon_state" => {
let version: Option<String> = sub_args.next_argument();
let version = version.as_deref();
let state = sbc
.get_icon_state(version)
.await
.expect("Failed to get icon state");
println!("{}", pretty_print_plist(&state));
if let Some(path) = sub_args.get_flag::<String>("save") {
tokio::fs::write(path, plist_value_to_xml_bytes(&state))
.await
.expect("Failed to save to path");
}
}
"set_icon_state" => {
let load_path = sub_args.next_argument::<String>().unwrap();
let load = tokio::fs::read(load_path)
.await
.expect("Failed to read plist");
let load: plist::Value =
plist::from_bytes(&load).expect("Failed to parse bytes as plist");
sbc.set_icon_state(load)
.await
.expect("Failed to set icon state");
}
"get_wallpaper_preview" => {
let (wallpaper_type, _) = sub_args.first_subcommand().unwrap();
let wallpaper = match wallpaper_type.as_str() {
"homescreen" => sbc.get_home_screen_wallpaper_preview_pngdata().await,
"lockscreen" => sbc.get_lock_screen_wallpaper_preview_pngdata().await,
_ => panic!("Invalid wallpaper type. Use 'homescreen' or 'lockscreen'"),
}
.expect("Failed to get wallpaper preview");
let save_path = sub_args
.get_flag::<String>("save")
.unwrap_or("preview.png".to_string());
tokio::fs::write(&save_path, wallpaper)
.await
.expect("Failed to save wallpaper");
}
"get_interface_orientation" => {
let orientation = sbc
.get_interface_orientation()
.await
.expect("Failed to get interface orientation");
println!("{:?}", orientation);
}
"get_homescreen_icon_metrics" => {
let metrics = sbc
.get_homescreen_icon_metrics()
.await
.expect("Failed to get homescreen icon metrics");
let metrics_value = plist::Value::Dictionary(metrics);
println!("{}", pretty_print_plist(&metrics_value));
}
"get_icon" => {
let bundle_id = sub_args.next_argument::<String>().unwrap();
let icon_data = sbc
.get_icon_pngdata(bundle_id)
.await
.expect("Failed to get icon");
let save_path = sub_args
.get_flag::<String>("save")
.unwrap_or("icon.png".to_string());
tokio::fs::write(&save_path, icon_data)
.await
.expect("Failed to save icon");
}
_ => unreachable!(),
}
}

View File

@@ -1,58 +1,13 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command}; use idevice::{IdeviceService, provider::IdeviceProvider, syslog_relay::SyslogRelayClient};
use idevice::{IdeviceService, syslog_relay::SyslogRelayClient}; use jkcli::{CollectedArguments, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new().help("Relay system logs")
}
#[tokio::main] pub async fn main(_arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
async fn main() {
tracing_subscriber::fmt::init();
let matches = Command::new("syslog_relay")
.about("Relay system logs")
.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),
)
.get_matches();
if matches.get_flag("about") {
println!("Relay logs on the device");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider = match common::get_provider(udid, host, pairing_file, "misagent-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let mut log_client = SyslogRelayClient::connect(&*provider) let mut log_client = SyslogRelayClient::connect(&*provider)
.await .await
.expect("Unable to connect to misagent"); .expect("Unable to connect to misagent");