ios_core/services/
mobileactivation.rs1use 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}