ios_core/lockdown/
protocol.rs1use serde::{Deserialize, Serialize};
2use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
3
4use crate::lockdown::LockdownError;
5
6pub const LOCKDOWN_PORT: u16 = 62078;
7const MAX_LOCKDOWN_FRAME_SIZE: usize = 4 * 1024 * 1024;
8
9pub fn encode_frame(payload: &[u8]) -> Vec<u8> {
10 debug_assert!(
11 payload.len() <= u32::MAX as usize,
12 "lockdown frame payload exceeds u32::MAX"
13 );
14 let mut buf = Vec::with_capacity(4 + payload.len());
15 buf.extend_from_slice(&(payload.len() as u32).to_be_bytes());
16 buf.extend_from_slice(payload);
17 buf
18}
19
20pub async fn recv_frame<R: AsyncRead + Unpin>(reader: &mut R) -> Result<Vec<u8>, LockdownError> {
21 let mut len_buf = [0u8; 4];
22 reader.read_exact(&mut len_buf).await?;
23 let length = u32::from_be_bytes(len_buf) as usize;
24 if length > MAX_LOCKDOWN_FRAME_SIZE {
25 return Err(LockdownError::Protocol(format!(
26 "frame too large: {length} bytes exceeds {MAX_LOCKDOWN_FRAME_SIZE}"
27 )));
28 }
29 let mut payload = vec![0u8; length];
30 reader.read_exact(&mut payload).await?;
31 Ok(payload)
32}
33
34pub async fn send_lockdown<W, T>(writer: &mut W, value: &T) -> Result<(), LockdownError>
35where
36 W: AsyncWrite + Unpin,
37 T: Serialize,
38{
39 let mut bytes = Vec::new();
40 plist::to_writer_xml(&mut bytes, value).map_err(|e| LockdownError::Protocol(e.to_string()))?;
41 writer.write_all(&encode_frame(&bytes)).await?;
42 writer.flush().await?;
43 Ok(())
44}
45
46pub async fn recv_lockdown<R, T>(reader: &mut R) -> Result<T, LockdownError>
47where
48 R: AsyncRead + Unpin,
49 T: for<'de> Deserialize<'de>,
50{
51 let payload = recv_frame(reader).await?;
52 plist::from_bytes(&payload).map_err(|e| LockdownError::Protocol(e.to_string()))
53}
54
55#[derive(Serialize, Deserialize)]
58#[serde(rename_all = "PascalCase")]
59pub struct QueryTypeRequest {
60 pub label: &'static str,
61 pub request: &'static str,
62}
63
64#[derive(Debug, Serialize, Deserialize)]
65#[serde(rename_all = "PascalCase")]
66pub struct QueryTypeResponse {
67 #[serde(rename = "Type")]
68 pub type_: String,
69}
70
71#[derive(Serialize)]
72#[serde(rename_all = "PascalCase")]
73pub struct GetValueRequest<'a> {
74 pub label: &'static str,
75 pub request: &'static str,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub domain: Option<&'a str>,
78 #[serde(skip_serializing_if = "Option::is_none")]
79 pub key: Option<&'a str>,
80}
81
82#[derive(Debug, Deserialize)]
83#[serde(rename_all = "PascalCase")]
84pub struct GetValueResponse {
85 pub value: plist::Value,
86}
87
88#[derive(Serialize)]
89#[serde(rename_all = "PascalCase")]
90pub struct SetValueRequest<'a, T>
91where
92 T: Serialize,
93{
94 pub label: &'static str,
95 pub request: &'static str,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub domain: Option<&'a str>,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 pub key: Option<&'a str>,
100 pub value: T,
101}
102
103#[derive(Serialize)]
104#[serde(rename_all = "PascalCase")]
105pub struct RemoveValueRequest<'a> {
106 pub label: &'static str,
107 pub request: &'static str,
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub domain: Option<&'a str>,
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub key: Option<&'a str>,
112}
113
114#[derive(Debug, Deserialize)]
115#[serde(rename_all = "PascalCase")]
116pub struct ValueOperationResponse {
117 #[serde(rename = "Error")]
118 pub error: Option<String>,
119 #[serde(rename = "Value")]
120 pub value: Option<plist::Value>,
121}
122
123#[derive(Serialize)]
124#[serde(rename_all = "PascalCase")]
125pub struct StartSessionRequest {
126 pub label: &'static str,
127 pub protocol_version: &'static str,
128 pub request: &'static str,
129 #[serde(rename = "HostID")]
130 pub host_id: String,
131 #[serde(rename = "SystemBUID")]
132 pub system_buid: String,
133}
134
135#[derive(Debug, Deserialize)]
136pub struct StartSessionResponse {
137 #[serde(rename = "SessionID")]
138 pub session_id: String,
139 #[serde(rename = "EnableSessionSSL")]
140 pub enable_session_ssl: bool,
141}
142
143#[derive(Serialize)]
144#[serde(rename_all = "PascalCase")]
145pub struct StartServiceRequest {
146 pub label: &'static str,
147 pub request: &'static str,
148 pub service: String,
149}
150
151#[derive(Debug, Serialize, Deserialize)]
152pub struct StartServiceResponse {
153 #[serde(rename = "Port")]
154 pub port: Option<u16>,
155 #[serde(rename = "EnableServiceSSL")]
156 pub enable_service_ssl: Option<bool>,
157 #[serde(rename = "Error")]
159 pub error: Option<String>,
160}
161
162#[derive(Serialize)]
163#[serde(rename_all = "PascalCase")]
164pub struct StopSessionRequest {
165 pub label: &'static str,
166 pub request: &'static str,
167 #[serde(rename = "SessionID")]
168 pub session_id: String,
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn test_encode_lockdown_frame() {
177 let payload = b"hello";
178 let frame = encode_frame(payload);
179 assert_eq!(&frame[..4], &5u32.to_be_bytes());
180 assert_eq!(&frame[4..], payload);
181 }
182
183 #[tokio::test]
184 async fn test_roundtrip_frame() {
185 let payload = b"<plist/>";
186 let frame = encode_frame(payload);
187 let mut cursor = std::io::Cursor::new(frame);
188 let decoded = recv_frame(&mut cursor).await.unwrap();
189 assert_eq!(decoded, payload);
190 }
191
192 #[tokio::test]
193 async fn test_recv_frame_empty_payload() {
194 let frame = encode_frame(b"");
195 let mut cursor = std::io::Cursor::new(frame);
196 let decoded = recv_frame(&mut cursor).await.unwrap();
197 assert!(decoded.is_empty());
198 }
199
200 #[tokio::test]
201 async fn test_recv_frame_rejects_oversized_payload() {
202 let mut frame = ((MAX_LOCKDOWN_FRAME_SIZE as u32) + 1)
203 .to_be_bytes()
204 .to_vec();
205 frame.extend_from_slice(b"ignored");
206 let mut cursor = std::io::Cursor::new(frame);
207
208 let err = recv_frame(&mut cursor).await.unwrap_err();
209 assert!(
210 matches!(err, LockdownError::Protocol(message) if message.contains("frame too large"))
211 );
212 }
213
214 #[test]
215 fn test_set_value_request_serializes_domain_key_and_value() {
216 let request = SetValueRequest {
217 label: "ios-rs",
218 request: "SetValue",
219 domain: Some("com.apple.international"),
220 key: Some("Language"),
221 value: "en",
222 };
223
224 let mut bytes = Vec::new();
225 plist::to_writer_xml(&mut bytes, &request).unwrap();
226 let xml = String::from_utf8(bytes).unwrap();
227
228 assert!(xml.contains("<key>Request</key>"));
229 assert!(xml.contains("<string>SetValue</string>"));
230 assert!(xml.contains("<key>Domain</key>"));
231 assert!(xml.contains("<string>com.apple.international</string>"));
232 assert!(xml.contains("<key>Key</key>"));
233 assert!(xml.contains("<string>Language</string>"));
234 assert!(xml.contains("<key>Value</key>"));
235 assert!(xml.contains("<string>en</string>"));
236 }
237
238 #[test]
239 fn test_remove_value_request_serializes_domain_and_key() {
240 let request = RemoveValueRequest {
241 label: "ios-rs",
242 request: "RemoveValue",
243 domain: Some("com.apple.mobile.wireless_lockdown"),
244 key: Some("EnableWifiConnections"),
245 };
246
247 let mut bytes = Vec::new();
248 plist::to_writer_xml(&mut bytes, &request).unwrap();
249 let xml = String::from_utf8(bytes).unwrap();
250
251 assert!(xml.contains("<key>Request</key>"));
252 assert!(xml.contains("<string>RemoveValue</string>"));
253 assert!(xml.contains("<key>Domain</key>"));
254 assert!(xml.contains("<string>com.apple.mobile.wireless_lockdown</string>"));
255 assert!(xml.contains("<key>Key</key>"));
256 assert!(xml.contains("<string>EnableWifiConnections</string>"));
257 }
258}