start refactoring sideloader

This commit is contained in:
nab138
2026-02-06 08:30:59 -05:00
parent 27de2210ec
commit 6d0f644e93
8 changed files with 282 additions and 80 deletions

98
Cargo.lock generated
View File

@@ -157,6 +157,12 @@ version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.11.1" version = "1.11.1"
@@ -370,6 +376,27 @@ version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
[[package]]
name = "dbus"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4"
dependencies = [
"libc",
"libdbus-sys",
"windows-sys 0.59.0",
]
[[package]]
name = "dbus-secret-service"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6"
dependencies = [
"dbus",
"zeroize",
]
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.5.5" version = "0.5.5"
@@ -1040,7 +1067,13 @@ version = "3.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c"
dependencies = [ dependencies = [
"byteorder",
"dbus-secret-service",
"linux-keyutils",
"log", "log",
"security-framework 2.11.1",
"security-framework 3.5.1",
"windows-sys 0.60.2",
"zeroize", "zeroize",
] ]
@@ -1062,6 +1095,25 @@ version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
[[package]]
name = "libdbus-sys"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043"
dependencies = [
"pkg-config",
]
[[package]]
name = "linux-keyutils"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "761e49ec5fd8a5a463f9b84e877c373d888935b71c6be78f3767fe2ae6bed18e"
dependencies = [
"bitflags",
"libc",
]
[[package]] [[package]]
name = "litemap" name = "litemap"
version = "0.8.1" version = "0.8.1"
@@ -1190,6 +1242,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]] [[package]]
name = "plist" name = "plist"
version = "1.8.0" version = "1.8.0"
@@ -1485,7 +1543,7 @@ dependencies = [
"openssl-probe", "openssl-probe",
"rustls-pki-types", "rustls-pki-types",
"schannel", "schannel",
"security-framework", "security-framework 3.5.1",
] ]
[[package]] [[package]]
@@ -1512,7 +1570,7 @@ dependencies = [
"rustls-native-certs", "rustls-native-certs",
"rustls-platform-verifier-android", "rustls-platform-verifier-android",
"rustls-webpki", "rustls-webpki",
"security-framework", "security-framework 3.5.1",
"security-framework-sys", "security-framework-sys",
"webpki-root-certs", "webpki-root-certs",
"windows-sys 0.61.2", "windows-sys 0.61.2",
@@ -1559,6 +1617,19 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags",
"core-foundation 0.9.4",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]] [[package]]
name = "security-framework" name = "security-framework"
version = "3.5.1" version = "3.5.1"
@@ -2416,6 +2487,15 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.60.2" version = "0.60.2"
@@ -2783,6 +2863,20 @@ name = "zeroize"
version = "1.8.2" version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "zerotrie" name = "zerotrie"

View File

