14 Commits

Author SHA1 Message Date
se2crid
e79d1c0498 fix(pair_rsd_ios): handle Linux mDNS link-local fallback (#73) 2026-02-18 12:18:40 -07:00
Jackson Coxson
0944708a8f Allow rppairing with underlying RemoteXPC connection 2026-02-18 09:09:43 -07:00
Jackson Coxson
7077e70e24 Send weird flags after opening reply stream 2026-02-17 07:41:17 -07:00
Jackson Coxson
7782df8bd9 Set RemoteXPC initial root ID to 0 2026-02-17 07:33:51 -07:00
Jackson Coxson
93d2f1b28c Merge master into rppairing-try2 2026-02-14 13:32:14 -07:00
khcrysalis
b459eebe9d feat: support ios-arm64_x86_64-maccatalyst in xcframework (#69)
* feat: support maccatalyst in xcframework

* fix: missing ios-macabi targets in ci
2026-02-14 13:19:32 -07:00
neo
c246362f54 chore(readme): update (#68)
* feat(springboard): add get_icon subcommand

* chore: update readme
2026-02-14 13:18:37 -07:00
neo
bfe44e16e4 feat: notification proxy (#70)
* init

* chore: clippy and fmt

* feat: ffi wrapper

* feat: multi-observe and timeout to notification proxy

* fix: nitpicks

1. proxy death its onw error in emun #69
2. make returned stream actual stream, copied from 54439b85dd/idevice/src/services/bt_packet_logger.rs (L126-L138)
2026-02-14 13:16:26 -07:00
neo
54439b85dd feat(springboard): get homescreen icon metrics (#67)
* feat(springboard): get homescreen icon metrics

* chore: clippy and fmt
2026-02-13 13:00:47 -07:00
Jackson Coxson
f8c5010b34 Start work on iOS rppairing 2025-12-31 16:12:47 -07:00
Jackson Coxson
637758ad7f Create pair_rsd_ios tool 2025-12-23 07:23:26 -07:00
Jackson Coxson
f5be1a000a Add more debug logging to rppairing 2025-12-19 00:01:28 -07:00
Jackson Coxson
d6e7b9aef4 Use idevice-srp crate 2025-12-18 21:32:40 -07:00
Jackson Coxson
4bea784260 Initial rppairing support 2025-12-18 21:21:30 -07:00
27 changed files with 3540 additions and 322 deletions

View File

@@ -35,6 +35,8 @@ jobs:
rustup target add aarch64-apple-ios-sim && \ 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 && \ 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 cargo install --force --locked bindgen-cli
- name: Build all Apple targets and examples/tools - name: Build all Apple targets and examples/tools

1264
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,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,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"]
@@ -61,6 +62,7 @@ full = [
"diagnostics_relay", "diagnostics_relay",
"dvt", "dvt",
"heartbeat", "heartbeat",
"notification_proxy",
"house_arrest", "house_arrest",
"installation_proxy", "installation_proxy",
"misagent", "misagent",

View File

@@ -29,6 +29,8 @@ 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;

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) };
}
}

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,
@@ -263,6 +264,43 @@ pub unsafe extern "C" fn springboard_services_get_interface_orientation(
} }
} }
/// 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

@@ -21,7 +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" } plist-macro = { version = "0.1.6" }
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 }
@@ -31,7 +31,7 @@ tracing = { version = "0.1.41" }
base64 = { version = "0.22" } base64 = { version = "0.22" }
indexmap = { version = "2.11", features = ["serde"], optional = true } indexmap = { version = "2.11", features = ["serde"], optional = true }
uuid = { version = "1.18", features = ["serde", "v4"], optional = true } uuid = { version = "1.18", features = ["serde", "v3", "v4"], optional = true }
chrono = { version = "0.4", optional = true, default-features = false, features = [ chrono = { version = "0.4", optional = true, default-features = false, features = [
"serde", "serde",
] } ] }
@@ -55,6 +55,11 @@ x509-cert = { version = "0.2", optional = true, features = [
"builder", "builder",
"pem", "pem",
], default-features = false } ], default-features = false }
x25519-dalek = { version = "2", optional = true }
ed25519-dalek = { version = "2", features = ["rand_core"], optional = true }
hkdf = { version = "0.12", optional = true }
chacha20poly1305 = { version = "0.10", optional = true }
idevice-srp = { version = "0.6", optional = true }
obfstr = { version = "0.4", optional = true } obfstr = { version = "0.4", optional = true }
@@ -97,11 +102,27 @@ 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 = []
preboard_service = [] preboard_service = []
obfuscate = ["dep:obfstr"] obfuscate = ["dep:obfstr"]
remote_pairing = [
"dep:serde_json",
"dep:json",
"dep:x25519-dalek",
"dep:ed25519-dalek",
"dep:hkdf",
"dep:chacha20poly1305",
"dep:idevice-srp",
"dep:uuid",
]
restore_service = [] restore_service = []
rsd = ["xpc"] rsd = ["xpc"]
screenshotr = [] screenshotr = []
@@ -138,9 +159,11 @@ full = [
"mobile_image_mounter", "mobile_image_mounter",
"mobileactivationd", "mobileactivationd",
"mobilebackup2", "mobilebackup2",
"notification_proxy",
"pair", "pair",
"pcapd", "pcapd",
"preboard_service", "preboard_service",
"remote_pairing",
"restore_service", "restore_service",
"rsd", "rsd",
"screenshotr", "screenshotr",

View File

@@ -9,6 +9,8 @@ pub mod cursor;
mod obfuscation; mod obfuscation;
pub mod pairing_file; pub mod pairing_file;
pub mod provider; pub mod provider;
#[cfg(feature = "remote_pairing")]
pub mod remote_pairing;
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
mod sni; mod sni;
#[cfg(feature = "tunnel_tcp_stack")] #[cfg(feature = "tunnel_tcp_stack")]
@@ -865,6 +867,26 @@ pub enum IdeviceError {
#[error("Developer mode is not enabled")] #[error("Developer mode is not enabled")]
DeveloperModeNotEnabled = -68, DeveloperModeNotEnabled = -68,
#[error("Unknown TLV {0}")]
UnknownTlv(u8) = -69,
#[error("Malformed TLV")]
MalformedTlv = -70,
#[error("Pairing rejected: {0}")]
PairingRejected(String) = -71,
#[cfg(feature = "remote_pairing")]
#[error("Base64 decode error")]
Base64DecodeError(#[from] base64::DecodeError) = -72,
#[error("Pair verified failed")]
PairVerifyFailed = -73,
#[error("SRP auth failed")]
SrpAuthFailed = -74,
#[cfg(feature = "remote_pairing")]
#[error("Chacha encryption error")]
ChachaEncryption(chacha20poly1305::Error) = -75,
#[cfg(feature = "notification_proxy")]
#[error("notification proxy died")]
NotificationProxyDeath = -76,
} }
impl IdeviceError { impl IdeviceError {
@@ -1030,6 +1052,17 @@ impl IdeviceError {
#[cfg(feature = "installation_proxy")] #[cfg(feature = "installation_proxy")]
IdeviceError::MalformedPackageArchive(_) => -67, IdeviceError::MalformedPackageArchive(_) => -67,
IdeviceError::DeveloperModeNotEnabled => -68, IdeviceError::DeveloperModeNotEnabled => -68,
IdeviceError::UnknownTlv(_) => -69,
IdeviceError::MalformedTlv => -70,
IdeviceError::PairingRejected(_) => -71,
#[cfg(feature = "remote_pairing")]
IdeviceError::Base64DecodeError(_) => -72,
IdeviceError::PairVerifyFailed => -73,
IdeviceError::SrpAuthFailed => -74,
IdeviceError::ChachaEncryption(_) => -75,
#[cfg(feature = "notification_proxy")]
IdeviceError::NotificationProxyDeath => -76,
} }
} }
} }

