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        .map_err(|e| MobileActivationError::Plist(e.to_string()))?;
65    stream.write_all(&(buf.len() as u32).to_be_bytes()).await?;
66    stream.write_all(&buf).await?;
67    stream.flush().await?;
68    Ok(())
69}
70
71async fn recv_plist<S: AsyncRead + Unpin>(
72    stream: &mut S,
73) -> Result<plist::Dictionary, MobileActivationError> {
74    let mut len_buf = [0u8; 4];
75    stream.read_exact(&mut len_buf).await?;
76    let len = u32::from_be_bytes(len_buf) as usize;
77    const MAX_PLIST_SIZE: usize = 8 * 1024 * 1024;
78    if len > MAX_PLIST_SIZE {
79        return Err(MobileActivationError::Protocol(format!(
80            "plist length {len} exceeds max {MAX_PLIST_SIZE}"
81        )));
82    }
83    let mut buf = vec![0u8; len];
84    stream.read_exact(&mut buf).await?;
85    plist::from_bytes(&buf).map_err(|e| MobileActivationError::Plist(e.to_string()))
86}
87
88#[cfg(test)]
89mod tests {
90    use crate::test_util::MockStream;
91
92    use super::*;
93
94    #[tokio::test]
95    async fn request_session_info_sends_tunnel1_command_and_returns_response_dict() {
96        let mut stream =
97            MockStream::with_response(plist::Value::Dictionary(plist::Dictionary::from_iter([
98                (
99                    "Command".to_string(),
100                    plist::Value::String("CreateTunnel1SessionInfoRequest".into()),
101                ),
102                (
103                    "Value".to_string(),
104                    plist::Value::Dictionary(plist::Dictionary::from_iter([(
105                        "HandshakeRequestMessage".to_string(),
106                        plist::Value::Data(vec![1, 2, 3]),
107                    )])),
108                ),
109            ])));
110        let mut client = MobileActivationClient::new(&mut stream);
111
112        let response = client.request_session_info().await.unwrap();
113        assert_eq!(
114            response.get("Command").and_then(plist::Value::as_string),
115            Some("CreateTunnel1SessionInfoRequest")
116        );
117        assert!(response.contains_key("Value"));
118
119        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
120        let payload = &stream.written[4..4 + len];
121        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
122        assert_eq!(
123            dict.get("Command").and_then(plist::Value::as_string),
124            Some("CreateTunnel1SessionInfoRequest")
125        );
126    }
127
128    #[tokio::test]
129    async fn request_activation_info_sends_handshake_value_and_options() {
130        let mut stream =
131            MockStream::with_response(plist::Value::Dictionary(plist::Dictionary::from_iter([
132                (
133                    "Command".to_string(),
134                    plist::Value::String("CreateActivationInfoRequest".into()),
135                ),
136                (
137                    "Value".to_string(),
138                    plist::Value::Dictionary(plist::Dictionary::from_iter([(
139                        "ActivationInfoXML".to_string(),
140                        plist::Value::String("<plist/>".into()),
141                    )])),
142                ),
143            ])));
144        let mut client = MobileActivationClient::new(&mut stream);
145
146        let response = client.request_activation_info(&[9, 8, 7]).await.unwrap();
147        assert_eq!(
148            response.get("Command").and_then(plist::Value::as_string),
149            Some("CreateActivationInfoRequest")
150        );
151
152        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
153        let payload = &stream.written[4..4 + len];
154        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
155        assert_eq!(
156            dict.get("Command").and_then(plist::Value::as_string),
157            Some("CreateActivationInfoRequest")
158        );
159        assert_eq!(
160            dict.get("Value").and_then(plist::Value::as_data),
161            Some(&b"\x09\x08\x07"[..])
162        );
163        let options = dict
164            .get("Options")
165            .and_then(plist::Value::as_dictionary)
166            .expect("Options dictionary");
167        assert_eq!(
168            options
169                .get("BasebandWaitCount")
170                .and_then(plist::Value::as_unsigned_integer),
171            Some(90)
172        );
173    }
174}