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
17pub type SessionIdentifier = String;
19
20#[derive(Debug)]
22pub struct ProteusConversationSession {
23 pub(crate) identifier: SessionIdentifier,
24 pub(crate) session: Session<Arc<IdentityKeyPair>>,
25}
26
27impl ProteusConversationSession {
28 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 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 pub fn identifier(&self) -> &str {
53 &self.identifier
54 }
55
56 pub fn fingerprint_local(&self) -> String {
58 self.session.local_identity().fingerprint()
59 }
60
61 pub fn fingerprint_remote(&self) -> String {
63 self.session.remote_identity().fingerprint()
64 }
65}
66
67#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
68#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
69impl GroupStoreEntity for ProteusConversationSession {
70 type RawStoreValue = core_crypto_keystore::entities::ProteusSession;
71 type IdentityType = Arc<proteus_wasm::keys::IdentityKeyPair>;
72
73 async fn fetch_from_id(
74 id: &[u8],
75 identity: Option<Self::IdentityType>,
76 keystore: &impl FetchFromDatabase,
77 ) -> crate::Result<Option<Self>> {
78 let result = keystore
79 .find::<Self::RawStoreValue>(id)
80 .await
81 .map_err(KeystoreError::wrap("finding raw group store entity by id"))?;
82 let Some(store_value) = result else {
83 return Ok(None);
84 };
85
86 let Some(identity) = identity else {
87 return Err(crate::Error::ProteusNotInitialized);
88 };
89
90 let session = proteus_wasm::session::Session::deserialise(identity, &store_value.session)
91 .map_err(ProteusError::wrap("deserializing session"))?;
92
93 Ok(Some(Self {
94 identifier: store_value.id.clone(),
95 session,
96 }))
97 }
98}
99
100impl CoreCrypto {
101 pub async fn proteus_session(
107 &self,
108 session_id: &str,
109 ) -> Result<Option<GroupStoreValue<ProteusConversationSession>>> {
110 let mut mutex = self.proteus.lock().await;
111 let proteus = mutex.as_mut().ok_or(Error::ProteusNotInitialized)?;
112 let keystore = self.mls.crypto_provider.keystore();
113 proteus.session(session_id, &keystore).await
114 }
115
116 pub async fn proteus_session_exists(&self, session_id: &str) -> Result<bool> {
122 let mut mutex = self.proteus.lock().await;
123 let proteus = mutex.as_mut().ok_or(Error::ProteusNotInitialized)?;
124 let keystore = self.mls.crypto_provider.keystore();
125 Ok(proteus.session_exists(session_id, &keystore).await)
126 }
127
128 pub fn proteus_last_resort_prekey_id() -> u16 {
130 ProteusCentral::last_resort_prekey_id()
131 }
132
133 pub async fn proteus_fingerprint(&self) -> Result<String> {
139 let mutex = self.proteus.lock().await;
140 let proteus = mutex.as_ref().ok_or(Error::ProteusNotInitialized)?;
141 Ok(proteus.fingerprint())
142 }
143
144 pub async fn proteus_fingerprint_local(&self, session_id: &str) -> Result<String> {
150 let mut mutex = self.proteus.lock().await;
151 let proteus = mutex.as_mut().ok_or(Error::ProteusNotInitialized)?;
152 let keystore = self.mls.crypto_provider.keystore();
153 proteus.fingerprint_local(session_id, &keystore).await
154 }
155
156 pub async fn proteus_fingerprint_remote(&self, session_id: &str) -> Result<String> {
162 let mut mutex = self.proteus.lock().await;
163 let proteus = mutex.as_mut().ok_or(Error::ProteusNotInitialized)?;
164 let keystore = self.mls.crypto_provider.keystore();
165 proteus.fingerprint_remote(session_id, &keystore).await
166 }
167}
168
169#[derive(Debug)]
174pub struct ProteusCentral {
175 proteus_identity: Arc<IdentityKeyPair>,
176 proteus_sessions: GroupStore<ProteusConversationSession>,
177}
178
179impl ProteusCentral {
180 pub async fn try_new(keystore: &CryptoKeystore) -> Result<Self> {
182 let proteus_identity: Arc<IdentityKeyPair> = Arc::new(Self::load_or_create_identity(keystore).await?);
183 let proteus_sessions = Self::restore_sessions(keystore, &proteus_identity).await?;
184
185 Ok(Self {
186 proteus_identity,
187 proteus_sessions,
188 })
189 }
190
191 pub(crate) async fn reload_sessions(&mut self, keystore: &CryptoKeystore) -> Result<()> {
193 self.proteus_sessions = Self::restore_sessions(keystore, &self.proteus_identity).await?;
194 Ok(())
195 }
196
197 async fn load_or_create_identity(keystore: &CryptoKeystore) -> Result<IdentityKeyPair> {
200 let Some(identity) = keystore
201 .find::<ProteusIdentity>(&[])
202 .await
203 .map_err(KeystoreError::wrap("finding proteus identity"))?
204 else {
205 return Self::create_identity(keystore).await;
206 };
207
208 let sk = identity.sk_raw();
209 let pk = identity.pk_raw();
210
211 IdentityKeyPair::from_raw_key_pair(*sk, *pk)
213 .map_err(ProteusError::wrap("constructing identity keypair"))
214 .map_err(Into::into)
215 }
216
217 async fn create_identity(keystore: &CryptoKeystore) -> Result<IdentityKeyPair> {
219 let kp = IdentityKeyPair::new();
220 let pk = kp.public_key.public_key.as_slice().to_vec();
221
222 let ks_identity = ProteusIdentity {
223 sk: kp.secret_key.to_keypair_bytes().into(),
224 pk,
225 };
226 keystore
227 .save(ks_identity)
228 .await
229 .map_err(KeystoreError::wrap("saving new proteus identity"))?;
230
231 Ok(kp)
232 }
233
234 async fn restore_sessions(
236 keystore: &core_crypto_keystore::Connection,
237 identity: &Arc<IdentityKeyPair>,
238 ) -> Result<GroupStore<ProteusConversationSession>> {
239 let mut proteus_sessions = GroupStore::new_with_limit(crate::group_store::ITEM_LIMIT * 2);
240 for session in keystore
241 .find_all::<ProteusSession>(Default::default())
242 .await
243 .map_err(KeystoreError::wrap("finding all proteus sessions"))?
244 .into_iter()
245 {
246 let proteus_session = Session::deserialise(identity.clone(), &session.session)
247 .map_err(ProteusError::wrap("deserializing session"))?;
248
249 let identifier = session.id.clone();
250
251 let proteus_conversation = ProteusConversationSession {
252 identifier: identifier.clone(),
253 session: proteus_session,
254 };
255
256 if proteus_sessions
257 .try_insert(identifier.into_bytes(), proteus_conversation)
258 .is_err()
259 {
260 break;
261 }
262 }
263
264 Ok(proteus_sessions)
265 }
266
267 pub async fn session_from_prekey(
269 &mut self,
270 session_id: &str,
271 key: &[u8],
272 ) -> Result<GroupStoreValue<ProteusConversationSession>> {
273 let prekey = PreKeyBundle::deserialise(key).map_err(ProteusError::wrap("deserializing prekey bundle"))?;
274 let proteus_session = Session::init_from_prekey::<core_crypto_keystore::CryptoKeystoreError>(
306 self.proteus_identity.clone(),
307 prekey,
308 )
309 .map_err(ProteusError::wrap("initializing session from prekey"))?;
310
311 let proteus_conversation = ProteusConversationSession {
312 identifier: session_id.into(),
313 session: proteus_session,
314 };
315
316 self.proteus_sessions.insert(session_id.into(), proteus_conversation);
317
318 Ok(self.proteus_sessions.get(session_id.as_bytes()).unwrap().clone())
319 }
320
321 pub(crate) async fn session_from_message(
323 &mut self,
324 keystore: &mut CryptoKeystore,
325 session_id: &str,
326 envelope: &[u8],
327 ) -> Result<(GroupStoreValue<ProteusConversationSession>, Vec<u8>)> {
328 let message = Envelope::deserialise(envelope).map_err(ProteusError::wrap("deserialising envelope"))?;
329 let (session, payload) = Session::init_from_message(self.proteus_identity.clone(), keystore, &message)
330 .await
331 .map_err(ProteusError::wrap("initializing session from message"))?;
332
333 let proteus_conversation = ProteusConversationSession {
334 identifier: session_id.into(),
335 session,
336 };
337
338 self.proteus_sessions.insert(session_id.into(), proteus_conversation);
339
340 Ok((
341 self.proteus_sessions.get(session_id.as_bytes()).unwrap().clone(),
342 payload,
343 ))
344 }
345
346 pub(crate) async fn session_save(&mut self, keystore: &CryptoKeystore, session_id: &str) -> Result<()> {
350 if let Some(session) = self
351 .proteus_sessions
352 .get_fetch(session_id.as_bytes(), keystore, Some(self.proteus_identity.clone()))
353 .await?
354 {
355 Self::session_save_by_ref(keystore, session).await?;
356 }
357
358 Ok(())
359 }
360
361 pub(crate) async fn session_save_by_ref(
362 keystore: &CryptoKeystore,
363 session: GroupStoreValue<ProteusConversationSession>,
364 ) -> Result<()> {
365 let session = session.read().await;
366 let db_session = ProteusSession {
367 id: session.identifier().to_string(),
368 session: session
369 .session
370 .serialise()
371 .map_err(ProteusError::wrap("serializing session"))?,
372 };
373 keystore
374 .save(db_session)
375 .await
376 .map_err(KeystoreError::wrap("saving proteus session"))?;
377 Ok(())
378 }
379
380 pub(crate) async fn session_delete(&mut self, keystore: &CryptoKeystore, session_id: &str) -> Result<()> {
382 if keystore.remove::<ProteusSession, _>(session_id).await.is_ok() {
383 let _ = self.proteus_sessions.remove(session_id.as_bytes());
384 }
385 Ok(())
386 }
387
388 pub(crate) async fn session(
390 &mut self,
391 session_id: &str,
392 keystore: &CryptoKeystore,
393 ) -> Result<Option<GroupStoreValue<ProteusConversationSession>>> {
394 self.proteus_sessions
395 .get_fetch(session_id.as_bytes(), keystore, Some(self.proteus_identity.clone()))
396 .await
397 }
398
399 pub(crate) async fn session_exists(&mut self, session_id: &str, keystore: &CryptoKeystore) -> bool {
401 self.session(session_id, keystore).await.ok().flatten().is_some()
402 }
403
404 pub(crate) async fn decrypt(
407 &mut self,
408 keystore: &mut CryptoKeystore,
409 session_id: &str,
410 ciphertext: &[u8],
411 ) -> Result<Vec<u8>> {
412 let session = self
413 .proteus_sessions
414 .get_fetch(session_id.as_bytes(), keystore, Some(self.proteus_identity.clone()))
415 .await?
416 .ok_or(LeafError::ConversationNotFound(session_id.as_bytes().into()))
417 .map_err(ProteusError::wrap("getting session"))?;
418
419 let plaintext = session.write().await.decrypt(keystore, ciphertext).await?;
420 ProteusCentral::session_save_by_ref(keystore, session).await?;
421
422 Ok(plaintext)
423 }
424
425 pub(crate) async fn encrypt(
427 &mut self,
428 keystore: &mut CryptoKeystore,
429 session_id: &str,
430 plaintext: &[u8],
431 ) -> Result<Vec<u8>> {
432 let session = self
433 .session(session_id, keystore)
434 .await?
435 .ok_or(LeafError::ConversationNotFound(session_id.as_bytes().into()))
436 .map_err(ProteusError::wrap("getting session"))?;
437
438 let ciphertext = session.write().await.encrypt(plaintext)?;
439 ProteusCentral::session_save_by_ref(keystore, session).await?;
440
441 Ok(ciphertext)
442 }
443
444 pub(crate) async fn encrypt_batched(
447 &mut self,
448 keystore: &mut CryptoKeystore,
449 sessions: &[impl AsRef<str>],
450 plaintext: &[u8],
451 ) -> Result<HashMap<String, Vec<u8>>> {
452 let mut acc = HashMap::new();
453 for session_id in sessions {
454 if let Some(session) = self.session(session_id.as_ref(), keystore).await? {
455 let mut session_w = session.write().await;
456 acc.insert(session_w.identifier.clone(), session_w.encrypt(plaintext)?);
457 drop(session_w);
458
459 ProteusCentral::session_save_by_ref(keystore, session).await?;
460 }
461 }
462 Ok(acc)
463 }
464
465 pub(crate) async fn new_prekey(&self, id: u16, keystore: &CryptoKeystore) -> Result<Vec<u8>> {
467 use proteus_wasm::keys::{PreKey, PreKeyId};
468
469 let prekey_id = PreKeyId::new(id);
470 let prekey = PreKey::new(prekey_id);
471 let keystore_prekey = core_crypto_keystore::entities::ProteusPrekey::from_raw(
472 id,
473 prekey.serialise().map_err(ProteusError::wrap("serialising prekey"))?,
474 );
475 let bundle = PreKeyBundle::new(self.proteus_identity.as_ref().public_key.clone(), &prekey);
476 let bundle = bundle
477 .serialise()
478 .map_err(ProteusError::wrap("serialising prekey bundle"))?;
479 keystore
480 .save(keystore_prekey)
481 .await
482 .map_err(KeystoreError::wrap("saving keystore prekey"))?;
483 Ok(bundle)
484 }
485
486 pub(crate) async fn new_prekey_auto(&self, keystore: &CryptoKeystore) -> Result<(u16, Vec<u8>)> {
490 let id = core_crypto_keystore::entities::ProteusPrekey::get_free_id(keystore)
491 .await
492 .map_err(KeystoreError::wrap("getting proteus prekey by id"))?;
493 Ok((id, self.new_prekey(id, keystore).await?))
494 }
495
496 pub fn last_resort_prekey_id() -> u16 {
498 proteus_wasm::keys::MAX_PREKEY_ID.value()
499 }
500
501 pub(crate) async fn last_resort_prekey(&self, keystore: &CryptoKeystore) -> Result<Vec<u8>> {
504 let last_resort = if let Some(last_resort) = keystore
505 .find::<core_crypto_keystore::entities::ProteusPrekey>(
506 Self::last_resort_prekey_id().to_le_bytes().as_slice(),
507 )
508 .await
509 .map_err(KeystoreError::wrap("finding proteus prekey"))?
510 {
511 proteus_wasm::keys::PreKey::deserialise(&last_resort.prekey)
512 .map_err(ProteusError::wrap("deserialising proteus prekey"))?
513 } else {
514 let last_resort = proteus_wasm::keys::PreKey::last_resort();
515
516 use core_crypto_keystore::CryptoKeystoreProteus as _;
517 keystore
518 .proteus_store_prekey(
519 Self::last_resort_prekey_id(),
520 &last_resort
521 .serialise()
522 .map_err(ProteusError::wrap("serialising last resort prekey"))?,
523 )
524 .await
525 .map_err(KeystoreError::wrap("storing proteus prekey"))?;
526
527 last_resort
528 };
529
530 let bundle = PreKeyBundle::new(self.proteus_identity.as_ref().public_key.clone(), &last_resort);
531 let bundle = bundle
532 .serialise()
533 .map_err(ProteusError::wrap("serialising prekey bundle"))?;
534
535 Ok(bundle)
536 }
537
538 pub fn identity(&self) -> &IdentityKeyPair {
540 self.proteus_identity.as_ref()
541 }
542
543 pub fn fingerprint(&self) -> String {
545 self.proteus_identity.as_ref().public_key.fingerprint()
546 }
547
548 pub(crate) async fn fingerprint_local(&mut self, session_id: &str, keystore: &CryptoKeystore) -> Result<String> {
553 let session = self
554 .session(session_id, keystore)
555 .await?
556 .ok_or(LeafError::ConversationNotFound(session_id.as_bytes().into()))
557 .map_err(ProteusError::wrap("getting session"))?;
558 let fingerprint = session.read().await.fingerprint_local();
559 Ok(fingerprint)
560 }
561
562 pub(crate) async fn fingerprint_remote(&mut self, session_id: &str, keystore: &CryptoKeystore) -> Result<String> {
567 let session = self
568 .session(session_id, keystore)
569 .await?
570 .ok_or(LeafError::ConversationNotFound(session_id.as_bytes().into()))
571 .map_err(ProteusError::wrap("getting session"))?;
572 let fingerprint = session.read().await.fingerprint_remote();
573 Ok(fingerprint)
574 }
575
576 pub fn fingerprint_prekeybundle(prekey: &[u8]) -> Result<String> {
581 let prekey = PreKeyBundle::deserialise(prekey).map_err(ProteusError::wrap("deserialising prekey bundle"))?;
582 Ok(prekey.identity_key.fingerprint())
583 }
584
585 #[cfg_attr(not(feature = "cryptobox-migrate"), allow(unused_variables))]
587 pub(crate) async fn cryptobox_migrate(keystore: &CryptoKeystore, path: &str) -> Result<()> {
588 cfg_if::cfg_if! {
589 if #[cfg(feature = "cryptobox-migrate")] {
590 Self::cryptobox_migrate_impl(keystore, path).await?;
591 Ok(())
592 } else {
593 Err(Error::FeatureDisabled("cryptobox-migrate"))
594 }
595 }
596 }
597}
598
599#[cfg(feature = "cryptobox-migrate")]
600#[allow(dead_code)]
601impl ProteusCentral {
602 #[cfg(not(target_family = "wasm"))]
603 async fn cryptobox_migrate_impl(keystore: &CryptoKeystore, path: &str) -> Result<()> {
604 let root_dir = std::path::PathBuf::from(path);
605
606 if !root_dir.exists() {
607 return Err(CryptoboxMigrationError::wrap("root dir does not exist")(
608 crate::CryptoboxMigrationErrorKind::ProvidedPathDoesNotExist(path.into()),
609 )
610 .into());
611 }
612
613 let session_dir = root_dir.join("sessions");
614 let prekey_dir = root_dir.join("prekeys");
615
616 let missing_identity = Err(CryptoboxMigrationError::wrap("taking identity keypair")(
618 crate::CryptoboxMigrationErrorKind::IdentityNotFound(path.into()),
619 )
620 .into());
621
622 let identity = if let Some(store_kp) = keystore
623 .find::<ProteusIdentity>(&[])
624 .await
625 .map_err(KeystoreError::wrap("finding proteus identity"))?
626 {
627 Box::new(
628 IdentityKeyPair::from_raw_key_pair(*store_kp.sk_raw(), *store_kp.pk_raw())
629 .map_err(ProteusError::wrap("constructing identity keypair from raw keypair"))?,
630 )
631 } else {
632 let identity_dir = root_dir.join("identities");
633
634 let identity = identity_dir.join("local");
635 let legacy_identity = identity_dir.join("local_identity");
636 let kp = if legacy_identity.exists() {
638 let kp_cbor = async_fs::read(&legacy_identity)
639 .await
640 .map_err(CryptoboxMigrationError::wrap("reading legacy identity from filesystem"))?;
641 let kp = IdentityKeyPair::deserialise(&kp_cbor)
642 .map_err(ProteusError::wrap("deserialising identity keypair"))?;
643
644 Box::new(kp)
645 } else if identity.exists() {
646 let kp_cbor = async_fs::read(&identity)
647 .await
648 .map_err(CryptoboxMigrationError::wrap("reading identity from filesystem"))?;
649 let kp = proteus_wasm::identity::Identity::deserialise(&kp_cbor)
650 .map_err(ProteusError::wrap("deserialising identity"))?;
651
652 if let proteus_wasm::identity::Identity::Sec(kp) = kp {
653 kp.into_owned()
654 } else {
655 return missing_identity;
656 }
657 } else {
658 return missing_identity;
659 };
660
661 let pk = kp.public_key.public_key.as_slice().into();
662
663 let ks_identity = ProteusIdentity {
664 sk: kp.secret_key.to_keypair_bytes().into(),
665 pk,
666 };
667
668 keystore
669 .save(ks_identity)
670 .await
671 .map_err(KeystoreError::wrap("saving proteus identity"))?;
672
673 if legacy_identity.exists() {
674 async_fs::remove_file(legacy_identity)
675 .await
676 .map_err(CryptoboxMigrationError::wrap("removing legacy identity"))?;
677 }
678
679 kp
680 };
681
682 let identity = *identity;
683
684 use futures_lite::stream::StreamExt as _;
685 let mut session_entries = async_fs::read_dir(session_dir)
687 .await
688 .map_err(CryptoboxMigrationError::wrap("reading session entries"))?;
689 while let Some(session_file) = session_entries
690 .try_next()
691 .await
692 .map_err(CryptoboxMigrationError::wrap("getting next session file"))?
693 {
694 let proteus_session_id: String = session_file.file_name().to_string_lossy().to_string();
696
697 if keystore
699 .find::<ProteusSession>(proteus_session_id.as_bytes())
700 .await
701 .map_err(KeystoreError::wrap("finding proteus session by id"))?
702 .is_some()
703 {
704 continue;
705 }
706
707 let raw_session = async_fs::read(session_file.path())
708 .await
709 .map_err(CryptoboxMigrationError::wrap("reading session file"))?;
710 let Ok(_) = Session::deserialise(&identity, &raw_session) else {
712 continue;
713 };
714
715 let keystore_session = ProteusSession {
716 id: proteus_session_id,
717 session: raw_session,
718 };
719
720 keystore
721 .save(keystore_session)
722 .await
723 .map_err(KeystoreError::wrap("saving proteus session"))?;
724 }
725
726 use core_crypto_keystore::entities::ProteusPrekey;
728
729 use crate::CryptoboxMigrationError;
730 let mut prekey_entries = async_fs::read_dir(prekey_dir)
731 .await
732 .map_err(CryptoboxMigrationError::wrap("reading prekey entries"))?;
733 while let Some(prekey_file) = prekey_entries
734 .try_next()
735 .await
736 .map_err(CryptoboxMigrationError::wrap("getting next prekey file"))?
737 {
738 let proteus_prekey_id = proteus_wasm::keys::PreKeyId::new(
740 prekey_file
741 .file_name()
742 .to_string_lossy()
743 .parse()
744 .map_err(CryptoboxMigrationError::wrap("parsing prekey file name"))?,
745 );
746
747 if keystore
749 .find::<ProteusPrekey>(&proteus_prekey_id.value().to_le_bytes())
750 .await
751 .map_err(KeystoreError::wrap("finding proteus prekey by id"))?
752 .is_some()
753 {
754 continue;
755 }
756
757 let raw_prekey = async_fs::read(prekey_file.path())
758 .await
759 .map_err(CryptoboxMigrationError::wrap("reading prekey file"))?;
760 if proteus_wasm::keys::PreKey::deserialise(&raw_prekey).is_ok() {
762 let keystore_prekey = ProteusPrekey::from_raw(proteus_prekey_id.value(), raw_prekey);
763 keystore
764 .save(keystore_prekey)
765 .await
766 .map_err(KeystoreError::wrap("saving proteus prekey"))?;
767 }
768 }
769
770 Ok(())
771 }
772
773 #[cfg(target_family = "wasm")]
774 fn get_cbor_bytes_from_map(map: serde_json::map::Map<String, serde_json::Value>) -> Result<Vec<u8>> {
775 use crate::{CryptoboxMigrationError, CryptoboxMigrationErrorKind};
776
777 let Some(js_value) = map.get("serialised") else {
778 return Err(CryptoboxMigrationError::wrap("getting serialised cbor bytes from map")(
779 CryptoboxMigrationErrorKind::MissingKeyInValue("serialised".to_string()),
780 )
781 .into());
782 };
783
784 let Some(b64_value) = js_value.as_str() else {
785 return Err(CryptoboxMigrationError::wrap("getting js value as string")(
786 CryptoboxMigrationErrorKind::WrongValueType("string".to_string()),
787 )
788 .into());
789 };
790
791 use base64::Engine as _;
792 let cbor_bytes = base64::prelude::BASE64_STANDARD
793 .decode(b64_value)
794 .map_err(CryptoboxMigrationError::wrap("decoding cbor bytes"))?;
795 Ok(cbor_bytes)
796 }
797
798 #[cfg(target_family = "wasm")]
799 async fn cryptobox_migrate_impl(keystore: &CryptoKeystore, path: &str) -> Result<()> {
800 use rexie::{Rexie, TransactionMode};
801
802 use crate::{CryptoboxMigrationError, CryptoboxMigrationErrorKind};
803 let local_identity_key = "local_identity";
804 let local_identity_store_name = "keys";
805 let prekeys_store_name = "prekeys";
806 let sessions_store_name = "sessions";
807
808 let db = Rexie::builder(path)
810 .build()
811 .await
812 .map_err(CryptoboxMigrationError::wrap("building rexie"))?;
813
814 let store_names = db.store_names();
815
816 let expected_stores = &[prekeys_store_name, sessions_store_name, local_identity_store_name];
817
818 if !expected_stores
819 .iter()
820 .map(ToString::to_string)
821 .all(|s| store_names.contains(&s))
822 {
823 return Err(CryptoboxMigrationError::wrap("checking expected stores")(
824 CryptoboxMigrationErrorKind::ProvidedPathDoesNotExist(path.into()),
825 )
826 .into());
827 }
828
829 let mut proteus_identity = if let Some(store_kp) = keystore
830 .find::<ProteusIdentity>(&[])
831 .await
832 .map_err(KeystoreError::wrap("finding proteus identity for empty id"))?
833 {
834 Some(
835 proteus_wasm::keys::IdentityKeyPair::from_raw_key_pair(*store_kp.sk_raw(), *store_kp.pk_raw())
836 .map_err(ProteusError::wrap("constructing identity keypair from raw"))?,
837 )
838 } else {
839 let transaction = db
840 .transaction(&[local_identity_store_name], TransactionMode::ReadOnly)
841 .map_err(CryptoboxMigrationError::wrap("initializing rexie transaction"))?;
842
843 let identity_store = transaction
844 .store(local_identity_store_name)
845 .map_err(CryptoboxMigrationError::wrap("storing local identity store name"))?;
846
847 if let Some(cryptobox_js_value) = identity_store
848 .get(local_identity_key.into())
849 .await
850 .map_err(CryptoboxMigrationError::wrap("getting local identity key js value"))?
851 {
852 let js_value: serde_json::map::Map<String, serde_json::Value> =
853 serde_wasm_bindgen::from_value(cryptobox_js_value).map_err(CryptoboxMigrationError::wrap(
854 "getting local identity key from identity store",
855 ))?;
856
857 let kp_cbor = Self::get_cbor_bytes_from_map(js_value)?;
858
859 let kp = proteus_wasm::keys::IdentityKeyPair::deserialise(&kp_cbor)
860 .map_err(ProteusError::wrap("deserializing identity keypair"))?;
861
862 let pk = kp.public_key.public_key.as_slice().to_vec();
863
864 let ks_identity = ProteusIdentity {
865 sk: kp.secret_key.to_keypair_bytes().into(),
866 pk,
867 };
868 keystore
869 .save(ks_identity)
870 .await
871 .map_err(KeystoreError::wrap("saving proteus identity in keystore"))?;
872
873 Some(kp)
874 } else {
875 None
876 }
877 };
878
879 let Some(proteus_identity) = proteus_identity.take() else {
880 return Err(CryptoboxMigrationError::wrap("taking proteus identity")(
881 CryptoboxMigrationErrorKind::IdentityNotFound(path.into()),
882 )
883 .into());
884 };
885
886 if store_names.contains(&sessions_store_name.to_string()) {
887 let transaction = db
888 .transaction(&[sessions_store_name], TransactionMode::ReadOnly)
889 .map_err(CryptoboxMigrationError::wrap("starting rexie transaction"))?;
890
891 let sessions_store = transaction
892 .store(sessions_store_name)
893 .map_err(CryptoboxMigrationError::wrap("getting sessions store"))?;
894
895 let sessions = sessions_store
896 .scan(None, None, None, None)
897 .await
898 .map_err(CryptoboxMigrationError::wrap("scanning sessions store for sessions"))?;
899
900 for (session_id, session_js_value) in sessions.into_iter().map(|(k, v)| (k.as_string().unwrap(), v)) {
901 if keystore
903 .find::<ProteusSession>(session_id.as_bytes())
904 .await
905 .map_err(KeystoreError::wrap("finding proteus session by id"))?
906 .is_some()
907 {
908 continue;
909 }
910
911 let js_value: serde_json::map::Map<String, serde_json::Value> =
912 serde_wasm_bindgen::from_value(session_js_value).map_err(CryptoboxMigrationError::wrap(
913 "converting session js value to serde map",
914 ))?;
915
916 let session_cbor_bytes = Self::get_cbor_bytes_from_map(js_value)?;
917
918 if proteus_wasm::session::Session::deserialise(&proteus_identity, &session_cbor_bytes).is_ok() {
920 let keystore_session = ProteusSession {
921 id: session_id,
922 session: session_cbor_bytes,
923 };
924
925 keystore
926 .save(keystore_session)
927 .await
928 .map_err(KeystoreError::wrap("saving keystore session"))?;
929 }
930 }
931 }
932
933 if store_names.contains(&prekeys_store_name.to_string()) {
934 use core_crypto_keystore::entities::ProteusPrekey;
935
936 let transaction = db
937 .transaction(&[prekeys_store_name], TransactionMode::ReadOnly)
938 .map_err(CryptoboxMigrationError::wrap("beginning rexie transaction"))?;
939
940 let prekeys_store = transaction
941 .store(prekeys_store_name)
942 .map_err(CryptoboxMigrationError::wrap("getting prekeys store"))?;
943
944 let prekeys = prekeys_store
945 .scan(None, None, None, None)
946 .await
947 .map_err(CryptoboxMigrationError::wrap("scanning for prekeys"))?;
948
949 for (prekey_id, prekey_js_value) in prekeys
950 .into_iter()
951 .map(|(id, prekey_js_value)| (id.as_string().unwrap(), prekey_js_value))
952 {
953 let prekey_id: u16 = prekey_id
954 .parse()
955 .map_err(CryptoboxMigrationError::wrap("parsing prekey id"))?;
956
957 if keystore
959 .find::<ProteusPrekey>(&prekey_id.to_le_bytes())
960 .await
961 .map_err(KeystoreError::wrap(
962 "finding proteus prekey by id to check for existence",
963 ))?
964 .is_some()
965 {
966 continue;
967 }
968
969 let js_value: serde_json::map::Map<String, serde_json::Value> =
970 serde_wasm_bindgen::from_value(prekey_js_value)
971 .map_err(CryptoboxMigrationError::wrap("converting prekey js value to serde map"))?;
972
973 let raw_prekey_cbor = Self::get_cbor_bytes_from_map(js_value)?;
974
975 if proteus_wasm::keys::PreKey::deserialise(&raw_prekey_cbor).is_ok() {
977 let keystore_prekey = ProteusPrekey::from_raw(prekey_id, raw_prekey_cbor);
978 keystore
979 .save(keystore_prekey)
980 .await
981 .map_err(KeystoreError::wrap("saving proteus prekey"))?;
982 }
983 }
984 }
985
986 Ok(())
987 }
988}
989
990#[cfg(test)]
991mod tests {
992 use crate::{
993 prelude::{CertificateBundle, ClientIdentifier, MlsCredentialType, Session, SessionConfig},
994 test_utils::{proteus_utils::*, x509::X509TestChain, *},
995 };
996
997 use crate::prelude::INITIAL_KEYING_MATERIAL_COUNT;
998 #[cfg(not(target_family = "wasm"))]
999 use proteus_traits::PreKeyStore;
1000
1001 use super::*;
1002
1003 use core_crypto_keystore::{ConnectionType, DatabaseKey};
1004
1005 #[apply(all_cred_cipher)]
1006 async fn cc_can_init(case: TestContext) {
1007 #[cfg(not(target_family = "wasm"))]
1008 let (path, db_file) = tmp_db_file();
1009 #[cfg(target_family = "wasm")]
1010 let (path, _) = tmp_db_file();
1011 let client_id = "alice".into();
1012 let cfg = SessionConfig::builder()
1013 .persistent(&path)
1014 .database_key(DatabaseKey::generate())
1015 .client_id(client_id)
1016 .ciphersuites([case.ciphersuite()])
1017 .build()
1018 .validate()
1019 .unwrap();
1020
1021 let cc: CoreCrypto = Session::try_new(cfg).await.unwrap().into();
1022 let context = cc.new_transaction().await.unwrap();
1023 assert!(context.proteus_init().await.is_ok());
1024 assert!(context.proteus_new_prekey(1).await.is_ok());
1025 context.finish().await.unwrap();
1026 #[cfg(not(target_family = "wasm"))]
1027 drop(db_file);
1028 }
1029
1030 #[apply(all_cred_cipher)]
1031 async fn cc_can_2_phase_init(case: TestContext) {
1032 #[cfg(not(target_family = "wasm"))]
1033 let (path, db_file) = tmp_db_file();
1034 #[cfg(target_family = "wasm")]
1035 let (path, _) = tmp_db_file();
1036 let cfg = SessionConfig::builder()
1038 .persistent(&path)
1039 .database_key(DatabaseKey::generate())
1040 .ciphersuites([case.ciphersuite()])
1041 .build()
1042 .validate()
1043 .unwrap();
1044
1045 let cc: CoreCrypto = Session::try_new(cfg).await.unwrap().into();
1046 let transaction = cc.new_transaction().await.unwrap();
1047 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
1048 x509_test_chain.register_with_central(&transaction).await;
1049 assert!(transaction.proteus_init().await.is_ok());
1050 assert!(transaction.proteus_new_prekey(1).await.is_ok());
1052 let client_id = "alice";
1054 let identifier = match case.credential_type {
1055 MlsCredentialType::Basic => ClientIdentifier::Basic(client_id.into()),
1056 MlsCredentialType::X509 => {
1057 CertificateBundle::rand_identifier(client_id, &[x509_test_chain.find_local_intermediate_ca()])
1058 }
1059 };
1060 transaction
1061 .mls_init(
1062 identifier,
1063 vec![case.ciphersuite()],
1064 Some(INITIAL_KEYING_MATERIAL_COUNT),
1065 )
1066 .await
1067 .unwrap();
1068 assert_eq!(
1070 transaction
1071 .get_or_create_client_keypackages(case.ciphersuite(), case.credential_type, 2)
1072 .await
1073 .unwrap()
1074 .len(),
1075 2
1076 );
1077 #[cfg(not(target_family = "wasm"))]
1078 drop(db_file);
1079 }
1080
1081 #[async_std::test]
1082 async fn can_init() {
1083 #[cfg(not(target_family = "wasm"))]
1084 let (path, db_file) = tmp_db_file();
1085 #[cfg(target_family = "wasm")]
1086 let (path, _) = tmp_db_file();
1087 let key = DatabaseKey::generate();
1088 let keystore = core_crypto_keystore::Connection::open(ConnectionType::Persistent(&path), &key)
1089 .await
1090 .unwrap();
1091 keystore.new_transaction().await.unwrap();
1092 let central = ProteusCentral::try_new(&keystore).await.unwrap();
1093 let identity = (*central.proteus_identity).clone();
1094 keystore.commit_transaction().await.unwrap();
1095
1096 let keystore = core_crypto_keystore::Connection::open(ConnectionType::Persistent(&path), &key)
1097 .await
1098 .unwrap();
1099 keystore.new_transaction().await.unwrap();
1100 let central = ProteusCentral::try_new(&keystore).await.unwrap();
1101 keystore.commit_transaction().await.unwrap();
1102 assert_eq!(identity, *central.proteus_identity);
1103
1104 keystore.wipe().await.unwrap();
1105 #[cfg(not(target_family = "wasm"))]
1106 drop(db_file);
1107 }
1108
1109 #[async_std::test]
1110 async fn can_talk_with_proteus() {
1111 #[cfg(not(target_family = "wasm"))]
1112 let (path, db_file) = tmp_db_file();
1113 #[cfg(target_family = "wasm")]
1114 let (path, _) = tmp_db_file();
1115
1116 let session_id = uuid::Uuid::new_v4().hyphenated().to_string();
1117
1118 let key = DatabaseKey::generate();
1119 let mut keystore = core_crypto_keystore::Connection::open(ConnectionType::Persistent(&path), &key)
1120 .await
1121 .unwrap();
1122 keystore.new_transaction().await.unwrap();
1123
1124 let mut alice = ProteusCentral::try_new(&keystore).await.unwrap();
1125
1126 let mut bob = CryptoboxLike::init();
1127 let bob_pk_bundle = bob.new_prekey();
1128
1129 alice
1130 .session_from_prekey(&session_id, &bob_pk_bundle.serialise().unwrap())
1131 .await
1132 .unwrap();
1133
1134 let message = b"Hello world";
1135
1136 let encrypted = alice.encrypt(&mut keystore, &session_id, message).await.unwrap();
1137 let decrypted = bob.decrypt(&session_id, &encrypted).await;
1138 assert_eq!(decrypted, message);
1139
1140 let encrypted = bob.encrypt(&session_id, message);
1141 let decrypted = alice.decrypt(&mut keystore, &session_id, &encrypted).await.unwrap();
1142 assert_eq!(decrypted, message);
1143
1144 keystore.commit_transaction().await.unwrap();
1145 keystore.wipe().await.unwrap();
1146 #[cfg(not(target_family = "wasm"))]
1147 drop(db_file);
1148 }
1149
1150 #[async_std::test]
1151 async fn can_produce_proteus_consumed_prekeys() {
1152 #[cfg(not(target_family = "wasm"))]
1153 let (path, db_file) = tmp_db_file();
1154 #[cfg(target_family = "wasm")]
1155 let (path, _) = tmp_db_file();
1156
1157 let session_id = uuid::Uuid::new_v4().hyphenated().to_string();
1158
1159 let key = DatabaseKey::generate();
1160 let mut keystore = core_crypto_keystore::Connection::open(ConnectionType::Persistent(&path), &key)
1161 .await
1162 .unwrap();
1163 keystore.new_transaction().await.unwrap();
1164 let mut alice = ProteusCentral::try_new(&keystore).await.unwrap();
1165
1166 let mut bob = CryptoboxLike::init();
1167
1168 let alice_prekey_bundle_ser = alice.new_prekey(1, &keystore).await.unwrap();
1169
1170 bob.init_session_from_prekey_bundle(&session_id, &alice_prekey_bundle_ser);
1171 let message = b"Hello world!";
1172 let encrypted = bob.encrypt(&session_id, message);
1173
1174 let (_, decrypted) = alice
1175 .session_from_message(&mut keystore, &session_id, &encrypted)
1176 .await
1177 .unwrap();
1178
1179 assert_eq!(message, decrypted.as_slice());
1180
1181 let encrypted = alice.encrypt(&mut keystore, &session_id, message).await.unwrap();
1182 let decrypted = bob.decrypt(&session_id, &encrypted).await;
1183
1184 assert_eq!(message, decrypted.as_slice());
1185 keystore.commit_transaction().await.unwrap();
1186 keystore.wipe().await.unwrap();
1187 #[cfg(not(target_family = "wasm"))]
1188 drop(db_file);
1189 }
1190
1191 #[async_std::test]
1192 async fn auto_prekeys_are_sequential() {
1193 use core_crypto_keystore::entities::ProteusPrekey;
1194 const GAP_AMOUNT: u16 = 5;
1195 const ID_TEST_RANGE: std::ops::RangeInclusive<u16> = 1..=30;
1196
1197 #[cfg(not(target_family = "wasm"))]
1198 let (path, db_file) = tmp_db_file();
1199 #[cfg(target_family = "wasm")]
1200 let (path, _) = tmp_db_file();
1201
1202 let key = DatabaseKey::generate();
1203 let keystore = core_crypto_keystore::Connection::open(ConnectionType::Persistent(&path), &key)
1204 .await
1205 .unwrap();
1206 keystore.new_transaction().await.unwrap();
1207 let alice = ProteusCentral::try_new(&keystore).await.unwrap();
1208
1209 for i in ID_TEST_RANGE {
1210 let (pk_id, pkb) = alice.new_prekey_auto(&keystore).await.unwrap();
1211 assert_eq!(i, pk_id);
1212 let prekey = proteus_wasm::keys::PreKeyBundle::deserialise(&pkb).unwrap();
1213 assert_eq!(prekey.prekey_id.value(), pk_id);
1214 }
1215
1216 use rand::Rng as _;
1217 let mut rng = rand::thread_rng();
1218 let mut gap_ids: Vec<u16> = (0..GAP_AMOUNT).map(|_| rng.gen_range(ID_TEST_RANGE)).collect();
1219 gap_ids.sort();
1220 gap_ids.dedup();
1221 while gap_ids.len() < GAP_AMOUNT as usize {
1222 gap_ids.push(rng.gen_range(ID_TEST_RANGE));
1223 gap_ids.sort();
1224 gap_ids.dedup();
1225 }
1226 for gap_id in gap_ids.iter() {
1227 keystore.remove::<ProteusPrekey, _>(gap_id.to_le_bytes()).await.unwrap();
1228 }
1229
1230 gap_ids.sort();
1231
1232 for gap_id in gap_ids.iter() {
1233 let (pk_id, pkb) = alice.new_prekey_auto(&keystore).await.unwrap();
1234 assert_eq!(pk_id, *gap_id);
1235 let prekey = proteus_wasm::keys::PreKeyBundle::deserialise(&pkb).unwrap();
1236 assert_eq!(prekey.prekey_id.value(), *gap_id);
1237 }
1238
1239 let mut gap_ids: Vec<u16> = (0..GAP_AMOUNT).map(|_| rng.gen_range(ID_TEST_RANGE)).collect();
1240 gap_ids.sort();
1241 gap_ids.dedup();
1242 while gap_ids.len() < GAP_AMOUNT as usize {
1243 gap_ids.push(rng.gen_range(ID_TEST_RANGE));
1244 gap_ids.sort();
1245 gap_ids.dedup();
1246 }
1247 for gap_id in gap_ids.iter() {
1248 keystore.remove::<ProteusPrekey, _>(gap_id.to_le_bytes()).await.unwrap();
1249 }
1250
1251 let potential_range = *ID_TEST_RANGE.end()..=(*ID_TEST_RANGE.end() * 2);
1252 let potential_range_check = potential_range.clone();
1253 for _ in potential_range {
1254 let (pk_id, pkb) = alice.new_prekey_auto(&keystore).await.unwrap();
1255 assert!(gap_ids.contains(&pk_id) || potential_range_check.contains(&pk_id));
1256 let prekey = proteus_wasm::keys::PreKeyBundle::deserialise(&pkb).unwrap();
1257 assert_eq!(prekey.prekey_id.value(), pk_id);
1258 }
1259 keystore.commit_transaction().await.unwrap();
1260 keystore.wipe().await.unwrap();
1261 #[cfg(not(target_family = "wasm"))]
1262 drop(db_file);
1263 }
1264
1265 #[cfg(all(feature = "cryptobox-migrate", not(target_family = "wasm")))]
1266 #[async_std::test]
1267 async fn can_import_cryptobox() {
1268 use crate::CryptoboxMigrationErrorKind;
1269
1270 let session_id = uuid::Uuid::new_v4().hyphenated().to_string();
1271
1272 let cryptobox_folder = tempfile::tempdir().unwrap();
1273 let alice = cryptobox::CBox::file_open(cryptobox_folder.path()).unwrap();
1274 let alice_fingerprint = alice.fingerprint();
1275
1276 let mut bob = CryptoboxLike::init();
1277 let bob_pk_bundle = bob.new_prekey();
1278
1279 let alice_pk_id = proteus::keys::PreKeyId::new(1u16);
1280 let alice_pk = alice.new_prekey(alice_pk_id).unwrap();
1281
1282 let mut alice_session = alice
1283 .session_from_prekey(session_id.clone(), &bob_pk_bundle.serialise().unwrap())
1284 .unwrap();
1285
1286 let message = b"Hello world!";
1287
1288 let alice_msg_envelope = alice_session.encrypt(message).unwrap();
1289 let decrypted = bob.decrypt(&session_id, &alice_msg_envelope).await;
1290 assert_eq!(decrypted, message);
1291
1292 alice.session_save(&mut alice_session).unwrap();
1293
1294 let encrypted = bob.encrypt(&session_id, &message[..]);
1295 let decrypted = alice_session.decrypt(&encrypted).unwrap();
1296 assert_eq!(decrypted, message);
1297
1298 alice.session_save(&mut alice_session).unwrap();
1299
1300 drop(alice);
1301
1302 let keystore_dir = tempfile::tempdir().unwrap();
1303 let keystore_file = keystore_dir.path().join("keystore");
1304
1305 let key = DatabaseKey::generate();
1306 let mut keystore = core_crypto_keystore::Connection::open(
1307 ConnectionType::Persistent(&keystore_file.as_os_str().to_string_lossy()),
1308 &key,
1309 )
1310 .await
1311 .unwrap();
1312 keystore.new_transaction().await.unwrap();
1313
1314 let Err(crate::Error::CryptoboxMigration(crate::CryptoboxMigrationError {
1315 source: CryptoboxMigrationErrorKind::ProvidedPathDoesNotExist(_),
1316 ..
1317 })) = ProteusCentral::cryptobox_migrate(&keystore, "invalid path").await
1318 else {
1319 panic!("ProteusCentral::cryptobox_migrate did not throw an error on invalid path");
1320 };
1321
1322 ProteusCentral::cryptobox_migrate(&keystore, &cryptobox_folder.path().to_string_lossy())
1323 .await
1324 .unwrap();
1325
1326 let mut proteus_central = ProteusCentral::try_new(&keystore).await.unwrap();
1327
1328 assert_eq!(proteus_central.fingerprint(), alice_fingerprint);
1330
1331 let alice_new_session_lock = proteus_central.session(&session_id, &keystore).await.unwrap().unwrap();
1333 let alice_new_session = alice_new_session_lock.read().await;
1334 assert_eq!(
1335 alice_new_session.session.local_identity().fingerprint(),
1336 alice_session.fingerprint_local()
1337 );
1338 assert_eq!(
1339 alice_new_session.session.remote_identity().fingerprint(),
1340 alice_session.fingerprint_remote()
1341 );
1342
1343 drop(alice_new_session);
1344 drop(alice_new_session_lock);
1345
1346 let keystore_pk = keystore.prekey(1).await.unwrap().unwrap();
1348 let keystore_pk = proteus_wasm::keys::PreKey::deserialise(&keystore_pk).unwrap();
1349 assert_eq!(alice_pk.prekey_id.value(), keystore_pk.key_id.value());
1350 assert_eq!(
1351 alice_pk.public_key.fingerprint(),
1352 keystore_pk.key_pair.public_key.fingerprint()
1353 );
1354
1355 let encrypted = proteus_central
1357 .encrypt(&mut keystore, &session_id, &message[..])
1358 .await
1359 .unwrap();
1360 let decrypted = bob.decrypt(&session_id, &encrypted).await;
1361
1362 assert_eq!(&decrypted, &message[..]);
1363
1364 let encrypted = bob.encrypt(&session_id, &message[..]);
1369 let decrypted = proteus_central
1370 .decrypt(&mut keystore, &session_id, &encrypted)
1371 .await
1372 .unwrap();
1373 assert_eq!(&decrypted, &message[..]);
1374
1375 proteus_central.session_save(&keystore, &session_id).await.unwrap();
1376 keystore.commit_transaction().await.unwrap();
1377 keystore.wipe().await.unwrap();
1378 }
1379}