core_crypto/
proteus.rs

1// Wire
2// Copyright (C) 2022 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::context::CentralContext;
18use crate::{
19    group_store::{GroupStore, GroupStoreValue},
20    CoreCrypto, CryptoError, CryptoResult, ProteusError,
21};
22use core_crypto_keystore::{
23    connection::FetchFromDatabase,
24    entities::{ProteusIdentity, ProteusSession},
25    Connection as CryptoKeystore,
26};
27use proteus_wasm::{
28    keys::{IdentityKeyPair, PreKeyBundle},
29    message::Envelope,
30    session::Session,
31};
32use std::{collections::HashMap, sync::Arc};
33
34/// Proteus session IDs, it seems it's basically a string
35pub type SessionIdentifier = String;
36
37/// Proteus Session wrapper, that contains the identifier and the associated proteus Session
38#[derive(Debug)]
39pub struct ProteusConversationSession {
40    pub(crate) identifier: SessionIdentifier,
41    pub(crate) session: Session<Arc<IdentityKeyPair>>,
42}
43
44impl ProteusConversationSession {
45    /// Encrypts a message for this Proteus session
46    pub fn encrypt(&mut self, plaintext: &[u8]) -> CryptoResult<Vec<u8>> {
47        Ok(self
48            .session
49            .encrypt(plaintext)
50            .and_then(|e| e.serialise())
51            .map_err(ProteusError::from)?)
52    }
53
54    /// Decrypts a message for this Proteus session
55    pub async fn decrypt(
56        &mut self,
57        store: &mut core_crypto_keystore::Connection,
58        ciphertext: &[u8],
59    ) -> CryptoResult<Vec<u8>> {
60        let envelope = Envelope::deserialise(ciphertext).map_err(ProteusError::from)?;
61        Ok(self
62            .session
63            .decrypt(store, &envelope)
64            .await
65            .map_err(ProteusError::from)?)
66    }
67
68    /// Returns the session identifier
69    pub fn identifier(&self) -> &str {
70        &self.identifier
71    }
72
73    /// Returns the public key fingerprint of the local identity (= self identity)
74    pub fn fingerprint_local(&self) -> String {
75        self.session.local_identity().fingerprint()
76    }
77
78    /// Returns the public key fingerprint of the remote identity (= client you're communicating with)
79    pub fn fingerprint_remote(&self) -> String {
80        self.session.remote_identity().fingerprint()
81    }
82}
83
84impl CoreCrypto {
85    /// Proteus session accessor
86    ///
87    /// Warning: The Proteus client **MUST** be initialized with [CoreCrypto::proteus_init] first or an error will be returned
88    pub async fn proteus_session(
89        &self,
90        session_id: &str,
91    ) -> CryptoResult<Option<GroupStoreValue<ProteusConversationSession>>> {
92        let mut mutex = self.proteus.lock().await;
93        let proteus = mutex.as_mut().ok_or(CryptoError::ProteusNotInitialized)?;
94        let keystore = self.mls.mls_backend.keystore();
95        proteus.session(session_id, &keystore).await
96    }
97
98    /// Proteus session exists
99    ///
100    /// Warning: The Proteus client **MUST** be initialized with [CoreCrypto::proteus_init] first or an error will be returned
101    pub async fn proteus_session_exists(&self, session_id: &str) -> CryptoResult<bool> {
102        let mut mutex = self.proteus.lock().await;
103        let proteus = mutex.as_mut().ok_or(CryptoError::ProteusNotInitialized)?;
104        let keystore = self.mls.mls_backend.keystore();
105        Ok(proteus.session_exists(session_id, &keystore).await)
106    }
107
108    /// Returns the proteus last resort prekey id (u16::MAX = 65535)
109    pub fn proteus_last_resort_prekey_id() -> u16 {
110        ProteusCentral::last_resort_prekey_id()
111    }
112
113    /// Returns the proteus identity's public key fingerprint
114    ///
115    /// Warning: The Proteus client **MUST** be initialized with [CoreCrypto::proteus_init] first or an error will be returned
116    pub async fn proteus_fingerprint(&self) -> CryptoResult<String> {
117        let mutex = self.proteus.lock().await;
118        let proteus = mutex.as_ref().ok_or(CryptoError::ProteusNotInitialized)?;
119        Ok(proteus.fingerprint())
120    }
121
122    /// Returns the proteus identity's public key fingerprint
123    ///
124    /// Warning: The Proteus client **MUST** be initialized with [CoreCrypto::proteus_init] first or an error will be returned
125    pub async fn proteus_fingerprint_local(&self, session_id: &str) -> CryptoResult<String> {
126        let mut mutex = self.proteus.lock().await;
127        let proteus = mutex.as_mut().ok_or(CryptoError::ProteusNotInitialized)?;
128        let keystore = self.mls.mls_backend.keystore();
129        proteus.fingerprint_local(session_id, &keystore).await
130    }
131
132    /// Returns the proteus identity's public key fingerprint
133    ///
134    /// Warning: The Proteus client **MUST** be initialized with [CoreCrypto::proteus_init] first or an error will be returned
135    pub async fn proteus_fingerprint_remote(&self, session_id: &str) -> CryptoResult<String> {
136        let mut mutex = self.proteus.lock().await;
137        let proteus = mutex.as_mut().ok_or(CryptoError::ProteusNotInitialized)?;
138        let keystore = self.mls.mls_backend.keystore();
139        proteus.fingerprint_remote(session_id, &keystore).await
140    }
141}
142
143impl CentralContext {
144    /// Initializes the proteus client
145    pub async fn proteus_init(&self) -> CryptoResult<()> {
146        let keystore = self.keystore().await?;
147        let proteus_client = ProteusCentral::try_new(&keystore).await?;
148
149        // ? Make sure the last resort prekey exists
150        let _ = proteus_client.last_resort_prekey(&keystore).await?;
151
152        let mutex = self.proteus_central().await?;
153        let mut guard = mutex.lock().await;
154        *guard = Some(proteus_client);
155        Ok(())
156    }
157
158    /// Reloads the sessions from the key store
159    ///
160    /// Warning: The Proteus client **MUST** be initialized with [CoreCrypto::proteus_init] first or it will do nothing
161    pub async fn proteus_reload_sessions(&self) -> CryptoResult<()> {
162        let arc = self.proteus_central().await?;
163        let mut mutex = arc.lock().await;
164        let proteus = mutex.as_mut().ok_or(CryptoError::ProteusNotInitialized)?;
165        let keystore = self.keystore().await?;
166        proteus.reload_sessions(&keystore).await
167    }
168
169    /// Creates a proteus session from a prekey
170    ///
171    /// Warning: The Proteus client **MUST** be initialized with [CoreCrypto::proteus_init] first or an error will be returned
172    pub async fn proteus_session_from_prekey(
173        &self,
174        session_id: &str,
175        prekey: &[u8],
176    ) -> CryptoResult<GroupStoreValue<ProteusConversationSession>> {
177        let arc = self.proteus_central().await?;
178        let mut mutex = arc.lock().await;
179        let proteus = mutex.as_mut().ok_or(CryptoError::ProteusNotInitialized)?;
180        let keystore = self.keystore().await?;
181        let session = proteus.session_from_prekey(session_id, prekey).await?;
182        ProteusCentral::session_save_by_ref(&keystore, session.clone()).await?;
183
184        Ok(session)
185    }
186
187    /// Creates a proteus session from a Proteus message envelope
188    ///
189    /// Warning: The Proteus client **MUST** be initialized with [CoreCrypto::proteus_init] first or an error will be returned
190    pub async fn proteus_session_from_message(
191        &self,
192        session_id: &str,
193        envelope: &[u8],
194    ) -> CryptoResult<(GroupStoreValue<ProteusConversationSession>, Vec<u8>)> {
195        let arc = self.proteus_central().await?;
196        let mut mutex = arc.lock().await;
197        let proteus = mutex.as_mut().ok_or(CryptoError::ProteusNotInitialized)?;
198        let mut keystore = self.keystore().await?;
199        let (session, message) = proteus
200            .session_from_message(&mut keystore, session_id, envelope)
201            .await?;
202        ProteusCentral::session_save_by_ref(&keystore, session.clone()).await?;
203
204        Ok((session, message))
205    }
206
207    /// Saves a proteus session in the keystore
208    ///
209    /// Warning: The Proteus client **MUST** be initialized with [CoreCrypto::proteus_init] first or an error will be returned
210    pub async fn proteus_session_save(&self, session_id: &str) -> CryptoResult<()> {
211        let arc = self.proteus_central().await?;
212        let mut mutex = arc.lock().await;
213        let proteus = mutex.as_mut().ok_or(CryptoError::ProteusNotInitialized)?;
214        let keystore = self.keystore().await?;
215        proteus.session_save(&keystore, session_id).await
216    }
217
218    /// Deletes a proteus session from the keystore
219    ///
220    /// Warning: The Proteus client **MUST** be initialized with [CoreCrypto::proteus_init] first or an error will be returned
221    pub async fn proteus_session_delete(&self, session_id: &str) -> CryptoResult<()> {
222        let arc = self.proteus_central().await?;
223        let mut mutex = arc.lock().await;
224        let proteus = mutex.as_mut().ok_or(CryptoError::ProteusNotInitialized)?;
225        let keystore = self.keystore().await?;
226        proteus.session_delete(&keystore, session_id).await
227    }
228
229    /// Proteus session accessor
230    ///
231    /// Warning: The Proteus client **MUST** be initialized with [CoreCrypto::proteus_init] first or an error will be returned
232    pub async fn proteus_session(
233        &self,
234        session_id: &str,
235    ) -> CryptoResult<Option<GroupStoreValue<ProteusConversationSession>>> {
236        let arc = self.proteus_central().await?;
237        let mut mutex = arc.lock().await;
238        let proteus = mutex.as_mut().ok_or(CryptoError::ProteusNotInitialized)?;
239        let keystore = self.keystore().await?;
240        proteus.session(session_id, &keystore).await
241    }
242
243    /// Proteus session exists
244    ///
245    /// Warning: The Proteus client **MUST** be initialized with [CoreCrypto::proteus_init] first or an error will be returned
246    pub async fn proteus_session_exists(&self, session_id: &str) -> CryptoResult<bool> {
247        let arc = self.proteus_central().await?;
248        let mut mutex = arc.lock().await;
249        let proteus = mutex.as_mut().ok_or(CryptoError::ProteusNotInitialized)?;
250        let keystore = self.keystore().await?;
251        Ok(proteus.session_exists(session_id, &keystore).await)
252    }
253
254    /// Decrypts a proteus message envelope
255    ///
256    /// Warning: The Proteus client **MUST** be initialized with [CoreCrypto::proteus_init] first or an error will be returned
257    pub async fn proteus_decrypt(&self, session_id: &str, ciphertext: &[u8]) -> CryptoResult<Vec<u8>> {
258        let arc = self.proteus_central().await?;
259        let mut mutex = arc.lock().await;
260        let proteus = mutex.as_mut().ok_or(CryptoError::ProteusNotInitialized)?;
261        let mut keystore = self.keystore().await?;
262        proteus.decrypt(&mut keystore, session_id, ciphertext).await
263    }
264
265    /// Encrypts proteus message for a given session ID
266    ///
267    /// Warning: The Proteus client **MUST** be initialized with [CoreCrypto::proteus_init] first or an error will be returned
268    pub async fn proteus_encrypt(&self, session_id: &str, plaintext: &[u8]) -> CryptoResult<Vec<u8>> {
269        let arc = self.proteus_central().await?;
270        let mut mutex = arc.lock().await;
271        let proteus = mutex.as_mut().ok_or(CryptoError::ProteusNotInitialized)?;
272        let mut keystore = self.keystore().await?;
273        proteus.encrypt(&mut keystore, session_id, plaintext).await
274    }
275
276    /// Encrypts a proteus message for several sessions ID. This is more efficient than other methods as the calls are batched.
277    /// This also reduces the rountrips when crossing over the FFI
278    ///
279    /// Warning: The Proteus client **MUST** be initialized with [CoreCrypto::proteus_init] first or an error will be returned
280    pub async fn proteus_encrypt_batched(
281        &self,
282        sessions: &[impl AsRef<str>],
283        plaintext: &[u8],
284    ) -> CryptoResult<std::collections::HashMap<String, Vec<u8>>> {
285        let arc = self.proteus_central().await?;
286        let mut mutex = arc.lock().await;
287        let proteus = mutex.as_mut().ok_or(CryptoError::ProteusNotInitialized)?;
288        let mut keystore = self.keystore().await?;
289        proteus.encrypt_batched(&mut keystore, sessions, plaintext).await
290    }
291
292    /// Creates a new Proteus prekey and returns the CBOR-serialized version of the prekey bundle
293    ///
294    /// Warning: The Proteus client **MUST** be initialized with [CoreCrypto::proteus_init] first or an error will be returned
295    pub async fn proteus_new_prekey(&self, prekey_id: u16) -> CryptoResult<Vec<u8>> {
296        let arc = self.proteus_central().await?;
297        let mut mutex = arc.lock().await;
298        let proteus = mutex.as_mut().ok_or(CryptoError::ProteusNotInitialized)?;
299        let keystore = self.keystore().await?;
300        proteus.new_prekey(prekey_id, &keystore).await
301    }
302
303    /// Creates a new Proteus prekey with an automatically incremented ID and returns the CBOR-serialized version of the prekey bundle
304    ///
305    /// Warning: The Proteus client **MUST** be initialized with [CoreCrypto::proteus_init] first or an error will be returned
306    pub async fn proteus_new_prekey_auto(&self) -> CryptoResult<(u16, Vec<u8>)> {
307        let arc = self.proteus_central().await?;
308        let mut mutex = arc.lock().await;
309        let proteus = mutex.as_mut().ok_or(CryptoError::ProteusNotInitialized)?;
310        let keystore = self.keystore().await?;
311        proteus.new_prekey_auto(&keystore).await
312    }
313
314    /// Returns the last resort prekey
315    pub async fn proteus_last_resort_prekey(&self) -> CryptoResult<Vec<u8>> {
316        let arc = self.proteus_central().await?;
317        let mut mutex = arc.lock().await;
318        let proteus = mutex.as_mut().ok_or(CryptoError::ProteusNotInitialized)?;
319        let keystore = self.keystore().await?;
320
321        proteus.last_resort_prekey(&keystore).await
322    }
323
324    /// Returns the proteus last resort prekey id (u16::MAX = 65535)
325    pub fn proteus_last_resort_prekey_id() -> u16 {
326        ProteusCentral::last_resort_prekey_id()
327    }
328
329    /// Returns the proteus identity's public key fingerprint
330    ///
331    /// Warning: The Proteus client **MUST** be initialized with [CoreCrypto::proteus_init] first or an error will be returned
332    pub async fn proteus_fingerprint(&self) -> CryptoResult<String> {
333        let arc = self.proteus_central().await?;
334        let mut mutex = arc.lock().await;
335        let proteus = mutex.as_mut().ok_or(CryptoError::ProteusNotInitialized)?;
336        Ok(proteus.fingerprint())
337    }
338
339    /// Returns the proteus identity's public key fingerprint
340    ///
341    /// Warning: The Proteus client **MUST** be initialized with [CoreCrypto::proteus_init] first or an error will be returned
342    pub async fn proteus_fingerprint_local(&self, session_id: &str) -> CryptoResult<String> {
343        let arc = self.proteus_central().await?;
344        let mut mutex = arc.lock().await;
345        let proteus = mutex.as_mut().ok_or(CryptoError::ProteusNotInitialized)?;
346        let keystore = self.keystore().await?;
347        proteus.fingerprint_local(session_id, &keystore).await
348    }
349
350    /// Returns the proteus identity's public key fingerprint
351    ///
352    /// Warning: The Proteus client **MUST** be initialized with [CoreCrypto::proteus_init] first or an error will be returned
353    pub async fn proteus_fingerprint_remote(&self, session_id: &str) -> CryptoResult<String> {
354        let arc = self.proteus_central().await?;
355        let mut mutex = arc.lock().await;
356        let proteus = mutex.as_mut().ok_or(CryptoError::ProteusNotInitialized)?;
357        let keystore = self.keystore().await?;
358        proteus.fingerprint_remote(session_id, &keystore).await
359    }
360
361    /// Migrates an existing Cryptobox data store (whether a folder or an IndexedDB database) located at `path` to the keystore.
362    ///
363    ///The client can then be initialized with [CoreCrypto::proteus_init]
364    pub async fn proteus_cryptobox_migrate(&self, path: &str) -> CryptoResult<()> {
365        let keystore = self.keystore().await?;
366        ProteusCentral::cryptobox_migrate(&keystore, path).await
367    }
368}
369
370/// Proteus counterpart of [crate::mls::MlsCentral]
371///
372/// The big difference is that [ProteusCentral] doesn't *own* its own keystore but must borrow it from the outside.
373/// Whether it's exclusively for this struct's purposes or it's shared with our main struct, [crate::mls::MlsCentral]
374#[derive(Debug)]
375pub struct ProteusCentral {
376    proteus_identity: Arc<IdentityKeyPair>,
377    proteus_sessions: GroupStore<ProteusConversationSession>,
378}
379
380impl ProteusCentral {
381    /// Initializes the [ProteusCentral]
382    pub async fn try_new(keystore: &CryptoKeystore) -> CryptoResult<Self> {
383        let proteus_identity: Arc<IdentityKeyPair> = Arc::new(Self::load_or_create_identity(keystore).await?);
384        let proteus_sessions = Self::restore_sessions(keystore, &proteus_identity).await?;
385
386        Ok(Self {
387            proteus_identity,
388            proteus_sessions,
389        })
390    }
391
392    /// Restore proteus sessions from disk
393    pub(crate) async fn reload_sessions(&mut self, keystore: &CryptoKeystore) -> CryptoResult<()> {
394        self.proteus_sessions = Self::restore_sessions(keystore, &self.proteus_identity).await?;
395        Ok(())
396    }
397
398    /// This function will try to load a proteus Identity from our keystore; If it cannot, it will create a new one
399    /// This means this function doesn't fail except in cases of deeper errors (such as in the Keystore and other crypto errors)
400    async fn load_or_create_identity(keystore: &CryptoKeystore) -> CryptoResult<IdentityKeyPair> {
401        let keypair = if let Some(identity) = keystore.find::<ProteusIdentity>(&[]).await? {
402            let sk = identity.sk_raw();
403            let pk = identity.pk_raw();
404            // SAFETY: Byte lengths are ensured at the keystore level so this function is safe to call, despite being cursed
405
406            IdentityKeyPair::from_raw_key_pair(*sk, *pk).map_err(ProteusError::from)?
407        } else {
408            Self::create_identity(keystore).await?
409        };
410
411        Ok(keypair)
412    }
413
414    /// Internal function to create and save a new Proteus Identity
415    async fn create_identity(keystore: &CryptoKeystore) -> CryptoResult<IdentityKeyPair> {
416        let kp = IdentityKeyPair::new();
417        let pk = kp.public_key.public_key.as_slice().to_vec();
418
419        let ks_identity = ProteusIdentity {
420            sk: kp.secret_key.to_keypair_bytes().into(),
421            pk,
422        };
423        keystore.save(ks_identity).await?;
424
425        Ok(kp)
426    }
427
428    /// Restores the saved sessions in memory. This is performed automatically on init
429    async fn restore_sessions(
430        keystore: &core_crypto_keystore::Connection,
431        identity: &Arc<IdentityKeyPair>,
432    ) -> CryptoResult<GroupStore<ProteusConversationSession>> {
433        let mut proteus_sessions = GroupStore::new_with_limit(crate::group_store::ITEM_LIMIT * 2);
434        for session in keystore
435            .find_all::<ProteusSession>(Default::default())
436            .await?
437            .into_iter()
438        {
439            let proteus_session =
440                Session::deserialise(identity.clone(), &session.session).map_err(ProteusError::from)?;
441
442            let identifier = session.id.clone();
443
444            let proteus_conversation = ProteusConversationSession {
445                identifier: identifier.clone(),
446                session: proteus_session,
447            };
448
449            if proteus_sessions
450                .try_insert(identifier.into_bytes(), proteus_conversation)
451                .is_err()
452            {
453                break;
454            }
455        }
456
457        Ok(proteus_sessions)
458    }
459
460    /// Creates a new session from a prekey
461    pub async fn session_from_prekey(
462        &mut self,
463        session_id: &str,
464        key: &[u8],
465    ) -> CryptoResult<GroupStoreValue<ProteusConversationSession>> {
466        let prekey = PreKeyBundle::deserialise(key).map_err(ProteusError::from)?;
467        let proteus_session =
468            Session::init_from_prekey(self.proteus_identity.clone(), prekey).map_err(ProteusError::from)?;
469
470        let proteus_conversation = ProteusConversationSession {
471            identifier: session_id.into(),
472            session: proteus_session,
473        };
474
475        self.proteus_sessions.insert(session_id.into(), proteus_conversation);
476
477        Ok(self.proteus_sessions.get(session_id.as_bytes()).unwrap().clone())
478    }
479
480    /// Creates a new proteus Session from a received message
481    pub(crate) async fn session_from_message(
482        &mut self,
483        keystore: &mut CryptoKeystore,
484        session_id: &str,
485        envelope: &[u8],
486    ) -> CryptoResult<(GroupStoreValue<ProteusConversationSession>, Vec<u8>)> {
487        let message = Envelope::deserialise(envelope).map_err(ProteusError::from)?;
488        let (session, payload) = Session::init_from_message(self.proteus_identity.clone(), keystore, &message)
489            .await
490            .map_err(ProteusError::from)?;
491
492        let proteus_conversation = ProteusConversationSession {
493            identifier: session_id.into(),
494            session,
495        };
496
497        self.proteus_sessions.insert(session_id.into(), proteus_conversation);
498
499        Ok((
500            self.proteus_sessions.get(session_id.as_bytes()).unwrap().clone(),
501            payload,
502        ))
503    }
504
505    /// Persists a session in store
506    ///
507    /// **Note**: This isn't usually needed as persisting sessions happens automatically when decrypting/encrypting messages and initializing Sessions
508    pub(crate) async fn session_save(&mut self, keystore: &CryptoKeystore, session_id: &str) -> CryptoResult<()> {
509        if let Some(session) = self
510            .proteus_sessions
511            .get_fetch(session_id.as_bytes(), keystore, Some(self.proteus_identity.clone()))
512            .await?
513        {
514            Self::session_save_by_ref(keystore, session).await?;
515        }
516
517        Ok(())
518    }
519
520    async fn session_save_by_ref(
521        keystore: &CryptoKeystore,
522        session: GroupStoreValue<ProteusConversationSession>,
523    ) -> CryptoResult<()> {
524        let session = session.read().await;
525        let db_session = ProteusSession {
526            id: session.identifier().to_string(),
527            session: session.session.serialise().map_err(ProteusError::from)?,
528        };
529        keystore.save(db_session).await?;
530        Ok(())
531    }
532
533    /// Deletes a session in the store
534    pub(crate) async fn session_delete(&mut self, keystore: &CryptoKeystore, session_id: &str) -> CryptoResult<()> {
535        if keystore.remove::<ProteusSession, _>(session_id).await.is_ok() {
536            let _ = self.proteus_sessions.remove(session_id.as_bytes());
537        }
538        Ok(())
539    }
540
541    /// Session accessor
542    pub(crate) async fn session(
543        &mut self,
544        session_id: &str,
545        keystore: &CryptoKeystore,
546    ) -> CryptoResult<Option<GroupStoreValue<ProteusConversationSession>>> {
547        self.proteus_sessions
548            .get_fetch(session_id.as_bytes(), keystore, Some(self.proteus_identity.clone()))
549            .await
550    }
551
552    /// Session exists
553    pub(crate) async fn session_exists(&mut self, session_id: &str, keystore: &CryptoKeystore) -> bool {
554        self.session(session_id, keystore).await.ok().flatten().is_some()
555    }
556
557    /// Decrypt a proteus message for an already existing session
558    /// Note: This cannot be used for handshake messages, see [ProteusCentral::session_from_message]
559    pub(crate) async fn decrypt(
560        &mut self,
561        keystore: &mut CryptoKeystore,
562        session_id: &str,
563        ciphertext: &[u8],
564    ) -> CryptoResult<Vec<u8>> {
565        if let Some(session) = self
566            .proteus_sessions
567            .get_fetch(session_id.as_bytes(), keystore, Some(self.proteus_identity.clone()))
568            .await?
569        {
570            let plaintext = session.write().await.decrypt(keystore, ciphertext).await?;
571            ProteusCentral::session_save_by_ref(keystore, session).await?;
572
573            Ok(plaintext)
574        } else {
575            Err(CryptoError::ConversationNotFound(session_id.as_bytes().into()))
576        }
577    }
578
579    /// Encrypt a message for a session
580    pub(crate) async fn encrypt(
581        &mut self,
582        keystore: &mut CryptoKeystore,
583        session_id: &str,
584        plaintext: &[u8],
585    ) -> CryptoResult<Vec<u8>> {
586        if let Some(session) = self.session(session_id, keystore).await? {
587            let ciphertext = session.write().await.encrypt(plaintext)?;
588            ProteusCentral::session_save_by_ref(keystore, session).await?;
589
590            Ok(ciphertext)
591        } else {
592            Err(CryptoError::ConversationNotFound(session_id.as_bytes().into()))
593        }
594    }
595
596    /// Encrypts a message for a list of sessions
597    /// This is mainly used for conversations with multiple clients, this allows to minimize FFI roundtrips
598    pub(crate) async fn encrypt_batched(
599        &mut self,
600        keystore: &mut CryptoKeystore,
601        sessions: &[impl AsRef<str>],
602        plaintext: &[u8],
603    ) -> CryptoResult<HashMap<String, Vec<u8>>> {
604        let mut acc = HashMap::new();
605        for session_id in sessions {
606            if let Some(session) = self.session(session_id.as_ref(), keystore).await? {
607                let mut session_w = session.write().await;
608                acc.insert(session_w.identifier.clone(), session_w.encrypt(plaintext)?);
609                drop(session_w);
610
611                ProteusCentral::session_save_by_ref(keystore, session).await?;
612            }
613        }
614        Ok(acc)
615    }
616
617    /// Generates a new Proteus PreKey, stores it in the keystore and returns a serialized PreKeyBundle to be consumed externally
618    pub(crate) async fn new_prekey(&self, id: u16, keystore: &CryptoKeystore) -> CryptoResult<Vec<u8>> {
619        use proteus_wasm::keys::{PreKey, PreKeyId};
620
621        let prekey_id = PreKeyId::new(id);
622        let prekey = PreKey::new(prekey_id);
623        let keystore_prekey = core_crypto_keystore::entities::ProteusPrekey::from_raw(
624            id,
625            prekey.serialise().map_err(ProteusError::from)?,
626        );
627        let bundle = PreKeyBundle::new(self.proteus_identity.as_ref().public_key.clone(), &prekey);
628        let bundle = bundle.serialise().map_err(ProteusError::from)?;
629        keystore.save(keystore_prekey).await?;
630        Ok(bundle)
631    }
632
633    /// Generates a new Proteus Prekey, with an automatically auto-incremented ID.
634    ///
635    /// See [ProteusCentral::new_prekey]
636    pub(crate) async fn new_prekey_auto(&self, keystore: &CryptoKeystore) -> CryptoResult<(u16, Vec<u8>)> {
637        let id = core_crypto_keystore::entities::ProteusPrekey::get_free_id(keystore).await?;
638        Ok((id, self.new_prekey(id, keystore).await?))
639    }
640
641    /// Returns the Proteus last resort prekey ID (u16::MAX = 65535 = 0xFFFF)
642    pub fn last_resort_prekey_id() -> u16 {
643        proteus_wasm::keys::MAX_PREKEY_ID.value()
644    }
645
646    /// Returns the Proteus last resort prekey
647    /// If it cannot be found, one will be created.
648    pub(crate) async fn last_resort_prekey(&self, keystore: &CryptoKeystore) -> CryptoResult<Vec<u8>> {
649        let last_resort = if let Some(last_resort) = keystore
650            .find::<core_crypto_keystore::entities::ProteusPrekey>(
651                Self::last_resort_prekey_id().to_le_bytes().as_slice(),
652            )
653            .await?
654        {
655            proteus_wasm::keys::PreKey::deserialise(&last_resort.prekey).map_err(ProteusError::from)?
656        } else {
657            let last_resort = proteus_wasm::keys::PreKey::last_resort();
658
659            use core_crypto_keystore::CryptoKeystoreProteus as _;
660            keystore
661                .proteus_store_prekey(
662                    Self::last_resort_prekey_id(),
663                    &last_resort.serialise().map_err(ProteusError::from)?,
664                )
665                .await?;
666
667            last_resort
668        };
669
670        let bundle = PreKeyBundle::new(self.proteus_identity.as_ref().public_key.clone(), &last_resort);
671        let bundle = bundle.serialise().map_err(ProteusError::from)?;
672
673        Ok(bundle)
674    }
675
676    /// Proteus identity keypair
677    pub fn identity(&self) -> &IdentityKeyPair {
678        self.proteus_identity.as_ref()
679    }
680
681    /// Proteus Public key hex-encoded fingerprint
682    pub fn fingerprint(&self) -> String {
683        self.proteus_identity.as_ref().public_key.fingerprint()
684    }
685
686    /// Proteus Session local hex-encoded fingerprint
687    ///
688    /// # Errors
689    /// When the session is not found
690    pub(crate) async fn fingerprint_local(
691        &mut self,
692        session_id: &str,
693        keystore: &CryptoKeystore,
694    ) -> CryptoResult<String> {
695        if let Some(session) = self.session(session_id, keystore).await? {
696            Ok(session.read().await.fingerprint_local())
697        } else {
698            Err(CryptoError::ConversationNotFound(session_id.as_bytes().into()))
699        }
700    }
701
702    /// Proteus Session remote hex-encoded fingerprint
703    ///
704    /// # Errors
705    /// When the session is not found
706    pub(crate) async fn fingerprint_remote(
707        &mut self,
708        session_id: &str,
709        keystore: &CryptoKeystore,
710    ) -> CryptoResult<String> {
711        if let Some(session) = self.session(session_id, keystore).await? {
712            Ok(session.read().await.fingerprint_remote())
713        } else {
714            Err(CryptoError::ConversationNotFound(session_id.as_bytes().into()))
715        }
716    }
717
718    /// Hex-encoded fingerprint of the given prekey
719    ///
720    /// # Errors
721    /// If the prekey cannot be deserialized
722    pub fn fingerprint_prekeybundle(prekey: &[u8]) -> CryptoResult<String> {
723        let prekey = PreKeyBundle::deserialise(prekey).map_err(ProteusError::from)?;
724        Ok(prekey.identity_key.fingerprint())
725    }
726
727    /// Cryptobox -> CoreCrypto migration
728    #[cfg_attr(not(feature = "cryptobox-migrate"), allow(unused_variables))]
729    pub(crate) async fn cryptobox_migrate(keystore: &CryptoKeystore, path: &str) -> CryptoResult<()> {
730        cfg_if::cfg_if! {
731            if #[cfg(feature = "cryptobox-migrate")] {
732                Self::cryptobox_migrate_impl(keystore, path).await?;
733                Ok(())
734            } else {
735                Err(CryptoError::ProteusSupportNotEnabled("cryptobox-migrate".into()))
736            }
737        }
738    }
739}
740
741#[cfg(feature = "cryptobox-migrate")]
742#[allow(dead_code)]
743impl ProteusCentral {
744    #[cfg(not(target_family = "wasm"))]
745    async fn cryptobox_migrate_impl(keystore: &CryptoKeystore, path: &str) -> CryptoResult<()> {
746        let root_dir = std::path::PathBuf::from(path);
747
748        if !root_dir.exists() {
749            return Err(crate::CryptoboxMigrationError::ProvidedPathDoesNotExist(path.into()).into());
750        }
751
752        let session_dir = root_dir.join("sessions");
753        let prekey_dir = root_dir.join("prekeys");
754
755        let mut identity = if let Some(store_kp) = keystore.find::<ProteusIdentity>(&[]).await? {
756            Some(Box::new(
757                IdentityKeyPair::from_raw_key_pair(*store_kp.sk_raw(), *store_kp.pk_raw())
758                    .map_err(ProteusError::from)?,
759            ))
760        } else {
761            let identity_dir = root_dir.join("identities");
762
763            let identity = identity_dir.join("local");
764            let legacy_identity = identity_dir.join("local_identity");
765            // Old "local_identity" migration step
766            let identity_check = if legacy_identity.exists() {
767                let kp_cbor = async_fs::read(&legacy_identity).await?;
768                let kp = IdentityKeyPair::deserialise(&kp_cbor).map_err(ProteusError::from)?;
769                Some((Box::new(kp), true))
770            } else if identity.exists() {
771                let kp_cbor = async_fs::read(&identity).await?;
772                let kp = proteus_wasm::identity::Identity::deserialise(&kp_cbor).map_err(ProteusError::from)?;
773                if let proteus_wasm::identity::Identity::Sec(kp) = kp {
774                    Some((kp.into_owned(), false))
775                } else {
776                    None
777                }
778            } else {
779                None
780            };
781
782            if let Some((kp, delete)) = identity_check {
783                let pk = kp.public_key.public_key.as_slice().into();
784
785                let ks_identity = ProteusIdentity {
786                    sk: kp.secret_key.to_keypair_bytes().into(),
787                    pk,
788                };
789
790                keystore.save(ks_identity).await?;
791
792                if delete && legacy_identity.exists() {
793                    async_fs::remove_file(legacy_identity).await?;
794                }
795
796                Some(kp)
797            } else {
798                None
799            }
800        };
801
802        let Some(identity) = identity.take() else {
803            return Err(crate::CryptoboxMigrationError::IdentityNotFound(path.into()).into());
804        };
805        let identity = *identity;
806
807        use futures_lite::stream::StreamExt as _;
808        // Session migration
809        let mut session_entries = async_fs::read_dir(session_dir).await?;
810        while let Some(session_file) = session_entries.try_next().await? {
811            // The name of the file is the session id
812            let proteus_session_id: String = session_file.file_name().to_string_lossy().to_string();
813
814            // If the session is already in store, skip ahead
815            if keystore
816                .find::<ProteusSession>(proteus_session_id.as_bytes())
817                .await?
818                .is_some()
819            {
820                continue;
821            }
822
823            let raw_session = async_fs::read(session_file.path()).await?;
824            // Session integrity check
825            let Ok(_) = Session::deserialise(&identity, &raw_session) else {
826                continue;
827            };
828
829            let keystore_session = ProteusSession {
830                id: proteus_session_id,
831                session: raw_session,
832            };
833
834            keystore.save(keystore_session).await?;
835        }
836
837        // Prekey migration
838        use core_crypto_keystore::entities::ProteusPrekey;
839        let mut prekey_entries = async_fs::read_dir(prekey_dir).await?;
840        while let Some(prekey_file) = prekey_entries.try_next().await? {
841            // The name of the file is the prekey id, so we parse it to get the ID
842            let proteus_prekey_id =
843                proteus_wasm::keys::PreKeyId::new(prekey_file.file_name().to_string_lossy().parse()?);
844
845            // Check if the prekey ID is already existing
846            if keystore
847                .find::<ProteusPrekey>(&proteus_prekey_id.value().to_le_bytes())
848                .await?
849                .is_some()
850            {
851                continue;
852            }
853
854            let raw_prekey = async_fs::read(prekey_file.path()).await?;
855            // Integrity check to see if the PreKey is actually correct
856            if proteus_wasm::keys::PreKey::deserialise(&raw_prekey).is_ok() {
857                let keystore_prekey = ProteusPrekey::from_raw(proteus_prekey_id.value(), raw_prekey);
858                keystore.save(keystore_prekey).await?;
859            }
860        }
861
862        Ok(())
863    }
864
865    #[cfg(target_family = "wasm")]
866    fn get_cbor_bytes_from_map(map: serde_json::map::Map<String, serde_json::Value>) -> CryptoResult<Vec<u8>> {
867        use crate::CryptoboxMigrationError;
868
869        let Some(js_value) = map.get("serialised") else {
870            return Err(CryptoboxMigrationError::MissingKeyInValue("serialised".to_string()).into());
871        };
872
873        let Some(b64_value) = js_value.as_str() else {
874            return Err(CryptoboxMigrationError::WrongValueType("string".to_string()).into());
875        };
876
877        use base64::Engine as _;
878        let cbor_bytes = base64::prelude::BASE64_STANDARD
879            .decode(b64_value)
880            .map_err(CryptoboxMigrationError::from)?;
881        Ok(cbor_bytes)
882    }
883
884    #[cfg(target_family = "wasm")]
885    async fn cryptobox_migrate_impl(keystore: &CryptoKeystore, path: &str) -> CryptoResult<()> {
886        use rexie::{Rexie, TransactionMode};
887
888        use crate::CryptoboxMigrationError;
889        let local_identity_key = "local_identity";
890        let local_identity_store_name = "keys";
891        let prekeys_store_name = "prekeys";
892        let sessions_store_name = "sessions";
893
894        // Path should be following this logic: https://github.com/wireapp/wire-web-packages/blob/main/packages/core/src/main/Account.ts#L645
895        let db = Rexie::builder(path)
896            .build()
897            .await
898            .map_err(CryptoboxMigrationError::from)?;
899
900        let store_names = db.store_names();
901
902        let expected_stores = &[prekeys_store_name, sessions_store_name, local_identity_store_name];
903
904        if !expected_stores
905            .iter()
906            .map(|s| s.to_string())
907            .all(|s| store_names.contains(&s))
908        {
909            return Err(crate::CryptoboxMigrationError::ProvidedPathDoesNotExist(path.into()).into());
910        }
911
912        let mut proteus_identity = if let Some(store_kp) = keystore.find::<ProteusIdentity>(&[]).await? {
913            Some(
914                proteus_wasm::keys::IdentityKeyPair::from_raw_key_pair(*store_kp.sk_raw(), *store_kp.pk_raw())
915                    .map_err(ProteusError::from)?,
916            )
917        } else {
918            let transaction = db
919                .transaction(&[local_identity_store_name], TransactionMode::ReadOnly)
920                .map_err(CryptoboxMigrationError::from)?;
921
922            let identity_store = transaction
923                .store(local_identity_store_name)
924                .map_err(CryptoboxMigrationError::from)?;
925
926            if let Some(cryptobox_js_value) = identity_store
927                .get(local_identity_key.into())
928                .await
929                .map_err(CryptoboxMigrationError::from)?
930            {
931                let js_value: serde_json::map::Map<String, serde_json::Value> =
932                    serde_wasm_bindgen::from_value(cryptobox_js_value).map_err(CryptoboxMigrationError::from)?;
933
934                let kp_cbor = Self::get_cbor_bytes_from_map(js_value)?;
935
936                let kp = proteus_wasm::keys::IdentityKeyPair::deserialise(&kp_cbor).map_err(ProteusError::from)?;
937
938                let pk = kp.public_key.public_key.as_slice().to_vec();
939
940                let ks_identity = ProteusIdentity {
941                    sk: kp.secret_key.to_keypair_bytes().into(),
942                    pk,
943                };
944                keystore.save(ks_identity).await?;
945
946                Some(kp)
947            } else {
948                None
949            }
950        };
951
952        let Some(proteus_identity) = proteus_identity.take() else {
953            return Err(crate::CryptoboxMigrationError::IdentityNotFound(path.into()).into());
954        };
955
956        if store_names.contains(&sessions_store_name.to_string()) {
957            let transaction = db
958                .transaction(&[sessions_store_name], TransactionMode::ReadOnly)
959                .map_err(CryptoboxMigrationError::from)?;
960
961            let sessions_store = transaction
962                .store(sessions_store_name)
963                .map_err(CryptoboxMigrationError::from)?;
964
965            let sessions = sessions_store
966                .scan(None, None, None, None)
967                .await
968                .map_err(CryptoboxMigrationError::from)?;
969
970            for (session_id, session_js_value) in sessions.into_iter().map(|(k, v)| (k.as_string().unwrap(), v)) {
971                // If the session is already in store, skip ahead
972                if keystore.find::<ProteusSession>(session_id.as_bytes()).await?.is_some() {
973                    continue;
974                }
975
976                let js_value: serde_json::map::Map<String, serde_json::Value> =
977                    serde_wasm_bindgen::from_value(session_js_value).map_err(CryptoboxMigrationError::from)?;
978
979                let session_cbor_bytes = Self::get_cbor_bytes_from_map(js_value)?;
980
981                // Integrity check
982                if proteus_wasm::session::Session::deserialise(&proteus_identity, &session_cbor_bytes).is_ok() {
983                    let keystore_session = ProteusSession {
984                        id: session_id,
985                        session: session_cbor_bytes,
986                    };
987
988                    keystore.save(keystore_session).await?;
989                }
990            }
991        }
992
993        if store_names.contains(&prekeys_store_name.to_string()) {
994            use core_crypto_keystore::entities::ProteusPrekey;
995
996            let transaction = db
997                .transaction(&[prekeys_store_name], TransactionMode::ReadOnly)
998                .map_err(CryptoboxMigrationError::from)?;
999
1000            let prekeys_store = transaction
1001                .store(prekeys_store_name)
1002                .map_err(CryptoboxMigrationError::from)?;
1003
1004            let prekeys = prekeys_store
1005                .scan(None, None, None, None)
1006                .await
1007                .map_err(CryptoboxMigrationError::from)?;
1008
1009            for (prekey_id, prekey_js_value) in prekeys
1010                .into_iter()
1011                .map(|(id, prekey_js_value)| (id.as_string().unwrap(), prekey_js_value))
1012            {
1013                let prekey_id: u16 = prekey_id.parse()?;
1014
1015                // Check if the prekey ID is already existing
1016                if keystore
1017                    .find::<ProteusPrekey>(&prekey_id.to_le_bytes())
1018                    .await?
1019                    .is_some()
1020                {
1021                    continue;
1022                }
1023
1024                let js_value: serde_json::map::Map<String, serde_json::Value> =
1025                    serde_wasm_bindgen::from_value(prekey_js_value).map_err(CryptoboxMigrationError::from)?;
1026
1027                let raw_prekey_cbor = Self::get_cbor_bytes_from_map(js_value)?;
1028
1029                // Integrity check to see if the PreKey is actually correct
1030                if proteus_wasm::keys::PreKey::deserialise(&raw_prekey_cbor).is_ok() {
1031                    let keystore_prekey = ProteusPrekey::from_raw(prekey_id, raw_prekey_cbor);
1032                    keystore.save(keystore_prekey).await?;
1033                }
1034            }
1035        }
1036
1037        Ok(())
1038    }
1039}
1040
1041#[cfg(test)]
1042mod tests {
1043    use crate::{
1044        prelude::{CertificateBundle, ClientIdentifier, MlsCentral, MlsCentralConfiguration, MlsCredentialType},
1045        test_utils::{proteus_utils::*, x509::X509TestChain, *},
1046    };
1047
1048    use crate::prelude::INITIAL_KEYING_MATERIAL_COUNT;
1049    use proteus_traits::PreKeyStore;
1050    use wasm_bindgen_test::*;
1051
1052    use super::*;
1053
1054    wasm_bindgen_test_configure!(run_in_browser);
1055
1056    #[apply(all_cred_cipher)]
1057    #[wasm_bindgen_test]
1058    async fn cc_can_init(case: TestCase) {
1059        #[cfg(not(target_family = "wasm"))]
1060        let (path, db_file) = tmp_db_file();
1061        #[cfg(target_family = "wasm")]
1062        let (path, _) = tmp_db_file();
1063        let client_id = "alice".into();
1064        let cfg = MlsCentralConfiguration::try_new(
1065            path,
1066            "test".to_string(),
1067            Some(client_id),
1068            vec![case.ciphersuite()],
1069            None,
1070            Some(INITIAL_KEYING_MATERIAL_COUNT),
1071        )
1072        .unwrap();
1073        let cc: CoreCrypto = MlsCentral::try_new(cfg).await.unwrap().into();
1074        let context = cc.new_transaction().await.unwrap();
1075        assert!(context.proteus_init().await.is_ok());
1076        assert!(context.proteus_new_prekey(1).await.is_ok());
1077        context.finish().await.unwrap();
1078        #[cfg(not(target_family = "wasm"))]
1079        drop(db_file);
1080    }
1081
1082    #[apply(all_cred_cipher)]
1083    #[wasm_bindgen_test]
1084    async fn cc_can_2_phase_init(case: TestCase) {
1085        #[cfg(not(target_family = "wasm"))]
1086        let (path, db_file) = tmp_db_file();
1087        #[cfg(target_family = "wasm")]
1088        let (path, _) = tmp_db_file();
1089        // we are deferring MLS initialization here, not passing a MLS 'client_id' yet
1090        let cfg = MlsCentralConfiguration::try_new(
1091            path,
1092            "test".to_string(),
1093            None,
1094            vec![case.ciphersuite()],
1095            None,
1096            Some(INITIAL_KEYING_MATERIAL_COUNT),
1097        )
1098        .unwrap();
1099        let cc: CoreCrypto = MlsCentral::try_new(cfg).await.unwrap().into();
1100        let transaction = cc.new_transaction().await.unwrap();
1101        let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
1102        x509_test_chain.register_with_central(&transaction).await;
1103        assert!(transaction.proteus_init().await.is_ok());
1104        // proteus is initialized, prekeys can be generated
1105        assert!(transaction.proteus_new_prekey(1).await.is_ok());
1106        // 👇 and so a unique 'client_id' can be fetched from wire-server
1107        let client_id = "alice";
1108        let identifier = match case.credential_type {
1109            MlsCredentialType::Basic => ClientIdentifier::Basic(client_id.into()),
1110            MlsCredentialType::X509 => {
1111                CertificateBundle::rand_identifier(client_id, &[x509_test_chain.find_local_intermediate_ca()])
1112            }
1113        };
1114        transaction
1115            .mls_init(
1116                identifier,
1117                vec![case.ciphersuite()],
1118                Some(INITIAL_KEYING_MATERIAL_COUNT),
1119            )
1120            .await
1121            .unwrap();
1122        // expect MLS to work
1123        assert_eq!(
1124            transaction
1125                .get_or_create_client_keypackages(case.ciphersuite(), case.credential_type, 2)
1126                .await
1127                .unwrap()
1128                .len(),
1129            2
1130        );
1131        #[cfg(not(target_family = "wasm"))]
1132        drop(db_file);
1133    }
1134
1135    #[async_std::test]
1136    #[wasm_bindgen_test]
1137    async fn can_init() {
1138        #[cfg(not(target_family = "wasm"))]
1139        let (path, db_file) = tmp_db_file();
1140        #[cfg(target_family = "wasm")]
1141        let (path, _) = tmp_db_file();
1142        let keystore = core_crypto_keystore::Connection::open_with_key(&path, "test")
1143            .await
1144            .unwrap();
1145        keystore.new_transaction().await.unwrap();
1146        let central = ProteusCentral::try_new(&keystore).await.unwrap();
1147        let identity = (*central.proteus_identity).clone();
1148        keystore.commit_transaction().await.unwrap();
1149
1150        let keystore = core_crypto_keystore::Connection::open_with_key(path, "test")
1151            .await
1152            .unwrap();
1153        keystore.new_transaction().await.unwrap();
1154        let central = ProteusCentral::try_new(&keystore).await.unwrap();
1155        keystore.commit_transaction().await.unwrap();
1156        assert_eq!(identity, *central.proteus_identity);
1157
1158        keystore.wipe().await.unwrap();
1159        #[cfg(not(target_family = "wasm"))]
1160        drop(db_file);
1161    }
1162
1163    #[async_std::test]
1164    #[wasm_bindgen_test]
1165    async fn can_talk_with_proteus() {
1166        #[cfg(not(target_family = "wasm"))]
1167        let (path, db_file) = tmp_db_file();
1168        #[cfg(target_family = "wasm")]
1169        let (path, _) = tmp_db_file();
1170
1171        let session_id = uuid::Uuid::new_v4().hyphenated().to_string();
1172
1173        let mut keystore = core_crypto_keystore::Connection::open_with_key(path, "test")
1174            .await
1175            .unwrap();
1176        keystore.new_transaction().await.unwrap();
1177
1178        let mut alice = ProteusCentral::try_new(&keystore).await.unwrap();
1179
1180        let mut bob = CryptoboxLike::init();
1181        let bob_pk_bundle = bob.new_prekey();
1182
1183        alice
1184            .session_from_prekey(&session_id, &bob_pk_bundle.serialise().unwrap())
1185            .await
1186            .unwrap();
1187
1188        let message = b"Hello world";
1189
1190        let encrypted = alice.encrypt(&mut keystore, &session_id, message).await.unwrap();
1191        let decrypted = bob.decrypt(&session_id, &encrypted).await;
1192        assert_eq!(decrypted, message);
1193
1194        let encrypted = bob.encrypt(&session_id, message);
1195        let decrypted = alice.decrypt(&mut keystore, &session_id, &encrypted).await.unwrap();
1196        assert_eq!(decrypted, message);
1197
1198        keystore.commit_transaction().await.unwrap();
1199        keystore.wipe().await.unwrap();
1200        #[cfg(not(target_family = "wasm"))]
1201        drop(db_file);
1202    }
1203
1204    #[async_std::test]
1205    #[wasm_bindgen_test]
1206    async fn can_produce_proteus_consumed_prekeys() {
1207        #[cfg(not(target_family = "wasm"))]
1208        let (path, db_file) = tmp_db_file();
1209        #[cfg(target_family = "wasm")]
1210        let (path, _) = tmp_db_file();
1211
1212        let session_id = uuid::Uuid::new_v4().hyphenated().to_string();
1213
1214        let mut keystore = core_crypto_keystore::Connection::open_with_key(path, "test")
1215            .await
1216            .unwrap();
1217        keystore.new_transaction().await.unwrap();
1218        let mut alice = ProteusCentral::try_new(&keystore).await.unwrap();
1219
1220        let mut bob = CryptoboxLike::init();
1221
1222        let alice_prekey_bundle_ser = alice.new_prekey(1, &keystore).await.unwrap();
1223
1224        bob.init_session_from_prekey_bundle(&session_id, &alice_prekey_bundle_ser);
1225        let message = b"Hello world!";
1226        let encrypted = bob.encrypt(&session_id, message);
1227
1228        let (_, decrypted) = alice
1229            .session_from_message(&mut keystore, &session_id, &encrypted)
1230            .await
1231            .unwrap();
1232
1233        assert_eq!(message, decrypted.as_slice());
1234
1235        let encrypted = alice.encrypt(&mut keystore, &session_id, message).await.unwrap();
1236        let decrypted = bob.decrypt(&session_id, &encrypted).await;
1237
1238        assert_eq!(message, decrypted.as_slice());
1239        keystore.commit_transaction().await.unwrap();
1240        keystore.wipe().await.unwrap();
1241        #[cfg(not(target_family = "wasm"))]
1242        drop(db_file);
1243    }
1244
1245    #[async_std::test]
1246    #[wasm_bindgen_test]
1247    async fn auto_prekeys_are_sequential() {
1248        use core_crypto_keystore::entities::ProteusPrekey;
1249        const GAP_AMOUNT: u16 = 5;
1250        const ID_TEST_RANGE: std::ops::RangeInclusive<u16> = 1..=30;
1251
1252        #[cfg(not(target_family = "wasm"))]
1253        let (path, db_file) = tmp_db_file();
1254        #[cfg(target_family = "wasm")]
1255        let (path, _) = tmp_db_file();
1256
1257        let keystore = core_crypto_keystore::Connection::open_with_key(path, "test")
1258            .await
1259            .unwrap();
1260        keystore.new_transaction().await.unwrap();
1261        let alice = ProteusCentral::try_new(&keystore).await.unwrap();
1262
1263        for i in ID_TEST_RANGE {
1264            let (pk_id, pkb) = alice.new_prekey_auto(&keystore).await.unwrap();
1265            assert_eq!(i, pk_id);
1266            let prekey = proteus_wasm::keys::PreKeyBundle::deserialise(&pkb).unwrap();
1267            assert_eq!(prekey.prekey_id.value(), pk_id);
1268        }
1269
1270        use rand::Rng as _;
1271        let mut rng = rand::thread_rng();
1272        let mut gap_ids: Vec<u16> = (0..GAP_AMOUNT).map(|_| rng.gen_range(ID_TEST_RANGE)).collect();
1273        gap_ids.sort();
1274        gap_ids.dedup();
1275        while gap_ids.len() < GAP_AMOUNT as usize {
1276            gap_ids.push(rng.gen_range(ID_TEST_RANGE));
1277            gap_ids.sort();
1278            gap_ids.dedup();
1279        }
1280        for gap_id in gap_ids.iter() {
1281            keystore.remove::<ProteusPrekey, _>(gap_id.to_le_bytes()).await.unwrap();
1282        }
1283
1284        gap_ids.sort();
1285
1286        for gap_id in gap_ids.iter() {
1287            let (pk_id, pkb) = alice.new_prekey_auto(&keystore).await.unwrap();
1288            assert_eq!(pk_id, *gap_id);
1289            let prekey = proteus_wasm::keys::PreKeyBundle::deserialise(&pkb).unwrap();
1290            assert_eq!(prekey.prekey_id.value(), *gap_id);
1291        }
1292
1293        let mut gap_ids: Vec<u16> = (0..GAP_AMOUNT).map(|_| rng.gen_range(ID_TEST_RANGE)).collect();
1294        gap_ids.sort();
1295        gap_ids.dedup();
1296        while gap_ids.len() < GAP_AMOUNT as usize {
1297            gap_ids.push(rng.gen_range(ID_TEST_RANGE));
1298            gap_ids.sort();
1299            gap_ids.dedup();
1300        }
1301        for gap_id in gap_ids.iter() {
1302            keystore.remove::<ProteusPrekey, _>(gap_id.to_le_bytes()).await.unwrap();
1303        }
1304
1305        let potential_range = *ID_TEST_RANGE.end()..=(*ID_TEST_RANGE.end() * 2);
1306        let potential_range_check = potential_range.clone();
1307        for _ in potential_range {
1308            let (pk_id, pkb) = alice.new_prekey_auto(&keystore).await.unwrap();
1309            assert!(gap_ids.contains(&pk_id) || potential_range_check.contains(&pk_id));
1310            let prekey = proteus_wasm::keys::PreKeyBundle::deserialise(&pkb).unwrap();
1311            assert_eq!(prekey.prekey_id.value(), pk_id);
1312        }
1313        keystore.commit_transaction().await.unwrap();
1314        keystore.wipe().await.unwrap();
1315        #[cfg(not(target_family = "wasm"))]
1316        drop(db_file);
1317    }
1318
1319    #[cfg(all(feature = "cryptobox-migrate", not(target_family = "wasm")))]
1320    #[async_std::test]
1321    async fn can_import_cryptobox() {
1322        let session_id = uuid::Uuid::new_v4().hyphenated().to_string();
1323
1324        let cryptobox_folder = tempfile::tempdir().unwrap();
1325        let alice = cryptobox::CBox::file_open(cryptobox_folder.path()).unwrap();
1326        let alice_fingerprint = alice.fingerprint();
1327
1328        let mut bob = CryptoboxLike::init();
1329        let bob_pk_bundle = bob.new_prekey();
1330
1331        let alice_pk_id = proteus::keys::PreKeyId::new(1u16);
1332        let alice_pk = alice.new_prekey(alice_pk_id).unwrap();
1333
1334        let mut alice_session = alice
1335            .session_from_prekey(session_id.clone(), &bob_pk_bundle.serialise().unwrap())
1336            .unwrap();
1337
1338        let message = b"Hello world!";
1339
1340        let alice_msg_envelope = alice_session.encrypt(message).unwrap();
1341        let decrypted = bob.decrypt(&session_id, &alice_msg_envelope).await;
1342        assert_eq!(decrypted, message);
1343
1344        alice.session_save(&mut alice_session).unwrap();
1345
1346        let encrypted = bob.encrypt(&session_id, &message[..]);
1347        let decrypted = alice_session.decrypt(&encrypted).unwrap();
1348        assert_eq!(decrypted, message);
1349
1350        alice.session_save(&mut alice_session).unwrap();
1351
1352        drop(alice);
1353
1354        let keystore_dir = tempfile::tempdir().unwrap();
1355        let keystore_file = keystore_dir.path().join("keystore");
1356
1357        let mut keystore =
1358            core_crypto_keystore::Connection::open_with_key(keystore_file.as_os_str().to_string_lossy(), "test")
1359                .await
1360                .unwrap();
1361        keystore.new_transaction().await.unwrap();
1362
1363        let Err(crate::CryptoError::CryptoboxMigrationError(crate::CryptoboxMigrationError::ProvidedPathDoesNotExist(
1364            _,
1365        ))) = ProteusCentral::cryptobox_migrate(&keystore, "invalid path").await
1366        else {
1367            panic!("ProteusCentral::cryptobox_migrate did not throw an error on invalid path");
1368        };
1369
1370        ProteusCentral::cryptobox_migrate(&keystore, &cryptobox_folder.path().to_string_lossy())
1371            .await
1372            .unwrap();
1373
1374        let mut proteus_central = ProteusCentral::try_new(&keystore).await.unwrap();
1375
1376        // Identity check
1377        assert_eq!(proteus_central.fingerprint(), alice_fingerprint);
1378
1379        // Session integrity check
1380        let alice_new_session_lock = proteus_central
1381            .session(&session_id, &mut keystore)
1382            .await
1383            .unwrap()
1384            .unwrap();
1385        let alice_new_session = alice_new_session_lock.read().await;
1386        assert_eq!(
1387            alice_new_session.session.local_identity().fingerprint(),
1388            alice_session.fingerprint_local()
1389        );
1390        assert_eq!(
1391            alice_new_session.session.remote_identity().fingerprint(),
1392            alice_session.fingerprint_remote()
1393        );
1394
1395        drop(alice_new_session);
1396        drop(alice_new_session_lock);
1397
1398        // Prekey integrity check
1399        let keystore_pk = keystore.prekey(1).await.unwrap().unwrap();
1400        let keystore_pk = proteus_wasm::keys::PreKey::deserialise(&keystore_pk).unwrap();
1401        assert_eq!(alice_pk.prekey_id.value(), keystore_pk.key_id.value());
1402        assert_eq!(
1403            alice_pk.public_key.fingerprint(),
1404            keystore_pk.key_pair.public_key.fingerprint()
1405        );
1406
1407        // Make sure ProteusCentral can still keep communicating with bob
1408        let encrypted = proteus_central
1409            .encrypt(&mut keystore, &session_id, &message[..])
1410            .await
1411            .unwrap();
1412        let decrypted = bob.decrypt(&session_id, &encrypted).await;
1413
1414        assert_eq!(&decrypted, &message[..]);
1415
1416        // CL-110 assertion
1417        // Happens when a migrated client ratchets just after migration. It does not happen when ratchet is not required
1418        // Having alice(A), bob(B) and migration(M), you can reproduce this behaviour with `[A->B][B->A] M [A->B][B->A]`
1419        // However you won't reproduce it like this because migrated alice does not ratchet `[A->B][B->A] M [B->A][A->B]`
1420        let encrypted = bob.encrypt(&session_id, &message[..]);
1421        let decrypted = proteus_central
1422            .decrypt(&mut keystore, &session_id, &encrypted)
1423            .await
1424            .unwrap();
1425        assert_eq!(&decrypted, &message[..]);
1426
1427        proteus_central.session_save(&mut keystore, &session_id).await.unwrap();
1428        keystore.commit_transaction().await.unwrap();
1429        keystore.wipe().await.unwrap();
1430    }
1431
1432    cfg_if::cfg_if! {
1433        if #[cfg(all(feature = "cryptobox-migrate", target_family = "wasm"))] {
1434            // use wasm_bindgen::prelude::*;
1435            const CRYPTOBOX_JS_DBNAME: &str = "cryptobox-migrate-test";
1436            // wasm-bindgen-test-runner is behaving weird with inline_js stuff (aka not working basically), which we had previously
1437            // So instead we emulate how cryptobox-js works
1438            // Returns Promise<JsString>
1439            fn run_cryptobox(alice: CryptoboxLike) -> js_sys::Promise {
1440                wasm_bindgen_futures::future_to_promise(async move {
1441                    use rexie::{Rexie, ObjectStore, TransactionMode};
1442                    use wasm_bindgen::JsValue;
1443
1444                    // Delete the maybe past database to make sure we start fresh
1445                    Rexie::builder(CRYPTOBOX_JS_DBNAME)
1446                        .delete()
1447                        .await.map_err(|err| err.to_string())?;
1448
1449                    let rexie = Rexie::builder(CRYPTOBOX_JS_DBNAME)
1450                        .version(1)
1451                        .add_object_store(ObjectStore::new("keys").auto_increment(false))
1452                        .add_object_store(ObjectStore::new("prekeys").auto_increment(false))
1453                        .add_object_store(ObjectStore::new("sessions").auto_increment(false))
1454                        .build()
1455                        .await.map_err(|err| err.to_string())?;
1456
1457                    // Add identity key
1458                    let transaction = rexie.transaction(&["keys"], TransactionMode::ReadWrite).map_err(|err| err.to_string())?;
1459                    let store = transaction.store("keys").map_err(|err| err.to_string())?;
1460
1461                    use base64::Engine as _;
1462                    let json = serde_json::json!({
1463                        "created": 0,
1464                        "id": "local_identity",
1465                        "serialised": base64::prelude::BASE64_STANDARD.encode(alice.identity.serialise().unwrap()),
1466                        "version": "1.0"
1467                    });
1468                    let js_value = serde_wasm_bindgen::to_value(&json)?;
1469
1470                    store.add(&js_value, Some(&JsValue::from_str("local_identity"))).await.map_err(|err| err.to_string())?;
1471
1472                    // Add prekeys
1473                    let transaction = rexie.transaction(&["prekeys"], TransactionMode::ReadWrite).map_err(|err| err.to_string())?;
1474                    let store = transaction.store("prekeys").map_err(|err| err.to_string())?;
1475                    for prekey in alice.prekeys.0.into_iter() {
1476                        let id = prekey.key_id.value().to_string();
1477                        let json = serde_json::json!({
1478                            "created": 0,
1479                            "id": &id,
1480                            "serialised": base64::prelude::BASE64_STANDARD.encode(prekey.serialise().unwrap()),
1481                            "version": "1.0"
1482                        });
1483                        let js_value = serde_wasm_bindgen::to_value(&json)?;
1484                        store.add(&js_value, Some(&JsValue::from_str(&id))).await.map_err(|err| err.to_string())?;
1485                    }
1486
1487                    // Add sessions
1488                    let transaction = rexie.transaction(&["sessions"], TransactionMode::ReadWrite).map_err(|err| err.to_string())?;
1489                    let store = transaction.store("sessions").map_err(|err| err.to_string())?;
1490                    for (session_id, session) in alice.sessions.into_iter() {
1491                        let json = serde_json::json!({
1492                            "created": 0,
1493                            "id": session_id,
1494                            "serialised": base64::prelude::BASE64_STANDARD.encode(session.serialise().unwrap()),
1495                            "version": "1.0"
1496                        });
1497
1498                        let js_value = serde_wasm_bindgen::to_value(&json)?;
1499                        store.add(&js_value, Some(&JsValue::from_str(&session_id))).await.map_err(|err| err.to_string())?;
1500                    }
1501
1502                    Ok(JsValue::UNDEFINED)
1503                })
1504            }
1505
1506            #[wasm_bindgen_test]
1507            async fn can_import_cryptobox() {
1508                let session_id = uuid::Uuid::new_v4().hyphenated().to_string();
1509
1510                let mut alice = CryptoboxLike::init();
1511                let alice_fingerprint = alice.fingerprint();
1512                const PREKEY_COUNT: usize = 10;
1513                let prekey_iter_range = 0..PREKEY_COUNT;
1514                // Save prekey bundles for later to check if they're the same after migration
1515                let prekey_bundles: Vec<proteus_wasm::keys::PreKeyBundle> = prekey_iter_range.clone().map(|_| alice.new_prekey()).collect();
1516
1517                // Ensure alice and bob can communicate before migration
1518                let mut bob = CryptoboxLike::init();
1519                let bob_pk_bundle = bob.new_prekey();
1520                let message = b"Hello world!";
1521
1522                alice.init_session_from_prekey_bundle(&session_id, &bob_pk_bundle.serialise().unwrap());
1523                let alice_to_bob_message = alice.encrypt(&session_id, message);
1524                let decrypted = bob.decrypt(&session_id, &alice_to_bob_message).await;
1525                assert_eq!(&message[..], decrypted.as_slice());
1526
1527                let bob_to_alice_message = bob.encrypt(&session_id, message);
1528                let decrypted = alice.decrypt(&session_id, &bob_to_alice_message).await;
1529                assert_eq!(&message[..], decrypted.as_slice());
1530
1531                let alice_session = alice.session(&session_id);
1532                let alice_session_fingerprint_local = alice_session.local_identity().fingerprint();
1533                let alice_session_fingerprint_remote = alice_session.remote_identity().fingerprint();
1534
1535                let _ = wasm_bindgen_futures::JsFuture::from(run_cryptobox(alice)).await.unwrap();
1536                let mut keystore = core_crypto_keystore::Connection::open_with_key(&format!("{CRYPTOBOX_JS_DBNAME}-imported"), "test").await.unwrap();
1537                keystore.new_transaction().await.unwrap();
1538                let Err(crate::CryptoError::CryptoboxMigrationError(crate::CryptoboxMigrationError::ProvidedPathDoesNotExist(_))) = ProteusCentral::cryptobox_migrate(&keystore, "invalid path").await else {
1539                    panic!("ProteusCentral::cryptobox_migrate did not throw an error on invalid path");
1540                };
1541
1542                ProteusCentral::cryptobox_migrate(&keystore, CRYPTOBOX_JS_DBNAME).await.unwrap();
1543
1544                let mut proteus_central = ProteusCentral::try_new(&keystore).await.unwrap();
1545
1546                // Identity check
1547                assert_eq!(proteus_central.fingerprint(), alice_fingerprint);
1548
1549                // Session integrity check
1550                let alice_new_session_lock = proteus_central
1551                    .session(&session_id, &mut keystore)
1552                    .await
1553                    .unwrap()
1554                    .unwrap();
1555                let alice_new_session = alice_new_session_lock.read().await;
1556                assert_eq!(
1557                    alice_new_session.session.local_identity().fingerprint(),
1558                    alice_session_fingerprint_local
1559                );
1560                assert_eq!(
1561                    alice_new_session.session.remote_identity().fingerprint(),
1562                    alice_session_fingerprint_remote
1563                );
1564
1565                drop(alice_new_session);
1566                drop(alice_new_session_lock);
1567
1568                // Prekey integrity check
1569                for i in prekey_iter_range {
1570                    let prekey_id = (i + 1) as u16;
1571                    let keystore_pk = keystore.prekey(prekey_id).await.unwrap().unwrap();
1572                    let keystore_pk = proteus_wasm::keys::PreKey::deserialise(&keystore_pk).unwrap();
1573                    let alice_pk = &prekey_bundles[i];
1574
1575                    assert_eq!(alice_pk.prekey_id.value(), keystore_pk.key_id.value());
1576                    assert_eq!(
1577                        alice_pk.public_key.fingerprint(),
1578                        keystore_pk.key_pair.public_key.fingerprint()
1579                    );
1580                }
1581
1582
1583                // Make sure ProteusCentral can still keep communicating with bob
1584                let encrypted = proteus_central.encrypt(&mut keystore, &session_id, &message[..]).await.unwrap();
1585                let decrypted = bob.decrypt(&session_id, &encrypted).await;
1586
1587                assert_eq!(&decrypted, &message[..]);
1588
1589                // CL-110 assertion
1590                let encrypted = bob.encrypt(&session_id, &message[..]);
1591                let decrypted = proteus_central
1592                    .decrypt(&mut keystore, &session_id, &encrypted)
1593                    .await
1594                    .unwrap();
1595                assert_eq!(&decrypted, &message[..]);
1596                keystore.commit_transaction().await.unwrap();
1597
1598                keystore.wipe().await.unwrap();
1599            }
1600        }
1601    }
1602}