View File

@@ -0,0 +1,720 @@
//! Remote Pairing
use crate::IdeviceError;
use chacha20poly1305::{
ChaCha20Poly1305, Key, KeyInit, Nonce,
aead::{Aead, Payload},
};
use ed25519_dalek::Signature;
use hkdf::Hkdf;
use idevice_srp::{client::SrpClient, groups::G_3072};
use plist_macro::plist;
use plist_macro::{PlistConvertible, PlistExt};
use rand::RngCore;
use rsa::{rand_core::OsRng, signature::SignerMut};
use serde::Serialize;
use sha2::Sha512;
use tracing::{debug, warn};
use x25519_dalek::{EphemeralSecret, PublicKey as X25519PublicKey};
mod opack;
mod rp_pairing_file;
mod socket;
mod tlv;
// export
pub use rp_pairing_file::RpPairingFile;
pub use socket::{RpPairingSocket, RpPairingSocketProvider};
const RPPAIRING_MAGIC: &[u8] = b"RPPairing";
const WIRE_PROTOCOL_VERSION: u8 = 19;
pub struct RemotePairingClient<'a, R: RpPairingSocketProvider> {
inner: R,
sequence_number: usize,
pairing_file: &'a mut RpPairingFile,
sending_host: String,
client_cipher: ChaCha20Poly1305,
server_cipher: ChaCha20Poly1305,
}
impl<'a, R: RpPairingSocketProvider> RemotePairingClient<'a, R> {
pub fn new(inner: R, sending_host: &str, pairing_file: &'a mut RpPairingFile) -> Self {
let hk = Hkdf::<sha2::Sha512>::new(None, pairing_file.e_private_key.as_bytes());
let mut okm = [0u8; 32];
hk.expand(b"ClientEncrypt-main", &mut okm).unwrap();
let client_cipher = ChaCha20Poly1305::new(chacha20poly1305::Key::from_slice(&okm));
let hk = Hkdf::<sha2::Sha512>::new(None, pairing_file.e_private_key.as_bytes());
let mut okm = [0u8; 32];
hk.expand(b"ServerEncrypt-main", &mut okm).unwrap();
let server_cipher = ChaCha20Poly1305::new(chacha20poly1305::Key::from_slice(&okm));
Self {
inner,
sequence_number: 0,
pairing_file,
sending_host: sending_host.to_string(),
client_cipher,
server_cipher,
}
}
pub async fn connect<Fut, S>(
&mut self,
pin_callback: impl Fn(S) -> Fut,
state: S,
) -> Result<(), IdeviceError>
where
Fut: std::future::Future<Output = String>,
{
self.attempt_pair_verify().await?;
if self.validate_pairing().await.is_err() {
self.pair(pin_callback, state).await?;
}
Ok(())
}
pub async fn validate_pairing(&mut self) -> Result<(), IdeviceError> {
let x_private_key = EphemeralSecret::random_from_rng(OsRng);
let x_public_key = X25519PublicKey::from(&x_private_key);
let pairing_data = tlv::serialize_tlv8(&[
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::State,
data: vec![0x01],
},
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::PublicKey,
data: x_public_key.to_bytes().to_vec(),
},
]);
let pairing_data = R::serialize_bytes(&pairing_data);
self.send_pairing_data(plist!({
"data": pairing_data,
"kind": "verifyManualPairing",
"startNewSession": true
}))
.await?;
debug!("Waiting for response from verifyManualPairing");
let pairing_data = self.receive_pairing_data().await?;
let data = match R::deserialize_bytes(pairing_data) {
Some(d) => d,
None => {
return Err(IdeviceError::UnexpectedResponse);
}
};
let data = tlv::deserialize_tlv8(&data)?;
if data
.iter()
.any(|x| x.tlv_type == tlv::PairingDataComponentType::ErrorResponse)
{
self.send_pair_verified_failed().await?;
return Err(IdeviceError::PairVerifyFailed);
}
let device_public_key = match data
.iter()
.find(|x| x.tlv_type == tlv::PairingDataComponentType::PublicKey)
{
Some(d) => d,
None => {
warn!("No public key in TLV data");
return Err(IdeviceError::UnexpectedResponse);
}
};
let peer_pub_bytes: [u8; 32] = match device_public_key.data.as_slice().try_into() {
Ok(d) => d,
Err(_) => {
warn!("Device public key isn't the expected size");
return Err(IdeviceError::NotEnoughBytes(
32,
device_public_key.data.len(),
));
}
};
let device_public_key = x25519_dalek::PublicKey::from(peer_pub_bytes);
let shared_secret = x_private_key.diffie_hellman(&device_public_key);
// Derive encryption key with HKDF-SHA512
let hk =
Hkdf::<sha2::Sha512>::new(Some(b"Pair-Verify-Encrypt-Salt"), shared_secret.as_bytes());
let mut okm = [0u8; 32];
hk.expand(b"Pair-Verify-Encrypt-Info", &mut okm).unwrap();
// ChaCha20Poly1305 AEAD cipher
let cipher = ChaCha20Poly1305::new(chacha20poly1305::Key::from_slice(&okm));
let ed25519_signing_key = &mut self.pairing_file.e_private_key;
let mut signbuf = Vec::with_capacity(32 + self.pairing_file.identifier.len() + 32);
signbuf.extend_from_slice(x_public_key.as_bytes()); // 32 bytes
signbuf.extend_from_slice(self.pairing_file.identifier.as_bytes()); // variable
signbuf.extend_from_slice(device_public_key.as_bytes()); // 32 bytes
let signature: Signature = ed25519_signing_key.sign(&signbuf);
let plaintext = vec![
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::Identifier,
data: self.pairing_file.identifier.as_bytes().to_vec(),
},
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::Signature,
data: signature.to_vec(),
},
];
let plaintext = tlv::serialize_tlv8(&plaintext);
let nonce = Nonce::from_slice(b"\x00\x00\x00\x00PV-Msg03"); // 12-byte nonce
let ciphertext = cipher
.encrypt(
nonce,
Payload {
msg: &plaintext,
aad: &[],
},
)
.expect("encryption should not fail");
let msg = vec![
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::State,
data: [0x03].to_vec(),
},
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::EncryptedData,
data: ciphertext,
},
];
debug!("Waiting for signbuf response");
self.send_pairing_data(plist! ({
"data": R::serialize_bytes(&tlv::serialize_tlv8(&msg)),
"kind": "verifyManualPairing",
"startNewSession": false
}))
.await?;
let res = self.receive_pairing_data().await?;
let data = match R::deserialize_bytes(res) {
Some(d) => d,
None => return Err(IdeviceError::UnexpectedResponse),
};
let data = tlv::deserialize_tlv8(&data)?;
debug!("Verify TLV: {data:#?}");
// Check if the device responded with an error (which is expected for a new pairing)
if data
.iter()
.any(|x| x.tlv_type == tlv::PairingDataComponentType::ErrorResponse)
{
debug!(
"Verification failed, device reported an error. This is expected for a new pairing."
);
self.send_pair_verified_failed().await?;
// Return a specific error to the caller.
return Err(IdeviceError::PairVerifyFailed);
}
Ok(())
}
pub async fn send_pair_verified_failed(&mut self) -> Result<(), IdeviceError> {
self.inner
.send_plain(
plist!({
"event": {
"_0": {
"pairVerifyFailed": {}
}
}
}),
self.sequence_number,
)
.await?;
self.sequence_number += 1;
Ok(())
}
pub async fn attempt_pair_verify(&mut self) -> Result<plist::Value, IdeviceError> {
debug!("Sending attemptPairVerify");
self.inner
.send_plain(
plist!({
"request": {
"_0": {
"handshake": {
"_0": {
"hostOptions": {
"attemptPairVerify": true
},
"wireProtocolVersion": plist::Value::Integer(WIRE_PROTOCOL_VERSION.into()),
}
}
}
}
}),
self.sequence_number,
)
.await?;
self.sequence_number += 1;
debug!("Waiting for attemptPairVerify response");
let response = self.inner.recv_plain().await?;
let response = response
.as_dictionary()
.and_then(|x| x.get("response"))
.and_then(|x| x.as_dictionary())
.and_then(|x| x.get("_1"))
.and_then(|x| x.as_dictionary())
.and_then(|x| x.get("handshake"))
.and_then(|x| x.as_dictionary())
.and_then(|x| x.get("_0"));
match response {
Some(v) => Ok(v.to_owned()),
None => Err(IdeviceError::UnexpectedResponse),
}
}
pub async fn pair<Fut, S>(
&mut self,
pin_callback: impl Fn(S) -> Fut,
state: S,
) -> Result<(), IdeviceError>
where
Fut: std::future::Future<Output = String>,
{
let (salt, public_key, pin) = self.request_pair_consent(pin_callback, state).await?;
let key = self.init_srp_context(&salt, &public_key, &pin).await?;
self.save_pair_record_on_peer(&key).await?;
Ok(())
}
/// Returns salt and public key and pin
async fn request_pair_consent<Fut, S>(
&mut self,
pin_callback: impl Fn(S) -> Fut,
state: S,
) -> Result<(Vec<u8>, Vec<u8>, String), IdeviceError>
where
Fut: std::future::Future<Output = String>,
{
let tlv = tlv::serialize_tlv8(&[
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::Method,
data: vec![0x00],
},
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::State,
data: vec![0x01],
},
]);
let tlv = R::serialize_bytes(&tlv);
self.send_pairing_data(plist!({
"data": tlv,
"kind": "setupManualPairing",
"sendingHost": &self.sending_host,
"startNewSession": true
}))
.await?;
let response = self.inner.recv_plain().await?;
let response = match response
.get_by("event")
.and_then(|x| x.get_by("_0"))
.and_then(|x| x.as_dictionary())
{
Some(r) => r,
None => {
return Err(IdeviceError::UnexpectedResponse);
}
};
let mut pin = None;
let pairing_data = match if let Some(err) = response.get("pairingRejectedWithError") {
let context = err
.get_by("wrappedError")
.and_then(|x| x.get_by("userInfo"))
.and_then(|x| x.get_by("NSLocalizedDescription"))
.and_then(|x| x.as_string())
.map(|x| x.to_string());
return Err(IdeviceError::PairingRejected(context.unwrap_or_default()));
} else if response.get("awaitingUserConsent").is_some() {
pin = Some("000000".to_string());
Some(self.receive_pairing_data().await?)
} else {
// On Apple TV, we can get the pin now
response
.get_by("pairingData")
.and_then(|x| x.get_by("_0"))
.and_then(|x| x.get_by("data"))
.map(|x| x.to_owned())
} {
Some(p) => p,
None => {
return Err(IdeviceError::UnexpectedResponse);
}
};
let tlv = tlv::deserialize_tlv8(&match R::deserialize_bytes(pairing_data) {
Some(t) => t,
None => return Err(IdeviceError::UnexpectedResponse),
})?;
debug!("Received pairingData response: {tlv:#?}");
let mut salt = Vec::new();
let mut public_key = Vec::new();
for t in tlv {
match t.tlv_type {
tlv::PairingDataComponentType::Salt => {
salt = t.data;
}
tlv::PairingDataComponentType::PublicKey => {
public_key.extend(t.data);
}
tlv::PairingDataComponentType::ErrorResponse => {
warn!("Pairing data contained error response");
return Err(IdeviceError::UnexpectedResponse);
}
_ => {
continue;
}
}
}
let pin = match pin {
Some(p) => p,
None => pin_callback(state).await,
};
if salt.is_empty() || public_key.is_empty() {
warn!("Pairing data did not contain salt or public key");
return Err(IdeviceError::UnexpectedResponse);
}
Ok((salt, public_key, pin))
}
/// Returns the encryption key
async fn init_srp_context(
&mut self,
salt: &[u8],
public_key: &[u8],
pin: &str,
) -> Result<Vec<u8>, IdeviceError> {
let client = SrpClient::<Sha512>::new(
&G_3072, // PRIME_3072 + generator
);
let mut a_private = [0u8; 32];
rand::rng().fill_bytes(&mut a_private);
let a_public = client.compute_public_ephemeral(&a_private);
let verifier = match client.process_reply(
&a_private,
"Pair-Setup".as_bytes(),
&pin.as_bytes()[..6],
salt,
public_key,
false,
) {
Ok(v) => v,
Err(e) => {
warn!("SRP verifier creation failed: {e:?}");
return Err(IdeviceError::SrpAuthFailed);
}
};
let client_proof = verifier.proof();
let tlv = tlv::serialize_tlv8(&[
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::State,
data: vec![0x03],
},
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::PublicKey,
data: a_public[..254].to_vec(),
},
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::PublicKey,
data: a_public[254..].to_vec(),
},
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::Proof,
data: client_proof.to_vec(),
},
]);
let tlv = R::serialize_bytes(&tlv);
self.send_pairing_data(plist!({
"data": tlv,
"kind": "setupManualPairing",
"sendingHost": &self.sending_host,
"startNewSession": false,
}))
.await?;
let response = self.receive_pairing_data().await?;
let response = tlv::deserialize_tlv8(&match R::deserialize_bytes(response.to_owned()) {
Some(r) => r,
None => return Err(IdeviceError::UnexpectedResponse),
})?;
debug!("Proof response: {response:#?}");
let proof = match response
.iter()
.find(|x| x.tlv_type == tlv::PairingDataComponentType::Proof)
{
Some(p) => &p.data,
None => {
warn!("Proof response did not contain server proof");
return Err(IdeviceError::UnexpectedResponse);
}
};
match verifier.verify_server(proof) {
Ok(_) => Ok(verifier.key().to_vec()),
Err(e) => {
warn!("Server auth failed: {e:?}");
Err(IdeviceError::SrpAuthFailed)
}
}
}
async fn save_pair_record_on_peer(
&mut self,
encryption_key: &[u8],
) -> Result<Vec<tlv::TLV8Entry>, IdeviceError> {
let salt = b"Pair-Setup-Encrypt-Salt";
let info = b"Pair-Setup-Encrypt-Info";
let hk = Hkdf::<Sha512>::new(Some(salt), encryption_key);
let mut setup_encryption_key = [0u8; 32];
hk.expand(info, &mut setup_encryption_key)
.expect("HKDF expand failed");
self.pairing_file.recreate_signing_keys();
{
// new scope, update our signing keys
let hk = Hkdf::<sha2::Sha512>::new(None, self.pairing_file.e_private_key.as_bytes());
let mut okm = [0u8; 32];
hk.expand(b"ClientEncrypt-main", &mut okm).unwrap();
self.client_cipher = ChaCha20Poly1305::new(chacha20poly1305::Key::from_slice(&okm));
let hk = Hkdf::<sha2::Sha512>::new(None, self.pairing_file.e_private_key.as_bytes());
let mut okm = [0u8; 32];
hk.expand(b"ServerEncrypt-main", &mut okm).unwrap();
self.server_cipher = ChaCha20Poly1305::new(chacha20poly1305::Key::from_slice(&okm));
}
let hk = Hkdf::<Sha512>::new(Some(b"Pair-Setup-Controller-Sign-Salt"), encryption_key);
let mut signbuf = Vec::with_capacity(32 + self.pairing_file.identifier.len() + 32);
let mut hkdf_out = [0u8; 32];
hk.expand(b"Pair-Setup-Controller-Sign-Info", &mut hkdf_out)
.expect("HKDF expand failed");
signbuf.extend_from_slice(&hkdf_out);
signbuf.extend_from_slice(self.pairing_file.identifier.as_bytes());
signbuf.extend_from_slice(self.pairing_file.e_public_key.as_bytes());
let signature = self.pairing_file.e_private_key.sign(&signbuf);
let device_info = crate::plist!({
"altIRK": b"\xe9\xe8-\xc0jIykVoT\x00\x19\xb1\xc7{".to_vec(),
"btAddr": "11:22:33:44:55:66",
"mac": b"\x11\x22\x33\x44\x55\x66".to_vec(),
"remotepairing_serial_number": "AAAAAAAAAAAA",
"accountID": self.pairing_file.identifier.as_str(),
"model": "computer-model",
"name": self.sending_host.as_str()
});
let device_info = opack::plist_to_opack(&device_info);
let tlv = tlv::serialize_tlv8(&[
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::Identifier,
data: self.pairing_file.identifier.as_bytes().to_vec(),
},
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::PublicKey,
data: self.pairing_file.e_public_key.to_bytes().to_vec(),
},
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::Signature,
data: signature.to_vec(),
},
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::Info,
data: device_info,
},
]);
let key = Key::from_slice(&setup_encryption_key); // 32 bytes
let cipher = ChaCha20Poly1305::new(key);
let nonce = Nonce::from_slice(b"\x00\x00\x00\x00PS-Msg05"); // 12 bytes
let plaintext = &tlv;
let ciphertext = match cipher.encrypt(
nonce,
Payload {
msg: plaintext,
aad: b"",
},
) {
Ok(c) => c,
Err(e) => {
warn!("Chacha encryption failed: {e:?}");
return Err(IdeviceError::ChachaEncryption(e));
}
};
debug!("ciphertext len: {}", ciphertext.len());
let tlv = tlv::serialize_tlv8(&[
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::EncryptedData,
data: ciphertext[..254].to_vec(),
},
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::EncryptedData,
data: ciphertext[254..].to_vec(),
},
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::State,
data: vec![0x05],
},
]);
let tlv = R::serialize_bytes(&tlv);
debug!("Sending encrypted data");
self.send_pairing_data(plist!({
"data": tlv,
"kind": "setupManualPairing",
"sendingHost": &self.sending_host,
"startNewSession": false,
}))
.await?;
debug!("Waiting for encrypted data");
let response = match R::deserialize_bytes(self.receive_pairing_data().await?) {
Some(r) => r,
None => {
warn!("Pairing data response was not deserializable");
return Err(IdeviceError::UnexpectedResponse);
}
};
let tlv = tlv::deserialize_tlv8(&response)?;
let mut encrypted_data = Vec::new();
for t in tlv {
match t.tlv_type {
tlv::PairingDataComponentType::EncryptedData => encrypted_data.extend(t.data),
tlv::PairingDataComponentType::ErrorResponse => {
warn!("TLV contained error response");
return Err(IdeviceError::UnexpectedResponse);
}
_ => {}
}
}
let nonce = Nonce::from_slice(b"\x00\x00\x00\x00PS-Msg06");
let plaintext = cipher
.decrypt(
nonce,
Payload {
msg: &encrypted_data,
aad: b"",
},
)
.expect("decryption failure!");
let tlv = tlv::deserialize_tlv8(&plaintext)?;
debug!("Decrypted plaintext TLV: {tlv:?}");
Ok(tlv)
}
async fn send_pairing_data(
&mut self,
pairing_data: impl Serialize + PlistConvertible,
) -> Result<(), IdeviceError> {
self.inner
.send_plain(
plist!({
"event": {
"_0": {
"pairingData": {
"_0": pairing_data
}
}
}
}),
self.sequence_number,
)
.await?;
self.sequence_number += 1;
Ok(())
}
async fn receive_pairing_data(&mut self) -> Result<plist::Value, IdeviceError> {
let response = self.inner.recv_plain().await?;
let response = match response.get_by("event").and_then(|x| x.get_by("_0")) {
Some(r) => r,
None => return Err(IdeviceError::UnexpectedResponse),
};
if let Some(data) = response
.get_by("pairingData")
.and_then(|x| x.get_by("_0"))
.and_then(|x| x.get_by("data"))
{
Ok(data.to_owned())
} else if let Some(err) = response.get_by("pairingRejectedWithError") {
let context = err
.get_by("wrappedError")
.and_then(|x| x.get_by("userInfo"))
.and_then(|x| x.get_by("NSLocalizedDescription"))
.and_then(|x| x.as_string())
.map(|x| x.to_string());
Err(IdeviceError::PairingRejected(context.unwrap_or_default()))
} else {
Err(IdeviceError::UnexpectedResponse)
}
}
}
impl<R: RpPairingSocketProvider> std::fmt::Debug for RemotePairingClient<'_, R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RemotePairingClient")
.field("inner", &self.inner)
.field("sequence_number", &self.sequence_number)
.field("pairing_file", &self.pairing_file)
.field("sending_host", &self.sending_host)
.finish()
}
}

