From d08ae68e7805db3dddaef4dfd51db4abfdfdbc7e Mon Sep 17 00:00:00 2001 From: nab138 Date: Sun, 10 Aug 2025 17:51:42 -0400 Subject: [PATCH] Improve API --- Cargo.lock | 1 + README.md | 33 ++++++++---- examples/minimal/Cargo.toml | 1 + examples/minimal/src/main.rs | 32 +++++++---- isideload/Cargo.toml | 4 +- isideload/src/certificate.rs | 6 ++- isideload/src/developer_session.rs | 6 +-- isideload/src/device.rs | 86 +++--------------------------- isideload/src/lib.rs | 43 ++++++++++++++- isideload/src/sideload.rs | 75 ++++++++++++++++++-------- 10 files changed, 158 insertions(+), 129 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 071942d..9419518 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1366,6 +1366,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" name = "minimal" version = "0.1.0" dependencies = [ + "idevice", "isideload", "tokio", ] diff --git a/README.md b/README.md index dd4da74..9be8372 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,10 @@ Then, you can use it like so: ```rs use std::{env, path::PathBuf, sync::Arc}; + +use idevice::usbmuxd::{UsbmuxdAddr, UsbmuxdConnection}; use isideload::{ - AnisetteConfiguration, AppleAccount, DefaultLogger, DeveloperSession, device::list_devices, + AnisetteConfiguration, AppleAccount, DeveloperSession, SideloadConfiguration, sideload::sideload_app, }; @@ -36,11 +38,23 @@ async fn main() { .expect("Please provide the Apple ID to use for installation"); let apple_password = args.get(3).expect("Please provide the Apple ID password"); - // You don't have to use the builtin list_devices method if you don't want to use usbmuxd - // You can use idevice to get the device info however you want - // This is just easier - let device = list_devices().await.unwrap().into_iter().next().unwrap(); - println!("Target device: {}", device.name); + // You don't have to use usbmuxd, you can use any IdeviceProvider + let usbmuxd = UsbmuxdConnection::default().await; + if usbmuxd.is_err() { + panic!("Failed to connect to usbmuxd: {:?}", usbmuxd.err()); + } + let mut usbmuxd = usbmuxd.unwrap(); + + let devs = usbmuxd.get_devices().await.unwrap(); + if devs.is_empty() { + panic!("No devices found"); + } + + let provider = devs + .iter() + .next() + .unwrap() + .to_provider(UsbmuxdAddr::from_env_var().unwrap(), "isideload-demo"); // Change the anisette url and such here // Note that right now only remote anisette servers are supported @@ -63,11 +77,10 @@ async fn main() { let dev_session = DeveloperSession::new(Arc::new(account)); - // This is where certificates, mobileprovision, and anisette data will be stored - let store_dir = std::env::current_dir().unwrap(); + // You can change the machine name, store directory (for certs, anisette data, & provision files), and logger + let config = SideloadConfiguration::default().set_machine_name("isideload-demo".to_string()); - // DefaultLogger just prints to the stdout/stderr, but you can provide your own implementation - sideload_app(DefaultLogger {}, &dev_session, &device, app_path, store_dir) + sideload_app(&provider, &dev_session, app_path, config) .await .unwrap() } diff --git a/examples/minimal/Cargo.toml b/examples/minimal/Cargo.toml index b740c8b..a7d102f 100644 --- a/examples/minimal/Cargo.toml +++ b/examples/minimal/Cargo.toml @@ -6,4 +6,5 @@ publish = false [dependencies] isideload = { path = "../../isideload", features = ["vendored-openssl", "vendored-botan"] } +idevice = { version = "0.1.37", features = ["usbmuxd"]} tokio = { version = "1", features = ["macros", "rt-multi-thread"] } diff --git a/examples/minimal/src/main.rs b/examples/minimal/src/main.rs index 013c566..ea26e54 100644 --- a/examples/minimal/src/main.rs +++ b/examples/minimal/src/main.rs @@ -1,7 +1,8 @@ use std::{env, path::PathBuf, sync::Arc}; +use idevice::usbmuxd::{UsbmuxdAddr, UsbmuxdConnection}; use isideload::{ - AnisetteConfiguration, AppleAccount, DefaultLogger, DeveloperSession, device::list_devices, + AnisetteConfiguration, AppleAccount, DeveloperSession, SideloadConfiguration, sideload::sideload_app, }; @@ -17,11 +18,23 @@ async fn main() { .expect("Please provide the Apple ID to use for installation"); let apple_password = args.get(3).expect("Please provide the Apple ID password"); - // You don't have to use the builtin list_devices method if you don't want to use usbmuxd - // You can use idevice to get the device info however you want - // This is just easier - let device = list_devices().await.unwrap().into_iter().next().unwrap(); - println!("Target device: {}", device.name); + // You don't have to use usbmuxd, you can use any IdeviceProvider + let usbmuxd = UsbmuxdConnection::default().await; + if usbmuxd.is_err() { + panic!("Failed to connect to usbmuxd: {:?}", usbmuxd.err()); + } + let mut usbmuxd = usbmuxd.unwrap(); + + let devs = usbmuxd.get_devices().await.unwrap(); + if devs.is_empty() { + panic!("No devices found"); + } + + let provider = devs + .iter() + .next() + .unwrap() + .to_provider(UsbmuxdAddr::from_env_var().unwrap(), "isideload-demo"); // Change the anisette url and such here // Note that right now only remote anisette servers are supported @@ -44,11 +57,10 @@ async fn main() { let dev_session = DeveloperSession::new(Arc::new(account)); - // This is where certificates, mobileprovision, and anisette data will be stored - let store_dir = std::env::current_dir().unwrap(); + // You can change the machine name, store directory (for certs, anisette data, & provision files), and logger + let config = SideloadConfiguration::default().set_machine_name("isideload-demo".to_string()); - // DefaultLogger just prints to the stdout/stderr, but you can provide your own implementation - sideload_app(DefaultLogger {}, &dev_session, &device, app_path, store_dir) + sideload_app(&provider, &dev_session, app_path, config) .await .unwrap() } diff --git a/isideload/Cargo.toml b/isideload/Cargo.toml index 743db44..bcab5c4 100644 --- a/isideload/Cargo.toml +++ b/isideload/Cargo.toml @@ -11,7 +11,7 @@ keywords = ["ios", "sideload"] [features] default = [] -vendored-openssl = ["openssl/vendored"] +vendored-openssl = ["openssl/vendored", "zsign-rust/vendored-openssl"] vendored-botan = ["icloud_auth/vendored-botan"] [dependencies] @@ -22,7 +22,7 @@ uuid = { version = "1.17.0", features = ["v4"] } zip = "4.3" hex = "0.4" sha1 = "0.10" -idevice = { version = "0.1.37", features = ["afc", "usbmuxd", "installation_proxy"] } +idevice = { version = "0.1.37", features = ["afc", "installation_proxy"] } openssl = "0.10" futures = "0.3" zsign-rust = "0.1" diff --git a/isideload/src/certificate.rs b/isideload/src/certificate.rs index bf279d6..9782656 100644 --- a/isideload/src/certificate.rs +++ b/isideload/src/certificate.rs @@ -22,6 +22,7 @@ pub struct CertificateIdentity { pub private_key: PKey, pub key_file: PathBuf, pub cert_file: PathBuf, + pub machine_name: String, } impl CertificateIdentity { @@ -29,6 +30,7 @@ impl CertificateIdentity { configuration_path: &Path, dev_session: &DeveloperSession, apple_id: String, + machine_name: String, ) -> Result { let mut hasher = Sha1::new(); hasher.update(apple_id.as_bytes()); @@ -64,6 +66,7 @@ impl CertificateIdentity { private_key, key_file, cert_file, + machine_name, }; if let Ok(cert) = cert_identity @@ -103,7 +106,7 @@ impl CertificateIdentity { for cert in certificates .iter() - .filter(|c| c.machine_name == "YCode".to_string()) + .filter(|c| c.machine_name == self.machine_name) { if let Ok(x509_cert) = X509::from_der(&cert.cert_content) { if let Ok(cert_public_key) = x509_cert.public_key() { @@ -166,6 +169,7 @@ impl CertificateIdentity { DeveloperDeviceType::Ios, team, String::from_utf8_lossy(&csr_pem).to_string(), + self.machine_name.clone(), ) .await .map_err(|e| { diff --git a/isideload/src/developer_session.rs b/isideload/src/developer_session.rs index a3ed585..3d6785a 100644 --- a/isideload/src/developer_session.rs +++ b/isideload/src/developer_session.rs @@ -297,6 +297,7 @@ impl DeveloperSession { device_type: DeveloperDeviceType, team: &DeveloperTeam, csr_content: String, + machine_name: String, ) -> Result { let url = dev_url(device_type, "submitDevelopmentCSR"); let mut body = Dictionary::new(); @@ -306,10 +307,7 @@ impl DeveloperSession { "machineId".to_string(), Value::String(uuid::Uuid::new_v4().to_string().to_uppercase()), ); - body.insert( - "machineName".to_string(), - Value::String("YCode".to_string()), - ); + body.insert("machineName".to_string(), Value::String(machine_name)); let response = self.send_developer_request(&url, Some(body)).await?; let cert_dict = response diff --git a/isideload/src/device.rs b/isideload/src/device.rs index 68a055b..b387f33 100644 --- a/isideload/src/device.rs +++ b/isideload/src/device.rs @@ -1,89 +1,19 @@ use idevice::{ - IdeviceService, - afc::AfcClient, - installation_proxy::InstallationProxyClient, - lockdown::LockdownClient, - usbmuxd::{UsbmuxdAddr, UsbmuxdConnection}, + IdeviceService, afc::AfcClient, installation_proxy::InstallationProxyClient, + provider::IdeviceProvider, }; -use serde::{Deserialize, Serialize}; use std::pin::Pin; use std::{future::Future, path::Path}; use crate::Error; -#[derive(Deserialize, Serialize, Clone)] -pub struct DeviceInfo { - pub name: String, - pub id: u32, - pub uuid: String, -} - -pub async fn list_devices() -> Result, String> { - let usbmuxd = UsbmuxdConnection::default().await; - if usbmuxd.is_err() { - eprintln!("Failed to connect to usbmuxd: {:?}", usbmuxd.err()); - return Err("Failed to connect to usbmuxd".to_string()); - } - let mut usbmuxd = usbmuxd.unwrap(); - - let devs = usbmuxd.get_devices().await.unwrap(); - if devs.is_empty() { - return Ok(vec![]); - } - - let device_info_futures: Vec<_> = devs - .iter() - .map(|d| async move { - let provider = d.to_provider(UsbmuxdAddr::from_env_var().unwrap(), "y-code"); - let device_uid = d.device_id; - - let mut lockdown_client = match LockdownClient::connect(&provider).await { - Ok(l) => l, - Err(e) => { - eprintln!("Unable to connect to lockdown: {e:?}"); - return DeviceInfo { - name: String::from("Unknown Device"), - id: device_uid, - uuid: d.udid.clone(), - }; - } - }; - - let device_name = lockdown_client - .get_value("DeviceName", None) - .await - .expect("Failed to get device name") - .as_string() - .expect("Failed to convert device name to string") - .to_string(); - - DeviceInfo { - name: device_name, - id: device_uid, - uuid: d.udid.clone(), - } - }) - .collect(); - - Ok(futures::future::join_all(device_info_futures).await) -} - +/// Installs an ***already signed*** app onto your device. pub async fn install_app( - device: &DeviceInfo, + provider: &impl IdeviceProvider, app_path: &Path, - callback: impl Fn(u64) -> (), + progress_callback: impl Fn(u64) -> (), ) -> Result<(), Error> { - let mut usbmuxd = UsbmuxdConnection::default() - .await - .map_err(|e| Error::IdeviceError(e))?; - let device = usbmuxd - .get_device(&device.uuid) - .await - .map_err(|e| Error::IdeviceError(e))?; - - let provider = device.to_provider(UsbmuxdAddr::from_env_var().unwrap(), "y-code"); - - let mut afc_client = AfcClient::connect(&provider) + let mut afc_client = AfcClient::connect(provider) .await .map_err(|e| Error::IdeviceError(e))?; @@ -93,7 +23,7 @@ pub async fn install_app( ); afc_upload_dir(&mut afc_client, app_path, &dir).await?; - let mut instproxy_client = InstallationProxyClient::connect(&provider) + let mut instproxy_client = InstallationProxyClient::connect(provider) .await .map_err(|e| Error::IdeviceError(e))?; @@ -104,7 +34,7 @@ pub async fn install_app( dir, Some(plist::Value::Dictionary(options)), async |(percentage, _)| { - callback(percentage); + progress_callback(percentage); }, (), ) diff --git a/isideload/src/lib.rs b/isideload/src/lib.rs index 7fcc8d6..6f3bd5d 100644 --- a/isideload/src/lib.rs +++ b/isideload/src/lib.rs @@ -39,7 +39,7 @@ pub enum Error { ZSignError(#[from] ZSignError), } -pub trait SideloadLogger { +pub trait SideloadLogger: Send + Sync { fn log(&self, message: &str); fn error(&self, error: &Error); } @@ -55,3 +55,44 @@ impl SideloadLogger for DefaultLogger { eprintln!("Error: {}", error); } } + +/// Sideload configuration options. +pub struct SideloadConfiguration { + /// An arbitrary machine name to appear on the certificate (e.x. "YCode") + pub machine_name: String, + /// Logger for reporting progress and errors + pub logger: Box, + /// Directory used to store intermediate artifacts (profiles, certs, etc.). This directory will not be cleared at the end. + pub store_dir: std::path::PathBuf, +} + +impl Default for SideloadConfiguration { + fn default() -> Self { + SideloadConfiguration::new() + } +} + +impl SideloadConfiguration { + pub fn new() -> Self { + SideloadConfiguration { + machine_name: "isideload".to_string(), + logger: Box::new(DefaultLogger), + store_dir: std::env::current_dir().unwrap(), + } + } + + pub fn set_machine_name(mut self, machine_name: String) -> Self { + self.machine_name = machine_name; + self + } + + pub fn set_logger(mut self, logger: Box) -> Self { + self.logger = logger; + self + } + + pub fn set_store_dir(mut self, store_dir: std::path::PathBuf) -> Self { + self.store_dir = store_dir; + self + } +} diff --git a/isideload/src/sideload.rs b/isideload/src/sideload.rs index 64c9f50..b6bec9c 100644 --- a/isideload/src/sideload.rs +++ b/isideload/src/sideload.rs @@ -1,39 +1,64 @@ // This file was made using https://github.com/Dadoum/Sideloader as a reference. +use idevice::IdeviceService; +use idevice::lockdown::LockdownClient; +use idevice::provider::IdeviceProvider; use zsign_rust::ZSignOptions; use crate::application::Application; -use crate::{DeveloperTeam, Error, SideloadLogger}; +use crate::device::install_app; +use crate::{DeveloperTeam, Error, SideloadConfiguration, SideloadLogger}; use crate::{ certificate::CertificateIdentity, developer_session::{DeveloperDeviceType, DeveloperSession}, - device::{DeviceInfo, install_app}, }; use std::{io::Write, path::PathBuf}; -fn error_and_return(logger: &impl SideloadLogger, error: Error) -> Result<(), Error> { +fn error_and_return(logger: &Box, error: Error) -> Result<(), Error> { logger.error(&error); Err(error) } -/// Sideloads an `.ipa` or `.app` onto a device. +/// Signs and installs an `.ipa` or `.app` onto a device. /// /// # Arguments -/// - `logger` — Reports progress and errors. -/// - `dev_session` — Authenticated Apple developer session ([`crate::developer_session::DeveloperSession`]). -/// - `device` — Target device information ([`crate::device::DeviceInfo`]). -/// - `app_path` — Path to the `.ipa` file or `.app` bundle to sign and install -/// - `store_dir` — Directory used to store intermediate artifacts (profiles, certs, etc.). This directory will not be cleared at the end. +/// - `device_provider` - [`idevice::provider::IdeviceProvider`] for the device +/// - `dev_session` - Authenticated Apple developer session ([`crate::developer_session::DeveloperSession`]). +/// - `app_path` - Path to the `.ipa` file or `.app` bundle to sign and install +/// - `config` - Sideload configuration options ([`crate::SideloadConfiguration`]) pub async fn sideload_app( - logger: impl SideloadLogger, + device_provider: &impl IdeviceProvider, dev_session: &DeveloperSession, - device: &DeviceInfo, app_path: PathBuf, - store_dir: PathBuf, + config: SideloadConfiguration, ) -> Result<(), Error> { - if device.uuid.is_empty() { - return error_and_return(&logger, Error::Generic("No device selected".to_string())); - } + let logger = config.logger; + let mut lockdown_client = match LockdownClient::connect(device_provider).await { + Ok(l) => l, + Err(e) => { + return error_and_return(&logger, Error::IdeviceError(e)); + } + }; + + let device_name = lockdown_client + .get_value("DeviceName", None) + .await + .map_err(|e| Error::IdeviceError(e))? + .as_string() + .ok_or(Error::Generic( + "Failed to convert DeviceName to string".to_string(), + ))? + .to_string(); + + let device_uuid = lockdown_client + .get_value("UniqueDeviceID", None) + .await + .map_err(|e| Error::IdeviceError(e))? + .as_string() + .ok_or(Error::Generic( + "Failed to convert UniqueDeviceID to string".to_string(), + ))? + .to_string(); let team = match dev_session.get_team().await { Ok(t) => t, @@ -44,12 +69,13 @@ pub async fn sideload_app( logger.log("Successfully retrieved team"); - ensure_device_registered(&logger, dev_session, &team, device).await?; + ensure_device_registered(&logger, dev_session, &team, &device_uuid, &device_name).await?; let cert = match CertificateIdentity::new( - &store_dir, + &config.store_dir, &dev_session, dev_session.account.apple_id.clone(), + config.machine_name, ) .await { @@ -301,7 +327,9 @@ pub async fn sideload_app( } }; - let profile_path = store_dir.join(format!("{}.mobileprovision", main_app_id_str)); + let profile_path = config + .store_dir + .join(format!("{}.mobileprovision", main_app_id_str)); if profile_path.exists() { std::fs::remove_file(&profile_path).map_err(|e| Error::Filesystem(e))?; @@ -337,7 +365,7 @@ pub async fn sideload_app( logger.log("Installing app (Transfer)... 0%"); - let res = install_app(&device, &app.bundle.bundle_dir, |percentage| { + let res = install_app(device_provider, &app.bundle.bundle_dir, |percentage| { logger.log(&format!("Installing app... {}%", percentage)); }) .await; @@ -349,10 +377,11 @@ pub async fn sideload_app( } pub async fn ensure_device_registered( - logger: &impl SideloadLogger, + logger: &Box, dev_session: &DeveloperSession, team: &DeveloperTeam, - device: &DeviceInfo, + uuid: &str, + name: &str, ) -> Result<(), Error> { let devices = dev_session .list_devices(DeveloperDeviceType::Ios, team) @@ -361,11 +390,11 @@ pub async fn ensure_device_registered( return error_and_return(logger, e); } let devices = devices.unwrap(); - if !devices.iter().any(|d| d.device_number == device.uuid) { + if !devices.iter().any(|d| d.device_number == uuid) { logger.log("Device not found in your account"); // TODO: Actually test! dev_session - .add_device(DeveloperDeviceType::Ios, team, &device.name, &device.uuid) + .add_device(DeveloperDeviceType::Ios, team, name, uuid) .await?; logger.log("Successfully added device to your account"); }