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 let differs = remote.lock_state != self.advertised_state(user_id).await.lock_state;
311 if differs {
312 tracing::debug!(
313 %user_id,
314 lock_state = remote.lock_state.describe(),
315 changed_at = remote.changed_at,
316 "Applying a peer's lock state through the driver"
317 );
318
319 match &remote.lock_state {
320 LockState::Locked => self
321 .0
322 .driver
323 .lock_user(user_id)
324 .await
325 .inspect_err(|_| warn!(%user_id, "Failed to lock user"))?,
326 LockState::Unlocked { user_key } => self
327 .0
328 .driver
329 .unlock_user(user_id, user_key.to_owned())
330 .await
331 .inspect_err(|_| warn!(%user_id, "Failed to unlock user"))?,
332 }
333 }
334
335 self.record_remote_state(user_id, remote);
336 Ok(())
337 }
338
339 async fn sync_all_users(&self) {
341 for user_id in self.0.driver.list_users().await {
342 self.sync_user(user_id).await;
343 }
344 }
345
346 async fn sync_user(&self, user_id: UserId) {
347 for target in self.sync_targets().await {
348 self.sync_user_to(user_id, &target).await;
349 }
350 }
351
352 async fn sync_all_users_to(&self, target: &SyncTarget) {
354 for user_id in self.0.driver.list_users().await {
355 self.sync_user_to(user_id, target).await;
356 }
357 }
358
359 async fn sync_user_to(&self, user_id: UserId, target: &SyncTarget) {
360 if !self.may_sync_user_to(user_id, target).await {
361 return;
362 }
363
364 let message = SharedUnlockSync {
365 user_id,
366 state: self.advertised_state(user_id).await,
367 };
368 self.send_message(message, target.endpoint.clone()).await;
369 }
370
371 async fn may_sync_user_to(&self, user_id: UserId, target: &SyncTarget) -> bool {
381 if !self.is_destination(user_id, &target.endpoint) {
382 return false;
383 }
384
385 if !matches!(target.endpoint, Endpoint::Web { .. }) {
386 return true;
387 }
388
389 let Some(origin) = target.origin.as_deref() else {
390 warn!(
391 ?target,
392 "Web peer has no validated origin, not syncing to it"
393 );
394 return false;
395 };
396
397 match self.0.driver.get_vault_url(user_id).await {
398 Some(user_vault_url) if user_vault_url == origin => true,
399 Some(user_vault_url) => {
400 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");
401 false
402 }
403 None => {
404 warn!(%origin, %user_id, "No vault URL found for user, not syncing that user to a web peer");
405 false
406 }
407 }
408 }
409
410 async fn sync_targets(&self) -> Vec<SyncTarget> {
412 let mut targets = self.0.active_peers.targets();
413 if let Some(leader) = self.0.driver.discover_leader().await
414 && !targets.iter().any(|target| target.endpoint == leader)
415 {
416 targets.push(SyncTarget::without_origin(leader));
419 }
420 targets
421 }
422
423 async fn send_message(&self, message: SharedUnlockSync, recipient: Endpoint) {
424 tracing::debug!(
425 user_id = %message.user_id,
426 lock_state = message.state.lock_state.describe(),
427 changed_at = message.state.changed_at,
428 ?recipient,
429 "Sending a shared unlock sync"
430 );
431
432 if let Err(error) = self
433 .0
434 .ipc_client
435 .send_typed(message, recipient.clone())
436 .await
437 {
438 match error {
439 RequestError::Unreachable => {
440 tracing::debug!(
444 ?recipient,
445 "Shared unlock peer unreachable; sync not delivered"
446 );
447 }
448 RequestError::Timeout(_) => {
449 tracing::debug!(?error, "Timeout sending shared unlock sync");
450 }
451 _ => {
452 tracing::debug!(?error, "Failed to send shared unlock IPC message");
453 }
454 }
455 }
456 }
457
458 #[cfg(any(test, feature = "test-support"))]
463 pub fn recorded_changed_at(&self, user_id: UserId) -> Option<u64> {
464 self.recorded_state(user_id).map(|state| state.changed_at)
465 }
466
467 fn recorded_state(&self, user_id: UserId) -> Option<TimestampedLockState> {
468 self.0
469 .states
470 .lock()
471 .unwrap_or_else(|poisoned| poisoned.into_inner())
472 .get(&user_id)
473 .cloned()
474 }
475
476 async fn restored_state(&self, user_id: UserId) -> Option<TimestampedLockState> {
483 if let Some(recorded) = self.recorded_state(user_id) {
484 return Some(recorded);
485 }
486
487 let changed_at = self.0.driver.get_last_manual_lock(user_id).await?;
488 let restored = TimestampedLockState {
489 lock_state: LockState::Locked,
490 changed_at,
491 };
492
493 let mut states = self
495 .0
496 .states
497 .lock()
498 .unwrap_or_else(|poisoned| poisoned.into_inner());
499 Some(states.entry(user_id).or_insert(restored).clone())
500 }
501
502 async fn advertised_state(&self, user_id: UserId) -> TimestampedLockState {
505 self.restored_state(user_id)
506 .await
507 .unwrap_or(TimestampedLockState {
508 lock_state: LockState::Locked,
509 changed_at: 0,
510 })
511 }
512
513 fn record_remote_state(&self, user_id: UserId, state: TimestampedLockState) {
515 let mut states = self
516 .0
517 .states
518 .lock()
519 .unwrap_or_else(|poisoned| poisoned.into_inner());
520
521 if state.supersedes(states.get(&user_id)) {
522 states.insert(user_id, state);
523 }
524 }
525
526 fn record_local_state(&self, user_id: UserId, lock_state: LockState) -> u64 {
528 let mut states = self
529 .0
530 .states
531 .lock()
532 .unwrap_or_else(|poisoned| poisoned.into_inner());
533
534 let changed_at = states
535 .get(&user_id)
536 .map_or(0, |recorded| recorded.changed_at.saturating_add(1))
537 .max(now_millis());
538
539 states.insert(
540 user_id,
541 TimestampedLockState {
542 lock_state,
543 changed_at,
544 },
545 );
546 changed_at
547 }
548}