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//! If the leader receives a `HeartBeat` from a user it does not know (for example due to a process
92//! reload), it responds with a `RequestSessionStart` message to request the follower to start a
93//! session.
94//!
95//! ## Security Definitions
96//!
97//! - Attacker Model:
98//! - Attacker gains user-space access to the device while the vault has been locked (steals the
99//! device)
100//! - Security Goal:
101//! - Attacker cannot gain access to the vault key material
102//!
103//! This security definition is aimed at stolen or seized devices. Forensics should not uncover
104//! (passively) recorded or otherwise left behind key material. The IPC encryption prevents such a
105//! compromise.
106//!
107//! There is no further protection provided against active attackers running in userspace while the
108//! vault is unlocked on any of the clients on the device.
109//!
110//! - Attacker Model:
111//! - Attacker controls a website that is not the web vault
112//! - Security Goal:
113//! - Attacker cannot gain access to the vault key material
114//!
115//! This is met by origin validation.
116
117use bitwarden_core::UserId;
118use bitwarden_crypto::SymmetricCryptoKey;
119use serde::{Deserialize, Serialize};
120
121mod drivers;
122pub use drivers::*;
123mod follower;
124pub use follower::*;
125mod leader;
126pub use leader::*;
127mod message;
128pub use message::*;
129
130/// Wasm support module for shared unlock
131#[cfg(feature = "wasm")]
132pub mod wasm;
133
134/// Interval used by followers to send heartbeat keep-alive messages to their leader.
135pub const HEARTBEAT_INTERVAL: std::time::Duration = std::time::Duration::from_secs(2);
136/// Additional grace period added to the vault timeout when suppressing it on heartbeat
137pub const VAULT_TIMEOUT_GRACE_PERIOD: std::time::Duration = std::time::Duration::from_secs(1);
138
139#[cfg(test)]
140mod tests;
141
142/// Represents the lock state of a user.
143#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
144pub enum LockState {
145 /// The user is locked (does not have a user-key in memory).
146 Locked,
147 /// The user is unlocked (has a user-key in memory).
148 Unlocked {
149 /// The user-key of the unlocked user
150 user_key: SymmetricCryptoKey,
151 },
152}
153
154/// The device (client) has several events that need to be reported to the shared unlock system.
155/// This enum represents the events that need to be reported.
156#[derive(Serialize, Deserialize, zeroize::ZeroizeOnDrop)]
157#[cfg_attr(
158 feature = "wasm",
159 derive(tsify::Tsify),
160 tsify(into_wasm_abi, from_wasm_abi)
161)]
162pub enum DeviceEvent {
163 /// The user with the given user id has been locked manually in the UI
164 ManualLock {
165 #[zeroize(skip)]
166 /// User whose vault was manually locked.
167 user_id: UserId,
168 },
169 /// The user with the given user id has been unlocked manually in the UI
170 ManualUnlock {
171 #[zeroize(skip)]
172 /// User whose vault was manually unlocked.
173 user_id: UserId,
174 /// Raw user key bytes used to unlock the vault.
175 #[tsify(type = "SymmetricKey")]
176 user_key: SymmetricCryptoKey,
177 },
178}