Start isideload-next

This commit is contained in:
nab138
2025-12-16 07:50:38 -05:00
parent cdedc84e8b
commit 8f44e8e3a1
16 changed files with 40 additions and 7798 deletions

View File

@@ -15,8 +15,9 @@ jobs:
fail-fast: false
matrix:
include:
- platform: "ubuntu-22.04"
- platform: "ubuntu-latest"
- platform: "windows-latest"
- platform: "macos-latest"
runs-on: ${{ matrix.platform }}
steps:
@@ -29,16 +30,12 @@ jobs:
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-tauri-${{ hashFiles('Cargo.lock') }}
key: ${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
restore-keys: |
${{ runner.os }}-tauri-
${{ runner.os }}-
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Add MSVC to PATH
if: matrix.platform == 'windows-latest'
uses: ilammy/msvc-dev-cmd@v1
- name: Build
run: cargo build --features "vendored-openssl"
run: cargo build

5643
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
[workspace]
resolver = "2"
members = ["isideload", "examples/minimal"]
members = ["isideload"]
default-members = ["isideload"]

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 nab138
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -96,13 +96,13 @@ See [examples/minimal/src/main.rs](examples/minimal/src/main.rs).
## Licensing
This project is licensed under the MPL-2.0 License. See the [LICENSE](LICENSE) file for details.
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
## Credits
- The amazing [idevice](https://github.com/jkcoxson/idevice) crate is used to communicate with the device
- Packages from [`apple-private-apis`](https://github.com/SideStore/apple-private-apis) were used for authentication, but the original project was left unfinished. To support isideload, `apple-private-apis` was forked and modified to add missing features. With permission from the original developers, the fork was published to crates.io until the official project is published.
- Packages from [`apple-private-apis`](https://github.com/SideStore/apple-private-apis), which is licensed under MPL-2.0, were used for authentication, but the original project was left unfinished. To support isideload, `apple-private-apis` was [forked](https://github.com/nab138/apple-private-apis) and modified to add missing features. With permission from the original developers, the fork was published to crates.io until the official project is published.
- [apple-codesign](https://crates.io/crates/apple-codesign) was used for code signing, which is licensed under MPL-2.0.

View File

@@ -1,5 +0,0 @@
.zsign_cache
keys
*.ipa
state.plist
*.mobileprovision

View File

@@ -1,10 +0,0 @@
[package]
name = "minimal"
version = "0.1.0"
edition = "2024"
publish = false
[dependencies]
isideload = { path = "../../isideload" }
idevice = { version = "0.1.46", features = ["usbmuxd", "ring"], default-features = false}
tokio = { version = "1.43", features = ["macros", "rt-multi-thread"] }

View File

@@ -1,68 +0,0 @@
use std::{env, path::PathBuf, sync::Arc};
use idevice::usbmuxd::{UsbmuxdAddr, UsbmuxdConnection};
use isideload::{
AnisetteConfiguration, AppleAccount, SideloadConfiguration,
developer_session::DeveloperSession, sideload::sideload_app,
};
#[tokio::main]
async fn main() {
let args: Vec<String> = env::args().collect();
let app_path = PathBuf::from(
args.get(1)
.expect("Please provide the path to the app to install"),
);
let apple_id = args
.get(2)
.expect("Please provide the Apple ID to use for installation");
let apple_password = args.get(3).expect("Please provide the Apple ID password");
// You don't have to use usbmuxd, you can use any IdeviceProvider
let usbmuxd = UsbmuxdConnection::default().await;
if usbmuxd.is_err() {
panic!("Failed to connect to usbmuxd: {:?}", usbmuxd.err());
}
let mut usbmuxd = usbmuxd.unwrap();
let devs = usbmuxd.get_devices().await.unwrap();
if devs.is_empty() {
panic!("No devices found");
}
let provider = devs
.iter()
.next()
.unwrap()
.to_provider(UsbmuxdAddr::from_env_var().unwrap(), "isideload-demo");
// Change the anisette url and such here
// Note that right now only remote anisette servers are supported
let anisette_config = AnisetteConfiguration::default();
let get_2fa_code = || {
let mut code = String::new();
println!("Enter 2FA code:");
std::io::stdin().read_line(&mut code).unwrap();
Ok(code.trim().to_string())
};
let account = AppleAccount::login(
|| Ok((apple_id.to_string(), apple_password.to_string())),
get_2fa_code,
anisette_config,
)
.await
.unwrap();
let dev_session = DeveloperSession::new(Arc::new(account));
// You can change the machine name, store directory (for certs, anisette data, & provision files), and logger
let config = SideloadConfiguration::default()
.set_machine_name("isideload-demo".to_string())
.set_force_sidestore(true);
sideload_app(&provider, &dev_session, app_path, config)
.await
.unwrap()
}

View File

@@ -1,9 +1,9 @@
[package]
name = "isideload"
description = "Sideload iOS/iPadOS applications"
license = "MPL-2.0"
license = "MIT"
authors = ["Nicholas Sharp <nab@nabdev.me>"]
version = "0.1.21"
version = "0.2.0"
edition = "2024"
repository = "https://github.com/nab138/isideload"
documentation = "https://docs.rs/isideload"
@@ -14,19 +14,3 @@ readme = "../README.md"
default = []
[dependencies]
serde = { version = "1", features = ["derive"] }
plist = { version = "1.7" }
icloud_auth = { version = "0.1.5", package = "nab138_icloud_auth" }
uuid = { version = "1.17.0", features = ["v4"] }
zip = { version = "4.3", default-features = false, features = ["deflate"] }
hex = "0.4"
sha1 = "0.10"
idevice = { version = "0.1.46", features = ["afc", "installation_proxy", "ring"], default-features = false }
thiserror = "2"
apple-codesign = "0.29.0"
x509-certificate = "0.24.0"
rsa = "0.9"
rcgen = "0.13"
rand = "0.8"
tokio = "1.48.0"
p12 = "0.6.3"

View File

@@ -1,75 +0,0 @@
// This file was made using https://github.com/Dadoum/Sideloader as a reference.
use crate::Error;
use crate::bundle::Bundle;
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<Self, Error> {
if !path.exists() {
return Err(Error::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).map_err(Error::Filesystem)?;
}
std::fs::create_dir_all(&temp_path).map_err(Error::Filesystem)?;
let file = File::open(&path).map_err(Error::Filesystem)?;
let mut archive = ZipArchive::new(file).map_err(|e| {
Error::Generic(format!("Failed to open application archive: {}", e))
})?;
archive.extract(&temp_path).map_err(|e| {
Error::Generic(format!("Failed to extract application archive: {}", e))
})?;
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)
.map_err(|e| {
Error::Generic(format!("Failed to read Payload directory: {}", e))
})?
.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() {
return Err(Error::InvalidBundle(
"No .app directory found in Payload".to_string(),
));
} else {
return Err(Error::InvalidBundle(
"Multiple .app directories found in Payload".to_string(),
));
}
} else {
return Err(Error::InvalidBundle(
"No Payload directory found in the application archive".to_string(),
));
}
}
let bundle = Bundle::new(bundle_path)?;
Ok(Application {
bundle, /*temp_path*/
})
}
}

View File

@@ -1,216 +0,0 @@
// This file was made using https://github.com/Dadoum/Sideloader as a reference.
use crate::Error;
use plist::{Dictionary, Value};
use std::{
fs,
path::{Path, PathBuf},
};
#[derive(Debug, Clone)]
pub struct Bundle {
pub app_info: Dictionary,
pub bundle_dir: PathBuf,
pub bundle_type: BundleType,
app_extensions: Vec<Bundle>,
frameworks: Vec<Bundle>,
_libraries: Vec<String>,
}
impl Bundle {
pub fn new(bundle_dir: PathBuf) -> Result<Self, Error> {
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)
.map_err(|e| Error::InvalidBundle(format!("Failed to read Info.plist: {}", e)))?;
let app_info = plist::from_bytes(&plist_data)
.map_err(|e| Error::InvalidBundle(format!("Failed to parse Info.plist: {}", e)))?;
// 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)
.map_err(|e| {
Error::InvalidBundle(format!("Failed to read PlugIns directory: {}", e))
})?
.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)
.map_err(|e| {
Error::InvalidBundle(format!("Failed to read Frameworks directory: {}", e))
})?
.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_type: BundleType::from_extension(
bundle_path
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or(""),
),
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 write_info(&self) -> Result<(), Error> {
let info_plist_path = self.bundle_dir.join("Info.plist");
let result = plist::to_file_binary(&info_plist_path, &self.app_info);
if result.is_err() {
return Err(Error::InvalidBundle(format!(
"Failed to write Info.plist: {}",
result.unwrap_err()
)));
}
Ok(())
}
pub fn embedded_bundles(&self) -> Vec<&Bundle> {
let mut bundles = Vec::new();
bundles.extend(self.app_extensions.iter());
bundles.extend(self.frameworks.iter());
bundles.push(self);
bundles.sort_by_key(|b| b.bundle_dir.components().count());
bundles
}
}
fn assert_bundle(condition: bool, msg: &str) -> Result<(), Error> {
if !condition {
Err(Error::InvalidBundle(msg.to_string()))
} else {
Ok(())
}
}
fn find_dylibs(dir: &Path, bundle_root: &Path) -> Result<Vec<String>, Error> {
let mut libraries = Vec::new();
fn collect_dylibs(
dir: &Path,
bundle_root: &Path,
libraries: &mut Vec<String>,
) -> Result<(), Error> {
let entries = fs::read_dir(dir).map_err(|e| {
Error::InvalidBundle(format!("Failed to read directory {}: {}", dir.display(), e))
})?;
for entry in entries {
let entry = entry.map_err(|e| {
Error::InvalidBundle(format!("Failed to read directory entry: {}", e))
})?;
let path = entry.path();
let file_type = entry
.file_type()
.map_err(|e| Error::InvalidBundle(format!("Failed to get file type: {}", e)))?;
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)
}
// Borrowed from https://github.com/khcrysalis/PlumeImpactor/blob/main/crates/utils/src/bundle.rs
#[derive(Debug, Clone)]
pub enum BundleType {
App,
AppExtension,
Framework,
Unknown,
}
impl BundleType {
pub fn from_extension(ext: &str) -> Self {
match ext {
"app" => BundleType::App,
"appex" => BundleType::AppExtension,
"framework" => BundleType::Framework,
_ => BundleType::Unknown,
}
}
}

View File

@@ -1,301 +0,0 @@
// This file was made using https://github.com/Dadoum/Sideloader as a reference.
use apple_codesign::SigningSettings;
use hex;
use rcgen::{CertificateParams, DnType, KeyPair};
use rsa::{
RsaPrivateKey,
pkcs8::{DecodePrivateKey, EncodePrivateKey, LineEnding},
};
use sha1::{Digest, Sha1};
use std::{
fs,
path::{Path, PathBuf},
};
use x509_certificate::{CapturedX509Certificate, InMemorySigningKeyPair, Sign, X509Certificate};
use crate::Error;
use crate::developer_session::{DeveloperDeviceType, DeveloperSession, DeveloperTeam};
#[derive(Debug)]
pub struct CertificateIdentity {
pub certificate: Option<X509Certificate>,
pub key_pair: InMemorySigningKeyPair,
pub private_key: RsaPrivateKey,
pub key_file: PathBuf,
pub cert_file: PathBuf,
pub machine_name: String,
pub machine_id: String,
}
impl CertificateIdentity {
pub async fn new(
configuration_path: &Path,
dev_session: &DeveloperSession,
apple_id: String,
machine_name: String,
) -> Result<Self, Error> {
let mut hasher = Sha1::new();
hasher.update(apple_id.as_bytes());
let hash_string = hex::encode(hasher.finalize()).to_lowercase();
let key_path = configuration_path.join("keys").join(hash_string);
fs::create_dir_all(&key_path).map_err(Error::Filesystem)?;
let key_file = key_path.join("key.pem");
let cert_file = key_path.join("cert.pem");
let teams = dev_session.list_teams().await?;
let team = teams
.first()
.ok_or(Error::Certificate("No teams found".to_string()))?;
let private_key = if key_file.exists() {
let key_data = fs::read_to_string(&key_file)
.map_err(|e| Error::Certificate(format!("Failed to read key file: {}", e)))?;
RsaPrivateKey::from_pkcs8_pem(&key_data)
.map_err(|e| Error::Certificate(format!("Failed to load private key: {}", e)))?
} else {
let mut rng = rand::thread_rng();
let private_key = RsaPrivateKey::new(&mut rng, 2048)
.map_err(|e| Error::Certificate(format!("Failed to generate RSA key: {}", e)))?;
let pem_data = private_key
.to_pkcs8_pem(LineEnding::LF)
.map_err(|e| Error::Certificate(format!("Failed to encode private key: {}", e)))?;
fs::write(&key_file, pem_data.as_bytes()).map_err(Error::Filesystem)?;
private_key
};
let key_pair = InMemorySigningKeyPair::from_pkcs8_der(
private_key
.to_pkcs8_der()
.map_err(|e| Error::Certificate(format!("Failed to encode private key: {}", e)))?
.as_bytes(),
)
.map_err(|e| Error::Certificate(format!("Failed to decode private key: {}", e)))?;
let mut cert_identity = CertificateIdentity {
certificate: None,
key_pair,
private_key,
key_file,
cert_file,
machine_name,
machine_id: "".to_owned(),
};
if let Ok((cert, machine_id)) = cert_identity
.find_matching_certificate(dev_session, team)
.await
{
cert_identity.certificate = Some(cert.clone());
cert_identity.machine_id = machine_id;
let cert_pem = cert
.encode_pem()
.map_err(|e| Error::Certificate(format!("Failed to encode cert: {}", e)))?;
fs::write(&cert_identity.cert_file, cert_pem).map_err(Error::Filesystem)?;
return Ok(cert_identity);
}
cert_identity
.request_new_certificate(dev_session, team)
.await?;
Ok(cert_identity)
}
async fn find_matching_certificate(
&self,
dev_session: &DeveloperSession,
team: &DeveloperTeam,
) -> Result<(X509Certificate, String), Error> {
let certificates = dev_session
.list_all_development_certs(DeveloperDeviceType::Ios, team)
.await
.map_err(|e| Error::Certificate(format!("Failed to list certificates: {:?}", e)))?;
let our_public_key_der = self.key_pair.public_key_data().to_vec();
for cert in certificates
.iter()
.filter(|c| c.machine_name == self.machine_name)
{
if let Ok(x509_cert) = X509Certificate::from_der(&cert.cert_content) {
let cert_public_key_der: Vec<u8> = x509_cert
.tbs_certificate()
.subject_public_key_info
.subject_public_key
.octets()
.collect();
if cert_public_key_der == our_public_key_der {
return Ok((x509_cert, cert.machine_id.clone()));
}
}
}
Err(Error::Certificate(
"No matching certificate found".to_string(),
))
}
async fn request_new_certificate(
&mut self,
dev_session: &DeveloperSession,
team: &DeveloperTeam,
) -> Result<(), Error> {
let mut params = CertificateParams::new(vec!["CN".to_string()])
.map_err(|e| Error::Certificate(format!("Failed to create params: {}", e)))?;
params.distinguished_name.push(DnType::CountryName, "US");
params
.distinguished_name
.push(DnType::StateOrProvinceName, "STATE");
params
.distinguished_name
.push(DnType::LocalityName, "LOCAL");
params
.distinguished_name
.push(DnType::OrganizationName, "ORGNIZATION");
params.distinguished_name.push(DnType::CommonName, "CN");
let key_pem = self
.private_key
.to_pkcs8_pem(LineEnding::LF)
.map_err(|e| Error::Certificate(format!("Failed to encode private key: {}", e)))?;
let key_pair = KeyPair::from_pem(&key_pem)
.map_err(|e| Error::Certificate(format!("Failed to load key pair for CSR: {}", e)))?;
let csr = params
.serialize_request(&key_pair)
.map_err(|e| Error::Certificate(format!("Failed to generate CSR: {}", e)))?;
let csr_pem = csr
.pem()
.map_err(|e| Error::Certificate(format!("Failed to encode CSR to PEM: {}", e)))?;
let certificate_id = dev_session
.submit_development_csr(
DeveloperDeviceType::Ios,
team,
csr_pem,
self.machine_name.clone(),
)
.await
.map_err(|e| {
let is_7460 = match &e {
Error::DeveloperSession(code, _) => *code == 7460,
_ => false,
};
if is_7460 {
Error::Certificate("You have too many certificates!".to_string())
} else {
Error::Certificate(format!("Failed to submit CSR: {:?}", e))
}
})?;
let certificates = dev_session
.list_all_development_certs(DeveloperDeviceType::Ios, team)
.await?;
let apple_cert = certificates
.iter()
.find(|cert| cert.certificate_id == certificate_id)
.ok_or(Error::Certificate(
"Certificate not found after submission".to_string(),
))?;
let certificate = X509Certificate::from_der(&apple_cert.cert_content)
.map_err(|e| Error::Certificate(format!("Failed to parse certificate: {}", e)))?;
// Write certificate to disk
let cert_pem = certificate
.encode_pem()
.map_err(|e| Error::Certificate(format!("Failed to encode cert: {}", e)))?;
fs::write(&self.cert_file, cert_pem).map_err(Error::Filesystem)?;
self.certificate = Some(certificate);
self.machine_id = apple_cert.machine_id.clone();
Ok(())
}
pub fn get_certificate_file_path(&self) -> &Path {
&self.cert_file
}
pub fn get_private_key_file_path(&self) -> &Path {
&self.key_file
}
pub fn get_serial_number(&self) -> Result<String, Error> {
let cert = match &self.certificate {
Some(c) => c,
None => {
return Err(Error::Certificate(
"No certificate available to get serial number".to_string(),
));
}
};
let serial = &cert.tbs_certificate().serial_number;
let hex_str = hex::encode(serial.as_slice());
Ok(hex_str.trim_start_matches("0").to_string().to_uppercase())
}
pub fn to_pkcs12(&self, password: &str) -> Result<Vec<u8>, Error> {
let cert = self
.certificate
.as_ref()
.ok_or(Error::Certificate("Certificate not found".to_string()))?;
let cert_der = cert
.encode_der()
.map_err(|e| Error::Certificate(format!("Failed to encode certificate: {}", e)))?;
let key_der = self
.private_key
.to_pkcs8_der()
.map_err(|e| Error::Certificate(format!("Failed to encode private key: {}", e)))?;
let pfx = p12::PFX::new(
&cert_der,
key_der.as_bytes(),
None,
password,
&self.machine_name,
)
.ok_or(Error::Certificate("Failed to create PKCS#12".to_string()))?;
Ok(pfx.to_der())
}
pub fn to_signing_settings(&self) -> Result<SigningSettings<'_>, Error> {
let mut settings = SigningSettings::default();
let certificate = self
.certificate
.as_ref()
.ok_or(Error::Certificate("Certificate not found".to_string()))?;
settings.set_signing_key(
&self.key_pair,
CapturedX509Certificate::from_der(
certificate.encode_der().map_err(|e| {
Error::Certificate(format!("Failed to encode certificate: {}", e))
})?,
)
.map_err(|e| {
Error::Certificate(format!("Failed to create captured certificate: {}", e))
})?,
);
settings.chain_apple_certificates();
settings.set_for_notarization(false);
settings.set_shallow(true);
settings.set_team_id_from_signing_certificate().ok_or({
Error::Certificate("Failed to set team ID from signing certificate".to_string())
})?;
settings
.set_time_stamp_url("http://timestamp.apple.com/ts01")
.map_err(|e| Error::AppleCodesignError(Box::new(e)))?;
Ok(settings)
}
}

