Add cpp bindings for image mounter

This commit is contained in:
Jackson Coxson
2025-09-25 10:04:55 -06:00
parent ec81169347
commit 9f7e57bb21
7 changed files with 595 additions and 4 deletions

4
Cargo.lock generated
View File

@@ -1825,9 +1825,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",

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

@@ -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,246 @@
// Jackson Coxson
#include <idevice++/mobile_image_mounter.hpp>
#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(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

@@ -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]