core_crypto/
proteus.rs

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