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