Skip to main content

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 bitwarden_crypto::SymmetricCryptoKey;
116use serde::{Deserialize, Serialize};
117
118mod drivers;
119pub use drivers::*;
120mod follower;
121pub use follower::*;
122mod leader;
123pub use leader::*;
124mod message;
125pub use message::*;
126
127/// Wasm support module for shared unlock
128#[cfg(feature = "wasm")]
129pub mod wasm;
130
131/// Interval used by followers to send heartbeat keep-alive messages to their leader.
132pub const HEARTBEAT_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5);
133/// Additional grace period added to the vault timeout when suppressing it on heartbeat
134pub const VAULT_TIMEOUT_GRACE_PERIOD: std::time::Duration = std::time::Duration::from_secs(1);
135
136#[cfg(test)]
137mod tests;
138
139/// Represents the lock state of a user.
140#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
141pub enum LockState {
142    /// The user is locked (does not have a user-key in memory).
143    Locked,
144    /// The user is unlocked (has a user-key in memory).
145    Unlocked {
146        /// The user-key of the unlocked user
147        user_key: SymmetricCryptoKey,
148    },
149}
150
151/// The device (client) has several events that need to be reported to the shared unlock system.
152/// This enum represents the events that need to be reported.
153#[derive(Serialize, Deserialize, zeroize::ZeroizeOnDrop)]
154#[cfg_attr(
155    feature = "wasm",
156    derive(tsify::Tsify),
157    tsify(into_wasm_abi, from_wasm_abi)
158)]
159pub enum DeviceEvent {
160    /// The user with the given user id has been locked manually in the UI
161    ManualLock {
162        #[zeroize(skip)]
163        /// User whose vault was manually locked.
164        user_id: UserId,
165    },
166    /// The user with the given user id has been unlocked manually in the UI
167    ManualUnlock {
168        #[zeroize(skip)]
169        /// User whose vault was manually unlocked.
170        user_id: UserId,
171        /// Raw user key bytes used to unlock the vault.
172        #[tsify(type = "SymmetricKey")]
173        user_key: SymmetricCryptoKey,
174    },
175}