diff --git a/Cargo.lock b/Cargo.lock index 18255bb..bba36e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -704,6 +704,7 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", + "zlib-rs", ] [[package]] @@ -1288,6 +1289,7 @@ dependencies = [ "tracing", "uuid", "x509-certificate", + "zip", ] [[package]] @@ -2740,6 +2742,12 @@ dependencies = [ "utf-8", ] +[[package]] +name = "typed-path" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3015e6ce46d5ad8751e4a772543a30c7511468070e98e64e20165f8f81155b64" + [[package]] name = "typenum" version = "1.19.0" @@ -3605,8 +3613,40 @@ dependencies = [ "syn", ] +[[package]] +name = "zip" +version = "7.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc12baa6db2b15a140161ce53d72209dacea594230798c24774139b54ecaa980" +dependencies = [ + "crc32fast", + "flate2", + "indexmap", + "memchr", + "typed-path", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7948af682ccbc3342b6e9420e8c51c1fe5d7bf7756002b4a3c6cabfe96a7e3c" + [[package]] name = "zmij" version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] diff --git a/isideload/Cargo.toml b/isideload/Cargo.toml index 601ac6a..a03ecf4 100644 --- a/isideload/Cargo.toml +++ b/isideload/Cargo.toml @@ -49,4 +49,5 @@ keyring = { version = "3.6.3", features = ["apple-native", "linux-native-sync-pe # TODO: Fork to update dependencies (doubt it will ever be updated) x509-certificate = "0.25" rcgen = { version = "0.14.7", default-features = false, features = ["aws_lc_rs", "pem"] } -p12-keystore = { optional = true, version = "0.2.0" } \ No newline at end of file +p12-keystore = { optional = true, version = "0.2.0" } +zip = { version = "7.4", default-features = false, features = ["deflate"] } \ No newline at end of file diff --git a/isideload/src/lib.rs b/isideload/src/lib.rs index 0c73bc3..13d7121 100644 --- a/isideload/src/lib.rs +++ b/isideload/src/lib.rs @@ -22,6 +22,9 @@ pub enum SideloadError { #[error("Developer error {0}: {1}")] DeveloperError(i64, String), + + #[error("Invalid bundle: {0}")] + InvalidBundle(String), } // The default reqwest error formatter sucks and provides no info diff --git a/isideload/src/sideload/application.rs b/isideload/src/sideload/application.rs new file mode 100644 index 0000000..91ba3fd --- /dev/null +++ b/isideload/src/sideload/application.rs @@ -0,0 +1,86 @@ +// This file was made using https://github.com/Dadoum/Sideloader as a reference. +// I'm planning on redoing this later to better handle entitlements, extensions, etc, but it will do for now + +use crate::SideloadError; +use crate::sideload::bundle::Bundle; +use rootcause::prelude::*; +use std::fs::File; +use std::path::PathBuf; +use zip::ZipArchive; + +pub struct Application { + pub bundle: Bundle, + //pub temp_path: PathBuf, +} + +impl Application { + pub fn new(path: PathBuf) -> Result { + if !path.exists() { + bail!(SideloadError::InvalidBundle( + "Application path does not exist".to_string(), + )); + } + + let mut bundle_path = path.clone(); + //let mut temp_path = PathBuf::new(); + + if path.is_file() { + let temp_dir = std::env::temp_dir(); + let temp_path = temp_dir + .join(path.file_name().unwrap().to_string_lossy().to_string() + "_extracted"); + if temp_path.exists() { + std::fs::remove_dir_all(&temp_path) + .context("Failed to remove existing temporary directory")?; + } + std::fs::create_dir_all(&temp_path).context("Failed to create temporary directory")?; + + let file = File::open(&path).context("Failed to open application archive")?; + let mut archive = + ZipArchive::new(file).context("Failed to open application archive")?; + archive + .extract(&temp_path) + .context("Failed to extract application archive")?; + + 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) + .context("Failed to read Payload directory")? + .filter_map(Result::ok) + .filter(|entry| entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false)) + .filter(|entry| entry.path().extension().is_some_and(|ext| ext == "app")) + .collect(); + if app_dirs.len() == 1 { + bundle_path = app_dirs[0].path(); + } else if app_dirs.is_empty() { + bail!(SideloadError::InvalidBundle( + "No .app directory found in Payload".to_string(), + )); + } else { + bail!(SideloadError::InvalidBundle( + "Multiple .app directories found in Payload".to_string(), + )); + } + } else { + bail!(SideloadError::InvalidBundle( + "No Payload directory found in the application archive".to_string(), + )); + } + } + let bundle = Bundle::new(bundle_path)?; + + Ok(Application { + bundle, /*temp_path*/ + }) + } + + pub fn is_sidestore(&self) -> bool { + self.bundle.bundle_identifier().unwrap_or("") == "com.SideStore.SideStore" + } + + pub fn is_lc_and_sidestore(&self) -> bool { + self.bundle + .frameworks() + .iter() + .any(|f| f.bundle_identifier().unwrap_or("") == "com.SideStore.SideStore") + } +} diff --git a/isideload/src/sideload/bundle.rs b/isideload/src/sideload/bundle.rs new file mode 100644 index 0000000..62f9732 --- /dev/null +++ b/isideload/src/sideload/bundle.rs @@ -0,0 +1,190 @@ +// This file was made using https://github.com/Dadoum/Sideloader as a reference. +// I'm planning on redoing this later to better handle entitlements, extensions, etc, but it will do for now + +use plist::{Dictionary, Value}; +use rootcause::prelude::*; +use std::{ + fs, + path::{Path, PathBuf}, +}; + +use crate::SideloadError; + +#[derive(Debug)] +pub struct Bundle { + pub app_info: Dictionary, + pub bundle_dir: PathBuf, + + app_extensions: Vec, + frameworks: Vec, + _libraries: Vec, +} + +impl Bundle { + pub fn new(bundle_dir: PathBuf) -> Result { + let mut bundle_path = bundle_dir; + // Remove trailing slash/backslash + if let Some(path_str) = bundle_path.to_str() + && (path_str.ends_with('/') || path_str.ends_with('\\')) + { + bundle_path = PathBuf::from(&path_str[..path_str.len() - 1]); + } + + let info_plist_path = bundle_path.join("Info.plist"); + assert_bundle( + info_plist_path.exists(), + &format!("No Info.plist here: {}", info_plist_path.display()), + )?; + + let plist_data = fs::read(&info_plist_path).context(SideloadError::InvalidBundle( + "Failed to read Info.plist".to_string(), + ))?; + + let app_info = plist::from_bytes(&plist_data).context(SideloadError::InvalidBundle( + "Failed to parse Info.plist".to_string(), + ))?; + + // Load app extensions from PlugIns directory + let plug_ins_dir = bundle_path.join("PlugIns"); + let app_extensions = if plug_ins_dir.exists() { + fs::read_dir(&plug_ins_dir) + .context(SideloadError::InvalidBundle( + "Failed to read PlugIns directory".to_string(), + ))? + .filter_map(|entry| entry.ok()) + .filter(|entry| { + entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) + && entry.path().join("Info.plist").exists() + }) + .filter_map(|entry| Bundle::new(entry.path()).ok()) + .collect() + } else { + Vec::new() + }; + + // Load frameworks from Frameworks directory + let frameworks_dir = bundle_path.join("Frameworks"); + let frameworks = if frameworks_dir.exists() { + fs::read_dir(&frameworks_dir) + .context(SideloadError::InvalidBundle( + "Failed to read Frameworks directory".to_string(), + ))? + .filter_map(|entry| entry.ok()) + .filter(|entry| { + entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) + && entry.path().join("Info.plist").exists() + }) + .filter_map(|entry| Bundle::new(entry.path()).ok()) + .collect() + } else { + Vec::new() + }; + + // Find all .dylib files in the bundle directory (recursive) + let libraries = find_dylibs(&bundle_path, &bundle_path)?; + + Ok(Bundle { + app_info, + bundle_dir: bundle_path, + app_extensions, + frameworks, + _libraries: libraries, + }) + } + + pub fn set_bundle_identifier(&mut self, id: &str) { + self.app_info.insert( + "CFBundleIdentifier".to_string(), + Value::String(id.to_string()), + ); + } + + pub fn bundle_identifier(&self) -> Option<&str> { + self.app_info + .get("CFBundleIdentifier") + .and_then(|v| v.as_string()) + } + + pub fn bundle_name(&self) -> Option<&str> { + self.app_info + .get("CFBundleName") + .and_then(|v| v.as_string()) + } + + pub fn app_extensions(&self) -> &[Bundle] { + &self.app_extensions + } + + pub fn app_extensions_mut(&mut self) -> &mut [Bundle] { + &mut self.app_extensions + } + + pub fn frameworks(&self) -> &[Bundle] { + &self.frameworks + } + + pub fn frameworks_mut(&mut self) -> &mut [Bundle] { + &mut self.frameworks + } + + pub fn write_info(&self) -> Result<(), Report> { + let info_plist_path = self.bundle_dir.join("Info.plist"); + plist::to_file_binary(&info_plist_path, &self.app_info).context( + SideloadError::InvalidBundle("Failed to write Info.plist".to_string()), + )?; + Ok(()) + } +} + +fn assert_bundle(condition: bool, msg: &str) -> Result<(), Report> { + if !condition { + bail!(SideloadError::InvalidBundle(msg.to_string())) + } else { + Ok(()) + } +} + +fn find_dylibs(dir: &Path, bundle_root: &Path) -> Result, Report> { + let mut libraries = Vec::new(); + + fn collect_dylibs( + dir: &Path, + bundle_root: &Path, + libraries: &mut Vec, + ) -> Result<(), Report> { + let entries = fs::read_dir(dir).context(SideloadError::InvalidBundle(format!( + "Failed to read directory {}", + dir.display() + )))?; + + for entry in entries { + let entry = entry.context(SideloadError::InvalidBundle( + "Failed to read directory entry".to_string(), + ))?; + + let path = entry.path(); + let file_type = entry.file_type().context(SideloadError::InvalidBundle( + "Failed to get file type".to_string(), + ))?; + + if file_type.is_file() { + if let Some(name) = path.file_name().and_then(|n| n.to_str()) + && name.ends_with(".dylib") + { + // Get relative path from bundle root + if let Ok(relative_path) = path.strip_prefix(bundle_root) + && let Some(relative_str) = relative_path.to_str() + { + libraries.push(relative_str.to_string()); + } + } + } else if file_type.is_dir() { + collect_dylibs(&path, bundle_root, libraries)?; + } + } + Ok(()) + } + + collect_dylibs(dir, bundle_root, &mut libraries)?; + Ok(libraries) +} diff --git a/isideload/src/sideload/mod.rs b/isideload/src/sideload/mod.rs index f8f8722..c194dc1 100644 --- a/isideload/src/sideload/mod.rs +++ b/isideload/src/sideload/mod.rs @@ -1,4 +1,6 @@ +pub mod application; pub mod builder; +pub mod bundle; pub mod cert_identity; pub mod sideloader; pub use builder::{SideloaderBuilder, TeamSelection}; diff --git a/isideload/src/sideload/sideloader.rs b/isideload/src/sideload/sideloader.rs index a164e6e..9b3d14e 100644 --- a/isideload/src/sideload/sideloader.rs +++ b/isideload/src/sideload/sideloader.rs @@ -4,7 +4,10 @@ use crate::{ devices::DevicesApi, teams::{DeveloperTeam, TeamsApi}, }, - sideload::{TeamSelection, builder::MaxCertsBehavior, cert_identity::CertificateIdentity}, + sideload::{ + TeamSelection, application::Application, builder::MaxCertsBehavior, + cert_identity::CertificateIdentity, + }, util::{device::IdeviceInfo, storage::SideloadingStorage}, }; @@ -69,10 +72,10 @@ impl Sideloader { ) .await?; - // info!( - // "Using certificate for machine {} with ID {}", - // cert_identity.machine_name, cert_identity.machine_id - // ); + let mut app = Application::new(app_path)?; + + let is_sidestore = app.is_sidestore(); + let is_lc_and_sidestore = app.is_lc_and_sidestore(); Ok(()) }