Merge branch 'master' into rppairing

This commit is contained in:
Jackson Coxson
2025-10-02 09:07:56 -06:00
38 changed files with 2758 additions and 488 deletions

View File

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

72
Cargo.lock generated
View File

@@ -119,6 +119,19 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "async-compression"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddb939d66e4ae03cee6091612804ba446b12878410cfa17f785f4dd67d4014e8"
dependencies = [
"flate2",
"futures-core",
"futures-io",
"memchr",
"pin-project-lite",
]
[[package]] [[package]]
name = "async-stream" name = "async-stream"
version = "0.3.6" version = "0.3.6"
@@ -147,6 +160,21 @@ version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
name = "async_zip"
version = "0.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c50d65ce1b0e0cb65a785ff615f78860d7754290647d3b983208daa4f85e6"
dependencies = [
"async-compression",
"crc32fast",
"futures-lite",
"pin-project",
"thiserror 2.0.16",
"tokio",
"tokio-util",
]
[[package]] [[package]]
name = "atomic-waker" name = "atomic-waker"
version = "1.1.2" version = "1.1.2"
@@ -916,7 +944,10 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
dependencies = [ dependencies = [
"fastrand",
"futures-core", "futures-core",
"futures-io",
"parking",
"pin-project-lite", "pin-project-lite",
] ]
@@ -1255,9 +1286,10 @@ dependencies = [
[[package]] [[package]]
name = "idevice" name = "idevice"
version = "0.1.41" version = "0.1.42"
dependencies = [ dependencies = [
"async-stream", "async-stream",
"async_zip",
"base64", "base64",
"byteorder", "byteorder",
"bytes", "bytes",
@@ -1868,6 +1900,26 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pin-project"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.16" version = "0.2.16"
@@ -1927,9 +1979,9 @@ dependencies = [
[[package]] [[package]]
name = "plist_ffi" name = "plist_ffi"
version = "0.1.5" version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22a5ca928241bc2e8c5fd28b81772962389efdbfcb71dfc9ec694369e063cb3a" checksum = "35ed070b06d9f2fdd7e816ef784fb07b09672f2acf37527f810dbedf450b7769"
dependencies = [ dependencies = [
"cbindgen", "cbindgen",
"cc", "cc",
@@ -2713,6 +2765,20 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tokio-util"
version = "0.7.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5"
dependencies = [
"bytes",
"futures-core",
"futures-io",
"futures-sink",
"pin-project-lite",
"tokio",
]
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.8.23" version = "0.8.23"

View File

@@ -1,13 +1,16 @@
# idevice # idevice
A Rust library for interacting with iOS services. A pure Rust library for interacting with iOS services.
Inspired by [libimobiledevice](https://github.com/libimobiledevice/libimobiledevice) Inspired by [libimobiledevice](https://github.com/libimobiledevice/libimobiledevice)
and [pymobiledevice3](https://github.com/doronz88/pymobiledevice3), [pymobiledevice3](https://github.com/doronz88/pymobiledevice3),
this library interfaces with lockdownd and usbmuxd to perform actions and [go-ios](https://github.com/danielpaulus/go-ios)
this library interfaces with lockdownd, usbmuxd, and RSD to perform actions
on an iOS device that a Mac normally would. on an iOS device that a Mac normally would.
For help and information, join the [idevice Discord](https://discord.gg/qtgv6QtYbV) For help and information, join the [idevice Discord](https://discord.gg/qtgv6QtYbV)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/jkcoxson/idevice)
## State ## State
**IMPORTANT**: Breaking changes will happen at each point release until 0.2.0. **IMPORTANT**: Breaking changes will happen at each point release until 0.2.0.
@@ -17,6 +20,25 @@ This library is in development and research stage.
Releases are being published to crates.io for use in other projects, Releases are being published to crates.io for use in other projects,
but the API and feature-set are far from final or even planned. but the API and feature-set are far from final or even planned.
## Why use this?
libimobiledevice is a groundbreaking library. Unfortunately, it hasn't
been seriously updated in a long time, and does not support many modern
iOS features.
Libraries such as pymobiledevice3 and go-ios have popped up to fill that
gap, but both lacked the support I needed for embedding into applications
and server programs. Python requires an interpreter, and Go's current
ability to be embedded in other languages is lacking.
This library is currently used in popular apps such as
[StikDebug](https://github.com/StephenDev0/StikDebug),
[CrossCode](https://github.com/nab138/CrossCode)
and
[Protokolle](https://github.com/khcrysalis/Protokolle).
``idevice`` has proven there is a need. It's currently deployed on tens of
thousands of devices, all across the world.
## Features ## Features
To keep dependency bloat and compile time down, everything is contained in features. To keep dependency bloat and compile time down, everything is contained in features.
@@ -81,7 +103,7 @@ async fn main() {
// We'll ask usbmuxd for a device // We'll ask usbmuxd for a device
let mut usbmuxd = UsbmuxdConnection::default() let mut usbmuxd = UsbmuxdConnection::default()
.await .await
.expect("Unable to connect to usbmxud") .expect("Unable to connect to usbmuxd");
let devs = usbmuxd.get_devices().unwrap(); let devs = usbmuxd.get_devices().unwrap();
if devs.is_empty() { if devs.is_empty() {
eprintln!("No devices connected!"); eprintln!("No devices connected!");
@@ -118,23 +140,43 @@ async fn main() {
} }
``` ```
More examples are in the ``tools`` crate and in the crate documentation. More examples are in the [`tools`](tools/) crate and in the crate documentation.
## FFI ## FFI
For use in other languages, a small FFI crate has been created to start exposing For use in other languages, a small FFI crate has been created to start exposing
idevice. Example C programs can be found in this repository. idevice. Example C programs can be found in the [`ffi/examples`](ffi/examples/) directory.
## Version Policy ### C++
As Apple prohibits downgrading to older versions, this library will "Hey wait a second, there's a lot of C++ code in this library!!"
not keep compatibility for older versions than the current stable release. C++ bindings have been made for many of idevice's features. This allows smooth
and safer usage in C++ and Swift codebases.
## Developer Disk Images ## Technical Explanation
doronz88 is kind enough to maintain a [repo](https://github.com/doronz88/DeveloperDiskImage) There are so many layers and protocols in this library, many stacked on top of
for disk images and personalized images. one another. It's difficult to describe the magnitude that is Apple's interfaces.
On MacOS, you can find them at ``~/Library/Developer/DeveloperDiskImages``.
I would recommend reading the DeepWiki explanations and overviews to get an idea
of how this library and their associated protocols work. But a general overview is:
### Lockdown
1. A lockdown service is accessible via a port given by lockdown
1. Lockdown is accessible by USB or TCP via TLS
1. USB is accessible via usbmuxd
1. usbmuxd is accessed through a unix socket
1. That Unix socket has its own protocol
### RemoteXPC/RSD
1. An RSD service is discovered through a RemoteXPC handshake response
1. RemoteXPC is transferred over non-compliant HTTP/2
1. That HTTP/2 is accessed through an NCM USB interface or CoreDeviceProxy
1. CoreDeviceProxy is a lockdown service, see above
This doesn't even touch RPPairing, which is still a mystery as of writing.
## License ## License

230
cpp/examples/mounter.cpp Normal file
View File

@@ -0,0 +1,230 @@
#include <fstream>
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>
// Idevice++ library headers
#include <idevice++/lockdown.hpp>
#include <idevice++/mobile_image_mounter.hpp>
#include <idevice++/provider.hpp>
#include <idevice++/usbmuxd.hpp>
#include <plist/plist++.h>
// --- Helper Functions ---
/**
* @brief Reads an entire file into a byte vector.
* @param path The path to the file.
* @return A vector containing the file's data.
*/
std::vector<uint8_t> read_file(const std::string& path) {
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file) {
throw std::runtime_error("Failed to open file: " + path);
}
std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> buffer(size);
if (!file.read(reinterpret_cast<char*>(buffer.data()), size)) {
throw std::runtime_error("Failed to read file: " + path);
}
return buffer;
}
/**
* @brief Prints the command usage instructions.
*/
void print_usage(const char* prog_name) {
std::cerr << "Usage: " << prog_name << " [options] <subcommand>\n\n"
<< "A tool to manage developer images on a device.\n\n"
<< "Options:\n"
<< " --udid <UDID> Target a specific device by its UDID.\n\n"
<< "Subcommands:\n"
<< " list List mounted images.\n"
<< " unmount Unmount the developer image.\n"
<< " mount [mount_options] Mount a developer image.\n\n"
<< "Mount Options:\n"
<< " --image <path> (Required) Path to the DeveloperDiskImage.dmg.\n"
<< " --signature <path> (Required for iOS < 17) Path to the .signature file.\n"
<< " --manifest <path> (Required for iOS 17+) Path to the BuildManifest.plist.\n"
<< " --trustcache <path> (Required for iOS 17+) Path to the trust cache file.\n"
<< std::endl;
}
// --- Main Logic ---
int main(int argc, char** argv) {
idevice_init_logger(Debug, Disabled, NULL);
// --- 1. Argument Parsing ---
if (argc < 2) {
print_usage(argv[0]);
return 1;
}
std::string udid_arg;
std::string subcommand;
std::string image_path;
std::string signature_path;
std::string manifest_path;
std::string trustcache_path;
for (int i = 1; i < argc; ++i) {
std::string arg = argv[i];
if (arg == "--udid" && i + 1 < argc) {
udid_arg = argv[++i];
} else if (arg == "--image" && i + 1 < argc) {
image_path = argv[++i];
} else if (arg == "--signature" && i + 1 < argc) {
signature_path = argv[++i];
} else if (arg == "--manifest" && i + 1 < argc) {
manifest_path = argv[++i];
} else if (arg == "--trustcache" && i + 1 < argc) {
trustcache_path = argv[++i];
} else if (arg == "list" || arg == "mount" || arg == "unmount") {
subcommand = arg;
} else if (arg == "--help" || arg == "-h") {
print_usage(argv[0]);
return 0;
}
}
if (subcommand.empty()) {
std::cerr << "Error: No subcommand specified. Use 'list', 'mount', or 'unmount'."
<< std::endl;
print_usage(argv[0]);
return 1;
}
try {
// --- 2. Device Connection ---
auto u =
IdeviceFFI::UsbmuxdConnection::default_new(0).expect("Failed to connect to usbmuxd");
auto devices = u.get_devices().expect("Failed to get devices from usbmuxd");
if (devices.empty()) {
throw std::runtime_error("No devices connected.");
}
IdeviceFFI::UsbmuxdDevice* target_dev = nullptr;
if (!udid_arg.empty()) {
for (auto& dev : devices) {
if (dev.get_udid().unwrap_or("") == udid_arg) {
target_dev = &dev;
break;
}
}
if (!target_dev) {
throw std::runtime_error("Device with UDID " + udid_arg + " not found.");
}
} else {
target_dev = &devices[0]; // Default to the first device
}
auto udid = target_dev->get_udid().expect("Device has no UDID");
auto id = target_dev->get_id().expect("Device has no ID");
IdeviceFFI::UsbmuxdAddr addr = IdeviceFFI::UsbmuxdAddr::default_new();
auto prov = IdeviceFFI::Provider::usbmuxd_new(std::move(addr), 0, udid, id, "mounter-tool")
.expect("Failed to create provider");
// --- 3. Connect to Lockdown & Get iOS Version ---
auto lockdown_client =
IdeviceFFI::Lockdown::connect(prov).expect("Lockdown connect failed");
auto pairing_file = prov.get_pairing_file().expect("Failed to get pairing file");
lockdown_client.start_session(pairing_file).expect("Failed to start session");
auto version_plist = lockdown_client.get_value("ProductVersion", NULL)
.expect("Failed to get ProductVersion");
PList::String version_node(version_plist);
std::string version_str = version_node.GetValue();
std::cout << "Version string: " << version_str << std::endl;
if (version_str.empty()) {
throw std::runtime_error(
"Failed to get a valid ProductVersion string from the device.");
}
int major_version = std::stoi(version_str);
// --- 4. Connect to MobileImageMounter ---
auto mounter_client = IdeviceFFI::MobileImageMounter::connect(prov).expect(
"Failed to connect to image mounter");
// --- 5. Execute Subcommand ---
if (subcommand == "list") {
auto images = mounter_client.copy_devices().expect("Failed to get images");
std::cout << "Mounted Images:\n";
for (plist_t p : images) {
PList::Dictionary dict(p);
std::cout << dict.ToXml() << std::endl;
}
} else if (subcommand == "unmount") {
const char* unmount_path = (major_version < 17) ? "/Developer" : "/System/Developer";
mounter_client.unmount_image(unmount_path).expect("Failed to unmount image");
std::cout << "Successfully unmounted image from " << unmount_path << std::endl;
} else if (subcommand == "mount") {
if (image_path.empty()) {
throw std::runtime_error("Mount command requires --image <path>");
}
auto image_data = read_file(image_path);
if (major_version < 17) {
if (signature_path.empty()) {
throw std::runtime_error("iOS < 17 requires --signature <path>");
}
auto signature_data = read_file(signature_path);
mounter_client
.mount_developer(image_data.data(),
image_data.size(),
signature_data.data(),
signature_data.size())
.expect("Failed to mount developer image");
} else { // iOS 17+
if (manifest_path.empty() || trustcache_path.empty()) {
throw std::runtime_error("iOS 17+ requires --manifest and --trustcache paths");
}
auto manifest_data = read_file(manifest_path);
auto trustcache_data = read_file(trustcache_path);
auto chip_id_plist = lockdown_client.get_value(nullptr, "UniqueChipID")
.expect("Failed to get UniqueChipID");
PList::Integer chip_id_node(chip_id_plist);
uint64_t unique_chip_id = chip_id_node.GetValue();
std::function<void(size_t, size_t)> progress_callback = [](size_t n, size_t d) {
if (d == 0) {
return;
}
double percent = (static_cast<double>(n) / d) * 100.0;
std::cout << "\rProgress: " << std::fixed << std::setprecision(2) << percent
<< "%" << std::flush;
if (n == d) {
std::cout << std::endl;
}
};
mounter_client
.mount_personalized_with_callback(prov,
image_data.data(),
image_data.size(),
trustcache_data.data(),
trustcache_data.size(),
manifest_data.data(),
manifest_data.size(),
nullptr, // info_plist
unique_chip_id,
progress_callback)
.expect("Failed to mount personalized image");
}
std::cout << "Successfully mounted image." << std::endl;
}
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
return 0;
}

View File

@@ -0,0 +1,40 @@
#pragma once
#include <idevice++/bindings.hpp>
#include <idevice++/ffi.hpp>
#include <idevice++/provider.hpp>
#include <memory>
#include <sys/_types/_u_int64_t.h>
namespace IdeviceFFI {
using HeartbeatPtr =
std::unique_ptr<HeartbeatClientHandle, FnDeleter<HeartbeatClientHandle, heartbeat_client_free>>;
class Heartbeat {
public:
// Factory: connect via Provider
static Result<Heartbeat, FfiError> connect(Provider& provider);
// Factory: wrap an existing Idevice socket (consumes it on success)
static Result<Heartbeat, FfiError> from_socket(Idevice&& socket);
// Ops
Result<void, FfiError> send_polo();
Result<u_int64_t, FfiError> get_marco(u_int64_t interval);
// RAII / moves
~Heartbeat() noexcept = default;
Heartbeat(Heartbeat&&) noexcept = default;
Heartbeat& operator=(Heartbeat&&) noexcept = default;
Heartbeat(const Heartbeat&) = delete;
Heartbeat& operator=(const Heartbeat&) = delete;
HeartbeatClientHandle* raw() const noexcept { return handle_.get(); }
static Heartbeat adopt(HeartbeatClientHandle* h) noexcept { return Heartbeat(h); }
private:
explicit Heartbeat(HeartbeatClientHandle* h) noexcept : handle_(h) {}
HeartbeatPtr handle_{};
};
} // namespace IdeviceFFI

View File

@@ -0,0 +1,60 @@
#pragma once
#include <functional>
#include <idevice++/bindings.hpp>
#include <idevice++/ffi.hpp>
#include <idevice++/provider.hpp>
#include <memory>
#include <sys/_types/_u_int64_t.h>
namespace IdeviceFFI {
using InstallationProxyPtr =
std::unique_ptr<InstallationProxyClientHandle,
FnDeleter<InstallationProxyClientHandle, installation_proxy_client_free>>;
class InstallationProxy {
public:
// Factory: connect via Provider
static Result<InstallationProxy, FfiError> connect(Provider& provider);
// Factory: wrap an existing Idevice socket (consumes it on success)
static Result<InstallationProxy, FfiError> from_socket(Idevice&& socket);
// Ops
Result<std::vector<plist_t>, FfiError>
get_apps(Option<std::string> application_type,
Option<std::vector<std::string>> bundle_identifiers);
Result<void, FfiError> install(std::string package_path, Option<plist_t> options);
Result<void, FfiError> install_with_callback(std::string package_path,
Option<plist_t> options,
std::function<void(u_int64_t)>& lambda);
Result<void, FfiError> upgrade(std::string package_path, Option<plist_t> options);
Result<void, FfiError> upgrade_with_callback(std::string package_path,
Option<plist_t> options,
std::function<void(u_int64_t)>& lambda);
Result<void, FfiError> uninstall(std::string package_path, Option<plist_t> options);
Result<void, FfiError> uninstall_with_callback(std::string package_path,
Option<plist_t> options,
std::function<void(u_int64_t)>& lambda);
Result<bool, FfiError> check_capabilities_match(std::vector<plist_t> capabilities,
Option<plist_t> options);
Result<std::vector<plist_t>, FfiError> browse(Option<plist_t> options);
// RAII / moves
~InstallationProxy() noexcept = default;
InstallationProxy(InstallationProxy&&) noexcept = default;
InstallationProxy& operator=(InstallationProxy&&) noexcept = default;
InstallationProxy(const InstallationProxy&) = delete;
InstallationProxy& operator=(const InstallationProxy&) = delete;
InstallationProxyClientHandle* raw() const noexcept { return handle_.get(); }
static InstallationProxy adopt(InstallationProxyClientHandle* h) noexcept {
return InstallationProxy(h);
}
private:
explicit InstallationProxy(InstallationProxyClientHandle* h) noexcept : handle_(h) {}
InstallationProxyPtr handle_{};
};
} // namespace IdeviceFFI

View File

@@ -1,4 +1,3 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <idevice++/bindings.hpp> #include <idevice++/bindings.hpp>

View File

@@ -0,0 +1,87 @@
#pragma once
#include <cstdint>
#include <functional>
#include <idevice++/bindings.hpp>
#include <idevice++/ffi.hpp>
#include <idevice++/provider.hpp>
#include <memory>
#include <string>
namespace IdeviceFFI {
using MobileImageMounterPtr =
std::unique_ptr<ImageMounterHandle, FnDeleter<ImageMounterHandle, image_mounter_free>>;
class MobileImageMounter {
public:
// Factory: connect via Provider
static Result<MobileImageMounter, FfiError> connect(Provider& provider);
// Factory: wrap an existing Idevice socket (consumes it on success)
static Result<MobileImageMounter, FfiError> from_socket(Idevice&& socket);
// Ops
Result<std::vector<plist_t>, FfiError> copy_devices();
Result<std::vector<uint8_t>, FfiError> lookup_image(std::string image_type);
Result<void, FfiError> upload_image(std::string image_type,
const uint8_t* image_data,
size_t image_size,
const uint8_t* signature_data,
size_t signature_size);
Result<void, FfiError> mount_image(std::string image_type,
const uint8_t* signature_data,
size_t signature_size,
const uint8_t* trust_cache_data,
size_t trust_cache_size,
plist_t info_plist);
Result<void, FfiError> unmount_image(std::string mount_path);
Result<bool, FfiError> query_developer_mode_status();
Result<void, FfiError> mount_developer(const uint8_t* image_data,
size_t image_size,
const uint8_t* signature_data,
size_t signature_size);
Result<std::vector<uint8_t>, FfiError> query_personalization_manifest(
std::string image_type, const uint8_t* signature_data, size_t signature_size);
Result<std::vector<uint8_t>, FfiError> query_nonce(std::string personalized_image_type);
Result<plist_t, FfiError> query_personalization_identifiers(std::string image_type);
Result<void, FfiError> roll_personalization_nonce();
Result<void, FfiError> roll_cryptex_nonce();
Result<void, FfiError> mount_personalized(Provider& provider,
const uint8_t* image_data,
size_t image_size,
const uint8_t* trust_cache_data,
size_t trust_cache_size,
const uint8_t* build_manifest_data,
size_t build_manifest_size,
plist_t info_plist,
uint64_t unique_chip_id);
Result<void, FfiError>
mount_personalized_with_callback(Provider& provider,
const uint8_t* image_data,
size_t image_size,
const uint8_t* trust_cache_data,
size_t trust_cache_size,
const uint8_t* build_manifest_data,
size_t build_manifest_size,
plist_t info_plist,
uint64_t unique_chip_id,
std::function<void(size_t, size_t)>& lambda);
// RAII / moves
~MobileImageMounter() noexcept = default;
MobileImageMounter(MobileImageMounter&&) noexcept = default;
MobileImageMounter& operator=(MobileImageMounter&&) noexcept = default;
MobileImageMounter(const MobileImageMounter&) = delete;
MobileImageMounter& operator=(const MobileImageMounter&) = delete;
ImageMounterHandle* raw() const noexcept { return handle_.get(); }
static MobileImageMounter adopt(ImageMounterHandle* h) noexcept {
return MobileImageMounter(h);
}
private:
explicit MobileImageMounter(ImageMounterHandle* h) noexcept : handle_(h) {}
MobileImageMounterPtr handle_{};
};
} // namespace IdeviceFFI

View File

@@ -8,6 +8,7 @@
#pragma once #pragma once
#include <cstdio>
#include <stdexcept> #include <stdexcept>
#include <type_traits> #include <type_traits>
#include <utility> #include <utility>
@@ -115,6 +116,34 @@ template <typename T> class Option {
return has_ ? std::move(*ptr()) : static_cast<T>(f()); return has_ ? std::move(*ptr()) : static_cast<T>(f());
} }
T expect(const char* message) && {
if (is_none()) {
std::fprintf(stderr, "Fatal (expect) error: %s\n", message);
std::terminate();
}
T tmp = std::move(*ptr());
reset();
return tmp;
}
// Returns a mutable reference from an lvalue Result
T& expect(const char* message) & {
if (is_none()) {
std::fprintf(stderr, "Fatal (expect) error: %s\n", message);
std::terminate();
}
return *ptr();
}
// Returns a const reference from a const lvalue Result
const T& expect(const char* message) const& {
if (is_none()) {
std::fprintf(stderr, "Fatal (expect) error: %s\n", message);
std::terminate();
}
return *ptr();
}
// map // map
template <typename F> template <typename F>
auto map(F&& f) const& -> Option<typename std::decay<decltype(f(*ptr()))>::type> { auto map(F&& f) const& -> Option<typename std::decay<decltype(f(*ptr()))>::type> {

View File

@@ -0,0 +1,41 @@
// Jackson Coxson
#pragma once
#include <idevice++/bindings.hpp>
#include <idevice++/remote_server.hpp>
#include <idevice++/result.hpp>
#include <memory>
namespace IdeviceFFI {
using ProcessControlPtr =
std::unique_ptr<ProcessControlHandle, FnDeleter<ProcessControlHandle, process_control_free>>;
class ProcessControl {
public:
// Factory: borrows the RemoteServer; not consumed
static Result<ProcessControl, FfiError> create(RemoteServer& server);
Result<u_int64_t, FfiError> launch_app(std::string bundle_id,
Option<std::vector<std::string>> env_vars,
Option<std::vector<std::string>> arguments,
bool start_suspended,
bool kill_existing);
Result<void, FfiError> kill_app(u_int64_t pid);
Result<void, FfiError> disable_memory_limit(u_int64_t pid);
~ProcessControl() noexcept = default;
ProcessControl(ProcessControl&&) noexcept = default;
ProcessControl& operator=(ProcessControl&&) noexcept = default;
ProcessControl(const ProcessControl&) = delete;
ProcessControl& operator=(const ProcessControl&) = delete;
ProcessControlHandle* raw() const noexcept { return handle_.get(); }
static ProcessControl adopt(ProcessControlHandle* h) noexcept { return ProcessControl(h); }
private:
explicit ProcessControl(ProcessControlHandle* h) noexcept : handle_(h) {}
ProcessControlPtr handle_{};
};
} // namespace IdeviceFFI

47
cpp/src/heartbeat.cpp Normal file
View File

@@ -0,0 +1,47 @@
// Jackson Coxson
#include <idevice++/bindings.hpp>
#include <idevice++/ffi.hpp>
#include <idevice++/heartbeat.hpp>
#include <idevice++/provider.hpp>
namespace IdeviceFFI {
Result<Heartbeat, FfiError> Heartbeat::connect(Provider& provider) {
HeartbeatClientHandle* out = nullptr;
FfiError e(::heartbeat_connect(provider.raw(), &out));
if (e) {
provider.release();
return Err(e);
}
return Ok(Heartbeat::adopt(out));
}
Result<Heartbeat, FfiError> Heartbeat::from_socket(Idevice&& socket) {
HeartbeatClientHandle* out = nullptr;
FfiError e(::heartbeat_new(socket.raw(), &out));
if (e) {
return Err(e);
}
socket.release();
return Ok(Heartbeat::adopt(out));
}
Result<void, FfiError> Heartbeat::send_polo() {
FfiError e(::heartbeat_send_polo(handle_.get()));
if (e) {
return Err(e);
}
return Ok();
}
Result<u_int64_t, FfiError> Heartbeat::get_marco(u_int64_t interval) {
u_int64_t new_interval = 0;
FfiError e(::heartbeat_get_marco(handle_.get(), interval, &new_interval));
if (e) {
return Err(e);
}
return Ok(new_interval);
}
} // namespace IdeviceFFI

View File

@@ -0,0 +1,240 @@
// Jackson Coxson
#include <cstring>
#include <idevice++/bindings.hpp>
#include <idevice++/installation_proxy.hpp>
#include <sys/_types/_u_int64_t.h>
#include <vector>
namespace IdeviceFFI {
// -------- Anonymous Namespace for Helpers --------
namespace {
/**
* @brief A C-style trampoline function to call back into a C++ std::function.
*
* This function is passed to the Rust FFI layer. It receives a void* context,
* which it casts back to the original std::function object to invoke it.
*/
extern "C" void progress_trampoline(u_int64_t progress, void* context) {
if (context) {
auto& callback_fn = *static_cast<std::function<void(u_int64_t)>*>(context);
callback_fn(progress);
}
}
} // namespace
// -------- Factory Methods --------
Result<InstallationProxy, FfiError> InstallationProxy::connect(Provider& provider) {
InstallationProxyClientHandle* handle = nullptr;
FfiError e(::installation_proxy_connect(provider.raw(), &handle));
if (e) {
return Err(e);
}
return Ok(InstallationProxy::adopt(handle));
}
Result<InstallationProxy, FfiError> InstallationProxy::from_socket(Idevice&& socket) {
InstallationProxyClientHandle* handle = nullptr;
// The Rust FFI function consumes the socket, so we must release it from the
// C++ RAII wrapper's control. An `Idevice::release()` method is assumed here.
FfiError e(::installation_proxy_new(socket.release(), &handle));
if (e) {
return Err(e);
}
return Ok(InstallationProxy::adopt(handle));
}
// -------- Ops --------
Result<std::vector<plist_t>, FfiError>
InstallationProxy::get_apps(Option<std::string> application_type,
Option<std::vector<std::string>> bundle_identifiers) {
plist_t* apps_raw = nullptr;
size_t apps_len = 0;
const char* application_type_ptr = NULL;
if (application_type.is_some()) {
application_type_ptr = application_type.unwrap().c_str();
}
std::vector<const char*> c_bundle_id;
size_t bundle_identifiers_len = 0;
if (bundle_identifiers.is_some()) {
c_bundle_id.reserve(bundle_identifiers.unwrap().size());
for (auto& a : bundle_identifiers.unwrap()) {
c_bundle_id.push_back(a.c_str());
}
}
FfiError e(::installation_proxy_get_apps(
this->raw(),
application_type_ptr,
c_bundle_id.empty() ? nullptr : const_cast<const char* const*>(c_bundle_id.data()),
bundle_identifiers_len,
apps_raw,
&apps_len));
if (e) {
return Err(e);
}
std::vector<plist_t> apps;
if (apps_raw) {
apps.assign(apps_raw, apps_raw + apps_len);
}
return Ok(std::move(apps));
}
Result<void, FfiError> InstallationProxy::install(std::string package_path,
Option<plist_t> options) {
plist_t unwrapped_options;
if (options.is_some()) {
unwrapped_options = std::move(options).unwrap();
} else {
unwrapped_options = NULL;
}
FfiError e(::installation_proxy_install(this->raw(), package_path.c_str(), &unwrapped_options));
if (e) {
return Err(e);
}
return Ok();
}
Result<void, FfiError> InstallationProxy::install_with_callback(
std::string package_path, Option<plist_t> options, std::function<void(u_int64_t)>& lambda
) {
plist_t unwrapped_options;
if (options.is_some()) {
unwrapped_options = std::move(options).unwrap();
} else {
unwrapped_options = NULL;
}
FfiError e(::installation_proxy_install_with_callback(
this->raw(), package_path.c_str(), &unwrapped_options, progress_trampoline, &lambda));
if (e) {
return Err(e);
}
return Ok();
}
Result<void, FfiError> InstallationProxy::upgrade(std::string package_path,
Option<plist_t> options) {
plist_t unwrapped_options;
if (options.is_some()) {
unwrapped_options = std::move(options).unwrap();
} else {
unwrapped_options = NULL;
}
FfiError e(::installation_proxy_upgrade(this->raw(), package_path.c_str(), &unwrapped_options));
if (e) {
return Err(e);
}
return Ok();
}
Result<void, FfiError> InstallationProxy::upgrade_with_callback(
std::string package_path, Option<plist_t> options, std::function<void(u_int64_t)>& lambda
) {
plist_t unwrapped_options;
if (options.is_some()) {
unwrapped_options = std::move(options).unwrap();
} else {
unwrapped_options = NULL;
}
FfiError e(::installation_proxy_upgrade_with_callback(
this->raw(), package_path.c_str(), &unwrapped_options, progress_trampoline, &lambda));
if (e) {
return Err(e);
}
return Ok();
}
Result<void, FfiError> InstallationProxy::uninstall(std::string package_path,
Option<plist_t> options) {
plist_t unwrapped_options;
if (options.is_some()) {
unwrapped_options = std::move(options).unwrap();
} else {
unwrapped_options = NULL;
}
FfiError e(
::installation_proxy_uninstall(this->raw(), package_path.c_str(), &unwrapped_options));
if (e) {
return Err(e);
}
return Ok();
}
Result<void, FfiError> InstallationProxy::uninstall_with_callback(
std::string package_path, Option<plist_t> options, std::function<void(u_int64_t)>& lambda
) {
plist_t unwrapped_options;
if (options.is_some()) {
unwrapped_options = std::move(options).unwrap();
} else {
unwrapped_options = NULL;
}
FfiError e(::installation_proxy_uninstall_with_callback(
this->raw(), package_path.c_str(), &unwrapped_options, progress_trampoline, &lambda));
if (e) {
return Err(e);
}
return Ok();
}
Result<bool, FfiError>
InstallationProxy::check_capabilities_match(std::vector<plist_t> capabilities,
Option<plist_t> options) {
plist_t unwrapped_options;
if (options.is_some()) {
unwrapped_options = std::move(options).unwrap();
} else {
unwrapped_options = NULL;
}
bool res = false;
FfiError e(::installation_proxy_check_capabilities_match(
this->raw(),
capabilities.empty() ? nullptr : capabilities.data(),
capabilities.size(),
unwrapped_options,
&res));
return e ? Result<bool, FfiError>(Err(e)) : Result<bool, FfiError>(Ok(res));
}
Result<std::vector<plist_t>, FfiError> InstallationProxy::browse(Option<plist_t> options) {
plist_t* apps_raw = nullptr;
size_t apps_len = 0;
plist_t unwrapped_options;
if (options.is_some()) {
unwrapped_options = std::move(options).unwrap();
} else {
unwrapped_options = NULL;
}
FfiError e(::installation_proxy_browse(this->raw(), unwrapped_options, &apps_raw, &apps_len));
if (e) {
return Err(e);
}
std::vector<plist_t> apps;
if (apps_raw) {
apps.assign(apps_raw, apps_raw + apps_len);
}
return Ok(std::move(apps));
}
} // namespace IdeviceFFI

View File

@@ -0,0 +1,247 @@
// Jackson Coxson
#include <idevice++/mobile_image_mounter.hpp>
#include <vector>
#include <cstring>
namespace IdeviceFFI {
// -------- Anonymous Namespace for Helpers --------
namespace {
/**
* @brief A C-style trampoline function to call back into a C++ std::function.
*
* This function is passed to the Rust FFI layer. It receives a void* context,
* which it casts back to the original std::function object to invoke it.
*/
extern "C" void progress_trampoline(size_t progress, size_t total, void* context) {
if (context) {
auto& callback_fn = *static_cast<std::function<void(size_t, size_t)>*>(context);
callback_fn(progress, total);
}
}
} // namespace
// -------- Factory Methods --------
Result<MobileImageMounter, FfiError> MobileImageMounter::connect(Provider& provider) {
ImageMounterHandle* handle = nullptr;
FfiError e(::image_mounter_connect(provider.raw(), &handle));
if (e) {
return Err(e);
}
return Ok(MobileImageMounter::adopt(handle));
}
Result<MobileImageMounter, FfiError> MobileImageMounter::from_socket(Idevice&& socket) {
ImageMounterHandle* handle = nullptr;
// The Rust FFI function consumes the socket, so we must release it from the
// C++ RAII wrapper's control. An `Idevice::release()` method is assumed here.
FfiError e(::image_mounter_new(socket.release(), &handle));
if (e) {
return Err(e);
}
return Ok(MobileImageMounter::adopt(handle));
}
// -------- Ops --------
Result<std::vector<plist_t>, FfiError> MobileImageMounter::copy_devices() {
plist_t* devices_raw = nullptr;
size_t devices_len = 0;
FfiError e(::image_mounter_copy_devices(this->raw(), &devices_raw, &devices_len));
if (e) {
return Err(e);
}
std::vector<plist_t> devices;
if (devices_raw) {
devices.assign(devices_raw, devices_raw + devices_len);
}
return Ok(std::move(devices));
}
Result<std::vector<uint8_t>, FfiError> MobileImageMounter::lookup_image(std::string image_type) {
uint8_t* signature_raw = nullptr;
size_t signature_len = 0;
FfiError e(::image_mounter_lookup_image(
this->raw(), image_type.c_str(), &signature_raw, &signature_len));
if (e) {
return Err(e);
}
std::vector<uint8_t> signature(signature_len);
std::memcpy(signature.data(), signature_raw, signature_len);
idevice_data_free(signature_raw, signature_len);
return Ok(std::move(signature));
}
Result<void, FfiError> MobileImageMounter::upload_image(std::string image_type,
const uint8_t* image_data,
size_t image_size,
const uint8_t* signature_data,
size_t signature_size) {
FfiError e(::image_mounter_upload_image(
this->raw(), image_type.c_str(), image_data, image_size, signature_data, signature_size));
return e ? Result<void, FfiError>(Err(e)) : Result<void, FfiError>(Ok());
}
Result<void, FfiError> MobileImageMounter::mount_image(std::string image_type,
const uint8_t* signature_data,
size_t signature_size,
const uint8_t* trust_cache_data,
size_t trust_cache_size,
plist_t info_plist) {
FfiError e(::image_mounter_mount_image(this->raw(),
image_type.c_str(),
signature_data,
signature_size,
trust_cache_data,
trust_cache_size,
info_plist));
return e ? Result<void, FfiError>(Err(e)) : Result<void, FfiError>(Ok());
}
Result<void, FfiError> MobileImageMounter::unmount_image(std::string mount_path) {
FfiError e(::image_mounter_unmount_image(this->raw(), mount_path.c_str()));
return e ? Result<void, FfiError>(Err(e)) : Result<void, FfiError>(Ok());
}
Result<bool, FfiError> MobileImageMounter::query_developer_mode_status() {
int status_c = 0;
FfiError e(::image_mounter_query_developer_mode_status(this->raw(), &status_c));
if (e) {
return Err(e);
}
return Ok(status_c != 0);
}
Result<void, FfiError> MobileImageMounter::mount_developer(const uint8_t* image_data,
size_t image_size,
const uint8_t* signature_data,
size_t signature_size) {
FfiError e(::image_mounter_mount_developer(
this->raw(), image_data, image_size, signature_data, signature_size));
return e ? Result<void, FfiError>(Err(e)) : Result<void, FfiError>(Ok());
}
Result<std::vector<uint8_t>, FfiError> MobileImageMounter::query_personalization_manifest(
std::string image_type, const uint8_t* signature_data, size_t signature_size) {
uint8_t* manifest_raw = nullptr;
size_t manifest_len = 0;
FfiError e(::image_mounter_query_personalization_manifest(this->raw(),
image_type.c_str(),
signature_data,
signature_size,
&manifest_raw,
&manifest_len));
if (e) {
return Err(e);
}
std::vector<uint8_t> manifest(manifest_len);
std::memcpy(manifest.data(), manifest_raw, manifest_len);
idevice_data_free(manifest_raw, manifest_len);
return Ok(std::move(manifest));
}
Result<std::vector<uint8_t>, FfiError>
MobileImageMounter::query_nonce(std::string personalized_image_type) {
uint8_t* nonce_raw = nullptr;
size_t nonce_len = 0;
const char* image_type_c =
personalized_image_type.empty() ? nullptr : personalized_image_type.c_str();
FfiError e(::image_mounter_query_nonce(this->raw(), image_type_c, &nonce_raw, &nonce_len));
if (e) {
return Err(e);
}
std::vector<uint8_t> nonce(nonce_len);
std::memcpy(nonce.data(), nonce_raw, nonce_len);
idevice_data_free(nonce_raw, nonce_len);
return Ok(std::move(nonce));
}
Result<plist_t, FfiError>
MobileImageMounter::query_personalization_identifiers(std::string image_type) {
plist_t identifiers = nullptr;
const char* image_type_c = image_type.empty() ? nullptr : image_type.c_str();
FfiError e(
::image_mounter_query_personalization_identifiers(this->raw(), image_type_c, &identifiers));
if (e) {
return Err(e);
}
// The caller now owns the returned `plist_t` and is responsible for freeing it.
return Ok(identifiers);
}
Result<void, FfiError> MobileImageMounter::roll_personalization_nonce() {
FfiError e(::image_mounter_roll_personalization_nonce(this->raw()));
return e ? Result<void, FfiError>(Err(e)) : Result<void, FfiError>(Ok());
}
Result<void, FfiError> MobileImageMounter::roll_cryptex_nonce() {
FfiError e(::image_mounter_roll_cryptex_nonce(this->raw()));
return e ? Result<void, FfiError>(Err(e)) : Result<void, FfiError>(Ok());
}
Result<void, FfiError> MobileImageMounter::mount_personalized(Provider& provider,
const uint8_t* image_data,
size_t image_size,
const uint8_t* trust_cache_data,
size_t trust_cache_size,
const uint8_t* build_manifest_data,
size_t build_manifest_size,
plist_t info_plist,
uint64_t unique_chip_id) {
FfiError e(::image_mounter_mount_personalized(this->raw(),
provider.raw(),
image_data,
image_size,
trust_cache_data,
trust_cache_size,
build_manifest_data,
build_manifest_size,
info_plist,
unique_chip_id));
return e ? Result<void, FfiError>(Err(e)) : Result<void, FfiError>(Ok());
}
Result<void, FfiError>
MobileImageMounter::mount_personalized_with_callback(Provider& provider,
const uint8_t* image_data,
size_t image_size,
const uint8_t* trust_cache_data,
size_t trust_cache_size,
const uint8_t* build_manifest_data,
size_t build_manifest_size,
plist_t info_plist,
uint64_t unique_chip_id,
std::function<void(size_t, size_t)>& lambda) {
FfiError e(::image_mounter_mount_personalized_with_callback(this->raw(),
provider.raw(),
image_data,
image_size,
trust_cache_data,
trust_cache_size,
build_manifest_data,
build_manifest_size,
info_plist,
unique_chip_id,
progress_trampoline,
&lambda /* context */));
return e ? Result<void, FfiError>(Err(e)) : Result<void, FfiError>(Ok());
}
} // namespace IdeviceFFI

View File

@@ -0,0 +1,73 @@
// Jackson Coxson
#include <idevice++/process_control.hpp>
namespace IdeviceFFI {
Result<ProcessControl, FfiError> ProcessControl::create(RemoteServer& server) {
ProcessControlHandle* out = nullptr;
FfiError e(::process_control_new(server.raw(), &out));
if (e) {
return Err(e);
}
return Ok(ProcessControl::adopt(out));
}
Result<u_int64_t, FfiError> ProcessControl::launch_app(std::string bundle_id,
Option<std::vector<std::string>> env_vars,
Option<std::vector<std::string>> arguments,
bool start_suspended,
bool kill_existing) {
std::vector<const char*> c_env_vars;
size_t env_vars_len = 0;
if (env_vars.is_some()) {
c_env_vars.reserve(env_vars.unwrap().size());
for (auto& a : env_vars.unwrap()) {
c_env_vars.push_back(a.c_str());
}
}
std::vector<const char*> c_arguments;
size_t arguments_len = 0;
if (arguments.is_some()) {
c_arguments.reserve(arguments.unwrap().size());
for (auto& a : arguments.unwrap()) {
c_arguments.push_back(a.c_str());
}
}
u_int64_t pid = 0;
FfiError e(::process_control_launch_app(
handle_.get(),
bundle_id.c_str(),
c_env_vars.empty() ? nullptr : const_cast<const char* const*>(c_env_vars.data()),
env_vars_len,
c_arguments.empty() ? nullptr : const_cast<const char* const*>(c_arguments.data()),
arguments_len,
start_suspended,
kill_existing,
&pid));
if (e) {
return Err(e);
}
return Ok(pid);
}
Result<void, FfiError> ProcessControl::kill_app(u_int64_t pid) {
FfiError e(::process_control_kill_app(handle_.get(), pid));
if (e) {
return Err(e);
}
return Ok();
}
Result<void, FfiError> ProcessControl::disable_memory_limit(u_int64_t pid) {
FfiError e(::process_control_disable_memory_limit(handle_.get(), pid));
if (e) {
return Err(e);
}
return Ok();
}
} // namespace IdeviceFFI

View File

@@ -13,7 +13,7 @@ once_cell = "1.21.1"
tokio = { version = "1.44.1", features = ["full"] } tokio = { version = "1.44.1", features = ["full"] }
libc = "0.2.171" libc = "0.2.171"
plist = "1.7.1" plist = "1.7.1"
plist_ffi = { version = "0.1.5" } plist_ffi = { version = "0.1.6" }
uuid = { version = "1.12", features = ["v4"], optional = true } uuid = { version = "1.12", features = ["v4"], optional = true }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]

View File

@@ -25,7 +25,7 @@ pub struct InstallationProxyClientHandle(pub InstallationProxyClient);
/// `provider` must be a valid pointer to a handle allocated by this library /// `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 /// `client` must be a valid, non-null pointer to a location where the handle will be stored
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub unsafe extern "C" fn installation_proxy_connect_tcp( pub unsafe extern "C" fn installation_proxy_connect(
provider: *mut IdeviceProviderHandle, provider: *mut IdeviceProviderHandle,
client: *mut *mut InstallationProxyClientHandle, client: *mut *mut InstallationProxyClientHandle,
) -> *mut IdeviceFfiError { ) -> *mut IdeviceFfiError {

View File

@@ -2,7 +2,7 @@
name = "idevice" name = "idevice"
description = "A Rust library to interact with services on iOS devices." description = "A Rust library to interact with services on iOS devices."
authors = ["Jackson Coxson"] authors = ["Jackson Coxson"]
version = "0.1.41" version = "0.1.42"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
documentation = "https://docs.rs/idevice" documentation = "https://docs.rs/idevice"
@@ -60,6 +60,8 @@ chacha20poly1305 = { version = "0.10", optional = true }
obfstr = { version = "0.4", optional = true } obfstr = { version = "0.4", optional = true }
async_zip = { version = "0.0.18", optional = true }
[dev-dependencies] [dev-dependencies]
tokio = { version = "1.43", features = ["full"] } tokio = { version = "1.43", features = ["full"] }
tun-rs = { version = "2.0.8", features = ["async_tokio"] } tun-rs = { version = "2.0.8", features = ["async_tokio"] }
@@ -82,10 +84,17 @@ diagnostics_relay = []
dvt = ["dep:byteorder", "dep:ns-keyed-archive"] dvt = ["dep:byteorder", "dep:ns-keyed-archive"]
heartbeat = ["tokio/macros", "tokio/time"] heartbeat = ["tokio/macros", "tokio/time"]
house_arrest = ["afc"] house_arrest = ["afc"]
installation_proxy = [] installation_proxy = [
"dep:async_zip",
"dep:futures",
"async_zip/tokio",
"async_zip/deflate",
"tokio/fs",
]
springboardservices = [] springboardservices = []
misagent = [] misagent = []
mobile_image_mounter = ["dep:sha2"] mobile_image_mounter = ["dep:sha2"]
mobileactivationd = ["dep:reqwest"]
mobilebackup2 = [] mobilebackup2 = []
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"]
@@ -102,6 +111,7 @@ remote_pairing = [
"dep:uuid", "dep:uuid",
] ]
rsd = ["xpc"] rsd = ["xpc"]
screenshotr = []
syslog_relay = ["dep:bytes"] syslog_relay = ["dep:bytes"]
tcp = ["tokio/net"] tcp = ["tokio/net"]
tunnel_tcp_stack = [ tunnel_tcp_stack = [
@@ -132,6 +142,7 @@ full = [
"location_simulation", "location_simulation",
"misagent", "misagent",
"mobile_image_mounter", "mobile_image_mounter",
"mobileactivationd",
"mobilebackup2", "mobilebackup2",
"pair", "pair",
"pcapd", "pcapd",
@@ -142,6 +153,7 @@ full = [
"location_simulation", "location_simulation",
"remote_pairing", "remote_pairing",
"rsd", "rsd",
"screenshotr",
"springboardservices", "springboardservices",
"syslog_relay", "syslog_relay",
"tcp", "tcp",

View File

@@ -692,32 +692,16 @@ pub enum IdeviceError {
#[error("failed to parse bytes as valid utf8")] #[error("failed to parse bytes as valid utf8")]
Utf8Error = -56, Utf8Error = -56,
#[cfg(feature = "debug_proxy")] #[cfg(any(
feature = "debug_proxy",
all(feature = "afc", feature = "installation_proxy")
))]
#[error("invalid argument passed")] #[error("invalid argument passed")]
InvalidArgument = -57, InvalidArgument = -57,
#[error("unknown error `{0}` returned from device")] #[error("unknown error `{0}` returned from device")]
UnknownErrorType(String) = -59, UnknownErrorType(String) = -59,
#[cfg(feature = "remote_pairing")]
#[error("could not parse as JSON")]
JsonParseFailed(#[from] json::Error) = -67,
#[cfg(feature = "remote_pairing")]
#[error("unknown TLV type: {0}")]
UnknownTlv(u8) = -68,
#[cfg(feature = "remote_pairing")]
#[error("malformed TLV")]
MalformedTlv = -69,
#[error("failed to decode base64 string")]
Base64Decode(#[from] base64::DecodeError) = -70,
#[cfg(feature = "remote_pairing")]
#[error("pair verify failed")]
PairVerifyFailed = -71,
#[error("invalid arguments were passed")] #[error("invalid arguments were passed")]
FfiInvalidArg = -60, FfiInvalidArg = -60,
#[error("invalid string was passed")] #[error("invalid string was passed")]
@@ -732,6 +716,32 @@ pub enum IdeviceError {
IntegerOverflow = -65, IntegerOverflow = -65,
#[error("canceled by user")] #[error("canceled by user")]
CanceledByUser = -66, CanceledByUser = -66,
#[cfg(feature = "installation_proxy")]
#[error("malformed package archive: {0}")]
MalformedPackageArchive(#[from] async_zip::error::ZipError) = -67,
#[error("Developer mode is not enabled")]
DeveloperModeNotEnabled = -68,
#[cfg(feature = "remote_pairing")]
#[error("could not parse as JSON")]
JsonParseFailed(#[from] json::Error) = -69,
#[cfg(feature = "remote_pairing")]
#[error("unknown TLV type: {0}")]
UnknownTlv(u8) = -70,
#[cfg(feature = "remote_pairing")]
#[error("malformed TLV")]
MalformedTlv = -71,
#[error("failed to decode base64 string")]
Base64Decode(#[from] base64::DecodeError) = -72,
#[cfg(feature = "remote_pairing")]
#[error("pair verify failed")]
PairVerifyFailed = -73,
} }
impl IdeviceError { impl IdeviceError {
@@ -746,6 +756,8 @@ impl IdeviceError {
fn from_device_error_type(e: &str, context: &plist::Dictionary) -> Option<Self> { fn from_device_error_type(e: &str, context: &plist::Dictionary) -> Option<Self> {
if e.contains("NSDebugDescription=Canceled by user.") { if e.contains("NSDebugDescription=Canceled by user.") {
return Some(Self::CanceledByUser); return Some(Self::CanceledByUser);
} else if e.contains("Developer mode is not enabled.") {
return Some(Self::DeveloperModeNotEnabled);
} }
match e { match e {
"GetProhibited" => Some(Self::GetProhibited), "GetProhibited" => Some(Self::GetProhibited),
@@ -876,7 +888,10 @@ impl IdeviceError {
IdeviceError::NotEnoughBytes(_, _) => -55, IdeviceError::NotEnoughBytes(_, _) => -55,
IdeviceError::Utf8Error => -56, IdeviceError::Utf8Error => -56,
#[cfg(feature = "debug_proxy")] #[cfg(any(
feature = "debug_proxy",
all(feature = "afc", feature = "installation_proxy")
))]
IdeviceError::InvalidArgument => -57, IdeviceError::InvalidArgument => -57,
IdeviceError::UnknownErrorType(_) => -59, IdeviceError::UnknownErrorType(_) => -59,
@@ -888,15 +903,18 @@ impl IdeviceError {
IdeviceError::IntegerOverflow => -65, IdeviceError::IntegerOverflow => -65,
IdeviceError::CanceledByUser => -66, IdeviceError::CanceledByUser => -66,
#[cfg(feature = "installation_proxy")]
IdeviceError::MalformedPackageArchive(_) => -67,
IdeviceError::DeveloperModeNotEnabled => -68,
#[cfg(feature = "remote_pairing")] #[cfg(feature = "remote_pairing")]
IdeviceError::JsonParseFailed(_) => -67, IdeviceError::JsonParseFailed(_) => -69,
#[cfg(feature = "remote_pairing")] #[cfg(feature = "remote_pairing")]
IdeviceError::UnknownTlv(_) => -68, IdeviceError::UnknownTlv(_) => -70,
#[cfg(feature = "remote_pairing")] #[cfg(feature = "remote_pairing")]
IdeviceError::MalformedTlv => -69, IdeviceError::MalformedTlv => -71,
IdeviceError::Base64Decode(_) => -70, IdeviceError::Base64Decode(_) => -72,
#[cfg(feature = "remote_pairing")] #[cfg(feature = "remote_pairing")]
IdeviceError::PairVerifyFailed => -71, IdeviceError::PairVerifyFailed => -73,
} }
} }
} }

View File

@@ -1,5 +1,7 @@
// Jackson Coxson // Jackson Coxson
use std::io::SeekFrom;
use crate::IdeviceError; use crate::IdeviceError;
use super::{ use super::{
@@ -19,28 +21,76 @@ pub struct FileDescriptor<'a> {
} }
impl FileDescriptor<'_> { impl FileDescriptor<'_> {
/// Closes the file descriptor /// Generic helper to send an AFC packet and read the response
pub async fn close(self) -> Result<(), IdeviceError> { async fn send_packet(
let header_payload = self.fd.to_le_bytes().to_vec(); &mut self,
opcode: AfcOpcode,
header_payload: Vec<u8>,
payload: Vec<u8>,
) -> Result<AfcPacket, IdeviceError> {
let header_len = header_payload.len() as u64 + AfcPacketHeader::LEN; let header_len = header_payload.len() as u64 + AfcPacketHeader::LEN;
let header = AfcPacketHeader { let header = AfcPacketHeader {
magic: super::MAGIC, magic: super::MAGIC,
entire_len: header_len, entire_len: header_len + payload.len() as u64,
header_payload_len: header_len, header_payload_len: header_len,
packet_num: self.client.package_number, packet_num: self.client.package_number,
operation: AfcOpcode::FileClose, operation: opcode,
}; };
self.client.package_number += 1; self.client.package_number += 1;
let packet = AfcPacket { let packet = AfcPacket {
header, header,
header_payload, header_payload,
payload: Vec::new(), payload,
}; };
self.client.send(packet).await?; self.client.send(packet).await?;
self.client.read().await?; self.client.read().await
}
/// Returns the current cursor position for the file
pub async fn seek_tell(&mut self) -> Result<u64, IdeviceError> {
let header_payload = self.fd.to_le_bytes().to_vec();
let res = self
.send_packet(AfcOpcode::FileTell, header_payload, Vec::new())
.await?;
let cur_pos = res
.header_payload
.get(..8)
.ok_or(IdeviceError::UnexpectedResponse)?
.try_into()
.map(u64::from_le_bytes)
.map_err(|_| IdeviceError::UnexpectedResponse)?;
Ok(cur_pos)
}
/// Moves the file cursor
pub async fn seek(&mut self, pos: SeekFrom) -> Result<(), IdeviceError> {
let (offset, whence) = match pos {
SeekFrom::Start(off) => (off as i64, 0),
SeekFrom::Current(off) => (off, 1),
SeekFrom::End(off) => (off, 2),
};
let mut header_payload = Vec::new();
header_payload.extend(self.fd.to_le_bytes());
header_payload.extend((whence as u64).to_le_bytes());
header_payload.extend(offset.to_le_bytes());
self.send_packet(AfcOpcode::FileSeek, header_payload, Vec::new())
.await?;
Ok(())
}
/// Closes the file descriptor
pub async fn close(mut self) -> Result<(), IdeviceError> {
let header_payload = self.fd.to_le_bytes().to_vec();
self.send_packet(AfcOpcode::FileClose, header_payload, Vec::new())
.await?;
Ok(()) Ok(())
} }
@@ -49,32 +99,18 @@ impl FileDescriptor<'_> {
/// # Returns /// # Returns
/// A vector containing the file's data /// A vector containing the file's data
pub async fn read(&mut self) -> Result<Vec<u8>, IdeviceError> { pub async fn read(&mut self) -> Result<Vec<u8>, IdeviceError> {
// Get the file size first let seek_pos = self.seek_tell().await? as usize;
let mut bytes_left = self.client.get_file_info(&self.path).await?.size; let file_info = self.client.get_file_info(&self.path).await?;
let mut bytes_left = file_info.size.saturating_sub(seek_pos);
let mut collected_bytes = Vec::with_capacity(bytes_left); let mut collected_bytes = Vec::with_capacity(bytes_left);
while bytes_left > 0 { while bytes_left > 0 {
let mut header_payload = self.fd.to_le_bytes().to_vec(); let mut header_payload = self.fd.to_le_bytes().to_vec();
header_payload.extend_from_slice(&MAX_TRANSFER.to_le_bytes()); header_payload.extend_from_slice(&MAX_TRANSFER.to_le_bytes());
let header_len = header_payload.len() as u64 + AfcPacketHeader::LEN; let res = self
.send_packet(AfcOpcode::Read, header_payload, Vec::new())
.await?;
let header = AfcPacketHeader {
magic: super::MAGIC,
entire_len: header_len,
header_payload_len: header_len,
packet_num: self.client.package_number,
operation: AfcOpcode::Read,
};
self.client.package_number += 1;
let packet = AfcPacket {
header,
header_payload,
payload: Vec::new(),
};
self.client.send(packet).await?;
let res = self.client.read().await?;
bytes_left -= res.payload.len(); bytes_left -= res.payload.len();
collected_bytes.extend(res.payload); collected_bytes.extend(res.payload);
} }
@@ -87,29 +123,10 @@ impl FileDescriptor<'_> {
/// # Arguments /// # Arguments
/// * `bytes` - Data to write to the file /// * `bytes` - Data to write to the file
pub async fn write(&mut self, bytes: &[u8]) -> Result<(), IdeviceError> { pub async fn write(&mut self, bytes: &[u8]) -> Result<(), IdeviceError> {
let chunks = bytes.chunks(MAX_TRANSFER as usize); for chunk in bytes.chunks(MAX_TRANSFER as usize) {
for chunk in chunks {
let header_payload = self.fd.to_le_bytes().to_vec(); let header_payload = self.fd.to_le_bytes().to_vec();
let header_len = header_payload.len() as u64 + AfcPacketHeader::LEN; self.send_packet(AfcOpcode::Write, header_payload, chunk.to_vec())
.await?;
let header = AfcPacketHeader {
magic: super::MAGIC,
entire_len: header_len + chunk.len() as u64,
header_payload_len: header_len,
packet_num: self.client.package_number,
operation: AfcOpcode::Write,
};
self.client.package_number += 1;
let packet = AfcPacket {
header,
header_payload,
payload: chunk.to_vec(),
};
self.client.send(packet).await?;
self.client.read().await?;
} }
Ok(()) Ok(())
} }

View File

@@ -392,10 +392,12 @@ impl Message {
/// # Errors /// # Errors
/// * Various IdeviceError variants for IO and parsing failures /// * Various IdeviceError variants for IO and parsing failures
pub async fn from_reader<R: AsyncRead + Unpin>(reader: &mut R) -> Result<Self, IdeviceError> { pub async fn from_reader<R: AsyncRead + Unpin>(reader: &mut R) -> Result<Self, IdeviceError> {
let mut packet_data: Vec<u8> = Vec::new();
// loop for deal with multiple fragments
let mheader = loop {
let mut buf = [0u8; 32]; let mut buf = [0u8; 32];
reader.read_exact(&mut buf).await?; reader.read_exact(&mut buf).await?;
let header = MessageHeader {
let mheader = MessageHeader {
magic: u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]), magic: u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]),
header_len: u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]), header_len: u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]),
fragment_id: u16::from_le_bytes([buf[8], buf[9]]), fragment_id: u16::from_le_bytes([buf[8], buf[9]]),
@@ -403,13 +405,24 @@ impl Message {
length: u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]), length: u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]),
identifier: u32::from_le_bytes([buf[16], buf[17], buf[18], buf[19]]), identifier: u32::from_le_bytes([buf[16], buf[17], buf[18], buf[19]]),
conversation_index: u32::from_le_bytes([buf[20], buf[21], buf[22], buf[23]]), conversation_index: u32::from_le_bytes([buf[20], buf[21], buf[22], buf[23]]),
channel: u32::from_le_bytes([buf[24], buf[25], buf[26], buf[27]]), //treat both as the negative and positive representation of the channel code in the response
// the same when performing fragmentation
channel: i32::abs(i32::from_le_bytes([buf[24], buf[25], buf[26], buf[27]])) as u32,
expects_reply: u32::from_le_bytes([buf[28], buf[29], buf[30], buf[31]]) == 1, expects_reply: u32::from_le_bytes([buf[28], buf[29], buf[30], buf[31]]) == 1,
}; };
if header.fragment_count > 1 && header.fragment_id == 0 {
let mut buf = [0u8; 16]; // when reading multiple message fragments, the first fragment contains only a message header.
continue;
}
let mut buf = vec![0u8; header.length as usize];
reader.read_exact(&mut buf).await?; reader.read_exact(&mut buf).await?;
packet_data.extend(buf);
if header.fragment_id == header.fragment_count - 1 {
break header;
}
};
// read the payload header
let buf = &packet_data[0..16];
let pheader = PayloadHeader { let pheader = PayloadHeader {
flags: u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]), flags: u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]),
aux_length: u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]), aux_length: u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]),
@@ -417,18 +430,17 @@ impl Message {
buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], buf[15], buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], buf[15],
]), ]),
}; };
let aux = if pheader.aux_length > 0 { let aux = if pheader.aux_length > 0 {
let mut buf = vec![0u8; pheader.aux_length as usize]; let buf = packet_data[16..(16 + pheader.aux_length as usize)].to_vec();
reader.read_exact(&mut buf).await?;
Some(Aux::from_bytes(buf)?) Some(Aux::from_bytes(buf)?)
} else { } else {
None None
}; };
// read the data
let mut buf = vec![0u8; (pheader.total_length - pheader.aux_length as u64) as usize]; let need_len = (pheader.total_length - pheader.aux_length as u64) as usize;
reader.read_exact(&mut buf).await?; let buf = packet_data
[(pheader.aux_length + 16) as usize..pheader.aux_length as usize + 16 + need_len]
.to_vec();
let data = if buf.is_empty() { let data = if buf.is_empty() {
None None
} else { } else {

View File

@@ -7,8 +7,10 @@ use crate::{Idevice, IdeviceError, ReadWrite, RsdService, obf};
#[cfg(feature = "location_simulation")] #[cfg(feature = "location_simulation")]
pub mod location_simulation; pub mod location_simulation;
pub mod message; pub mod message;
pub mod notifications;
pub mod process_control; pub mod process_control;
pub mod remote_server; pub mod remote_server;
pub mod screenshot;
impl RsdService for remote_server::RemoteServerClient<Box<dyn ReadWrite>> { impl RsdService for remote_server::RemoteServerClient<Box<dyn ReadWrite>> {
fn rsd_service_name() -> std::borrow::Cow<'static, str> { fn rsd_service_name() -> std::borrow::Cow<'static, str> {

View File

@@ -0,0 +1,163 @@
//! Notificaitons service client for iOS instruments protocol.
//!
//! Monitor memory and app notifications
use crate::{
IdeviceError, ReadWrite,
dvt::{
message::AuxValue,
remote_server::{Channel, RemoteServerClient},
},
obf,
};
use log::warn;
use plist::Value;
#[derive(Debug)]
pub struct NotificationInfo {
notification_type: String,
mach_absolute_time: i64,
exec_name: String,
app_name: String,
pid: u32,
state_description: String,
}
pub struct NotificationsClient<'a, R: ReadWrite> {
/// The underlying channel used for communication
pub channel: Channel<'a, R>,
}
impl<'a, R: ReadWrite> NotificationsClient<'a, R> {
/// Opens a new channel on the remote server client for app notifications
///
/// # Arguments
/// * `client` - The remote server client to connect with
///
/// # Returns
/// The client on success, IdeviceError on failure
pub async fn new(client: &'a mut RemoteServerClient<R>) -> Result<Self, IdeviceError> {
let channel = client
.make_channel(obf!(
"com.apple.instruments.server.services.mobilenotifications"
))
.await?; // Drop `&mut client` before continuing
Ok(Self { channel })
}
/// set the applicaitons and memory notifications enabled
pub async fn start_notifications(&mut self) -> Result<(), IdeviceError> {
let application_method = Value::String("setApplicationStateNotificationsEnabled:".into());
self.channel
.call_method(
Some(application_method),
Some(vec![AuxValue::archived_value(true)]),
false,
)
.await?;
let memory_method = Value::String("setMemoryNotificationsEnabled:".into());
self.channel
.call_method(
Some(memory_method),
Some(vec![AuxValue::archived_value(true)]),
false,
)
.await?;
Ok(())
}
/// Reads the next notification from the service
pub async fn get_notification(&mut self) -> Result<NotificationInfo, IdeviceError> {
let message = self.channel.read_message().await?;
let mut notification = NotificationInfo {
notification_type: "".to_string(),
mach_absolute_time: 0,
exec_name: String::new(),
app_name: String::new(),
pid: 0,
state_description: String::new(),
};
if let Some(aux) = message.aux {
for v in aux.values {
match v {
AuxValue::Array(a) => match ns_keyed_archive::decode::from_bytes(&a) {
Ok(archive) => {
if let Some(dict) = archive.into_dictionary() {
for (key, value) in dict.into_iter() {
match key.as_str() {
"mach_absolute_time" => {
if let Value::Integer(time) = value {
notification.mach_absolute_time =
time.as_signed().unwrap_or(0);
}
}
"execName" => {
if let Value::String(name) = value {
notification.exec_name = name;
}
}
"appName" => {
if let Value::String(name) = value {
notification.app_name = name;
}
}
"pid" => {
if let Value::Integer(pid) = value {
notification.pid =
pid.as_unsigned().unwrap_or(0) as u32;
}
}
"state_description" => {
if let Value::String(desc) = value {
notification.state_description = desc;
}
}
_ => {
warn!("Unknown notificaton key: {} = {:?}", key, value);
}
}
}
}
}
Err(e) => {
warn!("Failed to decode archive: {:?}", e);
}
},
_ => {
warn!("Non-array aux value: {:?}", v);
}
}
}
}
if let Some(Value::String(data)) = message.data {
notification.notification_type = data;
Ok(notification)
} else {
Err(IdeviceError::UnexpectedResponse)
}
}
/// set the applicaitons and memory notifications disable
pub async fn stop_notifications(&mut self) -> Result<(), IdeviceError> {
let application_method = Value::String("setApplicationStateNotificationsEnabled:".into());
self.channel
.call_method(
Some(application_method),
Some(vec![AuxValue::archived_value(false)]),
false,
)
.await?;
let memory_method = Value::String("setMemoryNotificationsEnabled:".into());
self.channel
.call_method(
Some(memory_method),
Some(vec![AuxValue::archived_value(false)]),
false,
)
.await?;
Ok(())
}
}

View File

@@ -201,6 +201,7 @@ impl<R: ReadWrite> RemoteServerClient<R> {
let message = Message::new(mheader, pheader, aux, data); let message = Message::new(mheader, pheader, aux, data);
debug!("Sending message: {message:#?}"); debug!("Sending message: {message:#?}");
self.idevice.write_all(&message.serialize()).await?; self.idevice.write_all(&message.serialize()).await?;
self.idevice.flush().await?; self.idevice.flush().await?;

View File

@@ -0,0 +1,64 @@
//! Screenshot service client for iOS instruments protocol.
//!
//! This module provides a client for interacting with the screenshot service
//! on iOS devices through the instruments protocol. It allows taking screenshots from the device.
//!
use plist::Value;
use crate::{
IdeviceError, ReadWrite,
dvt::remote_server::{Channel, RemoteServerClient},
obf,
};
/// Client for take screenshot operations on iOS devices
///
/// Provides methods for take screnn_shot through the
/// instruments protocol. Each instance maintains its own communication channel.
pub struct ScreenshotClient<'a, R: ReadWrite> {
/// The underlying channel for communication
channel: Channel<'a, R>,
}
impl<'a, R: ReadWrite> ScreenshotClient<'a, R> {
/// Creates a new ScreenshotClient
///
/// # Arguments
/// * `client` - The base RemoteServerClient to use
///
/// # Returns
/// * `Ok(ScreenshotClient)` - Connected client instance
/// * `Err(IdeviceError)` - If channel creation fails
///
/// # Errors
/// * Propagates errors from channel creation
pub async fn new(client: &'a mut RemoteServerClient<R>) -> Result<Self, IdeviceError> {
let channel = client
.make_channel(obf!("com.apple.instruments.server.services.screenshot"))
.await?; // Drop `&mut client` before continuing
Ok(Self { channel })
}
/// Take screenshot from the device
///
/// # Returns
/// * `Ok(Vec<u8>)` - the bytes of the screenshot
/// * `Err(IdeviceError)` - If communication fails
///
/// # Errors
/// * `IdeviceError::UnexpectedResponse` if server response is invalid
/// * Other communication or serialization errors
pub async fn take_screenshot(&mut self) -> Result<Vec<u8>, IdeviceError> {
let method = Value::String("takeScreenshot".into());
self.channel.call_method(Some(method), None, true).await?;
let msg = self.channel.read_message().await?;
match msg.data {
Some(Value::Data(data)) => Ok(data),
_ => Err(IdeviceError::UnexpectedResponse),
}
}
}

View File

@@ -0,0 +1,91 @@
//! mobileactivationd activates iOS devices.
//! This isn't a normal service, as it requires a new connection for each request.
//! As such, this service requires a provider itself, instead of temporary usage of one.
use plist::Dictionary;
use crate::{Idevice, IdeviceError, IdeviceService, lockdown::LockdownClient, obf};
pub struct MobileActivationdClient<'a> {
provider: &'a dyn crate::provider::IdeviceProvider,
}
/// Internal structure for temporary service connections.
/// This struct exists to take advantage of the service trait.
struct MobileActivationdInternal {
pub idevice: Idevice,
}
impl IdeviceService for MobileActivationdInternal {
/// Returns the service name as registered with lockdownd
fn service_name() -> std::borrow::Cow<'static, str> {
obf!("com.apple.mobileactivationd")
}
async fn from_stream(idevice: Idevice) -> Result<Self, crate::IdeviceError> {
Ok(Self::new(idevice))
}
}
impl MobileActivationdInternal {
fn new(idevice: Idevice) -> Self {
Self { idevice }
}
}
impl<'a> MobileActivationdClient<'a> {
pub fn new(provider: &'a dyn crate::provider::IdeviceProvider) -> Self {
Self { provider }
}
pub async fn state(&self) -> Result<String, IdeviceError> {
if let Ok(res) = self.send_command("GetActivationStateRequest", None).await
&& let Some(v) = res.get("Value").and_then(|x| x.as_string())
{
Ok(v.to_string())
} else {
let mut lc = LockdownClient::connect(self.provider).await?;
lc.start_session(&self.provider.get_pairing_file().await?)
.await?;
let res = lc.get_value(Some("ActivationState"), None).await?;
if let Some(v) = res.as_string() {
Ok(v.to_string())
} else {
Err(IdeviceError::UnexpectedResponse)
}
}
}
pub async fn activated(&self) -> Result<bool, IdeviceError> {
Ok(self.state().await? == "Activated")
}
/// Deactivates the device.
/// Protocol gives no response on whether it worked or not, so good luck
pub async fn deactivate(&self) -> Result<(), IdeviceError> {
self.send_command("DeactivateRequest", None).await?;
Ok(())
}
async fn send_command(
&self,
command: impl Into<String>,
value: Option<&str>,
) -> Result<Dictionary, IdeviceError> {
let mut service = self.service_connect().await?;
let command = command.into();
let req = crate::plist!({
"Command": command,
"Value":? value,
});
service.send_plist(req).await?;
service.read_plist().await
}
async fn service_connect(&self) -> Result<Idevice, IdeviceError> {
Ok(MobileActivationdInternal::connect(self.provider)
.await?
.idevice)
}
}

View File

@@ -29,6 +29,8 @@ pub mod lockdown;
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 = "mobileactivationd")]
pub mod mobileactivationd;
#[cfg(feature = "mobilebackup2")] #[cfg(feature = "mobilebackup2")]
pub mod mobilebackup2; pub mod mobilebackup2;
#[cfg(feature = "syslog_relay")] #[cfg(feature = "syslog_relay")]
@@ -43,7 +45,12 @@ pub mod remote_pairing;
pub mod restore_service; pub mod restore_service;
#[cfg(feature = "rsd")] #[cfg(feature = "rsd")]
pub mod rsd; pub mod rsd;
#[cfg(feature = "screenshotr")]
pub mod screenshotr;
#[cfg(feature = "springboardservices")] #[cfg(feature = "springboardservices")]
pub mod springboardservices; pub mod springboardservices;
#[cfg(feature = "syslog_relay")] #[cfg(feature = "syslog_relay")]
pub mod syslog_relay; pub mod syslog_relay;
#[cfg(feature = "location_simulation")]
pub mod simulate_location;

View File

@@ -0,0 +1,123 @@
//! iOS screenshotr service client
//!
//! Provides functionality for interacting with the screenshot service on iOS devices below iOS 17,
//! which allows taking screenshots.
use crate::{Idevice, IdeviceError, IdeviceService, obf};
use log::{debug, warn};
use std::borrow::Cow;
use tokio::io::AsyncReadExt;
pub struct ScreenshotService {
/// Underlying device connection
pub idevice: Idevice,
}
impl IdeviceService for ScreenshotService {
fn service_name() -> Cow<'static, str> {
obf!("com.apple.mobile.screenshotr")
}
async fn from_stream(idevice: Idevice) -> Result<Self, IdeviceError> {
let mut client = Self::new(idevice);
// Perform DeviceLink handshake first
client.dl_version_exchange().await?;
Ok(client)
}
}
impl ScreenshotService {
pub fn new(idevice: Idevice) -> Self {
Self { idevice }
}
async fn dl_version_exchange(&mut self) -> Result<(), IdeviceError> {
debug!("Starting DeviceLink version exchange");
// 1) Receive DLMessageVersionExchange
let (msg, _arr) = self.receive_dl_message().await?;
if msg != "DLMessageVersionExchange" {
warn!("Expected DLMessageVersionExchange, got {msg}");
return Err(IdeviceError::UnexpectedResponse);
}
// 2) Send DLVersionsOk with version 400
let out = vec![
plist::Value::String("DLMessageVersionExchange".into()),
plist::Value::String("DLVersionsOk".into()),
plist::Value::Integer(400u64.into()),
];
self.send_dl_array(out).await?;
// 3) Receive DLMessageDeviceReady
let (msg2, _arr2) = self.receive_dl_message().await?;
if msg2 != "DLMessageDeviceReady" {
warn!("Expected DLMessageDeviceReady, got {msg2}");
return Err(IdeviceError::UnexpectedResponse);
}
Ok(())
}
/// Sends a raw DL array as binary plist
async fn send_dl_array(&mut self, array: Vec<plist::Value>) -> Result<(), IdeviceError> {
self.idevice.send_bplist(plist::Value::Array(array)).await
}
/// Receives any DL* message and returns (message_tag, full_array_value)
pub async fn receive_dl_message(&mut self) -> Result<(String, plist::Value), IdeviceError> {
if let Some(socket) = &mut self.idevice.socket {
let mut buf = [0u8; 4];
socket.read_exact(&mut buf).await?;
let len = u32::from_be_bytes(buf);
let mut body = vec![0; len as usize];
socket.read_exact(&mut body).await?;
let value: plist::Value = plist::from_bytes(&body)?;
if let plist::Value::Array(arr) = &value
&& let Some(plist::Value::String(tag)) = arr.first()
{
return Ok((tag.clone(), value));
}
warn!("Invalid DL message format");
Err(IdeviceError::UnexpectedResponse)
} else {
Err(IdeviceError::NoEstablishedConnection)
}
}
pub async fn take_screenshot(&mut self) -> Result<Vec<u8>, IdeviceError> {
// Send DLMessageTakeScreenshot
let message_type_dict = crate::plist!(dict {
"MessageType": "ScreenShotRequest"
});
let out = vec![
plist::Value::String("DLMessageProcessMessage".into()),
plist::Value::Dictionary(message_type_dict),
];
self.send_dl_array(out).await?;
// Receive DLMessageScreenshotData
let (msg, value) = self.receive_dl_message().await?;
if msg != "DLMessageProcessMessage" {
warn!("Expected DLMessageProcessMessage, got {msg}");
return Err(IdeviceError::UnexpectedResponse);
}
if let plist::Value::Array(arr) = &value
&& arr.len() == 2
{
if let Some(plist::Value::Dictionary(dict)) = arr.get(1) {
if let Some(plist::Value::Data(data)) = dict.get("ScreenShotData") {
Ok(data.clone())
} else {
warn!("Invalid ScreenShotData format");
Err(IdeviceError::UnexpectedResponse)
}
} else {
warn!("Invalid DLMessageScreenshotData format");
Err(IdeviceError::UnexpectedResponse)
}
} else {
warn!("Invalid DLMessageScreenshotData format");
Err(IdeviceError::UnexpectedResponse)
}
}
}

View File

@@ -0,0 +1,38 @@
use crate::{Idevice, IdeviceError, IdeviceService, obf};
pub struct LocationSimulationService {
idevice: Idevice,
}
impl IdeviceService for LocationSimulationService {
fn service_name() -> std::borrow::Cow<'static, str> {
obf!("com.apple.dt.simulatelocation")
}
async fn from_stream(idevice: Idevice) -> Result<Self, IdeviceError> {
Ok(Self::new(idevice))
}
}
impl LocationSimulationService {
pub fn new(idevice: Idevice) -> Self {
Self { idevice }
}
pub async fn clear(&mut self) -> Result<(), IdeviceError> {
let message: [u8; 4] = [0x00, 0x00, 0x00, 0x01];
self.idevice.send_raw(&message).await?;
Ok(())
}
pub async fn set(&mut self, latitude: &str, longtiude: &str) -> Result<(), IdeviceError> {
let message: [u8; 4] = [0x00, 0x00, 0x00, 0x00];
let latitude_len = latitude.len() as u32;
let longtiude_len = longtiude.len() as u32;
let latitude_bytes = [&latitude_len.to_be_bytes(), latitude.as_bytes()].concat();
let longitude_bytes = [&longtiude_len.to_be_bytes(), longtiude.as_bytes()].concat();
let data = [&message[..], &latitude_bytes[..], &longitude_bytes[..]].concat();
self.idevice.send_raw(data.as_slice()).await?;
Ok(())
}
}

View File

@@ -1,306 +0,0 @@
//! High-level install/upgrade helpers
//!
//! This module provides convenient wrappers that mirror ideviceinstaller's
//! behavior by uploading a package to `PublicStaging` via AFC and then
//! issuing `Install`/`Upgrade` commands through InstallationProxy.
//!
//! Notes:
//! - The package path used by InstallationProxy must be a path inside the
//! AFC jail (e.g. `PublicStaging/<name>`)
//! - For `.ipa` files, we upload the whole file to `PublicStaging/<file_name>`
//! - For directories (developer bundles), we recursively mirror the directory
//! into `PublicStaging/<dir_name>` and pass that directory path.
use std::path::Path;
use crate::{
IdeviceError, IdeviceService,
provider::IdeviceProvider,
services::{
afc::{AfcClient, opcode::AfcFopenMode},
installation_proxy::InstallationProxyClient,
},
};
const PUBLIC_STAGING: &str = "PublicStaging";
/// Result of a prepared upload, containing the remote path to use in Install/Upgrade
struct UploadedPackageInfo {
/// Path inside the AFC jail for InstallationProxy `PackagePath`
remote_package_path: String,
}
/// Ensure `PublicStaging` exists on device via AFC
async fn ensure_public_staging(afc: &mut AfcClient) -> Result<(), IdeviceError> {
// Try to stat and if it fails, create directory
match afc.get_file_info(PUBLIC_STAGING).await {
Ok(_) => Ok(()),
Err(_) => afc.mk_dir(PUBLIC_STAGING).await,
}
}
/// Upload a single file to a destination path on device using AFC
async fn afc_upload_file(
afc: &mut AfcClient,
local_path: &Path,
remote_path: &str,
) -> Result<(), IdeviceError> {
let mut fd = afc.open(remote_path, AfcFopenMode::WrOnly).await?;
let bytes = tokio::fs::read(local_path).await?;
fd.write(&bytes).await?;
fd.close().await
}
/// Recursively upload a directory to device via AFC (mirror contents)
async fn afc_upload_dir(
afc: &mut AfcClient,
local_dir: &Path,
remote_dir: &str,
) -> Result<(), IdeviceError> {
use std::collections::VecDeque;
afc.mk_dir(remote_dir).await.ok();
let mut queue: VecDeque<(std::path::PathBuf, String)> = VecDeque::new();
queue.push_back((local_dir.to_path_buf(), remote_dir.to_string()));
while let Some((cur_local, cur_remote)) = queue.pop_front() {
let mut rd = tokio::fs::read_dir(&cur_local).await?;
while let Some(entry) = rd.next_entry().await? {
let meta = entry.metadata().await?;
let name = entry.file_name();
let name = name.to_string_lossy().into_owned();
if name == "." || name == ".." {
continue;
}
let child_local = entry.path();
let child_remote = format!("{}/{}", cur_remote, name);
if meta.is_dir() {
afc.mk_dir(&child_remote).await.ok();
queue.push_back((child_local, child_remote));
} else if meta.is_file() {
afc_upload_file(afc, &child_local, &child_remote).await?;
}
}
}
Ok(())
}
/// Upload a package to `PublicStaging` and return its InstallationProxy path
///
/// - If `local_path` is a file, it will be uploaded to `PublicStaging/<name>`
/// - If it is a directory, it will be mirrored to `PublicStaging/<dir_name>`
async fn upload_package_to_public_staging<P: AsRef<Path>>(
provider: &dyn IdeviceProvider,
local_path: P,
) -> Result<UploadedPackageInfo, IdeviceError> {
// Connect to AFC via the generic service connector
let mut afc = AfcClient::connect(provider).await?;
ensure_public_staging(&mut afc).await?;
let local_path = local_path.as_ref();
let file_name: String = local_path
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.ok_or_else(|| IdeviceError::InvalidArgument)?;
let remote_path = format!("{}/{}", PUBLIC_STAGING, file_name);
let meta = tokio::fs::metadata(local_path).await?;
if meta.is_dir() {
afc_upload_dir(&mut afc, local_path, &remote_path).await?;
} else {
afc_upload_file(&mut afc, local_path, &remote_path).await?;
}
Ok(UploadedPackageInfo {
remote_package_path: remote_path,
})
}
/// Install an application by first uploading the local package and then invoking InstallationProxy.
///
/// - Accepts a local file path or directory path.
/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults.
pub async fn install_package<P: AsRef<Path>>(
provider: &dyn IdeviceProvider,
local_path: P,
options: Option<plist::Value>,
) -> Result<(), IdeviceError> {
let UploadedPackageInfo {
remote_package_path,
} = upload_package_to_public_staging(provider, local_path).await?;
let mut inst = InstallationProxyClient::connect(provider).await?;
inst.install(remote_package_path, options).await
}
/// Upgrade an application by first uploading the local package and then invoking InstallationProxy.
///
/// - Accepts a local file path or directory path.
/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults.
pub async fn upgrade_package<P: AsRef<Path>>(
provider: &dyn IdeviceProvider,
local_path: P,
options: Option<plist::Value>,
) -> Result<(), IdeviceError> {
let UploadedPackageInfo {
remote_package_path,
} = upload_package_to_public_staging(provider, local_path).await?;
let mut inst = InstallationProxyClient::connect(provider).await?;
inst.upgrade(remote_package_path, options).await
}
/// Same as `install_package` but providing a callback that receives `(percent_complete, state)`
/// updates while InstallationProxy performs the operation.
pub async fn install_package_with_callback<P: AsRef<Path>, Fut, S>(
provider: &dyn IdeviceProvider,
local_path: P,
options: Option<plist::Value>,
callback: impl Fn((u64, S)) -> Fut,
state: S,
) -> Result<(), IdeviceError>
where
Fut: std::future::Future<Output = ()>,
S: Clone,
{
let UploadedPackageInfo {
remote_package_path,
} = upload_package_to_public_staging(provider, local_path).await?;
let mut inst = InstallationProxyClient::connect(provider).await?;
inst.install_with_callback(remote_package_path, options, callback, state)
.await
}
/// Same as `upgrade_package` but providing a callback that receives `(percent_complete, state)`
/// updates while InstallationProxy performs the operation.
pub async fn upgrade_package_with_callback<P: AsRef<Path>, Fut, S>(
provider: &dyn IdeviceProvider,
local_path: P,
options: Option<plist::Value>,
callback: impl Fn((u64, S)) -> Fut,
state: S,
) -> Result<(), IdeviceError>
where
Fut: std::future::Future<Output = ()>,
S: Clone,
{
let UploadedPackageInfo {
remote_package_path,
} = upload_package_to_public_staging(provider, local_path).await?;
let mut inst = InstallationProxyClient::connect(provider).await?;
inst.upgrade_with_callback(remote_package_path, options, callback, state)
.await
}
/// Upload raw bytes to `PublicStaging/<remote_name>` via AFC and return the remote package path.
///
/// - This is useful when the package is not present on disk or is generated in-memory.
async fn upload_bytes_to_public_staging(
provider: &dyn IdeviceProvider,
data: impl AsRef<[u8]>,
remote_name: &str,
) -> Result<UploadedPackageInfo, IdeviceError> {
// Connect to AFC
let mut afc = AfcClient::connect(provider).await?;
ensure_public_staging(&mut afc).await?;
let remote_path = format!("{}/{}", PUBLIC_STAGING, remote_name);
let mut fd = afc.open(&remote_path, AfcFopenMode::WrOnly).await?;
fd.write(data.as_ref()).await?;
fd.close().await?;
Ok(UploadedPackageInfo {
remote_package_path: remote_path,
})
}
/// Install an application from raw bytes by first uploading them to `PublicStaging` and then
/// invoking InstallationProxy `Install`.
///
/// - `remote_name` determines the remote filename under `PublicStaging`.
/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults.
pub async fn install_bytes(
provider: &dyn IdeviceProvider,
data: impl AsRef<[u8]>,
remote_name: &str,
options: Option<plist::Value>,
) -> Result<(), IdeviceError> {
let UploadedPackageInfo {
remote_package_path,
} = upload_bytes_to_public_staging(provider, data, remote_name).await?;
let mut inst = InstallationProxyClient::connect(provider).await?;
inst.install(remote_package_path, options).await
}
/// Same as `install_bytes` but providing a callback that receives `(percent_complete, state)`
/// updates while InstallationProxy performs the install operation.
///
/// Tip:
/// - When embedding assets into the binary, you can pass `include_bytes!("path/to/app.ipa")`
/// as the `data` argument and choose a desired `remote_name` (e.g. `"MyApp.ipa"`).
pub async fn install_bytes_with_callback<Fut, S>(
provider: &dyn IdeviceProvider,
data: impl AsRef<[u8]>,
remote_name: &str,
options: Option<plist::Value>,
callback: impl Fn((u64, S)) -> Fut,
state: S,
) -> Result<(), IdeviceError>
where
Fut: std::future::Future<Output = ()>,
S: Clone,
{
let UploadedPackageInfo {
remote_package_path,
} = upload_bytes_to_public_staging(provider, data, remote_name).await?;
let mut inst = InstallationProxyClient::connect(provider).await?;
inst.install_with_callback(remote_package_path, options, callback, state)
.await
}
/// Upgrade an application from raw bytes by first uploading them to `PublicStaging` and then
/// invoking InstallationProxy `Upgrade`.
///
/// - `remote_name` determines the remote filename under `PublicStaging`.
/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults.
pub async fn upgrade_bytes(
provider: &dyn IdeviceProvider,
data: impl AsRef<[u8]>,
remote_name: &str,
options: Option<plist::Value>,
) -> Result<(), IdeviceError> {
let UploadedPackageInfo {
remote_package_path,
} = upload_bytes_to_public_staging(provider, data, remote_name).await?;
let mut inst = InstallationProxyClient::connect(provider).await?;
inst.upgrade(remote_package_path, options).await
}
/// Same as `upgrade_bytes` but providing a callback that receives `(percent_complete, state)`
/// updates while InstallationProxy performs the upgrade operation.
///
/// Tip:
/// - When embedding assets into the binary, you can pass `include_bytes!("path/to/app.ipa")`
/// as the `data` argument and choose a desired `remote_name` (e.g. `"MyApp.ipa"`).
pub async fn upgrade_bytes_with_callback<Fut, S>(
provider: &dyn IdeviceProvider,
data: impl AsRef<[u8]>,
remote_name: &str,
options: Option<plist::Value>,
callback: impl Fn((u64, S)) -> Fut,
state: S,
) -> Result<(), IdeviceError>
where
Fut: std::future::Future<Output = ()>,
S: Clone,
{
let UploadedPackageInfo {
remote_package_path,
} = upload_bytes_to_public_staging(provider, data, remote_name).await?;
let mut inst = InstallationProxyClient::connect(provider).await?;
inst.upgrade_with_callback(remote_package_path, options, callback, state)
.await
}

View File

@@ -0,0 +1,273 @@
use std::{io::Cursor, path::Path};
use async_zip::base::read::seek::ZipFileReader;
use futures::AsyncReadExt as _;
use tokio::io::{AsyncBufRead, AsyncSeek, BufReader};
use crate::{
IdeviceError, IdeviceService,
afc::{AfcClient, opcode::AfcFopenMode},
plist,
provider::IdeviceProvider,
};
pub const PUBLIC_STAGING: &str = "PublicStaging";
pub const IPCC_REMOTE_FILE: &str = "idevice.ipcc";
pub const IPA_REMOTE_FILE: &str = "idevice.ipa";
/// Result of a prepared upload, containing the remote path to use in Install/Upgrade
pub struct InstallPackage {
/// Path inside the AFC jail for InstallationProxy `PackagePath`
pub remote_package_path: String,
// Each package type has a special option that has to be passed
pub options: plist::Value,
}
/// Represent the type of package being installed.
pub enum PackageType {
Ipcc, // Carrier bundle package
// an IPA package needs the build id to be installed
Ipa(String), // iOS app package
Unknown,
}
impl PackageType {
pub fn get_remote_file(&self) -> Result<&'static str, IdeviceError> {
match self {
Self::Ipcc => Ok(IPCC_REMOTE_FILE),
Self::Ipa(_) => Ok(IPA_REMOTE_FILE),
Self::Unknown => Err(IdeviceError::InstallationProxyOperationFailed(
"invalid package".into(),
)),
}
}
}
/// Ensure `PublicStaging` exists on device via AFC
pub async fn ensure_public_staging(afc: &mut AfcClient) -> Result<(), IdeviceError> {
// Try to stat and if it fails, create directory
match afc.get_file_info(PUBLIC_STAGING).await {
Ok(_) => Ok(()),
Err(_) => afc.mk_dir(PUBLIC_STAGING).await,
}
}
// Get the bundle id of a package by looping through it's files and looking inside of the
// `Info.plist`
pub async fn get_bundle_id<T>(file: &mut T) -> Result<String, IdeviceError>
where
T: AsyncBufRead + AsyncSeek + Unpin,
{
let mut zip_file = ZipFileReader::with_tokio(file).await?;
for i in 0..zip_file.file().entries().len() {
let mut entry_reader = zip_file.reader_with_entry(i).await?;
let entry = entry_reader.entry();
let inner_file_path = entry
.filename()
.as_str()
.map_err(|_| IdeviceError::Utf8Error)?
.trim_end_matches('/');
let path_segments_count = inner_file_path.split('/').count();
// there's multiple `Info.plist` files, we only need the one that's in the root of the
// package
//
// 1 2 3
// which is in this case: Playload -> APP_NAME.app -> Info.plist
if inner_file_path.ends_with("Info.plist") && path_segments_count == 3 {
let mut info_plist_bytes = Vec::new();
entry_reader.read_to_end(&mut info_plist_bytes).await?;
let info_plist: plist::Value = plist::from_bytes(&info_plist_bytes)?;
if let Some(bundle_id) = info_plist
.as_dictionary()
.and_then(|dict| dict.get("CFBundleIdentifier"))
.and_then(|v| v.as_string())
{
return Ok(bundle_id.to_string());
}
}
}
Err(IdeviceError::NotFound)
}
/// Determines the type of package based on its content (IPA or IPCC).
pub async fn determine_package_type<P: AsRef<[u8]>>(
package: &P,
) -> Result<PackageType, IdeviceError> {
let mut package_cursor = BufReader::new(Cursor::new(package.as_ref()));
let mut archive = ZipFileReader::with_tokio(&mut package_cursor).await?;
// the first index is the first folder name, which is probably `Payload`
//
// we need the folder inside of that `Payload`, which has an extension that we can
// determine the type of the package from it, hence the second index
let inside_folder = archive.reader_with_entry(1).await?;
let folder_name = inside_folder
.entry()
.filename()
.as_str()
.map_err(|_| IdeviceError::Utf8Error)?
.split('/')
.nth(1)
// only if the package does not have anything inside of the `Payload` folder
.ok_or(async_zip::error::ZipError::EntryIndexOutOfBounds)?
.to_string();
let bundle_id = get_bundle_id(&mut package_cursor).await?;
if folder_name.ends_with(".bundle") {
Ok(PackageType::Ipcc)
} else if folder_name.ends_with(".app") {
Ok(PackageType::Ipa(bundle_id))
} else {
Ok(PackageType::Unknown)
}
}
/// Upload a single file to a destination path on device using AFC
pub async fn afc_upload_file<F: AsRef<[u8]>>(
afc: &mut AfcClient,
file: F,
remote_path: &str,
) -> Result<(), IdeviceError> {
let mut fd = afc.open(remote_path, AfcFopenMode::WrOnly).await?;
fd.write(file.as_ref()).await?;
fd.close().await
}
/// Recursively upload a directory to device via AFC (mirror contents)
pub async fn afc_upload_dir(
afc: &mut AfcClient,
local_dir: &Path,
remote_dir: &str,
) -> Result<(), IdeviceError> {
use std::collections::VecDeque;
afc.mk_dir(remote_dir).await.ok();
let mut queue: VecDeque<(std::path::PathBuf, String)> = VecDeque::new();
queue.push_back((local_dir.to_path_buf(), remote_dir.to_string()));
while let Some((cur_local, cur_remote)) = queue.pop_front() {
let mut rd = tokio::fs::read_dir(&cur_local).await?;
while let Some(entry) = rd.next_entry().await? {
let meta = entry.metadata().await?;
let name = entry.file_name();
let name = name.to_string_lossy().into_owned();
if name == "." || name == ".." {
continue;
}
let child_local = entry.path();
let child_remote = format!("{cur_remote}/{name}");
if meta.is_dir() {
afc.mk_dir(&child_remote).await.ok();
queue.push_back((child_local, child_remote));
} else if meta.is_file() {
afc_upload_file(afc, tokio::fs::read(&child_local).await?, &child_remote).await?;
}
}
}
Ok(())
}
/// Upload a file to `PublicStaging` and return its InstallationProxy path
async fn upload_file_to_public_staging<P: AsRef<[u8]>>(
provider: &dyn IdeviceProvider,
file: P,
) -> Result<InstallPackage, IdeviceError> {
// Connect to AFC via the generic service connector
let mut afc = AfcClient::connect(provider).await?;
ensure_public_staging(&mut afc).await?;
let file = file.as_ref();
let package_type = determine_package_type(&file).await?;
let remote_path = format!("{PUBLIC_STAGING}/{}", package_type.get_remote_file()?);
afc_upload_file(&mut afc, file, &remote_path).await?;
let options = match package_type {
PackageType::Ipcc => plist!({"PackageType": "CarrierBundle"}),
PackageType::Ipa(build_id) => plist!({"CFBundleIdentifier": build_id}),
PackageType::Unknown => plist!({}),
};
Ok(InstallPackage {
remote_package_path: remote_path,
options,
})
}
/// Recursively Upload a directory of file to `PublicStaging`
async fn upload_dir_to_public_staging<P: AsRef<Path>>(
provider: &dyn IdeviceProvider,
file: P,
) -> Result<InstallPackage, IdeviceError> {
let mut afc = AfcClient::connect(provider).await?;
ensure_public_staging(&mut afc).await?;
let file = file.as_ref();
let remote_path = format!("{PUBLIC_STAGING}/{IPA_REMOTE_FILE}");
afc_upload_dir(&mut afc, file, &remote_path).await?;
Ok(InstallPackage {
remote_package_path: remote_path,
options: plist!({"PackageType": "Developer"}),
})
}
pub async fn prepare_file_upload(
provider: &dyn IdeviceProvider,
data: impl AsRef<[u8]>,
caller_options: Option<plist::Value>,
) -> Result<InstallPackage, IdeviceError> {
let InstallPackage {
remote_package_path,
options,
} = upload_file_to_public_staging(provider, data).await?;
let full_options = plist!({
:<? caller_options,
:< options,
});
Ok(InstallPackage {
remote_package_path,
options: full_options,
})
}
pub async fn prepare_dir_upload(
provider: &dyn IdeviceProvider,
local_path: impl AsRef<Path>,
caller_options: Option<plist::Value>,
) -> Result<InstallPackage, IdeviceError> {
let InstallPackage {
remote_package_path,
options,
} = upload_dir_to_public_staging(provider, &local_path).await?;
let full_options = plist!({
:<? caller_options,
:< options,
});
Ok(InstallPackage {
remote_package_path,
options: full_options,
})
}

View File

@@ -0,0 +1,186 @@
//! High-level install/upgrade helpers
//!
//! This module provides convenient wrappers that mirror ideviceinstaller's
//! behavior by uploading a package to `PublicStaging` via AFC and then
//! issuing `Install`/`Upgrade` commands through InstallationProxy.
//!
//! Notes:
//! - The package path used by InstallationProxy must be a path inside the
//! AFC jail (e.g. `PublicStaging/<name>`)
//! - For `.ipa` files, we upload the whole file to `PublicStaging/<file_name>`
//! - For directories (developer bundles), we recursively mirror the directory
//! into `PublicStaging/<dir_name>` and pass that directory path.
mod helpers;
use std::path::Path;
use helpers::{InstallPackage, prepare_dir_upload, prepare_file_upload};
use crate::{
IdeviceError, IdeviceService, provider::IdeviceProvider,
services::installation_proxy::InstallationProxyClient,
};
/// Install an application by first uploading the local package and then invoking InstallationProxy.
///
/// - Accepts a local file path or directory path.
/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults.
pub async fn install_package<P: AsRef<Path>>(
provider: &dyn IdeviceProvider,
local_path: P,
options: Option<plist::Value>,
) -> Result<(), IdeviceError> {
install_package_with_callback(provider, local_path, options, |_| async {}, ()).await
}
/// Same as `install_package` but providing a callback that receives `(percent_complete, state)`
/// updates while InstallationProxy performs the operation.
pub async fn install_package_with_callback<P: AsRef<Path>, Fut, S>(
provider: &dyn IdeviceProvider,
local_path: P,
options: Option<plist::Value>,
callback: impl Fn((u64, S)) -> Fut,
state: S,
) -> Result<(), IdeviceError>
where
Fut: std::future::Future<Output = ()>,
S: Clone,
{
let metadata = tokio::fs::metadata(&local_path).await?;
if metadata.is_dir() {
let InstallPackage {
remote_package_path,
options,
} = prepare_dir_upload(provider, local_path, options).await?;
let mut inst = InstallationProxyClient::connect(provider).await?;
inst.upgrade_with_callback(remote_package_path, Some(options), callback, state)
.await
} else {
let data = tokio::fs::read(&local_path).await?;
install_bytes_with_callback(provider, data, options, callback, state).await
}
}
/// Upgrade an application by first uploading the local package and then invoking InstallationProxy.
///
/// - Accepts a local file path or directory path.
/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults.
pub async fn upgrade_package<P: AsRef<Path>>(
provider: &dyn IdeviceProvider,
local_path: P,
options: Option<plist::Value>,
) -> Result<(), IdeviceError> {
upgrade_package_with_callback(provider, local_path, options, |_| async {}, ()).await
}
/// Same as `upgrade_package` but providing a callback that receives `(percent_complete, state)`
/// updates while InstallationProxy performs the operation.
pub async fn upgrade_package_with_callback<P: AsRef<Path>, Fut, S>(
provider: &dyn IdeviceProvider,
local_path: P,
options: Option<plist::Value>,
callback: impl Fn((u64, S)) -> Fut,
state: S,
) -> Result<(), IdeviceError>
where
Fut: std::future::Future<Output = ()>,
S: Clone,
{
let metadata = tokio::fs::metadata(&local_path).await?;
if metadata.is_dir() {
let InstallPackage {
remote_package_path,
options,
} = prepare_dir_upload(provider, local_path, options).await?;
let mut inst = InstallationProxyClient::connect(provider).await?;
inst.upgrade_with_callback(remote_package_path, Some(options), callback, state)
.await
} else {
let data = tokio::fs::read(&local_path).await?;
upgrade_bytes_with_callback(provider, data, options, callback, state).await
}
}
/// Install an application from raw bytes by first uploading them to `PublicStaging` and then
/// invoking InstallationProxy `Install`.
///
/// - `remote_name` determines the remote filename under `PublicStaging`.
/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults.
pub async fn install_bytes(
provider: &dyn IdeviceProvider,
data: impl AsRef<[u8]>,
options: Option<plist::Value>,
) -> Result<(), IdeviceError> {
install_bytes_with_callback(provider, data, options, |_| async {}, ()).await
}
/// Same as `install_bytes` but providing a callback that receives `(percent_complete, state)`
/// updates while InstallationProxy performs the install operation.
///
/// Tip:
/// - When embedding assets into the binary, you can pass `include_bytes!("path/to/app.ipa")`
/// as the `data` argument and choose a desired `remote_name` (e.g. `"MyApp.ipa"`).
pub async fn install_bytes_with_callback<Fut, S>(
provider: &dyn IdeviceProvider,
data: impl AsRef<[u8]>,
options: Option<plist::Value>,
callback: impl Fn((u64, S)) -> Fut,
state: S,
) -> Result<(), IdeviceError>
where
Fut: std::future::Future<Output = ()>,
S: Clone,
{
let InstallPackage {
remote_package_path,
options,
} = prepare_file_upload(provider, data, options).await?;
let mut inst = InstallationProxyClient::connect(provider).await?;
inst.install_with_callback(remote_package_path, Some(options), callback, state)
.await
}
/// Upgrade an application from raw bytes by first uploading them to `PublicStaging` and then
/// invoking InstallationProxy `Upgrade`.
///
/// - `remote_name` determines the remote filename under `PublicStaging`.
/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults.
pub async fn upgrade_bytes(
provider: &dyn IdeviceProvider,
data: impl AsRef<[u8]>,
options: Option<plist::Value>,
) -> Result<(), IdeviceError> {
upgrade_bytes_with_callback(provider, data, options, |_| async {}, ()).await
}
/// Same as `upgrade_bytes` but providing a callback that receives `(percent_complete, state)`
/// updates while InstallationProxy performs the upgrade operation.
///
/// Tip:
/// - When embedding assets into the binary, you can pass `include_bytes!("path/to/app.ipa")`
/// as the `data` argument and choose a desired `remote_name` (e.g. `"MyApp.ipa"`).
pub async fn upgrade_bytes_with_callback<Fut, S>(
provider: &dyn IdeviceProvider,
data: impl AsRef<[u8]>,
options: Option<plist::Value>,
callback: impl Fn((u64, S)) -> Fut,
state: S,
) -> Result<(), IdeviceError>
where
Fut: std::future::Future<Output = ()>,
S: Clone,
{
let InstallPackage {
remote_package_path,
options,
} = prepare_file_upload(provider, data, options).await?;
let mut inst = InstallationProxyClient::connect(provider).await?;
inst.upgrade_with_callback(remote_package_path, Some(options), callback, state)
.await
}

View File

@@ -6,7 +6,7 @@ check-features:
ci-check: build-ffi-native build-tools-native build-cpp build-c ci-check: build-ffi-native build-tools-native build-cpp build-c
cargo clippy --all-targets --all-features -- -D warnings cargo clippy --all-targets --all-features -- -D warnings
cargo fmt -- --check cargo fmt -- --check
macos-ci-check: ci-check xcframework macos-ci-check: ci-check
cd tools && cargo build --release --target x86_64-apple-darwin cd tools && cargo build --release --target x86_64-apple-darwin
windows-ci-check: build-ffi-native build-tools-native build-cpp windows-ci-check: build-ffi-native build-tools-native build-cpp

View File

@@ -129,6 +129,19 @@ path = "src/pcapd.rs"
name = "preboard" name = "preboard"
path = "src/preboard.rs" path = "src/preboard.rs"
[[bin]]
name = "screenshot"
path = "src/screenshot.rs"
[[bin]]
name = "activation"
path = "src/activation.rs"
[[bin]]
name = "notifications"
path = "src/notifications.rs"
[dependencies] [dependencies]
idevice = { path = "../idevice", features = ["full"], default-features = false } idevice = { path = "../idevice", features = ["full"], default-features = false }
tokio = { version = "1.43", features = ["full"] } tokio = { version = "1.43", features = ["full"] }

113
tools/src/activation.rs Normal file
View File

@@ -0,0 +1,113 @@
// Jackson Coxson
use clap::{Arg, Command};
use idevice::{
IdeviceService, lockdown::LockdownClient, mobileactivationd::MobileActivationdClient,
};
mod common;
#[tokio::main]
async fn main() {
env_logger::init();
let matches = Command::new("activation")
.about("mobileactivationd")
.arg(
Arg::new("host")
.long("host")
.value_name("HOST")
.help("IP address of the device"),
)
.arg(
Arg::new("pairing_file")
.long("pairing-file")
.value_name("PATH")
.help("Path to the pairing file"),
)
.arg(
Arg::new("udid")
.value_name("UDID")
.help("UDID of the device (overrides host/pairing file)")
.index(1),
)
.arg(
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.subcommand(Command::new("state").about("Gets the activation state"))
.subcommand(Command::new("deactivate").about("Deactivates the device"))
.get_matches();
if matches.get_flag("about") {
println!("activation - activate the device");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider = match common::get_provider(udid, host, pairing_file, "activation-jkcoxson").await
{
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let activation_client = MobileActivationdClient::new(&*provider);
let mut lc = LockdownClient::connect(&*provider)
.await
.expect("no lockdown");
lc.start_session(&provider.get_pairing_file().await.unwrap())
.await
.expect("no TLS");
let udid = lc
.get_value(Some("UniqueDeviceID"), None)
.await
.expect("no udid")
.into_string()
.unwrap();
if matches.subcommand_matches("state").is_some() {
let s = activation_client.state().await.expect("no state");
println!("Activation State: {s}");
} else if matches.subcommand_matches("deactivate").is_some() {
println!("CAUTION: You are deactivating {udid}, press enter to continue.");
let mut input = String::new();
std::io::stdin().read_line(&mut input).ok();
activation_client.deactivate().await.expect("no deactivate");
// } else if matches.subcommand_matches("accept").is_some() {
// amfi_client
// .accept_developer_mode()
// .await
// .expect("Failed to show");
// } else if matches.subcommand_matches("status").is_some() {
// let status = amfi_client
// .get_developer_mode_status()
// .await
// .expect("Failed to get status");
// println!("Enabled: {status}");
// } else if let Some(matches) = matches.subcommand_matches("state") {
// let uuid: &String = match matches.get_one("uuid") {
// Some(u) => u,
// None => {
// eprintln!("No UUID passed. Invalid usage, pass -h for help");
// return;
// }
// };
// let status = amfi_client
// .trust_app_signer(uuid)
// .await
// .expect("Failed to get state");
// println!("Enabled: {status}");
} else {
eprintln!("Invalid usage, pass -h for help");
}
return;
}

View File

@@ -4,6 +4,8 @@
use clap::{Arg, Command}; use clap::{Arg, Command};
use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake};
use idevice::dvt::location_simulation::LocationSimulationClient;
use idevice::services::simulate_location::LocationSimulationService;
mod common; mod common;
#[tokio::main] #[tokio::main]
@@ -63,9 +65,8 @@ async fn main() {
return; return;
} }
}; };
let proxy = CoreDeviceProxy::connect(&*provider)
.await if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await {
.expect("no core proxy");
let rsd_port = proxy.handshake.server_rsd_port; let rsd_port = proxy.handshake.server_rsd_port;
let adapter = proxy.create_software_tunnel().expect("no software tunnel"); let adapter = proxy.create_software_tunnel().expect("no software tunnel");
@@ -75,17 +76,16 @@ async fn main() {
// Make the connection to RemoteXPC // Make the connection to RemoteXPC
let mut handshake = RsdHandshake::new(stream).await.unwrap(); let mut handshake = RsdHandshake::new(stream).await.unwrap();
let mut ls_client = let mut ls_client = idevice::dvt::remote_server::RemoteServerClient::connect_rsd(
idevice::dvt::remote_server::RemoteServerClient::connect_rsd(&mut adapter, &mut handshake) &mut adapter,
&mut handshake,
)
.await .await
.expect("Failed to connect"); .expect("Failed to connect");
ls_client.read_message(0).await.expect("no read??"); ls_client.read_message(0).await.expect("no read??");
let mut ls_client = LocationSimulationClient::new(&mut ls_client)
let mut ls_client =
idevice::dvt::location_simulation::LocationSimulationClient::new(&mut ls_client)
.await .await
.expect("Unable to get channel for location simulation"); .expect("Unable to get channel for location simulation");
if matches.subcommand_matches("clear").is_some() { if matches.subcommand_matches("clear").is_some() {
ls_client.clear().await.expect("Unable to clear"); ls_client.clear().await.expect("Unable to clear");
println!("Location cleared!"); println!("Location cleared!");
@@ -123,5 +123,45 @@ async fn main() {
} else { } else {
eprintln!("Invalid usage, pass -h for help"); eprintln!("Invalid usage, pass -h for help");
} }
} else {
let mut location_client = match LocationSimulationService::connect(&*provider).await {
Ok(client) => client,
Err(e) => {
eprintln!(
"Unable to connect to simulate_location service: {e} Ensure Developer Disk Image is mounted."
);
return;
}
};
if matches.subcommand_matches("clear").is_some() {
location_client.clear().await.expect("Unable to clear");
println!("Location cleared!");
} else if let Some(matches) = matches.subcommand_matches("set") {
let latitude: &String = match matches.get_one("latitude") {
Some(l) => l,
None => {
eprintln!("No latitude passed! Pass -h for help");
return;
}
};
let longitude: &String = match matches.get_one("longitude") {
Some(l) => l,
None => {
eprintln!("No longitude passed! Pass -h for help");
return;
}
};
location_client
.set(latitude, longitude)
.await
.expect("Failed to set location");
println!("Location set!");
} else {
eprintln!("Invalid usage, pass -h for help");
}
};
return; return;
} }

101
tools/src/notifications.rs Normal file
View File

@@ -0,0 +1,101 @@
// Monitor memory and app notifications
use clap::{Arg, Command};
use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake};
mod common;
#[tokio::main]
async fn main() {
env_logger::init();
let matches = Command::new("notifications")
.about("start notifications")
.arg(
Arg::new("host")
.long("host")
.value_name("HOST")
.help("IP address of the device"),
)
.arg(
Arg::new("pairing_file")
.long("pairing-file")
.value_name("PATH")
.help("Path to the pairing file"),
)
.arg(
Arg::new("udid")
.value_name("UDID")
.help("UDID of the device (overrides host/pairing file)")
.index(1),
)
.arg(
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.get_matches();
if matches.get_flag("about") {
print!("notifications - start notifications to ios device");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider =
match common::get_provider(udid, host, pairing_file, "notifications-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let proxy = CoreDeviceProxy::connect(&*provider)
.await
.expect("no core proxy");
let rsd_port = proxy.handshake.server_rsd_port;
let adapter = proxy.create_software_tunnel().expect("no software tunnel");
let mut adapter = adapter.to_async_handle();
let stream = adapter.connect(rsd_port).await.expect("no RSD connect");
// Make the connection to RemoteXPC
let mut handshake = RsdHandshake::new(stream).await.unwrap();
let mut ts_client =
idevice::dvt::remote_server::RemoteServerClient::connect_rsd(&mut adapter, &mut handshake)
.await
.expect("Failed to connect");
ts_client.read_message(0).await.expect("no read??");
let mut notification_client =
idevice::dvt::notifications::NotificationsClient::new(&mut ts_client)
.await
.expect("Unable to get channel for notifications");
notification_client
.start_notifications()
.await
.expect("Failed to start notifications");
// Handle Ctrl+C gracefully
loop {
tokio::select! {
_ = tokio::signal::ctrl_c() => {
println!("\nShutdown signal received, exiting.");
break;
}
// Branch 2: Wait for the next batch of notifications.
result = notification_client.get_notification() => {
if let Err(e) = result {
eprintln!("Failed to get notifications: {}", e);
} else {
println!("Received notifications: {:#?}", result.unwrap());
}
}
}
}
}

View File

@@ -7,7 +7,6 @@ use idevice::{
}; };
mod common; mod common;
mod pcap;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {

109
tools/src/screenshot.rs Normal file
View File

@@ -0,0 +1,109 @@
use clap::{Arg, Command};
use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake};
use std::fs;
use idevice::screenshotr::ScreenshotService;
mod common;
#[tokio::main]
async fn main() {
env_logger::init();
let matches = Command::new("screen_shot")
.about("take screenshot")
.arg(
Arg::new("host")
.long("host")
.value_name("HOST")
.help("IP address of the device"),
)
.arg(
Arg::new("pairing_file")
.long("pairing-file")
.value_name("PATH")
.help("Path to the pairing file"),
)
.arg(
Arg::new("udid")
.value_name("UDID")
.help("UDID of the device (overrides host/pairing file)")
.index(1),
)
.arg(
Arg::new("output")
.short('o')
.long("output")
.value_name("FILE")
.help("Output file path for the screenshot (default: ./screenshot.png)")
.default_value("screenshot.png"),
)
.arg(
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.get_matches();
if matches.get_flag("about") {
print!("screen_shot - take screenshot from ios device");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let output_path = matches.get_one::<String>("output").unwrap();
let provider =
match common::get_provider(udid, host, pairing_file, "take_screenshot-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let res = if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await {
let rsd_port = proxy.handshake.server_rsd_port;
let adapter = proxy.create_software_tunnel().expect("no software tunnel");
let mut adapter = adapter.to_async_handle();
let stream = adapter.connect(rsd_port).await.expect("no RSD connect");
// Make the connection to RemoteXPC
let mut handshake = RsdHandshake::new(stream).await.unwrap();
let mut ts_client = idevice::dvt::remote_server::RemoteServerClient::connect_rsd(
&mut adapter,
&mut handshake,
)
.await
.expect("Failed to connect");
ts_client.read_message(0).await.expect("no read??");
let mut ts_client = idevice::dvt::screenshot::ScreenshotClient::new(&mut ts_client)
.await
.expect("Unable to get channel for take screenshot");
ts_client
.take_screenshot()
.await
.expect("Failed to take screenshot")
} else {
let mut screenshot_client = match ScreenshotService::connect(&*provider).await {
Ok(client) => client,
Err(e) => {
eprintln!(
"Unable to connect to screenshotr service: {e} Ensure Developer Disk Image is mounted."
);
return;
}
};
screenshot_client.take_screenshot().await.unwrap()
};
match fs::write(output_path, res) {
Ok(_) => println!("Screenshot saved to: {}", output_path),
Err(e) => eprintln!("Failed to write screenshot to file: {}", e),
}
}