diff --git a/README.md b/README.md index 8cf3427..77069b0 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,14 @@ This branch is home to isideload-next, the next major version of isideload. It f ## TODO -- [ ] Signing apps -- [ ] Installing apps -- [ ] Remove dependency on ring +Things left todo before the rewrite is considered finished + +- Download provisioning profiles +- Signing apps +- Installing apps +(will superceed the original isideload at this point) +- Remove dependency on ring +- More parallelism/cachng for better performance ## Licensing diff --git a/isideload/src/dev/app_ids.rs b/isideload/src/dev/app_ids.rs index c546438..58e6454 100644 --- a/isideload/src/dev/app_ids.rs +++ b/isideload/src/dev/app_ids.rs @@ -4,7 +4,7 @@ use crate::{ device_type::{DeveloperDeviceType, dev_url}, teams::DeveloperTeam, }, - util::plist::SensitivePlistAttachment, + util::plist::{PlistDataExtract, SensitivePlistAttachment}, }; use plist::{Data, Date, Dictionary, Value}; use plist_macro::plist; @@ -17,7 +17,7 @@ pub struct AppId { pub app_id_id: String, pub identifier: String, pub name: String, - pub features: Option, + pub features: Dictionary, pub expiration_date: Option, } @@ -178,3 +178,26 @@ impl AppIdsApi for DeveloperSession { self } } + +impl AppId { + pub async fn ensure_group_feature( + &mut self, + dev_session: &mut DeveloperSession, + team: &DeveloperTeam, + ) -> Result<(), Report> { + let app_group_feature_enabled = self.features.get_bool("APG3427HIY")?; + + if !app_group_feature_enabled { + let body = plist!(dict { + "APG3427HIY": true, + }); + let new_features = dev_session + .update_app_id(team, self, body, None) + .await? + .features; + self.features = new_features; + } + + Ok(()) + } +} diff --git a/isideload/src/sideload/application.rs b/isideload/src/sideload/application.rs index 91ba3fd..129dd08 100644 --- a/isideload/src/sideload/application.rs +++ b/isideload/src/sideload/application.rs @@ -2,7 +2,12 @@ // I'm planning on redoing this later to better handle entitlements, extensions, etc, but it will do for now use crate::SideloadError; +use crate::dev::app_ids::{AppId, AppIdsApi}; +use crate::dev::developer_session::DeveloperSession; +use crate::dev::teams::DeveloperTeam; +use crate::sideload::builder::ExtensionsBehavior; use crate::sideload::bundle::Bundle; +use rootcause::option_ext::OptionExt; use rootcause::prelude::*; use std::fs::File; use std::path::PathBuf; @@ -73,14 +78,140 @@ impl Application { }) } - pub fn is_sidestore(&self) -> bool { - self.bundle.bundle_identifier().unwrap_or("") == "com.SideStore.SideStore" - } + pub fn get_special_app(&self) -> Option { + let special_app = match self.bundle.bundle_identifier().unwrap_or("") { + "com.rileytestut.AltStore" => Some(SpecialApp::AltStore), + "com.SideStore.SideStore" => Some(SpecialApp::SideStore), + _ => None, + }; + if special_app.is_some() { + return special_app; + } - pub fn is_lc_and_sidestore(&self) -> bool { - self.bundle + if self + .bundle .frameworks() .iter() .any(|f| f.bundle_identifier().unwrap_or("") == "com.SideStore.SideStore") + { + return Some(SpecialApp::SideStoreLc); + } + + None + } + + pub fn main_bundle_id(&self) -> Result { + let str = self + .bundle + .bundle_identifier() + .ok_or_report() + .context("Failed to get main bundle identifier")? + .to_string(); + + Ok(str) + } + + pub fn main_app_name(&self) -> Result { + let str = self + .bundle + .bundle_name() + .ok_or_report() + .context("Failed to get main app name")? + .to_string(); + + Ok(str) + } + + pub fn update_bundle_id( + &mut self, + main_app_bundle_id: &str, + main_app_id_str: &str, + ) -> Result<(), Report> { + let extensions = self.bundle.app_extensions_mut(); + for ext in extensions.iter_mut() { + if let Some(id) = ext.bundle_identifier() { + if !(id.starts_with(&main_app_bundle_id) && id.len() > main_app_bundle_id.len()) { + bail!(SideloadError::InvalidBundle(format!( + "Extension {} is not part of the main app bundle identifier: {}", + ext.bundle_name().unwrap_or("Unknown"), + id + ))); + } else { + ext.set_bundle_identifier(&format!( + "{}{}", + main_app_id_str, + &id[main_app_bundle_id.len()..] + )); + } + } + } + self.bundle.set_bundle_identifier(&main_app_id_str); + + Ok(()) + } + + pub async fn register_app_ids( + &self, + mode: &ExtensionsBehavior, + dev_session: &mut DeveloperSession, + team: &DeveloperTeam, + ) -> Result, Report> { + let extension_refs: Vec<_> = self.bundle.app_extensions().iter().collect(); + let mut bundles_with_app_id = vec![&self.bundle]; + bundles_with_app_id.extend(extension_refs); + + let list_app_ids_response = dev_session + .list_app_ids(&team, None) + .await + .context("Failed to list app IDs for the developer team")?; + let app_ids_to_register = match mode { + ExtensionsBehavior::RegisterAll => bundles_with_app_id + .iter() + .filter(|bundle| { + let bundle_id = bundle.bundle_identifier().unwrap_or(""); + !list_app_ids_response + .app_ids + .iter() + .any(|app_id| app_id.identifier == bundle_id) + }) + .collect::>(), + _ => todo!(), + }; + + if let Some(available) = list_app_ids_response.available_quantity + && app_ids_to_register.len() > available.try_into().unwrap() + { + bail!( + "Not enough available app IDs. {} are required, but only {} are available.", + app_ids_to_register.len(), + available + ); + } + + for bundle in app_ids_to_register { + let id = bundle.bundle_identifier().unwrap_or(""); + let name = bundle.bundle_name().unwrap_or(""); + dev_session.add_app_id(&team, name, id, None).await?; + } + let list_app_id_response = dev_session.list_app_ids(&team, None).await?; + let app_ids: Vec<_> = list_app_id_response + .app_ids + .into_iter() + .filter(|app_id| { + bundles_with_app_id + .iter() + .any(|bundle| app_id.identifier == bundle.bundle_identifier().unwrap_or("")) + }) + .collect(); + + Ok(app_ids) } } + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SpecialApp { + SideStore, + SideStoreLc, + AltStore, + StikStore, +} diff --git a/isideload/src/sideload/builder.rs b/isideload/src/sideload/builder.rs index 776e218..fa70169 100644 --- a/isideload/src/sideload/builder.rs +++ b/isideload/src/sideload/builder.rs @@ -39,11 +39,44 @@ pub enum MaxCertsBehavior { Prompt(fn(&Vec) -> Option>), } +/// The actual behavior choices for extensions (non-prompt variants) +pub enum ExtensionsBehaviorChoice { + /// Use the main app id/profile for all sub-bundles + ReuseMain, + /// Create separate app ids/profiles for each sub-bundle + RegisterAll, + /// Remove all sub-bundles + RemoveExtensions, +} + +/// Behavior used when an app contains sub bundles +pub enum ExtensionsBehavior { + /// Use the main app id/profile for all sub-bundles + ReuseMain, + /// Create separate app ids/profiles for each sub-bundle + RegisterAll, + /// Remove all sub-bundles + RemoveExtensions, + /// Prompt the user to choose one of the above behaviors + Prompt(fn(&Vec) -> ExtensionsBehaviorChoice), +} + +impl From for ExtensionsBehavior { + fn from(choice: ExtensionsBehaviorChoice) -> Self { + match choice { + ExtensionsBehaviorChoice::ReuseMain => ExtensionsBehavior::ReuseMain, + ExtensionsBehaviorChoice::RegisterAll => ExtensionsBehavior::RegisterAll, + ExtensionsBehaviorChoice::RemoveExtensions => ExtensionsBehavior::RemoveExtensions, + } + } +} + pub struct SideloaderBuilder { developer_session: DeveloperSession, apple_email: String, team_selection: Option, max_certs_behavior: Option, + extensions_behavior: Option, storage: Option>, machine_name: Option, } @@ -57,6 +90,7 @@ impl SideloaderBuilder { machine_name: None, apple_email, max_certs_behavior: None, + extensions_behavior: None, } } @@ -94,6 +128,11 @@ impl SideloaderBuilder { self } + pub fn extensions_behavior(mut self, behavior: ExtensionsBehavior) -> Self { + self.extensions_behavior = Some(behavior); + self + } + /// Build the `Sideloader` instance with the provided configuration pub fn build(self) -> Sideloader { Sideloader::new( @@ -104,6 +143,8 @@ impl SideloaderBuilder { self.machine_name.unwrap_or_else(|| "isideload".to_string()), self.storage .unwrap_or_else(|| Box::new(crate::util::storage::new_storage())), + self.extensions_behavior + .unwrap_or(ExtensionsBehavior::RegisterAll), ) } } diff --git a/isideload/src/sideload/sideloader.rs b/isideload/src/sideload/sideloader.rs index 9b3d14e..a56ba39 100644 --- a/isideload/src/sideload/sideloader.rs +++ b/isideload/src/sideload/sideloader.rs @@ -5,7 +5,9 @@ use crate::{ teams::{DeveloperTeam, TeamsApi}, }, sideload::{ - TeamSelection, application::Application, builder::MaxCertsBehavior, + TeamSelection, + application::{Application, SpecialApp}, + builder::{ExtensionsBehavior, MaxCertsBehavior}, cert_identity::CertificateIdentity, }, util::{device::IdeviceInfo, storage::SideloadingStorage}, @@ -24,6 +26,7 @@ pub struct Sideloader { machine_name: String, apple_email: String, max_certs_behavior: MaxCertsBehavior, + extensions_behavior: ExtensionsBehavior, } impl Sideloader { @@ -37,6 +40,7 @@ impl Sideloader { max_certs_behavior: MaxCertsBehavior, machine_name: String, storage: Box, + extensions_behavior: ExtensionsBehavior, ) -> Self { Sideloader { team_selection, @@ -45,6 +49,7 @@ impl Sideloader { machine_name, apple_email, max_certs_behavior, + extensions_behavior, } } @@ -73,9 +78,44 @@ impl Sideloader { .await?; let mut app = Application::new(app_path)?; + let special = app.get_special_app(); - let is_sidestore = app.is_sidestore(); - let is_lc_and_sidestore = app.is_lc_and_sidestore(); + let main_bundle_id = app.main_bundle_id()?; + let main_app_name = app.main_app_name()?; + let main_app_id_str = format!("{}.{}", main_bundle_id, team.team_id); + app.update_bundle_id(&main_bundle_id, &main_app_id_str)?; + let mut app_ids = app + .register_app_ids(&self.extensions_behavior, &mut self.dev_session, &team) + .await?; + let main_app_id = match app_ids + .iter() + .find(|app_id| app_id.identifier == main_app_id_str) + { + Some(id) => id, + None => { + bail!( + "Main app ID {} not found in registered app IDs", + main_app_id_str + ); + } + }; + + for app_id in app_ids.iter_mut() { + app_id + .ensure_group_feature(&mut self.dev_session, &team) + .await?; + + // TODO: Increased memory entitlement + } + + let group_identifier = format!( + "group.{}", + if Some(SpecialApp::SideStoreLc) == special { + format!("com.SideStore.SideStore.{}", team.team_id) + } else { + main_app_id_str.clone() + } + ); Ok(()) } diff --git a/isideload/src/util/plist.rs b/isideload/src/util/plist.rs index be54ecf..6f7a8e0 100644 --- a/isideload/src/util/plist.rs +++ b/isideload/src/util/plist.rs @@ -55,6 +55,7 @@ pub trait PlistDataExtract { fn get_string(&self, key: &str) -> Result; fn get_signed_integer(&self, key: &str) -> Result; fn get_dict(&self, key: &str) -> Result<&Dictionary, Report>; + fn get_bool(&self, key: &str) -> Result; fn get_struct(&self, key: &str) -> Result; } @@ -120,4 +121,11 @@ impl PlistDataExtract for Dictionary { })?; Ok(struct_data) } + + fn get_bool(&self, key: &str) -> Result { + self.get(key).and_then(|v| v.as_boolean()).ok_or_else(|| { + report!("Plist missing boolean for key '{}'", key) + .attach(SensitivePlistAttachment::new(self.clone())) + }) + } }