View File

@@ -1,779 +0,0 @@
// This file was made using https://github.com/Dadoum/Sideloader as a reference for the apple private endpoints
use crate::Error;
use icloud_auth::{AppleAccount, Error as ICloudError};
use plist::{Date, Dictionary, Value};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use uuid::Uuid;
pub struct DeveloperSession {
pub account: Arc<AppleAccount>,
team: Option<DeveloperTeam>,
}
impl DeveloperSession {
pub fn new(account: Arc<AppleAccount>) -> Self {
DeveloperSession {
account,
team: None,
}
}
pub async fn send_developer_request(
&self,
url: &str,
body: Option<Dictionary>,
) -> Result<Dictionary, Error> {
let mut request = Dictionary::new();
request.insert(
"clientId".to_string(),
Value::String("XABBG36SBA".to_string()),
);
request.insert(
"protocolVersion".to_string(),
Value::String("QH65B2".to_string()),
);
request.insert(
"requestId".to_string(),
Value::String(Uuid::new_v4().to_string().to_uppercase()),
);
request.insert(
"userLocale".to_string(),
Value::Array(vec![Value::String("en_US".to_string())]),
);
if let Some(body) = body {
for (key, value) in body {
request.insert(key, value);
}
}
let response = self
.account
.send_request(url, Some(request))
.await
.map_err(|e| {
if let ICloudError::AuthSrpWithMessage(code, message) = e {
Error::DeveloperSession(code, format!("Developer request failed: {}", message))
} else {
Error::Generic("Failed to send developer request".to_string())
}
})?;
let status_code = response
.get("resultCode")
.and_then(|v| v.as_unsigned_integer())
.unwrap_or(0);
if status_code != 0 {
let description = response
.get("userString")
.and_then(|v| v.as_string())
.or_else(|| response.get("resultString").and_then(|v| v.as_string()))
.unwrap_or("(null)");
return Err(Error::DeveloperSession(
status_code as i64,
description.to_string(),
));
}
Ok(response)
}
pub async fn list_teams(&self) -> Result<Vec<DeveloperTeam>, Error> {
let url = "https://developerservices2.apple.com/services/QH65B2/listTeams.action?clientId=XABBG36SBA";
let response = self.send_developer_request(url, None).await?;
let teams = response
.get("teams")
.and_then(|v| v.as_array())
.ok_or(Error::Parse("teams".to_string()))?;
let mut result = Vec::new();
for team in teams {
let dict = team
.as_dictionary()
.ok_or(Error::Parse("team".to_string()))?;
let name = dict
.get("name")
.and_then(|v| v.as_string())
.ok_or(Error::Parse("name".to_string()))?
.to_string();
let team_id = dict
.get("teamId")
.and_then(|v| v.as_string())
.ok_or(Error::Parse("teamId".to_string()))?
.to_string();
result.push(DeveloperTeam {
_name: name,
team_id,
});
}
Ok(result)
}
pub async fn get_team(&self) -> Result<DeveloperTeam, Error> {
if let Some(team) = &self.team {
return Ok(team.clone());
}
let teams = self.list_teams().await?;
if teams.is_empty() {
return Err(Error::DeveloperSession(
-1,
"No developer teams found".to_string(),
));
}
// TODO: Handle multiple teams
Ok(teams[0].clone())
}
pub fn set_team(&mut self, team: DeveloperTeam) {
self.team = Some(team);
}
pub async fn list_devices(
&self,
device_type: DeveloperDeviceType,
team: &DeveloperTeam,
) -> Result<Vec<DeveloperDevice>, Error> {
let url = dev_url(device_type, "listDevices");
let mut body = Dictionary::new();
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
let response = self.send_developer_request(&url, Some(body)).await?;
let devices = response
.get("devices")
.and_then(|v| v.as_array())
.ok_or(Error::Parse("devices".to_string()))?;
let mut result = Vec::new();
for device in devices {
let dict = device
.as_dictionary()
.ok_or(Error::Parse("device".to_string()))?;
let device_id = dict
.get("deviceId")
.and_then(|v| v.as_string())
.ok_or(Error::Parse("deviceId".to_string()))?
.to_string();
let name = dict
.get("name")
.and_then(|v| v.as_string())
.ok_or(Error::Parse("name".to_string()))?
.to_string();
let device_number = dict
.get("deviceNumber")
.and_then(|v| v.as_string())
.ok_or(Error::Parse("deviceNumber".to_string()))?
.to_string();
result.push(DeveloperDevice {
_device_id: device_id,
_name: name,
device_number,
});
}
Ok(result)
}
pub async fn add_device(
&self,
device_type: DeveloperDeviceType,
team: &DeveloperTeam,
device_name: &str,
udid: &str,
) -> Result<DeveloperDevice, Error> {
let url = dev_url(device_type, "addDevice");
let mut body = Dictionary::new();
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
body.insert("name".to_string(), Value::String(device_name.to_string()));
body.insert("deviceNumber".to_string(), Value::String(udid.to_string()));
let response = self.send_developer_request(&url, Some(body)).await?;
let device_dict = response
.get("device")
.and_then(|v| v.as_dictionary())
.ok_or(Error::Parse("device".to_string()))?;
let device_id = device_dict
.get("deviceId")
.and_then(|v| v.as_string())
.ok_or(Error::Parse("deviceId".to_string()))?
.to_string();
let name = device_dict
.get("name")
.and_then(|v| v.as_string())
.ok_or(Error::Parse("name".to_string()))?
.to_string();
let device_number = device_dict
.get("deviceNumber")
.and_then(|v| v.as_string())
.ok_or(Error::Parse("deviceNumber".to_string()))?
.to_string();
Ok(DeveloperDevice {
_device_id: device_id,
_name: name,
device_number,
})
}
pub async fn list_all_development_certs(
&self,
device_type: DeveloperDeviceType,
team: &DeveloperTeam,
) -> Result<Vec<DevelopmentCertificate>, Error> {
let url = dev_url(device_type, "listAllDevelopmentCerts");
let mut body = Dictionary::new();
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
let response = self.send_developer_request(&url, Some(body)).await?;
let certs = response
.get("certificates")
.and_then(|v| v.as_array())
.ok_or(Error::Parse("certificates".to_string()))?;
let mut result = Vec::new();
for cert in certs {
let dict = cert
.as_dictionary()
.ok_or(Error::Parse("certificate".to_string()))?;
let name = dict
.get("name")
.and_then(|v| v.as_string())
.ok_or(Error::Parse("name".to_string()))?
.to_string();
let certificate_id = dict
.get("certificateId")
.and_then(|v| v.as_string())
.ok_or(Error::Parse("certificateId".to_string()))?
.to_string();
let serial_number = dict
.get("serialNumber")
.and_then(|v| v.as_string())
.ok_or(Error::Parse("serialNumber".to_string()))?
.to_string();
let machine_name = dict
.get("machineName")
.and_then(|v| v.as_string())
.unwrap_or("")
.to_string();
let machine_id = dict
.get("machineId")
.and_then(|v| v.as_string())
.ok_or(Error::Parse("machineId".to_string()))?
.to_string();
let cert_content = dict
.get("certContent")
.and_then(|v| v.as_data())
.ok_or(Error::Parse("certContent".to_string()))?
.to_vec();
result.push(DevelopmentCertificate {
name,
certificate_id,
serial_number,
machine_name,
machine_id,
cert_content,
});
}
Ok(result)
}
pub async fn revoke_development_cert(
&self,
device_type: DeveloperDeviceType,
team: &DeveloperTeam,
serial_number: &str,
) -> Result<(), Error> {
let url = dev_url(device_type, "revokeDevelopmentCert");
let mut body = Dictionary::new();
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
body.insert(
"serialNumber".to_string(),
Value::String(serial_number.to_string()),
);
self.send_developer_request(&url, Some(body)).await?;
Ok(())
}
pub async fn submit_development_csr(
&self,
device_type: DeveloperDeviceType,
team: &DeveloperTeam,
csr_content: String,
machine_name: String,
) -> Result<String, Error> {
let url = dev_url(device_type, "submitDevelopmentCSR");
let mut body = Dictionary::new();
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
body.insert("csrContent".to_string(), Value::String(csr_content));
body.insert(
"machineId".to_string(),
Value::String(uuid::Uuid::new_v4().to_string().to_uppercase()),
);
body.insert("machineName".to_string(), Value::String(machine_name));
let response = self.send_developer_request(&url, Some(body)).await?;
let cert_dict = response
.get("certRequest")
.and_then(|v| v.as_dictionary())
.ok_or(Error::Parse("certRequest".to_string()))?;
let id = cert_dict
.get("certRequestId")
.and_then(|v| v.as_string())
.ok_or(Error::Parse("certRequestId".to_string()))?
.to_string();
Ok(id)
}
pub async fn list_app_ids(
&self,
device_type: DeveloperDeviceType,
team: &DeveloperTeam,
) -> Result<ListAppIdsResponse, Error> {
let url = dev_url(device_type, "listAppIds");
let mut body = Dictionary::new();
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
let response = self.send_developer_request(&url, Some(body)).await?;
let app_ids = response
.get("appIds")
.and_then(|v| v.as_array())
.ok_or(Error::Parse("appIds".to_string()))?;
let mut result = Vec::new();
for app_id in app_ids {
let dict = app_id
.as_dictionary()
.ok_or(Error::Parse("appId".to_string()))?;
let name = dict
.get("name")
.and_then(|v| v.as_string())
.ok_or(Error::Parse("name".to_string()))?
.to_string();
let app_id_id = dict
.get("appIdId")
.and_then(|v| v.as_string())
.ok_or(Error::Parse("appIdId".to_string()))?
.to_string();
let identifier = dict
.get("identifier")
.and_then(|v| v.as_string())
.ok_or(Error::Parse("identifier".to_string()))?
.to_string();
let features = dict
.get("features")
.and_then(|v| v.as_dictionary())
.ok_or(Error::Parse("features".to_string()))?;
let expiration_date = if dict.contains_key("expirationDate") {
Some(
dict.get("expirationDate")
.and_then(|v| v.as_date())
.ok_or(Error::Parse("expirationDate".to_string()))?,
)
} else {
None
};
result.push(AppId {
name,
app_id_id,
identifier,
features: features.clone(),
expiration_date,
});
}
let max_quantity = if response.contains_key("maxQuantity") {
Some(
response
.get("maxQuantity")
.and_then(|v| v.as_unsigned_integer())
.ok_or(Error::Parse("maxQuantity".to_string()))?,
)
} else {
None
};
let available_quantity = if response.contains_key("availableQuantity") {
Some(
response
.get("availableQuantity")
.and_then(|v| v.as_unsigned_integer())
.ok_or(Error::Parse("availableQuantity".to_string()))?,
)
} else {
None
};
Ok(ListAppIdsResponse {
app_ids: result,
max_quantity,
available_quantity,
})
}
pub async fn add_app_id(
&self,
device_type: DeveloperDeviceType,
team: &DeveloperTeam,
name: &str,
identifier: &str,
) -> Result<(), Error> {
let url = dev_url(device_type, "addAppId");
let mut body = Dictionary::new();
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
body.insert("name".to_string(), Value::String(name.to_string()));
body.insert(
"identifier".to_string(),
Value::String(identifier.to_string()),
);
self.send_developer_request(&url, Some(body)).await?;
Ok(())
}
pub async fn update_app_id(
&self,
device_type: DeveloperDeviceType,
team: &DeveloperTeam,
app_id: &AppId,
features: &Dictionary,
) -> Result<Dictionary, Error> {
let url = dev_url(device_type, "updateAppId");
let mut body = Dictionary::new();
body.insert(
"appIdId".to_string(),
Value::String(app_id.app_id_id.clone()),
);
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
for (key, value) in features {
body.insert(key.clone(), value.clone());
}
let response = self.send_developer_request(&url, Some(body)).await?;
let cert_dict = response
.get("appId")
.and_then(|v| v.as_dictionary())
.ok_or(Error::Parse("appId".to_string()))?;
let feats = cert_dict
.get("features")
.and_then(|v| v.as_dictionary())
.ok_or(Error::Parse("features".to_string()))?;
Ok(feats.clone())
}
pub async fn delete_app_id(
&self,
device_type: DeveloperDeviceType,
team: &DeveloperTeam,
app_id_id: String,
) -> Result<(), Error> {
let url = dev_url(device_type, "deleteAppId");
let mut body = Dictionary::new();
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
body.insert("appIdId".to_string(), Value::String(app_id_id.clone()));
self.send_developer_request(&url, Some(body)).await?;
Ok(())
}
pub async fn list_application_groups(
&self,
device_type: DeveloperDeviceType,
team: &DeveloperTeam,
) -> Result<Vec<ApplicationGroup>, Error> {
let url = dev_url(device_type, "listApplicationGroups");
let mut body = Dictionary::new();
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
let response = self.send_developer_request(&url, Some(body)).await?;
let app_groups = response
.get("applicationGroupList")
.and_then(|v| v.as_array())
.ok_or(Error::Parse("applicationGroupList".to_string()))?;
let mut result = Vec::new();
for app_group in app_groups {
let dict = app_group
.as_dictionary()
.ok_or(Error::Parse("applicationGroup".to_string()))?;
let application_group = dict
.get("applicationGroup")
.and_then(|v| v.as_string())
.ok_or(Error::Parse("applicationGroup".to_string()))?
.to_string();
let name = dict
.get("name")
.and_then(|v| v.as_string())
.ok_or(Error::Parse("name".to_string()))?
.to_string();
let identifier = dict
.get("identifier")
.and_then(|v| v.as_string())
.ok_or(Error::Parse("identifier".to_string()))?
.to_string();
result.push(ApplicationGroup {
application_group,
_name: name,
identifier,
});
}
Ok(result)
}
pub async fn add_application_group(
&self,
device_type: DeveloperDeviceType,
team: &DeveloperTeam,
group_identifier: &str,
name: &str,
) -> Result<ApplicationGroup, Error> {
let url = dev_url(device_type, "addApplicationGroup");
let mut body = Dictionary::new();
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
body.insert("name".to_string(), Value::String(name.to_string()));
body.insert(
"identifier".to_string(),
Value::String(group_identifier.to_string()),
);
let response = self.send_developer_request(&url, Some(body)).await?;
let app_group_dict = response
.get("applicationGroup")
.and_then(|v| v.as_dictionary())
.ok_or(Error::Parse("applicationGroup".to_string()))?;
let application_group = app_group_dict
.get("applicationGroup")
.and_then(|v| v.as_string())
.ok_or(Error::Parse("applicationGroup".to_string()))?
.to_string();
let name = app_group_dict
.get("name")
.and_then(|v| v.as_string())
.ok_or(Error::Parse("name".to_string()))?
.to_string();
let identifier = app_group_dict
.get("identifier")
.and_then(|v| v.as_string())
.ok_or(Error::Parse("identifier".to_string()))?
.to_string();
Ok(ApplicationGroup {
application_group,
_name: name,
identifier,
})
}
pub async fn assign_application_group_to_app_id(
&self,
device_type: DeveloperDeviceType,
team: &DeveloperTeam,
app_id: &AppId,
app_group: &ApplicationGroup,
) -> Result<(), Error> {
let url = dev_url(device_type, "assignApplicationGroupToAppId");
let mut body = Dictionary::new();
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
body.insert(
"appIdId".to_string(),
Value::String(app_id.app_id_id.clone()),
);
body.insert(
"applicationGroups".to_string(),
Value::String(app_group.application_group.clone()),
);
self.send_developer_request(&url, Some(body)).await?;
Ok(())
}
pub async fn download_team_provisioning_profile(
&self,
device_type: DeveloperDeviceType,
team: &DeveloperTeam,
app_id: &AppId,
) -> Result<ProvisioningProfile, Error> {
let url = dev_url(device_type, "downloadTeamProvisioningProfile");
let mut body = Dictionary::new();
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
body.insert(
"appIdId".to_string(),
Value::String(app_id.app_id_id.clone()),
);
let response = self.send_developer_request(&url, Some(body)).await?;
let profile = response
.get("provisioningProfile")
.and_then(|v| v.as_dictionary())
.ok_or(Error::Parse("provisioningProfile".to_string()))?;
let name = profile
.get("name")
.and_then(|v| v.as_string())
.ok_or(Error::Parse("name".to_string()))?
.to_string();
let provisioning_profile_id = profile
.get("provisioningProfileId")
.and_then(|v| v.as_string())
.ok_or(Error::Parse("provisioningProfileId".to_string()))?
.to_string();
let encoded_profile = profile
.get("encodedProfile")
.and_then(|v| v.as_data())
.ok_or(Error::Parse("encodedProfile".to_string()))?
.to_vec();
Ok(ProvisioningProfile {
_name: name,
_provisioning_profile_id: provisioning_profile_id,
encoded_profile,
})
}
}
#[derive(Debug, Clone)]
pub enum DeveloperDeviceType {
Any,
Ios,
Tvos,
Watchos,
}
impl DeveloperDeviceType {
pub fn url_segment(&self) -> &'static str {
match self {
DeveloperDeviceType::Any => "",
DeveloperDeviceType::Ios => "ios/",
DeveloperDeviceType::Tvos => "tvos/",
DeveloperDeviceType::Watchos => "watchos/",
}
}
}
fn dev_url(device_type: DeveloperDeviceType, endpoint: &str) -> String {
format!(
"https://developerservices2.apple.com/services/QH65B2/{}{}.action?clientId=XABBG36SBA",
device_type.url_segment(),
endpoint
)
}
#[derive(Debug, Clone)]
pub struct DeveloperDevice {
pub _device_id: String,
pub _name: String,
pub device_number: String,
}
#[derive(Debug, Clone)]
pub struct DeveloperTeam {
pub _name: String,
pub team_id: String,
}
#[derive(Debug, Clone)]
pub struct DevelopmentCertificate {
pub name: String,
pub certificate_id: String,
pub serial_number: String,
pub machine_name: String,
pub machine_id: String,
pub cert_content: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppId {
pub app_id_id: String,
pub identifier: String,
pub name: String,
pub features: Dictionary,
pub expiration_date: Option<Date>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListAppIdsResponse {
pub app_ids: Vec<AppId>,
pub max_quantity: Option<u64>,
pub available_quantity: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct ApplicationGroup {
pub application_group: String,
pub _name: String,
pub identifier: String,
}
#[derive(Debug, Clone)]
pub struct ProvisioningProfile {
pub _provisioning_profile_id: String,
pub _name: String,
pub encoded_profile: Vec<u8>,
}
impl ProvisioningProfile {
// TODO: I'm not sure if this is the proper way to parse this but it works so...
pub fn profile_plist(&self) -> Result<plist::Dictionary, Error> {
let start_marker = b"<?xml";
let end_marker = b"</plist>";
let start = self
.encoded_profile
.windows(start_marker.len())
.position(|w| w == start_marker)
.ok_or_else(|| {
Error::Generic("Failed to find start of plist in provisioning profile".to_string())
})?;
let end = self
.encoded_profile
.windows(end_marker.len())
.position(|w| w == end_marker)
.ok_or_else(|| {
Error::Generic("Failed to find end of plist in provisioning profile".to_string())
})?
+ end_marker.len();
plist::from_bytes::<plist::Dictionary>(&self.encoded_profile[start..end]).map_err(|e| {
Error::Generic(format!("Failed to parse provisioning profile plist: {}", e))
})
}
pub fn entitlements_xml(&self) -> Result<String, Error> {
let profile_plist = self.profile_plist()?;
let entitlements = profile_plist.get("Entitlements").ok_or_else(|| {
Error::Generic("No Entitlements found in provisioning profile".to_string())
})?;
let mut buf = vec![];
entitlements.to_writer_xml(&mut buf).map_err(|e| {
Error::Generic(format!(
"Failed to convert entitlements to XML for codesigning: {}",
e
))
})?;
let entitlements = std::str::from_utf8(&buf)
.map_err(|e| {
Error::Generic(format!(
"Failed to convert entitlements to UTF-8 for codesigning: {}",
e
))
})?
.to_string();
Ok(entitlements)
}
}

View File

@@ -1,91 +0,0 @@
use idevice::{
IdeviceService, afc::AfcClient, installation_proxy::InstallationProxyClient,
provider::IdeviceProvider,
};
use std::pin::Pin;
use std::{future::Future, path::Path};
use crate::Error;
/// 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<(), Error> {
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 mut options = plist::Dictionary::new();
options.insert("PackageType".to_string(), "Developer".into());
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<(), Error>> + Send + 'a>> {
Box::pin(async move {
let entries = std::fs::read_dir(path).map_err(Error::Filesystem)?;
afc_client
.mk_dir(afc_path)
.await
.map_err(Error::IdeviceError)?;
for entry in entries {
let entry = entry.map_err(Error::Filesystem)?;
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).map_err(Error::Filesystem)?;
file_handle
.write_entire(&bytes)
.await
.map_err(Error::IdeviceError)?;
file_handle.close().await.map_err(Error::IdeviceError)?;
}
}
Ok(())
})
}

