Skip to main content

ios_core/services/
mobileactivation.rs

1//! Minimal mobileactivationd client.
2//!
3//! Current scope: read-only session-info request used by the activation flow.
4
5use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
6
7pub const SERVICE_NAME: &str = "com.apple.mobileactivationd";
8
9#[derive(Debug, thiserror::Error)]
10pub enum MobileActivationError {
11    #[error("IO error: {0}")]
12    Io(#[from] std::io::Error),
13    #[error("plist error: {0}")]
14    Plist(String),
15    #[error("protocol error: {0}")]
16    Protocol(String),
17}
18
19#[derive(Debug)]
20pub struct MobileActivationClient<S> {
21    stream: S,
22}
23
24impl<S: AsyncRead + AsyncWrite + Unpin> MobileActivationClient<S> {
25    pub fn new(stream: S) -> Self {
26        Self { stream }
27    }
28
29    pub async fn request_session_info(
30        &mut self,
31    ) -> Result<plist::Dictionary, MobileActivationError> {
32        let request = plist::Dictionary::from_iter([(
33            "Command".to_string(),
34            plist::Value::String("CreateTunnel1SessionInfoRequest".into()),
35        )]);
36        send_plist(&mut self.stream, &plist::Value::Dictionary(request)).await?;
37        recv_plist(&mut self.stream).await
38    }
39
40    pub async fn request_activation_info(
41        &mut self,
42        handshake_response: &[u8],
43    ) -> Result<plist::Dictionary, MobileActivationError> {
44        let request = plist::Dictionary::from_iter([
45            (
46                "Command".to_string(),
47                plist::Value::String("CreateActivationInfoRequest".into()),
48            ),
49            (
50                "Value".to_string(),
51                plist::Value::Data(handshake_response.to_vec()),
52            ),
53            (
54                "Options".to_string(),
55                plist::Value::Dictionary(plist::Dictionary::from_iter([(
56                    "BasebandWaitCount".to_string(),
57                    plist::Value::Integer(90i64.into()),
58                )])),
59            ),
60        ]);
61        send_plist(&mut self.stream, &plist::Value::Dictionary(request)).await?;
62        recv_plist(&mut self.stream).await
63    }
64}
65
66async fn send_plist<S: AsyncWrite + Unpin>(
67    stream: &mut S,
68    value: &plist::Value,
69) -> Result<(), MobileActivationError> {
70    let mut buf = Vec::new();
71    plist::to_writer_xml(&mut buf, value)
72        .map_err(|e| MobileActivationError::Plist(e.to_string()))?;
73    stream.write_all(&(buf.len() as u32).to_be_bytes()).await?;
74    stream.write_all(&buf).await?;
75    stream.flush().await?;
76    Ok(())
77}
78
79async fn recv_plist<S: AsyncRead + Unpin>(
80    stream: &mut S,
81) -> Result<plist::Dictionary, MobileActivationError> {
82    let mut len_buf = [0u8; 4];
83    stream.read_exact(&mut len_buf).await?;
84    let len = u32::from_be_bytes(len_buf) as usize;
85    const MAX_PLIST_SIZE: usize = 8 * 1024 * 1024;
86    if len > MAX_PLIST_SIZE {
87        return Err(MobileActivationError::Protocol(format!(
88            "plist length {len} exceeds max {MAX_PLIST_SIZE}"
89        )));
90    }
91    let mut buf = vec![0u8; len];
92    stream.read_exact(&mut buf).await?;
93    plist::from_bytes(&buf).map_err(|e| MobileActivationError::Plist(e.to_string()))
94}
95
96#[cfg(test)]
97mod tests {
98    use std::pin::Pin;
99    use std::task::{Context, Poll};
100
101    use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
102
103    use super::*;
104
105    #[derive(Default)]
106    struct MockStream {
107        read_buf: Vec<u8>,
108        written: Vec<u8>,
109        read_pos: usize,
110    }
111
112    impl MockStream {
113        fn with_response(value: plist::Value) -> Self {
114            let mut payload = Vec::new();
115            plist::to_writer_xml(&mut payload, &value).unwrap();
116            let mut read_buf = Vec::new();
117            read_buf.extend_from_slice(&(payload.len() as u32).to_be_bytes());
118            read_buf.extend_from_slice(&payload);
119            Self {
120                read_buf,
121                written: Vec::new(),
122                read_pos: 0,
123            }
124        }
125    }
126
127    impl AsyncRead for MockStream {
128        fn poll_read(
129            mut self: Pin<&mut Self>,
130            _cx: &mut Context<'_>,
131            buf: &mut ReadBuf<'_>,
132        ) -> Poll<std::io::Result<()>> {
133            let remaining = self.read_buf.len().saturating_sub(self.read_pos);
134            if remaining == 0 {
135                return Poll::Ready(Err(std::io::Error::new(
136                    std::io::ErrorKind::UnexpectedEof,
137                    "no more test data",
138                )));
139            }
140            let to_copy = remaining.min(buf.remaining());
141            let start = self.read_pos;
142            let end = start + to_copy;
143            buf.put_slice(&self.read_buf[start..end]);
144            self.read_pos = end;
145            Poll::Ready(Ok(()))
146        }
147    }
148
149    impl AsyncWrite for MockStream {
150        fn poll_write(
151            mut self: Pin<&mut Self>,
152            _cx: &mut Context<'_>,
153            buf: &[u8],
154        ) -> Poll<std::io::Result<usize>> {
155            self.written.extend_from_slice(buf);
156            Poll::Ready(Ok(buf.len()))
157        }
158
159        fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
160            Poll::Ready(Ok(()))
161        }
162
163        fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
164            Poll::Ready(Ok(()))
165        }
166    }
167
168    #[tokio::test]
169    async fn request_session_info_sends_tunnel1_command_and_returns_response_dict() {
170        let mut stream =
171            MockStream::with_response(plist::Value::Dictionary(plist::Dictionary::from_iter([
172                (
173                    "Command".to_string(),
174                    plist::Value::String("CreateTunnel1SessionInfoRequest".into()),
175                ),
176                (
177                    "Value".to_string(),
178                    plist::Value::Dictionary(plist::Dictionary::from_iter([(
179                        "HandshakeRequestMessage".to_string(),
180                        plist::Value::Data(vec![1, 2, 3]),
181                    )])),
182                ),
183            ])));
184        let mut client = MobileActivationClient::new(&mut stream);
185
186        let response = client.request_session_info().await.unwrap();
187        assert_eq!(
188            response.get("Command").and_then(plist::Value::as_string),
189            Some("CreateTunnel1SessionInfoRequest")
190        );
191        assert!(response.contains_key("Value"));
192
193        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
194        let payload = &stream.written[4..4 + len];
195        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
196        assert_eq!(
197            dict.get("Command").and_then(plist::Value::as_string),
198            Some("CreateTunnel1SessionInfoRequest")
199        );
200    }
201
202    #[tokio::test]
203    async fn request_activation_info_sends_handshake_value_and_options() {
204        let mut stream =
205            MockStream::with_response(plist::Value::Dictionary(plist::Dictionary::from_iter([
206                (
207                    "Command".to_string(),
208                    plist::Value::String("CreateActivationInfoRequest".into()),
209                ),
210                (
211                    "Value".to_string(),
212                    plist::Value::Dictionary(plist::Dictionary::from_iter([(
213                        "ActivationInfoXML".to_string(),
214                        plist::Value::String("<plist/>".into()),
215                    )])),
216                ),
217            ])));
218        let mut client = MobileActivationClient::new(&mut stream);
219
220        let response = client.request_activation_info(&[9, 8, 7]).await.unwrap();
221        assert_eq!(
222            response.get("Command").and_then(plist::Value::as_string),
223            Some("CreateActivationInfoRequest")
224        );
225
226        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
227        let payload = &stream.written[4..4 + len];
228        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
229        assert_eq!(
230            dict.get("Command").and_then(plist::Value::as_string),
231            Some("CreateActivationInfoRequest")
232        );
233        assert_eq!(
234            dict.get("Value").and_then(plist::Value::as_data),
235            Some(&b"\x09\x08\x07"[..])
236        );
237        let options = dict
238            .get("Options")
239            .and_then(plist::Value::as_dictionary)
240            .expect("Options dictionary");
241        assert_eq!(
242            options
243                .get("BasebandWaitCount")
244                .and_then(plist::Value::as_unsigned_integer),
245            Some(90)
246        );
247    }
248}