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, spawn, 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
250 let reported = state.lock_state.peer_state();
252 self.apply_remote_state(user_id, state).await?;
253 self.0.driver.on_peer_state(user_id, reported).await;
254
255 if first_contact {
256 self.sync_all_users_to(&target).await;
257 }
258
259 Ok(())
260 }
261
262 pub async fn handle_device_event(&self, event: DeviceEvent) -> Result<(), ()> {
264 let (user_id, lock_state) = match &event {
265 DeviceEvent::ManualLock { user_id } => (*user_id, LockState::Locked),
266 DeviceEvent::ManualUnlock { user_id, user_key } => (
267 *user_id,
268 LockState::Unlocked {
269 user_key: user_key.to_owned(),
270 },
271 ),
272 };
273
274 tracing::debug!(
275 %user_id,
276 lock_state = lock_state.describe(),
277 "Shared unlock device event reported by this client"
278 );
279
280 let changed_at = self.record_local_state(user_id, lock_state);
281
282 if let DeviceEvent::ManualLock { .. } = event {
284 self.0
285 .driver
286 .set_last_manual_lock(user_id, changed_at)
287 .await;
288 }
289
290 self.sync_user(user_id).await;
291 Ok(())
292 }
293
294 async fn apply_remote_state(
296 &self,
297 user_id: UserId,
298 remote: TimestampedLockState,
299 ) -> Result<(), ()> {
300 if !remote.supersedes(self.restored_state(user_id).await.as_ref()) {
301 return Ok(());
302 }
303
304 if let LockState::Locked = remote.lock_state {
309 self.0
310 .driver
311 .set_last_manual_lock(user_id, remote.changed_at)
312 .await;
313 }
314
315 let differs = remote.lock_state != self.advertised_state(user_id).await.lock_state;
322 if differs {
323 tracing::debug!(
324 %user_id,
325 lock_state = remote.lock_state.describe(),
326 changed_at = remote.changed_at,
327 "Applying a peer's lock state through the driver"
328 );
329
330 self.relay_user_state(user_id, &remote).await;
331
332 match &remote.lock_state {
333 LockState::Locked => self
334 .0
335 .driver
336 .lock_user(user_id)
337 .await
338 .inspect_err(|_| warn!(%user_id, "Failed to lock user"))?,
339 LockState::Unlocked { user_key } => self
340 .0
341 .driver
342 .unlock_user(user_id, user_key.to_owned())
343 .await
344 .inspect_err(|_| warn!(%user_id, "Failed to unlock user"))?,
345 }
346 }
347
348 self.record_remote_state(user_id, remote);
349 Ok(())
350 }
351
352 async fn sync_all_users(&self) {
354 for user_id in self.0.driver.list_users().await {
355 self.sync_user(user_id).await;
356 }
357 }
358
359 async fn sync_user(&self, user_id: UserId) {
360 for target in self.sync_targets().await {
361 self.sync_user_to(user_id, &target).await;
362 }
363 }
364
365 async fn relay_user_state(&self, user_id: UserId, state: &TimestampedLockState) {
368 for target in self.sync_targets().await {
369 if !self.may_sync_user_to(user_id, &target).await {
370 continue;
371 }
372
373 let message = SharedUnlockSync {
374 user_id,
375 state: state.clone(),
376 };
377 self.send_message(message, target.endpoint.clone()).await;
378 }
379 }
380
381 async fn sync_all_users_to(&self, target: &SyncTarget) {
383 for user_id in self.0.driver.list_users().await {
384 self.sync_user_to(user_id, target).await;
385 }
386 }
387
388 async fn sync_user_to(&self, user_id: UserId, target: &SyncTarget) {
389 if !self.may_sync_user_to(user_id, target).await {
390 return;
391 }
392
393 let message = SharedUnlockSync {
394 user_id,
395 state: self.advertised_state(user_id).await,
396 };
397 self.send_message(message, target.endpoint.clone()).await;
398 }
399
400 async fn may_sync_user_to(&self, user_id: UserId, target: &SyncTarget) -> bool {
410 if !self.is_destination(user_id, &target.endpoint) {
411 return false;
412 }
413
414 if !matches!(target.endpoint, Endpoint::Web { .. }) {
415 return true;
416 }
417
418 let Some(origin) = target.origin.as_deref() else {
419 warn!(
420 ?target,
421 "Web peer has no validated origin, not syncing to it"
422 );
423 return false;
424 };
425
426 match self.0.driver.get_vault_url(user_id).await {
427 Some(user_vault_url) if user_vault_url == origin => true,
428 Some(user_vault_url) => {
429 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");
430 false
431 }
432 None => {
433 warn!(%origin, %user_id, "No vault URL found for user, not syncing that user to a web peer");
434 false
435 }
436 }
437 }
438
439 async fn sync_targets(&self) -> Vec<SyncTarget> {
441 let mut targets = self.0.active_peers.targets();
442 if let Some(leader) = self.0.driver.discover_leader().await
443 && !targets.iter().any(|target| target.endpoint == leader)
444 {
445 targets.push(SyncTarget::without_origin(leader));
448 }
449 targets
450 }
451
452 async fn send_message(&self, message: SharedUnlockSync, recipient: Endpoint) {
453 tracing::debug!(
454 user_id = %message.user_id,
455 lock_state = message.state.lock_state.describe(),
456 changed_at = message.state.changed_at,
457 ?recipient,
458 "Sending a shared unlock sync"
459 );
460
461 if let Err(error) = self
462 .0
463 .ipc_client
464 .send_typed(message, recipient.clone())
465 .await
466 {
467 match error {
468 RequestError::Unreachable => {
469 tracing::debug!(
473 ?recipient,
474 "Shared unlock peer unreachable; sync not delivered"
475 );
476 }
477 RequestError::Timeout(_) => {
478 tracing::debug!(?error, "Timeout sending shared unlock sync");
479 }
480 _ => {
481 tracing::debug!(?error, "Failed to send shared unlock IPC message");
482 }
483 }
484 }
485 }
486
487 #[cfg(any(test, feature = "test-support"))]
492 pub fn recorded_changed_at(&self, user_id: UserId) -> Option<u64> {
493 self.recorded_state(user_id).map(|state| state.changed_at)
494 }
495
496 fn recorded_state(&self, user_id: UserId) -> Option<TimestampedLockState> {
497 self.0
498 .states
499 .lock()
500 .unwrap_or_else(|poisoned| poisoned.into_inner())
501 .get(&user_id)
502 .cloned()
503 }
504
505 async fn restored_state(&self, user_id: UserId) -> Option<TimestampedLockState> {
512 if let Some(recorded) = self.recorded_state(user_id) {
513 return Some(recorded);
514 }
515
516 let changed_at = self.0.driver.get_last_manual_lock(user_id).await?;
517 let restored = TimestampedLockState {
518 lock_state: LockState::Locked,
519 changed_at,
520 };
521
522 let mut states = self
524 .0
525 .states
526 .lock()
527 .unwrap_or_else(|poisoned| poisoned.into_inner());
528 Some(states.entry(user_id).or_insert(restored).clone())
529 }
530
531 async fn advertised_state(&self, user_id: UserId) -> TimestampedLockState {
534 self.restored_state(user_id)
535 .await
536 .unwrap_or(TimestampedLockState {
537 lock_state: LockState::Locked,
538 changed_at: 0,
539 })
540 }
541
542 fn record_remote_state(&self, user_id: UserId, state: TimestampedLockState) {
544 let mut states = self
545 .0
546 .states
547 .lock()
548 .unwrap_or_else(|poisoned| poisoned.into_inner());
549
550 if state.supersedes(states.get(&user_id)) {
551 states.insert(user_id, state);
552 }
553 }
554
555 fn record_local_state(&self, user_id: UserId, lock_state: LockState) -> u64 {
557 let mut states = self
558 .0
559 .states
560 .lock()
561 .unwrap_or_else(|poisoned| poisoned.into_inner());
562
563 let changed_at = states
564 .get(&user_id)
565 .map_or(0, |recorded| recorded.changed_at.saturating_add(1))
566 .max(now_millis());
567
568 states.insert(
569 user_id,
570 TimestampedLockState {
571 lock_state,
572 changed_at,
573 },
574 );
575 changed_at
576 }
577}