ios_core/services/idam/
mod.rs1use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
6
7pub const SERVICE_NAME: &str = "com.apple.idamd";
8pub const RSD_SERVICE_NAME: &str = "com.apple.idamd.shim.remote";
9
10service_error!(IdamError);
11
12pub struct IdamClient<S> {
13 stream: S,
14}
15
16impl<S: AsyncRead + AsyncWrite + Unpin> IdamClient<S> {
17 pub fn new(stream: S) -> Self {
18 Self { stream }
19 }
20
21 pub async fn configuration_inquiry(&mut self) -> Result<plist::Value, IdamError> {
22 let request = plist::Dictionary::from_iter([(
23 "Configuration Inquiry".to_string(),
24 plist::Value::Boolean(true),
25 )]);
26 send_plist(&mut self.stream, &plist::Value::Dictionary(request)).await?;
27 Ok(plist::Value::Dictionary(
28 recv_plist(&mut self.stream).await?,
29 ))
30 }
31
32 pub async fn set_configuration(&mut self, enabled: bool) -> Result<plist::Value, IdamError> {
33 let request = plist::Dictionary::from_iter([(
34 "Set IDAM Configuration".to_string(),
35 plist::Value::Boolean(enabled),
36 )]);
37 send_plist(&mut self.stream, &plist::Value::Dictionary(request)).await?;
38 match recv_plist(&mut self.stream).await {
39 Ok(response) => Ok(plist::Value::Dictionary(response)),
40 Err(IdamError::Io(err)) if err.kind() == std::io::ErrorKind::UnexpectedEof => {
41 Ok(plist::Value::Dictionary(plist::Dictionary::new()))
42 }
43 Err(err) => Err(err),
44 }
45 }
46}
47
48async fn send_plist<S: AsyncWrite + Unpin>(
49 stream: &mut S,
50 value: &plist::Value,
51) -> Result<(), IdamError> {
52 let mut buf = Vec::new();
53 plist::to_writer_xml(&mut buf, value)?;
54 stream.write_all(&(buf.len() as u32).to_be_bytes()).await?;
55 stream.write_all(&buf).await?;
56 stream.flush().await?;
57 Ok(())
58}
59
60async fn recv_plist<S: AsyncRead + Unpin>(stream: &mut S) -> Result<plist::Dictionary, IdamError> {
61 let mut len_buf = [0u8; 4];
62 stream.read_exact(&mut len_buf).await?;
63 let len = u32::from_be_bytes(len_buf) as usize;
64 const MAX_PLIST_SIZE: usize = 1024 * 1024;
65 if len > MAX_PLIST_SIZE {
66 return Err(IdamError::Protocol(format!(
67 "plist length {len} exceeds max {MAX_PLIST_SIZE}"
68 )));
69 }
70 let mut buf = vec![0u8; len];
71 stream.read_exact(&mut buf).await?;
72 Ok(plist::from_bytes(&buf)?)
73}
74
75#[cfg(test)]
76mod tests {
77 use crate::test_util::MockStream;
78
79 use super::*;
80
81 #[tokio::test]
82 async fn configuration_inquiry_sends_expected_request() {
83 let response = plist::Value::Dictionary(plist::Dictionary::from_iter([(
84 "SupportsIDAM".to_string(),
85 plist::Value::Boolean(true),
86 )]));
87 let mut stream = MockStream::with_response(response);
88 let mut client = IdamClient::new(&mut stream);
89
90 let value = client.configuration_inquiry().await.unwrap();
91 let dict = value
92 .as_dictionary()
93 .expect("configuration inquiry should return a plist dictionary");
94 assert_eq!(
95 dict.get("SupportsIDAM").and_then(plist::Value::as_boolean),
96 Some(true),
97 "configuration inquiry should return plist payload"
98 );
99
100 let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
101 let payload = &stream.written[4..4 + len];
102 let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
103 assert_eq!(dict["Configuration Inquiry"].as_boolean(), Some(true));
104 }
105
106 #[tokio::test]
107 async fn set_configuration_sends_expected_boolean() {
108 let response = plist::Value::Dictionary(plist::Dictionary::from_iter([(
109 "Status".to_string(),
110 plist::Value::String("Acknowledged".into()),
111 )]));
112 let mut stream = MockStream::with_response(response);
113 let mut client = IdamClient::new(&mut stream);
114
115 let value = client.set_configuration(false).await.unwrap();
116 let dict = value
117 .as_dictionary()
118 .expect("set_configuration should return a plist dictionary");
119 assert_eq!(
120 dict.get("Status").and_then(plist::Value::as_string),
121 Some("Acknowledged")
122 );
123
124 let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
125 let payload = &stream.written[4..4 + len];
126 let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
127 assert_eq!(dict["Set IDAM Configuration"].as_boolean(), Some(false));
128 }
129
130 #[tokio::test]
131 async fn set_configuration_treats_eof_as_success() {
132 let mut stream = MockStream::eof();
133 let mut client = IdamClient::new(&mut stream);
134
135 let value = client.set_configuration(true).await.unwrap();
136 assert_eq!(
137 value.as_dictionary().map(plist::Dictionary::len),
138 Some(0),
139 "EOF after set should be treated as a successful empty response"
140 );
141
142 let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
143 let payload = &stream.written[4..4 + len];
144 let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
145 assert_eq!(dict["Set IDAM Configuration"].as_boolean(), Some(true));
146 }
147}