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::{
18    CoreCrypto, Error, KeystoreError, LeafError, ProteusError, Result,
19    context::CentralContext,
20    group_store::{GroupStore, GroupStoreValue},
21};
22use core_crypto_keystore::{
23    Connection as CryptoKeystore,
24    connection::FetchFromDatabase,
25    entities::{ProteusIdentity, ProteusSession},
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]) -> Result<Vec<u8>> {
47        self.session
48            .encrypt(plaintext)
49            .and_then(|e| e.serialise())
50            .map_err(ProteusError::wrap("encrypting message for proteus session"))
51            .map_err(Into::into)
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    ) -> Result<Vec<u8>> {
60        let envelope = Envelope::deserialise(ciphertext).map_err(ProteusError::wrap("deserializing envelope"))?;
61        self.session
62            .decrypt(store, &envelope)
63            .await
64            .map_err(ProteusError::wrap("decrypting message for proteus session"))
65            .map_err(Into::into)
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 [CentralContext::proteus_init] first or an error will be returned
88    pub async fn proteus_session(
89        &self,
90        session_id: &str,
91    ) -> Result<Option<GroupStoreValue<ProteusConversationSession>>> {
92        let mut mutex = self.proteus.lock().await;
93        let proteus = mutex.as_mut().ok_or(Error::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 [CentralContext::proteus_init] first or an error will be returned
101    pub async fn proteus_session_exists(&self, session_id: &str) -> Result<bool> {
102        let mut mutex = self.proteus.lock().await;
103        let proteus = mutex.as_mut().ok_or(Error::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 [CentralContext::proteus_init] first or an error will be returned
116    pub async fn proteus_fingerprint(&self) -> Result<String> {
117        let mutex = self.proteus.lock().await;
118        let proteus = mutex.as_ref().ok_or(Error::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 [CentralContext::proteus_init] first or an error will be returned
125    pub async fn proteus_fingerprint_local(&self, session_id: &str) -> Result<String> {
126        let mut mutex = self.proteus.lock().await;
127        let proteus = mutex.as_mut().ok_or(Error::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 [CentralContext::proteus_init] first or an error will be returned
135    pub async fn proteus_fingerprint_remote(&self, session_id: &str) -> Result<String> {
136        let mut mutex = self.proteus.lock().await;
137        let proteus = mutex.as_mut().ok_or(Error::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) -> Result<()> {
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 [CentralContext::proteus_init] first or it will do nothing
161    pub async fn proteus_reload_sessions(&self) -> Result<()> {
162        let arc = self.proteus_central().await?;
163        let mut mutex = arc.lock().await;
164        let proteus = mutex.as_mut().ok_or(Error::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 [CentralContext::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    ) -> Result<GroupStoreValue<ProteusConversationSession>> {
177        let arc = self.proteus_central().await?;
178        let mut mutex = arc.lock().await;
179        let proteus = mutex.as_mut().ok_or(Error::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 [CentralContext::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    ) -> Result<(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(Error::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 [CentralContext::proteus_init] first or an error will be returned
210    pub async fn proteus_session_save(&self, session_id: &str) -> Result<()> {
211        let arc = self.proteus_central().await?;
212        let mut mutex = arc.lock().await;
213        let proteus = mutex.as_mut().ok_or(Error::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 [CentralContext::proteus_init] first or an error will be returned
221    pub async fn proteus_session_delete(&self, session_id: &str) -> Result<()> {
222        let arc = self.proteus_central().await?;
223        let mut mutex = arc.lock().await;
224        let proteus = mutex.as_mut().ok_or(Error::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 [CentralContext::proteus_init] first or an error will be returned
232    pub async fn proteus_session(
233        &self,
234        session_id: &str,
235    ) -> Result<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(Error::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 [CentralContext::proteus_init] first or an error will be returned
246    pub async fn proteus_session_exists(&self, session_id: &str) -> Result<bool> {
247        let arc = self.proteus_central().await?;
248        let mut mutex = arc.lock().await;
249        let proteus = mutex.as_mut().ok_or(Error::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 [CentralContext::proteus_init] first or an error will be returned
257    pub async fn proteus_decrypt(&self, session_id: &str, ciphertext: &[u8]) -> Result<Vec<u8>> {
258        let arc = self.proteus_central().await?;
259        let mut mutex = arc.lock().await;
260        let proteus = mutex.as_mut().ok_or(Error::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 [CentralContext::proteus_init] first or an error will be returned
268    pub async fn proteus_encrypt(&self, session_id: &str, plaintext: &[u8]) -> Result<Vec<u8>> {
269        let arc = self.proteus_central().await?;
270        let mut mutex = arc.lock().await;
271        let proteus = mutex.as_mut().ok_or(Error::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 [CentralContext::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    ) -> Result<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(Error::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 [CentralContext::proteus_init] first or an error will be returned
295    pub async fn proteus_new_prekey(&self, prekey_id: u16) -> Result<Vec<u8>> {
296        let arc = self.proteus_central().await?;
297        let mut mutex = arc.lock().await;
298        let proteus = mutex.as_mut().ok_or(Error::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 [CentralContext::proteus_init] first or an error will be returned
306    pub async fn proteus_new_prekey_auto(&self) -> Result<(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(Error::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) -> Result<Vec<u8>> {
316        let arc = self.proteus_central().await?;
317        let mut mutex = arc.lock().await;
318        let proteus = mutex.as_mut().ok_or(Error::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 [CentralContext::proteus_init] first or an error will be returned
332    pub async fn proteus_fingerprint(&self) -> Result<String> {
333        let arc = self.proteus_central().await?;
334        let mut mutex = arc.lock().await;
335        let proteus = mutex.as_mut().ok_or(Error::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 [CentralContext::proteus_init] first or an error will be returned
342    pub async fn proteus_fingerprint_local(&self, session_id: &str) -> Result<String> {
343        let arc = self.proteus_central().await?;
344        let mut mutex = arc.lock().await;
345        let proteus = mutex.as_mut().ok_or(Error::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 [CentralContext::proteus_init] first or an error will be returned
353    pub async fn proteus_fingerprint_remote(&self, session_id: &str) -> Result<String> {
354        let arc = self.proteus_central().await?;
355        let mut mutex = arc.lock().await;
356        let proteus = mutex.as_mut().ok_or(Error::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 [CentralContext::proteus_init]
364    pub async fn proteus_cryptobox_migrate(&self, path: &str) -> Result<()> {
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) -> Result<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) -> Result<()> {
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) -> Result<IdentityKeyPair> {
401        let Some(identity) = keystore
402            .find::<ProteusIdentity>(&[])
403            .await
404            .map_err(KeystoreError::wrap("finding proteus identity"))?
405        else {
406            return Self::create_identity(keystore).await;
407        };
408
409        let sk = identity.sk_raw();
410        let pk = identity.pk_raw();
411
412        // SAFETY: Byte lengths are ensured at the keystore level so this function is safe to call, despite being cursed
413        IdentityKeyPair::from_raw_key_pair(*sk, *pk)
414            .map_err(ProteusError::wrap("constructing identity keypair"))
415            .map_err(Into::into)
416    }
417
418    /// Internal function to create and save a new Proteus Identity
419    async fn create_identity(keystore: &CryptoKeystore) -> Result<IdentityKeyPair> {
420        let kp = IdentityKeyPair::new();
421        let pk = kp.public_key.public_key.as_slice().to_vec();
422
423        let ks_identity = ProteusIdentity {
424            sk: kp.secret_key.to_keypair_bytes().into(),
425            pk,
426        };
427        keystore
428            .save(ks_identity)
429            .await
430            .map_err(KeystoreError::wrap("saving new proteus identity"))?;
431
432        Ok(kp)
433    }
434
435    /// Restores the saved sessions in memory. This is performed automatically on init
436    async fn restore_sessions(
437        keystore: &core_crypto_keystore::Connection,
438        identity: &Arc<IdentityKeyPair>,
439    ) -> Result<GroupStore<ProteusConversationSession>> {
440        let mut proteus_sessions = GroupStore::new_with_limit(crate::group_store::ITEM_LIMIT * 2);
441        for session in keystore
442            .find_all::<ProteusSession>(Default::default())
443            .await
444            .map_err(KeystoreError::wrap("finding all proteus sessions"))?
445            .into_iter()
446        {
447            let proteus_session = Session::deserialise(identity.clone(), &session.session)
448                .map_err(ProteusError::wrap("deserializing session"))?;
449
450            let identifier = session.id.clone();
451
452            let proteus_conversation = ProteusConversationSession {
453                identifier: identifier.clone(),
454                session: proteus_session,
455            };
456
457            if proteus_sessions
458                .try_insert(identifier.into_bytes(), proteus_conversation)
459                .is_err()
460            {
461                break;
462            }
463        }
464
465        Ok(proteus_sessions)
466    }
467
468    /// Creates a new session from a prekey
469    pub async fn session_from_prekey(
470        &mut self,
471        session_id: &str,
472        key: &[u8],
473    ) -> Result<GroupStoreValue<ProteusConversationSession>> {
474        let prekey = PreKeyBundle::deserialise(key).map_err(ProteusError::wrap("deserializing prekey bundle"))?;
475        // Note on the `::<>` turbofish below:
476        //
477        // `init_from_prekey` returns an error type which is parametric over some wrapped `E`,
478        // because one variant (not relevant to this particular operation) wraps an error type based
479        // on a parameter of a different function entirely.
480        //
481        // Rust complains here, because it can't figure out what type that `E` should be. After all, it's
482        // not inferrable from this function call! It is also entirely irrelevant in this case.
483        //
484        // We can derive two general rules about error-handling in Rust from this example:
485        //
486        // 1. It's better to make smaller error types where possible, encapsulating fallible operations
487        //    with their own error variants, and then wrapping those errors where required, as opposed to
488        //    creating giant catch-all errors. Doing so also has knock-on benefits with regard to tracing
489        //    the precise origin of the error.
490        // 2. One should never make an error wrapper parametric. If you need to wrap an unknown error,
491        //    it's always better to wrap a `Box<dyn std::error::Error>` than to make your error type parametric.
492        //    The allocation cost of creating the `Box` is utterly trivial in an error-handling path, and
493        //    it avoids parametric virality. (`init_from_prekey` is itself only generic because it returns
494        //    this error type with a type-parametric variant, which the function never returns.)
495        //
496        // In this case, we have the out of band knowledge that `ProteusErrorKind` has a `#[from]` implementation
497        // for `proteus_wasm::session::Error<core_crypto_keystore::CryptoKeystoreError>` and for no other kinds
498        // of session error. So we can safely say that the type of error we are meant to catch here, and
499        // therefore pass in that otherwise-irrelevant type, to ensure that error handling works properly.
500        //
501        // Some people say that if it's stupid but it works, it's not stupid. I disagree. If it's stupid but
502        // it works, that's our cue to seek out even better, non-stupid ways to get things done. I reiterate:
503        // the actual type referred to in this turbofish is nothing but a magic incantation to make error
504        // handling work; it has no bearing on the error retured from this function. How much better would it
505        // have been if `session::Error` were not parametric and we could have avoided the turbofish entirely?
506        let proteus_session = Session::init_from_prekey::<core_crypto_keystore::CryptoKeystoreError>(
507            self.proteus_identity.clone(),
508            prekey,
509        )
510        .map_err(ProteusError::wrap("initializing session from prekey"))?;
511
512        let proteus_conversation = ProteusConversationSession {
513            identifier: session_id.into(),
514            session: proteus_session,
515        };
516
517        self.proteus_sessions.insert(session_id.into(), proteus_conversation);
518
519        Ok(self.proteus_sessions.get(session_id.as_bytes()).unwrap().clone())
520    }
521
522    /// Creates a new proteus Session from a received message
523    pub(crate) async fn session_from_message(
524        &mut self,
525        keystore: &mut CryptoKeystore,
526        session_id: &str,
527        envelope: &[u8],
528    ) -> Result<(GroupStoreValue<ProteusConversationSession>, Vec<u8>)> {
529        let message = Envelope::deserialise(envelope).map_err(ProteusError::wrap("deserialising envelope"))?;
530        let (session, payload) = Session::init_from_message(self.proteus_identity.clone(), keystore, &message)
531            .await
532            .map_err(ProteusError::wrap("initializing session from message"))?;
533
534        let proteus_conversation = ProteusConversationSession {
535            identifier: session_id.into(),
536            session,
537        };
538
539        self.proteus_sessions.insert(session_id.into(), proteus_conversation);
540
541        Ok((
542            self.proteus_sessions.get(session_id.as_bytes()).unwrap().clone(),
543            payload,
544        ))
545    }
546
547    /// Persists a session in store
548    ///
549    /// **Note**: This isn't usually needed as persisting sessions happens automatically when decrypting/encrypting messages and initializing Sessions
550    pub(crate) async fn session_save(&mut self, keystore: &CryptoKeystore, session_id: &str) -> Result<()> {
551        if let Some(session) = self
552            .proteus_sessions
553            .get_fetch(session_id.as_bytes(), keystore, Some(self.proteus_identity.clone()))
554            .await?
555        {
556            Self::session_save_by_ref(keystore, session).await?;
557        }
558
559        Ok(())
560    }
561
562    async fn session_save_by_ref(
563        keystore: &CryptoKeystore,
564        session: GroupStoreValue<ProteusConversationSession>,
565    ) -> Result<()> {
566        let session = session.read().await;
567        let db_session = ProteusSession {
568            id: session.identifier().to_string(),
569            session: session
570                .session
571                .serialise()
572                .map_err(ProteusError::wrap("serializing session"))?,
573        };
574        keystore
575            .save(db_session)
576            .await
577            .map_err(KeystoreError::wrap("saving proteus session"))?;
578        Ok(())
579    }
580
581    /// Deletes a session in the store
582    pub(crate) async fn session_delete(&mut self, keystore: &CryptoKeystore, session_id: &str) -> Result<()> {
583        if keystore.remove::<ProteusSession, _>(session_id).await.is_ok() {
584            let _ = self.proteus_sessions.remove(session_id.as_bytes());
585        }
586        Ok(())
587    }
588
589    /// Session accessor
590    pub(crate) async fn session(
591        &mut self,
592        session_id: &str,
593        keystore: &CryptoKeystore,
594    ) -> Result<Option<GroupStoreValue<ProteusConversationSession>>> {
595        self.proteus_sessions
596            .get_fetch(session_id.as_bytes(), keystore, Some(self.proteus_identity.clone()))
597            .await
598    }
599
600    /// Session exists
601    pub(crate) async fn session_exists(&mut self, session_id: &str, keystore: &CryptoKeystore) -> bool {
602        self.session(session_id, keystore).await.ok().flatten().is_some()
603    }
604
605    /// Decrypt a proteus message for an already existing session
606    /// Note: This cannot be used for handshake messages, see [ProteusCentral::session_from_message]
607    pub(crate) async fn decrypt(
608        &mut self,
609        keystore: &mut CryptoKeystore,
610        session_id: &str,
611        ciphertext: &[u8],
612    ) -> Result<Vec<u8>> {
613        let session = self
614            .proteus_sessions
615            .get_fetch(session_id.as_bytes(), keystore, Some(self.proteus_identity.clone()))
616            .await?
617            .ok_or(LeafError::ConversationNotFound(session_id.as_bytes().into()))
618            .map_err(ProteusError::wrap("getting session"))?;
619
620        let plaintext = session.write().await.decrypt(keystore, ciphertext).await?;
621        ProteusCentral::session_save_by_ref(keystore, session).await?;
622
623        Ok(plaintext)
624    }
625
626    /// Encrypt a message for a session
627    pub(crate) async fn encrypt(
628        &mut self,
629        keystore: &mut CryptoKeystore,
630        session_id: &str,
631        plaintext: &[u8],
632    ) -> Result<Vec<u8>> {
633        let session = self
634            .session(session_id, keystore)
635            .await?
636            .ok_or(LeafError::ConversationNotFound(session_id.as_bytes().into()))
637            .map_err(ProteusError::wrap("getting session"))?;
638
639        let ciphertext = session.write().await.encrypt(plaintext)?;
640        ProteusCentral::session_save_by_ref(keystore, session).await?;
641
642        Ok(ciphertext)
643    }
644
645    /// Encrypts a message for a list of sessions
646    /// This is mainly used for conversations with multiple clients, this allows to minimize FFI roundtrips
647    pub(crate) async fn encrypt_batched(
648        &mut self,
649        keystore: &mut CryptoKeystore,
650        sessions: &[impl AsRef<str>],
651        plaintext: &[u8],
652    ) -> Result<HashMap<String, Vec<u8>>> {
653        let mut acc = HashMap::new();
654        for session_id in sessions {
655            if let Some(session) = self.session(session_id.as_ref(), keystore).await? {
656                let mut session_w = session.write().await;
657                acc.insert(session_w.identifier.clone(), session_w.encrypt(plaintext)?);
658                drop(session_w);
659
660                ProteusCentral::session_save_by_ref(keystore, session).await?;
661            }
662        }
663        Ok(acc)
664    }
665
666    /// Generates a new Proteus PreKey, stores it in the keystore and returns a serialized PreKeyBundle to be consumed externally
667    pub(crate) async fn new_prekey(&self, id: u16, keystore: &CryptoKeystore) -> Result<Vec<u8>> {
668        use proteus_wasm::keys::{PreKey, PreKeyId};
669
670        let prekey_id = PreKeyId::new(id);
671        let prekey = PreKey::new(prekey_id);
672        let keystore_prekey = core_crypto_keystore::entities::ProteusPrekey::from_raw(
673            id,
674            prekey.serialise().map_err(ProteusError::wrap("serialising prekey"))?,
675        );
676        let bundle = PreKeyBundle::new(self.proteus_identity.as_ref().public_key.clone(), &prekey);
677        let bundle = bundle
678            .serialise()
679            .map_err(ProteusError::wrap("serialising prekey bundle"))?;
680        keystore
681            .save(keystore_prekey)
682            .await
683            .map_err(KeystoreError::wrap("saving keystore prekey"))?;
684        Ok(bundle)
685    }
686
687    /// Generates a new Proteus Prekey, with an automatically auto-incremented ID.
688    ///
689    /// See [ProteusCentral::new_prekey]
690    pub(crate) async fn new_prekey_auto(&self, keystore: &CryptoKeystore) -> Result<(u16, Vec<u8>)> {
691        let id = core_crypto_keystore::entities::ProteusPrekey::get_free_id(keystore)
692            .await
693            .map_err(KeystoreError::wrap("getting proteus prekey by id"))?;
694        Ok((id, self.new_prekey(id, keystore).await?))
695    }
696
697    /// Returns the Proteus last resort prekey ID (u16::MAX = 65535 = 0xFFFF)
698    pub fn last_resort_prekey_id() -> u16 {
699        proteus_wasm::keys::MAX_PREKEY_ID.value()
700    }
701
702    /// Returns the Proteus last resort prekey
703    /// If it cannot be found, one will be created.
704    pub(crate) async fn last_resort_prekey(&self, keystore: &CryptoKeystore) -> Result<Vec<u8>> {
705        let last_resort = if let Some(last_resort) = keystore
706            .find::<core_crypto_keystore::entities::ProteusPrekey>(
707                Self::last_resort_prekey_id().to_le_bytes().as_slice(),
708            )
709            .await
710            .map_err(KeystoreError::wrap("finding proteus prekey"))?
711        {
712            proteus_wasm::keys::PreKey::deserialise(&last_resort.prekey)
713                .map_err(ProteusError::wrap("deserialising proteus prekey"))?
714        } else {
715            let last_resort = proteus_wasm::keys::PreKey::last_resort();
716
717            use core_crypto_keystore::CryptoKeystoreProteus as _;
718            keystore
719                .proteus_store_prekey(
720                    Self::last_resort_prekey_id(),
721                    &last_resort
722                        .serialise()
723                        .map_err(ProteusError::wrap("serialising last resort prekey"))?,
724                )
725                .await
726                .map_err(KeystoreError::wrap("storing proteus prekey"))?;
727
728            last_resort
729        };
730
731        let bundle = PreKeyBundle::new(self.proteus_identity.as_ref().public_key.clone(), &last_resort);
732        let bundle = bundle
733            .serialise()
734            .map_err(ProteusError::wrap("serialising prekey bundle"))?;
735
736        Ok(bundle)
737    }
738
739    /// Proteus identity keypair
740    pub fn identity(&self) -> &IdentityKeyPair {
741        self.proteus_identity.as_ref()
742    }
743
744    /// Proteus Public key hex-encoded fingerprint
745    pub fn fingerprint(&self) -> String {
746        self.proteus_identity.as_ref().public_key.fingerprint()
747    }
748
749    /// Proteus Session local hex-encoded fingerprint
750    ///
751    /// # Errors
752    /// When the session is not found
753    pub(crate) async fn fingerprint_local(&mut self, session_id: &str, keystore: &CryptoKeystore) -> Result<String> {
754        let session = self
755            .session(session_id, keystore)
756            .await?
757            .ok_or(LeafError::ConversationNotFound(session_id.as_bytes().into()))
758            .map_err(ProteusError::wrap("getting session"))?;
759        let fingerprint = session.read().await.fingerprint_local();
760        Ok(fingerprint)
761    }
762
763    /// Proteus Session remote hex-encoded fingerprint
764    ///
765    /// # Errors
766    /// When the session is not found
767    pub(crate) async fn fingerprint_remote(&mut self, session_id: &str, keystore: &CryptoKeystore) -> Result<String> {
768        let session = self
769            .session(session_id, keystore)
770            .await?
771            .ok_or(LeafError::ConversationNotFound(session_id.as_bytes().into()))
772            .map_err(ProteusError::wrap("getting session"))?;
773        let fingerprint = session.read().await.fingerprint_remote();
774        Ok(fingerprint)
775    }
776
777    /// Hex-encoded fingerprint of the given prekey
778    ///
779    /// # Errors
780    /// If the prekey cannot be deserialized
781    pub fn fingerprint_prekeybundle(prekey: &[u8]) -> Result<String> {
782        let prekey = PreKeyBundle::deserialise(prekey).map_err(ProteusError::wrap("deserialising prekey bundle"))?;
783        Ok(prekey.identity_key.fingerprint())
784    }
785
786    /// Cryptobox -> CoreCrypto migration
787    #[cfg_attr(not(feature = "cryptobox-migrate"), allow(unused_variables))]
788    pub(crate) async fn cryptobox_migrate(keystore: &CryptoKeystore, path: &str) -> Result<()> {
789        cfg_if::cfg_if! {
790            if #[cfg(feature = "cryptobox-migrate")] {
791                Self::cryptobox_migrate_impl(keystore, path).await?;
792                Ok(())
793            } else {
794                Err(Error::FeatureDisabled("cryptobox-migrate"))
795            }
796        }
797    }
798}
799
800#[cfg(feature = "cryptobox-migrate")]
801#[allow(dead_code)]
802impl ProteusCentral {
803    #[cfg(not(target_family = "wasm"))]
804    async fn cryptobox_migrate_impl(keystore: &CryptoKeystore, path: &str) -> Result<()> {
805        let root_dir = std::path::PathBuf::from(path);
806
807        if !root_dir.exists() {
808            return Err(CryptoboxMigrationError::wrap("root dir does not exist")(
809                crate::CryptoboxMigrationErrorKind::ProvidedPathDoesNotExist(path.into()),
810            )
811            .into());
812        }
813
814        let session_dir = root_dir.join("sessions");
815        let prekey_dir = root_dir.join("prekeys");
816
817        // return early any time we can't figure out some part of the identity
818        let missing_identity = Err(CryptoboxMigrationError::wrap("taking identity keypair")(
819            crate::CryptoboxMigrationErrorKind::IdentityNotFound(path.into()),
820        )
821        .into());
822
823        let identity = if let Some(store_kp) = keystore
824            .find::<ProteusIdentity>(&[])
825            .await
826            .map_err(KeystoreError::wrap("finding proteus identity"))?
827        {
828            Box::new(
829                IdentityKeyPair::from_raw_key_pair(*store_kp.sk_raw(), *store_kp.pk_raw())
830                    .map_err(ProteusError::wrap("constructing identity keypair from raw keypair"))?,
831            )
832        } else {
833            let identity_dir = root_dir.join("identities");
834
835            let identity = identity_dir.join("local");
836            let legacy_identity = identity_dir.join("local_identity");
837            // Old "local_identity" migration step
838            let kp = if legacy_identity.exists() {
839                let kp_cbor = async_fs::read(&legacy_identity)
840                    .await
841                    .map_err(CryptoboxMigrationError::wrap("reading legacy identity from filesystem"))?;
842                let kp = IdentityKeyPair::deserialise(&kp_cbor)
843                    .map_err(ProteusError::wrap("deserialising identity keypair"))?;
844
845                Box::new(kp)
846            } else if identity.exists() {
847                let kp_cbor = async_fs::read(&identity)
848                    .await
849                    .map_err(CryptoboxMigrationError::wrap("reading identity from filesystem"))?;
850                let kp = proteus_wasm::identity::Identity::deserialise(&kp_cbor)
851                    .map_err(ProteusError::wrap("deserialising identity"))?;
852
853                if let proteus_wasm::identity::Identity::Sec(kp) = kp {
854                    kp.into_owned()
855                } else {
856                    return missing_identity;
857                }
858            } else {
859                return missing_identity;
860            };
861
862            let pk = kp.public_key.public_key.as_slice().into();
863
864            let ks_identity = ProteusIdentity {
865                sk: kp.secret_key.to_keypair_bytes().into(),
866                pk,
867            };
868
869            keystore
870                .save(ks_identity)
871                .await
872                .map_err(KeystoreError::wrap("saving proteus identity"))?;
873
874            if legacy_identity.exists() {
875                async_fs::remove_file(legacy_identity)
876                    .await
877                    .map_err(CryptoboxMigrationError::wrap("removing legacy identity"))?;
878            }
879
880            kp
881        };
882
883        let identity = *identity;
884
885        use futures_lite::stream::StreamExt as _;
886        // Session migration
887        let mut session_entries = async_fs::read_dir(session_dir)
888            .await
889            .map_err(CryptoboxMigrationError::wrap("reading session entries"))?;
890        while let Some(session_file) = session_entries
891            .try_next()
892            .await
893            .map_err(CryptoboxMigrationError::wrap("getting next session file"))?
894        {
895            // The name of the file is the session id
896            let proteus_session_id: String = session_file.file_name().to_string_lossy().to_string();
897
898            // If the session is already in store, skip ahead
899            if keystore
900                .find::<ProteusSession>(proteus_session_id.as_bytes())
901                .await
902                .map_err(KeystoreError::wrap("finding proteus session by id"))?
903                .is_some()
904            {
905                continue;
906            }
907
908            let raw_session = async_fs::read(session_file.path())
909                .await
910                .map_err(CryptoboxMigrationError::wrap("reading session file"))?;
911            // Session integrity check
912            let Ok(_) = Session::deserialise(&identity, &raw_session) else {
913                continue;
914            };
915
916            let keystore_session = ProteusSession {
917                id: proteus_session_id,
918                session: raw_session,
919            };
920
921            keystore
922                .save(keystore_session)
923                .await
924                .map_err(KeystoreError::wrap("saving proteus session"))?;
925        }
926
927        // Prekey migration
928        use core_crypto_keystore::entities::ProteusPrekey;
929
930        use crate::CryptoboxMigrationError;
931        let mut prekey_entries = async_fs::read_dir(prekey_dir)
932            .await
933            .map_err(CryptoboxMigrationError::wrap("reading prekey entries"))?;
934        while let Some(prekey_file) = prekey_entries
935            .try_next()
936            .await
937            .map_err(CryptoboxMigrationError::wrap("getting next prekey file"))?
938        {
939            // The name of the file is the prekey id, so we parse it to get the ID
940            let proteus_prekey_id = proteus_wasm::keys::PreKeyId::new(
941                prekey_file
942                    .file_name()
943                    .to_string_lossy()
944                    .parse()
945                    .map_err(CryptoboxMigrationError::wrap("parsing prekey file name"))?,
946            );
947
948            // Check if the prekey ID is already existing
949            if keystore
950                .find::<ProteusPrekey>(&proteus_prekey_id.value().to_le_bytes())
951                .await
952                .map_err(KeystoreError::wrap("finding proteus prekey by id"))?
953                .is_some()
954            {
955                continue;
956            }
957
958            let raw_prekey = async_fs::read(prekey_file.path())
959                .await
960                .map_err(CryptoboxMigrationError::wrap("reading prekey file"))?;
961            // Integrity check to see if the PreKey is actually correct
962            if proteus_wasm::keys::PreKey::deserialise(&raw_prekey).is_ok() {
963                let keystore_prekey = ProteusPrekey::from_raw(proteus_prekey_id.value(), raw_prekey);
964                keystore
965                    .save(keystore_prekey)
966                    .await
967                    .map_err(KeystoreError::wrap("saving proteus prekey"))?;
968            }
969        }
970
971        Ok(())
972    }
973
974    #[cfg(target_family = "wasm")]
975    fn get_cbor_bytes_from_map(map: serde_json::map::Map<String, serde_json::Value>) -> Result<Vec<u8>> {
976        use crate::{CryptoboxMigrationError, CryptoboxMigrationErrorKind};
977
978        let Some(js_value) = map.get("serialised") else {
979            return Err(CryptoboxMigrationError::wrap("getting serialised cbor bytes from map")(
980                CryptoboxMigrationErrorKind::MissingKeyInValue("serialised".to_string()),
981            )
982            .into());
983        };
984
985        let Some(b64_value) = js_value.as_str() else {
986            return Err(CryptoboxMigrationError::wrap("getting js value as string")(
987                CryptoboxMigrationErrorKind::WrongValueType("string".to_string()),
988            )
989            .into());
990        };
991
992        use base64::Engine as _;
993        let cbor_bytes = base64::prelude::BASE64_STANDARD
994            .decode(b64_value)
995            .map_err(CryptoboxMigrationError::wrap("decoding cbor bytes"))?;
996        Ok(cbor_bytes)
997    }
998
999    #[cfg(target_family = "wasm")]
1000    async fn cryptobox_migrate_impl(keystore: &CryptoKeystore, path: &str) -> Result<()> {
1001        use rexie::{Rexie, TransactionMode};
1002
1003        use crate::{CryptoboxMigrationError, CryptoboxMigrationErrorKind};
1004        let local_identity_key = "local_identity";
1005        let local_identity_store_name = "keys";
1006        let prekeys_store_name = "prekeys";
1007        let sessions_store_name = "sessions";
1008
1009        // Path should be following this logic: https://github.com/wireapp/wire-web-packages/blob/main/packages/core/src/main/Account.ts#L645
1010        let db = Rexie::builder(path)
1011            .build()
1012            .await
1013            .map_err(CryptoboxMigrationError::wrap("building rexie"))?;
1014
1015        let store_names = db.store_names();
1016
1017        let expected_stores = &[prekeys_store_name, sessions_store_name, local_identity_store_name];
1018
1019        if !expected_stores
1020            .iter()
1021            .map(ToString::to_string)
1022            .all(|s| store_names.contains(&s))
1023        {
1024            return Err(CryptoboxMigrationError::wrap("checking expected stores")(
1025                CryptoboxMigrationErrorKind::ProvidedPathDoesNotExist(path.into()),
1026            )
1027            .into());
1028        }
1029
1030        let mut proteus_identity = if let Some(store_kp) = keystore
1031            .find::<ProteusIdentity>(&[])
1032            .await
1033            .map_err(KeystoreError::wrap("finding proteus identity for empty id"))?
1034        {
1035            Some(
1036                proteus_wasm::keys::IdentityKeyPair::from_raw_key_pair(*store_kp.sk_raw(), *store_kp.pk_raw())
1037                    .map_err(ProteusError::wrap("constructing identity keypair from raw"))?,
1038            )
1039        } else {
1040            let transaction = db
1041                .transaction(&[local_identity_store_name], TransactionMode::ReadOnly)
1042                .map_err(CryptoboxMigrationError::wrap("initializing rexie transaction"))?;
1043
1044            let identity_store = transaction
1045                .store(local_identity_store_name)
1046                .map_err(CryptoboxMigrationError::wrap("storing local identity store name"))?;
1047
1048            if let Some(cryptobox_js_value) = identity_store
1049                .get(local_identity_key.into())
1050                .await
1051                .map_err(CryptoboxMigrationError::wrap("getting local identity key js value"))?
1052            {
1053                let js_value: serde_json::map::Map<String, serde_json::Value> =
1054                    serde_wasm_bindgen::from_value(cryptobox_js_value).map_err(CryptoboxMigrationError::wrap(
1055                        "getting local identity key from identity store",
1056                    ))?;
1057
1058                let kp_cbor = Self::get_cbor_bytes_from_map(js_value)?;
1059
1060                let kp = proteus_wasm::keys::IdentityKeyPair::deserialise(&kp_cbor)
1061                    .map_err(ProteusError::wrap("deserializing identity keypair"))?;
1062
1063                let pk = kp.public_key.public_key.as_slice().to_vec();
1064
1065                let ks_identity = ProteusIdentity {
1066                    sk: kp.secret_key.to_keypair_bytes().into(),
1067                    pk,
1068                };
1069                keystore
1070                    .save(ks_identity)
1071                    .await
1072                    .map_err(KeystoreError::wrap("saving proteus identity in keystore"))?;
1073
1074                Some(kp)
1075            } else {
1076                None
1077            }
1078        };
1079
1080        let Some(proteus_identity) = proteus_identity.take() else {
1081            return Err(CryptoboxMigrationError::wrap("taking proteus identity")(
1082                CryptoboxMigrationErrorKind::IdentityNotFound(path.into()),
1083            )
1084            .into());
1085        };
1086
1087        if store_names.contains(&sessions_store_name.to_string()) {
1088            let transaction = db
1089                .transaction(&[sessions_store_name], TransactionMode::ReadOnly)
1090                .map_err(CryptoboxMigrationError::wrap("starting rexie transaction"))?;
1091
1092            let sessions_store = transaction
1093                .store(sessions_store_name)
1094                .map_err(CryptoboxMigrationError::wrap("getting sessions store"))?;
1095
1096            let sessions = sessions_store
1097                .scan(None, None, None, None)
1098                .await
1099                .map_err(CryptoboxMigrationError::wrap("scanning sessions store for sessions"))?;
1100
1101            for (session_id, session_js_value) in sessions.into_iter().map(|(k, v)| (k.as_string().unwrap(), v)) {
1102                // If the session is already in store, skip ahead
1103                if keystore
1104                    .find::<ProteusSession>(session_id.as_bytes())
1105                    .await
1106                    .map_err(KeystoreError::wrap("finding proteus session by id"))?
1107                    .is_some()
1108                {
1109                    continue;
1110                }
1111
1112                let js_value: serde_json::map::Map<String, serde_json::Value> =
1113                    serde_wasm_bindgen::from_value(session_js_value).map_err(CryptoboxMigrationError::wrap(
1114                        "converting session js value to serde map",
1115                    ))?;
1116
1117                let session_cbor_bytes = Self::get_cbor_bytes_from_map(js_value)?;
1118
1119                // Integrity check
1120                if proteus_wasm::session::Session::deserialise(&proteus_identity, &session_cbor_bytes).is_ok() {
1121                    let keystore_session = ProteusSession {
1122                        id: session_id,
1123                        session: session_cbor_bytes,
1124                    };
1125
1126                    keystore
1127                        .save(keystore_session)
1128                        .await
1129                        .map_err(KeystoreError::wrap("saving keystore session"))?;
1130                }
1131            }
1132        }
1133
1134        if store_names.contains(&prekeys_store_name.to_string()) {
1135            use core_crypto_keystore::entities::ProteusPrekey;
1136
1137            let transaction = db
1138                .transaction(&[prekeys_store_name], TransactionMode::ReadOnly)
1139                .map_err(CryptoboxMigrationError::wrap("beginning rexie transaction"))?;
1140
1141            let prekeys_store = transaction
1142                .store(prekeys_store_name)
1143                .map_err(CryptoboxMigrationError::wrap("getting prekeys store"))?;
1144
1145            let prekeys = prekeys_store
1146                .scan(None, None, None, None)
1147                .await
1148                .map_err(CryptoboxMigrationError::wrap("scanning for prekeys"))?;
1149
1150            for (prekey_id, prekey_js_value) in prekeys
1151                .into_iter()
1152                .map(|(id, prekey_js_value)| (id.as_string().unwrap(), prekey_js_value))
1153            {
1154                let prekey_id: u16 = prekey_id
1155                    .parse()
1156                    .map_err(CryptoboxMigrationError::wrap("parsing prekey id"))?;
1157
1158                // Check if the prekey ID is already existing
1159                if keystore
1160                    .find::<ProteusPrekey>(&prekey_id.to_le_bytes())
1161                    .await
1162                    .map_err(KeystoreError::wrap(
1163                        "finding proteus prekey by id to check for existence",
1164                    ))?
1165                    .is_some()
1166                {
1167                    continue;
1168                }
1169
1170                let js_value: serde_json::map::Map<String, serde_json::Value> =
1171                    serde_wasm_bindgen::from_value(prekey_js_value)
1172                        .map_err(CryptoboxMigrationError::wrap("converting prekey js value to serde map"))?;
1173
1174                let raw_prekey_cbor = Self::get_cbor_bytes_from_map(js_value)?;
1175
1176                // Integrity check to see if the PreKey is actually correct
1177                if proteus_wasm::keys::PreKey::deserialise(&raw_prekey_cbor).is_ok() {
1178                    let keystore_prekey = ProteusPrekey::from_raw(prekey_id, raw_prekey_cbor);
1179                    keystore
1180                        .save(keystore_prekey)
1181                        .await
1182                        .map_err(KeystoreError::wrap("saving proteus prekey"))?;
1183                }
1184            }
1185        }
1186
1187        Ok(())
1188    }
1189}
1190
1191#[cfg(test)]
1192mod tests {
1193    use crate::{
1194        prelude::{CertificateBundle, ClientIdentifier, MlsCentral, MlsCentralConfiguration, MlsCredentialType},
1195        test_utils::{proteus_utils::*, x509::X509TestChain, *},
1196    };
1197
1198    use crate::prelude::INITIAL_KEYING_MATERIAL_COUNT;
1199    use proteus_traits::PreKeyStore;
1200    use wasm_bindgen_test::*;
1201
1202    use super::*;
1203
1204    wasm_bindgen_test_configure!(run_in_browser);
1205
1206    #[apply(all_cred_cipher)]
1207    #[wasm_bindgen_test]
1208    async fn cc_can_init(case: TestCase) {
1209        #[cfg(not(target_family = "wasm"))]
1210        let (path, db_file) = tmp_db_file();
1211        #[cfg(target_family = "wasm")]
1212        let (path, _) = tmp_db_file();
1213        let client_id = "alice".into();
1214        let cfg = MlsCentralConfiguration::try_new(
1215            path,
1216            "test".to_string(),
1217            Some(client_id),
1218            vec![case.ciphersuite()],
1219            None,
1220            Some(INITIAL_KEYING_MATERIAL_COUNT),
1221        )
1222        .unwrap();
1223        let cc: CoreCrypto = MlsCentral::try_new(cfg).await.unwrap().into();
1224        let context = cc.new_transaction().await.unwrap();
1225        assert!(context.proteus_init().await.is_ok());
1226        assert!(context.proteus_new_prekey(1).await.is_ok());
1227        context.finish().await.unwrap();
1228        #[cfg(not(target_family = "wasm"))]
1229        drop(db_file);
1230    }
1231
1232    #[apply(all_cred_cipher)]
1233    #[wasm_bindgen_test]
1234    async fn cc_can_2_phase_init(case: TestCase) {
1235        #[cfg(not(target_family = "wasm"))]
1236        let (path, db_file) = tmp_db_file();
1237        #[cfg(target_family = "wasm")]
1238        let (path, _) = tmp_db_file();
1239        // we are deferring MLS initialization here, not passing a MLS 'client_id' yet
1240        let cfg = MlsCentralConfiguration::try_new(
1241            path,
1242            "test".to_string(),
1243            None,
1244            vec![case.ciphersuite()],
1245            None,
1246            Some(INITIAL_KEYING_MATERIAL_COUNT),
1247        )
1248        .unwrap();
1249        let cc: CoreCrypto = MlsCentral::try_new(cfg).await.unwrap().into();
1250        let transaction = cc.new_transaction().await.unwrap();
1251        let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
1252        x509_test_chain.register_with_central(&transaction).await;
1253        assert!(transaction.proteus_init().await.is_ok());
1254        // proteus is initialized, prekeys can be generated
1255        assert!(transaction.proteus_new_prekey(1).await.is_ok());
1256        // 👇 and so a unique 'client_id' can be fetched from wire-server
1257        let client_id = "alice";
1258        let identifier = match case.credential_type {
1259            MlsCredentialType::Basic => ClientIdentifier::Basic(client_id.into()),
1260            MlsCredentialType::X509 => {
1261                CertificateBundle::rand_identifier(client_id, &[x509_test_chain.find_local_intermediate_ca()])
1262            }
1263        };
1264        transaction
1265            .mls_init(
1266                identifier,
1267                vec![case.ciphersuite()],
1268                Some(INITIAL_KEYING_MATERIAL_COUNT),
1269            )
1270            .await
1271            .unwrap();
1272        // expect MLS to work
1273        assert_eq!(
1274            transaction
1275                .get_or_create_client_keypackages(case.ciphersuite(), case.credential_type, 2)
1276                .await
1277                .unwrap()
1278                .len(),
1279            2
1280        );
1281        #[cfg(not(target_family = "wasm"))]
1282        drop(db_file);
1283    }
1284
1285    #[async_std::test]
1286    #[wasm_bindgen_test]
1287    async fn can_init() {
1288        #[cfg(not(target_family = "wasm"))]
1289        let (path, db_file) = tmp_db_file();
1290        #[cfg(target_family = "wasm")]
1291        let (path, _) = tmp_db_file();
1292        let keystore = core_crypto_keystore::Connection::open_with_key(&path, "test")
1293            .await
1294            .unwrap();
1295        keystore.new_transaction().await.unwrap();
1296        let central = ProteusCentral::try_new(&keystore).await.unwrap();
1297        let identity = (*central.proteus_identity).clone();
1298        keystore.commit_transaction().await.unwrap();
1299
1300        let keystore = core_crypto_keystore::Connection::open_with_key(path, "test")
1301            .await
1302            .unwrap();
1303        keystore.new_transaction().await.unwrap();
1304        let central = ProteusCentral::try_new(&keystore).await.unwrap();
1305        keystore.commit_transaction().await.unwrap();
1306        assert_eq!(identity, *central.proteus_identity);
1307
1308        keystore.wipe().await.unwrap();
1309        #[cfg(not(target_family = "wasm"))]
1310        drop(db_file);
1311    }
1312
1313    #[async_std::test]
1314    #[wasm_bindgen_test]
1315    async fn can_talk_with_proteus() {
1316        #[cfg(not(target_family = "wasm"))]
1317        let (path, db_file) = tmp_db_file();
1318        #[cfg(target_family = "wasm")]
1319        let (path, _) = tmp_db_file();
1320
1321        let session_id = uuid::Uuid::new_v4().hyphenated().to_string();
1322
1323        let mut keystore = core_crypto_keystore::Connection::open_with_key(path, "test")
1324            .await
1325            .unwrap();
1326        keystore.new_transaction().await.unwrap();
1327
1328        let mut alice = ProteusCentral::try_new(&keystore).await.unwrap();
1329
1330        let mut bob = CryptoboxLike::init();
1331        let bob_pk_bundle = bob.new_prekey();
1332
1333        alice
1334            .session_from_prekey(&session_id, &bob_pk_bundle.serialise().unwrap())
1335            .await
1336            .unwrap();
1337
1338        let message = b"Hello world";
1339
1340        let encrypted = alice.encrypt(&mut keystore, &session_id, message).await.unwrap();
1341        let decrypted = bob.decrypt(&session_id, &encrypted).await;
1342        assert_eq!(decrypted, message);
1343
1344        let encrypted = bob.encrypt(&session_id, message);
1345        let decrypted = alice.decrypt(&mut keystore, &session_id, &encrypted).await.unwrap();
1346        assert_eq!(decrypted, message);
1347
1348        keystore.commit_transaction().await.unwrap();
1349        keystore.wipe().await.unwrap();
1350        #[cfg(not(target_family = "wasm"))]
1351        drop(db_file);
1352    }
1353
1354    #[async_std::test]
1355    #[wasm_bindgen_test]
1356    async fn can_produce_proteus_consumed_prekeys() {
1357        #[cfg(not(target_family = "wasm"))]
1358        let (path, db_file) = tmp_db_file();
1359        #[cfg(target_family = "wasm")]
1360        let (path, _) = tmp_db_file();
1361
1362        let session_id = uuid::Uuid::new_v4().hyphenated().to_string();
1363
1364        let mut keystore = core_crypto_keystore::Connection::open_with_key(path, "test")
1365            .await
1366            .unwrap();
1367        keystore.new_transaction().await.unwrap();
1368        let mut alice = ProteusCentral::try_new(&keystore).await.unwrap();
1369
1370        let mut bob = CryptoboxLike::init();
1371
1372        let alice_prekey_bundle_ser = alice.new_prekey(1, &keystore).await.unwrap();
1373
1374        bob.init_session_from_prekey_bundle(&session_id, &alice_prekey_bundle_ser);
1375        let message = b"Hello world!";
1376        let encrypted = bob.encrypt(&session_id, message);
1377
1378        let (_, decrypted) = alice
1379            .session_from_message(&mut keystore, &session_id, &encrypted)
1380            .await
1381            .unwrap();
1382
1383        assert_eq!(message, decrypted.as_slice());
1384
1385        let encrypted = alice.encrypt(&mut keystore, &session_id, message).await.unwrap();
1386        let decrypted = bob.decrypt(&session_id, &encrypted).await;
1387
1388        assert_eq!(message, decrypted.as_slice());
1389        keystore.commit_transaction().await.unwrap();
1390        keystore.wipe().await.unwrap();
1391        #[cfg(not(target_family = "wasm"))]
1392        drop(db_file);
1393    }
1394
1395    #[async_std::test]
1396    #[wasm_bindgen_test]
1397    async fn auto_prekeys_are_sequential() {
1398        use core_crypto_keystore::entities::ProteusPrekey;
1399        const GAP_AMOUNT: u16 = 5;
1400        const ID_TEST_RANGE: std::ops::RangeInclusive<u16> = 1..=30;
1401
1402        #[cfg(not(target_family = "wasm"))]
1403        let (path, db_file) = tmp_db_file();
1404        #[cfg(target_family = "wasm")]
1405        let (path, _) = tmp_db_file();
1406
1407        let keystore = core_crypto_keystore::Connection::open_with_key(path, "test")
1408            .await
1409            .unwrap();
1410        keystore.new_transaction().await.unwrap();
1411        let alice = ProteusCentral::try_new(&keystore).await.unwrap();
1412
1413        for i in ID_TEST_RANGE {
1414            let (pk_id, pkb) = alice.new_prekey_auto(&keystore).await.unwrap();
1415            assert_eq!(i, pk_id);
1416            let prekey = proteus_wasm::keys::PreKeyBundle::deserialise(&pkb).unwrap();
1417            assert_eq!(prekey.prekey_id.value(), pk_id);
1418        }
1419
1420        use rand::Rng as _;
1421        let mut rng = rand::thread_rng();
1422        let mut gap_ids: Vec<u16> = (0..GAP_AMOUNT).map(|_| rng.gen_range(ID_TEST_RANGE)).collect();
1423        gap_ids.sort();
1424        gap_ids.dedup();
1425        while gap_ids.len() < GAP_AMOUNT as usize {
1426            gap_ids.push(rng.gen_range(ID_TEST_RANGE));
1427            gap_ids.sort();
1428            gap_ids.dedup();
1429        }
1430        for gap_id in gap_ids.iter() {
1431            keystore.remove::<ProteusPrekey, _>(gap_id.to_le_bytes()).await.unwrap();
1432        }
1433
1434        gap_ids.sort();
1435
1436        for gap_id in gap_ids.iter() {
1437            let (pk_id, pkb) = alice.new_prekey_auto(&keystore).await.unwrap();
1438            assert_eq!(pk_id, *gap_id);
1439            let prekey = proteus_wasm::keys::PreKeyBundle::deserialise(&pkb).unwrap();
1440            assert_eq!(prekey.prekey_id.value(), *gap_id);
1441        }
1442
1443        let mut gap_ids: Vec<u16> = (0..GAP_AMOUNT).map(|_| rng.gen_range(ID_TEST_RANGE)).collect();
1444        gap_ids.sort();
1445        gap_ids.dedup();
1446        while gap_ids.len() < GAP_AMOUNT as usize {
1447            gap_ids.push(rng.gen_range(ID_TEST_RANGE));
1448            gap_ids.sort();
1449            gap_ids.dedup();
1450        }
1451        for gap_id in gap_ids.iter() {
1452            keystore.remove::<ProteusPrekey, _>(gap_id.to_le_bytes()).await.unwrap();
1453        }
1454
1455        let potential_range = *ID_TEST_RANGE.end()..=(*ID_TEST_RANGE.end() * 2);
1456        let potential_range_check = potential_range.clone();
1457        for _ in potential_range {
1458            let (pk_id, pkb) = alice.new_prekey_auto(&keystore).await.unwrap();
1459            assert!(gap_ids.contains(&pk_id) || potential_range_check.contains(&pk_id));
1460            let prekey = proteus_wasm::keys::PreKeyBundle::deserialise(&pkb).unwrap();
1461            assert_eq!(prekey.prekey_id.value(), pk_id);
1462        }
1463        keystore.commit_transaction().await.unwrap();
1464        keystore.wipe().await.unwrap();
1465        #[cfg(not(target_family = "wasm"))]
1466        drop(db_file);
1467    }
1468
1469    #[cfg(all(feature = "cryptobox-migrate", not(target_family = "wasm")))]
1470    #[async_std::test]
1471    async fn can_import_cryptobox() {
1472        use crate::CryptoboxMigrationErrorKind;
1473
1474        let session_id = uuid::Uuid::new_v4().hyphenated().to_string();
1475
1476        let cryptobox_folder = tempfile::tempdir().unwrap();
1477        let alice = cryptobox::CBox::file_open(cryptobox_folder.path()).unwrap();
1478        let alice_fingerprint = alice.fingerprint();
1479
1480        let mut bob = CryptoboxLike::init();
1481        let bob_pk_bundle = bob.new_prekey();
1482
1483        let alice_pk_id = proteus::keys::PreKeyId::new(1u16);
1484        let alice_pk = alice.new_prekey(alice_pk_id).unwrap();
1485
1486        let mut alice_session = alice
1487            .session_from_prekey(session_id.clone(), &bob_pk_bundle.serialise().unwrap())
1488            .unwrap();
1489
1490        let message = b"Hello world!";
1491
1492        let alice_msg_envelope = alice_session.encrypt(message).unwrap();
1493        let decrypted = bob.decrypt(&session_id, &alice_msg_envelope).await;
1494        assert_eq!(decrypted, message);
1495
1496        alice.session_save(&mut alice_session).unwrap();
1497
1498        let encrypted = bob.encrypt(&session_id, &message[..]);
1499        let decrypted = alice_session.decrypt(&encrypted).unwrap();
1500        assert_eq!(decrypted, message);
1501
1502        alice.session_save(&mut alice_session).unwrap();
1503
1504        drop(alice);
1505
1506        let keystore_dir = tempfile::tempdir().unwrap();
1507        let keystore_file = keystore_dir.path().join("keystore");
1508
1509        let mut keystore =
1510            core_crypto_keystore::Connection::open_with_key(keystore_file.as_os_str().to_string_lossy(), "test")
1511                .await
1512                .unwrap();
1513        keystore.new_transaction().await.unwrap();
1514
1515        let Err(crate::Error::CryptoboxMigration(crate::CryptoboxMigrationError {
1516            source: CryptoboxMigrationErrorKind::ProvidedPathDoesNotExist(_),
1517            ..
1518        })) = ProteusCentral::cryptobox_migrate(&keystore, "invalid path").await
1519        else {
1520            panic!("ProteusCentral::cryptobox_migrate did not throw an error on invalid path");
1521        };
1522
1523        ProteusCentral::cryptobox_migrate(&keystore, &cryptobox_folder.path().to_string_lossy())
1524            .await
1525            .unwrap();
1526
1527        let mut proteus_central = ProteusCentral::try_new(&keystore).await.unwrap();
1528
1529        // Identity check
1530        assert_eq!(proteus_central.fingerprint(), alice_fingerprint);
1531
1532        // Session integrity check
1533        let alice_new_session_lock = proteus_central
1534            .session(&session_id, &mut keystore)
1535            .await
1536            .unwrap()
1537            .unwrap();
1538        let alice_new_session = alice_new_session_lock.read().await;
1539        assert_eq!(
1540            alice_new_session.session.local_identity().fingerprint(),
1541            alice_session.fingerprint_local()
1542        );
1543        assert_eq!(
1544            alice_new_session.session.remote_identity().fingerprint(),
1545            alice_session.fingerprint_remote()
1546        );
1547
1548        drop(alice_new_session);
1549        drop(alice_new_session_lock);
1550
1551        // Prekey integrity check
1552        let keystore_pk = keystore.prekey(1).await.unwrap().unwrap();
1553        let keystore_pk = proteus_wasm::keys::PreKey::deserialise(&keystore_pk).unwrap();
1554        assert_eq!(alice_pk.prekey_id.value(), keystore_pk.key_id.value());
1555        assert_eq!(
1556            alice_pk.public_key.fingerprint(),
1557            keystore_pk.key_pair.public_key.fingerprint()
1558        );
1559
1560        // Make sure ProteusCentral can still keep communicating with bob
1561        let encrypted = proteus_central
1562            .encrypt(&mut keystore, &session_id, &message[..])
1563            .await
1564            .unwrap();
1565        let decrypted = bob.decrypt(&session_id, &encrypted).await;
1566
1567        assert_eq!(&decrypted, &message[..]);
1568
1569        // CL-110 assertion
1570        // Happens when a migrated client ratchets just after migration. It does not happen when ratchet is not required
1571        // Having alice(A), bob(B) and migration(M), you can reproduce this behaviour with `[A->B][B->A] M [A->B][B->A]`
1572        // However you won't reproduce it like this because migrated alice does not ratchet `[A->B][B->A] M [B->A][A->B]`
1573        let encrypted = bob.encrypt(&session_id, &message[..]);
1574        let decrypted = proteus_central
1575            .decrypt(&mut keystore, &session_id, &encrypted)
1576            .await
1577            .unwrap();
1578        assert_eq!(&decrypted, &message[..]);
1579
1580        proteus_central.session_save(&mut keystore, &session_id).await.unwrap();
1581        keystore.commit_transaction().await.unwrap();
1582        keystore.wipe().await.unwrap();
1583    }
1584
1585    cfg_if::cfg_if! {
1586        if #[cfg(all(feature = "cryptobox-migrate", target_family = "wasm"))] {
1587            // use wasm_bindgen::prelude::*;
1588            const CRYPTOBOX_JS_DBNAME: &str = "cryptobox-migrate-test";
1589            // wasm-bindgen-test-runner is behaving weird with inline_js stuff (aka not working basically), which we had previously
1590            // So instead we emulate how cryptobox-js works
1591            // Returns Promise<JsString>
1592            fn run_cryptobox(alice: CryptoboxLike) -> js_sys::Promise {
1593                wasm_bindgen_futures::future_to_promise(async move {
1594                    use rexie::{Rexie, ObjectStore, TransactionMode};
1595                    use wasm_bindgen::JsValue;
1596
1597                    // Delete the maybe past database to make sure we start fresh
1598                    Rexie::builder(CRYPTOBOX_JS_DBNAME)
1599                        .delete()
1600                        .await.map_err(|err| err.to_string())?;
1601
1602                    let rexie = Rexie::builder(CRYPTOBOX_JS_DBNAME)
1603                        .version(1)
1604                        .add_object_store(ObjectStore::new("keys").auto_increment(false))
1605                        .add_object_store(ObjectStore::new("prekeys").auto_increment(false))
1606                        .add_object_store(ObjectStore::new("sessions").auto_increment(false))
1607                        .build()
1608                        .await.map_err(|err| err.to_string())?;
1609
1610                    // Add identity key
1611                    let transaction = rexie.transaction(&["keys"], TransactionMode::ReadWrite).map_err(|err| err.to_string())?;
1612                    let store = transaction.store("keys").map_err(|err| err.to_string())?;
1613
1614                    use base64::Engine as _;
1615                    let json = serde_json::json!({
1616                        "created": 0,
1617                        "id": "local_identity",
1618                        "serialised": base64::prelude::BASE64_STANDARD.encode(alice.identity.serialise().unwrap()),
1619                        "version": "1.0"
1620                    });
1621                    let js_value = serde_wasm_bindgen::to_value(&json)?;
1622
1623                    store.add(&js_value, Some(&JsValue::from_str("local_identity"))).await.map_err(|err| err.to_string())?;
1624
1625                    // Add prekeys
1626                    let transaction = rexie.transaction(&["prekeys"], TransactionMode::ReadWrite).map_err(|err| err.to_string())?;
1627                    let store = transaction.store("prekeys").map_err(|err| err.to_string())?;
1628                    for prekey in alice.prekeys.0.into_iter() {
1629                        let id = prekey.key_id.value().to_string();
1630                        let json = serde_json::json!({
1631                            "created": 0,
1632                            "id": &id,
1633                            "serialised": base64::prelude::BASE64_STANDARD.encode(prekey.serialise().unwrap()),
1634                            "version": "1.0"
1635                        });
1636                        let js_value = serde_wasm_bindgen::to_value(&json)?;
1637                        store.add(&js_value, Some(&JsValue::from_str(&id))).await.map_err(|err| err.to_string())?;
1638                    }
1639
1640                    // Add sessions
1641                    let transaction = rexie.transaction(&["sessions"], TransactionMode::ReadWrite).map_err(|err| err.to_string())?;
1642                    let store = transaction.store("sessions").map_err(|err| err.to_string())?;
1643                    for (session_id, session) in alice.sessions.into_iter() {
1644                        let json = serde_json::json!({
1645                            "created": 0,
1646                            "id": session_id,
1647                            "serialised": base64::prelude::BASE64_STANDARD.encode(session.serialise().unwrap()),
1648                            "version": "1.0"
1649                        });
1650
1651                        let js_value = serde_wasm_bindgen::to_value(&json)?;
1652                        store.add(&js_value, Some(&JsValue::from_str(&session_id))).await.map_err(|err| err.to_string())?;
1653                    }
1654
1655                    Ok(JsValue::UNDEFINED)
1656                })
1657            }
1658
1659            #[wasm_bindgen_test]
1660            async fn can_import_cryptobox() {
1661                let session_id = uuid::Uuid::new_v4().hyphenated().to_string();
1662
1663                let mut alice = CryptoboxLike::init();
1664                let alice_fingerprint = alice.fingerprint();
1665                const PREKEY_COUNT: usize = 10;
1666                let prekey_iter_range = 0..PREKEY_COUNT;
1667                // Save prekey bundles for later to check if they're the same after migration
1668                let prekey_bundles: Vec<proteus_wasm::keys::PreKeyBundle> = prekey_iter_range.clone().map(|_| alice.new_prekey()).collect();
1669
1670                // Ensure alice and bob can communicate before migration
1671                let mut bob = CryptoboxLike::init();
1672                let bob_pk_bundle = bob.new_prekey();
1673                let message = b"Hello world!";
1674
1675                alice.init_session_from_prekey_bundle(&session_id, &bob_pk_bundle.serialise().unwrap());
1676                let alice_to_bob_message = alice.encrypt(&session_id, message);
1677                let decrypted = bob.decrypt(&session_id, &alice_to_bob_message).await;
1678                assert_eq!(&message[..], decrypted.as_slice());
1679
1680                let bob_to_alice_message = bob.encrypt(&session_id, message);
1681                let decrypted = alice.decrypt(&session_id, &bob_to_alice_message).await;
1682                assert_eq!(&message[..], decrypted.as_slice());
1683
1684                let alice_session = alice.session(&session_id);
1685                let alice_session_fingerprint_local = alice_session.local_identity().fingerprint();
1686                let alice_session_fingerprint_remote = alice_session.remote_identity().fingerprint();
1687
1688                let _ = wasm_bindgen_futures::JsFuture::from(run_cryptobox(alice)).await.unwrap();
1689                let mut keystore = core_crypto_keystore::Connection::open_with_key(&format!("{CRYPTOBOX_JS_DBNAME}-imported"), "test").await.unwrap();
1690                keystore.new_transaction().await.unwrap();
1691                let Err(crate::Error::CryptoboxMigration(crate::CryptoboxMigrationError{
1692                    source: crate::CryptoboxMigrationErrorKind::ProvidedPathDoesNotExist(_),
1693                    ..
1694                })) = ProteusCentral::cryptobox_migrate(&keystore, "invalid path").await else {
1695                    panic!("ProteusCentral::cryptobox_migrate did not throw an error on invalid path");
1696                };
1697
1698                ProteusCentral::cryptobox_migrate(&keystore, CRYPTOBOX_JS_DBNAME).await.unwrap();
1699
1700                let mut proteus_central = ProteusCentral::try_new(&keystore).await.unwrap();
1701
1702                // Identity check
1703                assert_eq!(proteus_central.fingerprint(), alice_fingerprint);
1704
1705                // Session integrity check
1706                let alice_new_session_lock = proteus_central
1707                    .session(&session_id, &mut keystore)
1708                    .await
1709                    .unwrap()
1710                    .unwrap();
1711                let alice_new_session = alice_new_session_lock.read().await;
1712                assert_eq!(
1713                    alice_new_session.session.local_identity().fingerprint(),
1714                    alice_session_fingerprint_local
1715                );
1716                assert_eq!(
1717                    alice_new_session.session.remote_identity().fingerprint(),
1718                    alice_session_fingerprint_remote
1719                );
1720
1721                drop(alice_new_session);
1722                drop(alice_new_session_lock);
1723
1724                // Prekey integrity check
1725                for i in prekey_iter_range {
1726                    let prekey_id = (i + 1) as u16;
1727                    let keystore_pk = keystore.prekey(prekey_id).await.unwrap().unwrap();
1728                    let keystore_pk = proteus_wasm::keys::PreKey::deserialise(&keystore_pk).unwrap();
1729                    let alice_pk = &prekey_bundles[i];
1730
1731                    assert_eq!(alice_pk.prekey_id.value(), keystore_pk.key_id.value());
1732                    assert_eq!(
1733                        alice_pk.public_key.fingerprint(),
1734                        keystore_pk.key_pair.public_key.fingerprint()
1735                    );
1736                }
1737
1738
1739                // Make sure ProteusCentral can still keep communicating with bob
1740                let encrypted = proteus_central.encrypt(&mut keystore, &session_id, &message[..]).await.unwrap();
1741                let decrypted = bob.decrypt(&session_id, &encrypted).await;
1742
1743                assert_eq!(&decrypted, &message[..]);
1744
1745                // CL-110 assertion
1746                let encrypted = bob.encrypt(&session_id, &message[..]);
1747                let decrypted = proteus_central
1748                    .decrypt(&mut keystore, &session_id, &encrypted)
1749                    .await
1750                    .unwrap();
1751                assert_eq!(&decrypted, &message[..]);
1752                keystore.commit_transaction().await.unwrap();
1753
1754                keystore.wipe().await.unwrap();
1755            }
1756        }
1757    }
1758}