3 Commits

Author SHA1 Message Date
uncor3
c752ee92c5 Add lockdownd_pair to ffi (#75)
* Add lockdownd_pair to ffi

* fix missing param host_name
2026-02-22 19:47:02 -07:00
neo
76d847664b feat(crashreportcopymobile): ffi bindings (#71)
* feat(crashreportcopymobile): ffi bindings

* chore: update readme
2026-02-16 19:12:17 -07:00
Nicholas Sharp
1f7924b773 Add ApplicationVerificationFailed to list of known errors (#72)
* Add ApplicationVerificationFailed to list of known errors

* Add missing cfg directive
2026-02-16 19:11:54 -07:00
18 changed files with 861 additions and 2641 deletions

1248
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -76,6 +76,7 @@ To keep dependency bloat and compile time down, everything is contained in featu
| `tunneld` | Interface with [pymobiledevice3](https://github.com/doronz88/pymobiledevice3)'s tunneld. |
| `usbmuxd` | Connect using the usbmuxd daemon.|
| `xpc` | Access protected services via XPC over RSD. |
| `notification_proxy` | Post and observe iOS notifications. |
### Planned/TODO
@@ -85,7 +86,7 @@ Finish the following:
Implement the following:
- notification_proxy
- file_relay
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!

View File

@@ -0,0 +1,50 @@
// Jackson Coxson
#pragma once
#include <idevice++/bindings.hpp>
#include <idevice++/ffi.hpp>
#include <idevice++/provider.hpp>
#include <memory>
#include <string>
#include <vector>
namespace IdeviceFFI {
using CrashReportCopyMobilePtr =
std::unique_ptr<CrashReportCopyMobileHandle,
FnDeleter<CrashReportCopyMobileHandle, crash_report_client_free>>;
class CrashReportCopyMobile {
public:
// Factory: connect via Provider
static Result<CrashReportCopyMobile, FfiError> connect(Provider& provider);
// Factory: wrap an existing Idevice socket (consumes it on success)
static Result<CrashReportCopyMobile, FfiError> from_socket(Idevice&& socket);
// Static: flush crash reports from system storage
static Result<void, FfiError> flush(Provider& provider);
// Ops
Result<std::vector<std::string>, FfiError> ls(const char* dir_path = nullptr);
Result<std::vector<char>, FfiError> pull(const std::string& log_name);
Result<void, FfiError> remove(const std::string& log_name);
// RAII / moves
~CrashReportCopyMobile() noexcept = default;
CrashReportCopyMobile(CrashReportCopyMobile&&) noexcept = default;
CrashReportCopyMobile& operator=(CrashReportCopyMobile&&) noexcept = default;
CrashReportCopyMobile(const CrashReportCopyMobile&) = delete;
CrashReportCopyMobile& operator=(const CrashReportCopyMobile&) = delete;
CrashReportCopyMobileHandle* raw() const noexcept { return handle_.get(); }
static CrashReportCopyMobile adopt(CrashReportCopyMobileHandle* h) noexcept {
return CrashReportCopyMobile(h);
}
private:
explicit CrashReportCopyMobile(CrashReportCopyMobileHandle* h) noexcept : handle_(h) {}
CrashReportCopyMobilePtr handle_{};
};
} // namespace IdeviceFFI

View File

@@ -0,0 +1,94 @@
// Jackson Coxson
#include <cstring>
#include <idevice++/bindings.hpp>
#include <idevice++/crashreportcopymobile.hpp>
#include <idevice++/ffi.hpp>
#include <idevice++/provider.hpp>
namespace IdeviceFFI {
// -------- Factory Methods --------
Result<CrashReportCopyMobile, FfiError> CrashReportCopyMobile::connect(Provider& provider) {
CrashReportCopyMobileHandle* out = nullptr;
FfiError e(::crash_report_client_connect(provider.raw(), &out));
if (e) {
return Err(e);
}
return Ok(CrashReportCopyMobile::adopt(out));
}
Result<CrashReportCopyMobile, FfiError> CrashReportCopyMobile::from_socket(Idevice&& socket) {
CrashReportCopyMobileHandle* out = nullptr;
FfiError e(::crash_report_client_new(socket.raw(), &out));
if (e) {
return Err(e);
}
socket.release();
return Ok(CrashReportCopyMobile::adopt(out));
}
Result<void, FfiError> CrashReportCopyMobile::flush(Provider& provider) {
FfiError e(::crash_report_flush(provider.raw()));
if (e) {
return Err(e);
}
return Ok();
}
// -------- Ops --------
Result<std::vector<std::string>, FfiError>
CrashReportCopyMobile::ls(const char* dir_path) {
char** entries_raw = nullptr;
size_t count = 0;
FfiError e(::crash_report_client_ls(handle_.get(), dir_path, &entries_raw, &count));
if (e) {
return Err(e);
}
std::vector<std::string> result;
if (entries_raw) {
result.reserve(count);
for (size_t i = 0; i < count; ++i) {
if (entries_raw[i]) {
result.emplace_back(entries_raw[i]);
::idevice_string_free(entries_raw[i]);
}
}
std::free(entries_raw);
}
return Ok(std::move(result));
}
Result<std::vector<char>, FfiError>
CrashReportCopyMobile::pull(const std::string& log_name) {
uint8_t* data = nullptr;
size_t length = 0;
FfiError e(::crash_report_client_pull(handle_.get(), log_name.c_str(), &data, &length));
if (e) {
return Err(e);
}
std::vector<char> result;
if (data && length > 0) {
result.assign(reinterpret_cast<char*>(data), reinterpret_cast<char*>(data) + length);
::idevice_data_free(data, length);
}
return Ok(std::move(result));
}
Result<void, FfiError> CrashReportCopyMobile::remove(const std::string& log_name) {
FfiError e(::crash_report_client_remove(handle_.get(), log_name.c_str()));
if (e) {
return Err(e);
}
return Ok();
}
} // namespace IdeviceFFI

View File

@@ -0,0 +1,325 @@
// Jackson Coxson
use std::{
ffi::{CStr, c_char},
ptr::null_mut,
};
use idevice::{
IdeviceError, IdeviceService,
provider::IdeviceProvider,
services::crashreportcopymobile::{CrashReportCopyMobileClient, flush_reports},
};
use crate::{
IdeviceFfiError, IdeviceHandle, afc::AfcClientHandle, ffi_err, provider::IdeviceProviderHandle,
run_sync_local,
};
pub struct CrashReportCopyMobileHandle(pub CrashReportCopyMobileClient);
/// Automatically creates and connects to the crash report copy mobile service,
/// returning a client handle
///
/// # Arguments
/// * [`provider`] - An IdeviceProvider
/// * [`client`] - On success, will be set to point to a newly allocated 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 crash_report_client_connect(
provider: *mut IdeviceProviderHandle,
client: *mut *mut CrashReportCopyMobileHandle,
) -> *mut IdeviceFfiError {
if provider.is_null() || client.is_null() {
tracing::error!("Null pointer provided");
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let res: Result<CrashReportCopyMobileClient, IdeviceError> = run_sync_local(async move {
let provider_ref: &dyn IdeviceProvider = unsafe { &*(*provider).0 };
CrashReportCopyMobileClient::connect(provider_ref).await
});
match res {
Ok(r) => {
let boxed = Box::new(CrashReportCopyMobileHandle(r));
unsafe { *client = Box::into_raw(boxed) };
null_mut()
}
Err(e) => ffi_err!(e),
}
}
/// Creates a new CrashReportCopyMobile client from an existing Idevice connection
///
/// # Arguments
/// * [`socket`] - An IdeviceSocket handle
/// * [`client`] - On success, will be set to point to a newly allocated 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 crash_report_client_new(
socket: *mut IdeviceHandle,
client: *mut *mut CrashReportCopyMobileHandle,
) -> *mut IdeviceFfiError {
if socket.is_null() || client.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let socket = unsafe { Box::from_raw(socket) }.0;
let r = CrashReportCopyMobileClient::new(socket);
let boxed = Box::new(CrashReportCopyMobileHandle(r));
unsafe { *client = Box::into_raw(boxed) };
null_mut()
}
/// Lists crash report files in the specified directory
///
/// # Arguments
/// * [`client`] - A valid CrashReportCopyMobile handle
/// * [`dir_path`] - Optional directory path (NULL for root "/")
/// * [`entries`] - Will be set to point to an array of C strings
/// * [`count`] - Will be set to the number of entries
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// All pointers must be valid and non-null
/// `dir_path` may be NULL (defaults to root)
/// Caller must free the returned array with `afc_free_directory_entries`
#[unsafe(no_mangle)]
pub unsafe extern "C" fn crash_report_client_ls(
client: *mut CrashReportCopyMobileHandle,
dir_path: *const c_char,
entries: *mut *mut *mut c_char,
count: *mut libc::size_t,
) -> *mut IdeviceFfiError {
if client.is_null() || entries.is_null() || count.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let path = if dir_path.is_null() {
None
} else {
match unsafe { CStr::from_ptr(dir_path) }.to_str() {
Ok(s) => Some(s),
Err(_) => return ffi_err!(IdeviceError::FfiInvalidString),
}
};
let res: Result<Vec<String>, IdeviceError> = run_sync_local(async {
let client_ref = unsafe { &mut (*client).0 };
client_ref.ls(path).await
});
match res {
Ok(items) => {
let c_strings = items
.into_iter()
.filter_map(|s| std::ffi::CString::new(s).ok())
.collect::<Vec<_>>();
let string_count = c_strings.len();
// Allocate array for char pointers (with NULL terminator)
let layout = std::alloc::Layout::array::<*mut c_char>(string_count + 1).unwrap();
let ptr = unsafe { std::alloc::alloc(layout) as *mut *mut c_char };
if ptr.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
for (i, cstring) in c_strings.into_iter().enumerate() {
let string_ptr = cstring.into_raw();
unsafe { *ptr.add(i) = string_ptr };
}
// NULL terminator
unsafe { *ptr.add(string_count) = std::ptr::null_mut() };
unsafe {
*entries = ptr;
*count = string_count;
}
null_mut()
}
Err(e) => ffi_err!(e),
}
}
/// Downloads a crash report file from the device
///
/// # Arguments
/// * [`client`] - A valid CrashReportCopyMobile handle
/// * [`log_name`] - Name of the log file to download (C string)
/// * [`data`] - Will be set to point to the file contents
/// * [`length`] - Will be set to the size of the data
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// All pointers must be valid and non-null
/// `log_name` must be a valid C string
/// Caller must free the returned data with `idevice_data_free`
#[unsafe(no_mangle)]
pub unsafe extern "C" fn crash_report_client_pull(
client: *mut CrashReportCopyMobileHandle,
log_name: *const c_char,
data: *mut *mut u8,
length: *mut libc::size_t,
) -> *mut IdeviceFfiError {
if client.is_null() || log_name.is_null() || data.is_null() || length.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let name = match unsafe { CStr::from_ptr(log_name) }.to_str() {
Ok(s) => s.to_string(),
Err(_) => return ffi_err!(IdeviceError::FfiInvalidString),
};
let res: Result<Vec<u8>, IdeviceError> = run_sync_local(async {
let client_ref = unsafe { &mut (*client).0 };
client_ref.pull(name).await
});
match res {
Ok(file_data) => {
let len = file_data.len();
let mut boxed = file_data.into_boxed_slice();
unsafe {
*data = boxed.as_mut_ptr();
*length = len;
}
std::mem::forget(boxed);
null_mut()
}
Err(e) => ffi_err!(e),
}
}
/// Removes a crash report file from the device
///
/// # Arguments
/// * [`client`] - A valid CrashReportCopyMobile handle
/// * [`log_name`] - Name of the log file to remove (C string)
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `client` must be a valid pointer to a handle allocated by this library
/// `log_name` must be a valid C string
#[unsafe(no_mangle)]
pub unsafe extern "C" fn crash_report_client_remove(
client: *mut CrashReportCopyMobileHandle,
log_name: *const c_char,
) -> *mut IdeviceFfiError {
if client.is_null() || log_name.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let name = match unsafe { CStr::from_ptr(log_name) }.to_str() {
Ok(s) => s.to_string(),
Err(_) => return ffi_err!(IdeviceError::FfiInvalidString),
};
let res = run_sync_local(async {
let client_ref = unsafe { &mut (*client).0 };
client_ref.remove(name).await
});
match res {
Ok(_) => null_mut(),
Err(e) => ffi_err!(e),
}
}
/// Converts this client to an AFC client for advanced file operations
///
/// # Arguments
/// * [`client`] - A valid CrashReportCopyMobile handle (will be consumed)
/// * [`afc_client`] - On success, will be set to an AFC client handle
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `client` must be a valid pointer (will be freed after this call)
/// `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 crash_report_client_to_afc(
client: *mut CrashReportCopyMobileHandle,
afc_client: *mut *mut AfcClientHandle,
) -> *mut IdeviceFfiError {
if client.is_null() || afc_client.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let crash_client = unsafe { Box::from_raw(client) }.0;
let afc = crash_client.to_afc_client();
let a = Box::into_raw(Box::new(AfcClientHandle(afc)));
unsafe { *afc_client = a };
null_mut()
}
/// Triggers a flush of crash logs from system storage
///
/// This connects to the crashreportmover service to move crash logs
/// into the AFC-accessible directory. Should be called before listing logs.
///
/// # Arguments
/// * [`provider`] - An IdeviceProvider
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `provider` must be a valid pointer to a handle allocated by this library
#[unsafe(no_mangle)]
pub unsafe extern "C" fn crash_report_flush(
provider: *mut IdeviceProviderHandle,
) -> *mut IdeviceFfiError {
if provider.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let res = run_sync_local(async {
let provider_ref: &dyn IdeviceProvider = unsafe { &*(*provider).0 };
flush_reports(provider_ref).await
});
match res {
Ok(_) => null_mut(),
Err(e) => ffi_err!(e),
}
}
/// Frees a CrashReportCopyMobile client 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 crash_report_client_free(handle: *mut CrashReportCopyMobileHandle) {
if !handle.is_null() {
tracing::debug!("Freeing crash_report_client");
let _ = unsafe { Box::from_raw(handle) };
}
}

View File

@@ -10,6 +10,8 @@ pub mod amfi;
pub mod core_device;
#[cfg(feature = "core_device_proxy")]
pub mod core_device_proxy;
#[cfg(feature = "crashreportcopymobile")]
pub mod crashreportcopymobile;
#[cfg(feature = "debug_proxy")]
pub mod debug_proxy;
#[cfg(feature = "diagnostics_relay")]

View File

@@ -157,6 +157,74 @@ pub unsafe extern "C" fn lockdownd_start_service(
}
}
/// Pairs with the device using lockdownd
///
/// # Arguments
/// * `client` - A valid LockdowndClient handle
/// * `host_id` - The host ID (null-terminated string)
/// * `system_buid` - The system BUID (null-terminated string)
/// * `pairing_file` - On success, will be set to point to a newly allocated IdevicePairingFile handle
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `client` must be a valid pointer to a handle allocated by this library
/// `host_id` must be a valid null-terminated string
/// `system_buid` must be a valid null-terminated string
/// `pairing_file` must be a valid, non-null pointer to a location where the handle will be stored
#[unsafe(no_mangle)]
pub unsafe extern "C" fn lockdownd_pair(
client: *mut LockdowndClientHandle,
host_id: *const libc::c_char,
system_buid: *const libc::c_char,
host_name: *const libc::c_char,
pairing_file: *mut *mut IdevicePairingFile,
) -> *mut IdeviceFfiError {
if client.is_null() || host_id.is_null() || system_buid.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let host_id = unsafe {
std::ffi::CStr::from_ptr(host_id)
.to_string_lossy()
.into_owned()
};
let system_buid = unsafe {
std::ffi::CStr::from_ptr(system_buid)
.to_string_lossy()
.into_owned()
};
let host_name = if host_name.is_null() {
None
} else {
Some(
match unsafe { std::ffi::CStr::from_ptr(host_name) }.to_str() {
Ok(v) => v,
Err(_) => {
return ffi_err!(IdeviceError::InvalidCString);
}
},
)
};
let res = run_sync_local(async move {
let client_ref = unsafe { &mut (*client).0 };
client_ref.pair(host_id, system_buid, host_name).await
});
match res {
Ok(pairing_file_res) => {
let boxed_pairing_file = Box::new(IdevicePairingFile(pairing_file_res));
unsafe { *pairing_file = Box::into_raw(boxed_pairing_file) };
null_mut()
}
Err(e) => ffi_err!(e),
}
}
/// Gets a value from lockdownd
///
/// # Arguments

