mirror of
https://github.com/jkcoxson/idevice.git
synced 2026-03-02 14:36:16 +01:00
Implement pairing
This commit is contained in:
@@ -40,7 +40,13 @@ reqwest = { version = "0.12", features = [
|
|||||||
rand = { version = "0.9", optional = true }
|
rand = { version = "0.9", optional = true }
|
||||||
futures = { version = "0.3", 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]
|
[dev-dependencies]
|
||||||
tokio = { version = "1.43", features = ["fs"] }
|
tokio = { version = "1.43", features = ["fs"] }
|
||||||
@@ -59,6 +65,7 @@ springboardservices = []
|
|||||||
misagent = []
|
misagent = []
|
||||||
mobile_image_mounter = ["dep:sha2"]
|
mobile_image_mounter = ["dep:sha2"]
|
||||||
location_simulation = []
|
location_simulation = []
|
||||||
|
pair = ["chrono/default", "dep:sha2", "dep:rsa", "dep:x509-cert"]
|
||||||
tcp = ["tokio/net"]
|
tcp = ["tokio/net"]
|
||||||
tunnel_tcp_stack = ["dep:rand", "dep:futures", "tokio/fs", "tokio/sync"]
|
tunnel_tcp_stack = ["dep:rand", "dep:futures", "tokio/fs", "tokio/sync"]
|
||||||
tss = ["dep:uuid", "dep:reqwest"]
|
tss = ["dep:uuid", "dep:reqwest"]
|
||||||
@@ -82,6 +89,7 @@ full = [
|
|||||||
"installation_proxy",
|
"installation_proxy",
|
||||||
"misagent",
|
"misagent",
|
||||||
"mobile_image_mounter",
|
"mobile_image_mounter",
|
||||||
|
"pair",
|
||||||
"usbmuxd",
|
"usbmuxd",
|
||||||
"xpc",
|
"xpc",
|
||||||
"location_simulation",
|
"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;
|
pub mod afc;
|
||||||
#[cfg(feature = "amfi")]
|
#[cfg(feature = "amfi")]
|
||||||
pub mod amfi;
|
pub mod amfi;
|
||||||
|
#[cfg(feature = "pair")]
|
||||||
|
mod ca;
|
||||||
#[cfg(feature = "core_device_proxy")]
|
#[cfg(feature = "core_device_proxy")]
|
||||||
pub mod core_device_proxy;
|
pub mod core_device_proxy;
|
||||||
#[cfg(feature = "debug_proxy")]
|
#[cfg(feature = "debug_proxy")]
|
||||||
@@ -413,6 +415,14 @@ pub enum IdeviceError {
|
|||||||
#[error("image not mounted")]
|
#[error("image not mounted")]
|
||||||
ImageNotMounted,
|
ImageNotMounted,
|
||||||
|
|
||||||
|
#[cfg(feature = "pair")]
|
||||||
|
#[error("pairing trust dialog pending")]
|
||||||
|
PairingDialogResponsePending,
|
||||||
|
|
||||||
|
#[cfg(feature = "pair")]
|
||||||
|
#[error("user denied pairing trust")]
|
||||||
|
UserDeniedPairing,
|
||||||
|
|
||||||
#[cfg(feature = "misagent")]
|
#[cfg(feature = "misagent")]
|
||||||
#[error("misagent operation failed")]
|
#[error("misagent operation failed")]
|
||||||
MisagentFailure,
|
MisagentFailure,
|
||||||
@@ -496,6 +506,10 @@ impl IdeviceError {
|
|||||||
"InvalidHostID" => Some(Self::InvalidHostID),
|
"InvalidHostID" => Some(Self::InvalidHostID),
|
||||||
"SessionInactive" => Some(Self::SessionInactive),
|
"SessionInactive" => Some(Self::SessionInactive),
|
||||||
"DeviceLocked" => Some(Self::DeviceLocked),
|
"DeviceLocked" => Some(Self::DeviceLocked),
|
||||||
|
#[cfg(feature = "pair")]
|
||||||
|
"PairingDialogResponsePending" => Some(Self::PairingDialogResponsePending),
|
||||||
|
#[cfg(feature = "pair")]
|
||||||
|
"UserDeniedPairing" => Some(Self::UserDeniedPairing),
|
||||||
"InternalError" => {
|
"InternalError" => {
|
||||||
let detailed_error = context
|
let detailed_error = context
|
||||||
.get("DetailedError")
|
.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 {
|
impl From<Idevice> for LockdownClient {
|
||||||
|
|||||||
Reference in New Issue
Block a user