Sign and install apps

This commit is contained in:
nab138
2026-02-14 01:10:01 -05:00
parent eefbdcafe7
commit 81d79ca11b
12 changed files with 2096 additions and 101 deletions

1717
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,12 +10,11 @@ This branch is home to isideload-next, the next major version of isideload. It f
Things left todo before the rewrite is considered finished Things left todo before the rewrite is considered finished
- Download provisioning profiles - Proper entitlement handling
- Signing apps - actually parse macho files and stuff, right now it just uses the bare minimum and applies extra entitlements for livecontainer
- Installing apps - Remove dependency on ring and reduce duplicate dependencies
(will superceed the original isideload at this point) - partially just need to wait for the rust crypto ecosystem to get through another release cycle
- Remove dependency on ring - More parallelism/caching for better performance
- More parallelism/cachng for better performance
## Licensing ## Licensing

View File

@@ -19,7 +19,7 @@ use tracing_subscriber::FmtSubscriber;
async fn main() { async fn main() {
isideload::init().expect("Failed to initialize error reporting"); isideload::init().expect("Failed to initialize error reporting");
let subscriber = FmtSubscriber::builder() let subscriber = FmtSubscriber::builder()
.with_max_level(Level::INFO) .with_max_level(Level::DEBUG)
.finish(); .finish();
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");

View File

@@ -19,7 +19,7 @@ keyring-storage = ["keyring"]
# Once that becomes stable, hopefuly duplicate dependencies should clean up.\ # Once that becomes stable, hopefuly duplicate dependencies should clean up.\
# Until then, I will wince in pain every time I see how long the output of cargo tree -d is. # Until then, I will wince in pain every time I see how long the output of cargo tree -d is.
[dependencies] [dependencies]
idevice = { version = "0.1.52", optional = true } idevice = { version = "0.1.52", optional = true, features = ["afc", "installation_proxy"]}
plist = "1.8" plist = "1.8"
plist-macro = "0.1.4" plist-macro = "0.1.4"
reqwest = { version = "0.13.2", features = ["json", "gzip"] } reqwest = { version = "0.13.2", features = ["json", "gzip"] }
@@ -49,4 +49,5 @@ keyring = { version = "3.6.3", features = ["apple-native", "linux-native-sync-pe
x509-certificate = "0.25" x509-certificate = "0.25"
rcgen = { version = "0.14.7", default-features = false, features = ["aws_lc_rs", "pem"] } rcgen = { version = "0.14.7", default-features = false, features = ["aws_lc_rs", "pem"] }
p12-keystore = "0.2.0" p12-keystore = "0.2.0"
zip = { version = "7.4", default-features = false, features = ["deflate"] } zip = { version = "7.4", default-features = false, features = ["deflate"] }
apple-codesign = { path = "../../plume-apple-platform-rs/apple-codesign", default-features = false}

View File

@@ -1,3 +1,4 @@
use idevice::IdeviceError;
use rootcause::{ use rootcause::{
hooks::{Hooks, context_formatter::ContextFormatterHook}, hooks::{Hooks, context_formatter::ContextFormatterHook},
prelude::*, prelude::*,
@@ -25,6 +26,9 @@ pub enum SideloadError {
#[error("Invalid bundle: {0}")] #[error("Invalid bundle: {0}")]
InvalidBundle(String), InvalidBundle(String),
#[error(transparent)]
IdeviceError(#[from] IdeviceError),
} }
// The default reqwest error formatter sucks and provides no info // The default reqwest error formatter sucks and provides no info

View File

@@ -5,7 +5,6 @@ use crate::SideloadError;
use crate::dev::app_ids::{AppId, AppIdsApi}; use crate::dev::app_ids::{AppId, AppIdsApi};
use crate::dev::developer_session::DeveloperSession; use crate::dev::developer_session::DeveloperSession;
use crate::dev::teams::DeveloperTeam; use crate::dev::teams::DeveloperTeam;
use crate::sideload::builder::ExtensionsBehavior;
use crate::sideload::bundle::Bundle; use crate::sideload::bundle::Bundle;
use crate::sideload::cert_identity::CertificateIdentity; use crate::sideload::cert_identity::CertificateIdentity;
use rootcause::option_ext::OptionExt; use rootcause::option_ext::OptionExt;
@@ -13,6 +12,7 @@ use rootcause::prelude::*;
use std::fs::File; use std::fs::File;
use std::path::PathBuf; use std::path::PathBuf;
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use tracing::info;
use zip::ZipArchive; use zip::ZipArchive;
pub struct Application { pub struct Application {
@@ -81,7 +81,8 @@ impl Application {
} }
pub fn get_special_app(&self) -> Option<SpecialApp> { pub fn get_special_app(&self) -> Option<SpecialApp> {
let special_app = match self.bundle.bundle_identifier().unwrap_or("") { let bundle_id = self.bundle.bundle_identifier().unwrap_or("");
let special_app = match bundle_id {
"com.rileytestut.AltStore" => Some(SpecialApp::AltStore), "com.rileytestut.AltStore" => Some(SpecialApp::AltStore),
"com.SideStore.SideStore" => Some(SpecialApp::SideStore), "com.SideStore.SideStore" => Some(SpecialApp::SideStore),
_ => None, _ => None,
@@ -99,6 +100,10 @@ impl Application {
return Some(SpecialApp::SideStoreLc); return Some(SpecialApp::SideStoreLc);
} }
if bundle_id == "com.kdt.livecontainer" {
return Some(SpecialApp::LiveContainer);
}
None None
} }
@@ -154,7 +159,7 @@ impl Application {
pub async fn register_app_ids( pub async fn register_app_ids(
&self, &self,
mode: &ExtensionsBehavior, //mode: &ExtensionsBehavior,
dev_session: &mut DeveloperSession, dev_session: &mut DeveloperSession,
team: &DeveloperTeam, team: &DeveloperTeam,
) -> Result<Vec<AppId>, Report> { ) -> Result<Vec<AppId>, Report> {
@@ -166,19 +171,16 @@ impl Application {
.list_app_ids(&team, None) .list_app_ids(&team, None)
.await .await
.context("Failed to list app IDs for the developer team")?; .context("Failed to list app IDs for the developer team")?;
let app_ids_to_register = match mode { let app_ids_to_register = bundles_with_app_id
ExtensionsBehavior::RegisterAll => bundles_with_app_id .iter()
.iter() .filter(|bundle| {
.filter(|bundle| { let bundle_id = bundle.bundle_identifier().unwrap_or("");
let bundle_id = bundle.bundle_identifier().unwrap_or(""); !list_app_ids_response
!list_app_ids_response .app_ids
.app_ids .iter()
.iter() .any(|app_id| app_id.identifier == bundle_id)
.any(|app_id| app_id.identifier == bundle_id) })
}) .collect::<Vec<_>>();
.collect::<Vec<_>>(),
_ => todo!(),
};
if let Some(available) = list_app_ids_response.available_quantity if let Some(available) = list_app_ids_response.available_quantity
&& app_ids_to_register.len() > available.try_into().unwrap() && app_ids_to_register.len() > available.try_into().unwrap()
@@ -220,10 +222,11 @@ impl Application {
} }
let special = special.as_ref().unwrap(); let special = special.as_ref().unwrap();
if special == &SpecialApp::SideStoreLc if matches!(
|| special == &SpecialApp::SideStore special,
|| special == &SpecialApp::AltStore SpecialApp::SideStoreLc | SpecialApp::SideStore | SpecialApp::AltStore
{ ) {
info!("Injecting certificate for {}", special);
self.bundle.app_info.insert( self.bundle.app_info.insert(
"ALTAppGroups".to_string(), "ALTAppGroups".to_string(),
plist::Value::Array(vec![plist::Value::String(group_identifier.to_string())]), plist::Value::Array(vec![plist::Value::String(group_identifier.to_string())]),
@@ -265,6 +268,20 @@ impl Application {
pub enum SpecialApp { pub enum SpecialApp {
SideStore, SideStore,
SideStoreLc, SideStoreLc,
LiveContainer,
AltStore, AltStore,
StikStore, StikStore,
} }
// impl display
impl std::fmt::Display for SpecialApp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SpecialApp::SideStore => write!(f, "SideStore"),
SpecialApp::SideStoreLc => write!(f, "SideStore+LiveContainer"),
SpecialApp::LiveContainer => write!(f, "LiveContainer"),
SpecialApp::AltStore => write!(f, "AltStore"),
SpecialApp::StikStore => write!(f, "StikStore"),
}
}
}

View File

@@ -49,34 +49,34 @@ pub enum ExtensionsBehaviorChoice {
RemoveExtensions, RemoveExtensions,
} }
/// Behavior used when an app contains sub bundles // /// Behavior used when an app contains sub bundles
pub enum ExtensionsBehavior { // pub enum ExtensionsBehavior {
/// Use the main app id/profile for all sub-bundles // /// Use the main app id/profile for all sub-bundles
ReuseMain, // ReuseMain,
/// Create separate app ids/profiles for each sub-bundle // /// Create separate app ids/profiles for each sub-bundle
RegisterAll, // RegisterAll,
/// Remove all sub-bundles // /// Remove all sub-bundles
RemoveExtensions, // RemoveExtensions,
/// Prompt the user to choose one of the above behaviors // /// Prompt the user to choose one of the above behaviors
Prompt(fn(&Vec<String>) -> ExtensionsBehaviorChoice), // Prompt(fn(&Vec<String>) -> ExtensionsBehaviorChoice),
} // }
impl From<ExtensionsBehaviorChoice> for ExtensionsBehavior { // impl From<ExtensionsBehaviorChoice> for ExtensionsBehavior {
fn from(choice: ExtensionsBehaviorChoice) -> Self { // fn from(choice: ExtensionsBehaviorChoice) -> Self {
match choice { // match choice {
ExtensionsBehaviorChoice::ReuseMain => ExtensionsBehavior::ReuseMain, // ExtensionsBehaviorChoice::ReuseMain => ExtensionsBehavior::ReuseMain,
ExtensionsBehaviorChoice::RegisterAll => ExtensionsBehavior::RegisterAll, // ExtensionsBehaviorChoice::RegisterAll => ExtensionsBehavior::RegisterAll,
ExtensionsBehaviorChoice::RemoveExtensions => ExtensionsBehavior::RemoveExtensions, // ExtensionsBehaviorChoice::RemoveExtensions => ExtensionsBehavior::RemoveExtensions,
} // }
} // }
} // }
pub struct SideloaderBuilder { pub struct SideloaderBuilder {
developer_session: DeveloperSession, developer_session: DeveloperSession,
apple_email: String, apple_email: String,
team_selection: Option<TeamSelection>, team_selection: Option<TeamSelection>,
max_certs_behavior: Option<MaxCertsBehavior>, max_certs_behavior: Option<MaxCertsBehavior>,
extensions_behavior: Option<ExtensionsBehavior>, //extensions_behavior: Option<ExtensionsBehavior>,
storage: Option<Box<dyn SideloadingStorage>>, storage: Option<Box<dyn SideloadingStorage>>,
machine_name: Option<String>, machine_name: Option<String>,
} }
@@ -90,7 +90,7 @@ impl SideloaderBuilder {
machine_name: None, machine_name: None,
apple_email, apple_email,
max_certs_behavior: None, max_certs_behavior: None,
extensions_behavior: None, // extensions_behavior: None,
} }
} }
@@ -128,10 +128,10 @@ impl SideloaderBuilder {
self self
} }
pub fn extensions_behavior(mut self, behavior: ExtensionsBehavior) -> Self { // pub fn extensions_behavior(mut self, behavior: ExtensionsBehavior) -> Self {
self.extensions_behavior = Some(behavior); // self.extensions_behavior = Some(behavior);
self // self
} // }
/// Build the `Sideloader` instance with the provided configuration /// Build the `Sideloader` instance with the provided configuration
pub fn build(self) -> Sideloader { pub fn build(self) -> Sideloader {
@@ -143,8 +143,8 @@ impl SideloaderBuilder {
self.machine_name.unwrap_or_else(|| "isideload".to_string()), self.machine_name.unwrap_or_else(|| "isideload".to_string()),
self.storage self.storage
.unwrap_or_else(|| Box::new(crate::util::storage::new_storage())), .unwrap_or_else(|| Box::new(crate::util::storage::new_storage())),
self.extensions_behavior // self.extensions_behavior
.unwrap_or(ExtensionsBehavior::RegisterAll), // .unwrap_or(ExtensionsBehavior::RegisterAll),
) )
} }
} }

View File

@@ -10,7 +10,7 @@ use std::{
use crate::SideloadError; use crate::SideloadError;
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct Bundle { pub struct Bundle {
pub app_info: Dictionary, pub app_info: Dictionary,
pub bundle_dir: PathBuf, pub bundle_dir: PathBuf,
@@ -134,6 +134,50 @@ impl Bundle {
)?; )?;
Ok(()) Ok(())
} }
fn from_dylib_path(dylib_path: PathBuf) -> Self {
Self {
app_info: Dictionary::new(),
bundle_dir: dylib_path,
app_extensions: Vec::new(),
frameworks: Vec::new(),
_libraries: Vec::new(),
}
}
fn collect_dylib_bundles(&self) -> Vec<Bundle> {
self._libraries
.iter()
.map(|relative| Self::from_dylib_path(self.bundle_dir.join(relative)))
.collect()
}
fn collect_nested_bundles_into(&self, bundles: &mut Vec<Bundle>) {
for bundle in &self.app_extensions {
bundles.push(bundle.clone());
bundle.collect_nested_bundles_into(bundles);
}
for bundle in &self.frameworks {
bundles.push(bundle.clone());
bundle.collect_nested_bundles_into(bundles);
}
}
pub fn collect_nested_bundles(&self) -> Vec<Bundle> {
let mut bundles = Vec::new();
self.collect_nested_bundles_into(&mut bundles);
bundles.extend(self.collect_dylib_bundles());
bundles
}
pub fn collect_bundles_sorted(&self) -> Vec<Bundle> {
let mut bundles = self.collect_nested_bundles();
bundles.push(self.clone());
bundles.sort_by_key(|b| b.bundle_dir.components().count());
bundles.reverse();
bundles
}
} }
fn assert_bundle(condition: bool, msg: &str) -> Result<(), Report> { fn assert_bundle(condition: bool, msg: &str) -> Result<(), Report> {

View File

@@ -1,3 +1,7 @@
use apple_codesign::{
SigningSettings,
cryptography::{InMemoryPrivateKey, PrivateKey},
};
use hex::ToHex; use hex::ToHex;
use rcgen::{CertificateParams, DistinguishedName, DnType, KeyPair, PKCS_RSA_SHA256}; use rcgen::{CertificateParams, DistinguishedName, DnType, KeyPair, PKCS_RSA_SHA256};
use rootcause::prelude::*; use rootcause::prelude::*;
@@ -9,7 +13,7 @@ use rsa::{
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use tracing::{error, info}; use tracing::{error, info};
use x509_certificate::X509Certificate; use x509_certificate::CapturedX509Certificate;
use crate::{ use crate::{
SideloadError, SideloadError,
@@ -25,8 +29,9 @@ use crate::{
pub struct CertificateIdentity { pub struct CertificateIdentity {
pub machine_id: String, pub machine_id: String,
pub machine_name: String, pub machine_name: String,
pub certificate: X509Certificate, pub certificate: CapturedX509Certificate,
pub private_key: RsaPrivateKey, pub private_key: RsaPrivateKey,
pub signing_key: InMemoryPrivateKey,
} }
impl CertificateIdentity { impl CertificateIdentity {
@@ -63,7 +68,7 @@ impl CertificateIdentity {
pub fn get_serial_number(&self) -> String { pub fn get_serial_number(&self) -> String {
let serial: String = self.certificate.serial_number_asn1().encode_hex(); let serial: String = self.certificate.serial_number_asn1().encode_hex();
serial.trim_start_matches('0').to_string() serial.trim_start_matches('0').to_string().to_uppercase()
} }
pub async fn retrieve( pub async fn retrieve(
@@ -75,6 +80,7 @@ impl CertificateIdentity {
max_certs_behavior: &MaxCertsBehavior, max_certs_behavior: &MaxCertsBehavior,
) -> Result<Self, Report> { ) -> Result<Self, Report> {
let pr = Self::retrieve_private_key(apple_email, storage).await?; let pr = Self::retrieve_private_key(apple_email, storage).await?;
let signing_key = Self::build_signing_key(&pr)?;
let found = Self::find_matching(&pr, machine_name, developer_session, team).await; let found = Self::find_matching(&pr, machine_name, developer_session, team).await;
if let Ok(Some((cert, x509_cert))) = found { if let Ok(Some((cert, x509_cert))) = found {
@@ -84,6 +90,7 @@ impl CertificateIdentity {
machine_name: cert.machine_name.clone().unwrap_or_default(), machine_name: cert.machine_name.clone().unwrap_or_default(),
certificate: x509_cert, certificate: x509_cert,
private_key: pr, private_key: pr,
signing_key,
}); });
} }
@@ -107,6 +114,7 @@ impl CertificateIdentity {
machine_name: cert.machine_name.clone().unwrap_or_default(), machine_name: cert.machine_name.clone().unwrap_or_default(),
certificate: x509_cert, certificate: x509_cert,
private_key: pr, private_key: pr,
signing_key,
}) })
} }
@@ -139,7 +147,7 @@ impl CertificateIdentity {
machine_name: &str, machine_name: &str,
developer_session: &mut DeveloperSession, developer_session: &mut DeveloperSession,
team: &DeveloperTeam, team: &DeveloperTeam,
) -> Result<Option<(DevelopmentCertificate, X509Certificate)>, Report> { ) -> Result<Option<(DevelopmentCertificate, CapturedX509Certificate)>, Report> {
let public_key_der = private_key let public_key_der = private_key
.to_public_key() .to_public_key()
.to_pkcs1_der()? .to_pkcs1_der()?
@@ -156,7 +164,7 @@ impl CertificateIdentity {
}) })
{ {
let x509_cert = let x509_cert =
X509Certificate::from_der(cert.cert_content.as_ref().unwrap().as_ref())?; CapturedX509Certificate::from_der(cert.cert_content.as_ref().unwrap().as_ref())?;
if public_key_der == x509_cert.public_key_data().as_ref() { if public_key_der == x509_cert.public_key_data().as_ref() {
return Ok(Some((cert.clone(), x509_cert))); return Ok(Some((cert.clone(), x509_cert)));
@@ -172,7 +180,7 @@ impl CertificateIdentity {
developer_session: &mut DeveloperSession, developer_session: &mut DeveloperSession,
team: &DeveloperTeam, team: &DeveloperTeam,
max_certs_behavior: &MaxCertsBehavior, max_certs_behavior: &MaxCertsBehavior,
) -> Result<(DevelopmentCertificate, X509Certificate), Report> { ) -> Result<(DevelopmentCertificate, CapturedX509Certificate), Report> {
let csr = Self::build_csr(private_key).context("Failed to generate CSR")?; let csr = Self::build_csr(private_key).context("Failed to generate CSR")?;
let mut i = 0; let mut i = 0;
@@ -196,7 +204,7 @@ impl CertificateIdentity {
report!("Failed to find certificate after submitting CSR") report!("Failed to find certificate after submitting CSR")
})?; })?;
let x509_cert = X509Certificate::from_der( let x509_cert = CapturedX509Certificate::from_der(
apple_cert apple_cert
.cert_content .cert_content
.as_ref() .as_ref()
@@ -264,6 +272,11 @@ impl CertificateIdentity {
Ok(params.serialize_request(&subject_key)?.pem()?) Ok(params.serialize_request(&subject_key)?.pem()?)
} }
fn build_signing_key(private_key: &RsaPrivateKey) -> Result<InMemoryPrivateKey, Report> {
let pkcs8 = private_key.to_pkcs8_der()?;
Ok(InMemoryPrivateKey::from_pkcs8_der(pkcs8.as_bytes())?)
}
async fn revoke_others( async fn revoke_others(
developer_session: &mut DeveloperSession, developer_session: &mut DeveloperSession,
team: &DeveloperTeam, team: &DeveloperTeam,
@@ -310,4 +323,18 @@ impl CertificateIdentity {
} }
} }
} }
pub fn setup_signing_settings<'a>(
&'a self,
settings: &mut SigningSettings<'a>,
) -> Result<(), Report> {
settings.set_signing_key(
self.signing_key.as_key_info_signer(),
self.certificate.clone(),
);
settings.chain_apple_certificates();
settings.set_team_id_from_signing_certificate();
Ok(())
}
} }

View File

@@ -0,0 +1,95 @@
use idevice::{
IdeviceService, afc::AfcClient, installation_proxy::InstallationProxyClient,
provider::IdeviceProvider,
};
use plist_macro::plist;
use rootcause::prelude::*;
use crate::SideloadError as Error;
use std::pin::Pin;
use std::{future::Future, path::Path};
/// Installs an ***already signed*** app onto your device.
/// To sign and install an app, see [`crate::sideload::sideload_app`]
pub async fn install_app(
provider: &impl IdeviceProvider,
app_path: &Path,
progress_callback: impl Fn(u64),
) -> Result<(), Report> {
let mut afc_client = AfcClient::connect(provider)
.await
.map_err(Error::IdeviceError)?;
let dir = format!(
"PublicStaging/{}",
app_path.file_name().unwrap().to_string_lossy()
);
afc_upload_dir(&mut afc_client, app_path, &dir).await?;
let mut instproxy_client = InstallationProxyClient::connect(provider)
.await
.map_err(Error::IdeviceError)?;
let options = plist!(dict {
"PackageType": "Developer"
});
instproxy_client
.install_with_callback(
dir,
Some(plist::Value::Dictionary(options)),
async |(percentage, _)| {
progress_callback(percentage);
},
(),
)
.await
.map_err(Error::IdeviceError)?;
Ok(())
}
fn afc_upload_dir<'a>(
afc_client: &'a mut AfcClient,
path: &'a Path,
afc_path: &'a str,
) -> Pin<Box<dyn Future<Output = Result<(), Report>> + Send + 'a>> {
Box::pin(async move {
let entries = std::fs::read_dir(path)?;
afc_client
.mk_dir(afc_path)
.await
.map_err(Error::IdeviceError)?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
let new_afc_path = format!(
"{}/{}",
afc_path,
path.file_name().unwrap().to_string_lossy()
);
afc_upload_dir(afc_client, &path, &new_afc_path).await?;
} else {
let mut file_handle = afc_client
.open(
format!(
"{}/{}",
afc_path,
path.file_name().unwrap().to_string_lossy()
),
idevice::afc::opcode::AfcFopenMode::WrOnly,
)
.await
.map_err(Error::IdeviceError)?;
let bytes = std::fs::read(&path)?;
file_handle
.write_entire(&bytes)
.await
.map_err(Error::IdeviceError)?;
file_handle.close().await.map_err(Error::IdeviceError)?;
}
}
Ok(())
})
}

