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