mirror of
https://github.com/nab138/isideload.git
synced 2026-03-02 14:36:16 +01:00
462 lines
15 KiB
Rust
462 lines
15 KiB
Rust
// This file was made using https://github.com/Dadoum/Sideloader as a reference.
|
|
|
|
use apple_codesign::{BundleSigner, SigningSettings};
|
|
use der::Encode;
|
|
use idevice::IdeviceService;
|
|
use idevice::lockdown::LockdownClient;
|
|
use idevice::provider::IdeviceProvider;
|
|
|
|
use crate::application::Application;
|
|
use crate::device::install_app;
|
|
use crate::{DeveloperTeam, Error, SideloadConfiguration, SideloadLogger};
|
|
use crate::{
|
|
certificate::CertificateIdentity,
|
|
developer_session::{DeveloperDeviceType, DeveloperSession},
|
|
};
|
|
use std::{io::Write, path::PathBuf};
|
|
|
|
fn error_and_return(logger: &dyn SideloadLogger, error: Error) -> Result<(), Error> {
|
|
logger.error(&error);
|
|
Err(error)
|
|
}
|
|
|
|
/// Signs and installs an `.ipa` or `.app` onto a device.
|
|
///
|
|
/// # Arguments
|
|
/// - `device_provider` - [`idevice::provider::IdeviceProvider`] for the device
|
|
/// - `dev_session` - Authenticated Apple developer session ([`crate::developer_session::DeveloperSession`]).
|
|
/// - `app_path` - Path to the `.ipa` file or `.app` bundle to sign and install
|
|
/// - `config` - Sideload configuration options ([`crate::SideloadConfiguration`])
|
|
pub async fn sideload_app(
|
|
device_provider: &impl IdeviceProvider,
|
|
dev_session: &DeveloperSession,
|
|
app_path: PathBuf,
|
|
config: SideloadConfiguration<'_>,
|
|
) -> Result<(), Error> {
|
|
let logger = config.logger;
|
|
let mut lockdown_client = match LockdownClient::connect(device_provider).await {
|
|
Ok(l) => l,
|
|
Err(e) => {
|
|
return error_and_return(logger, Error::IdeviceError(e));
|
|
}
|
|
};
|
|
|
|
if let Ok(pairing_file) = device_provider.get_pairing_file().await {
|
|
lockdown_client
|
|
.start_session(&pairing_file)
|
|
.await
|
|
.map_err(Error::IdeviceError)?;
|
|
}
|
|
|
|
let device_name = lockdown_client
|
|
.get_value(Some("DeviceName"), None)
|
|
.await
|
|
.map_err(Error::IdeviceError)?
|
|
.as_string()
|
|
.ok_or(Error::Generic(
|
|
"Failed to convert DeviceName to string".to_string(),
|
|
))?
|
|
.to_string();
|
|
|
|
let device_uuid = lockdown_client
|
|
.get_value(Some("UniqueDeviceID"), None)
|
|
.await
|
|
.map_err(Error::IdeviceError)?
|
|
.as_string()
|
|
.ok_or(Error::Generic(
|
|
"Failed to convert UniqueDeviceID to string".to_string(),
|
|
))?
|
|
.to_string();
|
|
|
|
let team = match dev_session.get_team().await {
|
|
Ok(t) => t,
|
|
Err(e) => {
|
|
return error_and_return(logger, e);
|
|
}
|
|
};
|
|
|
|
logger.log("Successfully retrieved team");
|
|
|
|
ensure_device_registered(logger, dev_session, &team, &device_uuid, &device_name).await?;
|
|
|
|
let cert = match CertificateIdentity::new(
|
|
&config.store_dir,
|
|
dev_session,
|
|
dev_session.account.apple_id.clone(),
|
|
config.machine_name,
|
|
)
|
|
.await
|
|
{
|
|
Ok(c) => c,
|
|
Err(e) => {
|
|
return error_and_return(logger, e);
|
|
}
|
|
};
|
|
|
|
logger.log("Successfully acquired certificate");
|
|
|
|
let mut list_app_id_response = match dev_session
|
|
.list_app_ids(DeveloperDeviceType::Ios, &team)
|
|
.await
|
|
{
|
|
Ok(ids) => ids,
|
|
Err(e) => {
|
|
return error_and_return(logger, e);
|
|
}
|
|
};
|
|
|
|
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(),
|
|
None => {
|
|
return error_and_return(
|
|
logger,
|
|
Error::InvalidBundle("No bundle identifier found in IPA".to_string()),
|
|
);
|
|
}
|
|
};
|
|
let main_app_id_str = format!("{}.{}", main_app_bundle_id, team.team_id);
|
|
let main_app_name = match app.bundle.bundle_name() {
|
|
Some(name) => name.to_string(),
|
|
None => {
|
|
return error_and_return(
|
|
logger,
|
|
Error::InvalidBundle("No bundle name found in IPA".to_string()),
|
|
);
|
|
}
|
|
};
|
|
|
|
let extensions = app.bundle.app_extensions_mut();
|
|
// for each extension, ensure it has a unique bundle identifier that starts with the main app's bundle identifier
|
|
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()) {
|
|
return error_and_return(
|
|
logger,
|
|
Error::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()..]
|
|
));
|
|
}
|
|
}
|
|
}
|
|
app.bundle.set_bundle_identifier(&main_app_id_str);
|
|
|
|
let extension_refs: Vec<_> = app.bundle.app_extensions().iter().collect();
|
|
let mut bundles_with_app_id = vec![&app.bundle];
|
|
bundles_with_app_id.extend(extension_refs);
|
|
|
|
let app_ids_to_register = bundles_with_app_id
|
|
.iter()
|
|
.filter(|bundle| {
|
|
let bundle_id = bundle.bundle_identifier().unwrap_or("");
|
|
!list_app_id_response
|
|
.app_ids
|
|
.iter()
|
|
.any(|app_id| app_id.identifier == bundle_id)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
if let Some(available) = list_app_id_response.available_quantity
|
|
&& app_ids_to_register.len() > available.try_into().unwrap()
|
|
{
|
|
return error_and_return(
|
|
logger,
|
|
Error::InvalidBundle(format!(
|
|
"This app requires {} app ids, but you only have {} 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("");
|
|
if let Err(e) = dev_session
|
|
.add_app_id(DeveloperDeviceType::Ios, &team, name, id)
|
|
.await
|
|
{
|
|
return error_and_return(logger, e);
|
|
}
|
|
}
|
|
list_app_id_response = match dev_session
|
|
.list_app_ids(DeveloperDeviceType::Ios, &team)
|
|
.await
|
|
{
|
|
Ok(ids) => ids,
|
|
Err(e) => {
|
|
return error_and_return(logger, e);
|
|
}
|
|
};
|
|
|
|
let mut 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();
|
|
let main_app_id = match app_ids
|
|
.iter()
|
|
.find(|app_id| app_id.identifier == main_app_id_str)
|
|
.cloned()
|
|
{
|
|
Some(id) => id,
|
|
None => {
|
|
return error_and_return(
|
|
logger,
|
|
Error::Generic(format!(
|
|
"Main app ID {} not found in registered app IDs",
|
|
main_app_id_str
|
|
)),
|
|
);
|
|
}
|
|
};
|
|
|
|
logger.log("Successfully registered app IDs");
|
|
|
|
for app_id in app_ids.iter_mut() {
|
|
let app_group_feature_enabled = app_id
|
|
.features
|
|
.get(
|
|
"APG3427HIY", /* Gotta love apple and their magic strings! */
|
|
)
|
|
.and_then(|v| v.as_boolean())
|
|
.ok_or(Error::Generic(
|
|
"App group feature not found in app id".to_string(),
|
|
))?;
|
|
if !app_group_feature_enabled {
|
|
let mut body = plist::Dictionary::new();
|
|
body.insert("APG3427HIY".to_string(), plist::Value::Boolean(true));
|
|
let new_features = match dev_session
|
|
.update_app_id(DeveloperDeviceType::Ios, &team, app_id, &body)
|
|
.await
|
|
{
|
|
Ok(new_feats) => new_feats,
|
|
Err(e) => {
|
|
return error_and_return(logger, e);
|
|
}
|
|
};
|
|
app_id.features = new_features;
|
|
}
|
|
}
|
|
|
|
let group_identifier = format!(
|
|
"group.{}",
|
|
if config.force_sidestore_app_group {
|
|
format!("com.SideStore.SideStore.{}", team.team_id)
|
|
} else {
|
|
main_app_id_str.clone()
|
|
}
|
|
);
|
|
|
|
if is_sidestore {
|
|
app.bundle.app_info.insert(
|
|
"ALTAppGroups".to_string(),
|
|
plist::Value::Array(vec![plist::Value::String(group_identifier.clone())]),
|
|
);
|
|
|
|
app.bundle.app_info.insert(
|
|
"ALTCertificateID".to_string(),
|
|
plist::Value::String(cert.get_serial_number().unwrap()),
|
|
);
|
|
|
|
match cert.to_pkcs12(&cert.machine_id) {
|
|
Ok(p12_bytes) => {
|
|
let alt_cert_path = app.bundle.bundle_dir.join("ALTCertificate.p12");
|
|
if alt_cert_path.exists() {
|
|
std::fs::remove_file(&alt_cert_path).map_err(Error::Filesystem)?;
|
|
}
|
|
|
|
let mut file = std::fs::File::create(&alt_cert_path).map_err(Error::Filesystem)?;
|
|
file.write_all(&p12_bytes).map_err(Error::Filesystem)?;
|
|
}
|
|
Err(e) => return error_and_return(logger, e),
|
|
}
|
|
}
|
|
|
|
let app_groups = match dev_session
|
|
.list_application_groups(DeveloperDeviceType::Ios, &team)
|
|
.await
|
|
{
|
|
Ok(groups) => groups,
|
|
Err(e) => {
|
|
return error_and_return(logger, e);
|
|
}
|
|
};
|
|
|
|
let matching_app_groups = app_groups
|
|
.iter()
|
|
.filter(|group| group.identifier == group_identifier.clone())
|
|
.collect::<Vec<_>>();
|
|
|
|
let app_group = if matching_app_groups.is_empty() {
|
|
match dev_session
|
|
.add_application_group(
|
|
DeveloperDeviceType::Ios,
|
|
&team,
|
|
&group_identifier,
|
|
&main_app_name,
|
|
)
|
|
.await
|
|
{
|
|
Ok(group) => group,
|
|
Err(e) => {
|
|
return error_and_return(logger, e);
|
|
}
|
|
}
|
|
} else {
|
|
matching_app_groups[0].clone()
|
|
};
|
|
|
|
//let mut provisioning_profiles: HashMap<String, ProvisioningProfile> = HashMap::new();
|
|
for app_id in app_ids {
|
|
let assign_res = dev_session
|
|
.assign_application_group_to_app_id(
|
|
DeveloperDeviceType::Ios,
|
|
&team,
|
|
&app_id,
|
|
&app_group,
|
|
)
|
|
.await;
|
|
if assign_res.is_err() {
|
|
return error_and_return(logger, assign_res.err().unwrap());
|
|
}
|
|
// let provisioning_profile = match account
|
|
// // This doesn't seem right to me, but it's what Sideloader does... Shouldn't it be downloading the provisioning profile for this app ID, not the main?
|
|
// .download_team_provisioning_profile(DeveloperDeviceType::Ios, &team, &main_app_id)
|
|
// .await
|
|
// {
|
|
// Ok(pp /* tee hee */) => pp,
|
|
// Err(e) => {
|
|
// return emit_error_and_return(
|
|
// &window,
|
|
// &format!("Failed to download provisioning profile: {:?}", e),
|
|
// );
|
|
// }
|
|
// };
|
|
// provisioning_profiles.insert(app_id.identifier.clone(), provisioning_profile);
|
|
}
|
|
|
|
logger.log("Successfully registered app groups");
|
|
|
|
let provisioning_profile = match dev_session
|
|
.download_team_provisioning_profile(DeveloperDeviceType::Ios, &team, &main_app_id)
|
|
.await
|
|
{
|
|
Ok(pp /* tee hee */) => pp,
|
|
Err(e) => {
|
|
return error_and_return(logger, e);
|
|
}
|
|
};
|
|
|
|
let profile_path = config
|
|
.store_dir
|
|
.join(format!("{}.mobileprovision", main_app_id_str));
|
|
|
|
if profile_path.exists() {
|
|
std::fs::remove_file(&profile_path).map_err(Error::Filesystem)?;
|
|
}
|
|
|
|
let mut file = std::fs::File::create(&profile_path).map_err(Error::Filesystem)?;
|
|
file.write_all(&provisioning_profile.encoded_profile)
|
|
.map_err(Error::Filesystem)?;
|
|
|
|
// Without this, zsign complains it can't find the provision file
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
file.sync_all().map_err(|e| Error::Filesystem(e))?;
|
|
drop(file);
|
|
}
|
|
|
|
app.bundle.write_info()?;
|
|
for ext in app.bundle.app_extensions_mut() {
|
|
ext.write_info()?;
|
|
}
|
|
|
|
// match ZSignOptions::new(app.bundle.bundle_dir.to_str().unwrap())
|
|
// .with_cert_file(cert.get_certificate_file_path().to_str().unwrap())
|
|
// .with_pkey_file(cert.get_private_key_file_path().to_str().unwrap())
|
|
// .with_prov_file(profile_path.to_str().unwrap())
|
|
// .sign()
|
|
// {
|
|
// Ok(_) => {}
|
|
// Err(e) => {
|
|
// return error_and_return(logger, Error::ZSignError(e));
|
|
// }
|
|
// };
|
|
|
|
let mut signer = BundleSigner::new_from_path(&app.bundle.bundle_dir)
|
|
.map_err(|e| Error::AppleCodesignError(Box::new(e)))?;
|
|
|
|
signer
|
|
.collect_nested_bundles()
|
|
.map_err(|e| Error::AppleCodesignError(Box::new(e)))?;
|
|
|
|
let mut settings = SigningSettings::default();
|
|
|
|
settings.set_signing_key(cert.private_key.clone(), cert.certificate.unwrap());
|
|
|
|
signer
|
|
.write_signed_bundle(&app.bundle.bundle_dir, &settings)
|
|
.map_err(|e| Error::AppleCodesignError(Box::new(e)))?;
|
|
logger.log("App signed!");
|
|
|
|
logger.log("Installing app (Transfer)... 0%");
|
|
|
|
let res = install_app(device_provider, &app.bundle.bundle_dir, |percentage| {
|
|
logger.log(&format!("Installing app... {}%", percentage));
|
|
})
|
|
.await;
|
|
if let Err(e) = res {
|
|
return error_and_return(logger, e);
|
|
}
|
|
|
|
if config.revoke_cert {
|
|
dev_session
|
|
.revoke_development_cert(DeveloperDeviceType::Ios, &team, &cert.get_serial_number()?)
|
|
.await?;
|
|
logger.log("Certificate revoked");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn ensure_device_registered(
|
|
logger: &dyn SideloadLogger,
|
|
dev_session: &DeveloperSession,
|
|
team: &DeveloperTeam,
|
|
uuid: &str,
|
|
name: &str,
|
|
) -> 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 == uuid) {
|
|
logger.log("Device not found in your account");
|
|
// TODO: Actually test!
|
|
dev_session
|
|
.add_device(DeveloperDeviceType::Ios, team, name, uuid)
|
|
.await?;
|
|
logger.log("Successfully added device to your account");
|
|
}
|
|
logger.log("Device is a development device");
|
|
Ok(())
|
|
}
|