1pub mod sealed;
93
94use crate::core::Counters;
95use crate::ids::{DeviceId, RequestId, RoomId, UserId};
96use crate::outgoing_queue::PendingRequest;
97use crate::persist::{FlushBatch, FlushEpoch, FlushGate, RecordKey, RequiredSeq, SealedRecord};
98use crate::wire::{percent_decode_segment, percent_encode_segment};
99use std::collections::BTreeMap;
100
101#[derive(Debug, thiserror::Error)]
105pub enum StoreError {
106 #[error("record {key}: {reason}")]
111 Decode {
112 key: RecordKey,
114 reason: String,
116 },
117
118 #[error("record {key}: codec rejected the sealed bytes")]
122 CodecOpen {
123 key: RecordKey,
125 },
126
127 #[error("record {key}: recognized kind but malformed key layout")]
133 UnknownRecordLayout {
134 key: RecordKey,
136 },
137}
138
139impl StoreError {
140 fn decode(key: &RecordKey, reason: impl Into<String>) -> Self {
141 StoreError::Decode { key: key.clone(), reason: reason.into() }
142 }
143
144 fn unknown_layout(key: &RecordKey) -> Self {
145 StoreError::UnknownRecordLayout { key: key.clone() }
146 }
147}
148
149pub trait RecordCodec {
156 fn seal(&self, key: &RecordKey, plaintext: &[u8]) -> Vec<u8>;
160
161 fn open(&self, key: &RecordKey, sealed: &[u8]) -> Result<Vec<u8>, StoreError>;
166}
167
168#[cfg(feature = "test-support")]
180#[derive(Debug, Default, Clone, Copy)]
181pub struct InsecurePlainCodecForTests;
182
183#[cfg(feature = "test-support")]
184impl RecordCodec for InsecurePlainCodecForTests {
185 fn seal(&self, _key: &RecordKey, plaintext: &[u8]) -> Vec<u8> {
186 plaintext.to_vec()
187 }
188
189 fn open(&self, _key: &RecordKey, sealed: &[u8]) -> Result<Vec<u8>, StoreError> {
190 Ok(sealed.to_vec())
191 }
192}
193
194pub trait CryptoStore {
200 fn account(&self) -> Result<Option<&[u8]>, StoreError>;
202 fn save_account(&mut self, account: Vec<u8>) -> Result<(), StoreError>;
204
205 fn olm_sessions_for_device(&self, their_curve25519: &str) -> Result<Vec<Vec<u8>>, StoreError>;
212 fn save_olm_session(&mut self, their_curve25519: &str, session_id: &str, session: Vec<u8>) -> Result<(), StoreError>;
231
232 fn inbound_group_session(
235 &self,
236 room_id: &RoomId,
237 session_id: &str,
238 ) -> Result<Option<&[u8]>, StoreError>;
239 fn save_inbound_group_session(
242 &mut self,
243 room_id: &RoomId,
244 session_id: &str,
245 session: Vec<u8>,
246 ) -> Result<(), StoreError>;
247
248 fn outbound_group_session(&self, room_id: &RoomId) -> Result<Option<&[u8]>, StoreError>;
250 fn save_outbound_group_session(&mut self, room_id: &RoomId, session: Vec<u8>) -> Result<(), StoreError>;
253
254 fn devices_for_user(&self, user_id: &UserId) -> Result<Option<&[u8]>, StoreError>;
257 fn save_devices(&mut self, user_id: &UserId, devices: Vec<u8>) -> Result<(), StoreError>;
259
260 fn tracked_users(&self) -> Result<Vec<(UserId, bool)>, StoreError>;
263 fn mark_user_tracked(&mut self, user_id: &UserId, outdated: bool) -> Result<(), StoreError>;
266
267 fn withheld_reason(&self, room_id: &RoomId, session_id: &str) -> Result<Option<&[u8]>, StoreError>;
270 fn save_withheld(&mut self, room_id: &RoomId, session_id: &str, code: Vec<u8>) -> Result<(), StoreError>;
272
273 fn backup_key(&self) -> Result<Option<&[u8]>, StoreError>;
275 fn save_backup_key(&mut self, key: Vec<u8>) -> Result<(), StoreError>;
277
278 fn cross_signing_keys(&self) -> Result<Option<&[u8]>, StoreError>;
280 fn save_cross_signing_keys(&mut self, keys: Vec<u8>) -> Result<(), StoreError>;
283}
284
285pub trait StateStore {
293 fn sync_token(&self) -> Result<Option<&str>, StoreError>;
296 fn save_sync_token(&mut self, token: String) -> Result<(), StoreError>;
298
299 fn room_state(&self, room_id: &RoomId) -> Result<Option<&[u8]>, StoreError>;
301 fn save_room_state(&mut self, room_id: &RoomId, state: Vec<u8>) -> Result<(), StoreError>;
303 fn room_ids(&self) -> Result<Vec<RoomId>, StoreError>;
305
306 fn pending_requests(&self) -> Result<Vec<PendingRequest>, StoreError>;
308 fn save_pending_request(&mut self, req: PendingRequest) -> Result<(), StoreError>;
311 fn delete_pending_request(&mut self, id: &RequestId) -> Result<(), StoreError>;
314
315 fn counters(&self) -> Result<Option<Counters>, StoreError>;
320 fn save_counters(&mut self, counters: Counters) -> Result<(), StoreError>;
325}
326
327const MAX_OLM_SESSIONS_PER_DEVICE: usize = 10;
333
334const KIND_ACCOUNT: &str = "account";
335const KIND_OLM_SESSION: &str = "olm_session";
336const KIND_OLM_SESSION_ORDER: &str = "olm_session_order";
337const KIND_MEGOLM_IN: &str = "megolm_in";
338const KIND_MEGOLM_OUT: &str = "megolm_out";
339const KIND_DEVICES: &str = "devices";
340const KIND_TRACKED_USER: &str = "tracked_user";
341const KIND_WITHHELD: &str = "withheld";
342const KIND_BACKUP_KEY: &str = "backup_key";
343const KIND_CROSS_SIGNING_KEYS: &str = "cross_signing_keys";
344const KIND_SYNC_TOKEN: &str = "sync_token";
345const KIND_ROOM_STATE: &str = "room_state";
346const KIND_PENDING: &str = "pending";
347const KIND_COUNTERS: &str = "counters";
348
349fn account_record_key(device_id: &DeviceId) -> RecordKey {
350 RecordKey::new(format!("messenger/{}/{KIND_ACCOUNT}", device_id.as_str()))
351}
352
353fn olm_session_record_key(device_id: &DeviceId, their_curve25519: &str, session_id: &str) -> RecordKey {
354 RecordKey::new(format!(
355 "messenger/{}/{KIND_OLM_SESSION}/{}/{}",
356 device_id.as_str(),
357 percent_encode_segment(their_curve25519),
358 percent_encode_segment(session_id),
359 ))
360}
361
362fn olm_session_order_record_key(device_id: &DeviceId, their_curve25519: &str) -> RecordKey {
363 RecordKey::new(format!(
364 "messenger/{}/{KIND_OLM_SESSION_ORDER}/{}",
365 device_id.as_str(),
366 percent_encode_segment(their_curve25519),
367 ))
368}
369
370fn inbound_group_session_record_key(device_id: &DeviceId, room_id: &RoomId, session_id: &str) -> RecordKey {
371 RecordKey::new(format!(
372 "messenger/{}/{KIND_MEGOLM_IN}/{}/{}",
373 device_id.as_str(),
374 percent_encode_segment(room_id.as_str()),
375 percent_encode_segment(session_id),
376 ))
377}
378
379fn outbound_group_session_record_key(device_id: &DeviceId, room_id: &RoomId) -> RecordKey {
380 RecordKey::new(format!(
381 "messenger/{}/{KIND_MEGOLM_OUT}/{}",
382 device_id.as_str(),
383 percent_encode_segment(room_id.as_str()),
384 ))
385}
386
387fn devices_record_key(device_id: &DeviceId, user_id: &UserId) -> RecordKey {
388 RecordKey::new(format!(
389 "messenger/{}/{KIND_DEVICES}/{}",
390 device_id.as_str(),
391 percent_encode_segment(user_id.as_str()),
392 ))
393}
394
395fn tracked_user_record_key(device_id: &DeviceId, user_id: &UserId) -> RecordKey {
396 RecordKey::new(format!(
397 "messenger/{}/{KIND_TRACKED_USER}/{}",
398 device_id.as_str(),
399 percent_encode_segment(user_id.as_str()),
400 ))
401}
402
403fn withheld_record_key(device_id: &DeviceId, room_id: &RoomId, session_id: &str) -> RecordKey {
404 RecordKey::new(format!(
405 "messenger/{}/{KIND_WITHHELD}/{}/{}",
406 device_id.as_str(),
407 percent_encode_segment(room_id.as_str()),
408 percent_encode_segment(session_id),
409 ))
410}
411
412fn backup_key_record_key(device_id: &DeviceId) -> RecordKey {
413 RecordKey::new(format!("messenger/{}/{KIND_BACKUP_KEY}", device_id.as_str()))
414}
415
416fn cross_signing_keys_record_key(device_id: &DeviceId) -> RecordKey {
417 RecordKey::new(format!("messenger/{}/{KIND_CROSS_SIGNING_KEYS}", device_id.as_str()))
418}
419
420fn sync_token_record_key(device_id: &DeviceId) -> RecordKey {
421 RecordKey::new(format!("messenger/{}/{KIND_SYNC_TOKEN}", device_id.as_str()))
422}
423
424fn room_state_record_key(device_id: &DeviceId, room_id: &RoomId) -> RecordKey {
425 RecordKey::new(format!(
426 "messenger/{}/{KIND_ROOM_STATE}/{}",
427 device_id.as_str(),
428 percent_encode_segment(room_id.as_str()),
429 ))
430}
431
432fn pending_request_record_key(device_id: &DeviceId, request_id: &RequestId) -> RecordKey {
433 RecordKey::new(format!(
434 "messenger/{}/{KIND_PENDING}/{}",
435 device_id.as_str(),
436 percent_encode_segment(request_id.as_str()),
437 ))
438}
439
440fn counters_record_key(device_id: &DeviceId) -> RecordKey {
441 RecordKey::new(format!("messenger/{}/{KIND_COUNTERS}", device_id.as_str()))
442}
443
444enum RecognizedRecord {
447 Account,
448 OlmSession { their_curve25519: String, session_id: String },
449 OlmSessionOrder { their_curve25519: String },
450 InboundGroupSession { room_id: RoomId, session_id: String },
451 OutboundGroupSession { room_id: RoomId },
452 Devices { user_id: UserId },
453 TrackedUser { user_id: UserId },
454 Withheld { room_id: RoomId, session_id: String },
455 BackupKey,
456 CrossSigningKeys,
457 SyncToken,
458 RoomState { room_id: RoomId },
459 PendingRequest { request_id: RequestId },
460 Counters,
461}
462
463fn decode_room_id(encoded: &str, key: &RecordKey) -> Result<RoomId, StoreError> {
464 let decoded = percent_decode_segment(encoded).ok_or_else(|| StoreError::unknown_layout(key))?;
465 RoomId::parse(decoded).map_err(|_| StoreError::unknown_layout(key))
466}
467
468fn decode_user_id(encoded: &str, key: &RecordKey) -> Result<UserId, StoreError> {
469 let decoded = percent_decode_segment(encoded).ok_or_else(|| StoreError::unknown_layout(key))?;
470 UserId::parse(decoded).map_err(|_| StoreError::unknown_layout(key))
471}
472
473fn decode_plain_segment(encoded: &str, key: &RecordKey) -> Result<String, StoreError> {
474 percent_decode_segment(encoded).ok_or_else(|| StoreError::unknown_layout(key))
475}
476
477fn parse_record_key(device_id: &DeviceId, key: &RecordKey) -> Result<Option<RecognizedRecord>, StoreError> {
484 let prefix = format!("messenger/{}/", device_id.as_str());
485 let Some(rest) = key.as_str().strip_prefix(prefix.as_str()) else {
486 return Ok(None);
487 };
488 let mut parts = rest.splitn(2, '/');
489 let kind = parts.next().unwrap_or_default();
490 let remainder = parts.next();
491 let malformed = || StoreError::unknown_layout(key);
492
493 match kind {
494 KIND_ACCOUNT if remainder.is_none() => Ok(Some(RecognizedRecord::Account)),
495 KIND_SYNC_TOKEN if remainder.is_none() => Ok(Some(RecognizedRecord::SyncToken)),
496 KIND_BACKUP_KEY if remainder.is_none() => Ok(Some(RecognizedRecord::BackupKey)),
497 KIND_CROSS_SIGNING_KEYS if remainder.is_none() => Ok(Some(RecognizedRecord::CrossSigningKeys)),
498 KIND_COUNTERS if remainder.is_none() => Ok(Some(RecognizedRecord::Counters)),
499 KIND_OLM_SESSION => {
500 let remainder = remainder.ok_or_else(malformed)?;
501 let (curve_enc, session_enc) = remainder.split_once('/').ok_or_else(malformed)?;
502 let their_curve25519 = decode_plain_segment(curve_enc, key)?;
503 let session_id = decode_plain_segment(session_enc, key)?;
504 Ok(Some(RecognizedRecord::OlmSession { their_curve25519, session_id }))
505 }
506 KIND_OLM_SESSION_ORDER => {
507 let curve_enc = remainder.ok_or_else(malformed)?;
508 Ok(Some(RecognizedRecord::OlmSessionOrder { their_curve25519: decode_plain_segment(curve_enc, key)? }))
509 }
510 KIND_MEGOLM_IN => {
511 let remainder = remainder.ok_or_else(malformed)?;
512 let (room_enc, session_enc) = remainder.split_once('/').ok_or_else(malformed)?;
513 let room_id = decode_room_id(room_enc, key)?;
514 let session_id = decode_plain_segment(session_enc, key)?;
515 Ok(Some(RecognizedRecord::InboundGroupSession { room_id, session_id }))
516 }
517 KIND_MEGOLM_OUT => {
518 let room_enc = remainder.ok_or_else(malformed)?;
519 Ok(Some(RecognizedRecord::OutboundGroupSession { room_id: decode_room_id(room_enc, key)? }))
520 }
521 KIND_DEVICES => {
522 let user_enc = remainder.ok_or_else(malformed)?;
523 Ok(Some(RecognizedRecord::Devices { user_id: decode_user_id(user_enc, key)? }))
524 }
525 KIND_TRACKED_USER => {
526 let user_enc = remainder.ok_or_else(malformed)?;
527 Ok(Some(RecognizedRecord::TrackedUser { user_id: decode_user_id(user_enc, key)? }))
528 }
529 KIND_WITHHELD => {
530 let remainder = remainder.ok_or_else(malformed)?;
531 let (room_enc, session_enc) = remainder.split_once('/').ok_or_else(malformed)?;
532 let room_id = decode_room_id(room_enc, key)?;
533 let session_id = decode_plain_segment(session_enc, key)?;
534 Ok(Some(RecognizedRecord::Withheld { room_id, session_id }))
535 }
536 KIND_ROOM_STATE => {
537 let room_enc = remainder.ok_or_else(malformed)?;
538 Ok(Some(RecognizedRecord::RoomState { room_id: decode_room_id(room_enc, key)? }))
539 }
540 KIND_PENDING => {
541 let request_enc = remainder.ok_or_else(malformed)?;
542 let request_id = RequestId::from(decode_plain_segment(request_enc, key)?);
543 Ok(Some(RecognizedRecord::PendingRequest { request_id }))
544 }
545 _ => Ok(None),
546 }
547}
548
549fn decode_bool(bytes: &[u8], key: &RecordKey) -> Result<bool, StoreError> {
550 match bytes {
551 [0] => Ok(false),
552 [1] => Ok(true),
553 other => Err(StoreError::decode(key, format!("expected a single 0/1 byte, got {} bytes", other.len()))),
554 }
555}
556
557#[derive(Debug)]
563pub struct Store<C: RecordCodec> {
564 device_id: DeviceId,
565 codec: C,
566 gate: FlushGate,
567 account: Option<Vec<u8>>,
568 olm_sessions: BTreeMap<String, BTreeMap<String, Vec<u8>>>,
573 olm_session_order: BTreeMap<String, Vec<String>>,
577 inbound_group_sessions: BTreeMap<RoomId, BTreeMap<String, Vec<u8>>>,
578 outbound_group_sessions: BTreeMap<RoomId, Vec<u8>>,
579 devices: BTreeMap<UserId, Vec<u8>>,
580 tracked_users: BTreeMap<UserId, bool>,
581 withheld: BTreeMap<RoomId, BTreeMap<String, Vec<u8>>>,
582 backup_key: Option<Vec<u8>>,
583 cross_signing_keys: Option<Vec<u8>>,
584 sync_token: Option<String>,
585 room_state: BTreeMap<RoomId, Vec<u8>>,
586 pending_requests: BTreeMap<RequestId, PendingRequest>,
587 counters: Option<Counters>,
588 unknown: BTreeMap<RecordKey, Vec<u8>>,
592}
593
594impl<C: RecordCodec> Store<C> {
595 pub fn new(device_id: DeviceId, codec: C) -> Self {
601 Self {
602 device_id,
603 codec,
604 gate: FlushGate::new(),
605 account: None,
606 olm_sessions: BTreeMap::new(),
607 olm_session_order: BTreeMap::new(),
608 inbound_group_sessions: BTreeMap::new(),
609 outbound_group_sessions: BTreeMap::new(),
610 devices: BTreeMap::new(),
611 tracked_users: BTreeMap::new(),
612 withheld: BTreeMap::new(),
613 backup_key: None,
614 cross_signing_keys: None,
615 sync_token: None,
616 room_state: BTreeMap::new(),
617 pending_requests: BTreeMap::new(),
618 counters: None,
619 unknown: BTreeMap::new(),
620 }
621 }
622
623 pub fn load(
630 records: impl IntoIterator<Item = SealedRecord>,
631 codec: C,
632 device_id: DeviceId,
633 ) -> Result<Self, StoreError> {
634 let mut store = Self::new(device_id, codec);
635 for record in records {
636 match parse_record_key(&store.device_id, &record.key)? {
637 Some(recognized) => {
638 let plaintext = store.codec.open(&record.key, &record.bytes)?;
639 store.apply_recognized(recognized, plaintext, &record.key)?;
640 }
641 None => {
642 store.gate.mark_dirty(record.key.clone(), record.bytes.clone());
643 store.unknown.insert(record.key, record.bytes);
644 }
645 }
646 }
647 Ok(store)
648 }
649
650 fn apply_recognized(
651 &mut self,
652 recognized: RecognizedRecord,
653 plaintext: Vec<u8>,
654 key: &RecordKey,
655 ) -> Result<(), StoreError> {
656 match recognized {
657 RecognizedRecord::Account => self.account = Some(plaintext),
658 RecognizedRecord::OlmSession { their_curve25519, session_id } => {
659 self.olm_sessions.entry(their_curve25519).or_default().insert(session_id, plaintext);
660 }
661 RecognizedRecord::OlmSessionOrder { their_curve25519 } => {
662 let order: Vec<String> = serde_json::from_slice(&plaintext)
663 .map_err(|source| StoreError::decode(key, format!("session order is not valid JSON: {source}")))?;
664 self.olm_session_order.insert(their_curve25519, order);
665 }
666 RecognizedRecord::InboundGroupSession { room_id, session_id } => {
667 self.inbound_group_sessions.entry(room_id).or_default().insert(session_id, plaintext);
668 }
669 RecognizedRecord::OutboundGroupSession { room_id } => {
670 self.outbound_group_sessions.insert(room_id, plaintext);
671 }
672 RecognizedRecord::Devices { user_id } => {
673 self.devices.insert(user_id, plaintext);
674 }
675 RecognizedRecord::TrackedUser { user_id } => {
676 let outdated = decode_bool(&plaintext, key)?;
677 self.tracked_users.insert(user_id, outdated);
678 }
679 RecognizedRecord::Withheld { room_id, session_id } => {
680 self.withheld.entry(room_id).or_default().insert(session_id, plaintext);
681 }
682 RecognizedRecord::BackupKey => self.backup_key = Some(plaintext),
683 RecognizedRecord::CrossSigningKeys => self.cross_signing_keys = Some(plaintext),
684 RecognizedRecord::SyncToken => {
685 let token = String::from_utf8(plaintext)
686 .map_err(|_| StoreError::decode(key, "sync token is not valid UTF-8"))?;
687 self.sync_token = Some(token);
688 }
689 RecognizedRecord::RoomState { room_id } => {
690 self.room_state.insert(room_id, plaintext);
691 }
692 RecognizedRecord::PendingRequest { request_id } => {
693 let req: PendingRequest = serde_json::from_slice(&plaintext).map_err(|source| {
694 StoreError::decode(key, format!("pending request is not valid JSON: {source}"))
695 })?;
696 self.pending_requests.insert(request_id, req);
697 }
698 RecognizedRecord::Counters => {
699 let counters: Counters = serde_json::from_slice(&plaintext)
700 .map_err(|source| StoreError::decode(key, format!("counters record is not valid JSON: {source}")))?;
701 self.counters = Some(counters);
702 }
703 }
704 Ok(())
705 }
706
707 fn mark_dirty(&mut self, key: RecordKey, plaintext: Vec<u8>) {
708 let sealed = self.codec.seal(&key, &plaintext);
709 self.gate.mark_dirty(key, sealed);
710 }
711
712 fn mark_deleted(&mut self, key: RecordKey) {
713 self.gate.mark_deleted(key);
714 }
715
716 pub fn take_flush_batch(&mut self) -> Option<FlushBatch> {
720 self.gate.take_batch()
721 }
722
723 pub fn ack_flush(&mut self, id: u64) {
726 self.gate.ack(id);
727 }
728}
729
730impl<C: RecordCodec> FlushEpoch for Store<C> {
731 fn seal_for_request(&mut self) -> (RequiredSeq, Option<FlushBatch>) {
732 self.gate.seal_for_request()
733 }
734
735 fn is_released(&self, required: RequiredSeq) -> bool {
736 self.gate.is_released(required)
737 }
738}
739
740impl<C: RecordCodec> CryptoStore for Store<C> {
741 fn account(&self) -> Result<Option<&[u8]>, StoreError> {
742 Ok(self.account.as_deref())
743 }
744
745 fn save_account(&mut self, account: Vec<u8>) -> Result<(), StoreError> {
746 let key = account_record_key(&self.device_id);
747 self.mark_dirty(key, account.clone());
748 self.account = Some(account);
749 Ok(())
750 }
751
752 fn olm_sessions_for_device(&self, their_curve25519: &str) -> Result<Vec<Vec<u8>>, StoreError> {
753 let Some(sessions) = self.olm_sessions.get(their_curve25519) else { return Ok(Vec::new()) };
754 let Some(order) = self.olm_session_order.get(their_curve25519) else {
755 return Ok(sessions.values().cloned().collect());
756 };
757 Ok(order.iter().rev().filter_map(|session_id| sessions.get(session_id).cloned()).collect())
761 }
762
763 fn save_olm_session(&mut self, their_curve25519: &str, session_id: &str, session: Vec<u8>) -> Result<(), StoreError> {
764 let key = olm_session_record_key(&self.device_id, their_curve25519, session_id);
768 self.mark_dirty(key, session.clone());
769 self.olm_sessions.entry(their_curve25519.to_string()).or_default().insert(session_id.to_string(), session);
770
771 let mut order = self.olm_session_order.remove(their_curve25519).unwrap_or_default();
775 order.retain(|id| id != session_id);
776 order.push(session_id.to_string());
777 let evicted = (order.len() > MAX_OLM_SESSIONS_PER_DEVICE).then(|| order.remove(0));
778
779 let order_key = olm_session_order_record_key(&self.device_id, their_curve25519);
780 let order_bytes = serde_json::to_vec(&order).map_err(|source| {
781 StoreError::decode(&order_key, format!("failed to serialize session order: {source}"))
782 })?;
783 self.mark_dirty(order_key, order_bytes);
784 self.olm_session_order.insert(their_curve25519.to_string(), order);
785
786 if let Some(evicted_id) = evicted {
790 if let Some(sessions) = self.olm_sessions.get_mut(their_curve25519) {
791 sessions.remove(&evicted_id);
792 }
793 self.mark_deleted(olm_session_record_key(&self.device_id, their_curve25519, &evicted_id));
794 }
795 Ok(())
796 }
797
798 fn inbound_group_session(
799 &self,
800 room_id: &RoomId,
801 session_id: &str,
802 ) -> Result<Option<&[u8]>, StoreError> {
803 Ok(self
804 .inbound_group_sessions
805 .get(room_id)
806 .and_then(|sessions| sessions.get(session_id))
807 .map(Vec::as_slice))
808 }
809
810 fn save_inbound_group_session(
811 &mut self,
812 room_id: &RoomId,
813 session_id: &str,
814 session: Vec<u8>,
815 ) -> Result<(), StoreError> {
816 let key = inbound_group_session_record_key(&self.device_id, room_id, session_id);
817 self.mark_dirty(key, session.clone());
818 self.inbound_group_sessions
819 .entry(room_id.clone())
820 .or_default()
821 .insert(session_id.to_string(), session);
822 Ok(())
823 }
824
825 fn outbound_group_session(&self, room_id: &RoomId) -> Result<Option<&[u8]>, StoreError> {
826 Ok(self.outbound_group_sessions.get(room_id).map(Vec::as_slice))
827 }
828
829 fn save_outbound_group_session(&mut self, room_id: &RoomId, session: Vec<u8>) -> Result<(), StoreError> {
830 let key = outbound_group_session_record_key(&self.device_id, room_id);
831 self.mark_dirty(key, session.clone());
832 self.outbound_group_sessions.insert(room_id.clone(), session);
833 Ok(())
834 }
835
836 fn devices_for_user(&self, user_id: &UserId) -> Result<Option<&[u8]>, StoreError> {
837 Ok(self.devices.get(user_id).map(Vec::as_slice))
838 }
839
840 fn save_devices(&mut self, user_id: &UserId, devices: Vec<u8>) -> Result<(), StoreError> {
841 let key = devices_record_key(&self.device_id, user_id);
842 self.mark_dirty(key, devices.clone());
843 self.devices.insert(user_id.clone(), devices);
844 Ok(())
845 }
846
847 fn tracked_users(&self) -> Result<Vec<(UserId, bool)>, StoreError> {
848 Ok(self.tracked_users.iter().map(|(user_id, outdated)| (user_id.clone(), *outdated)).collect())
849 }
850
851 fn mark_user_tracked(&mut self, user_id: &UserId, outdated: bool) -> Result<(), StoreError> {
852 let key = tracked_user_record_key(&self.device_id, user_id);
853 self.mark_dirty(key, vec![u8::from(outdated)]);
854 self.tracked_users.insert(user_id.clone(), outdated);
855 Ok(())
856 }
857
858 fn withheld_reason(&self, room_id: &RoomId, session_id: &str) -> Result<Option<&[u8]>, StoreError> {
859 Ok(self.withheld.get(room_id).and_then(|sessions| sessions.get(session_id)).map(Vec::as_slice))
860 }
861
862 fn save_withheld(&mut self, room_id: &RoomId, session_id: &str, code: Vec<u8>) -> Result<(), StoreError> {
863 let key = withheld_record_key(&self.device_id, room_id, session_id);
864 self.mark_dirty(key, code.clone());
865 self.withheld.entry(room_id.clone()).or_default().insert(session_id.to_string(), code);
866 Ok(())
867 }
868
869 fn backup_key(&self) -> Result<Option<&[u8]>, StoreError> {
870 Ok(self.backup_key.as_deref())
871 }
872
873 fn save_backup_key(&mut self, key: Vec<u8>) -> Result<(), StoreError> {
874 let record_key = backup_key_record_key(&self.device_id);
875 self.mark_dirty(record_key, key.clone());
876 self.backup_key = Some(key);
877 Ok(())
878 }
879
880 fn cross_signing_keys(&self) -> Result<Option<&[u8]>, StoreError> {
881 Ok(self.cross_signing_keys.as_deref())
882 }
883
884 fn save_cross_signing_keys(&mut self, keys: Vec<u8>) -> Result<(), StoreError> {
885 let key = cross_signing_keys_record_key(&self.device_id);
886 self.mark_dirty(key, keys.clone());
887 self.cross_signing_keys = Some(keys);
888 Ok(())
889 }
890}
891
892impl<C: RecordCodec> StateStore for Store<C> {
893 fn sync_token(&self) -> Result<Option<&str>, StoreError> {
894 Ok(self.sync_token.as_deref())
895 }
896
897 fn save_sync_token(&mut self, token: String) -> Result<(), StoreError> {
898 let key = sync_token_record_key(&self.device_id);
899 self.mark_dirty(key, token.clone().into_bytes());
900 self.sync_token = Some(token);
901 Ok(())
902 }
903
904 fn room_state(&self, room_id: &RoomId) -> Result<Option<&[u8]>, StoreError> {
905 Ok(self.room_state.get(room_id).map(Vec::as_slice))
906 }
907
908 fn save_room_state(&mut self, room_id: &RoomId, state: Vec<u8>) -> Result<(), StoreError> {
909 let key = room_state_record_key(&self.device_id, room_id);
910 self.mark_dirty(key, state.clone());
911 self.room_state.insert(room_id.clone(), state);
912 Ok(())
913 }
914
915 fn room_ids(&self) -> Result<Vec<RoomId>, StoreError> {
916 Ok(self.room_state.keys().cloned().collect())
917 }
918
919 fn pending_requests(&self) -> Result<Vec<PendingRequest>, StoreError> {
920 Ok(self.pending_requests.values().cloned().collect())
921 }
922
923 fn save_pending_request(&mut self, req: PendingRequest) -> Result<(), StoreError> {
924 let id = req.request.id.clone();
925 let key = pending_request_record_key(&self.device_id, &id);
926 let plaintext = serde_json::to_vec(&req)
927 .map_err(|source| StoreError::decode(&key, format!("failed to serialize pending request: {source}")))?;
928 self.mark_dirty(key, plaintext);
929 self.pending_requests.insert(id, req);
930 Ok(())
931 }
932
933 fn delete_pending_request(&mut self, id: &RequestId) -> Result<(), StoreError> {
934 if self.pending_requests.remove(id).is_some() {
935 let key = pending_request_record_key(&self.device_id, id);
936 self.mark_deleted(key);
937 }
938 Ok(())
939 }
940
941 fn counters(&self) -> Result<Option<Counters>, StoreError> {
942 Ok(self.counters)
943 }
944
945 fn save_counters(&mut self, counters: Counters) -> Result<(), StoreError> {
946 let key = counters_record_key(&self.device_id);
947 let plaintext = serde_json::to_vec(&counters)
948 .map_err(|source| StoreError::decode(&key, format!("failed to serialize counters: {source}")))?;
949 self.mark_dirty(key, plaintext);
950 self.counters = Some(counters);
951 Ok(())
952 }
953}
954
955#[cfg(test)]
956mod tests {
957 use super::*;
958 use crate::ids::{DeviceId, RequestId, RoomId, UserId};
959 use crate::outgoing_queue::Lane;
960 use crate::wire::OutgoingRequest;
961
962 fn device_id() -> DeviceId {
963 DeviceId::parse("DEV1").expect("valid device id")
964 }
965
966 fn room_id() -> RoomId {
967 RoomId::parse("!room:example.org").expect("valid room id")
968 }
969
970 fn user_id() -> UserId {
971 UserId::parse("@alice:example.org").expect("valid user id")
972 }
973
974 fn pending_request(room: &RoomId) -> PendingRequest {
979 PendingRequest {
980 request: OutgoingRequest::room_members(RequestId::next(0), room),
981 lane: Lane::Other,
982 required_seq: RequiredSeq::NONE,
983 }
984 }
985
986 #[test]
987 fn in_memory_store_round_trips_account() {
988 let mut store = Store::new(device_id(), InsecurePlainCodecForTests);
989 assert_eq!(store.account().expect("no error"), None);
990
991 store.save_account(b"pickled-account".to_vec()).expect("save succeeds");
992 assert_eq!(store.account().expect("no error"), Some(b"pickled-account".as_slice()));
993 }
994
995 #[test]
996 fn in_memory_store_round_trips_pending_requests() {
997 let mut store = Store::new(device_id(), InsecurePlainCodecForTests);
998 assert!(store.pending_requests().expect("no error").is_empty());
999
1000 let req = pending_request(&room_id());
1001 store.save_pending_request(req.clone()).expect("save succeeds");
1002 assert_eq!(store.pending_requests().expect("no error"), vec![req]);
1003 }
1004
1005 #[test]
1006 fn round_trip_every_record_kind_through_load() {
1007 let device = device_id();
1008 let room = room_id();
1009 let user = user_id();
1010 let mut store = Store::new(device.clone(), InsecurePlainCodecForTests);
1011
1012 store.save_account(b"account-bytes".to_vec()).expect("save account");
1013 store
1014 .save_olm_session("curve25519-peer", "session-1", b"olm-session-bytes".to_vec())
1015 .expect("save olm session");
1016 store
1017 .save_inbound_group_session(&room, "session-1", b"megolm-in-bytes".to_vec())
1018 .expect("save inbound group session");
1019 store
1020 .save_outbound_group_session(&room, b"megolm-out-bytes".to_vec())
1021 .expect("save outbound group session");
1022 store.save_devices(&user, b"devices-bytes".to_vec()).expect("save devices");
1023 store.mark_user_tracked(&user, true).expect("mark tracked");
1024 store.save_withheld(&room, "session-1", b"withheld-bytes".to_vec()).expect("save withheld");
1025 store.save_backup_key(b"backup-bytes".to_vec()).expect("save backup key");
1026 store.save_cross_signing_keys(b"xsign-bytes".to_vec()).expect("save cross signing keys");
1027 store.save_sync_token("s123".to_string()).expect("save sync token");
1028 store.save_room_state(&room, b"room-state-bytes".to_vec()).expect("save room state");
1029 let req = pending_request(&room);
1030 store.save_pending_request(req.clone()).expect("save pending request");
1031 store.save_counters(Counters { next_request_id: 3, next_txn_id: 9 }).expect("save counters");
1032
1033 let batch = store.take_flush_batch().expect("everything above dirtied something");
1034 assert!(batch.deletes.is_empty());
1035
1036 let loaded =
1037 Store::load(batch.records, InsecurePlainCodecForTests, device).expect("load succeeds");
1038
1039 assert_eq!(loaded.account().expect("no error"), Some(b"account-bytes".as_slice()));
1040 assert_eq!(
1041 loaded.olm_sessions_for_device("curve25519-peer").expect("no error"),
1042 vec![b"olm-session-bytes".to_vec()]
1043 );
1044 assert_eq!(
1045 loaded.inbound_group_session(&room, "session-1").expect("no error"),
1046 Some(b"megolm-in-bytes".as_slice())
1047 );
1048 assert_eq!(
1049 loaded.outbound_group_session(&room).expect("no error"),
1050 Some(b"megolm-out-bytes".as_slice())
1051 );
1052 assert_eq!(loaded.devices_for_user(&user).expect("no error"), Some(b"devices-bytes".as_slice()));
1053 assert_eq!(loaded.tracked_users().expect("no error"), vec![(user.clone(), true)]);
1054 assert_eq!(
1055 loaded.withheld_reason(&room, "session-1").expect("no error"),
1056 Some(b"withheld-bytes".as_slice())
1057 );
1058 assert_eq!(loaded.backup_key().expect("no error"), Some(b"backup-bytes".as_slice()));
1059 assert_eq!(loaded.cross_signing_keys().expect("no error"), Some(b"xsign-bytes".as_slice()));
1060 assert_eq!(loaded.sync_token().expect("no error"), Some("s123"));
1061 assert_eq!(loaded.room_state(&room).expect("no error"), Some(b"room-state-bytes".as_slice()));
1062 assert_eq!(loaded.pending_requests().expect("no error"), vec![req]);
1063 assert_eq!(loaded.counters().expect("no error"), Some(Counters { next_request_id: 3, next_txn_id: 9 }));
1064 }
1065
1066 #[test]
1067 fn delete_is_exported_and_applied_on_load() {
1068 let device = device_id();
1069 let room = room_id();
1070 let mut store = Store::new(device.clone(), InsecurePlainCodecForTests);
1071
1072 let req = pending_request(&room);
1073 store.save_pending_request(req.clone()).expect("save pending request");
1074 let write_batch = store.take_flush_batch().expect("pending request save is pending");
1075 store.ack_flush(write_batch.id);
1076
1077 store.delete_pending_request(&req.request.id).expect("delete succeeds");
1078 let delete_batch = store.take_flush_batch().expect("deletion is pending");
1079 assert_eq!(delete_batch.deletes, vec![pending_request_record_key(&device, &req.request.id)]);
1080 store.ack_flush(delete_batch.id);
1081
1082 let mut durable: BTreeMap<RecordKey, Vec<u8>> =
1085 write_batch.records.into_iter().map(|record| (record.key, record.bytes)).collect();
1086 for key in delete_batch.deletes {
1087 durable.remove(&key);
1088 }
1089 let records = durable.into_iter().map(|(key, bytes)| SealedRecord { key, bytes });
1090
1091 let loaded = Store::load(records, InsecurePlainCodecForTests, device).expect("load succeeds");
1092 assert!(loaded.pending_requests().expect("no error").is_empty());
1093 }
1094
1095 #[test]
1096 fn unknown_keys_survive_a_load_and_reflush_untouched() {
1097 let device = device_id();
1098 let unknown_key = RecordKey::new(format!("messenger/{}/future_field/whatever", device.as_str()));
1099 let unknown_bytes = b"opaque-future-bytes".to_vec();
1100 let records = vec![SealedRecord { key: unknown_key.clone(), bytes: unknown_bytes.clone() }];
1101
1102 let mut store =
1103 Store::load(records, InsecurePlainCodecForTests, device).expect("unknown keys don't fail a load");
1104
1105 let batch = store.take_flush_batch().expect("the unknown record is re-adopted into the next flush");
1106 assert_eq!(batch.records.len(), 1);
1107 assert_eq!(batch.records[0].key, unknown_key);
1108 assert_eq!(batch.records[0].bytes, unknown_bytes, "re-emitted byte-for-byte, not re-sealed");
1109
1110 store.ack_flush(batch.id);
1111 let (required, extra_batch) = store.seal_for_request();
1112 assert!(extra_batch.is_none(), "nothing new became pending after the ack");
1113 assert!(store.is_released(required), "the only batch produced so far is acked");
1114 }
1115
1116 #[test]
1117 fn corrupt_record_fails_load_naming_the_key() {
1118 let device = device_id();
1119 let key = sync_token_record_key(&device);
1120 let corrupt = SealedRecord { key: key.clone(), bytes: vec![0xFF, 0xFE] };
1121
1122 let err = Store::load(vec![corrupt], InsecurePlainCodecForTests, device).unwrap_err();
1123 assert!(matches!(err, StoreError::Decode { .. }));
1124 assert!(err.to_string().contains(key.as_str()), "error names the offending key: {err}");
1125 }
1126
1127 #[test]
1128 fn malformed_known_prefix_key_is_a_hard_error() {
1129 let device = device_id();
1130 let key = RecordKey::new(format!("messenger/{}/olm_session", device.as_str()));
1133 let record = SealedRecord { key: key.clone(), bytes: b"whatever".to_vec() };
1134
1135 let err = Store::load(vec![record], InsecurePlainCodecForTests, device).unwrap_err();
1136 assert!(matches!(err, StoreError::UnknownRecordLayout { .. }));
1137 assert!(err.to_string().contains(key.as_str()));
1138 }
1139
1140 #[test]
1141 fn seal_for_request_blocks_release_until_the_sealed_batch_is_acked() {
1142 let mut store = Store::new(device_id(), InsecurePlainCodecForTests);
1143 assert!(store.is_released(RequiredSeq::NONE), "nothing dirtied yet");
1144
1145 store.save_account(b"account-bytes".to_vec()).expect("save succeeds");
1146 let (required, batch) = store.seal_for_request();
1147 let batch = batch.expect("the pending account mutation was sealed into a batch");
1148 assert!(!store.is_released(required), "sealed but not yet acked");
1149
1150 store.ack_flush(batch.id);
1151 assert!(store.is_released(required), "sealed and acked: safe to release");
1152 }
1153
1154 #[test]
1155 fn saving_a_ratcheted_session_leaves_no_older_snapshot_in_the_flush_batches_or_store() {
1156 let mut store = Store::new(device_id(), InsecurePlainCodecForTests);
1157
1158 store.save_olm_session("curve-peer", "session-1", b"pickle-v1".to_vec()).expect("save v1");
1159 let batch1 = store.take_flush_batch().expect("v1 is pending");
1160 store.ack_flush(batch1.id);
1161
1162 store.save_olm_session("curve-peer", "session-1", b"pickle-v2".to_vec()).expect("save v2, ratcheted forward");
1163 let batch2 = store.take_flush_batch().expect("v2 is pending");
1164 store.ack_flush(batch2.id);
1165
1166 fn session_keys_in(batch: &FlushBatch) -> Vec<&RecordKey> {
1167 batch.records.iter().filter(|r| r.key.as_str().contains("/olm_session/")).map(|r| &r.key).collect()
1168 }
1169 let batch1_keys = session_keys_in(&batch1);
1170 let batch2_keys = session_keys_in(&batch2);
1171 assert_eq!(batch1_keys.len(), 1, "v1's own flush carries exactly one session record");
1172 assert_eq!(batch2_keys.len(), 1, "v2's own flush carries exactly one session record");
1173 assert_eq!(batch1_keys[0], batch2_keys[0], "the second save replaced the SAME record key, not a new one");
1174 assert!(batch2.deletes.is_empty(), "no eviction yet -- only one distinct session id has ever been saved");
1175
1176 assert_eq!(store.olm_sessions_for_device("curve-peer").expect("no error"), vec![b"pickle-v2".to_vec()]);
1179 }
1180
1181 #[test]
1182 fn olm_sessions_per_device_are_capped() {
1183 let mut store = Store::new(device_id(), InsecurePlainCodecForTests);
1184 for i in 0..=MAX_OLM_SESSIONS_PER_DEVICE {
1185 store
1186 .save_olm_session("curve-peer", &format!("session-{i}"), format!("pickle-{i}").into_bytes())
1187 .expect("save");
1188 }
1189
1190 let sessions = store.olm_sessions_for_device("curve-peer").expect("no error");
1191 assert_eq!(sessions.len(), MAX_OLM_SESSIONS_PER_DEVICE, "capped, not accumulated without bound");
1192
1193 let evicted = b"pickle-0".to_vec();
1194 assert!(!sessions.contains(&evicted), "the least-recently-used session (the first ever saved) was evicted");
1195 let newest = format!("pickle-{MAX_OLM_SESSIONS_PER_DEVICE}").into_bytes();
1196 assert_eq!(sessions.first(), Some(&newest), "most-recently-used first");
1197
1198 let batch = store.take_flush_batch().expect("everything above is still pending");
1199 assert_eq!(batch.deletes.len(), 1, "exactly one eviction happened, producing exactly one delete");
1200 }
1201}