Improve API

This commit is contained in:
nab138
2025-08-10 17:51:42 -04:00
parent 77894f0663
commit d08ae68e78
10 changed files with 158 additions and 129 deletions

1
Cargo.lock generated
View File

@@ -1366,6 +1366,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
name = "minimal" name = "minimal"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"idevice",
"isideload", "isideload",
"tokio", "tokio",
] ]

View File

@@ -19,8 +19,10 @@ Then, you can use it like so:
```rs ```rs
use std::{env, path::PathBuf, sync::Arc}; use std::{env, path::PathBuf, sync::Arc};
use idevice::usbmuxd::{UsbmuxdAddr, UsbmuxdConnection};
use isideload::{ use isideload::{
AnisetteConfiguration, AppleAccount, DefaultLogger, DeveloperSession, device::list_devices, AnisetteConfiguration, AppleAccount, DeveloperSession, SideloadConfiguration,
sideload::sideload_app, sideload::sideload_app,
}; };
@@ -36,11 +38,23 @@ async fn main() {
.expect("Please provide the Apple ID to use for installation"); .expect("Please provide the Apple ID to use for installation");
let apple_password = args.get(3).expect("Please provide the Apple ID password"); 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 don't have to use usbmuxd, you can use any IdeviceProvider
// You can use idevice to get the device info however you want let usbmuxd = UsbmuxdConnection::default().await;
// This is just easier if usbmuxd.is_err() {
let device = list_devices().await.unwrap().into_iter().next().unwrap(); panic!("Failed to connect to usbmuxd: {:?}", usbmuxd.err());
println!("Target device: {}", device.name); }
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 // Change the anisette url and such here
// Note that right now only remote anisette servers are supported // 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)); let dev_session = DeveloperSession::new(Arc::new(account));
// This is where certificates, mobileprovision, and anisette data will be stored // You can change the machine name, store directory (for certs, anisette data, & provision files), and logger
let store_dir = std::env::current_dir().unwrap(); 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(&provider, &dev_session, app_path, config)
sideload_app(DefaultLogger {}, &dev_session, &device, app_path, store_dir)
.await .await
.unwrap() .unwrap()
} }

View File

@@ -6,4 +6,5 @@ publish = false
[dependencies] [dependencies]
isideload = { path = "../../isideload", features = ["vendored-openssl", "vendored-botan"] } isideload = { path = "../../isideload", features = ["vendored-openssl", "vendored-botan"] }
idevice = { version = "0.1.37", features = ["usbmuxd"]}
tokio = { version = "1", features = ["macros", "rt-multi-thread"] } tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

View File

@@ -1,7 +1,8 @@
use std::{env, path::PathBuf, sync::Arc}; use std::{env, path::PathBuf, sync::Arc};
use idevice::usbmuxd::{UsbmuxdAddr, UsbmuxdConnection};
use isideload::{ use isideload::{
AnisetteConfiguration, AppleAccount, DefaultLogger, DeveloperSession, device::list_devices, AnisetteConfiguration, AppleAccount, DeveloperSession, SideloadConfiguration,
sideload::sideload_app, sideload::sideload_app,
}; };
@@ -17,11 +18,23 @@ async fn main() {
.expect("Please provide the Apple ID to use for installation"); .expect("Please provide the Apple ID to use for installation");
let apple_password = args.get(3).expect("Please provide the Apple ID password"); 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 don't have to use usbmuxd, you can use any IdeviceProvider
// You can use idevice to get the device info however you want let usbmuxd = UsbmuxdConnection::default().await;
// This is just easier if usbmuxd.is_err() {
let device = list_devices().await.unwrap().into_iter().next().unwrap(); panic!("Failed to connect to usbmuxd: {:?}", usbmuxd.err());
println!("Target device: {}", device.name); }
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 // Change the anisette url and such here
// Note that right now only remote anisette servers are supported // 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)); let dev_session = DeveloperSession::new(Arc::new(account));
// This is where certificates, mobileprovision, and anisette data will be stored // You can change the machine name, store directory (for certs, anisette data, & provision files), and logger
let store_dir = std::env::current_dir().unwrap(); 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(&provider, &dev_session, app_path, config)
sideload_app(DefaultLogger {}, &dev_session, &device, app_path, store_dir)
.await .await
.unwrap() .unwrap()
} }

View File

@@ -11,7 +11,7 @@ keywords = ["ios", "sideload"]
[features] [features]
default = [] default = []
vendored-openssl = ["openssl/vendored"] vendored-openssl = ["openssl/vendored", "zsign-rust/vendored-openssl"]
vendored-botan = ["icloud_auth/vendored-botan"] vendored-botan = ["icloud_auth/vendored-botan"]
[dependencies] [dependencies]
@@ -22,7 +22,7 @@ uuid = { version = "1.17.0", features = ["v4"] }
zip = "4.3" zip = "4.3"
hex = "0.4" hex = "0.4"
sha1 = "0.10" 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" openssl = "0.10"
futures = "0.3" futures = "0.3"
zsign-rust = "0.1" zsign-rust = "0.1"