View File

@@ -0,0 +1,165 @@
// Jackson Coxson
use plist::Value;
pub fn plist_to_opack(value: &Value) -> Vec<u8> {
let mut buf = Vec::new();
plist_to_opack_inner(value, &mut buf);
buf
}
fn plist_to_opack_inner(node: &Value, buf: &mut Vec<u8>) {
match node {
Value::Dictionary(dict) => {
let count = dict.len() as u32;
let blen = if count < 15 {
(count as u8).wrapping_sub(32)
} else {
0xEF
};
buf.push(blen);
for (key, val) in dict {
plist_to_opack_inner(&Value::String(key.clone()), buf);
plist_to_opack_inner(val, buf);
}
if count > 14 {
buf.push(0x03);
}
}
Value::Array(array) => {
let count = array.len() as u32;
let blen = if count < 15 {
(count as u8).wrapping_sub(48)
} else {
0xDF
};
buf.push(blen);
for val in array {
plist_to_opack_inner(val, buf);
}
if count > 14 {
buf.push(0x03); // Terminator
}
}
Value::Boolean(b) => {
let bval = if *b { 1u8 } else { 2u8 };
buf.push(bval);
}
Value::Integer(integer) => {
let u64val = integer.as_unsigned().unwrap_or(0);
if u64val <= u8::MAX as u64 {
let u8val = u64val as u8;
if u8val > 0x27 {
buf.push(0x30);
buf.push(u8val);
} else {
buf.push(u8val + 8);
}
} else if u64val <= u32::MAX as u64 {
buf.push(0x32);
buf.extend_from_slice(&(u64val as u32).to_le_bytes());
} else {
buf.push(0x33);
buf.extend_from_slice(&u64val.to_le_bytes());
}
}
Value::Real(real) => {
let dval = *real;
let fval = dval as f32;
if fval as f64 == dval {
buf.push(0x35);
buf.extend_from_slice(&fval.to_bits().swap_bytes().to_ne_bytes());
} else {
buf.push(0x36);
buf.extend_from_slice(&dval.to_bits().swap_bytes().to_ne_bytes());
}
}
Value::String(s) => {
let bytes = s.as_bytes();
let len = bytes.len();
if len > 0x20 {
if len <= 0xFF {
buf.push(0x61);
buf.push(len as u8);
} else if len <= 0xFFFF {
buf.push(0x62);
buf.extend_from_slice(&(len as u16).to_le_bytes());
} else if len <= 0xFFFFFFFF {
buf.push(0x63);
buf.extend_from_slice(&(len as u32).to_le_bytes());
} else {
buf.push(0x64);
buf.extend_from_slice(&(len as u64).to_le_bytes());
}
} else {
buf.push(0x40 + len as u8);
}
buf.extend_from_slice(bytes);
}
Value::Data(data) => {
let len = data.len();
if len > 0x20 {
if len <= 0xFF {
buf.push(0x91);
buf.push(len as u8);
} else if len <= 0xFFFF {
buf.push(0x92);
buf.extend_from_slice(&(len as u16).to_le_bytes());
} else if len <= 0xFFFFFFFF {
buf.push(0x93);
buf.extend_from_slice(&(len as u32).to_le_bytes());
} else {
buf.push(0x94);
buf.extend_from_slice(&(len as u64).to_le_bytes());
}
} else {
buf.push(0x70 + len as u8);
}
buf.extend_from_slice(data);
}
_ => {}
}
}
#[cfg(test)]
mod tests {
#[test]
fn t1() {
let v = crate::plist!({
"altIRK": b"\xe9\xe8-\xc0jIykVoT\x00\x19\xb1\xc7{".to_vec(),
"btAddr": "11:22:33:44:55:66",
"mac": b"\x11\x22\x33\x44\x55\x66".to_vec(),
"remotepairing_serial_number": "AAAAAAAAAAAA",
"accountID": "lolsssss",
"model": "computer-model",
"name": "reeeee",
});
let res = super::plist_to_opack(&v);
let expected = [
0xe7, 0x46, 0x61, 0x6c, 0x74, 0x49, 0x52, 0x4b, 0x80, 0xe9, 0xe8, 0x2d, 0xc0, 0x6a,
0x49, 0x79, 0x6b, 0x56, 0x6f, 0x54, 0x00, 0x19, 0xb1, 0xc7, 0x7b, 0x46, 0x62, 0x74,
0x41, 0x64, 0x64, 0x72, 0x51, 0x31, 0x31, 0x3a, 0x32, 0x32, 0x3a, 0x33, 0x33, 0x3a,
0x34, 0x34, 0x3a, 0x35, 0x35, 0x3a, 0x36, 0x36, 0x43, 0x6d, 0x61, 0x63, 0x76, 0x11,
0x22, 0x33, 0x44, 0x55, 0x66, 0x5b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x70, 0x61,
0x69, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x5f, 0x6e,
0x75, 0x6d, 0x62, 0x65, 0x72, 0x4c, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x49, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x44,
0x48, 0x6c, 0x6f, 0x6c, 0x73, 0x73, 0x73, 0x73, 0x73, 0x45, 0x6d, 0x6f, 0x64, 0x65,
0x6c, 0x4e, 0x63, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x2d, 0x6d, 0x6f, 0x64,
0x65, 0x6c, 0x44, 0x6e, 0x61, 0x6d, 0x65, 0x46, 0x72, 0x65, 0x65, 0x65, 0x65, 0x65,
];
println!("{res:02X?}");
assert_eq!(res, expected);
}
}

