actually functional

This commit is contained in:
nab138
2025-08-09 21:47:14 -04:00
parent 74f5af717c
commit 8592efb0d5
50 changed files with 216 additions and 5128 deletions

View File

@@ -1,5 +1,6 @@
// This file was made using https://github.com/Dadoum/Sideloader as a reference.
use crate::Error;
use crate::bundle::Bundle;
use std::fs::File;
use std::path::PathBuf;
@@ -11,9 +12,11 @@ pub struct Application {
}
impl Application {
pub fn new(path: PathBuf) -> Self {
pub fn new(path: PathBuf) -> Result<Self, Error> {
if !path.exists() {
panic!("Application path does not exist: {}", path.display());
return Err(Error::InvalidBundle(
"Application path does not exist".to_string(),
));
}
let mut bundle_path = path.clone();
@@ -23,21 +26,24 @@ impl Application {
let temp_dir = std::env::temp_dir();
let temp_path = temp_dir.join(path.file_name().unwrap());
if temp_path.exists() {
std::fs::remove_dir_all(&temp_path)
.expect("Failed to remove existing temporary files");
std::fs::remove_dir_all(&temp_path).map_err(|e| Error::Filesystem(e))?;
}
std::fs::create_dir_all(&temp_path).expect("Failed to create temporary directory");
std::fs::create_dir_all(&temp_path).map_err(|e| Error::Filesystem(e))?;
let file = File::open(&path).expect("Failed to open application file");
let mut archive = ZipArchive::new(file).expect("Failed to read application archive");
archive
.extract(&temp_path)
.expect("Failed to extract application archive");
let file = File::open(&path).map_err(|e| Error::Filesystem(e))?;
let mut archive = ZipArchive::new(file).map_err(|e| {
Error::Generic(format!("Failed to open application archive: {}", e))
})?;
archive.extract(&temp_path).map_err(|e| {
Error::Generic(format!("Failed to extract application archive: {}", e))
})?;
let payload_folder = temp_path.join("Payload");
if payload_folder.exists() && payload_folder.is_dir() {
let app_dirs: Vec<_> = std::fs::read_dir(&payload_folder)
.expect("Failed to read Payload directory")
.map_err(|e| {
Error::Generic(format!("Failed to read Payload directory: {}", e))
})?
.filter_map(Result::ok)
.filter(|entry| entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false))
.filter(|entry| entry.path().extension().map_or(false, |ext| ext == "app"))
@@ -45,18 +51,24 @@ impl Application {
if app_dirs.len() == 1 {
bundle_path = app_dirs[0].path();
} else if app_dirs.is_empty() {
panic!("No .app directory found in Payload");
return Err(Error::InvalidBundle(
"No .app directory found in Payload".to_string(),
));
} else {
panic!("Multiple .app directories found in Payload");
return Err(Error::InvalidBundle(
"Multiple .app directories found in Payload".to_string(),
));
}
} else {
panic!("No Payload directory found in the application archive");
return Err(Error::InvalidBundle(
"No Payload directory found in the application archive".to_string(),
));
}
}
let bundle = Bundle::new(bundle_path).expect("Failed to create application bundle");
let bundle = Bundle::new(bundle_path)?;
Application {
Ok(Application {
bundle, /*temp_path*/
}
})
}
}

View File

@@ -8,7 +8,10 @@ use openssl::{
x509::{X509, X509Name, X509ReqBuilder},
};
use sha1::{Digest, Sha1};
use std::{fs, path::PathBuf};
use std::{
fs,
path::{Path, PathBuf},
};
use crate::Error;
use crate::developer_session::{DeveloperDeviceType, DeveloperSession, DeveloperTeam};
@@ -23,7 +26,7 @@ pub struct CertificateIdentity {
impl CertificateIdentity {
pub async fn new(
configuration_path: PathBuf,
configuration_path: &Path,
dev_session: &DeveloperSession,
apple_id: String,
) -> Result<Self, Error> {
@@ -31,8 +34,7 @@ impl CertificateIdentity {
hasher.update(apple_id.as_bytes());
let hash_string = hex::encode(hasher.finalize()).to_lowercase();
let key_path = configuration_path.join("keys").join(hash_string);
fs::create_dir_all(&key_path)
.map_err(|e| Error::Filesystem(format!("Failed to create key directory: {}", e)))?;
fs::create_dir_all(&key_path).map_err(|e| Error::Filesystem(e))?;
let key_file = key_path.join("key.pem");
let cert_file = key_path.join("cert.pem");
@@ -53,8 +55,7 @@ impl CertificateIdentity {
let pem_data = key
.private_key_to_pem_pkcs8()
.map_err(|e| Error::Certificate(format!("Failed to encode private key: {}", e)))?;
fs::write(&key_file, pem_data)
.map_err(|e| Error::Filesystem(format!("Failed to save key file: {}", e)))?;
fs::write(&key_file, pem_data).map_err(|e| Error::Filesystem(e))?;
key
};
@@ -74,9 +75,7 @@ impl CertificateIdentity {
let cert_pem = cert.to_pem().map_err(|e| {
Error::Certificate(format!("Failed to encode certificate to PEM: {}", e))
})?;
fs::write(&cert_identity.cert_file, cert_pem).map_err(|e| {
Error::Filesystem(format!("Failed to save certificate file: {}", e))
})?;
fs::write(&cert_identity.cert_file, cert_pem).map_err(|e| Error::Filesystem(e))?;
return Ok(cert_identity);
}
@@ -199,19 +198,18 @@ impl CertificateIdentity {
let cert_pem = certificate.to_pem().map_err(|e| {
Error::Certificate(format!("Failed to encode certificate to PEM: {}", e))
})?;
fs::write(&self.cert_file, cert_pem)
.map_err(|e| Error::Filesystem(format!("Failed to save certificate file: {}", e)))?;
fs::write(&self.cert_file, cert_pem).map_err(|e| Error::Filesystem(e))?;
self.certificate = Some(certificate);
Ok(())
}
pub fn get_certificate_file_path(&self) -> &PathBuf {
pub fn get_certificate_file_path(&self) -> &Path {
&self.cert_file
}
pub fn get_private_key_file_path(&self) -> &PathBuf {
pub fn get_private_key_file_path(&self) -> &Path {
&self.key_file
}
}

View File

@@ -6,9 +6,10 @@ use idevice::{
usbmuxd::{UsbmuxdAddr, UsbmuxdConnection},
};
use serde::{Deserialize, Serialize};
use std::future::Future;
use std::path::PathBuf;
use std::pin::Pin;
use std::{future::Future, path::Path};
use crate::Error;
#[derive(Deserialize, Serialize, Clone)]
pub struct DeviceInfo {
@@ -69,34 +70,32 @@ pub async fn list_devices() -> Result<Vec<DeviceInfo>, String> {
pub async fn install_app(
device: &DeviceInfo,
app_path: &PathBuf,
app_path: &Path,
callback: impl Fn(u64) -> (),
) -> Result<(), String> {
) -> Result<(), Error> {
let mut usbmuxd = UsbmuxdConnection::default()
.await
.map_err(|e| format!("Failed to connect to usbmuxd: {:?}", e))?;
.map_err(|e| Error::IdeviceError(e))?;
let device = usbmuxd
.get_device(&device.uuid)
.await
.map_err(|e| format!("Failed to get device: {:?}", e))?;
.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
.map_err(|e| format!("Failed to connect to AFC: {:?}", e))?;
.map_err(|e| Error::IdeviceError(e))?;
let dir = format!(
"PublicStaging/{}",
app_path.file_name().unwrap().to_string_lossy()
);
afc_upload_dir(&mut afc_client, app_path, &dir)
.await
.map_err(|e| format!("Failed to upload directory: {:?}", e))?;
afc_upload_dir(&mut afc_client, app_path, &dir).await?;
let mut instproxy_client = InstallationProxyClient::connect(&provider)
.await
.map_err(|e| format!("Failed to connect to installation proxy: {:?}", e))?;
.map_err(|e| Error::IdeviceError(e))?;
let mut options = plist::Dictionary::new();
options.insert("PackageType".to_string(), "Developer".into());
@@ -110,25 +109,24 @@ pub async fn install_app(
(),
)
.await
.map_err(|e| format!("Failed to install app: {:?}", e))?;
.map_err(|e| Error::IdeviceError(e))?;
Ok(())
}
fn afc_upload_dir<'a>(
afc_client: &'a mut AfcClient,
path: &'a PathBuf,
path: &'a Path,
afc_path: &'a str,
) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>> {
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>> {
Box::pin(async move {
let entries =
std::fs::read_dir(path).map_err(|e| format!("Failed to read directory: {}", e))?;
let entries = std::fs::read_dir(path).map_err(|e| Error::Filesystem(e))?;
afc_client
.mk_dir(afc_path)
.await
.map_err(|e| format!("Failed to create directory: {}", e))?;
.map_err(|e| Error::IdeviceError(e))?;
for entry in entries {
let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?;
let entry = entry.map_err(|e| Error::Filesystem(e))?;
let path = entry.path();
if path.is_dir() {
let new_afc_path = format!(
@@ -148,13 +146,12 @@ fn afc_upload_dir<'a>(
idevice::afc::opcode::AfcFopenMode::WrOnly,
)
.await
.map_err(|e| format!("Failed to open file: {}", e))?;
let bytes =
std::fs::read(&path).map_err(|e| format!("Failed to read file: {}", e))?;
.map_err(|e| Error::IdeviceError(e))?;
let bytes = std::fs::read(&path).map_err(|e| Error::Filesystem(e))?;
file_handle
.write(&bytes)
.await
.map_err(|e| format!("Failed to write file: {}", e))?;
.map_err(|e| Error::IdeviceError(e))?;
}
}
Ok(())

View File

@@ -5,14 +5,19 @@ pub mod developer_session;
pub mod device;
pub mod sideload;
use std::io::Error as IOError;
pub use developer_session::{
AppId, ApplicationGroup, DeveloperDevice, DeveloperDeviceType, DeveloperSession, DeveloperTeam,
DevelopmentCertificate, ListAppIdsResponse, ProvisioningProfile,
};
pub use icloud_auth::{AnisetteConfiguration, AppleAccount};
use idevice::IdeviceError;
use thiserror::Error as ThisError;
use zsign_rust::ZSignError;
#[derive(Debug, Clone, ThisError)]
#[derive(Debug, ThisError)]
pub enum Error {
#[error("Authentication error {0}: {1}")]
Auth(i64, String),
@@ -26,23 +31,27 @@ pub enum Error {
InvalidBundle(String),
#[error("Certificate error: {0}")]
Certificate(String),
#[error("Failed to use files: {0}")]
Filesystem(String),
#[error(transparent)]
Filesystem(#[from] IOError),
#[error(transparent)]
IdeviceError(#[from] IdeviceError),
#[error(transparent)]
ZSignError(#[from] ZSignError),
}
pub trait SideloadLogger {
async fn log(&self, message: &str);
async fn error(&self, error: &Error);
fn log(&self, message: &str);
fn error(&self, error: &Error);
}
pub struct DefaultLogger;
impl SideloadLogger for DefaultLogger {
async fn log(&self, message: &str) {
fn log(&self, message: &str) {
println!("{message}");
}
async fn error(&self, error: &Error) {
fn error(&self, error: &Error) {
eprintln!("Error: {}", error);
}
}

View File

@@ -3,7 +3,7 @@
use zsign_rust::ZSignOptions;
use crate::application::Application;
use crate::{Error, SideloadLogger};
use crate::{DeveloperTeam, Error, SideloadLogger};
use crate::{
certificate::CertificateIdentity,
developer_session::{DeveloperDeviceType, DeveloperSession},
@@ -16,11 +16,20 @@ fn error_and_return(logger: &impl SideloadLogger, error: Error) -> Result<(), Er
Err(error)
}
/// Sideloads 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.
pub async fn sideload_app(
logger: impl SideloadLogger,
dev_session: &DeveloperSession,
device: &DeviceInfo,
app_path: PathBuf,
store_dir: PathBuf,
) -> Result<(), Error> {
if device.uuid.is_empty() {
return error_and_return(&logger, Error::Generic("No device selected".to_string()));
@@ -35,10 +44,15 @@ pub async fn sideload_app(
logger.log("Successfully retrieved team");
ensure_device_registered(&dev_session, window, &team, &device).await?;
ensure_device_registered(&logger, dev_session, &team, device).await?;
let config_dir = handle.path().app_config_dir().map_err(|e| e.to_string())?;
let cert = match CertificateIdentity::new(config_dir, &dev_session, get_apple_email()).await {
let cert = match CertificateIdentity::new(
&store_dir,
&dev_session,
dev_session.account.apple_id.clone(),
)
.await
{
Ok(c) => c,
Err(e) => {
return error_and_return(&logger, e);
@@ -57,7 +71,7 @@ pub async fn sideload_app(
}
};
let mut app = Application::new(app_path);
let mut app = Application::new(app_path)?;
let is_sidestore = app.bundle.bundle_identifier().unwrap_or("") == "com.SideStore.SideStore";
let main_app_bundle_id = match app.bundle.bundle_identifier() {
Some(id) => id.to_string(),
@@ -287,20 +301,15 @@ pub async fn sideload_app(
}
};
let profile_path = handle
.path()
.app_config_dir()
.map_err(|e| e.to_string())?
.join(format!("{}.mobileprovision", main_app_id_str));
let profile_path = store_dir.join(format!("{}.mobileprovision", main_app_id_str));
if profile_path.exists() {
std::fs::remove_file(&profile_path).map_err(|e| e.to_string())?;
std::fs::remove_file(&profile_path).map_err(|e| Error::Filesystem(e))?;
}
let mut file =
std::fs::File::create(&profile_path).map_err(|e| Error::Filesystem(e.to_string()))?;
let mut file = std::fs::File::create(&profile_path).map_err(|e| Error::Filesystem(e))?;
file.write_all(&provisioning_profile.encoded_profile)
.map_err(|e| Error::Filesystem(e.to_string()))?;
.map_err(|e| Error::Filesystem(e))?;
// Without this, zsign complains it can't find the provision file
#[cfg(target_os = "windows")]
@@ -310,7 +319,7 @@ pub async fn sideload_app(
}
// TODO: Recursive for sub-bundles?
app.bundle.write_info().map_err(|e| e.to_string())?;
app.bundle.write_info()?;
match ZSignOptions::new(app.bundle.bundle_dir.to_str().unwrap())
.with_cert_file(cert.get_certificate_file_path().to_str().unwrap())
@@ -320,7 +329,7 @@ pub async fn sideload_app(
{
Ok(_) => {}
Err(e) => {
return error_and_return(&logger, &format!("Failed to sign app: {:?}", e));
return error_and_return(&logger, Error::ZSignError(e));
}
};
@@ -329,12 +338,37 @@ pub async fn sideload_app(
logger.log("Installing app (Transfer)... 0%");
let res = install_app(&device, &app.bundle.bundle_dir, |percentage| {
logger.log(format!("Installing app... {}%", percentage));
logger.log(&format!("Installing app... {}%", percentage));
})
.await;
if let Err(e) = res {
return error_and_return(&logger, &format!("Failed to install app: {:?}", e));
return error_and_return(&logger, e);
}
Ok(())
}
pub async fn ensure_device_registered(
logger: &impl SideloadLogger,
dev_session: &DeveloperSession,
team: &DeveloperTeam,
device: &DeviceInfo,
) -> Result<(), Error> {
let devices = dev_session
.list_devices(DeveloperDeviceType::Ios, team)
.await;
if let Err(e) = devices {
return error_and_return(logger, e);
}
let devices = devices.unwrap();
if !devices.iter().any(|d| d.device_number == device.uuid) {
logger.log("Device not found in your account");
// TODO: Actually test!
dev_session
.add_device(DeveloperDeviceType::Ios, team, &device.name, &device.uuid)
.await?;
logger.log("Successfully added device to your account");
}
logger.log("Device is a development device");
Ok(())
}