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