core_crypto/
proteus.rs

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