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
9service_error!(MobileActivationError);
10
11#[derive(Debug)]
12pub struct MobileActivationClient<S> {
13    stream: S,
14}
15
16impl<S: AsyncRead + AsyncWrite + Unpin> MobileActivationClient<S> {
17    pub fn new(stream: S) -> Self {
18        Self { stream }
19    }
20
21    pub async fn request_session_info(
22        &mut self,
23    ) -> Result<plist::Dictionary, MobileActivationError> {
24        let request = plist::Dictionary::from_iter([(
25            "Command".to_string(),
26            plist::Value::String("CreateTunnel1SessionInfoRequest".into()),
27        )]);
28        send_plist(&mut self.stream, &plist::Value::Dictionary(request)).await?;
29        recv_plist(&mut self.stream).await
30    }
31
32    pub async fn request_activation_info(
33        &mut self,
34        handshake_response: &[u8],
35    ) -> Result<plist::Dictionary, MobileActivationError> {
36        let request = plist::Dictionary::from_iter([
37            (
38                "Command".to_string(),
39                plist::Value::String("CreateActivationInfoRequest".into()),
40            ),
41            (
42                "Value".to_string(),
43                plist::Value::Data(handshake_response.to_vec()),
44            ),
45            (
46                "Options".to_string(),
47                plist::Value::Dictionary(plist::Dictionary::from_iter([(
48                    "BasebandWaitCount".to_string(),
49                    plist::Value::Integer(90i64.into()),
50                )])),
51            ),
52        ]);
53        send_plist(&mut self.stream, &plist::Value::Dictionary(request)).await?;
54        recv_plist(&mut self.stream).await
55    }
56}
57
58async fn send_plist<S: AsyncWrite + Unpin>(
59    stream: &mut S,
60    value: &plist::Value,
61) -> Result<(), MobileActivationError> {
62    let mut buf = Vec::new();
63    plist::to_writer_xml(&mut buf, value)?;
64    stream.write_all(&(buf.len() as u32).to_be_bytes()).await?;
65    stream.write_all(&buf).await?;
66    stream.flush().await?;
67    Ok(())
68}
69
70async fn recv_plist<S: AsyncRead + Unpin>(
71    stream: &mut S,
72) -> Result<plist::Dictionary, MobileActivationError> {
73    let mut len_buf = [0u8; 4];
74    stream.read_exact(&mut len_buf).await?;
75    let len = u32::from_be_bytes(len_buf) as usize;
76    const MAX_PLIST_SIZE: usize = 8 * 1024 * 1024;
77    if len > MAX_PLIST_SIZE {
78        return Err(MobileActivationError::Protocol(format!(
79            "plist length {len} exceeds max {MAX_PLIST_SIZE}"
80        )));
81    }
82    let mut buf = vec![0u8; len];
83    stream.read_exact(&mut buf).await?;
84    Ok(plist::from_bytes(&buf)?)
85}
86
87#[cfg(test)]
88mod tests {
89    use crate::test_util::MockStream;
90
91    use super::*;
92
93    #[tokio::test]
94    async fn request_session_info_sends_tunnel1_command_and_returns_response_dict() {
95        let mut stream =
96            MockStream::with_response(plist::Value::Dictionary(plist::Dictionary::from_iter([
97                (
98                    "Command".to_string(),
99                    plist::Value::String("CreateTunnel1SessionInfoRequest".into()),
100                ),
101                (
102                    "Value".to_string(),
103                    plist::Value::Dictionary(plist::Dictionary::from_iter([(
104                        "HandshakeRequestMessage".to_string(),
105                        plist::Value::Data(vec![1, 2, 3]),
106                    )])),
107                ),
108            ])));
109        let mut client = MobileActivationClient::new(&mut stream);
110
111        let response = client.request_session_info().await.unwrap();
112        assert_eq!(
113            response.get("Command").and_then(plist::Value::as_string),
114            Some("CreateTunnel1SessionInfoRequest")
115        );
116        assert!(response.contains_key("Value"));
117
118        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
119        let payload = &stream.written[4..4 + len];
120        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
121        assert_eq!(
122            dict.get("Command").and_then(plist::Value::as_string),
123            Some("CreateTunnel1SessionInfoRequest")
124        );
125    }
126
127    #[tokio::test]
128    async fn request_activation_info_sends_handshake_value_and_options() {
129        let mut stream =
130            MockStream::with_response(plist::Value::Dictionary(plist::Dictionary::from_iter([
131                (
132                    "Command".to_string(),
133                    plist::Value::String("CreateActivationInfoRequest".into()),
134                ),
135                (
136                    "Value".to_string(),
137                    plist::Value::Dictionary(plist::Dictionary::from_iter([(
138                        "ActivationInfoXML".to_string(),
139                        plist::Value::String("<plist/>".into()),
140                    )])),
141                ),
142            ])));
143        let mut client = MobileActivationClient::new(&mut stream);
144
145        let response = client.request_activation_info(&[9, 8, 7]).await.unwrap();
146        assert_eq!(
147            response.get("Command").and_then(plist::Value::as_string),
148            Some("CreateActivationInfoRequest")
149        );
150
151        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
152        let payload = &stream.written[4..4 + len];
153        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
154        assert_eq!(
155            dict.get("Command").and_then(plist::Value::as_string),
156            Some("CreateActivationInfoRequest")
157        );
158        assert_eq!(
159            dict.get("Value").and_then(plist::Value::as_data),
160            Some(&b"\x09\x08\x07"[..])
161        );
162        let options = dict
163            .get("Options")
164            .and_then(plist::Value::as_dictionary)
165            .expect("Options dictionary");
166        assert_eq!(
167            options
168                .get("BasebandWaitCount")
169                .and_then(plist::Value::as_unsigned_integer),
170            Some(90)
171        );
172    }
173}