bitwarden_shared_unlock/
message.rs1use bitwarden_core::UserId;
4use bitwarden_ipc::PayloadTypeName;
5use serde::{Deserialize, Serialize};
6
7use crate::LockState;
8
9#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
11pub struct TimestampedLockState {
12 pub lock_state: LockState,
14 pub changed_at: u64,
16}
17
18impl Default for TimestampedLockState {
19 fn default() -> Self {
23 Self {
24 lock_state: LockState::Locked,
25 changed_at: 0,
26 }
27 }
28}
29
30impl TimestampedLockState {
31 pub(crate) fn supersedes(&self, recorded: Option<&TimestampedLockState>) -> bool {
40 let recorded_at = recorded.map_or(0, |recorded| recorded.changed_at);
41 match self.changed_at.cmp(&recorded_at) {
42 std::cmp::Ordering::Greater => true,
43 std::cmp::Ordering::Less => false,
44 std::cmp::Ordering::Equal => {
45 matches!(self.lock_state, LockState::Locked)
46 && matches!(
47 recorded.map(|recorded| &recorded.lock_state),
48 Some(LockState::Unlocked { .. })
49 )
50 }
51 }
52 }
53}
54
55#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
57pub struct SharedUnlockSync {
58 pub user_id: UserId,
60 pub state: TimestampedLockState,
62}
63
64impl PayloadTypeName for SharedUnlockSync {
65 const PAYLOAD_TYPE_NAME: &'static str = "password-manager.shared-unlock.sync";
66}
67#[cfg(test)]
68mod tests {
69 use bitwarden_crypto::SymmetricCryptoKey;
70 use bitwarden_encoding::B64;
71
72 use super::*;
73
74 fn key() -> SymmetricCryptoKey {
75 SymmetricCryptoKey::try_from(B64::from([1u8; 64].to_vec()))
76 .expect("A 64-byte key should be valid")
77 }
78
79 fn locked_at(changed_at: u64) -> TimestampedLockState {
80 TimestampedLockState {
81 lock_state: LockState::Locked,
82 changed_at,
83 }
84 }
85
86 fn unlocked_at(changed_at: u64) -> TimestampedLockState {
87 TimestampedLockState {
88 lock_state: LockState::Unlocked { user_key: key() },
89 changed_at,
90 }
91 }
92
93 #[test]
94 fn a_newer_date_supersedes() {
95 assert!(unlocked_at(2).supersedes(Some(&locked_at(1))));
96 }
97
98 #[test]
99 fn an_older_date_does_not() {
100 assert!(!unlocked_at(1).supersedes(Some(&locked_at(2))));
101 }
102
103 #[test]
104 fn nothing_recorded_counts_as_date_zero() {
105 assert!(unlocked_at(1).supersedes(None));
106 assert!(
107 !locked_at(0).supersedes(None),
108 "The default advertisement must not read as an authoritative lock"
109 );
110 }
111
112 #[test]
113 fn an_equal_date_resolves_toward_locked() {
114 assert!(
115 locked_at(5).supersedes(Some(&unlocked_at(5))),
116 "A tie must fail closed"
117 );
118 assert!(
119 !unlocked_at(5).supersedes(Some(&locked_at(5))),
120 "and must resolve the same way seen from the other side"
121 );
122 }
123
124 #[test]
125 fn an_equal_date_with_the_same_state_is_a_no_op() {
126 assert!(!locked_at(5).supersedes(Some(&locked_at(5))));
127 assert!(!unlocked_at(5).supersedes(Some(&unlocked_at(5))));
128 }
129}