Skip to main content

ios_core/lockdown/
pair_record.rs

1use std::path::PathBuf;
2
3use serde::Deserialize;
4
5#[derive(Debug, thiserror::Error)]
6pub enum PairRecordError {
7    #[error("pair record not found for UDID: {0}")]
8    NotFound(String),
9    #[error("failed to parse pair record: {0}")]
10    Parse(String),
11}
12
13/// iOS device pair record, loaded from the platform-specific lockdown directory.
14#[derive(Debug, Deserialize)]
15#[serde(rename_all = "PascalCase")]
16pub struct PairRecord {
17    /// DER/PEM-encoded device certificate
18    #[serde(with = "serde_bytes")]
19    pub device_certificate: Vec<u8>,
20    /// DER/PEM-encoded host certificate
21    #[serde(with = "serde_bytes")]
22    pub host_certificate: Vec<u8>,
23    /// DER/PEM-encoded host private key
24    #[serde(with = "serde_bytes")]
25    pub host_private_key: Vec<u8>,
26    /// DER/PEM-encoded root certificate
27    #[serde(with = "serde_bytes")]
28    pub root_certificate: Vec<u8>,
29    /// Host identifier (UUID string)
30    #[serde(rename = "HostID")]
31    pub host_id: String,
32    /// System BUID
33    #[serde(rename = "SystemBUID")]
34    pub system_buid: String,
35    /// Wi-Fi MAC address recorded by lockdown pairing, used for mobdev2 discovery matching.
36    pub wifi_mac_address: Option<String>,
37}
38
39impl PairRecord {
40    /// Load from the platform default path.
41    pub fn load(udid: &str) -> Result<Self, PairRecordError> {
42        let path = default_pair_record_path(udid);
43        Self::load_from_path(&path, udid)
44    }
45
46    /// Load from an explicit path.
47    pub fn load_from_path(path: &std::path::Path, udid: &str) -> Result<Self, PairRecordError> {
48        let data = std::fs::read(path).map_err(|_| PairRecordError::NotFound(udid.to_string()))?;
49        plist::from_bytes(&data).map_err(|e| PairRecordError::Parse(e.to_string()))
50    }
51}
52
53pub fn default_pair_record_path(udid: &str) -> PathBuf {
54    default_pair_record_dir().join(format!("{udid}.plist"))
55}
56
57pub fn default_pair_record_dir() -> PathBuf {
58    pair_record_dir_for_platform(
59        cfg!(target_os = "macos"),
60        cfg!(windows),
61        &std::env::var("ALLUSERSPROFILE").unwrap_or_default(),
62    )
63}
64
65#[cfg(test)]
66pub(crate) fn pair_record_path_for_platform(
67    udid: &str,
68    is_macos: bool,
69    is_windows: bool,
70    all_users_profile: &str,
71) -> PathBuf {
72    pair_record_dir_for_platform(is_macos, is_windows, all_users_profile)
73        .join(format!("{udid}.plist"))
74}
75
76fn pair_record_dir_for_platform(
77    is_macos: bool,
78    is_windows: bool,
79    all_users_profile: &str,
80) -> PathBuf {
81    if is_windows {
82        PathBuf::from(all_users_profile)
83            .join("Apple")
84            .join("Lockdown")
85    } else if is_macos {
86        PathBuf::from("/var/db/lockdown")
87    } else {
88        PathBuf::from("/var/lib/lockdown")
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn test_pair_record_path_macos() {
98        let path = pair_record_path_for_platform("ABC123DEF", true, false, "");
99        assert_eq!(path, PathBuf::from("/var/db/lockdown/ABC123DEF.plist"));
100    }
101
102    #[test]
103    fn test_pair_record_path_windows() {
104        let path = pair_record_path_for_platform("ABC123DEF", false, true, "C:\\ProgramData");
105        let s = path.to_string_lossy();
106        assert!(s.contains("ABC123DEF"));
107        assert!(s.contains("Apple"));
108        assert!(s.contains("Lockdown"));
109    }
110
111    #[test]
112    fn test_pair_record_path_linux() {
113        let path = pair_record_path_for_platform("ABC123DEF", false, false, "");
114        assert_eq!(path, PathBuf::from("/var/lib/lockdown/ABC123DEF.plist"));
115    }
116
117    #[test]
118    fn test_pair_record_dir_windows() {
119        let path = pair_record_dir_for_platform(false, true, "C:\\ProgramData");
120        assert!(path.starts_with("C:\\ProgramData"));
121        assert!(path.ends_with(PathBuf::from("Apple").join("Lockdown")));
122    }
123}