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