Skip to main content

ios_core/services/mcinstall/
mod.rs

1//! Minimal MCInstall client for read-only profile inspection.
2//!
3//! Service: `com.apple.mobile.MCInstall`
4
5use 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}