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 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}