View File

@@ -1,123 +1,14 @@
pub mod application;
pub mod bundle;
pub mod certificate;
pub mod developer_session;
pub mod device;
pub mod sideload;
use std::io::Error as IOError;
use apple_codesign::AppleCodesignError;
pub use icloud_auth::{AnisetteConfiguration, AppleAccount};
use developer_session::DeveloperTeam;
use idevice::IdeviceError;
use thiserror::Error as ThisError;
#[derive(Debug, ThisError)]
pub enum Error {
#[error("Authentication error {0}: {1}")]
Auth(i64, String),
#[error("Developer session error {0}: {1}")]
DeveloperSession(i64, String),
#[error("Error: {0}")]
Generic(String),
#[error("Failed to parse: {0}")]
Parse(String),
#[error("Invalid bundle: {0}")]
InvalidBundle(String),
#[error("Certificate error: {0}")]
Certificate(String),
#[error(transparent)]
Filesystem(#[from] IOError),
#[error(transparent)]
IdeviceError(#[from] IdeviceError),
#[error(transparent)]
AppleCodesignError(#[from] Box<AppleCodesignError>),
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
pub trait SideloadLogger: Send + Sync {
fn log(&self, message: &str);
fn error(&self, error: &Error);
}
#[cfg(test)]
mod tests {
use super::*;
pub struct DefaultLogger;
impl SideloadLogger for DefaultLogger {
fn log(&self, message: &str) {
println!("{message}");
}
fn error(&self, error: &Error) {
eprintln!("Error: {}", error);
}
}
/// Sideload configuration options.
pub struct SideloadConfiguration<'a> {
pub machine_name: String,
pub logger: &'a dyn SideloadLogger,
pub store_dir: std::path::PathBuf,
pub revoke_cert: bool,
pub force_sidestore: bool,
pub skip_register_extensions: bool,
}
impl Default for SideloadConfiguration<'_> {
fn default() -> Self {
SideloadConfiguration::new()
}
}
impl<'a> SideloadConfiguration<'a> {
pub fn new() -> Self {
SideloadConfiguration {
machine_name: "isideload".to_string(),
logger: &DefaultLogger,
store_dir: std::env::current_dir().unwrap(),
revoke_cert: false,
force_sidestore: false,
skip_register_extensions: true,
}
}
/// An arbitrary machine name to appear on the certificate (e.x. "CrossCode")
pub fn set_machine_name(mut self, machine_name: String) -> Self {
self.machine_name = machine_name;
self
}
/// Logger for reporting progress and errors
pub fn set_logger(mut self, logger: &'a dyn SideloadLogger) -> Self {
self.logger = logger;
self
}
/// Directory used to store intermediate artifacts (profiles, certs, etc.). This directory will not be cleared at the end.
pub fn set_store_dir(mut self, store_dir: std::path::PathBuf) -> Self {
self.store_dir = store_dir;
self
}
/// Whether or not to revoke the certificate immediately after installation
#[deprecated(
since = "0.1.0",
note = "Certificates will now be placed in SideStore automatically so there is no need to revoke"
)]
pub fn set_revoke_cert(mut self, revoke_cert: bool) -> Self {
self.revoke_cert = revoke_cert;
self
}
/// Whether or not to treat the app as SideStore (fixes LiveContainer+SideStore issues)
pub fn set_force_sidestore(mut self, force: bool) -> Self {
self.force_sidestore = force;
self
}
/// Whether or not to skip registering app extensions (save app IDs, default true)
pub fn set_skip_register_extensions(mut self, skip: bool) -> Self {
self.skip_register_extensions = skip;
self
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}

View File

@@ -1,463 +0,0 @@
// This file was made using https://github.com/Dadoum/Sideloader as a reference.
use apple_codesign::{SettingsScope, UnifiedSigner};
use idevice::IdeviceService;
use idevice::lockdown::LockdownClient;
use idevice::provider::IdeviceProvider;
use crate::application::Application;
use crate::developer_session::ProvisioningProfile;
use crate::device::install_app;
use crate::{DeveloperTeam, Error, SideloadConfiguration, SideloadLogger};
use crate::{
certificate::CertificateIdentity,
developer_session::{DeveloperDeviceType, DeveloperSession},
};
use std::collections::HashMap;
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 = config.force_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 {
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 dev_session
.download_team_provisioning_profile(DeveloperDeviceType::Ios, &team, &app_id)
.await
{
Ok(pp /* tee hee */) => pp,
Err(e) => {
return error_and_return(logger, e);
}
};
provisioning_profiles.insert(app_id.identifier.clone(), provisioning_profile);
}
logger.log("Successfully registered app groups");
let profile_path = app.bundle.bundle_dir.join("embedded.mobileprovision");
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()?;
}
// Collect owned bundle identifiers and directories so we don't capture `app` or `logger` by reference in the blocking thread.
let embedded_bundles_info: Vec<(String, PathBuf)> = app
.bundle
.embedded_bundles()
.iter()
.map(|bundle| {
(
bundle.bundle_identifier().unwrap_or("Unknown").to_string(),
bundle.bundle_dir.clone(),
)
})
.collect();
let main_bundle_dir = app.bundle.bundle_dir.clone();
// Log bundle signing messages outside the blocking closure to avoid capturing non-'static references.
for (id, _) in &embedded_bundles_info {
logger.log(&format!("Signing bundle: {}", id));
}
// Move owned data (cert, provisioning_profile, embedded_bundles_info) into the blocking task.
tokio::task::spawn_blocking(move || {
for (_id, bundle_dir) in embedded_bundles_info {
// Recreate settings for each bundle so ownership is clear and we don't move settings across iterations.
let mut settings = cert.to_signing_settings()?;
settings
.set_entitlements_xml(
SettingsScope::Main,
provisioning_profile.entitlements_xml()?,
)
.map_err(|e| Error::AppleCodesignError(Box::new(e)))?;
let signer = UnifiedSigner::new(settings);
signer
.sign_path_in_place(&bundle_dir)
.map_err(|e| Error::AppleCodesignError(Box::new(e)))?;
}
Ok::<(), Error>(())
})
.await
.map_err(|e| Error::Generic(format!("Signing task failed: {}", e)))??;
logger.log("Sucessfully signed app");
logger.log("Installing app... 0%");
let res = install_app(device_provider, &main_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(())
}