Skip to main content

ios_core/services/house_arrest/
mod.rs

1//! Minimal House Arrest client for vending an app container and then
2//! reusing the returned stream as AFC.
3
4use serde::{Deserialize, Serialize};
5use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
6
7use crate::services::afc::{AfcClient, AfcError};
8
9pub const SERVICE_NAME: &str = "com.apple.mobile.house_arrest";
10
11service_error!(
12    HouseArrestError,
13    #[error("house arrest error: {0}")]
14    Service(String),
15);
16
17impl From<AfcError> for HouseArrestError {
18    fn from(err: AfcError) -> Self {
19        match err {
20            AfcError::Io(e) => Self::Io(e),
21            AfcError::Status(code) => Self::Service(code.to_string()),
22            AfcError::Protocol(msg) => Self::Protocol(msg),
23        }
24    }
25}
26
27#[derive(Debug)]
28pub struct HouseArrestClient<S> {
29    stream: S,
30}
31
32impl<S: AsyncRead + AsyncWrite + Unpin> HouseArrestClient<S> {
33    pub fn new(stream: S) -> Self {
34        Self { stream }
35    }
36
37    pub async fn vend_container(self, bundle_id: &str) -> Result<AfcClient<S>, HouseArrestError> {
38        self.send_command("VendContainer", bundle_id).await
39    }
40
41    pub async fn vend_documents(self, bundle_id: &str) -> Result<AfcClient<S>, HouseArrestError> {
42        self.send_command("VendDocuments", bundle_id).await
43    }
44
45    async fn send_command(
46        mut self,
47        command: &'static str,
48        bundle_id: &str,
49    ) -> Result<AfcClient<S>, HouseArrestError> {
50        self.send_plist(&VendContainerRequest {
51            command,
52            identifier: bundle_id,
53        })
54        .await?;
55
56        let response: VendContainerResponse = self.recv_plist().await?;
57        match response.status.as_deref() {
58            Some("Complete") => Ok(AfcClient::new(self.stream)),
59            Some(status) => Err(HouseArrestError::Service(status.to_string())),
60            None => {
61                if let Some(error) = response.error {
62                    Err(HouseArrestError::Service(error))
63                } else {
64                    Err(HouseArrestError::Protocol(
65                        "unknown house arrest response".into(),
66                    ))
67                }
68            }
69        }
70    }
71
72    async fn send_plist<T: Serialize>(&mut self, value: &T) -> Result<(), HouseArrestError> {
73        let mut buf = Vec::new();
74        plist::to_writer_xml(&mut buf, value)
75            .map_err(|e| HouseArrestError::Plist(e.to_string()))?;
76        let len = buf.len() as u32;
77        self.stream.write_all(&len.to_be_bytes()).await?;
78        self.stream.write_all(&buf).await?;
79        self.stream.flush().await?;
80        Ok(())
81    }
82
83    async fn recv_plist<T>(&mut self) -> Result<T, HouseArrestError>
84    where
85        T: for<'de> Deserialize<'de>,
86    {
87        let mut len_buf = [0u8; 4];
88        self.stream.read_exact(&mut len_buf).await?;
89        let len = u32::from_be_bytes(len_buf) as usize;
90        const MAX_PLIST_SIZE: usize = 4 * 1024 * 1024;
91        if len > MAX_PLIST_SIZE {
92            return Err(HouseArrestError::Protocol(format!(
93                "plist length {len} exceeds max {MAX_PLIST_SIZE}"
94            )));
95        }
96        let mut buf = vec![0u8; len];
97        self.stream.read_exact(&mut buf).await?;
98        plist::from_bytes(&buf).map_err(|e| HouseArrestError::Plist(e.to_string()))
99    }
100}
101
102#[derive(Serialize)]
103#[serde(rename_all = "PascalCase")]
104struct VendContainerRequest<'a> {
105    command: &'static str,
106    identifier: &'a str,
107}
108
109#[derive(Debug, Deserialize)]
110#[serde(rename_all = "PascalCase")]
111struct VendContainerResponse {
112    #[serde(default)]
113    status: Option<String>,
114    #[serde(default)]
115    error: Option<String>,
116}
117
118#[cfg(test)]
119mod tests {
120    use crate::proto::afc::{AfcHeader, AfcOpcode};
121    use zerocopy::{FromBytes, IntoBytes};
122
123    use super::*;
124
125    async fn read_plist_frame<S>(stream: &mut S) -> Vec<u8>
126    where
127        S: AsyncRead + Unpin,
128    {
129        let mut len_buf = [0u8; 4];
130        stream.read_exact(&mut len_buf).await.unwrap();
131        let len = u32::from_be_bytes(len_buf) as usize;
132        let mut buf = vec![0u8; len];
133        stream.read_exact(&mut buf).await.unwrap();
134        buf
135    }
136
137    async fn read_afc_request<S>(stream: &mut S)
138    where
139        S: AsyncRead + Unpin,
140    {
141        let mut hdr_buf = [0u8; AfcHeader::SIZE];
142        stream.read_exact(&mut hdr_buf).await.unwrap();
143        let hdr = AfcHeader::ref_from_bytes(&hdr_buf).unwrap();
144        let header_payload_len = hdr.this_len.get() as usize - AfcHeader::SIZE;
145        let payload_len = hdr.entire_len.get() as usize - hdr.this_len.get() as usize;
146
147        let mut header_payload = vec![0u8; header_payload_len];
148        let mut payload = vec![0u8; payload_len];
149        if header_payload_len > 0 {
150            stream.read_exact(&mut header_payload).await.unwrap();
151        }
152        if payload_len > 0 {
153            stream.read_exact(&mut payload).await.unwrap();
154        }
155        assert_eq!(hdr.operation.get(), AfcOpcode::ReadDir as u64);
156    }
157
158    #[test]
159    fn test_service_name_matches_house_arrest() {
160        assert_eq!(SERVICE_NAME, "com.apple.mobile.house_arrest");
161    }
162
163    #[tokio::test]
164    async fn test_vend_container_returns_afc_client_over_same_stream() {
165        let (client_side, mut server_side) = tokio::io::duplex(4096);
166
167        tokio::spawn(async move {
168            let request = read_plist_frame(&mut server_side).await;
169            let req_value: plist::Value = plist::from_bytes(&request).unwrap();
170            let dict = req_value.into_dictionary().unwrap();
171            assert_eq!(
172                dict.get("Command").and_then(|v| v.as_string()),
173                Some("VendContainer")
174            );
175            assert_eq!(
176                dict.get("Identifier").and_then(|v| v.as_string()),
177                Some("com.example.TestApp")
178            );
179
180            let response = plist::Dictionary::from_iter([(
181                "Status".to_string(),
182                plist::Value::String("Complete".into()),
183            )]);
184            let mut buf = Vec::new();
185            plist::to_writer_xml(&mut buf, &plist::Value::Dictionary(response)).unwrap();
186            let len = buf.len() as u32;
187            server_side.write_all(&len.to_be_bytes()).await.unwrap();
188            server_side.write_all(&buf).await.unwrap();
189
190            read_afc_request(&mut server_side).await;
191
192            let names = b".\0..\0Sandbox\0";
193            let hdr = AfcHeader::new(1, AfcOpcode::ReadDir, 0, names.len());
194            let mut resp = hdr.as_bytes().to_vec();
195            resp.extend_from_slice(names);
196            server_side.write_all(&resp).await.unwrap();
197        });
198
199        let client = HouseArrestClient::new(client_side);
200        let mut afc = client.vend_container("com.example.TestApp").await.unwrap();
201        let entries = afc.list_dir("/").await.unwrap();
202        assert_eq!(entries, vec!["Sandbox"]);
203    }
204
205    #[tokio::test]
206    async fn test_vend_documents_sends_expected_command() {
207        let (client_side, mut server_side) = tokio::io::duplex(4096);
208
209        tokio::spawn(async move {
210            let request = read_plist_frame(&mut server_side).await;
211            let req_value: plist::Value = plist::from_bytes(&request).unwrap();
212            let dict = req_value.into_dictionary().unwrap();
213            assert_eq!(
214                dict.get("Command").and_then(|v| v.as_string()),
215                Some("VendDocuments")
216            );
217            assert_eq!(
218                dict.get("Identifier").and_then(|v| v.as_string()),
219                Some("com.example.DocumentsApp")
220            );
221
222            let response = plist::Dictionary::from_iter([(
223                "Status".to_string(),
224                plist::Value::String("Complete".into()),
225            )]);
226            let mut buf = Vec::new();
227            plist::to_writer_xml(&mut buf, &plist::Value::Dictionary(response)).unwrap();
228            let len = buf.len() as u32;
229            server_side.write_all(&len.to_be_bytes()).await.unwrap();
230            server_side.write_all(&buf).await.unwrap();
231
232            read_afc_request(&mut server_side).await;
233
234            let names = b".\0..\0Documents\0";
235            let hdr = AfcHeader::new(1, AfcOpcode::ReadDir, 0, names.len());
236            let mut resp = hdr.as_bytes().to_vec();
237            resp.extend_from_slice(names);
238            server_side.write_all(&resp).await.unwrap();
239        });
240
241        let client = HouseArrestClient::new(client_side);
242        let mut afc = client
243            .vend_documents("com.example.DocumentsApp")
244            .await
245            .unwrap();
246        let entries = afc.list_dir("/").await.unwrap();
247        assert_eq!(entries, vec!["Documents"]);
248    }
249}