View File

@@ -0,0 +1,114 @@
// Jackson Coxson
use std::path::Path;
use ed25519_dalek::{SigningKey, VerifyingKey};
use plist::Dictionary;
use plist_macro::plist_to_xml_bytes;
use rsa::rand_core::OsRng;
use serde::de::Error;
use tracing::{debug, warn};
use crate::IdeviceError;
#[derive(Clone)]
pub struct RpPairingFile {
pub(crate) e_private_key: SigningKey,
pub(crate) e_public_key: VerifyingKey,
pub(crate) identifier: String,
}
impl RpPairingFile {
pub fn generate(sending_host: &str) -> Self {
// Ed25519 private key (persistent signing key)
let ed25519_private_key = SigningKey::generate(&mut OsRng);
let ed25519_public_key = VerifyingKey::from(&ed25519_private_key);
let identifier =
uuid::Uuid::new_v3(&uuid::Uuid::NAMESPACE_DNS, sending_host.as_bytes()).to_string();
Self {
e_private_key: ed25519_private_key,
e_public_key: ed25519_public_key,
identifier,
}
}
pub(crate) fn recreate_signing_keys(&mut self) {
let ed25519_private_key = SigningKey::generate(&mut OsRng);
let ed25519_public_key = VerifyingKey::from(&ed25519_private_key);
self.e_public_key = ed25519_public_key;
self.e_private_key = ed25519_private_key;
}
pub async fn write_to_file(&self, path: impl AsRef<Path>) -> Result<(), IdeviceError> {
let v = crate::plist!(dict {
"public_key": self.e_public_key.to_bytes().to_vec(),
"private_key": self.e_private_key.to_bytes().to_vec(),
"identifier": self.identifier.as_str()
});
tokio::fs::write(path, plist_to_xml_bytes(&v)).await?;
Ok(())
}
pub async fn read_from_file(path: impl AsRef<Path>) -> Result<Self, IdeviceError> {
let s = tokio::fs::read_to_string(path).await?;
let mut p: Dictionary = plist::from_bytes(s.as_bytes())?;
debug!("Read dictionary for rppairingfile: {p:#?}");
let public_key = match p
.remove("public_key")
.and_then(|x| x.into_data())
.filter(|x| x.len() == 32)
.and_then(|x| VerifyingKey::from_bytes(&x[..32].try_into().unwrap()).ok())
{
Some(p) => p,
None => {
warn!("plist did not contain valid public key bytes");
return Err(IdeviceError::Plist(plist::Error::missing_field(
"public_key",
)));
}
};
let private_key = match p
.remove("private_key")
.and_then(|x| x.into_data())
.filter(|x| x.len() == 32)
{
Some(p) => SigningKey::from_bytes(&p.try_into().unwrap()),
None => {
warn!("plist did not contain valid private key bytes");
return Err(IdeviceError::Plist(plist::Error::missing_field(
"private_key",
)));
}
};
let identifier = match p.remove("identifier").and_then(|x| x.into_string()) {
Some(i) => i,
None => {
warn!("plist did not contain identifier");
return Err(IdeviceError::Plist(plist::Error::missing_field(
"identifier",
)));
}
};
Ok(Self {
e_private_key: private_key,
e_public_key: public_key,
identifier,
})
}
}
impl std::fmt::Debug for RpPairingFile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RpPairingFile")
.field("e_public_key", &self.e_public_key)
.field("identifier", &self.identifier)
.finish()
}
}

