diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index c08544a..c91d294 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -40,7 +40,13 @@ reqwest = { version = "0.12", features = [ rand = { version = "0.9", optional = true } futures = { version = "0.3", optional = true } -sha2 = { version = "0.10", optional = true } +sha2 = { version = "0.10", optional = true, features = ["oid"] } + +rsa = { version = "0.9", optional = true, features = ["sha2"] } +x509-cert = { version = "0.2", optional = true, features = [ + "builder", + "pem", +], default-features = false } [dev-dependencies] tokio = { version = "1.43", features = ["fs"] } @@ -59,6 +65,7 @@ springboardservices = [] misagent = [] mobile_image_mounter = ["dep:sha2"] location_simulation = [] +pair = ["chrono/default", "dep:sha2", "dep:rsa", "dep:x509-cert"] tcp = ["tokio/net"] tunnel_tcp_stack = ["dep:rand", "dep:futures", "tokio/fs", "tokio/sync"] tss = ["dep:uuid", "dep:reqwest"] @@ -82,6 +89,7 @@ full = [ "installation_proxy", "misagent", "mobile_image_mounter", + "pair", "usbmuxd", "xpc", "location_simulation", diff --git a/idevice/src/ca.rs b/idevice/src/ca.rs new file mode 100644 index 0000000..006c395 --- /dev/null +++ b/idevice/src/ca.rs @@ -0,0 +1,103 @@ +// Jackson Coxson +// Inspired by pymobiledevice3 + +use std::str::FromStr; + +use rsa::{ + pkcs1::DecodeRsaPublicKey, + pkcs1v15::SigningKey, + pkcs8::{EncodePrivateKey, LineEnding, SubjectPublicKeyInfo}, + RsaPrivateKey, RsaPublicKey, +}; +use sha2::Sha256; +use x509_cert::{ + builder::{Builder, CertificateBuilder, Profile}, + der::EncodePem, + name::Name, + serial_number::SerialNumber, + time::Validity, + Certificate, +}; + +#[derive(Clone, Debug)] +pub struct CaReturn { + pub host_cert: Vec, + pub dev_cert: Vec, + pub private_key: Vec, +} + +pub fn make_cert( + signing_key: &RsaPrivateKey, + public_key: &RsaPublicKey, + common_name: Option<&str>, +) -> Result> { + // Create subject/issuer name + let name = match common_name { + Some(name) => Name::from_str(&format!("CN={name}"))?, + None => Name::default(), + }; + + // Set validity (10 years) + let validity = Validity::from_now(std::time::Duration::from_secs( + 365 * 9 * 12 * 31 * 24 * 60 * 60, // idk like 9 years + ))?; + + let signing_key = SigningKey::::new(signing_key.clone()); + let public_key = SubjectPublicKeyInfo::from_key(public_key.clone())?; + + // Build certificate + let cert = CertificateBuilder::new( + Profile::Root, + SerialNumber::new(&[1])?, + validity, + name, + public_key, + &signing_key, + )?; + + // Sign the certificate + let tbs_cert = cert.build()?; + + Ok(tbs_cert) +} + +// Equivalent to dump_cert +fn dump_cert(cert: &Certificate) -> Result> { + let b = cert.to_pem(LineEnding::LF)?; + Ok(b) +} + +pub(crate) fn generate_certificates( + device_public_key_pem: &[u8], + private_key: Option, +) -> Result> { + // Load device public key + println!("{}", std::str::from_utf8(device_public_key_pem)?); + let device_public_key = + RsaPublicKey::from_pkcs1_pem(std::str::from_utf8(device_public_key_pem)?)?; + + // Generate or use provided private key + let private_key = match private_key { + Some(p) => p, + None => { + let mut rng = rsa::rand_core::OsRng; + RsaPrivateKey::new(&mut rng, 2048)? + } + }; + + // Create CA cert + let ca_public_key = RsaPublicKey::from(&private_key); + let ca_cert = make_cert(&private_key, &ca_public_key, None)?; + + // Create device cert + let dev_cert = make_cert(&private_key, &device_public_key, Some("Device"))?; + + Ok(CaReturn { + host_cert: dump_cert(&ca_cert)?.into_bytes(), + dev_cert: dump_cert(&dev_cert)?.into_bytes(), + private_key: private_key + .to_pkcs8_pem(LineEnding::LF)? + .as_bytes() + .to_vec(), + }) +} diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index c529207..96aa2c8 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -5,6 +5,8 @@ pub mod afc; #[cfg(feature = "amfi")] pub mod amfi; +#[cfg(feature = "pair")] +mod ca; #[cfg(feature = "core_device_proxy")] pub mod core_device_proxy; #[cfg(feature = "debug_proxy")] @@ -413,6 +415,14 @@ pub enum IdeviceError { #[error("image not mounted")] ImageNotMounted, + #[cfg(feature = "pair")] + #[error("pairing trust dialog pending")] + PairingDialogResponsePending, + + #[cfg(feature = "pair")] + #[error("user denied pairing trust")] + UserDeniedPairing, + #[cfg(feature = "misagent")] #[error("misagent operation failed")] MisagentFailure, @@ -496,6 +506,10 @@ impl IdeviceError { "InvalidHostID" => Some(Self::InvalidHostID), "SessionInactive" => Some(Self::SessionInactive), "DeviceLocked" => Some(Self::DeviceLocked), + #[cfg(feature = "pair")] + "PairingDialogResponsePending" => Some(Self::PairingDialogResponsePending), + #[cfg(feature = "pair")] + "UserDeniedPairing" => Some(Self::UserDeniedPairing), "InternalError" => { let detailed_error = context .get("DetailedError") diff --git a/idevice/src/lockdown.rs b/idevice/src/lockdown.rs index 4e301b0..4dd2d2e 100644 --- a/idevice/src/lockdown.rs +++ b/idevice/src/lockdown.rs @@ -250,6 +250,94 @@ impl LockdownClient { } } } + + /// Generates a pairing file and sends it to the device for trusting. + /// Note that this does NOT save the file to usbmuxd's cache. That's a responsibility of the + /// caller. + /// Note that this function is computationally heavy in a debug build. + /// + /// # Arguments + /// * `host_id` - The host ID, in the form of a UUID. Typically generated from the host name + /// * `wifi_mac` - The MAC address of the WiFi interface. Does not affect anything. + /// * `system_buid` - UUID fetched from usbmuxd. Doesn't appear to affect function. + /// + /// # Returns + /// The newly generated pairing record + /// + /// # Errors + /// Returns `IdeviceError` + #[cfg(feature = "pair")] + pub async fn pair( + &mut self, + host_id: impl Into, + wifi_mac: impl Into, + system_buid: impl Into, + ) -> Result { + let host_id = host_id.into(); + let wifi_mac = wifi_mac.into(); + let system_buid = system_buid.into(); + + let pub_key = self.get_value("DevicePublicKey", None).await?; + let pub_key = match pub_key.as_data().map(|x| x.to_vec()) { + Some(p) => p, + None => { + log::warn!("Did not get public key data response"); + return Err(IdeviceError::UnexpectedResponse); + } + }; + + let ca = crate::ca::generate_certificates(&pub_key, None).unwrap(); + let mut pair_record = plist::Dictionary::new(); + pair_record.insert("DevicePublicKey".into(), plist::Value::Data(pub_key)); + pair_record.insert("DeviceCertificate".into(), plist::Value::Data(ca.dev_cert)); + pair_record.insert( + "HostCertificate".into(), + plist::Value::Data(ca.host_cert.clone()), + ); + pair_record.insert("HostID".into(), host_id.into()); + pair_record.insert("RootCertificate".into(), plist::Value::Data(ca.host_cert)); + pair_record.insert( + "RootPrivateKey".into(), + plist::Value::Data(ca.private_key.clone()), + ); + pair_record.insert("WiFiMACAddress".into(), wifi_mac.into()); + pair_record.insert("SystemBUID".into(), system_buid.into()); + + let mut options = plist::Dictionary::new(); + options.insert("ExtendedPairingErrors".into(), true.into()); + + let mut req = plist::Dictionary::new(); + req.insert("Label".into(), self.idevice.label.clone().into()); + req.insert("Request".into(), "Pair".into()); + req.insert( + "PairRecord".into(), + plist::Value::Dictionary(pair_record.clone()), + ); + req.insert("ProtocolVersion".into(), "2".into()); + req.insert("PairingOptions".into(), plist::Value::Dictionary(options)); + + loop { + self.idevice.send_plist(req.clone().into()).await?; + match self.idevice.read_plist().await { + Ok(escrow) => { + pair_record.insert("HostPrivateKey".into(), plist::Value::Data(ca.private_key)); + if let Some(escrow) = escrow.get("EscrowBag").and_then(|x| x.as_data()) { + pair_record.insert("EscrowBag".into(), plist::Value::Data(escrow.to_vec())); + } + + let p = crate::pairing_file::PairingFile::from_value( + &plist::Value::Dictionary(pair_record), + )?; + + break Ok(p); + } + Err(IdeviceError::PairingDialogResponsePending) => { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } + Err(e) => break Err(e), + } + } + } } impl From for LockdownClient {