interop/clients/corecrypto/
web.rs

1use crate::{
2    CIPHERSUITE_IN_USE,
3    clients::{EmulatedClient, EmulatedClientProtocol, EmulatedClientType, EmulatedMlsClient},
4};
5use color_eyre::eyre::Result;
6use core_crypto::prelude::{KeyPackage, KeyPackageIn};
7use std::cell::Cell;
8use std::net::SocketAddr;
9use tls_codec::Deserialize;
10
11#[derive(Debug)]
12pub(crate) struct CoreCryptoWebClient {
13    browser: fantoccini::Client,
14    client_id: Vec<u8>,
15    #[cfg(feature = "proteus")]
16    prekey_last_id: Cell<u16>,
17}
18
19impl CoreCryptoWebClient {
20    pub(crate) async fn new(driver_addr: &SocketAddr, server: &SocketAddr) -> Result<Self> {
21        let client_id = uuid::Uuid::new_v4();
22        let client_id_str = client_id.as_hyphenated().to_string();
23        let ciphersuite = CIPHERSUITE_IN_USE as u16;
24        let client_config = serde_json::json!({
25            "databaseName": format!("db-{client_id_str}"),
26            "ciphersuites": [ciphersuite],
27            "clientId": client_id_str
28        });
29        let browser = crate::build::web::webdriver::setup_browser(driver_addr, server, "core-crypto").await?;
30
31        let _ = browser
32            .execute_async(
33                r#"
34const [clientConfig, callback] = arguments;
35const { CoreCrypto, Ciphersuite, CredentialType, DatabaseKey, initWasmModule } = await import("./corecrypto.js");
36const key = new Uint8Array(32);
37await initWasmModule();
38window.crypto.getRandomValues(key);
39clientConfig.key = new DatabaseKey(key);
40window.CoreCrypto = CoreCrypto;
41window.cc = await window.CoreCrypto.init(clientConfig);
42window.ciphersuite = Ciphersuite.MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519;
43window.credentialType = CredentialType.Basic;
44
45window.deliveryService = {
46    async sendCommitBundle() {
47        return "success";
48    },
49    async sendMessage() {
50        return "success";
51    },
52};
53
54await window.cc.provideTransport(window.deliveryService);
55
56callback();"#,
57                vec![client_config],
58            )
59            .await?;
60
61        Ok(Self {
62            browser,
63            client_id: client_id.into_bytes().into(),
64            #[cfg(feature = "proteus")]
65            prekey_last_id: Cell::new(0),
66        })
67    }
68}
69
70#[async_trait::async_trait(?Send)]
71impl EmulatedClient for CoreCryptoWebClient {
72    fn client_name(&self) -> &str {
73        "CoreCrypto::wasm"
74    }
75
76    fn client_type(&self) -> EmulatedClientType {
77        EmulatedClientType::Web
78    }
79
80    fn client_id(&self) -> &[u8] {
81        self.client_id.as_slice()
82    }
83
84    fn client_protocol(&self) -> EmulatedClientProtocol {
85        EmulatedClientProtocol::MLS | EmulatedClientProtocol::PROTEUS
86    }
87
88    async fn wipe(&mut self) -> Result<()> {
89        let client_id = uuid::Uuid::from_slice(self.client_id.as_slice())?;
90        let client_id_str = client_id.as_hyphenated().to_string();
91        let database_name = format!("db-{client_id_str}");
92        let _ = self
93            .browser
94            .execute_async(
95                r#"
96    const [databaseName, callback] = arguments;
97    await window.cc.close();
98    const result = window.indexedDB.deleteDatabase(databaseName);
99    result.onsuccess = callback;
100    result.onfailure = callback;"#,
101                vec![serde_json::json!(database_name)],
102            )
103            .await?;
104
105        Ok(())
106    }
107}
108
109#[async_trait::async_trait(?Send)]
110impl EmulatedMlsClient for CoreCryptoWebClient {
111    async fn get_keypackage(&self) -> Result<Vec<u8>> {
112        let ciphersuite = CIPHERSUITE_IN_USE as u16;
113        let start = std::time::Instant::now();
114        let kp_raw = self
115            .browser
116            .execute_async(
117                r#"
118const [ciphersuite, callback] = arguments;
119window.cc.transaction((ctx) =>
120    ctx.clientKeypackages(ciphersuite, window.credentialType, 1)
121).then(([kp]) => callback(kp));"#,
122                vec![serde_json::json!(ciphersuite)],
123            )
124            .await
125            .and_then(|value| Ok(serde_json::from_value::<Vec<u8>>(value)?))?;
126
127        let kp: KeyPackage = KeyPackageIn::tls_deserialize(&mut kp_raw.as_slice())?.into();
128
129        log::info!(
130            "KP Init Key [took {}ms]: Client {} [{}] - {}",
131            start.elapsed().as_millis(),
132            self.client_name(),
133            hex::encode(&self.client_id),
134            hex::encode(kp.hpke_init_key()),
135        );
136
137        Ok(kp_raw)
138    }
139
140    async fn add_client(&self, conversation_id: &[u8], kp: &[u8]) -> Result<()> {
141        self.browser
142            .execute_async(
143                r#"
144const [cId, kp, callback] = arguments;
145const conversationId = Uint8Array.from(Object.values(cId));
146const keyPackage = Uint8Array.from(Object.values(kp));
147if (!window.cc.conversationExists(conversationId)) {
148    await window.cc.transaction((ctx) =>
149        ctx.createConversation(conversationId)
150    );
151}
152window.cc.transaction((ctx) =>
153    ctx.addClientsToConversation(conversationId, [{ kp: keyPackage }]))
154.then(({ welcome }) => callback(welcome));"#,
155                vec![conversation_id.into(), kp.into()],
156            )
157            .await?;
158        Ok(())
159    }
160
161    async fn kick_client(&self, conversation_id: &[u8], client_id: &[u8]) -> Result<()> {
162        Ok(self
163            .browser
164            .execute_async(
165                r#"
166const [cId, clId, callback] = arguments;
167const conversationId = Uint8Array.from(Object.values(cId));
168const clientId = Uint8Array.from(Object.values(clId));
169window.cc.transaction((ctx) =>
170    ctx.removeClientsFromConversation(conversationId, [clientId]))
171.then(({ commit }) => callback(commit));"#,
172                vec![conversation_id.into(), client_id.into()],
173            )
174            .await
175            .and_then(|value| Ok(serde_json::from_value(value)?))?)
176    }
177
178    async fn process_welcome(&self, welcome: &[u8]) -> Result<Vec<u8>> {
179        Ok(self
180            .browser
181            .execute_async(
182                r#"
183const [welcome, callback] = arguments;
184const welcomeMessage = Uint8Array.from(Object.values(welcome));
185window.cc.transaction((ctx) =>
186    ctx.processWelcomeMessage(welcomeMessage))
187.then(({ id }) => callback(id));"#,
188                vec![welcome.into()],
189            )
190            .await
191            .and_then(|value| Ok(serde_json::from_value(value)?))?)
192    }
193
194    async fn encrypt_message(&self, conversation_id: &[u8], message: &[u8]) -> Result<Vec<u8>> {
195        Ok(self
196            .browser
197            .execute_async(
198                r#"
199const [cId, cleartext, callback] = arguments;
200const conversationId = Uint8Array.from(Object.values(cId));
201const message = Uint8Array.from(Object.values(cleartext));
202window.cc.transaction((ctx) =>
203    ctx.encryptMessage(conversationId, message))
204.then(callback);"#,
205                vec![conversation_id.into(), message.into()],
206            )
207            .await
208            .and_then(|value| Ok(serde_json::from_value(value)?))?)
209    }
210
211    async fn decrypt_message(&self, conversation_id: &[u8], message: &[u8]) -> Result<Option<Vec<u8>>> {
212        let res = self
213            .browser
214            .execute_async(
215                r#"
216const [cId, encMessage, callback] = arguments;
217const conversationId = Uint8Array.from(Object.values(cId));
218const encryptedMessage = Uint8Array.from(Object.values(encMessage));
219window.cc.transaction((ctx) =>
220    ctx.decryptMessage(conversationId, encryptedMessage)
221).then(({ message }) => callback(message));"#,
222                vec![conversation_id.into(), message.into()],
223            )
224            .await?;
225
226        if res.is_null() {
227            Ok(None)
228        } else {
229            Ok(Some(serde_json::from_value(res)?))
230        }
231    }
232}
233
234#[cfg(feature = "proteus")]
235#[async_trait::async_trait(?Send)]
236impl crate::clients::EmulatedProteusClient for CoreCryptoWebClient {
237    async fn init(&mut self) -> Result<()> {
238        self.browser
239            .execute_async(
240                r#"
241const [callback] = arguments;
242window.cc.transaction((ctx) =>
243    ctx.proteusInit()
244).then(callback);"#,
245                vec![],
246            )
247            .await?;
248
249        Ok(())
250    }
251
252    async fn get_prekey(&self) -> Result<Vec<u8>> {
253        let prekey_last_id = self.prekey_last_id.get() + 1;
254        self.prekey_last_id.replace(prekey_last_id);
255        let prekey = self
256            .browser
257            .execute_async(
258                r#"
259const [prekeyId, callback] = arguments;
260window.cc.transaction((ctx) =>
261    ctx.proteusNewPrekey(prekeyId)
262).then(callback);"#,
263                vec![prekey_last_id.into()],
264            )
265            .await
266            .and_then(|value| Ok(serde_json::from_value(value)?))?;
267
268        Ok(prekey)
269    }
270
271    async fn session_from_prekey(&self, session_id: &str, prekey: &[u8]) -> Result<()> {
272        self.browser
273            .execute_async(
274                r#"
275const [sessionId, prekey, callback] = arguments;
276const prekeyBuffer = Uint8Array.from(Object.values(prekey));
277window.cc.transaction((ctx) =>
278    ctx.proteusSessionFromPrekey(sessionId, prekeyBuffer)
279).then(callback);"#,
280                vec![session_id.into(), prekey.into()],
281            )
282            .await?;
283        Ok(())
284    }
285
286    async fn session_from_message(&self, session_id: &str, message: &[u8]) -> Result<Vec<u8>> {
287        let cleartext = self
288            .browser
289            .execute_async(
290                r#"
291const [sessionId, message, callback] = arguments;
292const messageBuffer = Uint8Array.from(Object.values(message));
293window.cc.transaction((ctx) =>
294    ctx.proteusSessionFromMessage(sessionId, messageBuffer)
295).then(callback);"#,
296                vec![session_id.into(), message.into()],
297            )
298            .await
299            .and_then(|value| Ok(serde_json::from_value(value)?))?;
300
301        Ok(cleartext)
302    }
303    async fn encrypt(&self, session_id: &str, plaintext: &[u8]) -> Result<Vec<u8>> {
304        let ciphertext = self
305            .browser
306            .execute_async(
307                r#"
308const [sessionId, plaintext, callback] = arguments;
309const plaintextBuffer = Uint8Array.from(Object.values(plaintext));
310window.cc.transaction((ctx) =>
311    ctx.proteusEncrypt(sessionId, plaintextBuffer)
312).then(callback);"#,
313                vec![session_id.into(), plaintext.into()],
314            )
315            .await
316            .and_then(|value| Ok(serde_json::from_value(value)?))?;
317
318        Ok(ciphertext)
319    }
320
321    async fn decrypt(&self, session_id: &str, ciphertext: &[u8]) -> Result<Vec<u8>> {
322        let cleartext = self
323            .browser
324            .execute_async(
325                r#"
326const [sessionId, ciphertext, callback] = arguments;
327const ciphertextBuffer = Uint8Array.from(Object.values(ciphertext));
328window.cc.transaction((ctx) =>
329    ctx.proteusDecrypt(sessionId, ciphertextBuffer)
330).then(callback);"#,
331                vec![session_id.into(), ciphertext.into()],
332            )
333            .await
334            .and_then(|value| Ok(serde_json::from_value(value)?))?;
335
336        Ok(cleartext)
337    }
338
339    async fn fingerprint(&self) -> Result<String> {
340        Ok(self
341            .browser
342            .execute_async(
343                "const [callback] = arguments; window.cc.proteusFingerprint().then(callback);",
344                vec![],
345            )
346            .await?
347            .as_str()
348            .unwrap()
349            .into())
350    }
351}