View File

@@ -22,6 +22,7 @@ pub struct CertificateIdentity {
pub private_key: PKey<Private>, pub private_key: PKey<Private>,
pub key_file: PathBuf, pub key_file: PathBuf,
pub cert_file: PathBuf, pub cert_file: PathBuf,
pub machine_name: String,
} }
impl CertificateIdentity { impl CertificateIdentity {
@@ -29,6 +30,7 @@ impl CertificateIdentity {
configuration_path: &Path, configuration_path: &Path,
dev_session: &DeveloperSession, dev_session: &DeveloperSession,
apple_id: String, apple_id: String,
machine_name: String,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let mut hasher = Sha1::new(); let mut hasher = Sha1::new();
hasher.update(apple_id.as_bytes()); hasher.update(apple_id.as_bytes());
@@ -64,6 +66,7 @@ impl CertificateIdentity {
private_key, private_key,
key_file, key_file,
cert_file, cert_file,
machine_name,
}; };
if let Ok(cert) = cert_identity if let Ok(cert) = cert_identity
@@ -103,7 +106,7 @@ impl CertificateIdentity {
for cert in certificates for cert in certificates
.iter() .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(x509_cert) = X509::from_der(&cert.cert_content) {
if let Ok(cert_public_key) = x509_cert.public_key() { if let Ok(cert_public_key) = x509_cert.public_key() {
@@ -166,6 +169,7 @@ impl CertificateIdentity {
DeveloperDeviceType::Ios, DeveloperDeviceType::Ios,
team, team,
String::from_utf8_lossy(&csr_pem).to_string(), String::from_utf8_lossy(&csr_pem).to_string(),
self.machine_name.clone(),
) )
.await .await
.map_err(|e| { .map_err(|e| {

View File

@@ -297,6 +297,7 @@ impl DeveloperSession {
device_type: DeveloperDeviceType, device_type: DeveloperDeviceType,
team: &DeveloperTeam, team: &DeveloperTeam,
csr_content: String, csr_content: String,
machine_name: String,
) -> Result<String, Error> { ) -> Result<String, Error> {
let url = dev_url(device_type, "submitDevelopmentCSR"); let url = dev_url(device_type, "submitDevelopmentCSR");
let mut body = Dictionary::new(); let mut body = Dictionary::new();
@@ -306,10 +307,7 @@ impl DeveloperSession {
"machineId".to_string(), "machineId".to_string(),
Value::String(uuid::Uuid::new_v4().to_string().to_uppercase()), Value::String(uuid::Uuid::new_v4().to_string().to_uppercase()),
); );
body.insert( body.insert("machineName".to_string(), Value::String(machine_name));
"machineName".to_string(),
Value::String("YCode".to_string()),
);
let response = self.send_developer_request(&url, Some(body)).await?; let response = self.send_developer_request(&url, Some(body)).await?;
let cert_dict = response let cert_dict = response

View File

@@ -1,89 +1,19 @@
use idevice::{ use idevice::{
IdeviceService, IdeviceService, afc::AfcClient, installation_proxy::InstallationProxyClient,
afc::AfcClient, provider::IdeviceProvider,
installation_proxy::InstallationProxyClient,
lockdown::LockdownClient,
usbmuxd::{UsbmuxdAddr, UsbmuxdConnection},
}; };
use serde::{Deserialize, Serialize};
use std::pin::Pin; use std::pin::Pin;
use std::{future::Future, path::Path}; use std::{future::Future, path::Path};
use crate::Error; use crate::Error;
#[derive(Deserialize, Serialize, Clone)] /// Installs an ***already signed*** app onto your device.
pub struct DeviceInfo {
pub name: String,
pub id: u32,
pub uuid: String,
}
pub async fn list_devices() -> Result<Vec<DeviceInfo>, 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)
}
pub async fn install_app( pub async fn install_app(
device: &DeviceInfo, provider: &impl IdeviceProvider,
app_path: &Path, app_path: &Path,
callback: impl Fn(u64) -> (), progress_callback: impl Fn(u64) -> (),
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut usbmuxd = UsbmuxdConnection::default() let mut afc_client = AfcClient::connect(provider)
.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)
.await .await
.map_err(|e| Error::IdeviceError(e))?; .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?; 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 .await
.map_err(|e| Error::IdeviceError(e))?; .map_err(|e| Error::IdeviceError(e))?;
@@ -104,7 +34,7 @@ pub async fn install_app(
dir, dir,
Some(plist::Value::Dictionary(options)), Some(plist::Value::Dictionary(options)),
async |(percentage, _)| { async |(percentage, _)| {
callback(percentage); progress_callback(percentage);
}, },
(), (),
) )

View File

@@ -39,7 +39,7 @@ pub enum Error {
ZSignError(#[from] ZSignError), ZSignError(#[from] ZSignError),
} }
pub trait SideloadLogger { pub trait SideloadLogger: Send + Sync {
fn log(&self, message: &str); fn log(&self, message: &str);
fn error(&self, error: &Error); fn error(&self, error: &Error);
} }
@@ -55,3 +55,44 @@ impl SideloadLogger for DefaultLogger {
eprintln!("Error: {}", error); 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<dyn SideloadLogger>,
/// 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<dyn SideloadLogger>) -> Self {
self.logger = logger;
self
}
pub fn set_store_dir(mut self, store_dir: std::path::PathBuf) -> Self {
self.store_dir = store_dir;
self
}
}

View File

@@ -1,39 +1,64 @@
// This file was made using https://github.com/Dadoum/Sideloader as a reference. // 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 zsign_rust::ZSignOptions;
use crate::application::Application; use crate::application::Application;
use crate::{DeveloperTeam, Error, SideloadLogger}; use crate::device::install_app;
use crate::{DeveloperTeam, Error, SideloadConfiguration, SideloadLogger};
use crate::{ use crate::{
certificate::CertificateIdentity, certificate::CertificateIdentity,
developer_session::{DeveloperDeviceType, DeveloperSession}, developer_session::{DeveloperDeviceType, DeveloperSession},
device::{DeviceInfo, install_app},
}; };
use std::{io::Write, path::PathBuf}; use std::{io::Write, path::PathBuf};
fn error_and_return(logger: &impl SideloadLogger, error: Error) -> Result<(), Error> { fn error_and_return(logger: &Box<dyn SideloadLogger>, error: Error) -> Result<(), Error> {
logger.error(&error); logger.error(&error);
Err(error) Err(error)
} }
/// Sideloads an `.ipa` or `.app` onto a device. /// Signs and installs an `.ipa` or `.app` onto a device.
/// ///
/// # Arguments /// # Arguments
/// - `logger` — Reports progress and errors. /// - `device_provider` - [`idevice::provider::IdeviceProvider`] for the device
/// - `dev_session` Authenticated Apple developer session ([`crate::developer_session::DeveloperSession`]). /// - `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
/// - `app_path` — Path to the `.ipa` file or `.app` bundle to sign and install /// - `config` - Sideload configuration options ([`crate::SideloadConfiguration`])
/// - `store_dir` — Directory used to store intermediate artifacts (profiles, certs, etc.). This directory will not be cleared at the end.
pub async fn sideload_app( pub async fn sideload_app(
logger: impl SideloadLogger, device_provider: &impl IdeviceProvider,
dev_session: &DeveloperSession, dev_session: &DeveloperSession,
device: &DeviceInfo,
app_path: PathBuf, app_path: PathBuf,
store_dir: PathBuf, config: SideloadConfiguration,
) -> Result<(), Error> { ) -> Result<(), Error> {
if device.uuid.is_empty() { let logger = config.logger;
return error_and_return(&logger, Error::Generic("No device selected".to_string())); 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 { let team = match dev_session.get_team().await {
Ok(t) => t, Ok(t) => t,
@@ -44,12 +69,13 @@ pub async fn sideload_app(
logger.log("Successfully retrieved team"); 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( let cert = match CertificateIdentity::new(
&store_dir, &config.store_dir,
&dev_session, &dev_session,
dev_session.account.apple_id.clone(), dev_session.account.apple_id.clone(),
config.machine_name,
) )
.await .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() { if profile_path.exists() {
std::fs::remove_file(&profile_path).map_err(|e| Error::Filesystem(e))?; 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%"); 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)); logger.log(&format!("Installing app... {}%", percentage));
}) })
.await; .await;
@@ -349,10 +377,11 @@ pub async fn sideload_app(
} }
pub async fn ensure_device_registered( pub async fn ensure_device_registered(
logger: &impl SideloadLogger, logger: &Box<dyn SideloadLogger>,
dev_session: &DeveloperSession, dev_session: &DeveloperSession,
team: &DeveloperTeam, team: &DeveloperTeam,
device: &DeviceInfo, uuid: &str,
name: &str,
) -> Result<(), Error> { ) -> Result<(), Error> {
let devices = dev_session let devices = dev_session
.list_devices(DeveloperDeviceType::Ios, team) .list_devices(DeveloperDeviceType::Ios, team)
@@ -361,11 +390,11 @@ pub async fn ensure_device_registered(
return error_and_return(logger, e); return error_and_return(logger, e);
} }
let devices = devices.unwrap(); 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"); logger.log("Device not found in your account");
// TODO: Actually test! // TODO: Actually test!
dev_session dev_session
.add_device(DeveloperDeviceType::Ios, team, &device.name, &device.uuid) .add_device(DeveloperDeviceType::Ios, team, name, uuid)
.await?; .await?;
logger.log("Successfully added device to your account"); logger.log("Successfully added device to your account");
} }