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