Skip to main content

bitwarden_shared_unlock/
follower.rs

1use std::{ops::Add, sync::Arc};
2
3use bitwarden_error::bitwarden_error;
4use bitwarden_ipc::{
5    Endpoint, IpcClient, IpcClientExt, RequestError, SubscribeError, TypedIncomingMessage,
6};
7use bitwarden_threading::{cancellation_token, time::sleep};
8use thiserror::Error;
9
10use crate::{DeviceEvent, FollowerMessage, LeaderMessage, LockState, drivers::SharedUnlockDriver};
11
12/// Error type for failure to start the shared unlock follower.
13#[bitwarden_error(basic)]
14#[derive(Debug, Error)]
15#[error("Could not start shared unlock follower: {0}")]
16pub struct FollowerStartError(#[from] SubscribeError);
17
18/// Tracks local state and follows authoritative lock updates from a leader.
19pub struct Follower<L: SharedUnlockDriver>(Arc<InnerFollower<L>>);
20
21impl<L: SharedUnlockDriver> Clone for Follower<L> {
22    fn clone(&self) -> Self {
23        Self(self.0.clone())
24    }
25}
26
27/// Inner implementation of the shared unlock follower, containing the actual state and logic. The
28/// outer `Follower` struct is a thin wrapper around an `Arc` to allow for shared ownership across
29/// async tasks.
30struct InnerFollower<D: SharedUnlockDriver> {
31    driver: D,
32    ipc_client: Arc<dyn IpcClient>,
33}
34
35impl<L: SharedUnlockDriver + Send + Sync + 'static> Follower<L> {
36    /// Creates a follower instance and starts sessions for all currently known users.
37    ///
38    /// During startup, a `StartSession` message is sent per user so the leader can reconcile
39    /// initial lock state.
40    pub fn create(driver: L, ipc_client: Arc<dyn IpcClient>) -> Self {
41        Self(Arc::new(InnerFollower { driver, ipc_client }))
42    }
43
44    pub(crate) async fn start_sessions(&self) {
45        let users: Vec<bitwarden_core::UserId> = self.0.driver.list_users().await;
46        let leader = self
47            .0
48            .driver
49            .discover_leader()
50            .await
51            .expect("leader discovery should return a leader");
52
53        if !users.is_empty() {
54            tracing::info!("Starting shared unlock sessions for users: {:?}", users);
55        }
56
57        for user_id in users {
58            let lock_state = self.0.driver.get_user_lock_state(user_id).await;
59            let message = FollowerMessage::StartSession {
60                user_id,
61                lock_state,
62            };
63            self.send_message(message, leader.clone()).await;
64        }
65    }
66
67    /// Starts background tasks for IPC message handling and heartbeat timers.
68    pub async fn start(
69        &self,
70        cancellation_token: Option<cancellation_token::CancellationToken>,
71    ) -> Result<(), FollowerStartError> {
72        let cancellation_token = cancellation_token.unwrap_or_default();
73        let mut subscription = self.0.ipc_client.subscribe_typed::<LeaderMessage>().await?;
74        let follower = self.clone();
75
76        let cancellation_token_clone = cancellation_token.clone();
77        let future = async move {
78            loop {
79                let result = subscription
80                    .receive(Some(cancellation_token_clone.clone()))
81                    .await;
82                match result {
83                    Ok(message) => {
84                        if let Err(error) = follower.receive_message(message).await {
85                            tracing::error!(
86                                ?error,
87                                "Failed to handle shared unlock follower message"
88                            );
89                        }
90                    }
91                    Err(bitwarden_ipc::TypedReceiveError::Cancelled) => {
92                        tracing::info!("Shared unlock follower stopped by cancellation");
93                        break;
94                    }
95                    // This is required because otherwise the browser may freeze in this loop
96                    Err(bitwarden_ipc::TypedReceiveError::Channel(
97                        tokio::sync::broadcast::error::RecvError::Closed,
98                    )) => {
99                        tracing::info!("Transport channel closed. Waiting for it to open");
100                        sleep(std::time::Duration::from_secs(1)).await;
101                        break;
102                    }
103                    Err(error) => {
104                        tracing::error!(?error, "Failed to receive shared unlock IPC message");
105                    }
106                }
107            }
108        };
109
110        #[cfg(not(target_arch = "wasm32"))]
111        tokio::spawn(future);
112
113        #[cfg(target_arch = "wasm32")]
114        wasm_bindgen_futures::spawn_local(future);
115
116        let cancellation_token = cancellation_token.clone();
117        let follower = self.clone();
118        let timer_future = async move {
119            loop {
120                tokio::select! {
121                    _ = cancellation_token.cancelled() => {
122                        tracing::debug!("Shared unlock follower timer cancelled");
123                        break;
124                    }
125                    _ = bitwarden_threading::time::sleep(crate::HEARTBEAT_INTERVAL) => {
126                        if let Some(leader) = follower.0.driver.discover_leader().await {
127                            // For all users that are logged in, send a heartbeat message to the leader.
128                            for user_id in follower.0.driver.list_users().await {
129                                let message = FollowerMessage::HeartBeat { user_id };
130                                follower.send_message(message, leader.clone()).await;
131                            }
132                        }
133                    }
134                }
135            }
136        };
137
138        #[cfg(not(target_arch = "wasm32"))]
139        tokio::spawn(timer_future);
140
141        #[cfg(target_arch = "wasm32")]
142        wasm_bindgen_futures::spawn_local(timer_future);
143
144        self.start_sessions().await;
145        Ok(())
146    }
147
148    /// Handles an authoritative message from the leader.
149    ///
150    /// Lock state updates overwrite local state to keep follower and leader in sync. Heartbeat
151    /// responses are forwarded to the heartbeat response handler.
152    pub async fn receive_message(
153        &self,
154        incoming_message: TypedIncomingMessage<LeaderMessage>,
155    ) -> Result<(), ()> {
156        let message = incoming_message.payload;
157        match message {
158            LeaderMessage::LockStateUpdate {
159                user_id,
160                lock_state,
161            } => {
162                // The leader is the authoritative state source for the follow, and it should
163                // always overwrite the local state of the follower.
164                let current_state = self.0.driver.get_user_lock_state(user_id).await;
165
166                match (current_state, lock_state) {
167                    (LockState::Unlocked { .. }, LockState::Locked) => {
168                        // If the user is currently unlocked and it receives an authoritative lock
169                        // state update from the leader that is Locked, then
170                        // it should follow, and lock the local state.
171                        self.0.driver.lock_user(user_id).await?;
172                    }
173                    (LockState::Locked, LockState::Unlocked { user_key }) => {
174                        // If the user is currently locked and it receives an authoritative lock
175                        // state update from the leader that is Unlocked,
176                        // then it should follow, and unlock the local state.
177                        self.0.driver.unlock_user(user_id, user_key).await?;
178                    }
179                    (LockState::Locked, LockState::Locked)
180                    | (LockState::Unlocked { .. }, LockState::Unlocked { .. }) => {
181                        // If both the current state and the received lock state are the same, then
182                        // do nothing, as they are already in sync.
183                    }
184                }
185            }
186            LeaderMessage::HeartBeat { user_id } => {
187                self.0
188                    .driver
189                    .suppress_vault_timeout(
190                        user_id,
191                        crate::HEARTBEAT_INTERVAL.add(crate::VAULT_TIMEOUT_GRACE_PERIOD),
192                    )
193                    .await;
194            }
195            LeaderMessage::RequestSessionStart { user_id } => {
196                let lock_state = self.0.driver.get_user_lock_state(user_id).await;
197                let message = FollowerMessage::StartSession {
198                    user_id,
199                    lock_state,
200                };
201                self.send_message(message, self.0.driver.discover_leader().await.ok_or(())?)
202                    .await;
203            }
204        }
205
206        Ok(())
207    }
208
209    /// Handles local device events and forwards them to the discovered leader.
210    ///
211    /// Manual lock/unlock events are sent as lock state updates. Timer events send per-user
212    /// heartbeats to keep the shared session active.
213    pub async fn handle_device_event(&self, event: DeviceEvent) -> Result<(), ()> {
214        let leader = self.0.driver.discover_leader().await.ok_or(())?;
215
216        match event {
217            DeviceEvent::ManualLock { user_id } => {
218                let message = FollowerMessage::LockStateUpdate {
219                    user_id,
220                    lock_state: LockState::Locked,
221                };
222                self.send_message(message, leader).await;
223            }
224            DeviceEvent::ManualUnlock {
225                user_id,
226                ref user_key,
227            } => {
228                let message = FollowerMessage::LockStateUpdate {
229                    user_id,
230                    lock_state: LockState::Unlocked {
231                        user_key: user_key.to_owned(),
232                    },
233                };
234                self.send_message(message, leader).await;
235            }
236        }
237
238        Ok(())
239    }
240
241    async fn send_message(&self, message: FollowerMessage, recipient: Endpoint) {
242        if let Err(error) = self.0.ipc_client.send_typed(message, recipient).await {
243            match error {
244                RequestError::Unreachable => {
245                    // The leader is not connected; the message simply could not be delivered.
246                }
247                RequestError::Timeout(_) => {
248                    tracing::warn!(
249                        ?error,
250                        "Timeout sending shared unlock follower message to leader"
251                    );
252                }
253                _ => {
254                    tracing::error!(?error, "Failed to send shared unlock IPC message");
255                }
256            }
257        }
258    }
259}