View File

@@ -0,0 +1,173 @@
// Jackson Coxson
use base64::{Engine as _, engine::general_purpose::STANDARD as B64};
use plist_macro::{plist, pretty_print_plist};
use serde::Serialize;
use serde_json::json;
use std::{fmt::Debug, pin::Pin};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tracing::{debug, warn};
use crate::{
IdeviceError, ReadWrite, RemoteXpcClient, remote_pairing::RPPAIRING_MAGIC, xpc::XPCObject,
};
pub trait RpPairingSocketProvider: Debug {
fn send_plain(
&mut self,
value: impl Serialize,
seq: usize,
) -> Pin<Box<dyn Future<Output = Result<(), IdeviceError>> + Send + '_>>;
fn recv_plain<'a>(
&'a mut self,
) -> Pin<Box<dyn Future<Output = Result<plist::Value, IdeviceError>> + Send + 'a>>;
/// rppairing uses b64, while RemoteXPC uses raw bytes just fine
fn serialize_bytes(b: &[u8]) -> plist::Value;
fn deserialize_bytes(v: plist::Value) -> Option<Vec<u8>>;
}
#[derive(Debug)]
pub struct RpPairingSocket<R: ReadWrite> {
pub inner: R,
}
impl<R: ReadWrite> RpPairingSocket<R> {
pub fn new(socket: R) -> Self {
Self { inner: socket }
}
async fn send_rppairing(&mut self, value: impl Serialize) -> Result<(), IdeviceError> {
let value = serde_json::to_string(&value)?;
let x = value.as_bytes();
self.inner.write_all(RPPAIRING_MAGIC).await?;
self.inner
.write_all(&(x.len() as u16).to_be_bytes())
.await?;
self.inner.write_all(x).await?;
Ok(())
}
}
impl<R: ReadWrite> RpPairingSocketProvider for RpPairingSocket<R> {
fn send_plain(
&mut self,
value: impl Serialize,
seq: usize,
) -> Pin<Box<dyn Future<Output = Result<(), IdeviceError>> + Send + '_>> {
let v = json!({
"message": {"plain": {"_0": value}},
"originatedBy": "host",
"sequenceNumber": seq
});
Box::pin(async move {
self.send_rppairing(v).await?;
Ok(())
})
}
fn recv_plain<'a>(
&'a mut self,
) -> Pin<Box<dyn Future<Output = Result<plist::Value, IdeviceError>> + Send + 'a>> {
Box::pin(async move {
self.inner
.read_exact(&mut vec![0u8; RPPAIRING_MAGIC.len()])
.await?;
let mut packet_len_bytes = [0u8; 2];
self.inner.read_exact(&mut packet_len_bytes).await?;
let packet_len = u16::from_be_bytes(packet_len_bytes);
let mut value = vec![0u8; packet_len as usize];
self.inner.read_exact(&mut value).await?;
let value: serde_json::Value = serde_json::from_slice(&value)?;
let value = value
.get("message")
.and_then(|x| x.get("plain"))
.and_then(|x| x.get("_0"));
match value {
Some(v) => Ok(plist::to_value(v).unwrap()),
None => Err(IdeviceError::UnexpectedResponse),
}
})
}
fn serialize_bytes(b: &[u8]) -> plist::Value {
plist!(B64.encode(b))
}
fn deserialize_bytes(v: plist::Value) -> Option<Vec<u8>> {
if let plist::Value::String(v) = v {
B64.decode(v).ok()
} else {
None
}
}
}
impl<R: ReadWrite> RpPairingSocketProvider for RemoteXpcClient<R> {
fn send_plain(
&mut self,
value: impl Serialize,
seq: usize,
) -> Pin<Box<dyn Future<Output = Result<(), IdeviceError>> + Send + '_>> {
let value: plist::Value = plist::to_value(&value).expect("plist assert failed");
let value: XPCObject = value.into();
let v = crate::xpc!({
"mangledTypeName": "RemotePairing.ControlChannelMessageEnvelope",
"value": {
"message": {"plain": {"_0": value}},
"originatedBy": "host",
"sequenceNumber": seq as u64
}
});
debug!("Sending XPC: {v:#?}");
Box::pin(async move {
self.send_object(v, true).await?;
Ok(())
})
}
fn recv_plain<'a>(
&'a mut self,
) -> Pin<Box<dyn Future<Output = Result<plist::Value, IdeviceError>> + Send + 'a>> {
Box::pin(async move {
let msg = self.recv_root().await.unwrap();
debug!("Received RemoteXPC {}", pretty_print_plist(&msg));
let value = msg
.into_dictionary()
.and_then(|mut x| x.remove("value"))
.and_then(|x| x.into_dictionary())
.and_then(|mut x| x.remove("message"))
.and_then(|x| x.into_dictionary())
.and_then(|mut x| x.remove("plain"))
.and_then(|x| x.into_dictionary())
.and_then(|mut x| x.remove("_0"));
match value {
Some(v) => Ok(v),
None => Err(IdeviceError::UnexpectedResponse),
}
})
}
fn serialize_bytes(b: &[u8]) -> plist::Value {
plist::Value::Data(b.to_owned())
}
fn deserialize_bytes(v: plist::Value) -> Option<Vec<u8>> {
if let plist::Value::Data(v) = v {
Some(v)
} else {
warn!("Non-data passed to rppairingsocket::deserialize_bytes for RemoteXPC provider");
None
}
}
}

