bitwarden_shared_unlock/lib.rs
1//! # Shared Unlock Protocol
2//!
3//! Synchronizes vault lock state across multiple Bitwarden clients (web, browser extension,
4//! desktop) running in the same session. When a user unlocks their vault on one client, the
5//! unlock propagates to all connected clients.
6//!
7//! ## Leader-Follower Model
8//!
9//! The protocol uses a leader-follower architecture where each client type has exactly one
10//! leader determined by the device hierarchy:
11//!
12//! ```text
13//! Web Client ──follows──▶ Browser Extension ──follows──▶ Desktop App
14//! CLI Client ──follows──▶ Desktop App
15//! ```
16//!
17//! - **Leader**: Holds authoritative lock state, broadcasts state changes to all followers.
18//! - **Follower**: Reports local state changes to its leader, applies authoritative updates from
19//! the leader.
20//!
21//! A client can be both a leader (to clients below it) and a follower (to the client above it)
22//! simultaneously. For example, the browser extension leads web clients while following the
23//! desktop app.
24//!
25//! ## Message Types
26//!
27//! All messages are serialized as CBOR and sent over the IPC transport.
28//!
29//! | Message | Direction | Purpose |
30//! |------------------|--------------------|---------------------------------------------------|
31//! | `StartSession` | Follower → Leader | Announce presence with current lock state |
32//! | `LockStateUpdate`| Bidirectional | Propagate lock/unlock events |
33//! | `HeartBeat` | Bidirectional | Keep session alive, suppress vault timeout |
34//!
35//! ## Session Lifecycle
36//!
37//! ### Follower Startup
38//!
39//! ```text
40//! Follower Leader
41//! │ │
42//! │──StartSession(user, state)─────▶│ Follower announces itself
43//! │ │ Leader applies state if unlocked
44//! │◀─LockStateUpdate(user, state)───│ Leader responds with authoritative state
45//! │ │
46//! ```
47//!
48//! On startup, the follower sends a `StartSession` for each logged-in user. If the follower
49//! is unlocked and the leader is locked, the leader unlocks using the provided user key.
50//! The leader always responds with a `LockStateUpdate` containing the authoritative state.
51//!
52//! ### Lock/Unlock Propagation
53//!
54//! **User unlocks on follower:**
55//!
56//! ```text
57//! Follower A Leader Follower B
58//! │ │ │
59//! │──LockStateUpdate(Unlocked)─────▶│ │
60//! │ │──unlocks locally── │
61//! │ │──LockStateUpdate(Unlocked)────▶│
62//! │ │ │──unlocks locally──
63//! ```
64//!
65//! **User locks on leader (via device event):**
66//!
67//! ```text
68//! Leader Follower A Follower B
69//! │ │ │
70//! │──LockStateUpdate(Locked)───────▶│ │
71//! │──LockStateUpdate(Locked)────────┼───────────────────────────────▶│
72//! │ │──locks locally── │──locks locally──
73//! ```
74//!
75//! ### Heartbeat Keep-Alive
76//!
77//! ```text
78//! Follower Leader
79//! │ │
80//! │──HeartBeat(user)───────────────▶│ Every N seconds
81//! │ │ Leader updates last-seen timestamp
82//! │◀─HeartBeat(user)────────────────│ Leader echoes back
83//! |◀─LockStateUpdate────────────────│ Leader always sends an authoritative state update to prevent desyncs
84//! │──suppresses vault timeout── │
85//! │ │
86//! ```
87//!
88//! The follower sends a `HeartBeat` for each logged-in user every [`HEARTBEAT_INTERVAL`]
89//! On receiving the echo, the follower suppresses its vault timeout timer,
90//! keeping the vault unlocked as long as the session is active. Stale sessions are pruned.
91//!
92//! ## Security Definitions
93//!
94//! - Attacker Model:
95//! - Attacker gains user-space access to the device while the vault has been locked (steals the
96//! device)
97//! - Security Goal:
98//! - Attacker cannot gain access to the vault key material
99//!
100//! This security definition is aimed at stolen or seized devices. Forensics should not uncover
101//! (passively) recorded or otherwise left behind key material. The IPC encryption prevents such a
102//! compromise.
103//!
104//! There is no further protection provided against active attackers running in userspace while the
105//! vault is unlocked on any of the clients on the device.
106//!
107//! - Attacker Model:
108//! - Attacker controls a website that is not the web vault
109//! - Security Goal:
110//! - Attacker cannot gain access to the vault key material
111//!
112//! This is met by origin validation.
113
114use bitwarden_core::UserId;
115use serde::{Deserialize, Serialize};
116use serde_bytes::ByteBuf;
117
118mod drivers;
119pub use drivers::*;
120mod follower;
121pub use follower::*;
122mod leader;
123pub use leader::*;
124mod message;
125pub use message::*;
126
127/// Interval used by followers to send heartbeat keep-alive messages to their leader.
128pub const HEARTBEAT_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5);
129/// Additional grace period added to the vault timeout when suppressing it on heartbeat
130pub const VAULT_TIMEOUT_GRACE_PERIOD: std::time::Duration = std::time::Duration::from_secs(1);
131
132#[cfg(test)]
133mod tests;
134
135/// Wrapper type containing a serialized user key used for unlock propagation.
136#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, zeroize::ZeroizeOnDrop)]
137pub struct UserKey(ByteBuf);
138
139impl UserKey {
140 /// Returns the raw user key bytes.
141 pub fn as_bytes(&self) -> &[u8] {
142 &self.0
143 }
144
145 /// Creates a user key wrapper from raw key bytes.
146 pub fn from_bytes(bytes: Vec<u8>) -> Self {
147 Self(ByteBuf::from(bytes))
148 }
149}
150
151impl std::fmt::Debug for UserKey {
152 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153 f.debug_tuple("UserKey").field(&"<redacted>").finish()
154 }
155}
156
157/// Represents the lock state of a user.
158#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
159pub enum LockState {
160 /// The user is locked (does not have a user-key in memory).
161 Locked,
162 /// The user is unlocked (has a user-key in memory).
163 Unlocked {
164 /// The user-key of the unlocked user
165 user_key: UserKey,
166 },
167}
168
169/// The device (client) has several events that need to be reported to the shared unlock system.
170/// This enum represents the events that need to be reported.
171#[derive(Serialize, Deserialize, zeroize::ZeroizeOnDrop)]
172#[cfg_attr(
173 feature = "wasm",
174 derive(tsify::Tsify),
175 tsify(into_wasm_abi, from_wasm_abi)
176)]
177pub enum DeviceEvent {
178 /// The user with the given user id has been locked manually in the UI
179 ManualLock {
180 #[zeroize(skip)]
181 /// User whose vault was manually locked.
182 user_id: UserId,
183 },
184 /// The user with the given user id has been unlocked manually in the UI
185 ManualUnlock {
186 #[zeroize(skip)]
187 /// User whose vault was manually unlocked.
188 user_id: UserId,
189 /// Raw user key bytes used to unlock the vault.
190 user_key: Vec<u8>,
191 },
192}