interop/clients/corecrypto/
ios.rs

1// Wire
2// Copyright (C) 2025 Wire Swiss GmbH
3
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with this program. If not, see http://www.gnu.org/licenses/.
16
17use crate::{
18    CIPHERSUITE_IN_USE,
19    clients::{EmulatedClient, EmulatedClientProtocol, EmulatedClientType, EmulatedMlsClient},
20};
21use base64::{Engine as _, engine::general_purpose};
22use color_eyre::eyre::Result;
23use core_crypto::prelude::{KeyPackage, KeyPackageIn};
24use std::cell::{Cell, RefCell};
25use std::fs;
26use std::io::{BufRead, BufReader, Read};
27use std::process::{Child, ChildStdout, Command, Output, Stdio};
28use std::time::Duration;
29use thiserror::Error;
30use tls_codec::Deserialize;
31
32#[derive(Debug)]
33struct SimulatorDriver {
34    device: String,
35    process: Child,
36    output: RefCell<BufReader<ChildStdout>>,
37}
38
39#[derive(Debug, serde::Deserialize)]
40enum InteropResult {
41    #[serde(rename = "success")]
42    Success { value: String },
43    #[serde(rename = "failure")]
44    Failure { message: String },
45}
46
47#[derive(Error, Debug)]
48#[error("simulator driver error: {msg}")]
49struct SimulatorDriverError {
50    msg: String,
51}
52
53impl SimulatorDriver {
54    fn new(device: String, application: String) -> Self {
55        let application = Self::launch_application(&device, &application, true).expect("Failed ot launch application");
56
57        Self {
58            device,
59            process: application.0,
60            output: RefCell::new(application.1),
61        }
62    }
63
64    fn boot_device(device: &str) -> std::io::Result<Output> {
65        Command::new("xcrun").args(["simctl", "boot", device]).output()
66    }
67
68    fn launch_application(
69        device: &str,
70        application: &str,
71        boot_device: bool,
72    ) -> Result<(Child, BufReader<ChildStdout>)> {
73        log::info!("launching application: {} on {}", application, device);
74
75        let mut process = Command::new("xcrun")
76            .args([
77                "simctl",
78                "launch",
79                "--console-pty",
80                "--terminate-running-process",
81                device,
82                application,
83            ])
84            .stdout(Stdio::piped())
85            .stderr(Stdio::piped())
86            .spawn()
87            .expect("Failed to launch application");
88
89        let mut output = BufReader::new(
90            process
91                .stdout
92                .take()
93                .expect("Expected stdout to be available on child process"),
94        );
95
96        // Wait for child process to launch or fail
97        std::thread::sleep(Duration::from_secs(3));
98        match process.try_wait() {
99            Ok(None) => {}
100            Ok(Some(exit_status)) => {
101                if boot_device && exit_status.code() == Some(149) {
102                    log::info!("device is shutdown, booting...");
103                    Self::boot_device(device)?;
104                    return Self::launch_application(device, application, false);
105                }
106
107                let mut error_message = String::new();
108                process
109                    .stderr
110                    .map(|mut stderr| stderr.read_to_string(&mut error_message));
111                panic!("Failed to launch application ({}): {}", exit_status, error_message)
112            }
113            Err(error) => {
114                panic!("Failed to launch application: {}", error)
115            }
116        }
117
118        // Waiting for confirmation that the application has launched.
119        let mut line = String::new();
120        while !line.contains("Ready") {
121            line.clear();
122            output
123                .read_line(&mut line)
124                .expect("was expecting ready signal on stdout");
125        }
126
127        log::info!("application launched: {}", line);
128        Ok((process, output))
129    }
130
131    async fn execute(&self, action: String) -> Result<String> {
132        log::info!("interop://{}", action);
133
134        Command::new("xcrun")
135            .args([
136                "simctl",
137                "openurl",
138                &self.device,
139                format!("interop://{}", action).as_str(),
140            ])
141            .output()
142            .expect("Failed to execute action");
143
144        let mut result = String::new();
145        let mut output = self.output.try_borrow_mut()?;
146
147        output.read_line(&mut result)?;
148
149        log::info!("{}", result);
150
151        let result: InteropResult = serde_json::from_str(result.trim())?;
152
153        match result {
154            InteropResult::Success { value } => Ok(value),
155            InteropResult::Failure { message } => Err(SimulatorDriverError { msg: message }.into()),
156        }
157    }
158}
159
160impl Drop for SimulatorDriver {
161    fn drop(&mut self) {
162        self.process.kill().expect("expected child process to be killed")
163    }
164}
165
166#[derive(Debug)]
167pub(crate) struct CoreCryptoIosClient {
168    driver: SimulatorDriver,
169    client_id: Vec<u8>,
170    #[cfg(feature = "proteus")]
171    prekey_last_id: Cell<u16>,
172}
173
174impl CoreCryptoIosClient {
175    pub(crate) async fn new() -> Result<Self> {
176        let client_id = uuid::Uuid::new_v4();
177        let client_id_str = client_id.as_hyphenated().to_string();
178        let client_id_base64 = general_purpose::STANDARD.encode(client_id_str.as_str());
179        let ciphersuite = CIPHERSUITE_IN_USE as u16;
180        let device = std::env::var("INTEROP_SIMULATOR_DEVICE").unwrap_or("booted".into());
181
182        let driver = SimulatorDriver::new(device, "com.wire.InteropClient".into());
183        log::info!("initialising core crypto with ciphersuite {}", ciphersuite);
184        driver
185            .execute(format!(
186                "init-mls?client={}&ciphersuite={}",
187                client_id_base64, ciphersuite
188            ))
189            .await?;
190
191        Ok(Self {
192            driver,
193            client_id: client_id.into_bytes().into(),
194            #[cfg(feature = "proteus")]
195            prekey_last_id: Cell::new(0),
196        })
197    }
198}
199
200#[async_trait::async_trait(?Send)]
201impl EmulatedClient for CoreCryptoIosClient {
202    fn client_name(&self) -> &str {
203        "CoreCrypto::ios"
204    }
205
206    fn client_type(&self) -> EmulatedClientType {
207        EmulatedClientType::AppleiOS
208    }
209
210    fn client_id(&self) -> &[u8] {
211        self.client_id.as_slice()
212    }
213
214    fn client_protocol(&self) -> EmulatedClientProtocol {
215        EmulatedClientProtocol::MLS | EmulatedClientProtocol::PROTEUS
216    }
217
218    async fn wipe(mut self) -> Result<()> {
219        Ok(())
220    }
221}
222
223#[async_trait::async_trait(?Send)]
224impl EmulatedMlsClient for CoreCryptoIosClient {
225    async fn get_keypackage(&self) -> Result<Vec<u8>> {
226        let ciphersuite = CIPHERSUITE_IN_USE as u16;
227        let start = std::time::Instant::now();
228        let kp_base64 = self
229            .driver
230            .execute(format!("get-key-package?ciphersuite={}", ciphersuite))
231            .await?;
232        let kp_raw = general_purpose::STANDARD.decode(kp_base64)?;
233        let kp: KeyPackage = KeyPackageIn::tls_deserialize(&mut kp_raw.as_slice())?.into();
234
235        log::info!(
236            "KP Init Key [took {}ms]: Client {} [{}] - {}",
237            start.elapsed().as_millis(),
238            self.client_name(),
239            hex::encode(&self.client_id),
240            hex::encode(kp.hpke_init_key()),
241        );
242
243        Ok(kp_raw)
244    }
245
246    async fn add_client(&self, conversation_id: &[u8], kp: &[u8]) -> Result<()> {
247        let cid_base64 = general_purpose::STANDARD.encode(conversation_id);
248        let kp_base64 = general_purpose::STANDARD.encode(kp);
249        let ciphersuite = CIPHERSUITE_IN_USE as u16;
250        self.driver
251            .execute(format!(
252                "add-client?cid={}&ciphersuite={}&kp={}",
253                cid_base64, ciphersuite, kp_base64
254            ))
255            .await?;
256
257        Ok(())
258    }
259
260    async fn kick_client(&self, conversation_id: &[u8], client_id: &[u8]) -> Result<()> {
261        let cid_base64 = general_purpose::STANDARD.encode(conversation_id);
262        let client_id_base64 = general_purpose::STANDARD.encode(client_id);
263        self.driver
264            .execute(format!("remove-client?cid={}&client={}", cid_base64, client_id_base64))
265            .await?;
266
267        Ok(())
268    }
269
270    async fn process_welcome(&self, welcome: &[u8]) -> Result<Vec<u8>> {
271        let welcome_path = std::env::temp_dir().join(format!("welcome-{}", uuid::Uuid::new_v4().as_hyphenated()));
272        fs::write(&welcome_path, welcome)?;
273        let conversation_id_base64 = self
274            .driver
275            .execute(format!(
276                "process-welcome?welcome_path={}",
277                welcome_path.to_str().unwrap()
278            ))
279            .await?;
280        let conversation_id = general_purpose::STANDARD.decode(conversation_id_base64)?;
281
282        Ok(conversation_id)
283    }
284
285    async fn encrypt_message(&self, conversation_id: &[u8], message: &[u8]) -> Result<Vec<u8>> {
286        let cid_base64 = general_purpose::STANDARD.encode(conversation_id);
287        let message_base64 = general_purpose::STANDARD.encode(message);
288        let encrypted_message_base64 = self
289            .driver
290            .execute(format!("encrypt-message?cid={}&message={}", cid_base64, message_base64))
291            .await?;
292        let encrypted_message = general_purpose::STANDARD.decode(encrypted_message_base64)?;
293
294        Ok(encrypted_message)
295    }
296
297    async fn decrypt_message(&self, conversation_id: &[u8], message: &[u8]) -> Result<Option<Vec<u8>>> {
298        let cid_base64 = general_purpose::STANDARD.encode(conversation_id);
299        let message_base64 = general_purpose::STANDARD.encode(message);
300        let result = self
301            .driver
302            .execute(format!("decrypt-message?cid={}&message={}", cid_base64, message_base64))
303            .await?;
304
305        if result == "decrypted protocol message" {
306            Ok(None)
307        } else {
308            let decrypted_message = general_purpose::STANDARD.decode(result)?;
309            Ok(Some(decrypted_message))
310        }
311    }
312}
313
314#[cfg(feature = "proteus")]
315#[async_trait::async_trait(?Send)]
316impl crate::clients::EmulatedProteusClient for CoreCryptoIosClient {
317    async fn init(&mut self) -> Result<()> {
318        self.driver.execute("init-proteus".into()).await?;
319        Ok(())
320    }
321
322    async fn get_prekey(&self) -> Result<Vec<u8>> {
323        let prekey_last_id = self.prekey_last_id.get() + 1;
324        self.prekey_last_id.replace(prekey_last_id);
325
326        let prekey_base64 = self.driver.execute(format!("get-prekey?id={}", prekey_last_id)).await?;
327        let prekey = general_purpose::STANDARD.decode(prekey_base64)?;
328
329        Ok(prekey)
330    }
331
332    async fn session_from_prekey(&self, session_id: &str, prekey: &[u8]) -> Result<()> {
333        let prekey_base64 = general_purpose::STANDARD.encode(prekey);
334        self.driver
335            .execute(format!(
336                "session-from-prekey?session_id={}&prekey={}",
337                session_id, prekey_base64
338            ))
339            .await?;
340
341        Ok(())
342    }
343
344    async fn session_from_message(&self, session_id: &str, message: &[u8]) -> Result<Vec<u8>> {
345        let message_base64 = general_purpose::STANDARD.encode(message);
346        let decrypted_message_base64 = self
347            .driver
348            .execute(format!(
349                "session-from-message?session_id={}&message={}",
350                session_id, message_base64
351            ))
352            .await?;
353        let decrypted_message = general_purpose::STANDARD.decode(decrypted_message_base64)?;
354
355        Ok(decrypted_message)
356    }
357    async fn encrypt(&self, session_id: &str, plaintext: &[u8]) -> Result<Vec<u8>> {
358        let plaintext_base64 = general_purpose::STANDARD.encode(plaintext);
359        let encrypted_message_base64 = self
360            .driver
361            .execute(format!(
362                "encrypt-proteus?session_id={}&message={}",
363                session_id, plaintext_base64
364            ))
365            .await?;
366        let encrypted_message = general_purpose::STANDARD.decode(encrypted_message_base64)?;
367
368        Ok(encrypted_message)
369    }
370
371    async fn decrypt(&self, session_id: &str, ciphertext: &[u8]) -> Result<Vec<u8>> {
372        let ciphertext_base64 = general_purpose::STANDARD.encode(ciphertext);
373        let decrypted_message_base64 = self
374            .driver
375            .execute(format!(
376                "decrypt-proteus?session_id={}&message={}",
377                session_id, ciphertext_base64
378            ))
379            .await?;
380        let decrypted_message = general_purpose::STANDARD.decode(decrypted_message_base64)?;
381
382        Ok(decrypted_message)
383    }
384
385    async fn fingerprint(&self) -> Result<String> {
386        let fingerprint = self.driver.execute("get-fingerprint".into()).await?;
387
388        Ok(fingerprint)
389    }
390}