View File

@@ -0,0 +1,123 @@
// Jackson Coxson
use crate::IdeviceError;
// from pym3
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum PairingDataComponentType {
Method = 0x00,
Identifier = 0x01,
Salt = 0x02,
PublicKey = 0x03,
Proof = 0x04,
EncryptedData = 0x05,
State = 0x06,
ErrorResponse = 0x07,
RetryDelay = 0x08,
Certificate = 0x09,
Signature = 0x0a,
Permissions = 0x0b,
FragmentData = 0x0c,
FragmentLast = 0x0d,
SessionId = 0x0e,
Ttl = 0x0f,
ExtraData = 0x10,
Info = 0x11,
Acl = 0x12,
Flags = 0x13,
ValidationData = 0x14,
MfiAuthToken = 0x15,
MfiProductType = 0x16,
SerialNumber = 0x17,
MfiAuthTokenUuid = 0x18,
AppFlags = 0x19,
OwnershipProof = 0x1a,
SetupCodeType = 0x1b,
ProductionData = 0x1c,
AppInfo = 0x1d,
Separator = 0xff,
}
#[derive(Debug, Clone)]
pub struct TLV8Entry {
pub tlv_type: PairingDataComponentType,
pub data: Vec<u8>,
}
pub fn serialize_tlv8(entries: &[TLV8Entry]) -> Vec<u8> {
let mut out = Vec::new();
for entry in entries {
out.push(entry.tlv_type as u8);
out.push(entry.data.len() as u8);
out.extend(&entry.data);
}
out
}
pub fn deserialize_tlv8(input: &[u8]) -> Result<Vec<TLV8Entry>, IdeviceError> {
let mut index = 0;
let mut result = Vec::new();
while index + 2 <= input.len() {
let type_byte = input[index];
let length = input[index + 1] as usize;
index += 2;
if index + length > input.len() {
return Err(IdeviceError::MalformedTlv);
}
let data = input[index..index + length].to_vec();
index += length;
let tlv_type = PairingDataComponentType::try_from(type_byte)
.map_err(|_| IdeviceError::UnknownTlv(type_byte))?;
result.push(TLV8Entry { tlv_type, data });
}
Ok(result)
}
impl TryFrom<u8> for PairingDataComponentType {
type Error = u8;
fn try_from(value: u8) -> Result<Self, Self::Error> {
use PairingDataComponentType::*;
Ok(match value {
0x00 => Method,
0x01 => Identifier,
0x02 => Salt,
0x03 => PublicKey,
0x04 => Proof,
0x05 => EncryptedData,
0x06 => State,
0x07 => ErrorResponse,
0x08 => RetryDelay,
0x09 => Certificate,
0x0a => Signature,
0x0b => Permissions,
0x0c => FragmentData,
0x0d => FragmentLast,
0x0e => SessionId,
0x0f => Ttl,
0x10 => ExtraData,
0x11 => Info,
0x12 => Acl,
0x13 => Flags,
0x14 => ValidationData,
0x15 => MfiAuthToken,
0x16 => MfiProductType,
0x17 => SerialNumber,
0x18 => MfiAuthTokenUuid,
0x19 => AppFlags,
0x1a => OwnershipProof,
0x1b => SetupCodeType,
0x1c => ProductionData,
0x1d => AppInfo,
0xff => Separator,
other => return Err(other),
})
}
}

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