@@ -5,7 +5,7 @@ use isideload::{
anisette::remote_v3::RemoteV3AnisetteProvider, anisette::remote_v3::RemoteV3AnisetteProvider,
auth::apple_account::AppleAccount, auth::apple_account::AppleAccount,
dev::developer_session::DeveloperSession, dev::developer_session::DeveloperSession,
sideload::{SideloadConfiguration, TeamSelection, sideload_app}, sideload::{SideloaderBuilder, TeamSelection},
}; };
use tracing::Level; use tracing::Level;
@@ -70,8 +70,7 @@ async fn main() {
.unwrap() .unwrap()
.to_provider(UsbmuxdAddr::from_env_var().unwrap(), "isideload-demo"); .to_provider(UsbmuxdAddr::from_env_var().unwrap(), "isideload-demo");
let sideload_config = let builder = SideloaderBuilder::new().team_selection(TeamSelection::Prompt(|teams| {
SideloadConfiguration::builder().team_selection(TeamSelection::Prompt(|teams| {
println!("Please select a team:"); println!("Please select a team:");
for (index, team) in teams.iter().enumerate() { for (index, team) in teams.iter().enumerate() {
println!( println!(
@@ -90,7 +89,7 @@ async fn main() {
Some(teams[selection - 1].team_id.clone()) Some(teams[selection - 1].team_id.clone())
})); }));
let result = sideload_app(&provider, &mut dev_session, app_path, &sideload_config).await; // let result = bu(&provider, &mut dev_session, app_path, &sideload_config).await;
match result { match result {
Ok(_) => println!("App installed successfully"), Ok(_) => println!("App installed successfully"),
Err(e) => panic!("Failed to install app: {:?}", e), Err(e) => panic!("Failed to install app: {:?}", e),

View File

@@ -40,4 +40,4 @@ cbc = { version = "0.2.0-rc.3", features = ["alloc"] }
aes = "0.9.0-rc.4" aes = "0.9.0-rc.4"
aes-gcm = "0.11.0-rc.3" aes-gcm = "0.11.0-rc.3"
tokio = "1.49.0" tokio = "1.49.0"
keyring = { version = "3.6.3", optional = true } keyring = { version = "3.6.3", features = ["apple-native", "linux-native-sync-persistent", "windows-native"], optional = true }

View File

@@ -1,6 +1,6 @@
use std::fmt::Display; use std::fmt::Display;
use crate::dev::teams::DeveloperTeam; use crate::{dev::teams::DeveloperTeam, util::storage::SideloadingStorage};
/// Configuration for selecting a developer team during sideloading /// Configuration for selecting a developer team during sideloading
/// ///
@@ -22,20 +22,22 @@ impl Display for TeamSelection {
} }
} }
pub struct SideloadConfiguration { pub struct SideloaderBuilder {
pub team_selection: TeamSelection, pub team_selection: TeamSelection,
pub storage: Box<dyn SideloadingStorage>,
} }
impl Default for SideloadConfiguration { impl Default for SideloaderBuilder {
fn default() -> Self { fn default() -> Self {
SideloadConfiguration { SideloaderBuilder {
team_selection: TeamSelection::First, team_selection: TeamSelection::First,
storage: Box::new(crate::util::storage::new_storage()),
} }
} }
} }
impl SideloadConfiguration { impl SideloaderBuilder {
pub fn builder() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
@@ -43,4 +45,9 @@ impl SideloadConfiguration {
self.team_selection = selection; self.team_selection = selection;
self self
} }
pub fn storage(mut self, storage: Box<dyn SideloadingStorage>) -> Self {
self.storage = storage;
self
}
} }

View File

@@ -1,51 +1,4 @@
use std::path::PathBuf; pub mod builder;
use idevice::provider::IdeviceProvider;
use rootcause::prelude::*;
use tracing::info;
use crate::dev::teams::TeamsApi;
use crate::dev::{developer_session::DeveloperSession, devices::DevicesApi};
use crate::util::device::IdeviceInfo;
pub mod certificate; pub mod certificate;
pub mod config; pub mod sideloader;
pub use config::{SideloadConfiguration, TeamSelection}; pub use builder::{SideloaderBuilder, TeamSelection};
pub async fn sideload_app(
device_provider: &impl IdeviceProvider,
dev_session: &mut DeveloperSession,
app_path: PathBuf,
config: &SideloadConfiguration,
) -> Result<(), Report> {
let device_info = IdeviceInfo::from_device(device_provider).await?;
let teams = dev_session.list_teams().await?;
let team = match teams.len() {
0 => {
bail!("No developer teams available")
}
1 => &teams[0],
_ => {
info!(
"Multiple developer teams found, {} as per configuration",
config.team_selection
);
match &config.team_selection {
TeamSelection::First => &teams[0],
TeamSelection::Prompt(prompt_fn) => {
let selection = prompt_fn(&teams).ok_or_else(|| report!("No team selected"))?;
teams
.iter()
.find(|t| t.team_id == selection)
.ok_or_else(|| report!("No team found with ID {}", selection))?
}
}
}
};
dev_session
.ensure_device_registered(team, &device_info.name, &device_info.udid, None)
.await?;
Ok(())
}

View File

@@ -0,0 +1,66 @@
use crate::{
dev::{
developer_session::DeveloperSession,
devices::DevicesApi,
teams::{DeveloperTeam, TeamsApi},
},
sideload::TeamSelection,
util::{device::IdeviceInfo, storage::SideloadingStorage},
};
use std::path::PathBuf;
use idevice::provider::IdeviceProvider;
use rootcause::prelude::*;
use tracing::info;
pub struct Sideloader {
pub team_selection: TeamSelection,
pub storage: Box<dyn SideloadingStorage>,
pub dev_session: DeveloperSession,
}
impl Sideloader {
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?;
Ok(())
}
pub async fn get_team(&mut self) -> Result<DeveloperTeam, Report> {
let teams = self.dev_session.list_teams().await?;
Ok(match teams.len() {
0 => {
bail!("No developer teams available")
}
1 => teams.into_iter().next().unwrap(),
_ => {
info!(
"Multiple developer teams found, {} as per configuration",
self.team_selection
);
match &self.team_selection {
TeamSelection::First => teams.into_iter().next().unwrap(),
TeamSelection::Prompt(prompt_fn) => {
let selection =
prompt_fn(&teams).ok_or_else(|| report!("No team selected"))?;
teams
.into_iter()
.find(|t| t.team_id == selection)
.ok_or_else(|| report!("No team found with ID {}", selection))?
}
}
}
})
}
}

View File

@@ -0,0 +1,36 @@
use crate::util::storage::SideloadingStorage;
use keyring::Entry;
use rootcause::prelude::*;
pub struct KeyringStorage {}
impl KeyringStorage {
pub fn new() -> Self {
KeyringStorage {}
}
}
impl SideloadingStorage for KeyringStorage {
fn store(&self, key: &str, value: &str) -> Result<(), Report> {
Entry::new("isideload", key)?.set_password(value)?;
Ok(())
}
fn retrieve(&self, key: &str) -> Result<Option<String>, Report> {
let entry = Entry::new("isideload", key)?;
match entry.get_password() {
Ok(password) => Ok(Some(password)),
Err(keyring::Error::NoEntry) => Ok(None),
Err(e) => Err(e.into()),
}
}
fn delete(&self, key: &str) -> Result<(), Report> {
let entry = Entry::new("isideload", key)?;
match entry.delete_credential() {
Ok(()) => Ok(()),
Err(keyring::Error::NoEntry) => Ok(()),
Err(e) => Err(e.into()),
}
}
}

View File

@@ -1,9 +1,14 @@
use std::{collections::HashMap, sync::Mutex};
use base64::prelude::*; use base64::prelude::*;
use rootcause::prelude::*; use rootcause::prelude::*;
pub trait SideloadingStorage: Send + Sync { pub trait SideloadingStorage: Send + Sync {
fn store(&self, key: &str, value: &str) -> Result<(), Report>; fn store(&self, key: &str, value: &str) -> Result<(), Report>;
fn retrieve(&self, key: &str) -> Result<Option<String>, Report>; fn retrieve(&self, key: &str) -> Result<Option<String>, Report>;
fn delete(&self, key: &str) -> Result<(), Report> {
self.store(key, "")
}
fn store_data(&self, key: &str, value: &[u8]) -> Result<(), Report> { fn store_data(&self, key: &str, value: &[u8]) -> Result<(), Report> {
let encoded = BASE64_STANDARD.encode(value); let encoded = BASE64_STANDARD.encode(value);
@@ -19,3 +24,45 @@ pub trait SideloadingStorage: Send + Sync {
} }
} }
} }
pub fn new_storage() -> impl SideloadingStorage {
#[cfg(feature = "keyring-storage")]
{
crate::util::keyring_storage::KeyringStorage::new()
}
#[cfg(not(feature = "keyring-storage"))]
{
InMemoryStorage::new()
}
}
pub struct InMemoryStorage {
storage: Mutex<HashMap<String, String>>,
}
impl InMemoryStorage {
pub fn new() -> Self {
InMemoryStorage {
storage: Mutex::new(HashMap::new()),
}
}
}
impl SideloadingStorage for InMemoryStorage {
fn store(&self, key: &str, value: &str) -> Result<(), Report> {
let mut storage = self.storage.lock().unwrap();
storage.insert(key.to_string(), value.to_string());
Ok(())
}
fn retrieve(&self, key: &str) -> Result<Option<String>, Report> {
let storage = self.storage.lock().unwrap();
Ok(storage.get(key).cloned())
}
fn delete(&self, key: &str) -> Result<(), Report> {
let mut storage = self.storage.lock().unwrap();
storage.remove(key);
Ok(())
}
}