mirror of
https://github.com/nab138/isideload.git
synced 2026-03-02 06:26:16 +01:00
Sign and install apps
This commit is contained in:
1717
Cargo.lock
generated
1717
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
11
README.md
11
README.md
@@ -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
|
||||
|
||||
- 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
|
||||
- Proper entitlement handling
|
||||
- actually parse macho files and stuff, right now it just uses the bare minimum and applies extra entitlements for livecontainer
|
||||
- Remove dependency on ring and reduce duplicate dependencies
|
||||
- partially just need to wait for the rust crypto ecosystem to get through another release cycle
|
||||
- More parallelism/caching for better performance
|
||||
|
||||
## Licensing
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ use tracing_subscriber::FmtSubscriber;
|
||||
async fn main() {
|
||||
isideload::init().expect("Failed to initialize error reporting");
|
||||
let subscriber = FmtSubscriber::builder()
|
||||
.with_max_level(Level::INFO)
|
||||
.with_max_level(Level::DEBUG)
|
||||
.finish();
|
||||
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ keyring-storage = ["keyring"]
|
||||
# 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.
|
||||
[dependencies]
|
||||
idevice = { version = "0.1.52", optional = true }
|
||||
idevice = { version = "0.1.52", optional = true, features = ["afc", "installation_proxy"]}
|
||||
plist = "1.8"
|
||||
plist-macro = "0.1.4"
|
||||
reqwest = { version = "0.13.2", features = ["json", "gzip"] }
|
||||
@@ -50,3 +50,4 @@ x509-certificate = "0.25"
|
||||
rcgen = { version = "0.14.7", default-features = false, features = ["aws_lc_rs", "pem"] }
|
||||
p12-keystore = "0.2.0"
|
||||
zip = { version = "7.4", default-features = false, features = ["deflate"] }
|
||||
apple-codesign = { path = "../../plume-apple-platform-rs/apple-codesign", default-features = false}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use idevice::IdeviceError;
|
||||
use rootcause::{
|
||||
hooks::{Hooks, context_formatter::ContextFormatterHook},
|
||||
prelude::*,
|
||||
@@ -25,6 +26,9 @@ pub enum SideloadError {
|
||||
|
||||
#[error("Invalid bundle: {0}")]
|
||||
InvalidBundle(String),
|
||||
|
||||
#[error(transparent)]
|
||||
IdeviceError(#[from] IdeviceError),
|
||||
}
|
||||
|
||||
// The default reqwest error formatter sucks and provides no info
|
||||
|
||||
@@ -5,7 +5,6 @@ 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 crate::sideload::cert_identity::CertificateIdentity;
|
||||
use rootcause::option_ext::OptionExt;
|
||||
@@ -13,6 +12,7 @@ use rootcause::prelude::*;
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tracing::info;
|
||||
use zip::ZipArchive;
|
||||
|
||||
pub struct Application {
|
||||
@@ -81,7 +81,8 @@ impl Application {
|
||||
}
|
||||
|
||||
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.SideStore.SideStore" => Some(SpecialApp::SideStore),
|
||||
_ => None,
|
||||
@@ -99,6 +100,10 @@ impl Application {
|
||||
return Some(SpecialApp::SideStoreLc);
|
||||
}
|
||||
|
||||
if bundle_id == "com.kdt.livecontainer" {
|
||||
return Some(SpecialApp::LiveContainer);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
@@ -154,7 +159,7 @@ impl Application {
|
||||
|
||||
pub async fn register_app_ids(
|
||||
&self,
|
||||
mode: &ExtensionsBehavior,
|
||||
//mode: &ExtensionsBehavior,
|
||||
dev_session: &mut DeveloperSession,
|
||||
team: &DeveloperTeam,
|
||||
) -> Result<Vec<AppId>, Report> {
|
||||
@@ -166,19 +171,16 @@ impl Application {
|
||||
.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::<Vec<_>>(),
|
||||
_ => todo!(),
|
||||
};
|
||||
let app_ids_to_register = 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::<Vec<_>>();
|
||||
|
||||
if let Some(available) = list_app_ids_response.available_quantity
|
||||
&& app_ids_to_register.len() > available.try_into().unwrap()
|
||||
@@ -220,10 +222,11 @@ impl Application {
|
||||
}
|
||||
let special = special.as_ref().unwrap();
|
||||
|
||||
if special == &SpecialApp::SideStoreLc
|
||||
|| special == &SpecialApp::SideStore
|
||||
|| special == &SpecialApp::AltStore
|
||||
{
|
||||
if matches!(
|
||||
special,
|
||||
SpecialApp::SideStoreLc | SpecialApp::SideStore | SpecialApp::AltStore
|
||||
) {
|
||||
info!("Injecting certificate for {}", special);
|
||||
self.bundle.app_info.insert(
|
||||
"ALTAppGroups".to_string(),
|
||||
plist::Value::Array(vec![plist::Value::String(group_identifier.to_string())]),
|
||||
@@ -265,6 +268,20 @@ impl Application {
|
||||
pub enum SpecialApp {
|
||||
SideStore,
|
||||
SideStoreLc,
|
||||
LiveContainer,
|
||||
AltStore,
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,34 +49,34 @@ pub enum ExtensionsBehaviorChoice {
|
||||
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<String>) -> ExtensionsBehaviorChoice),
|
||||
}
|
||||
// /// 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<String>) -> ExtensionsBehaviorChoice),
|
||||
// }
|
||||
|
||||
impl From<ExtensionsBehaviorChoice> for ExtensionsBehavior {
|
||||
fn from(choice: ExtensionsBehaviorChoice) -> Self {
|
||||
match choice {
|
||||
ExtensionsBehaviorChoice::ReuseMain => ExtensionsBehavior::ReuseMain,
|
||||
ExtensionsBehaviorChoice::RegisterAll => ExtensionsBehavior::RegisterAll,
|
||||
ExtensionsBehaviorChoice::RemoveExtensions => ExtensionsBehavior::RemoveExtensions,
|
||||
}
|
||||
}
|
||||
}
|
||||
// impl From<ExtensionsBehaviorChoice> 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<TeamSelection>,
|
||||
max_certs_behavior: Option<MaxCertsBehavior>,
|
||||
extensions_behavior: Option<ExtensionsBehavior>,
|
||||
//extensions_behavior: Option<ExtensionsBehavior>,
|
||||
storage: Option<Box<dyn SideloadingStorage>>,
|
||||
machine_name: Option<String>,
|
||||
}
|
||||
@@ -90,7 +90,7 @@ impl SideloaderBuilder {
|
||||
machine_name: None,
|
||||
apple_email,
|
||||
max_certs_behavior: None,
|
||||
extensions_behavior: None,
|
||||
// extensions_behavior: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,10 +128,10 @@ impl SideloaderBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn extensions_behavior(mut self, behavior: ExtensionsBehavior) -> Self {
|
||||
self.extensions_behavior = Some(behavior);
|
||||
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 {
|
||||
@@ -143,8 +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),
|
||||
// self.extensions_behavior
|
||||
// .unwrap_or(ExtensionsBehavior::RegisterAll),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::{
|
||||
|
||||
use crate::SideloadError;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Bundle {
|
||||
pub app_info: Dictionary,
|
||||
pub bundle_dir: PathBuf,
|
||||
@@ -134,6 +134,50 @@ impl Bundle {
|
||||
)?;
|
||||
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> {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
use apple_codesign::{
|
||||
SigningSettings,
|
||||
cryptography::{InMemoryPrivateKey, PrivateKey},
|
||||
};
|
||||
use hex::ToHex;
|
||||
use rcgen::{CertificateParams, DistinguishedName, DnType, KeyPair, PKCS_RSA_SHA256};
|
||||
use rootcause::prelude::*;
|
||||
@@ -9,7 +13,7 @@ use rsa::{
|
||||
|
||||
use sha2::{Digest, Sha256};
|
||||
use tracing::{error, info};
|
||||
use x509_certificate::X509Certificate;
|
||||
use x509_certificate::CapturedX509Certificate;
|
||||
|
||||
use crate::{
|
||||
SideloadError,
|
||||
@@ -25,8 +29,9 @@ use crate::{
|
||||
pub struct CertificateIdentity {
|
||||
pub machine_id: String,
|
||||
pub machine_name: String,
|
||||
pub certificate: X509Certificate,
|
||||
pub certificate: CapturedX509Certificate,
|
||||
pub private_key: RsaPrivateKey,
|
||||
pub signing_key: InMemoryPrivateKey,
|
||||
}
|
||||
|
||||
impl CertificateIdentity {
|
||||
@@ -63,7 +68,7 @@ impl CertificateIdentity {
|
||||
|
||||
pub fn get_serial_number(&self) -> String {
|
||||
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(
|
||||
@@ -75,6 +80,7 @@ impl CertificateIdentity {
|
||||
max_certs_behavior: &MaxCertsBehavior,
|
||||
) -> Result<Self, Report> {
|
||||
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;
|
||||
if let Ok(Some((cert, x509_cert))) = found {
|
||||
@@ -84,6 +90,7 @@ impl CertificateIdentity {
|
||||
machine_name: cert.machine_name.clone().unwrap_or_default(),
|
||||
certificate: x509_cert,
|
||||
private_key: pr,
|
||||
signing_key,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -107,6 +114,7 @@ impl CertificateIdentity {
|
||||
machine_name: cert.machine_name.clone().unwrap_or_default(),
|
||||
certificate: x509_cert,
|
||||
private_key: pr,
|
||||
signing_key,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -139,7 +147,7 @@ impl CertificateIdentity {
|
||||
machine_name: &str,
|
||||
developer_session: &mut DeveloperSession,
|
||||
team: &DeveloperTeam,
|
||||
) -> Result<Option<(DevelopmentCertificate, X509Certificate)>, Report> {
|
||||
) -> Result<Option<(DevelopmentCertificate, CapturedX509Certificate)>, Report> {
|
||||
let public_key_der = private_key
|
||||
.to_public_key()
|
||||
.to_pkcs1_der()?
|
||||
@@ -156,7 +164,7 @@ impl CertificateIdentity {
|
||||
})
|
||||
{
|
||||
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() {
|
||||
return Ok(Some((cert.clone(), x509_cert)));
|
||||
@@ -172,7 +180,7 @@ impl CertificateIdentity {
|
||||
developer_session: &mut DeveloperSession,
|
||||
team: &DeveloperTeam,
|
||||
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 mut i = 0;
|
||||
@@ -196,7 +204,7 @@ impl CertificateIdentity {
|
||||
report!("Failed to find certificate after submitting CSR")
|
||||
})?;
|
||||
|
||||
let x509_cert = X509Certificate::from_der(
|
||||
let x509_cert = CapturedX509Certificate::from_der(
|
||||
apple_cert
|
||||
.cert_content
|
||||
.as_ref()
|
||||
@@ -264,6 +272,11 @@ impl CertificateIdentity {
|
||||
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(
|
||||
developer_session: &mut DeveloperSession,
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
95
isideload/src/sideload/install.rs
Normal file
95
isideload/src/sideload/install.rs
Normal 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(())
|
||||
})
|
||||
}
|
||||
@@ -2,5 +2,7 @@ pub mod application;
|
||||
pub mod builder;
|
||||
pub mod bundle;
|
||||
pub mod cert_identity;
|
||||
#[cfg(feature = "install")]
|
||||
pub mod install;
|
||||
pub mod sideloader;
|
||||
pub use builder::{SideloaderBuilder, TeamSelection};
|
||||
|
||||
@@ -9,16 +9,19 @@ use crate::{
|
||||
sideload::{
|
||||
TeamSelection,
|
||||
application::{Application, SpecialApp},
|
||||
builder::{ExtensionsBehavior, MaxCertsBehavior},
|
||||
builder::MaxCertsBehavior,
|
||||
cert_identity::CertificateIdentity,
|
||||
},
|
||||
util::{device::IdeviceInfo, storage::SideloadingStorage},
|
||||
util::{device::IdeviceInfo, plist::PlistDataExtract, storage::SideloadingStorage},
|
||||
};
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use apple_codesign::{SigningSettings, UnifiedSigner};
|
||||
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;
|
||||
|
||||
pub struct Sideloader {
|
||||
@@ -28,7 +31,7 @@ pub struct Sideloader {
|
||||
machine_name: String,
|
||||
apple_email: String,
|
||||
max_certs_behavior: MaxCertsBehavior,
|
||||
extensions_behavior: ExtensionsBehavior,
|
||||
//extensions_behavior: ExtensionsBehavior,
|
||||
}
|
||||
|
||||
impl Sideloader {
|
||||
@@ -42,7 +45,7 @@ impl Sideloader {
|
||||
max_certs_behavior: MaxCertsBehavior,
|
||||
machine_name: String,
|
||||
storage: Box<dyn SideloadingStorage>,
|
||||
extensions_behavior: ExtensionsBehavior,
|
||||
//extensions_behavior: ExtensionsBehavior,
|
||||
) -> Self {
|
||||
Sideloader {
|
||||
team_selection,
|
||||
@@ -51,24 +54,20 @@ impl Sideloader {
|
||||
machine_name,
|
||||
apple_email,
|
||||
max_certs_behavior,
|
||||
extensions_behavior,
|
||||
//extensions_behavior,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sign and install an app
|
||||
pub async fn install_app(
|
||||
pub async fn sign_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?;
|
||||
|
||||
team: Option<DeveloperTeam>,
|
||||
) -> Result<PathBuf, Report> {
|
||||
let team = match team {
|
||||
Some(t) => t,
|
||||
None => self.get_team().await?,
|
||||
};
|
||||
let cert_identity = CertificateIdentity::retrieve(
|
||||
&self.machine_name,
|
||||
&self.apple_email,
|
||||
@@ -87,7 +86,10 @@ impl Sideloader {
|
||||
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)
|
||||
.register_app_ids(
|
||||
/*&self.extensions_behavior, */ &mut self.dev_session,
|
||||
&team,
|
||||
)
|
||||
.await?;
|
||||
let main_app_id = match app_ids
|
||||
.iter()
|
||||
@@ -130,7 +132,8 @@ impl Sideloader {
|
||||
}
|
||||
|
||||
app.apply_special_app_behavior(&special, &group_identifier, &cert_identity)
|
||||
.await?;
|
||||
.await
|
||||
.context("Failed to modify app bundle")?;
|
||||
|
||||
let provisioning_profile = self
|
||||
.dev_session
|
||||
@@ -145,9 +148,67 @@ impl Sideloader {
|
||||
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
|
||||
pub async fn get_team(&mut self) -> Result<DeveloperTeam, Report> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user