Skip to main content

ios_core/lockdown/
protocol.rs

1use 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// ── Request / Response structs ────────────────────────────
56
57#[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    /// Device may return an Error field instead of Port when service is unavailable.
158    #[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}