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