@@ -325,4 +325,31 @@ impl SpringBoardServicesClient {
Ok(orientation) 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

@@ -53,10 +53,6 @@ impl<R: ReadWrite> RemoteXpcClient<R> {
)) ))
.await?; .await?;
debug!("Sending weird flags");
self.send_root(XPCMessage::new(Some(XPCFlag::Custom(0x201)), None, None))
.await?;
debug!("Opening reply stream"); debug!("Opening reply stream");
self.h2_client.open_stream(REPLY_CHANNEL).await?; self.h2_client.open_stream(REPLY_CHANNEL).await?;
self.send_reply(XPCMessage::new( self.send_reply(XPCMessage::new(
@@ -66,6 +62,10 @@ impl<R: ReadWrite> RemoteXpcClient<R> {
)) ))
.await?; .await?;
debug!("Sending weird flags");
self.send_root(XPCMessage::new(Some(XPCFlag::Custom(0x201)), None, None))
.await?;
Ok(()) Ok(())
} }

View File

@@ -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,6 +51,7 @@ 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
@@ -68,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

@@ -10,6 +10,14 @@ repository = "https://github.com/jkcoxson/idevice"
keywords = ["lockdownd", "ios"] keywords = ["lockdownd", "ios"]
default-run = "idevice-tools" default-run = "idevice-tools"
[[bin]]
name = "pair_apple_tv"
path = "src/pair_apple_tv.rs"
[[bin]]
name = "pair_rsd_ios"
path = "src/pair_rsd_ios.rs"
# [[bin]] # [[bin]]
# name = "core_device_proxy_tun" # name = "core_device_proxy_tun"
# path = "src/core_device_proxy_tun.rs" # path = "src/core_device_proxy_tun.rs"
@@ -37,6 +45,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" }
zeroconf = { version = "0.17" }
[features] [features]
default = ["aws-lc"] default = ["aws-lc"]

View File

