use std::path::Path;
#[cfg(all(feature = "openssl", not(feature = "rustls")))]
use openssl::{
pkey::{PKey, Private},
x509::X509,
};
use plist::Data;
#[cfg(feature = "rustls")]
use rustls::pki_types::{CertificateDer, pem::PemObject};
use serde::{Deserialize, Serialize};
use tracing::warn;
#[cfg(feature = "rustls")]
#[derive(Clone, Debug)]
pub struct PairingFile {
pub device_certificate: CertificateDer<'static>,
pub host_private_key: Vec<u8>,
pub host_certificate: CertificateDer<'static>,
pub root_private_key: Vec<u8>,
pub root_certificate: CertificateDer<'static>,
pub system_buid: String,
pub host_id: String,
pub escrow_bag: Option<Vec<u8>>,
pub wifi_mac_address: String,
pub udid: Option<String>,
}
#[cfg(all(feature = "openssl", not(feature = "rustls")))]
#[derive(Clone, Debug)]
pub struct PairingFile {
pub device_certificate: X509,
pub host_private_key: PKey<Private>,
pub host_certificate: X509,
pub root_private_key: PKey<Private>,
pub root_certificate: X509,
pub system_buid: String,
pub host_id: String,
pub escrow_bag: Vec<u8>,
pub wifi_mac_address: String,
pub udid: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "PascalCase")]
struct RawPairingFile {
device_certificate: Data,
host_private_key: Data,
host_certificate: Data,
root_private_key: Data,
root_certificate: Data,
#[serde(rename = "SystemBUID")]
system_buid: String,
#[serde(rename = "HostID")]
host_id: String,
escrow_bag: Option<Data>, #[serde(rename = "WiFiMACAddress")]
wifi_mac_address: String,
#[serde(rename = "UDID")]
udid: Option<String>,
}
impl PairingFile {
pub fn read_from_file(path: impl AsRef<Path>) -> Result<Self, crate::IdeviceError> {
let f = std::fs::read(path)?;
Self::from_bytes(&f)
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, crate::IdeviceError> {
let r = match ::plist::from_bytes::<RawPairingFile>(bytes) {
Ok(r) => r,
Err(e) => {
warn!("Unable to convert bytes to raw pairing file: {e:?}");
return Err(crate::IdeviceError::UnexpectedResponse(
"failed to parse raw pairing file from bytes".into(),
));
}
};
match r.try_into() {
Ok(r) => Ok(r),
Err(e) => {
warn!("Unable to convert raw pairing file into pairing file: {e:?}");
Err(crate::IdeviceError::UnexpectedResponse(
"failed to convert raw pairing file into pairing file".into(),
))
}
}
}
pub fn from_value(v: &plist::Value) -> Result<Self, crate::IdeviceError> {
let raw: RawPairingFile = plist::from_value(v)?;
match raw.try_into() {
Ok(p) => Ok(p),
Err(e) => {
warn!("Unable to convert raw pairing file into pairing file: {e:?}");
Err(crate::IdeviceError::UnexpectedResponse(
"failed to convert raw pairing file into pairing file".into(),
))
}
}
}
#[cfg(feature = "rustls")]
pub fn serialize(self) -> Result<Vec<u8>, crate::IdeviceError> {
let raw = RawPairingFile::from(self);
let mut buf = Vec::new();
plist::to_writer_xml(&mut buf, &raw)?;
Ok(buf)
}
#[cfg(all(feature = "openssl", not(feature = "rustls")))]
pub fn serialize(self) -> Result<Vec<u8>, crate::IdeviceError> {
let raw = RawPairingFile::try_from(self)?;
let mut buf = Vec::new();
plist::to_writer_xml(&mut buf, &raw)?;
Ok(buf)
}
}
#[cfg(feature = "rustls")]
impl TryFrom<RawPairingFile> for PairingFile {
type Error = rustls::pki_types::pem::Error;
fn try_from(value: RawPairingFile) -> Result<Self, Self::Error> {
let device_cert_data = Into::<Vec<u8>>::into(value.device_certificate);
let host_private_key_data = Into::<Vec<u8>>::into(value.host_private_key);
let host_cert_data = Into::<Vec<u8>>::into(value.host_certificate);
let root_private_key_data = Into::<Vec<u8>>::into(value.root_private_key);
let root_cert_data = Into::<Vec<u8>>::into(value.root_certificate);
let device_certificate_pem = ensure_pem_headers(&device_cert_data, "CERTIFICATE");
let host_certificate_pem = ensure_pem_headers(&host_cert_data, "CERTIFICATE");
let root_certificate_pem = ensure_pem_headers(&root_cert_data, "CERTIFICATE");
Ok(Self {
device_certificate: CertificateDer::from_pem_slice(&device_certificate_pem)?,
host_private_key: host_private_key_data,
host_certificate: CertificateDer::from_pem_slice(&host_certificate_pem)?,
root_private_key: root_private_key_data,
root_certificate: CertificateDer::from_pem_slice(&root_certificate_pem)?,
system_buid: value.system_buid,
host_id: value.host_id,
escrow_bag: value.escrow_bag.map(|x| x.into()),
wifi_mac_address: value.wifi_mac_address,
udid: value.udid,
})
}
}
#[cfg(all(feature = "openssl", not(feature = "rustls")))]
impl TryFrom<RawPairingFile> for PairingFile {
type Error = openssl::error::ErrorStack;
fn try_from(value: RawPairingFile) -> Result<Self, Self::Error> {
Ok(Self {
device_certificate: X509::from_pem(&Into::<Vec<u8>>::into(value.device_certificate))?,
host_private_key: PKey::private_key_from_pem(&Into::<Vec<u8>>::into(
value.host_private_key,
))?,
host_certificate: X509::from_pem(&Into::<Vec<u8>>::into(value.host_certificate))?,
root_private_key: PKey::private_key_from_pem(&Into::<Vec<u8>>::into(
value.root_private_key,
))?,
root_certificate: X509::from_pem(&Into::<Vec<u8>>::into(value.root_certificate))?,
system_buid: value.system_buid,
host_id: value.host_id,
escrow_bag: value.escrow_bag.map(|x| x.into()),
wifi_mac_address: value.wifi_mac_address,
udid: value.udid,
})
}
}
#[cfg(feature = "rustls")]
impl From<PairingFile> for RawPairingFile {
fn from(value: PairingFile) -> Self {
let device_cert_data = ensure_pem_headers(&value.device_certificate, "CERTIFICATE");
let host_cert_data = ensure_pem_headers(&value.host_certificate, "CERTIFICATE");
let root_cert_data = ensure_pem_headers(&value.root_certificate, "CERTIFICATE");
let host_private_key_data = ensure_pem_headers(&value.host_private_key, "PRIVATE KEY");
let root_private_key_data = ensure_pem_headers(&value.root_private_key, "PRIVATE KEY");
Self {
device_certificate: Data::new(device_cert_data),
host_private_key: Data::new(host_private_key_data),
host_certificate: Data::new(host_cert_data),
root_private_key: Data::new(root_private_key_data),
root_certificate: Data::new(root_cert_data),
system_buid: value.system_buid,
host_id: value.host_id.clone(),
escrow_bag: value.escrow_bag.map(Data::new),
wifi_mac_address: value.wifi_mac_address,
udid: value.udid,
}
}
}
#[cfg(all(feature = "openssl", not(feature = "rustls")))]
impl TryFrom<PairingFile> for RawPairingFile {
type Error = openssl::error::ErrorStack;
fn try_from(value: PairingFile) -> Result<Self, Self::Error> {
Ok(Self {
device_certificate: Data::new(value.device_certificate.to_pem()?),
host_private_key: Data::new(value.host_private_key.private_key_to_pem_pkcs8()?),
host_certificate: Data::new(value.host_certificate.to_pem()?),
root_private_key: Data::new(value.root_private_key.private_key_to_pem_pkcs8()?),
root_certificate: Data::new(value.root_certificate.to_pem()?),
system_buid: value.system_buid,
host_id: value.host_id.clone(),
escrow_bag: value.escrow_bag.map(Data::new),
wifi_mac_address: value.wifi_mac_address,
udid: value.udid,
})
}
}
fn ensure_pem_headers(data: &[u8], pem_type: &str) -> Vec<u8> {
if is_pem_formatted(data) {
return data.to_vec();
}
let mut result = Vec::new();
let header = format!("-----BEGIN {pem_type}-----\n");
result.extend_from_slice(header.as_bytes());
let base64_content = if is_base64(data) {
let data_str = String::from_utf8_lossy(data);
data_str.replace(['\n', '\r', ' '], "").into_bytes()
} else {
let engine = base64::prelude::BASE64_STANDARD;
base64::Engine::encode(&engine, data).into_bytes()
};
for (i, chunk) in base64_content.chunks(64).enumerate() {
if i > 0 {
result.push(b'\n');
}
result.extend_from_slice(chunk);
}
result.push(b'\n');
let footer = format!("-----END {pem_type}-----");
result.extend_from_slice(footer.as_bytes());
result
}
fn is_pem_formatted(data: &[u8]) -> bool {
if let Ok(data_str) = std::str::from_utf8(data) {
data_str.contains("-----BEGIN") && data_str.contains("-----END")
} else {
false
}
}
fn is_base64(data: &[u8]) -> bool {
if let Ok(data_str) = std::str::from_utf8(data) {
data_str.chars().all(|c| {
c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=' || c.is_whitespace()
})
} else {
false
}
}
#[test]
fn test_pairing_file_roundtrip() {
let f = std::fs::read("/var/lib/lockdown/test.plist").unwrap();
println!("{}", String::from_utf8_lossy(&f));
let input = PairingFile::from_bytes(&f).unwrap();
let output = input.serialize().unwrap();
println!("{}", String::from_utf8_lossy(&output));
assert_eq!(f[..output.len()], output);
}