1use std::{
10 collections::{HashMap, HashSet},
11 sync::{Arc, Mutex},
12 time::Duration,
13};
14
15use bitwarden_core::UserId;
16use bitwarden_error::bitwarden_error;
17use bitwarden_ipc::{
18 Endpoint, IpcClient, IpcClientExt, RequestError, Source, SubscribeError, TypedIncomingMessage,
19};
20use bitwarden_threading::{cancellation_token, time::sleep};
21use thiserror::Error;
22use tracing::warn;
23
24use crate::{
25 DeviceEvent, LockState, SharedUnlockClient, SharedUnlockSync, TimestampedLockState,
26 active_peers::{ActivePeerTracker, SyncTarget},
27 drivers::SharedUnlockDriver,
28 timing::{SharedUnlockTiming, now_millis},
29};
30
31#[bitwarden_error(basic)]
33#[derive(Debug, Error)]
34#[error("Could not start shared unlock peer: {0}")]
35pub struct PeerStartError(#[from] SubscribeError);
36
37pub struct SharedUnlockPeer<D: SharedUnlockDriver>(Arc<InnerPeer<D>>);
39
40impl<D: SharedUnlockDriver> Clone for SharedUnlockPeer<D> {
41 fn clone(&self) -> Self {
42 Self(self.0.clone())
43 }
44}
45
46struct InnerPeer<D: SharedUnlockDriver> {
50 driver: D,
51 ipc_client: Arc<dyn IpcClient>,
52 states: Mutex<HashMap<UserId, TimestampedLockState>>,
54 active_peers: ActivePeerTracker,
55 timing: SharedUnlockTiming,
56 destinations: Mutex<HashMap<UserId, HashSet<SharedUnlockClient>>>,
60}
61
62impl<D: SharedUnlockDriver + Send + Sync + 'static> SharedUnlockPeer<D> {
63 pub fn create(driver: D, ipc_client: Arc<dyn IpcClient>) -> Self {
65 Self::create_with_timing(driver, ipc_client, SharedUnlockTiming::default())
66 }
67
68 #[cfg(any(test, feature = "test-support"))]
71 pub fn create_for_test(
72 driver: D,
73 ipc_client: Arc<dyn IpcClient>,
74 sync_interval: Duration,
75 vault_timeout_grace_period: Duration,
76 peer_stale_after: Duration,
77 ) -> Self {
78 Self::create_with_timing(
79 driver,
80 ipc_client,
81 SharedUnlockTiming {
82 sync_interval,
83 vault_timeout_grace_period,
84 peer_stale_after,
85 },
86 )
87 }
88
89 fn create_with_timing(
90 driver: D,
91 ipc_client: Arc<dyn IpcClient>,
92 timing: SharedUnlockTiming,
93 ) -> Self {
94 Self(Arc::new(InnerPeer {
95 driver,
96 ipc_client,
97 states: Mutex::new(HashMap::new()),
98 active_peers: ActivePeerTracker::default(),
99 timing,
100 destinations: Mutex::new(HashMap::new()),
101 }))
102 }
103
104 pub fn set_destinations(&self, user_id: UserId, destinations: Vec<SharedUnlockClient>) {
111 self.0
112 .destinations
113 .lock()
114 .unwrap_or_else(|poisoned| poisoned.into_inner())
115 .insert(user_id, destinations.into_iter().collect());
116 }
117
118 fn is_destination(&self, user_id: UserId, endpoint: &Endpoint) -> bool {
119 self.0
120 .destinations
121 .lock()
122 .unwrap_or_else(|poisoned| poisoned.into_inner())
123 .get(&user_id)
124 .is_some_and(|clients| clients.contains(&SharedUnlockClient::of_endpoint(endpoint)))
125 }
126
127 pub async fn start(
130 &self,
131 cancellation_token: Option<cancellation_token::CancellationToken>,
132 ) -> Result<(), PeerStartError> {
133 let cancellation_token = cancellation_token.unwrap_or_default();
134 let mut subscription = self
135 .0
136 .ipc_client
137 .subscribe_typed::<SharedUnlockSync>()
138 .await?;
139
140 let peer = self.clone();
141 let receive_token = cancellation_token.clone();
142 let receive_loop = async move {
143 loop {
144 match subscription.receive(Some(receive_token.clone())).await {
145 Ok(message) => {
146 if let Err(error) = peer.receive_message(message).await {
147 tracing::error!(?error, "Failed to handle shared unlock sync");
148 }
149 }
150 Err(bitwarden_ipc::TypedReceiveError::Cancelled) => {
151 tracing::info!("Shared unlock peer stopped by cancellation");
152 break;
153 }
154 Err(bitwarden_ipc::TypedReceiveError::Channel(
156 tokio::sync::broadcast::error::RecvError::Closed,
157 )) => {
158 tracing::info!("Transport channel closed. Waiting for it to open");
159 sleep(Duration::from_secs(1)).await;
160 }
161 Err(error) => {
162 tracing::error!(?error, "Failed to receive shared unlock IPC message");
163 }
164 }
165 }
166 };
167
168 spawn(receive_loop);
169
170 let peer = self.clone();
171 let timer_token = cancellation_token.clone();
172 let timing = self.0.timing;
173 let timer_loop = async move {
174 loop {
175 tokio::select! {
176 _ = timer_token.cancelled() => {
177 tracing::debug!("Shared unlock peer timer cancelled");
178 break;
179 }
180 _ = sleep(timing.sync_interval) => {
181 peer.0.active_peers.prune_stale(timing.peer_stale_after);
182 peer.sync_all_users().await;
183 }
184 }
185 }
186 };
187
188 spawn(timer_loop);
189
190 self.sync_all_users().await;
191 Ok(())
192 }
193
194 pub async fn receive_message(
196 &self,
197 incoming_message: TypedIncomingMessage<SharedUnlockSync>,
198 ) -> Result<(), ()> {
199 let source = incoming_message.source;
200 let SharedUnlockSync { user_id, state } = incoming_message.payload;
201
202 if !self.is_destination(user_id, &source.to_endpoint()) {
203 tracing::debug!(
204 ?source,
205 %user_id,
206 "Ignoring shared unlock sync from a client this user is not shared with"
207 );
208 return Ok(());
209 }
210
211 if let Source::Web { origin, .. } = &source {
213 match self.0.driver.get_vault_url(user_id).await {
214 Some(user_vault_url) if origin == &user_vault_url => {}
215 Some(user_vault_url) => {
216 warn!(%origin, %user_vault_url, "IPC message origin does not match user's vault URL, ignoring message");
217 return Ok(());
218 }
219 None => {
220 warn!(%origin, "No vault URL found for user, ignoring message");
221 return Ok(());
222 }
223 }
224 }
225
226 if !self.0.driver.list_users().await.contains(&user_id) {
227 tracing::debug!(
228 %user_id,
229 "Ignoring shared unlock sync for a user this device has no account for"
230 );
231 return Ok(());
232 }
233
234 let target = SyncTarget::from_source(&source);
235 let from_leader = self.0.driver.discover_leader().await.as_ref() == Some(&target.endpoint);
236 if from_leader {
237 self.0
240 .driver
241 .suppress_vault_timeout(
242 user_id,
243 self.0.timing.sync_interval + self.0.timing.vault_timeout_grace_period,
244 )
245 .await;
246 }
247
248 let first_contact = self.0.active_peers.upsert(&target);
249 self.apply_remote_state(user_id, state).await?;
250
251 if first_contact {
252 self.sync_all_users_to(&target).await;
253 }
254
255 Ok(())
256 }
257
258 pub async fn handle_device_event(&self, event: DeviceEvent) -> Result<(), ()> {
260 let (user_id, lock_state) = match &event {
261 DeviceEvent::ManualLock { user_id } => (*user_id, LockState::Locked),
262 DeviceEvent::ManualUnlock { user_id, user_key } => (
263 *user_id,
264 LockState::Unlocked {
265 user_key: user_key.to_owned(),
266 },
267 ),
268 };
269
270 tracing::debug!(
271 %user_id,
272 lock_state = lock_state.describe(),
273 "Shared unlock device event reported by this client"
274 );
275
276 self.record_local_state(user_id, lock_state);
277 self.sync_user(user_id).await;
278 Ok(())
279 }
280
281 async fn apply_remote_state(
283 &self,
284 user_id: UserId,
285 remote: TimestampedLockState,
286 ) -> Result<(), ()> {
287 if !remote.supersedes(self.recorded_state(user_id).as_ref()) {
288 return Ok(());
289 }
290
291 let differs = remote.lock_state != self.advertised_state(user_id).lock_state;
298 if differs {
299 tracing::debug!(
300 %user_id,
301 lock_state = remote.lock_state.describe(),
302 changed_at = remote.changed_at,
303 "Applying a peer's lock state through the driver"
304 );
305
306 match &remote.lock_state {
307 LockState::Locked => self
308 .0
309 .driver
310 .lock_user(user_id)
311 .await
312 .inspect_err(|_| warn!(%user_id, "Failed to lock user"))?,
313 LockState::Unlocked { user_key } => self
314 .0
315 .driver
316 .unlock_user(user_id, user_key.to_owned())
317 .await
318 .inspect_err(|_| warn!(%user_id, "Failed to unlock user"))?,
319 }
320 }
321
322 self.record_remote_state(user_id, remote);
323 Ok(())
324 }
325
326 async fn sync_all_users(&self) {
328 for user_id in self.0.driver.list_users().await {
329 self.sync_user(user_id).await;
330 }
331 }
332
333 async fn sync_user(&self, user_id: UserId) {
334 for target in self.sync_targets().await {
335 self.sync_user_to(user_id, &target).await;
336 }
337 }
338
339 async fn sync_all_users_to(&self, target: &SyncTarget) {
341 for user_id in self.0.driver.list_users().await {
342 self.sync_user_to(user_id, target).await;
343 }
344 }
345
346 async fn sync_user_to(&self, user_id: UserId, target: &SyncTarget) {
347 if !self.may_sync_user_to(user_id, target).await {
348 return;
349 }
350
351 let message = SharedUnlockSync {
352 user_id,
353 state: self.advertised_state(user_id),
354 };
355 self.send_message(message, target.endpoint.clone()).await;
356 }
357
358 async fn may_sync_user_to(&self, user_id: UserId, target: &SyncTarget) -> bool {
368 if !self.is_destination(user_id, &target.endpoint) {
369 return false;
370 }
371
372 if !matches!(target.endpoint, Endpoint::Web { .. }) {
373 return true;
374 }
375
376 let Some(origin) = target.origin.as_deref() else {
377 warn!(
378 ?target,
379 "Web peer has no validated origin, not syncing to it"
380 );
381 return false;
382 };
383
384 match self.0.driver.get_vault_url(user_id).await {
385 Some(user_vault_url) if user_vault_url == origin => true,
386 Some(user_vault_url) => {
387 warn!(%origin, %user_vault_url, %user_id, "Web peer's origin does not match the user's vault URL, not syncing that user to it");
388 false
389 }
390 None => {
391 warn!(%origin, %user_id, "No vault URL found for user, not syncing that user to a web peer");
392 false
393 }
394 }
395 }
396
397 async fn sync_targets(&self) -> Vec<SyncTarget> {
399 let mut targets = self.0.active_peers.targets();
400 if let Some(leader) = self.0.driver.discover_leader().await
401 && !targets.iter().any(|target| target.endpoint == leader)
402 {
403 targets.push(SyncTarget::without_origin(leader));
406 }
407 targets
408 }
409
410 async fn send_message(&self, message: SharedUnlockSync, recipient: Endpoint) {
411 tracing::debug!(
412 user_id = %message.user_id,
413 lock_state = message.state.lock_state.describe(),
414 changed_at = message.state.changed_at,
415 ?recipient,
416 "Sending a shared unlock sync"
417 );
418
419 if let Err(error) = self
420 .0
421 .ipc_client
422 .send_typed(message, recipient.clone())
423 .await
424 {
425 match error {
426 RequestError::Unreachable => {
427 tracing::debug!(
431 ?recipient,
432 "Shared unlock peer unreachable; sync not delivered"
433 );
434 }
435 RequestError::Timeout(_) => {
436 tracing::warn!(?error, "Timeout sending shared unlock sync");
437 }
438 _ => {
439 tracing::error!(?error, "Failed to send shared unlock IPC message");
440 }
441 }
442 }
443 }
444
445 #[cfg(any(test, feature = "test-support"))]
450 pub fn recorded_changed_at(&self, user_id: UserId) -> Option<u64> {
451 self.recorded_state(user_id).map(|state| state.changed_at)
452 }
453
454 fn recorded_state(&self, user_id: UserId) -> Option<TimestampedLockState> {
455 self.0
456 .states
457 .lock()
458 .unwrap_or_else(|poisoned| poisoned.into_inner())
459 .get(&user_id)
460 .cloned()
461 }
462
463 fn advertised_state(&self, user_id: UserId) -> TimestampedLockState {
465 self.recorded_state(user_id).unwrap_or_default()
466 }
467
468 fn record_remote_state(&self, user_id: UserId, state: TimestampedLockState) {
470 let mut states = self
471 .0
472 .states
473 .lock()
474 .unwrap_or_else(|poisoned| poisoned.into_inner());
475
476 if state.supersedes(states.get(&user_id)) {
477 states.insert(user_id, state);
478 }
479 }
480
481 fn record_local_state(&self, user_id: UserId, lock_state: LockState) {
483 let mut states = self
484 .0
485 .states
486 .lock()
487 .unwrap_or_else(|poisoned| poisoned.into_inner());
488
489 let changed_at = states
490 .get(&user_id)
491 .map_or(0, |recorded| recorded.changed_at.saturating_add(1))
492 .max(now_millis());
493
494 states.insert(
495 user_id,
496 TimestampedLockState {
497 lock_state,
498 changed_at,
499 },
500 );
501 }
502}
503
504#[cfg(not(target_arch = "wasm32"))]
505fn spawn(future: impl std::future::Future<Output = ()> + Send + 'static) {
506 tokio::spawn(future);
507}
508
509#[cfg(target_arch = "wasm32")]
510fn spawn(future: impl std::future::Future<Output = ()> + 'static) {
511 wasm_bindgen_futures::spawn_local(future);
512}