@@ -33,6 +33,7 @@ mod lockdown;
mod misagent; mod misagent;
mod mobilebackup2; mod mobilebackup2;
mod mounter; mod mounter;
mod notification_proxy_client;
mod notifications; mod notifications;
mod os_trace_relay; mod os_trace_relay;
mod pair; mod pair;
@@ -113,6 +114,7 @@ async fn main() {
.with_subcommand("mobilebackup2", mobilebackup2::register()) .with_subcommand("mobilebackup2", mobilebackup2::register())
.with_subcommand("mounter", mounter::register()) .with_subcommand("mounter", mounter::register())
.with_subcommand("notifications", notifications::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("os_trace_relay", os_trace_relay::register())
.with_subcommand("pair", pair::register()) .with_subcommand("pair", pair::register())
.with_subcommand("pcapd", pcapd::register()) .with_subcommand("pcapd", pcapd::register())
@@ -214,6 +216,9 @@ async fn main() {
"notifications" => { "notifications" => {
notifications::main(sub_args, provider).await; notifications::main(sub_args, provider).await;
} }
"notification_proxy" => {
notification_proxy_client::main(sub_args, provider).await;
}
"os_trace_relay" => { "os_trace_relay" => {
os_trace_relay::main(sub_args, provider).await; os_trace_relay::main(sub_args, provider).await;
} }

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

@@ -0,0 +1,92 @@
// Jackson Coxson
// A PoC to pair by IP
// Ideally you'd browse by mDNS in production
use std::{io::Write, net::IpAddr, str::FromStr};
use clap::{Arg, Command};
use idevice::remote_pairing::{RemotePairingClient, RpPairingFile, RpPairingSocket};
#[tokio::main]
async fn main() {
// tracing_subscriber::fmt::init();
let matches = Command::new("pair")
.about("Pair with the device")
.arg(
Arg::new("ip")
.value_name("IP")
.help("The IP of the Apple TV")
.required(true)
.index(1),
)
.arg(
Arg::new("port")
.value_name("port")
.help("The port of the Apple TV")
.required(true)
.index(2),
)
.arg(
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.get_matches();
if matches.get_flag("about") {
println!("pair - pair with the Apple TV");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let ip = matches.get_one::<String>("ip").expect("no IP passed");
let port = matches.get_one::<String>("port").expect("no port passed");
let port = port.parse::<u16>().unwrap();
let conn =
tokio::net::TcpStream::connect((IpAddr::from_str(ip).expect("failed to parse IP"), port))
.await
.expect("Failed to connect");
let conn = RpPairingSocket::new(conn);
let host = "idevice-rs-jkcoxson";
let mut rpf = RpPairingFile::generate(host);
let mut rpc = RemotePairingClient::new(conn, host, &mut rpf);
rpc.connect(
async |_| {
let mut buf = String::new();
print!("Enter the Apple TV pin: ");
std::io::stdout().flush().unwrap();
std::io::stdin()
.read_line(&mut buf)
.expect("Failed to read line");
buf.trim_end().to_string()
},
0u8, // we need no state, so pass a single byte that will hopefully get optimized out
)
.await
.expect("no pair");
// now that we are paired, we should be good
println!("Reconnecting...");
let conn =
tokio::net::TcpStream::connect((IpAddr::from_str(ip).expect("failed to parse IP"), port))
.await
.expect("Failed to connect");
let conn = RpPairingSocket::new(conn);
let mut rpc = RemotePairingClient::new(conn, host, &mut rpf);
rpc.connect(
async |_| {
panic!("we tried to pair again :(");
},
0u8,
)
.await
.expect("no reconnect");
rpf.write_to_file("atv_pairing_file.plist").await.unwrap();
println!("Pairing file validated and written to disk. Have a nice day.");
}

235
tools/src/pair_rsd_ios.rs Normal file
View File

@@ -0,0 +1,235 @@
// let ip = Ipv6Addr::new(0xfe80, 0, 0, 0, 0x282a, 0x9aff, 0xfedb, 0x8cbb);
// let addr = SocketAddrV6::new(ip, 60461, 0, 28);
// let conn = tokio::net::TcpStream::connect(addr).await.unwrap();
// Jackson Coxson
use std::{
any::Any,
net::{IpAddr, SocketAddr, SocketAddrV6},
sync::Arc,
time::Duration,
};
#[cfg(target_os = "linux")]
use std::{fs, process::Command as OsCommand};
use clap::{Arg, Command};
use idevice::{
RemoteXpcClient,
remote_pairing::{RemotePairingClient, RpPairingFile},
rsd::RsdHandshake,
};
use tokio::net::TcpStream;
use zeroconf::{
BrowserEvent, MdnsBrowser, ServiceType,
prelude::{TEventLoop, TMdnsBrowser},
};
const SERVICE_NAME: &str = "remoted";
const SERVICE_PROTOCOL: &str = "tcp";
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let matches = Command::new("pair")
.about("Pair with the device")
.arg(
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.get_matches();
if matches.get_flag("about") {
println!("pair - pair with the device");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let mut browser = MdnsBrowser::new(
ServiceType::new(SERVICE_NAME, SERVICE_PROTOCOL).expect("Unable to start mDNS browse"),
);
browser.set_service_callback(Box::new(on_service_discovered));
let event_loop = browser.browse_services().unwrap();
loop {
// calling `poll()` will keep this browser alive
event_loop.poll(Duration::from_secs(0)).unwrap();
}
}
fn on_service_discovered(
result: zeroconf::Result<BrowserEvent>,
_context: Option<Arc<dyn Any + Send + Sync>>,
) {
if let Ok(BrowserEvent::Add(result)) = result {
tokio::task::spawn(async move {
println!("Found iOS device to pair with!! - {result:?}");
let host_name = result.host_name().to_string();
let service_address = result.address().to_string();
let scope_id = link_local_scope_id_from_avahi(&host_name, &service_address);
let stream = match connect_to_service_port(
&host_name,
&service_address,
scope_id,
*result.port(),
)
.await
{
Some(s) => s,
None => {
println!("Couldn't open TCP port on device");
return;
}
};
let handshake = RsdHandshake::new(stream).await.expect("no rsd");
println!("handshake: {handshake:#?}");
let ts = handshake
.services
.get("com.apple.internal.dt.coredevice.untrusted.tunnelservice")
.unwrap();
println!("connecting to tunnel service");
let stream = connect_to_service_port(&host_name, &service_address, scope_id, ts.port)
.await
.expect("failed to connect to tunnselservice");
let mut conn = RemoteXpcClient::new(stream).await.unwrap();
println!("doing tunnel service handshake");
conn.do_handshake().await.unwrap();
let msg = conn.recv_root().await.unwrap();
println!("{msg:#?}");
let host = "idevice-rs-jkcoxson";
let mut rpf = RpPairingFile::generate(host);
let mut rpc = RemotePairingClient::new(conn, host, &mut rpf);
rpc.connect(
async |_| "000000".to_string(),
0u8, // we need no state, so pass a single byte that will hopefully get optimized out
)
.await
.expect("no pair");
rpf.write_to_file("ios_pairing_file.plist").await.unwrap();
println!(
"congrats you're paired now, the rppairing record has been saved. Have a nice day."
);
});
}
}
async fn connect_to_service_port(
host_name: &str,
service_address: &str,
scope_id: Option<u32>,
port: u16,
) -> Option<TcpStream> {
if let Some(stream) = lookup_host_and_connect(host_name, port).await {
return Some(stream);
}
let addr: IpAddr = match service_address.parse() {
Ok(addr) => addr,
Err(e) => {
println!("failed to parse resolved service address {service_address}: {e}");
return None;
}
};
let socket = match addr {
IpAddr::V6(v6) if v6.is_unicast_link_local() => {
SocketAddr::V6(SocketAddrV6::new(v6, port, 0, scope_id.unwrap_or(0)))
}
_ => SocketAddr::new(addr, port),
};
println!("using resolved service address fallback: {socket}");
match TcpStream::connect(socket).await {
Ok(s) => {
println!("connected with local addr {:?}", s.local_addr());
Some(s)
}
Err(e) => {
println!("failed to connect with service address fallback: {e:?}");
None
}
}
}
async fn lookup_host_and_connect(host: &str, port: u16) -> Option<TcpStream> {
let looked_up = match tokio::net::lookup_host((host, port)).await {
Ok(addrs) => addrs,
Err(e) => {
println!("hostname lookup failed for {host}:{port}: {e}");
return None;
}
};
let mut stream = None;
for l in looked_up {
if l.is_ipv4() {
continue;
}
println!("Found IP: {l:?}");
match tokio::net::TcpStream::connect(l).await {
Ok(s) => {
println!("connected with local addr {:?}", s.local_addr());
stream = Some(s);
break;
}
Err(e) => println!("failed to connect: {e:?}"),
}
}
stream
}
#[cfg(target_os = "linux")]
fn link_local_scope_id_from_avahi(host_name: &str, service_address: &str) -> Option<u32> {
let output = OsCommand::new("avahi-browse")
.args(["-rpt", "_remoted._tcp"])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if !line.starts_with("=;") {
continue;
}
let parts: Vec<&str> = line.split(';').collect();
if parts.len() < 9 {
continue;
}
let ifname = parts[1];
let resolved_host = parts[6];
let resolved_addr = parts[7];
if resolved_host == host_name && resolved_addr == service_address {
let ifindex_path = format!("/sys/class/net/{ifname}/ifindex");
return fs::read_to_string(ifindex_path).ok()?.trim().parse().ok();
}
}
None
}
#[cfg(not(target_os = "linux"))]
fn link_local_scope_id_from_avahi(_host_name: &str, _service_address: &str) -> Option<u32> {
None
}

View File

@@ -49,6 +49,25 @@ pub fn register() -> JkCommand {
"get_interface_orientation", "get_interface_orientation",
JkCommand::new().help("Gets the device's current screen 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) .subcommand_required(true)
} }
@@ -113,6 +132,30 @@ pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvi
.expect("Failed to get interface orientation"); .expect("Failed to get interface orientation");
println!("{:?}", 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!(), _ => unreachable!(),
} }
} }