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    Database 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(&mut self, store: &mut core_crypto_keystore::Database, ciphertext: &[u8]) -> Result<Vec<u8>> {
39        let envelope = Envelope::deserialise(ciphertext).map_err(ProteusError::wrap("deserializing envelope"))?;
40        self.session
41            .decrypt(store, &envelope)
42            .await
43            .map_err(ProteusError::wrap("decrypting message for proteus session"))
44            .map_err(Into::into)
45    }
46
47    /// Returns the session identifier
48    pub fn identifier(&self) -> &str {
49        &self.identifier
50    }
51
52    /// Returns the public key fingerprint of the local identity (= self identity)
53    pub fn fingerprint_local(&self) -> String {
54        self.session.local_identity().fingerprint()
55    }
56
57    /// Returns the public key fingerprint of the remote identity (= client you're communicating with)
58    pub fn fingerprint_remote(&self) -> String {
59        self.session.remote_identity().fingerprint()
60    }
61}
62
63#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
64#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
65impl GroupStoreEntity for ProteusConversationSession {
66    type RawStoreValue = core_crypto_keystore::entities::ProteusSession;
67    type IdentityType = Arc<proteus_wasm::keys::IdentityKeyPair>;
68
69    async fn fetch_from_id(
70        id: &[u8],
71        identity: Option<Self::IdentityType>,
72        keystore: &impl FetchFromDatabase,
73    ) -> crate::Result<Option<Self>> {
74        let result = keystore
75            .find::<Self::RawStoreValue>(id)
76            .await
77            .map_err(KeystoreError::wrap("finding raw group store entity by id"))?;
78        let Some(store_value) = result else {
79            return Ok(None);
80        };
81
82        let Some(identity) = identity else {
83            return Err(crate::Error::ProteusNotInitialized);
84        };
85
86        let session = proteus_wasm::session::Session::deserialise(identity, &store_value.session)
87            .map_err(ProteusError::wrap("deserializing session"))?;
88
89        Ok(Some(Self {
90            identifier: store_value.id.clone(),
91            session,
92        }))
93    }
94}
95
96impl CoreCrypto {
97    /// Proteus session accessor
98    ///
99    /// Warning: The Proteus client **MUST** be initialized with
100    /// [crate::transaction_context::TransactionContext::proteus_init] first or an error will be
101    /// returned
102    pub async fn proteus_session(
103        &self,
104        session_id: &str,
105    ) -> Result<Option<GroupStoreValue<ProteusConversationSession>>> {
106        let mut mutex = self.proteus.lock().await;
107        let proteus = mutex.as_mut().ok_or(Error::ProteusNotInitialized)?;
108        let keystore = self.mls.crypto_provider.keystore();
109        proteus.session(session_id, &keystore).await
110    }
111
112    /// Proteus session exists
113    ///
114    /// Warning: The Proteus client **MUST** be initialized with
115    /// [crate::transaction_context::TransactionContext::proteus_init] first or an error will be
116    /// returned
117    pub async fn proteus_session_exists(&self, session_id: &str) -> Result<bool> {
118        let mut mutex = self.proteus.lock().await;
119        let proteus = mutex.as_mut().ok_or(Error::ProteusNotInitialized)?;
120        let keystore = self.mls.crypto_provider.keystore();
121        Ok(proteus.session_exists(session_id, &keystore).await)
122    }
123
124    /// Returns the proteus last resort prekey id (u16::MAX = 65535)
125    pub fn proteus_last_resort_prekey_id() -> u16 {
126        ProteusCentral::last_resort_prekey_id()
127    }
128
129    /// Returns the proteus identity's public key fingerprint
130    ///
131    /// Warning: The Proteus client **MUST** be initialized with
132    /// [crate::transaction_context::TransactionContext::proteus_init] first or an error will be
133    /// returned
134    pub async fn proteus_fingerprint(&self) -> Result<String> {
135        let mutex = self.proteus.lock().await;
136        let proteus = mutex.as_ref().ok_or(Error::ProteusNotInitialized)?;
137        Ok(proteus.fingerprint())
138    }
139
140    /// Returns the proteus identity's public key fingerprint
141    ///
142    /// Warning: The Proteus client **MUST** be initialized with
143    /// [crate::transaction_context::TransactionContext::proteus_init] first or an error will be
144    /// returned
145    pub async fn proteus_fingerprint_local(&self, session_id: &str) -> Result<String> {
146        let mut mutex = self.proteus.lock().await;
147        let proteus = mutex.as_mut().ok_or(Error::ProteusNotInitialized)?;
148        let keystore = self.mls.crypto_provider.keystore();
149        proteus.fingerprint_local(session_id, &keystore).await
150    }
151
152    /// Returns the proteus identity's public key fingerprint
153    ///
154    /// Warning: The Proteus client **MUST** be initialized with
155    /// [crate::transaction_context::TransactionContext::proteus_init] first or an error will be
156    /// returned
157    pub async fn proteus_fingerprint_remote(&self, session_id: &str) -> Result<String> {
158        let mut mutex = self.proteus.lock().await;
159        let proteus = mutex.as_mut().ok_or(Error::ProteusNotInitialized)?;
160        let keystore = self.mls.crypto_provider.keystore();
161        proteus.fingerprint_remote(session_id, &keystore).await
162    }
163}
164
165/// Proteus counterpart of [crate::mls::session::Session]
166///
167/// The big difference is that [ProteusCentral] doesn't *own* its own keystore but must borrow it from the outside.
168/// Whether it's exclusively for this struct's purposes or it's shared with our main struct, [crate::mls::session::Session]
169#[derive(Debug)]
170pub struct ProteusCentral {
171    proteus_identity: Arc<IdentityKeyPair>,
172    proteus_sessions: GroupStore<ProteusConversationSession>,
173}
174
175impl ProteusCentral {
176    /// Initializes the [ProteusCentral]
177    pub async fn try_new(keystore: &CryptoKeystore) -> Result<Self> {
178        let proteus_identity: Arc<IdentityKeyPair> = Arc::new(Self::load_or_create_identity(keystore).await?);
179        let proteus_sessions = Self::restore_sessions(keystore, &proteus_identity).await?;
180
181        Ok(Self {
182            proteus_identity,
183            proteus_sessions,
184        })
185    }
186
187    /// Restore proteus sessions from disk
188    pub(crate) async fn reload_sessions(&mut self, keystore: &CryptoKeystore) -> Result<()> {
189        self.proteus_sessions = Self::restore_sessions(keystore, &self.proteus_identity).await?;
190        Ok(())
191    }
192
193    /// This function will try to load a proteus Identity from our keystore; If it cannot, it will create a new one
194    /// This means this function doesn't fail except in cases of deeper errors (such as in the Keystore and other crypto errors)
195    async fn load_or_create_identity(keystore: &CryptoKeystore) -> Result<IdentityKeyPair> {
196        let Some(identity) = keystore
197            .find::<ProteusIdentity>(ProteusIdentity::ID)
198            .await
199            .map_err(KeystoreError::wrap("finding proteus identity"))?
200        else {
201            return Self::create_identity(keystore).await;
202        };
203
204        let sk = identity.sk_raw();
205        let pk = identity.pk_raw();
206
207        // SAFETY: Byte lengths are ensured at the keystore level so this function is safe to call, despite being cursed
208        IdentityKeyPair::from_raw_key_pair(*sk, *pk)
209            .map_err(ProteusError::wrap("constructing identity keypair"))
210            .map_err(Into::into)
211    }
212
213    /// Internal function to create and save a new Proteus Identity
214    async fn create_identity(keystore: &CryptoKeystore) -> Result<IdentityKeyPair> {
215        let kp = IdentityKeyPair::new();
216        let pk = kp.public_key.public_key.as_slice().to_vec();
217
218        let ks_identity = ProteusIdentity {
219            sk: kp.secret_key.to_keypair_bytes().into(),
220            pk,
221        };
222        keystore
223            .save(ks_identity)
224            .await
225            .map_err(KeystoreError::wrap("saving new proteus identity"))?;
226
227        Ok(kp)
228    }
229
230    /// Restores the saved sessions in memory. This is performed automatically on init
231    async fn restore_sessions(
232        keystore: &core_crypto_keystore::Database,
233        identity: &Arc<IdentityKeyPair>,
234    ) -> Result<GroupStore<ProteusConversationSession>> {
235        let mut proteus_sessions = GroupStore::new_with_limit(crate::group_store::ITEM_LIMIT * 2);
236        for session in keystore
237            .find_all::<ProteusSession>(Default::default())
238            .await
239            .map_err(KeystoreError::wrap("finding all proteus sessions"))?
240            .into_iter()
241        {
242            let proteus_session = Session::deserialise(identity.clone(), &session.session)
243                .map_err(ProteusError::wrap("deserializing session"))?;
244
245            let identifier = session.id.clone();
246
247            let proteus_conversation = ProteusConversationSession {
248                identifier: identifier.clone(),
249                session: proteus_session,
250            };
251
252            if proteus_sessions
253                .try_insert(identifier.into_bytes(), proteus_conversation)
254                .is_err()
255            {
256                break;
257            }
258        }
259
260        Ok(proteus_sessions)
261    }
262
263    /// Creates a new session from a prekey
264    pub async fn session_from_prekey(
265        &mut self,
266        session_id: &str,
267        key: &[u8],
268    ) -> Result<GroupStoreValue<ProteusConversationSession>> {
269        let prekey = PreKeyBundle::deserialise(key).map_err(ProteusError::wrap("deserializing prekey bundle"))?;
270        // Note on the `::<>` turbofish below:
271        //
272        // `init_from_prekey` returns an error type which is parametric over some wrapped `E`,
273        // because one variant (not relevant to this particular operation) wraps an error type based
274        // on a parameter of a different function entirely.
275        //
276        // Rust complains here, because it can't figure out what type that `E` should be. After all, it's
277        // not inferrable from this function call! It is also entirely irrelevant in this case.
278        //
279        // We can derive two general rules about error-handling in Rust from this example:
280        //
281        // 1. It's better to make smaller error types where possible, encapsulating fallible operations
282        //    with their own error variants, and then wrapping those errors where required, as opposed to
283        //    creating giant catch-all errors. Doing so also has knock-on benefits with regard to tracing
284        //    the precise origin of the error.
285        // 2. One should never make an error wrapper parametric. If you need to wrap an unknown error,
286        //    it's always better to wrap a `Box<dyn std::error::Error>` than to make your error type parametric.
287        //    The allocation cost of creating the `Box` is utterly trivial in an error-handling path, and
288        //    it avoids parametric virality. (`init_from_prekey` is itself only generic because it returns
289        //    this error type with a type-parametric variant, which the function never returns.)
290        //
291        // In this case, we have the out of band knowledge that `ProteusErrorKind` has a `#[from]` implementation
292        // for `proteus_wasm::session::Error<core_crypto_keystore::CryptoKeystoreError>` and for no other kinds
293        // of session error. So we can safely say that the type of error we are meant to catch here, and
294        // therefore pass in that otherwise-irrelevant type, to ensure that error handling works properly.
295        //
296        // Some people say that if it's stupid but it works, it's not stupid. I disagree. If it's stupid but
297        // it works, that's our cue to seek out even better, non-stupid ways to get things done. I reiterate:
298        // the actual type referred to in this turbofish is nothing but a magic incantation to make error
299        // handling work; it has no bearing on the error retured from this function. How much better would it
300        // have been if `session::Error` were not parametric and we could have avoided the turbofish entirely?
301        let proteus_session = Session::init_from_prekey::<core_crypto_keystore::CryptoKeystoreError>(
302            self.proteus_identity.clone(),
303            prekey,
304        )
305        .map_err(ProteusError::wrap("initializing session from prekey"))?;
306
307        let proteus_conversation = ProteusConversationSession {
308            identifier: session_id.into(),
309            session: proteus_session,
310        };
311
312        self.proteus_sessions.insert(session_id.into(), proteus_conversation);
313
314        Ok(self.proteus_sessions.get(session_id.as_bytes()).unwrap().clone())
315    }
316
317    /// Creates a new proteus Session from a received message
318    pub(crate) async fn session_from_message(
319        &mut self,
320        keystore: &mut CryptoKeystore,
321        session_id: &str,
322        envelope: &[u8],
323    ) -> Result<(GroupStoreValue<ProteusConversationSession>, Vec<u8>)> {
324        let message = Envelope::deserialise(envelope).map_err(ProteusError::wrap("deserialising envelope"))?;
325        let (session, payload) = Session::init_from_message(self.proteus_identity.clone(), keystore, &message)
326            .await
327            .map_err(ProteusError::wrap("initializing session from message"))?;
328
329        let proteus_conversation = ProteusConversationSession {
330            identifier: session_id.into(),
331            session,
332        };
333
334        self.proteus_sessions.insert(session_id.into(), proteus_conversation);
335
336        Ok((
337            self.proteus_sessions.get(session_id.as_bytes()).unwrap().clone(),
338            payload,
339        ))
340    }
341
342    /// Persists a session in store
343    ///
344    /// **Note**: This isn't usually needed as persisting sessions happens automatically when decrypting/encrypting messages and initializing Sessions
345    pub(crate) async fn session_save(&mut self, keystore: &CryptoKeystore, session_id: &str) -> Result<()> {
346        if let Some(session) = self
347            .proteus_sessions
348            .get_fetch(session_id.as_bytes(), keystore, Some(self.proteus_identity.clone()))
349            .await?
350        {
351            Self::session_save_by_ref(keystore, session).await?;
352        }
353
354        Ok(())
355    }
356
357    pub(crate) async fn session_save_by_ref(
358        keystore: &CryptoKeystore,
359        session: GroupStoreValue<ProteusConversationSession>,
360    ) -> Result<()> {
361        let session = session.read().await;
362        let db_session = ProteusSession {
363            id: session.identifier().to_string(),
364            session: session
365                .session
366                .serialise()
367                .map_err(ProteusError::wrap("serializing session"))?,
368        };
369        keystore
370            .save(db_session)
371            .await
372            .map_err(KeystoreError::wrap("saving proteus session"))?;
373        Ok(())
374    }
375
376    /// Deletes a session in the store
377    pub(crate) async fn session_delete(&mut self, keystore: &CryptoKeystore, session_id: &str) -> Result<()> {
378        if keystore.remove::<ProteusSession, _>(session_id).await.is_ok() {
379            let _ = self.proteus_sessions.remove(session_id.as_bytes());
380        }
381        Ok(())
382    }
383
384    /// Session accessor
385    pub(crate) async fn session(
386        &mut self,
387        session_id: &str,
388        keystore: &CryptoKeystore,
389    ) -> Result<Option<GroupStoreValue<ProteusConversationSession>>> {
390        self.proteus_sessions
391            .get_fetch(session_id.as_bytes(), keystore, Some(self.proteus_identity.clone()))
392            .await
393    }
394
395    /// Session exists
396    pub(crate) async fn session_exists(&mut self, session_id: &str, keystore: &CryptoKeystore) -> bool {
397        self.session(session_id, keystore).await.ok().flatten().is_some()
398    }
399
400    /// Decrypt a proteus message for an already existing session
401    /// Note: This cannot be used for handshake messages, see [ProteusCentral::session_from_message]
402    pub(crate) async fn decrypt(
403        &mut self,
404        keystore: &mut CryptoKeystore,
405        session_id: &str,
406        ciphertext: &[u8],
407    ) -> Result<Vec<u8>> {
408        let session = self
409            .proteus_sessions
410            .get_fetch(session_id.as_bytes(), keystore, Some(self.proteus_identity.clone()))
411            .await?
412            .ok_or(LeafError::ConversationNotFound(session_id.as_bytes().into()))
413            .map_err(ProteusError::wrap("getting session"))?;
414
415        let plaintext = session.write().await.decrypt(keystore, ciphertext).await?;
416        ProteusCentral::session_save_by_ref(keystore, session).await?;
417
418        Ok(plaintext)
419    }
420
421    /// Encrypt a message for a session
422    pub(crate) async fn encrypt(
423        &mut self,
424        keystore: &mut CryptoKeystore,
425        session_id: &str,
426        plaintext: &[u8],
427    ) -> Result<Vec<u8>> {
428        let session = self
429            .session(session_id, keystore)
430            .await?
431            .ok_or(LeafError::ConversationNotFound(session_id.as_bytes().into()))
432            .map_err(ProteusError::wrap("getting session"))?;
433
434        let ciphertext = session.write().await.encrypt(plaintext)?;
435        ProteusCentral::session_save_by_ref(keystore, session).await?;
436
437        Ok(ciphertext)
438    }
439
440    /// Encrypts a message for a list of sessions
441    /// This is mainly used for conversations with multiple clients, this allows to minimize FFI roundtrips
442    pub(crate) async fn encrypt_batched(
443        &mut self,
444        keystore: &mut CryptoKeystore,
445        sessions: &[impl AsRef<str>],
446        plaintext: &[u8],
447    ) -> Result<HashMap<String, Vec<u8>>> {
448        let mut acc = HashMap::new();
449        for session_id in sessions {
450            if let Some(session) = self.session(session_id.as_ref(), keystore).await? {
451                let mut session_w = session.write().await;
452                acc.insert(session_w.identifier.clone(), session_w.encrypt(plaintext)?);
453                drop(session_w);
454
455                ProteusCentral::session_save_by_ref(keystore, session).await?;
456            }
457        }
458        Ok(acc)
459    }
460
461    /// Generates a new Proteus PreKey, stores it in the keystore and returns a serialized PreKeyBundle to be consumed externally
462    pub(crate) async fn new_prekey(&self, id: u16, keystore: &CryptoKeystore) -> Result<Vec<u8>> {
463        use proteus_wasm::keys::{PreKey, PreKeyId};
464
465        let prekey_id = PreKeyId::new(id);
466        let prekey = PreKey::new(prekey_id);
467        let keystore_prekey = core_crypto_keystore::entities::ProteusPrekey::from_raw(
468            id,
469            prekey.serialise().map_err(ProteusError::wrap("serialising prekey"))?,
470        );
471        let bundle = PreKeyBundle::new(self.proteus_identity.as_ref().public_key.clone(), &prekey);
472        let bundle = bundle
473            .serialise()
474            .map_err(ProteusError::wrap("serialising prekey bundle"))?;
475        keystore
476            .save(keystore_prekey)
477            .await
478            .map_err(KeystoreError::wrap("saving keystore prekey"))?;
479        Ok(bundle)
480    }
481
482    /// Generates a new Proteus Prekey, with an automatically auto-incremented ID.
483    ///
484    /// See [ProteusCentral::new_prekey]
485    pub(crate) async fn new_prekey_auto(&self, keystore: &CryptoKeystore) -> Result<(u16, Vec<u8>)> {
486        let id = core_crypto_keystore::entities::ProteusPrekey::get_free_id(keystore)
487            .await
488            .map_err(KeystoreError::wrap("getting proteus prekey by id"))?;
489        Ok((id, self.new_prekey(id, keystore).await?))
490    }
491
492    /// Returns the Proteus last resort prekey ID (u16::MAX = 65535 = 0xFFFF)
493    pub fn last_resort_prekey_id() -> u16 {
494        proteus_wasm::keys::MAX_PREKEY_ID.value()
495    }
496
497    /// Returns the Proteus last resort prekey
498    /// If it cannot be found, one will be created.
499    pub(crate) async fn last_resort_prekey(&self, keystore: &CryptoKeystore) -> Result<Vec<u8>> {
500        let last_resort = if let Some(last_resort) = keystore
501            .find::<core_crypto_keystore::entities::ProteusPrekey>(
502                Self::last_resort_prekey_id().to_le_bytes().as_slice(),
503            )
504            .await
505            .map_err(KeystoreError::wrap("finding proteus prekey"))?
506        {
507            proteus_wasm::keys::PreKey::deserialise(&last_resort.prekey)
508                .map_err(ProteusError::wrap("deserialising proteus prekey"))?
509        } else {
510            let last_resort = proteus_wasm::keys::PreKey::last_resort();
511
512            use core_crypto_keystore::CryptoKeystoreProteus as _;
513            keystore
514                .proteus_store_prekey(
515                    Self::last_resort_prekey_id(),
516                    &last_resort
517                        .serialise()
518                        .map_err(ProteusError::wrap("serialising last resort prekey"))?,
519                )
520                .await
521                .map_err(KeystoreError::wrap("storing proteus prekey"))?;
522
523            last_resort
524        };
525
526        let bundle = PreKeyBundle::new(self.proteus_identity.as_ref().public_key.clone(), &last_resort);
527        let bundle = bundle
528            .serialise()
529            .map_err(ProteusError::wrap("serialising prekey bundle"))?;
530
531        Ok(bundle)
532    }
533
534    /// Proteus identity keypair
535    pub fn identity(&self) -> &IdentityKeyPair {
536        self.proteus_identity.as_ref()
537    }
538
539    /// Proteus Public key hex-encoded fingerprint
540    pub fn fingerprint(&self) -> String {
541        self.proteus_identity.as_ref().public_key.fingerprint()
542    }
543
544    /// Proteus Session local hex-encoded fingerprint
545    ///
546    /// # Errors
547    /// When the session is not found
548    pub(crate) async fn fingerprint_local(&mut self, session_id: &str, keystore: &CryptoKeystore) -> Result<String> {
549        let session = self
550            .session(session_id, keystore)
551            .await?
552            .ok_or(LeafError::ConversationNotFound(session_id.as_bytes().into()))
553            .map_err(ProteusError::wrap("getting session"))?;
554        let fingerprint = session.read().await.fingerprint_local();
555        Ok(fingerprint)
556    }
557
558    /// Proteus Session remote hex-encoded fingerprint
559    ///
560    /// # Errors
561    /// When the session is not found
562    pub(crate) async fn fingerprint_remote(&mut self, session_id: &str, keystore: &CryptoKeystore) -> Result<String> {
563        let session = self
564            .session(session_id, keystore)
565            .await?
566            .ok_or(LeafError::ConversationNotFound(session_id.as_bytes().into()))
567            .map_err(ProteusError::wrap("getting session"))?;
568        let fingerprint = session.read().await.fingerprint_remote();
569        Ok(fingerprint)
570    }
571
572    /// Hex-encoded fingerprint of the given prekey
573    ///
574    /// # Errors
575    /// If the prekey cannot be deserialized
576    pub fn fingerprint_prekeybundle(prekey: &[u8]) -> Result<String> {
577        let prekey = PreKeyBundle::deserialise(prekey).map_err(ProteusError::wrap("deserialising prekey bundle"))?;
578        Ok(prekey.identity_key.fingerprint())
579    }
580}
581
582#[cfg(test)]
583mod tests {
584    use crate::{
585        prelude::{CertificateBundle, ClientIdentifier, MlsCredentialType, Session, SessionConfig},
586        test_utils::{proteus_utils::*, x509::X509TestChain, *},
587    };
588
589    use crate::prelude::INITIAL_KEYING_MATERIAL_COUNT;
590
591    use super::*;
592
593    use core_crypto_keystore::{ConnectionType, DatabaseKey};
594
595    #[apply(all_cred_cipher)]
596    async fn cc_can_init(case: TestContext) {
597        #[cfg(not(target_family = "wasm"))]
598        let (path, db_file) = tmp_db_file();
599        #[cfg(target_family = "wasm")]
600        let (path, _) = tmp_db_file();
601        let client_id = "alice".into();
602        let cfg = SessionConfig::builder()
603            .persistent(&path)
604            .database_key(DatabaseKey::generate())
605            .client_id(client_id)
606            .ciphersuites([case.ciphersuite()])
607            .build()
608            .validate()
609            .unwrap();
610
611        let cc: CoreCrypto = Session::try_new(cfg).await.unwrap().into();
612        let context = cc.new_transaction().await.unwrap();
613        assert!(context.proteus_init().await.is_ok());
614        assert!(context.proteus_new_prekey(1).await.is_ok());
615        context.finish().await.unwrap();
616        #[cfg(not(target_family = "wasm"))]
617        drop(db_file);
618    }
619
620    #[apply(all_cred_cipher)]
621    async fn cc_can_2_phase_init(case: TestContext) {
622        #[cfg(not(target_family = "wasm"))]
623        let (path, db_file) = tmp_db_file();
624        #[cfg(target_family = "wasm")]
625        let (path, _) = tmp_db_file();
626        // we are deferring MLS initialization here, not passing a MLS 'client_id' yet
627        let cfg = SessionConfig::builder()
628            .persistent(&path)
629            .database_key(DatabaseKey::generate())
630            .ciphersuites([case.ciphersuite()])
631            .build()
632            .validate()
633            .unwrap();
634
635        let cc: CoreCrypto = Session::try_new(cfg).await.unwrap().into();
636        let transaction = cc.new_transaction().await.unwrap();
637        let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
638        x509_test_chain.register_with_central(&transaction).await;
639        assert!(transaction.proteus_init().await.is_ok());
640        // proteus is initialized, prekeys can be generated
641        assert!(transaction.proteus_new_prekey(1).await.is_ok());
642        // 👇 and so a unique 'client_id' can be fetched from wire-server
643        let client_id = "alice";
644        let identifier = match case.credential_type {
645            MlsCredentialType::Basic => ClientIdentifier::Basic(client_id.into()),
646            MlsCredentialType::X509 => {
647                CertificateBundle::rand_identifier(client_id, &[x509_test_chain.find_local_intermediate_ca()])
648            }
649        };
650        transaction
651            .mls_init(
652                identifier,
653                vec![case.ciphersuite()],
654                Some(INITIAL_KEYING_MATERIAL_COUNT),
655            )
656            .await
657            .unwrap();
658        // expect MLS to work
659        assert_eq!(
660            transaction
661                .get_or_create_client_keypackages(case.ciphersuite(), case.credential_type, 2)
662                .await
663                .unwrap()
664                .len(),
665            2
666        );
667        #[cfg(not(target_family = "wasm"))]
668        drop(db_file);
669    }
670
671    #[macro_rules_attribute::apply(smol_macros::test)]
672    async fn can_init() {
673        #[cfg(not(target_family = "wasm"))]
674        let (path, db_file) = tmp_db_file();
675        #[cfg(target_family = "wasm")]
676        let (path, _) = tmp_db_file();
677        let key = DatabaseKey::generate();
678        let keystore = core_crypto_keystore::Database::open(ConnectionType::Persistent(&path), &key)
679            .await
680            .unwrap();
681        keystore.new_transaction().await.unwrap();
682        let central = ProteusCentral::try_new(&keystore).await.unwrap();
683        let identity = (*central.proteus_identity).clone();
684        keystore.commit_transaction().await.unwrap();
685
686        let keystore = core_crypto_keystore::Database::open(ConnectionType::Persistent(&path), &key)
687            .await
688            .unwrap();
689        keystore.new_transaction().await.unwrap();
690        let central = ProteusCentral::try_new(&keystore).await.unwrap();
691        keystore.commit_transaction().await.unwrap();
692        assert_eq!(identity, *central.proteus_identity);
693
694        keystore.wipe().await.unwrap();
695        #[cfg(not(target_family = "wasm"))]
696        drop(db_file);
697    }
698
699    #[macro_rules_attribute::apply(smol_macros::test)]
700    async fn can_talk_with_proteus() {
701        #[cfg(not(target_family = "wasm"))]
702        let (path, db_file) = tmp_db_file();
703        #[cfg(target_family = "wasm")]
704        let (path, _) = tmp_db_file();
705
706        let session_id = uuid::Uuid::new_v4().hyphenated().to_string();
707
708        let key = DatabaseKey::generate();
709        let mut keystore = core_crypto_keystore::Database::open(ConnectionType::Persistent(&path), &key)
710            .await
711            .unwrap();
712        keystore.new_transaction().await.unwrap();
713
714        let mut alice = ProteusCentral::try_new(&keystore).await.unwrap();
715
716        let mut bob = CryptoboxLike::init();
717        let bob_pk_bundle = bob.new_prekey();
718
719        alice
720            .session_from_prekey(&session_id, &bob_pk_bundle.serialise().unwrap())
721            .await
722            .unwrap();
723
724        let message = b"Hello world";
725
726        let encrypted = alice.encrypt(&mut keystore, &session_id, message).await.unwrap();
727        let decrypted = bob.decrypt(&session_id, &encrypted).await;
728        assert_eq!(decrypted, message);
729
730        let encrypted = bob.encrypt(&session_id, message);
731        let decrypted = alice.decrypt(&mut keystore, &session_id, &encrypted).await.unwrap();
732        assert_eq!(decrypted, message);
733
734        keystore.commit_transaction().await.unwrap();
735        keystore.wipe().await.unwrap();
736        #[cfg(not(target_family = "wasm"))]
737        drop(db_file);
738    }
739
740    #[macro_rules_attribute::apply(smol_macros::test)]
741    async fn can_produce_proteus_consumed_prekeys() {
742        #[cfg(not(target_family = "wasm"))]
743        let (path, db_file) = tmp_db_file();
744        #[cfg(target_family = "wasm")]
745        let (path, _) = tmp_db_file();
746
747        let session_id = uuid::Uuid::new_v4().hyphenated().to_string();
748
749        let key = DatabaseKey::generate();
750        let mut keystore = core_crypto_keystore::Database::open(ConnectionType::Persistent(&path), &key)
751            .await
752            .unwrap();
753        keystore.new_transaction().await.unwrap();
754        let mut alice = ProteusCentral::try_new(&keystore).await.unwrap();
755
756        let mut bob = CryptoboxLike::init();
757
758        let alice_prekey_bundle_ser = alice.new_prekey(1, &keystore).await.unwrap();
759
760        bob.init_session_from_prekey_bundle(&session_id, &alice_prekey_bundle_ser);
761        let message = b"Hello world!";
762        let encrypted = bob.encrypt(&session_id, message);
763
764        let (_, decrypted) = alice
765            .session_from_message(&mut keystore, &session_id, &encrypted)
766            .await
767            .unwrap();
768
769        assert_eq!(message, decrypted.as_slice());
770
771        let encrypted = alice.encrypt(&mut keystore, &session_id, message).await.unwrap();
772        let decrypted = bob.decrypt(&session_id, &encrypted).await;
773
774        assert_eq!(message, decrypted.as_slice());
775        keystore.commit_transaction().await.unwrap();
776        keystore.wipe().await.unwrap();
777        #[cfg(not(target_family = "wasm"))]
778        drop(db_file);
779    }
780
781    #[macro_rules_attribute::apply(smol_macros::test)]
782    async fn auto_prekeys_are_sequential() {
783        use core_crypto_keystore::entities::ProteusPrekey;
784        const GAP_AMOUNT: u16 = 5;
785        const ID_TEST_RANGE: std::ops::RangeInclusive<u16> = 1..=30;
786
787        #[cfg(not(target_family = "wasm"))]
788        let (path, db_file) = tmp_db_file();
789        #[cfg(target_family = "wasm")]
790        let (path, _) = tmp_db_file();
791
792        let key = DatabaseKey::generate();
793        let keystore = core_crypto_keystore::Database::open(ConnectionType::Persistent(&path), &key)
794            .await
795            .unwrap();
796        keystore.new_transaction().await.unwrap();
797        let alice = ProteusCentral::try_new(&keystore).await.unwrap();
798
799        for i in ID_TEST_RANGE {
800            let (pk_id, pkb) = alice.new_prekey_auto(&keystore).await.unwrap();
801            assert_eq!(i, pk_id);
802            let prekey = proteus_wasm::keys::PreKeyBundle::deserialise(&pkb).unwrap();
803            assert_eq!(prekey.prekey_id.value(), pk_id);
804        }
805
806        use rand::Rng as _;
807        let mut rng = rand::thread_rng();
808        let mut gap_ids: Vec<u16> = (0..GAP_AMOUNT).map(|_| rng.gen_range(ID_TEST_RANGE)).collect();
809        gap_ids.sort();
810        gap_ids.dedup();
811        while gap_ids.len() < GAP_AMOUNT as usize {
812            gap_ids.push(rng.gen_range(ID_TEST_RANGE));
813            gap_ids.sort();
814            gap_ids.dedup();
815        }
816        for gap_id in gap_ids.iter() {
817            keystore.remove::<ProteusPrekey, _>(gap_id.to_le_bytes()).await.unwrap();
818        }
819
820        gap_ids.sort();
821
822        for gap_id in gap_ids.iter() {
823            let (pk_id, pkb) = alice.new_prekey_auto(&keystore).await.unwrap();
824            assert_eq!(pk_id, *gap_id);
825            let prekey = proteus_wasm::keys::PreKeyBundle::deserialise(&pkb).unwrap();
826            assert_eq!(prekey.prekey_id.value(), *gap_id);
827        }
828
829        let mut gap_ids: Vec<u16> = (0..GAP_AMOUNT).map(|_| rng.gen_range(ID_TEST_RANGE)).collect();
830        gap_ids.sort();
831        gap_ids.dedup();
832        while gap_ids.len() < GAP_AMOUNT as usize {
833            gap_ids.push(rng.gen_range(ID_TEST_RANGE));
834            gap_ids.sort();
835            gap_ids.dedup();
836        }
837        for gap_id in gap_ids.iter() {
838            keystore.remove::<ProteusPrekey, _>(gap_id.to_le_bytes()).await.unwrap();
839        }
840
841        let potential_range = *ID_TEST_RANGE.end()..=(*ID_TEST_RANGE.end() * 2);
842        let potential_range_check = potential_range.clone();
843        for _ in potential_range {
844            let (pk_id, pkb) = alice.new_prekey_auto(&keystore).await.unwrap();
845            assert!(gap_ids.contains(&pk_id) || potential_range_check.contains(&pk_id));
846            let prekey = proteus_wasm::keys::PreKeyBundle::deserialise(&pkb).unwrap();
847            assert_eq!(prekey.prekey_id.value(), pk_id);
848        }
849        keystore.commit_transaction().await.unwrap();
850        keystore.wipe().await.unwrap();
851        #[cfg(not(target_family = "wasm"))]
852        drop(db_file);
853    }
854}