View File

@@ -2,5 +2,7 @@ pub mod application;
pub mod builder; pub mod builder;
pub mod bundle; pub mod bundle;
pub mod cert_identity; pub mod cert_identity;
#[cfg(feature = "install")]
pub mod install;
pub mod sideloader; pub mod sideloader;
pub use builder::{SideloaderBuilder, TeamSelection}; pub use builder::{SideloaderBuilder, TeamSelection};

View File

@@ -9,16 +9,19 @@ use crate::{
sideload::{ sideload::{
TeamSelection, TeamSelection,
application::{Application, SpecialApp}, application::{Application, SpecialApp},
builder::{ExtensionsBehavior, MaxCertsBehavior}, builder::MaxCertsBehavior,
cert_identity::CertificateIdentity, cert_identity::CertificateIdentity,
}, },
util::{device::IdeviceInfo, storage::SideloadingStorage}, util::{device::IdeviceInfo, plist::PlistDataExtract, storage::SideloadingStorage},
}; };
use std::path::PathBuf; use std::path::PathBuf;
use apple_codesign::{SigningSettings, UnifiedSigner};
use idevice::provider::IdeviceProvider; use idevice::provider::IdeviceProvider;
use rootcause::prelude::*; use plist::Dictionary;
use plist_macro::plist_to_xml_string;
use rootcause::{option_ext::OptionExt, prelude::*};
use tracing::info; use tracing::info;
pub struct Sideloader { pub struct Sideloader {
@@ -28,7 +31,7 @@ pub struct Sideloader {
machine_name: String, machine_name: String,
apple_email: String, apple_email: String,
max_certs_behavior: MaxCertsBehavior, max_certs_behavior: MaxCertsBehavior,
extensions_behavior: ExtensionsBehavior, //extensions_behavior: ExtensionsBehavior,
} }
impl Sideloader { impl Sideloader {
@@ -42,7 +45,7 @@ impl Sideloader {
max_certs_behavior: MaxCertsBehavior, max_certs_behavior: MaxCertsBehavior,
machine_name: String, machine_name: String,
storage: Box<dyn SideloadingStorage>, storage: Box<dyn SideloadingStorage>,
extensions_behavior: ExtensionsBehavior, //extensions_behavior: ExtensionsBehavior,
) -> Self { ) -> Self {
Sideloader { Sideloader {
team_selection, team_selection,
@@ -51,24 +54,20 @@ impl Sideloader {
machine_name, machine_name,
apple_email, apple_email,
max_certs_behavior, max_certs_behavior,
extensions_behavior, //extensions_behavior,
} }
} }
/// Sign and install an app /// Sign and install an app
pub async fn install_app( pub async fn sign_app(
&mut self, &mut self,
device_provider: &impl IdeviceProvider,
app_path: PathBuf, app_path: PathBuf,
) -> Result<(), Report> { team: Option<DeveloperTeam>,
let device_info = IdeviceInfo::from_device(device_provider).await?; ) -> Result<PathBuf, Report> {
let team = match team {
let team = self.get_team().await?; Some(t) => t,
None => self.get_team().await?,
self.dev_session };
.ensure_device_registered(&team, &device_info.name, &device_info.udid, None)
.await?;
let cert_identity = CertificateIdentity::retrieve( let cert_identity = CertificateIdentity::retrieve(
&self.machine_name, &self.machine_name,
&self.apple_email, &self.apple_email,
@@ -87,7 +86,10 @@ impl Sideloader {
let main_app_id_str = format!("{}.{}", main_bundle_id, team.team_id); let main_app_id_str = format!("{}.{}", main_bundle_id, team.team_id);
app.update_bundle_id(&main_bundle_id, &main_app_id_str)?; app.update_bundle_id(&main_bundle_id, &main_app_id_str)?;
let mut app_ids = app let mut app_ids = app
.register_app_ids(&self.extensions_behavior, &mut self.dev_session, &team) .register_app_ids(
/*&self.extensions_behavior, */ &mut self.dev_session,
&team,
)
.await?; .await?;
let main_app_id = match app_ids let main_app_id = match app_ids
.iter() .iter()
@@ -130,7 +132,8 @@ impl Sideloader {
} }
app.apply_special_app_behavior(&special, &group_identifier, &cert_identity) app.apply_special_app_behavior(&special, &group_identifier, &cert_identity)
.await?; .await
.context("Failed to modify app bundle")?;
let provisioning_profile = self let provisioning_profile = self
.dev_session .dev_session
@@ -145,9 +148,67 @@ impl Sideloader {
ext.write_info()?; ext.write_info()?;
} }
Ok(()) tokio::fs::write(
app.bundle.bundle_dir.join("embedded.mobileprovision"),
provisioning_profile.encoded_profile.as_ref(),
)
.await?;
let mut settings = Self::signing_settings(&cert_identity)?;
let entitlements: Dictionary = Self::entitlements_from_prov(
provisioning_profile.encoded_profile.as_ref(),
&special,
&team,
)?;
settings
.set_entitlements_xml(
apple_codesign::SettingsScope::Main,
plist_to_xml_string(&entitlements),
)
.context("Failed to set entitlements XML")?;
let signer = UnifiedSigner::new(settings);
for bundle in app.bundle.collect_bundles_sorted() {
info!("Signing bundle {}", bundle.bundle_dir.display());
signer
.sign_path_in_place(&bundle.bundle_dir)
.context(format!(
"Failed to sign bundle: {}",
bundle.bundle_dir.display()
))?;
}
info!("App signed!");
Ok(app.bundle.bundle_dir.clone())
} }
#[cfg(feature = "install")]
pub async fn install_app(
&mut self,
device_provider: &impl IdeviceProvider,
app_path: PathBuf,
) -> Result<(), Report> {
let device_info = IdeviceInfo::from_device(device_provider).await?;
let team = self.get_team().await?;
self.dev_session
.ensure_device_registered(&team, &device_info.name, &device_info.udid, None)
.await?;
let signed_app_path = self.sign_app(app_path, Some(team)).await?;
info!("Installing...");
crate::sideload::install::install_app(device_provider, &signed_app_path, |progress| {
info!("Installing: {}%", progress);
})
.await
.context("Failed to install app on device")?;
Ok(())
}
/// Get the developer team according to the configured team selection behavior /// Get the developer team according to the configured team selection behavior
pub async fn get_team(&mut self) -> Result<DeveloperTeam, Report> { pub async fn get_team(&mut self) -> Result<DeveloperTeam, Report> {
let teams = self.dev_session.list_teams().await?; let teams = self.dev_session.list_teams().await?;
@@ -175,4 +236,64 @@ impl Sideloader {
} }
}) })
} }
pub fn signing_settings<'a>(
cert: &'a CertificateIdentity,
) -> Result<SigningSettings<'a>, Report> {
let mut settings = SigningSettings::default();
cert.setup_signing_settings(&mut settings)?;
settings.set_for_notarization(false);
settings.set_shallow(true);
Ok(settings)
}
fn entitlements_from_prov(
data: &[u8],
special: &Option<SpecialApp>,
team: &DeveloperTeam,
) -> Result<Dictionary, Report> {
let start = data
.windows(6)
.position(|w| w == b"<plist")
.ok_or_report()?;
let end = data
.windows(8)
.rposition(|w| w == b"</plist>")
.ok_or_report()?
+ 8;
let plist_data = &data[start..end];
let plist = plist::Value::from_reader_xml(plist_data)?;
let mut entitlements = plist
.as_dictionary()
.ok_or_report()?
.get_dict("Entitlements")?
.clone();
if matches!(
special,
Some(SpecialApp::SideStoreLc) | Some(SpecialApp::LiveContainer)
) {
let mut keychain_access = vec![plist::Value::String(format!(
"{}.com.kdt.livecontainer.shared",
team.team_id
))];
for number in 1..128 {
keychain_access.push(plist::Value::String(format!(
"{}.com.kdt.livecontainer.shared.{}",
team.team_id, number
)));
}
entitlements.insert(
"keychain-access-groups".to_string(),
plist::Value::Array(keychain_access),
);
}
Ok(entitlements)
}
} }