View File

@@ -21,7 +21,7 @@ tokio-openssl = { version = "0.6", optional = true }
openssl = { version = "0.10", optional = true }
plist = { version = "1.8" }
plist-macro = { version = "0.1.6" }
plist-macro = { version = "0.1.3" }
serde = { version = "1", features = ["derive"] }
ns-keyed-archive = { version = "0.1.4", optional = true }
crossfire = { version = "2.1", optional = true }
@@ -31,7 +31,7 @@ tracing = { version = "0.1.41" }
base64 = { version = "0.22" }
indexmap = { version = "2.11", features = ["serde"], optional = true }
uuid = { version = "1.18", features = ["serde", "v3", "v4"], optional = true }
uuid = { version = "1.18", features = ["serde", "v4"], optional = true }
chrono = { version = "0.4", optional = true, default-features = false, features = [
"serde",
] }
@@ -55,11 +55,6 @@ x509-cert = { version = "0.2", optional = true, features = [
"builder",
"pem",
], 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 }
@@ -102,27 +97,12 @@ misagent = []
mobile_image_mounter = ["dep:sha2"]
mobileactivationd = ["dep:reqwest"]
mobilebackup2 = []
notification_proxy = [
"tokio/macros",
"tokio/time",
"dep:async-stream",
"dep:futures",
]
notification_proxy = ["tokio/macros", "tokio/time", "dep:async-stream", "dep:futures"]
location_simulation = []
pair = ["chrono/default", "tokio/time", "dep:sha2", "dep:rsa", "dep:x509-cert"]
pcapd = []
preboard_service = []
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 = []
rsd = ["xpc"]
screenshotr = []
@@ -163,7 +143,6 @@ full = [
"pair",
"pcapd",
"preboard_service",
"remote_pairing",
"restore_service",
"rsd",
"screenshotr",

View File

@@ -9,8 +9,6 @@ pub mod cursor;
mod obfuscation;
pub mod pairing_file;
pub mod provider;
#[cfg(feature = "remote_pairing")]
pub mod remote_pairing;
#[cfg(feature = "rustls")]
mod sni;
#[cfg(feature = "tunnel_tcp_stack")]
@@ -868,25 +866,13 @@ pub enum IdeviceError {
#[error("Developer mode is not enabled")]
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,
NotificationProxyDeath = -69,
#[cfg(feature = "installation_proxy")]
#[error("Application verification failed: {0}")]
ApplicationVerificationFailed(String) = -70,
}
impl IdeviceError {
@@ -930,6 +916,15 @@ impl IdeviceError {
Some(Self::InternalError(detailed_error))
}
}
#[cfg(feature = "installation_proxy")]
"ApplicationVerificationFailed" => {
let msg = context
.get("ErrorDescription")
.and_then(|x| x.as_string())
.unwrap_or("No context")
.to_string();
Some(Self::ApplicationVerificationFailed(msg))
}
_ => None,
}
}
@@ -1052,17 +1047,12 @@ impl IdeviceError {
#[cfg(feature = "installation_proxy")]
IdeviceError::MalformedPackageArchive(_) => -67,
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,
IdeviceError::NotificationProxyDeath => -69,
#[cfg(feature = "installation_proxy")]
IdeviceError::ApplicationVerificationFailed(_) => -70,
}
}
}

View File

@@ -1,720 +0,0 @@
//! 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

@@ -1,165 +0,0 @@
// 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

@@ -1,114 +0,0 @@
// 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

@@ -1,173 +0,0 @@
// 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

@@ -1,123 +0,0 @@
// 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

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

View File

@@ -10,14 +10,6 @@ repository = "https://github.com/jkcoxson/idevice"
keywords = ["lockdownd", "ios"]
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]]
# name = "core_device_proxy_tun"
# path = "src/core_device_proxy_tun.rs"
@@ -45,7 +37,6 @@ plist-macro = { version = "0.1.3" }
ns-keyed-archive = "0.1.2"
uuid = "1.16"
futures-util = { version = "0.3" }
zeroconf = { version = "0.17" }
[features]
default = ["aws-lc"]

View File

@@ -1,92 +0,0 @@
// 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.");
}

View File

@@ -1,235 +0,0 @@
// 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
}