ios_core/services/misagent/
mod.rs1use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
7
8pub const SERVICE_NAME: &str = "com.apple.misagent";
9
10service_error!(
11 MisagentError,
12 #[error("status {0}")]
13 Status(u32),
14);
15
16#[derive(Debug, Clone)]
18pub struct Profile {
19 pub uuid: String,
20 pub name: String,
21 pub app_id: String,
22 pub expiry_date: Option<String>,
23 pub raw_data: Vec<u8>,
24}
25
26pub struct MisagentClient<S> {
28 stream: S,
29}
30
31impl<S: AsyncRead + AsyncWrite + Unpin> MisagentClient<S> {
32 pub fn new(stream: S) -> Self {
33 Self { stream }
34 }
35
36 pub async fn copy_all(&mut self) -> Result<Vec<Vec<u8>>, MisagentError> {
38 self.send_value(plist::Value::Dictionary(plist::Dictionary::from_iter([
39 (
40 "MessageType".to_string(),
41 plist::Value::String("CopyAll".into()),
42 ),
43 (
44 "ProfileType".to_string(),
45 plist::Value::String("Provisioning".into()),
46 ),
47 ])))
48 .await?;
49
50 let data = self.recv_raw().await?;
51 let val: plist::Value =
52 plist::from_bytes(&data).map_err(|e| MisagentError::Plist(e.to_string()))?;
53
54 let status = val
55 .as_dictionary()
56 .and_then(|d| d.get("Status"))
57 .and_then(|v| v.as_unsigned_integer())
58 .unwrap_or(0) as u32;
59
60 if status != 0 {
61 return Err(MisagentError::Status(status));
62 }
63
64 let profiles = val
65 .as_dictionary()
66 .and_then(|d| d.get("Payload"))
67 .and_then(|v| v.as_array())
68 .map(|arr| {
69 arr.iter()
70 .filter_map(|v| v.as_data().map(|d| d.to_vec()))
71 .collect()
72 })
73 .unwrap_or_default();
74
75 Ok(profiles)
76 }
77
78 pub async fn list_profiles(&mut self) -> Result<Vec<Profile>, MisagentError> {
80 let raw_profiles = self.copy_all().await?;
81 raw_profiles
82 .into_iter()
83 .map(|raw_data| decode_profile(&raw_data))
84 .collect()
85 }
86
87 pub async fn install(&mut self, profile_data: &[u8]) -> Result<(), MisagentError> {
89 self.send_value(plist::Value::Dictionary(plist::Dictionary::from_iter([
90 (
91 "MessageType".to_string(),
92 plist::Value::String("Install".into()),
93 ),
94 (
95 "ProfileType".to_string(),
96 plist::Value::String("Provisioning".into()),
97 ),
98 (
99 "Profile".to_string(),
100 plist::Value::Data(profile_data.to_vec()),
101 ),
102 ])))
103 .await?;
104 let data = self.recv_raw().await?;
105 let val: plist::Value =
106 plist::from_bytes(&data).map_err(|e| MisagentError::Plist(e.to_string()))?;
107 let status = val
108 .as_dictionary()
109 .and_then(|d| d.get("Status"))
110 .and_then(|v| v.as_unsigned_integer())
111 .unwrap_or(0) as u32;
112 if status != 0 {
113 return Err(MisagentError::Status(status));
114 }
115 Ok(())
116 }
117
118 pub async fn remove(&mut self, uuid: &str) -> Result<(), MisagentError> {
120 self.send_value(plist::Value::Dictionary(plist::Dictionary::from_iter([
121 (
122 "MessageType".to_string(),
123 plist::Value::String("Remove".into()),
124 ),
125 (
126 "ProfileType".to_string(),
127 plist::Value::String("Provisioning".into()),
128 ),
129 (
130 "ProfileID".to_string(),
131 plist::Value::String(uuid.to_string()),
132 ),
133 ])))
134 .await?;
135 let data = self.recv_raw().await?;
136 let val: plist::Value =
137 plist::from_bytes(&data).map_err(|e| MisagentError::Plist(e.to_string()))?;
138 let status = val
139 .as_dictionary()
140 .and_then(|d| d.get("Status"))
141 .and_then(|v| v.as_unsigned_integer())
142 .unwrap_or(0) as u32;
143 if status != 0 {
144 return Err(MisagentError::Status(status));
145 }
146 Ok(())
147 }
148
149 async fn send_value(&mut self, plist_val: plist::Value) -> Result<(), MisagentError> {
150 let mut buf = Vec::new();
151 plist::to_writer_xml(&mut buf, &plist_val)
152 .map_err(|e| MisagentError::Plist(e.to_string()))?;
153 self.stream
154 .write_all(&(buf.len() as u32).to_be_bytes())
155 .await?;
156 self.stream.write_all(&buf).await?;
157 self.stream.flush().await?;
158 Ok(())
159 }
160
161 async fn recv_raw(&mut self) -> Result<Vec<u8>, MisagentError> {
162 let mut len_buf = [0u8; 4];
163 self.stream.read_exact(&mut len_buf).await?;
164 let len = u32::from_be_bytes(len_buf) as usize;
165 const MAX_PLIST_SIZE: usize = 4 * 1024 * 1024;
166 if len > MAX_PLIST_SIZE {
167 return Err(MisagentError::Io(std::io::Error::new(
168 std::io::ErrorKind::InvalidData,
169 format!("plist length {len} exceeds maximum of {MAX_PLIST_SIZE}"),
170 )));
171 }
172 let mut buf = vec![0u8; len];
173 self.stream.read_exact(&mut buf).await?;
174 Ok(buf)
175 }
176}
177
178fn decode_profile(raw_data: &[u8]) -> Result<Profile, MisagentError> {
179 let plist_bytes = embedded_plist_bytes(raw_data)?;
180 let value: plist::Value =
181 plist::from_bytes(plist_bytes).map_err(|e| MisagentError::Plist(e.to_string()))?;
182 let dict = value.into_dictionary().ok_or_else(|| {
183 MisagentError::Protocol("provisioning profile payload was not a dictionary".into())
184 })?;
185
186 let uuid = required_string(&dict, "UUID")?;
187 let name = dict
188 .get("Name")
189 .and_then(plist::Value::as_string)
190 .unwrap_or(&uuid)
191 .to_string();
192 let app_id = dict
193 .get("AppIDName")
194 .and_then(plist::Value::as_string)
195 .or_else(|| {
196 dict.get("ApplicationIdentifierPrefix")
197 .and_then(plist::Value::as_array)
198 .and_then(|arr| arr.first())
199 .and_then(plist::Value::as_string)
200 })
201 .unwrap_or("")
202 .to_string();
203 let expiry_date = dict.get("ExpirationDate").map(plist_value_to_string);
204
205 Ok(Profile {
206 uuid,
207 name,
208 app_id,
209 expiry_date,
210 raw_data: raw_data.to_vec(),
211 })
212}
213
214fn embedded_plist_bytes(raw_data: &[u8]) -> Result<&[u8], MisagentError> {
215 let start = find_bytes(raw_data, b"<?xml").or_else(|| find_bytes(raw_data, b"<plist"));
216 let end = find_bytes(raw_data, b"</plist>");
217 match (start, end) {
218 (Some(start), Some(end)) if end >= start => Ok(&raw_data[start..end + b"</plist>".len()]),
219 _ => Err(MisagentError::Protocol(
220 "could not locate embedded plist in provisioning profile".into(),
221 )),
222 }
223}
224
225fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
226 haystack
227 .windows(needle.len())
228 .position(|window| window == needle)
229}
230
231fn required_string(dict: &plist::Dictionary, key: &str) -> Result<String, MisagentError> {
232 dict.get(key)
233 .and_then(plist::Value::as_string)
234 .map(ToOwned::to_owned)
235 .ok_or_else(|| MisagentError::Protocol(format!("missing provisioning profile key {key}")))
236}
237
238fn plist_value_to_string(value: &plist::Value) -> String {
239 match value {
240 plist::Value::String(s) => s.clone(),
241 plist::Value::Date(d) => d.to_xml_format(),
242 other => format!("{other:?}"),
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use crate::test_util::MockStream;
249
250 use super::*;
251
252 #[test]
253 fn decode_profile_extracts_basic_metadata_from_embedded_plist() {
254 let xml = br#"garbage<?xml version="1.0" encoding="UTF-8"?>
255<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
256<plist version="1.0"><dict>
257<key>UUID</key><string>ABC-123</string>
258<key>Name</key><string>Example Dev Profile</string>
259<key>AppIDName</key><string>Example App</string>
260<key>ExpirationDate</key><date>2026-04-08T00:00:00Z</date>
261</dict></plist>trailer"#;
262
263 let profile = decode_profile(xml).unwrap();
264 assert_eq!(profile.uuid, "ABC-123");
265 assert_eq!(profile.name, "Example Dev Profile");
266 assert_eq!(profile.app_id, "Example App");
267 assert_eq!(profile.expiry_date.as_deref(), Some("2026-04-08T00:00:00Z"));
268 }
269
270 #[test]
271 fn decode_profile_errors_without_embedded_plist() {
272 let err = decode_profile(b"not-a-profile").unwrap_err();
273 assert!(
274 matches!(err, MisagentError::Protocol(message) if message.contains("embedded plist"))
275 );
276 }
277
278 #[tokio::test]
279 async fn copy_all_uses_copy_all_message_type() {
280 let response = plist::Value::Dictionary(plist::Dictionary::from_iter([
281 ("Status".to_string(), plist::Value::Integer(0.into())),
282 ("Payload".to_string(), plist::Value::Array(Vec::new())),
283 ]));
284 let mut stream = MockStream::with_response(response);
285 let mut client = MisagentClient::new(&mut stream);
286
287 let _ = client.copy_all().await.unwrap();
288
289 let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
290 let payload = &stream.written[4..4 + len];
291 let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
292 assert_eq!(
293 dict.get("MessageType").and_then(plist::Value::as_string),
294 Some("CopyAll")
295 );
296 }
297
298 #[tokio::test]
299 async fn install_uses_profile_field() {
300 let response = plist::Value::Dictionary(plist::Dictionary::from_iter([(
301 "Status".to_string(),
302 plist::Value::Integer(0.into()),
303 )]));
304 let mut stream = MockStream::with_response(response);
305 let mut client = MisagentClient::new(&mut stream);
306
307 client.install(b"PROFILE-DATA").await.unwrap();
308
309 let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
310 let payload = &stream.written[4..4 + len];
311 let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
312 assert_eq!(
313 dict.get("MessageType").and_then(plist::Value::as_string),
314 Some("Install")
315 );
316 assert_eq!(
317 dict.get("Profile").and_then(plist::Value::as_data),
318 Some(&b"PROFILE-DATA"[..])
319 );
320 }
321}