diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 86a1808..8ce15ff 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -90,6 +90,7 @@ installation_proxy = [ springboardservices = [] misagent = [] mobile_image_mounter = ["dep:sha2"] +mobileactivationd = ["dep:reqwest"] mobilebackup2 = [] location_simulation = [] pair = ["chrono/default", "tokio/time", "dep:sha2", "dep:rsa", "dep:x509-cert"] @@ -129,6 +130,7 @@ full = [ "location_simulation", "misagent", "mobile_image_mounter", + "mobileactivationd", "mobilebackup2", "pair", "pcapd", diff --git a/idevice/src/services/mobileactivationd.rs b/idevice/src/services/mobileactivationd.rs new file mode 100644 index 0000000..6a5aca1 --- /dev/null +++ b/idevice/src/services/mobileactivationd.rs @@ -0,0 +1,91 @@ +//! mobileactivationd activates iOS devices. +//! This isn't a normal service, as it requires a new connection for each request. +//! As such, this service requires a provider itself, instead of temporary usage of one. + +use plist::Dictionary; + +use crate::{Idevice, IdeviceError, IdeviceService, lockdown::LockdownClient, obf}; + +pub struct MobileActivationdClient<'a> { + provider: &'a dyn crate::provider::IdeviceProvider, +} + +/// Internal structure for temporary service connections. +/// This struct exists to take advantage of the service trait. +struct MobileActivationdInternal { + pub idevice: Idevice, +} + +impl IdeviceService for MobileActivationdInternal { + /// Returns the service name as registered with lockdownd + fn service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.mobileactivationd") + } + + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) + } +} + +impl MobileActivationdInternal { + fn new(idevice: Idevice) -> Self { + Self { idevice } + } +} + +impl<'a> MobileActivationdClient<'a> { + pub fn new(provider: &'a dyn crate::provider::IdeviceProvider) -> Self { + Self { provider } + } + + pub async fn state(&self) -> Result { + if let Ok(res) = self.send_command("GetActivationStateRequest", None).await + && let Some(v) = res.get("Value").and_then(|x| x.as_string()) + { + Ok(v.to_string()) + } else { + let mut lc = LockdownClient::connect(self.provider).await?; + lc.start_session(&self.provider.get_pairing_file().await?) + .await?; + + let res = lc.get_value(Some("ActivationState"), None).await?; + if let Some(v) = res.as_string() { + Ok(v.to_string()) + } else { + Err(IdeviceError::UnexpectedResponse) + } + } + } + + pub async fn activated(&self) -> Result { + Ok(self.state().await? == "Activated") + } + + /// Deactivates the device. + /// Protocol gives no response on whether it worked or not, so good luck + pub async fn deactivate(&self) -> Result<(), IdeviceError> { + self.send_command("DeactivateRequest", None).await?; + Ok(()) + } + + async fn send_command( + &self, + command: impl Into, + value: Option<&str>, + ) -> Result { + let mut service = self.service_connect().await?; + let command = command.into(); + let req = crate::plist!({ + "Command": command, + "Value":? value, + }); + service.send_plist(req).await?; + service.read_plist().await + } + + async fn service_connect(&self) -> Result { + Ok(MobileActivationdInternal::connect(self.provider) + .await? + .idevice) + } +} diff --git a/idevice/src/services/mod.rs b/idevice/src/services/mod.rs index a4d520d..f827678 100644 --- a/idevice/src/services/mod.rs +++ b/idevice/src/services/mod.rs @@ -29,6 +29,8 @@ pub mod lockdown; pub mod misagent; #[cfg(feature = "mobile_image_mounter")] pub mod mobile_image_mounter; +#[cfg(feature = "mobileactivationd")] +pub mod mobileactivationd; #[cfg(feature = "mobilebackup2")] pub mod mobilebackup2; #[cfg(feature = "syslog_relay")] diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 3c63429..95b4a42 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -125,11 +125,14 @@ path = "src/pcapd.rs" name = "preboard" path = "src/preboard.rs" - [[bin]] name = "screenshot" path = "src/screenshot.rs" +[[bin]] +name = "activation" +path = "src/activation.rs" + [dependencies] idevice = { path = "../idevice", features = ["full"], default-features = false } tokio = { version = "1.43", features = ["full"] } diff --git a/tools/src/activation.rs b/tools/src/activation.rs new file mode 100644 index 0000000..74bef0e --- /dev/null +++ b/tools/src/activation.rs @@ -0,0 +1,114 @@ +// Jackson Coxson + +use clap::{Arg, Command}; +use idevice::{ + IdeviceService, amfi::AmfiClient, lockdown::LockdownClient, + mobileactivationd::MobileActivationdClient, +}; + +mod common; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let matches = Command::new("activation") + .about("mobileactivationd") + .arg( + Arg::new("host") + .long("host") + .value_name("HOST") + .help("IP address of the device"), + ) + .arg( + Arg::new("pairing_file") + .long("pairing-file") + .value_name("PATH") + .help("Path to the pairing file"), + ) + .arg( + Arg::new("udid") + .value_name("UDID") + .help("UDID of the device (overrides host/pairing file)") + .index(1), + ) + .arg( + Arg::new("about") + .long("about") + .help("Show about information") + .action(clap::ArgAction::SetTrue), + ) + .subcommand(Command::new("state").about("Gets the activation state")) + .subcommand(Command::new("deactivate").about("Deactivates the device")) + .get_matches(); + + if matches.get_flag("about") { + println!("activation - activate the device"); + println!("Copyright (c) 2025 Jackson Coxson"); + return; + } + + let udid = matches.get_one::("udid"); + let host = matches.get_one::("host"); + let pairing_file = matches.get_one::("pairing_file"); + + let provider = match common::get_provider(udid, host, pairing_file, "activation-jkcoxson").await + { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + + let activation_client = MobileActivationdClient::new(&*provider); + let mut lc = LockdownClient::connect(&*provider) + .await + .expect("no lockdown"); + lc.start_session(&provider.get_pairing_file().await.unwrap()) + .await + .expect("no TLS"); + let udid = lc + .get_value(Some("UniqueDeviceID"), None) + .await + .expect("no udid") + .into_string() + .unwrap(); + + if matches.subcommand_matches("state").is_some() { + let s = activation_client.state().await.expect("no state"); + println!("Activation State: {s}"); + } else if matches.subcommand_matches("deactivate").is_some() { + println!("CAUTION: You are deactivating {udid}, press enter to continue."); + let mut input = String::new(); + std::io::stdin().read_line(&mut input).ok(); + activation_client.deactivate().await.expect("no deactivate"); + // } else if matches.subcommand_matches("accept").is_some() { + // amfi_client + // .accept_developer_mode() + // .await + // .expect("Failed to show"); + // } else if matches.subcommand_matches("status").is_some() { + // let status = amfi_client + // .get_developer_mode_status() + // .await + // .expect("Failed to get status"); + // println!("Enabled: {status}"); + // } else if let Some(matches) = matches.subcommand_matches("state") { + // let uuid: &String = match matches.get_one("uuid") { + // Some(u) => u, + // None => { + // eprintln!("No UUID passed. Invalid usage, pass -h for help"); + // return; + // } + // }; + // let status = amfi_client + // .trust_app_signer(uuid) + // .await + // .expect("Failed to get state"); + // println!("Enabled: {status}"); + } else { + eprintln!("Invalid usage, pass -h for help"); + } + return; +}