mirror of
https://github.com/jkcoxson/idevice.git
synced 2026-03-02 06:26:15 +01:00
Compare commits
14 Commits
v0.1.53
...
rppairing-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e79d1c0498 | ||
|
|
0944708a8f | ||
|
|
7077e70e24 | ||
|
|
7782df8bd9 | ||
|
|
93d2f1b28c | ||
|
|
b459eebe9d | ||
|
|
c246362f54 | ||
|
|
bfe44e16e4 | ||
|
|
54439b85dd | ||
|
|
f8c5010b34 | ||
|
|
637758ad7f | ||
|
|
f5be1a000a | ||
|
|
d6e7b9aef4 | ||
|
|
4bea784260 |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -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
1264
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
25
README.md
25
README.md
@@ -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!
|
||||||
|
|||||||
46
cpp/include/idevice++/notification_proxy.hpp
Normal file
46
cpp/include/idevice++/notification_proxy.hpp
Normal 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
|
||||||
82
cpp/src/notification_proxy.cpp
Normal file
82
cpp/src/notification_proxy.cpp
Normal 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
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
311
ffi/src/notification_proxy.rs
Normal file
311
ffi/src/notification_proxy.rs
Normal 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) };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
720
idevice/src/remote_pairing/mod.rs
Normal file
720
idevice/src/remote_pairing/mod.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
165
idevice/src/remote_pairing/opack.rs
Normal file
165
idevice/src/remote_pairing/opack.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
114
idevice/src/remote_pairing/rp_pairing_file.rs
Normal file
114
idevice/src/remote_pairing/rp_pairing_file.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
173
idevice/src/remote_pairing/socket.rs
Normal file
173
idevice/src/remote_pairing/socket.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
123
idevice/src/remote_pairing/tlv.rs
Normal file
123
idevice/src/remote_pairing/tlv.rs
Normal 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),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")]
|
||||||
|
|||||||
212
idevice/src/services/notification_proxy.rs
Normal file
212
idevice/src/services/notification_proxy.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
17
justfile
17
justfile
@@ -40,6 +40,9 @@ xcframework: apple-build
|
|||||||
lipo -create -output swift/libs/idevice-ios-sim.a \
|
lipo -create -output swift/libs/idevice-ios-sim.a \
|
||||||
target/aarch64-apple-ios-sim/release/libidevice_ffi.a \
|
target/aarch64-apple-ios-sim/release/libidevice_ffi.a \
|
||||||
target/x86_64-apple-ios/release/libidevice_ffi.a
|
target/x86_64-apple-ios/release/libidevice_ffi.a
|
||||||
|
lipo -create -output swift/libs/idevice-maccatalyst.a \
|
||||||
|
target/aarch64-apple-ios-macabi/release/libidevice_ffi.a \
|
||||||
|
target/x86_64-apple-ios-macabi/release/libidevice_ffi.a
|
||||||
lipo -create -output swift/libs/idevice-macos.a \
|
lipo -create -output swift/libs/idevice-macos.a \
|
||||||
target/aarch64-apple-darwin/release/libidevice_ffi.a \
|
target/aarch64-apple-darwin/release/libidevice_ffi.a \
|
||||||
target/x86_64-apple-darwin/release/libidevice_ffi.a
|
target/x86_64-apple-darwin/release/libidevice_ffi.a
|
||||||
@@ -48,8 +51,9 @@ xcframework: apple-build
|
|||||||
-library target/aarch64-apple-ios/release/libidevice_ffi.a -headers swift/include \
|
-library target/aarch64-apple-ios/release/libidevice_ffi.a -headers swift/include \
|
||||||
-library swift/libs/idevice-ios-sim.a -headers swift/include \
|
-library swift/libs/idevice-ios-sim.a -headers swift/include \
|
||||||
-library swift/libs/idevice-macos.a -headers swift/include \
|
-library swift/libs/idevice-macos.a -headers swift/include \
|
||||||
|
-library swift/libs/idevice-maccatalyst.a -headers swift/include \
|
||||||
-output swift/IDevice.xcframework
|
-output swift/IDevice.xcframework
|
||||||
|
|
||||||
zip -r swift/bundle.zip swift/IDevice.xcframework
|
zip -r swift/bundle.zip swift/IDevice.xcframework
|
||||||
openssl dgst -sha256 swift/bundle.zip
|
openssl dgst -sha256 swift/bundle.zip
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
85
tools/src/notification_proxy_client.rs
Normal file
85
tools/src/notification_proxy_client.rs
Normal 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(¬ifications)
|
||||||
|
.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(¬ification)
|
||||||
|
.await
|
||||||
|
.expect("Failed to post notification");
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
92
tools/src/pair_apple_tv.rs
Normal file
92
tools/src/pair_apple_tv.rs
Normal 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
235
tools/src/pair_rsd_ios.rs
Normal 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
|
||||||
|
}
|
||||||
@@ -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!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user