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//! ## Peer Model
8//!
9//! Every client runs exactly one [`SharedUnlockPeer`]. Each peer knows the one peer above it in the
10//! device hierarchy — its *leader* — and syncs to it, while also serving whichever peers sync to
11//! it:
12//!
13//! ```text
14//!   Web Client  ──syncs to──▶  Browser Extension  ──syncs to──▶  Desktop App
15//!   CLI Client  ──syncs to──▶  Desktop App
16//! ```
17//!
18//! The hierarchy decides who talks to whom, not who is in charge. There is no authoritative
19//! participant: reconciliation is symmetric and the freshest state wins. A peer in the middle of
20//! the chain relays simply by applying what it hears and advertising what it holds — the browser
21//! extension needs no special handling for leading the web vault while following the desktop app.
22//!
23//! The desktop app is the only client with no leader; it exclusively serves.
24//!
25//! ## Messages
26//!
27//! There is one message, [`SharedUnlockSync`], carrying a user id and that device's
28//! [`TimestampedLockState`]. It is sent in both directions.
29//!
30//! A peer sends one sync per logged-in user, to its leader and to every active peer:
31//!
32//! - immediately on [`SharedUnlockPeer::start`], so it does not wait an interval to be discovered,
33//! - on every [`DeviceEvent`], so a manual lock or unlock propagates without delay, and
34//! - every [`SYNC_INTERVAL`].
35//! - as a reply to first sync connection
36//!
37//! ```text
38//!   Peer                                      Peer above (leader)
39//!     │                                          │
40//!     │──Sync(user, state@date)─────────────────▶│  on start, on device event, every interval
41//!     │                                          │  · applies the state if the date is newer
42//!     │                                          │  · registers the sender as an active peer
43//!     │                                          │
44//!     │◀─Sync(user, state@date)──────────────────│  once on first contact, then on device
45//!     │  · applies the state if the date is newer │  events and every interval
46//!     │  · suppresses its vault timeout           │
47//! ```
48//!
49//! ## Reconciliation
50//!
51//! On receiving a sync, a peer:
52//!
53//! 1. Drops it if the source is a web client whose origin does not match the user's vault URL. The
54//!    origin that passed this check is kept with the peer, and the same check is applied again on
55//!    the way out (see [Origin scoping](#origin-scoping)).
56//! 2. Drops it if the user is not in [`SharedUnlockDriver::list_users`] — that is how a peer knows
57//!    it has no account for a user, and it never advertises such a user either.
58//! 3. Drops it if `changed_at` is older than the date this device has recorded. A user this device
59//!    has recorded nothing for counts as date `0`.
60//! 4. On an *equal* date, drops it unless it is a `Locked` arriving at an unlocked device. Equal
61//!    dates mean two devices acted inside the same millisecond without having seen each other, so
62//!    the tie is broken toward `Locked`: both sides then resolve it identically and converge
63//!    without another round, and the ambiguous case fails closed rather than resurrecting an
64//!    unlock.
65//! 5. Otherwise records the incoming state *and its date*, and calls
66//!    [`SharedUnlockDriver::lock_user`] or [`SharedUnlockDriver::unlock_user`] if — and only if —
67//!    the state actually differs from what was recorded.
68//!
69//! ## Origin scoping
70//!
71//! A web peer is scoped to one origin, in both directions:
72//!
73//! - an incoming sync from a web source is dropped unless its origin is the vault URL of the user
74//!   it carries, and
75//! - an outgoing sync is withheld from a web peer unless the user's vault URL is the origin that
76//!   peer was registered with — which covers the introductory reply on first contact as much as the
77//!   periodic and device-event syncs.
78//!
79//! ## Keep-alive
80//!
81//! A sync received *from this peer's leader* also calls
82//! [`SharedUnlockDriver::suppress_vault_timeout`] for [`SYNC_INTERVAL`] plus
83//! [`VAULT_TIMEOUT_GRACE_PERIOD`], keeping the vault unlocked as long as the shared session is
84//! active. Syncs from peers below do not suppress anything; they only mark the sender active.
85//!
86//! Peers that have not been heard from in [`PEER_STALE_AFTER`] are pruned and stop being synced to.
87//!
88//! ## Security Definitions
89//!
90//! - Attacker Model:
91//!   - Attacker gains user-space access to the device while the vault has been locked (steals the
92//!     device)
93//! - Security Goal:
94//!   - Attacker cannot gain access to the vault key material
95//!
96//! This security definition is aimed at stolen or seized devices. Forensics should not uncover
97//! (passively) recorded or otherwise left behind key material. The IPC encryption prevents such a
98//! compromise.
99//!
100//! There is no further protection provided against active attackers running in userspace while the
101//! vault is unlocked on any of the clients on the device.
102//!
103//! - Attacker Model:
104//!   - Attacker controls a website that is not the web vault
105//! - Security Goal:
106//!   - Attacker cannot gain access to the vault key material
107//!
108//! This is met by origin validation, which is enforced on both the receive and the send path — see
109//! [Origin scoping](#origin-scoping). Validating only what arrives would not meet the goal: a peer
110//! that is registered after one validated sync goes on to be sent every user's state.
111
112use bitwarden_core::UserId;
113use bitwarden_crypto::SymmetricCryptoKey;
114use bitwarden_ipc::Endpoint;
115use serde::{Deserialize, Serialize};
116
117mod active_peers;
118mod drivers;
119pub use drivers::*;
120mod message;
121pub use message::*;
122mod peer;
123pub use peer::*;
124mod timing;
125
126/// Wasm support module for shared unlock
127#[cfg(feature = "wasm")]
128pub mod wasm;
129
130/// Interval at which a peer syncs its lock state to its leader and to its active peers.
131pub const SYNC_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5);
132/// Additional grace period added to the vault timeout when suppressing it on a sync from the
133/// leader.
134pub const VAULT_TIMEOUT_GRACE_PERIOD: std::time::Duration = std::time::Duration::from_secs(2);
135/// How long a peer may go without syncing before it is pruned and no longer synced to.
136pub const PEER_STALE_AFTER: std::time::Duration = std::time::Duration::from_secs(30);
137
138/// Represents the lock state of a user.
139#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
140pub enum LockState {
141    /// The user is locked (does not have a user-key in memory).
142    Locked,
143    /// The user is unlocked (has a user-key in memory).
144    Unlocked {
145        /// The user-key of the unlocked user
146        user_key: SymmetricCryptoKey,
147    },
148}
149
150impl LockState {
151    /// Names the state without touching the key it may carry, so it is safe to log.
152    pub(crate) fn describe(&self) -> &'static str {
153        match self {
154            LockState::Locked => "locked",
155            LockState::Unlocked { .. } => "unlocked",
156        }
157    }
158}
159
160/// The device (client) has several events that need to be reported to the shared unlock system.
161/// This enum represents the events that need to be reported.
162#[derive(Serialize, Deserialize, zeroize::ZeroizeOnDrop)]
163#[cfg_attr(
164    feature = "wasm",
165    derive(tsify::Tsify),
166    tsify(into_wasm_abi, from_wasm_abi)
167)]
168pub enum DeviceEvent {
169    /// The user with the given user id has been locked manually in the UI
170    ManualLock {
171        #[zeroize(skip)]
172        /// User whose vault was manually locked.
173        user_id: UserId,
174    },
175    /// The user with the given user id has been unlocked manually in the UI
176    ManualUnlock {
177        #[zeroize(skip)]
178        /// User whose vault was manually unlocked.
179        user_id: UserId,
180        /// Raw user key bytes used to unlock the vault.
181        #[cfg_attr(feature = "wasm", tsify(type = "SymmetricKey"))]
182        user_key: SymmetricCryptoKey,
183    },
184}
185
186/// A kind of client a peer may share unlock state with, independent of the IPC endpoint variants
187/// that address its individual contexts (foreground/background, renderer/main).
188#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
189#[cfg_attr(
190    feature = "wasm",
191    derive(tsify::Tsify),
192    tsify(into_wasm_abi, from_wasm_abi)
193)]
194pub enum SharedUnlockClient {
195    /// The browser extension, in any of its contexts.
196    Browser,
197    /// The desktop app, in any of its processes.
198    Desktop,
199    /// A web vault tab.
200    Web,
201}
202
203impl SharedUnlockClient {
204    /// The client an endpoint belongs to.
205    pub(crate) fn of_endpoint(endpoint: &Endpoint) -> Self {
206        match endpoint {
207            Endpoint::Web { .. } => SharedUnlockClient::Web,
208            Endpoint::BrowserForeground { .. } | Endpoint::BrowserBackground { .. } => {
209                SharedUnlockClient::Browser
210            }
211            Endpoint::DesktopRenderer | Endpoint::DesktopMain => SharedUnlockClient::Desktop,
212        }
213    }
214}