mirror of
https://github.com/jkcoxson/idevice.git
synced 2026-03-02 06:26:15 +01:00
Implement pairing
This commit is contained in:
@@ -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",
|
||||
|
||||
103
idevice/src/ca.rs
Normal file
103
idevice/src/ca.rs
Normal file
@@ -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<u8>,
|
||||
pub dev_cert: Vec<u8>,
|
||||
pub private_key: Vec<u8>,
|
||||
}
|
||||
|
||||
pub fn make_cert(
|
||||
signing_key: &RsaPrivateKey,
|
||||
public_key: &RsaPublicKey,
|
||||
common_name: Option<&str>,
|
||||
) -> Result<Certificate, Box<dyn std::error::Error>> {
|
||||
// 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::<Sha256>::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<String, Box<dyn std::error::Error>> {
|
||||
let b = cert.to_pem(LineEnding::LF)?;
|
||||
Ok(b)
|
||||
}
|
||||
|
||||
pub(crate) fn generate_certificates(
|
||||
device_public_key_pem: &[u8],
|
||||
private_key: Option<RsaPrivateKey>,
|
||||
) -> Result<CaReturn, Box<dyn std::error::Error>> {
|
||||
// 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(),
|
||||
})
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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<String>,
|
||||
wifi_mac: impl Into<String>,
|
||||
system_buid: impl Into<String>,
|
||||
) -> Result<crate::pairing_file::PairingFile, IdeviceError> {
|
||||
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<Idevice> for LockdownClient {
|
||||
|
||||
Reference in New Issue
Block a user