1use std::path::PathBuf;
7
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct PersistedCredentials {
12 #[serde(default, skip_serializing_if = "Option::is_none")]
13 pub remote_identifier: Option<String>,
14 pub host_identifier: String,
15 pub host_public_key_hex: String,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
19 pub host_private_key_hex: Option<String>,
20 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub remote_unlock_host_key: Option<String>,
23 pub device_address: String,
25 pub rsd_port: u16,
27}
28
29impl PersistedCredentials {
30 pub fn default_dir() -> PathBuf {
32 if cfg!(target_os = "macos") {
33 dirs_next::home_dir()
34 .unwrap_or_else(|| PathBuf::from("/tmp"))
35 .join(".ios-rs")
36 } else if cfg!(windows) {
37 std::env::var("APPDATA")
38 .map(PathBuf::from)
39 .unwrap_or_else(|_| PathBuf::from("C:\\ProgramData"))
40 .join("ios-rs")
41 } else {
42 dirs_next::home_dir()
43 .unwrap_or_else(|| PathBuf::from("/tmp"))
44 .join(".ios-rs")
45 }
46 }
47
48 pub fn pymobiledevice3_dir() -> PathBuf {
50 dirs_next::home_dir()
51 .unwrap_or_else(|| PathBuf::from("/tmp"))
52 .join(".pymobiledevice3")
53 }
54
55 pub fn path_for(dir: &std::path::Path, device_addr: &str) -> PathBuf {
57 let safe_addr = device_addr.replace([':', '%'], "_");
58 dir.join(format!("{safe_addr}.json"))
59 }
60
61 pub fn save(&self, dir: &std::path::Path) -> std::io::Result<()> {
63 std::fs::create_dir_all(dir)?;
64 let path = Self::path_for(dir, &self.device_address);
65 let json = serde_json::to_string_pretty(self).map_err(std::io::Error::other)?;
66 std::fs::write(path, json)
67 }
68
69 pub fn load(dir: &std::path::Path, device_addr: &str) -> Option<Self> {
71 let path = Self::path_for(dir, device_addr);
72 let json = std::fs::read_to_string(path).ok()?;
73 serde_json::from_str(&json).ok()
74 }
75
76 pub fn list(dir: &std::path::Path) -> Vec<Self> {
78 let Ok(entries) = std::fs::read_dir(dir) else {
79 return vec![];
80 };
81 entries
82 .filter_map(|e| e.ok())
83 .filter(|e| e.path().extension().map(|x| x == "json").unwrap_or(false))
84 .filter_map(|e| std::fs::read_to_string(e.path()).ok())
85 .filter_map(|s| serde_json::from_str(&s).ok())
86 .collect()
87 }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
91pub struct RemotePairingRecord {
92 pub public_key: Vec<u8>,
93 pub private_key: Vec<u8>,
94 #[serde(default, skip_serializing_if = "Option::is_none")]
95 pub remote_unlock_host_key: Option<String>,
96}
97
98impl RemotePairingRecord {
99 pub fn path_for_identifier(dir: &std::path::Path, remote_identifier: &str) -> PathBuf {
100 dir.join(format!("remote_{remote_identifier}.plist"))
101 }
102
103 pub fn save_for_identifier(
104 &self,
105 dir: &std::path::Path,
106 remote_identifier: &str,
107 ) -> std::io::Result<()> {
108 std::fs::create_dir_all(dir)?;
109 plist::to_file_xml(Self::path_for_identifier(dir, remote_identifier), self)
110 .map_err(std::io::Error::other)
111 }
112
113 pub fn load_for_identifier(dir: &std::path::Path, remote_identifier: &str) -> Option<Self> {
114 plist::from_file(Self::path_for_identifier(dir, remote_identifier)).ok()
115 }
116
117 pub fn list(dir: &std::path::Path) -> Vec<(String, Self)> {
118 let Ok(entries) = std::fs::read_dir(dir) else {
119 return vec![];
120 };
121
122 entries
123 .filter_map(|entry| entry.ok())
124 .filter_map(|entry| {
125 let path = entry.path();
126 let file_name = path.file_name()?.to_str()?;
127 let remote_identifier = file_name
128 .strip_prefix("remote_")?
129 .strip_suffix(".plist")?
130 .to_string();
131 let record = plist::from_file(&path).ok()?;
132 Some((remote_identifier, record))
133 })
134 .collect()
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn test_roundtrip() {
144 let dir = std::env::temp_dir().join("ios_rs_test_creds");
145 let cred = PersistedCredentials {
146 remote_identifier: Some("test-remote".into()),
147 host_identifier: "test-id".into(),
148 host_public_key_hex: "deadbeef".into(),
149 host_private_key_hex: Some("cafebabe".into()),
150 remote_unlock_host_key: Some("host-key".into()),
151 device_address: "fd00::1".into(),
152 rsd_port: 58783,
153 };
154 cred.save(&dir).unwrap();
155 let loaded = PersistedCredentials::load(&dir, "fd00::1").unwrap();
156 assert_eq!(loaded.remote_identifier.as_deref(), Some("test-remote"));
157 assert_eq!(loaded.host_identifier, "test-id");
158 assert_eq!(loaded.rsd_port, 58783);
159 assert_eq!(loaded.host_private_key_hex.as_deref(), Some("cafebabe"));
160 assert_eq!(loaded.remote_unlock_host_key.as_deref(), Some("host-key"));
161 let _ = std::fs::remove_dir_all(&dir);
163 }
164
165 #[test]
166 fn test_backward_compatible_load_without_private_key() {
167 let dir = std::env::temp_dir().join("ios_rs_test_creds_legacy");
168 std::fs::create_dir_all(&dir).unwrap();
169 let path = PersistedCredentials::path_for(&dir, "fd00::2");
170 std::fs::write(
171 &path,
172 r#"{
173 "host_identifier": "legacy-id",
174 "host_public_key_hex": "deadbeef",
175 "device_address": "fd00::2",
176 "rsd_port": 58783
177}"#,
178 )
179 .unwrap();
180
181 let loaded = PersistedCredentials::load(&dir, "fd00::2").unwrap();
182 assert_eq!(loaded.host_identifier, "legacy-id");
183 assert!(loaded.host_private_key_hex.is_none());
184 assert!(loaded.remote_identifier.is_none());
185 assert!(loaded.remote_unlock_host_key.is_none());
186
187 let _ = std::fs::remove_dir_all(&dir);
188 }
189
190 #[test]
191 fn test_remote_pairing_record_roundtrip() {
192 let dir = std::env::temp_dir().join("ios_rs_test_remote_pair_record");
193 let record = RemotePairingRecord {
194 public_key: vec![0x01, 0x02, 0x03],
195 private_key: vec![0x04, 0x05, 0x06],
196 remote_unlock_host_key: Some("PcV5xhyuJBL7Qq9HOGeGVwtU4sJLe1jtl/vRy1tRKcI=".into()),
197 };
198
199 record
200 .save_for_identifier(&dir, "00008150-000D6D6A1122401C")
201 .unwrap();
202
203 let loaded =
204 RemotePairingRecord::load_for_identifier(&dir, "00008150-000D6D6A1122401C").unwrap();
205 assert_eq!(loaded, record);
206
207 let listed = RemotePairingRecord::list(&dir);
208 assert_eq!(listed.len(), 1);
209 assert_eq!(listed[0].0, "00008150-000D6D6A1122401C");
210 assert_eq!(listed[0].1, record);
211
212 let _ = std::fs::remove_dir_all(&dir);
213 }
214}