1#[cfg(feature = "supervised-pair")]
6use openssl::pkcs12::Pkcs12;
7#[cfg(feature = "supervised-pair")]
8use openssl::pkcs7::{Pkcs7, Pkcs7Flags};
9#[cfg(feature = "supervised-pair")]
10use openssl::stack::Stack;
11use serde::Serialize;
12use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
13
14pub const SERVICE_NAME: &str = "com.apple.mobile.MCInstall";
15
16service_error!(
17 McInstallError,
18 #[error("crypto error: {0}")]
19 Crypto(String),
20);
21
22#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
23pub struct ProfileInfo {
24 pub identifier: String,
25 pub display_name: String,
26 pub description: Option<String>,
27 pub is_active: bool,
28 pub removal_disallowed: Option<bool>,
29 pub status: Option<String>,
30 pub uuid: Option<String>,
31 pub version: Option<u64>,
32}
33
34#[derive(Debug)]
35pub struct McInstallClient<S> {
36 stream: S,
37}
38
39impl<S: AsyncRead + AsyncWrite + Unpin> McInstallClient<S> {
40 pub fn new(stream: S) -> Self {
41 Self { stream }
42 }
43
44 pub async fn list_profiles(&mut self) -> Result<Vec<ProfileInfo>, McInstallError> {
45 let response = self.get_profile_list_raw().await?;
46 parse_profile_list(response)
47 }
48
49 pub async fn get_profile_list_raw(&mut self) -> Result<plist::Value, McInstallError> {
50 self.send_plist(&Request {
51 request_type: "GetProfileList",
52 })
53 .await?;
54
55 self.recv_plist().await
56 }
57
58 pub async fn get_cloud_configuration(&mut self) -> Result<plist::Dictionary, McInstallError> {
59 self.send_plist(&Request {
60 request_type: "GetCloudConfiguration",
61 })
62 .await?;
63
64 let response: plist::Value = self.recv_plist().await?;
65 parse_cloud_configuration(response)
66 }
67
68 pub async fn get_stored_profile_raw(
69 &mut self,
70 purpose: &str,
71 ) -> Result<plist::Value, McInstallError> {
72 let request = plist::Dictionary::from_iter([
73 (
74 "RequestType".to_string(),
75 plist::Value::String("GetStoredProfile".into()),
76 ),
77 (
78 "Purpose".to_string(),
79 plist::Value::String(purpose.to_string()),
80 ),
81 ]);
82 send_plist(&mut self.stream, &plist::Value::Dictionary(request)).await?;
83 self.recv_plist().await
84 }
85
86 pub async fn flush(&mut self) -> Result<(), McInstallError> {
87 let request = plist::Dictionary::from_iter([(
88 "RequestType".to_string(),
89 plist::Value::String("Flush".into()),
90 )]);
91 send_request(&mut self.stream, request).await
92 }
93
94 pub async fn hello_host_identifier(&mut self) -> Result<(), McInstallError> {
95 let request = plist::Dictionary::from_iter([(
96 "RequestType".to_string(),
97 plist::Value::String("HelloHostIdentifier".into()),
98 )]);
99 send_request(&mut self.stream, request).await
100 }
101
102 pub async fn set_cloud_configuration(
103 &mut self,
104 cloud_configuration: plist::Dictionary,
105 ) -> Result<(), McInstallError> {
106 let request = plist::Dictionary::from_iter([
107 (
108 "RequestType".to_string(),
109 plist::Value::String("SetCloudConfiguration".into()),
110 ),
111 (
112 "CloudConfiguration".to_string(),
113 plist::Value::Dictionary(cloud_configuration),
114 ),
115 ]);
116 send_request(&mut self.stream, request).await
117 }
118
119 pub async fn install_profile(&mut self, payload: &[u8]) -> Result<(), McInstallError> {
120 let request = plist::Dictionary::from_iter([
121 (
122 "RequestType".to_string(),
123 plist::Value::String("InstallProfile".into()),
124 ),
125 ("Payload".to_string(), plist::Value::Data(payload.to_vec())),
126 ]);
127 send_request(&mut self.stream, request).await
128 }
129
130 pub async fn install_profile_silent(
131 &mut self,
132 payload: &[u8],
133 p12_bytes: &[u8],
134 password: &str,
135 ) -> Result<(), McInstallError> {
136 #[cfg(not(feature = "supervised-pair"))]
137 {
138 let _ = (payload, p12_bytes, password);
139 return Err(McInstallError::Crypto(
140 "silent profile installation requires ios-core feature 'supervised-pair'".into(),
141 ));
142 }
143
144 #[cfg(feature = "supervised-pair")]
145 {
146 self.escalate(p12_bytes, password).await?;
147 let request = plist::Dictionary::from_iter([
148 (
149 "RequestType".to_string(),
150 plist::Value::String("InstallProfileSilent".into()),
151 ),
152 ("Payload".to_string(), plist::Value::Data(payload.to_vec())),
153 ]);
154 send_request(&mut self.stream, request).await
155 }
156 }
157
158 pub async fn remove_profile(&mut self, identifier: &str) -> Result<(), McInstallError> {
159 let profile_identifier = match self.get_profile_list_raw().await {
160 Ok(value) => build_remove_profile_identifier(&value, identifier)
161 .map_err(|err| McInstallError::Protocol(err.to_string()))?
162 .unwrap_or_else(|| plist::Value::String(identifier.to_string())),
163 Err(_) => plist::Value::String(identifier.to_string()),
164 };
165 let request = plist::Dictionary::from_iter([
166 (
167 "RequestType".to_string(),
168 plist::Value::String("RemoveProfile".into()),
169 ),
170 ("ProfileIdentifier".to_string(), profile_identifier),
171 ]);
172 send_request(&mut self.stream, request).await
173 }
174
175 pub async fn erase_device(
176 &mut self,
177 preserve_data_plan: bool,
178 disallow_proximity_setup: bool,
179 ) -> Result<(), McInstallError> {
180 let request = plist::Dictionary::from_iter([
181 (
182 "RequestType".to_string(),
183 plist::Value::String("EraseDevice".into()),
184 ),
185 (
186 "PreserveDataPlan".to_string(),
187 plist::Value::Boolean(preserve_data_plan),
188 ),
189 (
190 "DisallowProximitySetup".to_string(),
191 plist::Value::Boolean(disallow_proximity_setup),
192 ),
193 ]);
194 send_request_allow_eof(&mut self.stream, request).await
195 }
196
197 pub async fn escalate_unsupervised(&mut self) -> Result<(), McInstallError> {
198 let request = plist::Dictionary::from_iter([
199 (
200 "RequestType".to_string(),
201 plist::Value::String("Escalate".into()),
202 ),
203 (
204 "SupervisorCertificate".to_string(),
205 plist::Value::Data(vec![0]),
206 ),
207 ]);
208 send_request(&mut self.stream, request).await
209 }
210
211 #[cfg(feature = "supervised-pair")]
212 async fn escalate(&mut self, p12_bytes: &[u8], password: &str) -> Result<(), McInstallError> {
213 let pkcs12 =
214 Pkcs12::from_der(p12_bytes).map_err(|err| McInstallError::Crypto(err.to_string()))?;
215 let parsed = pkcs12
216 .parse2(password)
217 .map_err(|err| McInstallError::Crypto(err.to_string()))?;
218 let cert = parsed
219 .cert
220 .ok_or_else(|| McInstallError::Crypto("P12 missing certificate".into()))?;
221 let pkey = parsed
222 .pkey
223 .ok_or_else(|| McInstallError::Crypto("P12 missing private key".into()))?;
224
225 let request = plist::Dictionary::from_iter([
226 (
227 "RequestType".to_string(),
228 plist::Value::String("Escalate".into()),
229 ),
230 (
231 "SupervisorCertificate".to_string(),
232 plist::Value::Data(
233 cert.to_der()
234 .map_err(|err| McInstallError::Crypto(err.to_string()))?,
235 ),
236 ),
237 ]);
238 send_plist(&mut self.stream, &plist::Value::Dictionary(request)).await?;
239 let response = recv_plist(&mut self.stream).await?;
240 ensure_acknowledged(&response)?;
241 let challenge = response
242 .get("Challenge")
243 .and_then(plist::Value::as_data)
244 .ok_or_else(|| {
245 McInstallError::Protocol("MCInstall escalate response missing Challenge".into())
246 })?;
247 let certs = Stack::new().map_err(|err| McInstallError::Crypto(err.to_string()))?;
248 let signed_request = Pkcs7::sign(&cert, &pkey, &certs, challenge, Pkcs7Flags::BINARY)
249 .and_then(|pkcs7| pkcs7.to_der())
250 .map_err(|err| McInstallError::Crypto(err.to_string()))?;
251
252 let response_request = plist::Dictionary::from_iter([
253 (
254 "RequestType".to_string(),
255 plist::Value::String("EscalateResponse".into()),
256 ),
257 (
258 "SignedRequest".to_string(),
259 plist::Value::Data(signed_request),
260 ),
261 ]);
262 send_request(&mut self.stream, response_request).await?;
263
264 let proceed_request = plist::Dictionary::from_iter([(
265 "RequestType".to_string(),
266 plist::Value::String("ProceedWithKeybagMigration".into()),
267 )]);
268 send_request(&mut self.stream, proceed_request).await
269 }
270
271 async fn send_plist<T: Serialize>(&mut self, value: &T) -> Result<(), McInstallError> {
272 let mut buf = Vec::new();
273 plist::to_writer_xml(&mut buf, value)?;
274 self.stream
275 .write_all(&(buf.len() as u32).to_be_bytes())
276 .await?;
277 self.stream.write_all(&buf).await?;
278 self.stream.flush().await?;
279 Ok(())
280 }
281
282 async fn recv_plist<T>(&mut self) -> Result<T, McInstallError>
283 where
284 T: for<'de> serde::Deserialize<'de>,
285 {
286 let mut len_buf = [0u8; 4];
287 self.stream.read_exact(&mut len_buf).await?;
288 let len = u32::from_be_bytes(len_buf) as usize;
289 const MAX_PLIST_SIZE: usize = 8 * 1024 * 1024;
290 if len > MAX_PLIST_SIZE {
291 return Err(McInstallError::Protocol(format!(
292 "plist length {len} exceeds max {MAX_PLIST_SIZE}"
293 )));
294 }
295 let mut buf = vec![0u8; len];
296 self.stream.read_exact(&mut buf).await?;
297 Ok(plist::from_bytes(&buf)?)
298 }
299}
300
301#[derive(Serialize)]
302#[serde(rename_all = "PascalCase")]
303struct Request {
304 request_type: &'static str,
305}
306
307fn parse_profile_list(value: plist::Value) -> Result<Vec<ProfileInfo>, McInstallError> {
308 let dict = value.into_dictionary().ok_or_else(|| {
309 McInstallError::Protocol("MCInstall response was not a dictionary".into())
310 })?;
311
312 let ordered = dict
313 .get("OrderedIdentifiers")
314 .and_then(plist::Value::as_array)
315 .ok_or_else(|| {
316 McInstallError::Protocol("MCInstall response missing OrderedIdentifiers".into())
317 })?;
318 let manifest_root = dict
319 .get("ProfileManifest")
320 .and_then(plist::Value::as_dictionary)
321 .ok_or_else(|| {
322 McInstallError::Protocol("MCInstall response missing ProfileManifest".into())
323 })?;
324 let metadata_root = dict
325 .get("ProfileMetadata")
326 .and_then(plist::Value::as_dictionary)
327 .ok_or_else(|| {
328 McInstallError::Protocol("MCInstall response missing ProfileMetadata".into())
329 })?;
330 let status = dict
331 .get("Status")
332 .and_then(plist::Value::as_string)
333 .map(ToOwned::to_owned);
334
335 let mut profiles = Vec::with_capacity(ordered.len());
336 for identifier in ordered {
337 let identifier = identifier.as_string().ok_or_else(|| {
338 McInstallError::Protocol("OrderedIdentifiers entry was not a string".into())
339 })?;
340 let manifest = manifest_root
341 .get(identifier)
342 .and_then(plist::Value::as_dictionary)
343 .ok_or_else(|| {
344 McInstallError::Protocol(format!("ProfileManifest missing entry for {identifier}"))
345 })?;
346 let metadata = metadata_root
347 .get(identifier)
348 .and_then(plist::Value::as_dictionary)
349 .ok_or_else(|| {
350 McInstallError::Protocol(format!("ProfileMetadata missing entry for {identifier}"))
351 })?;
352
353 profiles.push(ProfileInfo {
354 identifier: identifier.to_string(),
355 display_name: metadata
356 .get("PayloadDisplayName")
357 .and_then(plist::Value::as_string)
358 .unwrap_or(identifier)
359 .to_string(),
360 description: metadata
361 .get("PayloadDescription")
362 .and_then(plist::Value::as_string)
363 .map(ToOwned::to_owned),
364 is_active: manifest
365 .get("IsActive")
366 .and_then(plist::Value::as_boolean)
367 .unwrap_or(false),
368 removal_disallowed: metadata
369 .get("PayloadRemovalDisallowed")
370 .and_then(plist::Value::as_boolean),
371 status: status.clone(),
372 uuid: metadata
373 .get("PayloadUUID")
374 .and_then(plist::Value::as_string)
375 .map(ToOwned::to_owned),
376 version: metadata
377 .get("PayloadVersion")
378 .and_then(plist::Value::as_unsigned_integer),
379 });
380 }
381 Ok(profiles)
382}
383
384fn parse_cloud_configuration(value: plist::Value) -> Result<plist::Dictionary, McInstallError> {
385 value.into_dictionary().ok_or_else(|| {
386 McInstallError::Protocol("MCInstall cloud configuration was not a dictionary".into())
387 })
388}
389
390fn build_remove_profile_identifier(
391 value: &plist::Value,
392 identifier: &str,
393) -> Result<Option<plist::Value>, plist::Error> {
394 let metadata = match value
395 .as_dictionary()
396 .and_then(|dict| dict.get("ProfileMetadata"))
397 .and_then(plist::Value::as_dictionary)
398 .and_then(|metadata| metadata.get(identifier))
399 .and_then(plist::Value::as_dictionary)
400 {
401 Some(metadata) => metadata,
402 None => return Ok(None),
403 };
404 let payload_uuid = match metadata
405 .get("PayloadUUID")
406 .and_then(plist::Value::as_string)
407 {
408 Some(uuid) => uuid,
409 None => return Ok(None),
410 };
411 let payload_version = match metadata
412 .get("PayloadVersion")
413 .and_then(plist::Value::as_unsigned_integer)
414 {
415 Some(version) => version,
416 None => return Ok(None),
417 };
418
419 let profile_identifier = plist::Value::Dictionary(plist::Dictionary::from_iter([
420 (
421 "PayloadType".to_string(),
422 plist::Value::String("Configuration".into()),
423 ),
424 (
425 "PayloadIdentifier".to_string(),
426 plist::Value::String(identifier.to_string()),
427 ),
428 (
429 "PayloadUUID".to_string(),
430 plist::Value::String(payload_uuid.to_string()),
431 ),
432 (
433 "PayloadVersion".to_string(),
434 plist::Value::Integer((payload_version as i64).into()),
435 ),
436 ]));
437 let mut buf = Vec::new();
438 plist::to_writer_xml(&mut buf, &profile_identifier)?;
439 Ok(Some(plist::Value::Data(buf)))
440}
441
442async fn send_request<S: AsyncRead + AsyncWrite + Unpin>(
443 stream: &mut S,
444 request: plist::Dictionary,
445) -> Result<(), McInstallError> {
446 send_plist(stream, &plist::Value::Dictionary(request)).await?;
447 let response = recv_plist(stream).await?;
448 ensure_acknowledged(&response)
449}
450
451async fn send_request_allow_eof<S: AsyncRead + AsyncWrite + Unpin>(
452 stream: &mut S,
453 request: plist::Dictionary,
454) -> Result<(), McInstallError> {
455 send_plist(stream, &plist::Value::Dictionary(request)).await?;
456 match recv_plist(stream).await {
457 Ok(response) => ensure_acknowledged(&response),
458 Err(McInstallError::Io(err)) if err.kind() == std::io::ErrorKind::UnexpectedEof => Ok(()),
459 Err(err) => Err(err),
460 }
461}
462
463fn ensure_acknowledged(response: &plist::Dictionary) -> Result<(), McInstallError> {
464 let status = response
465 .get("Status")
466 .and_then(plist::Value::as_string)
467 .ok_or_else(|| McInstallError::Protocol("MCInstall response missing Status".into()))?;
468 if status != "Acknowledged" {
469 let detail = response
470 .get("Error")
471 .and_then(plist::Value::as_string)
472 .map(ToOwned::to_owned)
473 .or_else(|| response.get("ErrorChain").map(|value| format!("{value:?}")))
474 .unwrap_or_else(|| status.to_string());
475 return Err(McInstallError::Protocol(format!(
476 "MCInstall request not acknowledged: {detail}"
477 )));
478 }
479 Ok(())
480}
481
482async fn send_plist<S: AsyncWrite + Unpin>(
483 stream: &mut S,
484 value: &plist::Value,
485) -> Result<(), McInstallError> {
486 let mut buf = Vec::new();
487 plist::to_writer_xml(&mut buf, value)?;
488 stream.write_all(&(buf.len() as u32).to_be_bytes()).await?;
489 stream.write_all(&buf).await?;
490 stream.flush().await?;
491 Ok(())
492}
493
494async fn recv_plist<S: AsyncRead + Unpin>(
495 stream: &mut S,
496) -> Result<plist::Dictionary, McInstallError> {
497 let mut len_buf = [0u8; 4];
498 stream.read_exact(&mut len_buf).await?;
499 let len = u32::from_be_bytes(len_buf) as usize;
500 const MAX_PLIST_SIZE: usize = 8 * 1024 * 1024;
501 if len > MAX_PLIST_SIZE {
502 return Err(McInstallError::Protocol(format!(
503 "plist length {len} exceeds max {MAX_PLIST_SIZE}"
504 )));
505 }
506 let mut buf = vec![0u8; len];
507 stream.read_exact(&mut buf).await?;
508 Ok(plist::from_bytes(&buf)?)
509}
510
511#[cfg(test)]
512mod tests {
513 use crate::test_util::MockStream;
514
515 use super::*;
516
517 #[test]
518 fn parses_ordered_profile_list() {
519 let response = plist::Value::Dictionary(plist::Dictionary::from_iter([
520 (
521 "OrderedIdentifiers".to_string(),
522 plist::Value::Array(vec![plist::Value::String("com.example.profile".into())]),
523 ),
524 (
525 "ProfileManifest".to_string(),
526 plist::Value::Dictionary(plist::Dictionary::from_iter([(
527 "com.example.profile".to_string(),
528 plist::Value::Dictionary(plist::Dictionary::from_iter([
529 (
530 "Description".to_string(),
531 plist::Value::String("Example".into()),
532 ),
533 ("IsActive".to_string(), plist::Value::Boolean(true)),
534 ])),
535 )])),
536 ),
537 (
538 "ProfileMetadata".to_string(),
539 plist::Value::Dictionary(plist::Dictionary::from_iter([(
540 "com.example.profile".to_string(),
541 plist::Value::Dictionary(plist::Dictionary::from_iter([
542 (
543 "PayloadDisplayName".to_string(),
544 plist::Value::String("Example Profile".into()),
545 ),
546 (
547 "PayloadDescription".to_string(),
548 plist::Value::String("Example description".into()),
549 ),
550 (
551 "PayloadRemovalDisallowed".to_string(),
552 plist::Value::Boolean(false),
553 ),
554 (
555 "PayloadUUID".to_string(),
556 plist::Value::String("1234".into()),
557 ),
558 (
559 "PayloadVersion".to_string(),
560 plist::Value::Integer(1i64.into()),
561 ),
562 ])),
563 )])),
564 ),
565 (
566 "Status".to_string(),
567 plist::Value::String("Acknowledged".into()),
568 ),
569 ]));
570
571 let profiles = parse_profile_list(response).unwrap();
572 assert_eq!(profiles.len(), 1);
573 let profile = &profiles[0];
574 assert_eq!(profile.identifier, "com.example.profile");
575 assert_eq!(profile.display_name, "Example Profile");
576 assert_eq!(profile.description.as_deref(), Some("Example description"));
577 assert!(profile.is_active);
578 assert_eq!(profile.removal_disallowed, Some(false));
579 assert_eq!(profile.status.as_deref(), Some("Acknowledged"));
580 assert_eq!(profile.uuid.as_deref(), Some("1234"));
581 assert_eq!(profile.version, Some(1));
582 }
583
584 #[test]
585 fn cloud_configuration_requires_dictionary_response() {
586 let err = parse_cloud_configuration(plist::Value::Array(Vec::new()));
587 assert!(matches!(
588 err,
589 Err(McInstallError::Protocol(message)) if message.contains("cloud configuration")
590 ));
591 }
592
593 #[test]
594 fn parses_cloud_configuration_dictionary() {
595 let dict = plist::Dictionary::from_iter([(
596 "IsSupervised".to_string(),
597 plist::Value::Boolean(true),
598 )]);
599 let parsed = parse_cloud_configuration(plist::Value::Dictionary(dict.clone())).unwrap();
600 assert_eq!(
601 parsed
602 .get("IsSupervised")
603 .and_then(plist::Value::as_boolean),
604 Some(true)
605 );
606 }
607
608 #[tokio::test]
609 async fn install_profile_sends_payload_request() {
610 let response = plist::Value::Dictionary(plist::Dictionary::from_iter([(
611 "Status".to_string(),
612 plist::Value::String("Acknowledged".into()),
613 )]));
614 let mut stream = MockStream::with_response(response);
615 let mut client = McInstallClient::new(&mut stream);
616
617 client.install_profile(b"<plist/>").await.unwrap();
618
619 let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
620 let payload = &stream.written[4..4 + len];
621 let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
622 assert_eq!(
623 dict.get("RequestType").and_then(plist::Value::as_string),
624 Some("InstallProfile")
625 );
626 assert_eq!(
627 dict.get("Payload").and_then(plist::Value::as_data),
628 Some(&b"<plist/>"[..])
629 );
630 }
631
632 #[tokio::test]
633 async fn remove_profile_sends_identifier_request() {
634 let profile_list = plist::Value::Dictionary(plist::Dictionary::from_iter([
635 (
636 "OrderedIdentifiers".to_string(),
637 plist::Value::Array(Vec::new()),
638 ),
639 (
640 "ProfileManifest".to_string(),
641 plist::Value::Dictionary(plist::Dictionary::new()),
642 ),
643 (
644 "ProfileMetadata".to_string(),
645 plist::Value::Dictionary(plist::Dictionary::new()),
646 ),
647 (
648 "Status".to_string(),
649 plist::Value::String("Acknowledged".into()),
650 ),
651 ]));
652 let remove_response = plist::Value::Dictionary(plist::Dictionary::from_iter([(
653 "Status".to_string(),
654 plist::Value::String("Acknowledged".into()),
655 )]));
656 let mut stream = MockStream::with_responses(vec![profile_list, remove_response]);
657 let mut client = McInstallClient::new(&mut stream);
658
659 client.remove_profile("com.example.profile").await.unwrap();
660
661 let first_len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
662 let offset = 4 + first_len;
663 let len =
664 u32::from_be_bytes(stream.written[offset..offset + 4].try_into().unwrap()) as usize;
665 let payload = &stream.written[offset + 4..offset + 4 + len];
666 let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
667 assert_eq!(
668 dict.get("RequestType").and_then(plist::Value::as_string),
669 Some("RemoveProfile")
670 );
671 assert_eq!(
672 dict.get("ProfileIdentifier")
673 .and_then(plist::Value::as_string),
674 Some("com.example.profile")
675 );
676 }
677
678 #[tokio::test]
679 async fn remove_profile_uses_metadata_backed_identifier_when_available() {
680 let profile_list = plist::Value::Dictionary(plist::Dictionary::from_iter([
681 (
682 "OrderedIdentifiers".to_string(),
683 plist::Value::Array(vec![plist::Value::String("com.example.profile".into())]),
684 ),
685 (
686 "ProfileManifest".to_string(),
687 plist::Value::Dictionary(plist::Dictionary::from_iter([(
688 "com.example.profile".to_string(),
689 plist::Value::Dictionary(plist::Dictionary::from_iter([(
690 "IsActive".to_string(),
691 plist::Value::Boolean(true),
692 )])),
693 )])),
694 ),
695 (
696 "ProfileMetadata".to_string(),
697 plist::Value::Dictionary(plist::Dictionary::from_iter([(
698 "com.example.profile".to_string(),
699 plist::Value::Dictionary(plist::Dictionary::from_iter([
700 (
701 "PayloadUUID".to_string(),
702 plist::Value::String("1234-5678".into()),
703 ),
704 (
705 "PayloadVersion".to_string(),
706 plist::Value::Integer(7.into()),
707 ),
708 ])),
709 )])),
710 ),
711 (
712 "Status".to_string(),
713 plist::Value::String("Acknowledged".into()),
714 ),
715 ]));
716 let remove_response = plist::Value::Dictionary(plist::Dictionary::from_iter([(
717 "Status".to_string(),
718 plist::Value::String("Acknowledged".into()),
719 )]));
720 let mut stream = MockStream::with_responses(vec![profile_list, remove_response]);
721 let mut client = McInstallClient::new(&mut stream);
722
723 client.remove_profile("com.example.profile").await.unwrap();
724
725 let first_len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
726 let second_offset = 4 + first_len;
727 let second_len = u32::from_be_bytes(
728 stream.written[second_offset..second_offset + 4]
729 .try_into()
730 .unwrap(),
731 ) as usize;
732 let second_payload = &stream.written[second_offset + 4..second_offset + 4 + second_len];
733 let second_request: plist::Dictionary = plist::from_bytes(second_payload).unwrap();
734 let profile_identifier = second_request
735 .get("ProfileIdentifier")
736 .and_then(plist::Value::as_data)
737 .expect("metadata-backed profile identifier should be plist data");
738 let identifier_plist = plist::Value::from_reader(std::io::Cursor::new(profile_identifier))
739 .unwrap()
740 .into_dictionary()
741 .unwrap();
742 assert_eq!(
743 identifier_plist
744 .get("PayloadIdentifier")
745 .and_then(plist::Value::as_string),
746 Some("com.example.profile")
747 );
748 assert_eq!(
749 identifier_plist
750 .get("PayloadUUID")
751 .and_then(plist::Value::as_string),
752 Some("1234-5678")
753 );
754 assert_eq!(
755 identifier_plist
756 .get("PayloadVersion")
757 .and_then(plist::Value::as_unsigned_integer),
758 Some(7)
759 );
760 assert_eq!(
761 identifier_plist
762 .get("PayloadType")
763 .and_then(plist::Value::as_string),
764 Some("Configuration")
765 );
766 }
767
768 #[tokio::test]
769 async fn get_profile_list_raw_preserves_unparsed_fields() {
770 let response = plist::Value::Dictionary(plist::Dictionary::from_iter([
771 (
772 "OrderedIdentifiers".to_string(),
773 plist::Value::Array(vec![plist::Value::String("com.example.profile".into())]),
774 ),
775 (
776 "ProfileManifest".to_string(),
777 plist::Value::Dictionary(plist::Dictionary::from_iter([(
778 "com.example.profile".to_string(),
779 plist::Value::Dictionary(plist::Dictionary::from_iter([(
780 "IsActive".to_string(),
781 plist::Value::Boolean(true),
782 )])),
783 )])),
784 ),
785 (
786 "ProfileMetadata".to_string(),
787 plist::Value::Dictionary(plist::Dictionary::from_iter([(
788 "com.example.profile".to_string(),
789 plist::Value::Dictionary(plist::Dictionary::from_iter([(
790 "PayloadDisplayName".to_string(),
791 plist::Value::String("Example".into()),
792 )])),
793 )])),
794 ),
795 (
796 "Unhandled".to_string(),
797 plist::Value::String("preserved".into()),
798 ),
799 (
800 "Status".to_string(),
801 plist::Value::String("Acknowledged".into()),
802 ),
803 ]));
804 let mut stream = MockStream::with_response(response);
805 let mut client = McInstallClient::new(&mut stream);
806
807 let raw = client.get_profile_list_raw().await.unwrap();
808 let dict = raw.as_dictionary().unwrap();
809 assert_eq!(dict["Unhandled"].as_string(), Some("preserved"));
810 }
811
812 #[tokio::test]
813 async fn get_stored_profile_raw_includes_requested_purpose() {
814 let response = plist::Value::Dictionary(plist::Dictionary::from_iter([
815 (
816 "Status".to_string(),
817 plist::Value::String("Acknowledged".into()),
818 ),
819 (
820 "ProfileData".to_string(),
821 plist::Value::Data(b"<plist/>".to_vec()),
822 ),
823 ]));
824 let mut stream = MockStream::with_response(response);
825 let mut client = McInstallClient::new(&mut stream);
826
827 let raw = client
828 .get_stored_profile_raw("PostSetupInstallation")
829 .await
830 .unwrap();
831 let dict = raw.as_dictionary().unwrap();
832 assert_eq!(dict["Status"].as_string(), Some("Acknowledged"));
833 assert_eq!(dict["ProfileData"].as_data(), Some(&b"<plist/>"[..]));
834
835 let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
836 let payload = &stream.written[4..4 + len];
837 let sent: plist::Dictionary = plist::from_bytes(payload).unwrap();
838 assert_eq!(
839 sent.get("RequestType").and_then(plist::Value::as_string),
840 Some("GetStoredProfile")
841 );
842 assert_eq!(
843 sent.get("Purpose").and_then(plist::Value::as_string),
844 Some("PostSetupInstallation")
845 );
846 }
847
848 #[tokio::test]
849 async fn flush_sends_flush_request() {
850 let response = plist::Value::Dictionary(plist::Dictionary::from_iter([(
851 "Status".to_string(),
852 plist::Value::String("Acknowledged".into()),
853 )]));
854 let mut stream = MockStream::with_response(response);
855 let mut client = McInstallClient::new(&mut stream);
856
857 client.flush().await.unwrap();
858
859 let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
860 let payload = &stream.written[4..4 + len];
861 let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
862 assert_eq!(
863 dict.get("RequestType").and_then(plist::Value::as_string),
864 Some("Flush")
865 );
866 }
867
868 #[tokio::test]
869 async fn hello_host_identifier_sends_request_type() {
870 let response = plist::Value::Dictionary(plist::Dictionary::from_iter([(
871 "Status".to_string(),
872 plist::Value::String("Acknowledged".into()),
873 )]));
874 let mut stream = MockStream::with_response(response);
875 let mut client = McInstallClient::new(&mut stream);
876
877 client.hello_host_identifier().await.unwrap();
878
879 let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
880 let payload = &stream.written[4..4 + len];
881 let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
882 assert_eq!(
883 dict.get("RequestType").and_then(plist::Value::as_string),
884 Some("HelloHostIdentifier")
885 );
886 }
887
888 #[tokio::test]
889 async fn set_cloud_configuration_sends_payload() {
890 let response = plist::Value::Dictionary(plist::Dictionary::from_iter([(
891 "Status".to_string(),
892 plist::Value::String("Acknowledged".into()),
893 )]));
894 let mut stream = MockStream::with_response(response);
895 let mut client = McInstallClient::new(&mut stream);
896 let cloud_configuration = plist::Dictionary::from_iter([
897 ("AllowPairing".to_string(), plist::Value::Boolean(true)),
898 (
899 "SkipSetup".to_string(),
900 plist::Value::Array(vec![plist::Value::String("WiFi".into())]),
901 ),
902 ]);
903
904 client
905 .set_cloud_configuration(cloud_configuration.clone())
906 .await
907 .unwrap();
908
909 let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
910 let payload = &stream.written[4..4 + len];
911 let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
912 assert_eq!(
913 dict.get("RequestType").and_then(plist::Value::as_string),
914 Some("SetCloudConfiguration")
915 );
916 assert_eq!(
917 dict.get("CloudConfiguration")
918 .and_then(plist::Value::as_dictionary),
919 Some(&cloud_configuration)
920 );
921 }
922
923 #[tokio::test]
924 async fn escalate_unsupervised_uses_zero_byte_certificate() {
925 let response = plist::Value::Dictionary(plist::Dictionary::from_iter([(
926 "Status".to_string(),
927 plist::Value::String("Acknowledged".into()),
928 )]));
929 let mut stream = MockStream::with_response(response);
930 let mut client = McInstallClient::new(&mut stream);
931
932 client.escalate_unsupervised().await.unwrap();
933
934 let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
935 let payload = &stream.written[4..4 + len];
936 let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
937 assert_eq!(
938 dict.get("RequestType").and_then(plist::Value::as_string),
939 Some("Escalate")
940 );
941 assert_eq!(
942 dict.get("SupervisorCertificate")
943 .and_then(plist::Value::as_data),
944 Some(&b"\x00"[..])
945 );
946 }
947
948 #[tokio::test]
949 async fn erase_device_sends_expected_flags() {
950 let response = plist::Value::Dictionary(plist::Dictionary::from_iter([(
951 "Status".to_string(),
952 plist::Value::String("Acknowledged".into()),
953 )]));
954 let mut stream = MockStream::with_response(response);
955 let mut client = McInstallClient::new(&mut stream);
956
957 client.erase_device(true, false).await.unwrap();
958
959 let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
960 let payload = &stream.written[4..4 + len];
961 let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
962 assert_eq!(
963 dict.get("RequestType").and_then(plist::Value::as_string),
964 Some("EraseDevice")
965 );
966 assert_eq!(
967 dict.get("PreserveDataPlan")
968 .and_then(plist::Value::as_boolean),
969 Some(true)
970 );
971 assert_eq!(
972 dict.get("DisallowProximitySetup")
973 .and_then(plist::Value::as_boolean),
974 Some(false)
975 );
976 }
977}