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