Skip to main content

bitwarden_user_crypto_management/
key_id_backfill.rs

1//! Record the user key's id with the server for accounts where it was never captured.
2//!
3//! Key ids were introduced after V2 user keys, so accounts created or upgraded before the server
4//! started tracking them hold a key id in their key material that the server does not know about.
5//!
6//! This module adds a backfill functionality.
7
8use bitwarden_api_api::models::SetUserKeyIdRequestModel;
9use bitwarden_core::key_management::SymmetricKeySlotId;
10use bitwarden_crypto::KeyId;
11use bitwarden_error::bitwarden_error;
12use thiserror::Error;
13use tracing::{error, info};
14#[cfg(feature = "wasm")]
15use wasm_bindgen::prelude::*;
16
17use crate::UserCryptoManagementClient;
18
19/// Errors returned by the user key id backfill.
20#[derive(Debug, Error)]
21#[bitwarden_error(flat)]
22pub enum KeyIdBackfillError {
23    /// The user key is not in the key store, so the client is locked or not initialized.
24    #[error("User key is not available in key store")]
25    UserKeyNotAvailable,
26    /// The current user key carries no key id
27    #[error("The current user key has no key id to backfill")]
28    NoKeyId,
29    /// The key id the server knows is read from client-managed state, which needs a bridge.
30    #[error("No state bridge registered, the user key id backfill is not supported")]
31    StateBridgeNotRegistered,
32    /// The API call recording the key id failed.
33    #[error("API call failed during user key id backfill")]
34    Api,
35}
36
37#[cfg_attr(feature = "wasm", wasm_bindgen)]
38#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
39impl UserCryptoManagementClient {
40    /// Returns whether the server is missing the id of the user's current user key.
41    pub async fn user_key_id_needs_backfill(&self) -> Result<bool, KeyIdBackfillError> {
42        let state_bridge = self.client.km_state_bridge();
43        if !state_bridge.is_bridge_registered() {
44            return Err(KeyIdBackfillError::StateBridgeNotRegistered);
45        }
46
47        // An id the server already knows needs no backfilling. A mismatch between it and the local
48        // one would mean a key rotation the server has not seen, which is not this concern.
49        if let Some(recorded) = state_bridge.get_user_key_id().await {
50            info!(?recorded, "Server already knows the user key id");
51            return Ok(false);
52        }
53
54        Ok(self.current_user_key_id()?.is_some())
55    }
56
57    /// Records the id of the user's current user key with the server, and stores it as the id the
58    /// server knows.
59    ///
60    /// Safe to call when no backfill is needed, but [`Self::user_key_id_needs_backfill`] avoids the
61    /// round trip.
62    ///
63    /// Requires the client to be unlocked so the current user key is available in memory.
64    pub async fn user_key_id_backfill(&self) -> Result<(), KeyIdBackfillError> {
65        let state_bridge = self.client.km_state_bridge();
66        if !state_bridge.is_bridge_registered() {
67            return Err(KeyIdBackfillError::StateBridgeNotRegistered);
68        }
69
70        let user_key_id = self
71            .current_user_key_id()?
72            .ok_or(KeyIdBackfillError::NoKeyId)?;
73
74        info!("Recording the user key id with the server");
75        self.client
76            .internal
77            .get_api_configurations()
78            .api_client
79            .accounts_key_management_api()
80            .post_user_key_id(Some(SetUserKeyIdRequestModel {
81                user_key_id: user_key_id.to_string(),
82            }))
83            .await
84            .map_err(|e| {
85                error!("Failed to post the user key id: {e:?}");
86                KeyIdBackfillError::Api
87            })?;
88
89        // Written only after the server accepted it, so state never claims an id the server lacks.
90        state_bridge.set_user_key_id(&user_key_id).await;
91
92        Ok(())
93    }
94}
95
96impl UserCryptoManagementClient {
97    /// Reads the key id of the user key currently in the key store.
98    fn current_user_key_id(&self) -> Result<Option<KeyId>, KeyIdBackfillError> {
99        let key_store = self.client.internal.get_key_store();
100        let ctx = key_store.context();
101
102        if !ctx.has_symmetric_key(SymmetricKeySlotId::User) {
103            return Err(KeyIdBackfillError::UserKeyNotAvailable);
104        }
105
106        Ok(ctx.get_symmetric_key_id(SymmetricKeySlotId::User))
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use std::sync::Arc;
113
114    use bitwarden_api_api::apis::ApiClient;
115    use bitwarden_core::{
116        Client, client::internal::ApiConfigurations,
117        key_management::state_bridge::test_support::InMemoryStateBridge,
118    };
119    use bitwarden_crypto::SymmetricKeyAlgorithm;
120
121    use super::*;
122    use crate::UserCryptoManagementClientExt;
123
124    /// Builds a client holding a user key of the given algorithm, with a state bridge registered
125    /// and its API calls served by `api_client`.
126    fn client_with_user_key(algorithm: SymmetricKeyAlgorithm, api_client: ApiClient) -> Client {
127        let client = Client::builder()
128            .with_api_configurations(Arc::new(ApiConfigurations::from_api_client(api_client)))
129            .build();
130        client
131            .km_state_bridge()
132            .register_bridge(Box::new(InMemoryStateBridge::default()));
133        {
134            let key_store = client.internal.get_key_store();
135            let mut ctx = key_store.context_mut();
136            let local = ctx.make_symmetric_key(algorithm);
137            ctx.persist_symmetric_key(local, SymmetricKeySlotId::User)
138                .unwrap();
139        }
140        client
141    }
142
143    /// An API client that fails the test if any endpoint is called.
144    fn no_api_calls() -> ApiClient {
145        ApiClient::new_mocked(|mock| {
146            mock.accounts_key_management_api
147                .expect_post_user_key_id()
148                .never();
149        })
150    }
151
152    fn user_key_id(client: &Client) -> KeyId {
153        client
154            .internal
155            .get_key_store()
156            .context()
157            .get_symmetric_key_id(SymmetricKeySlotId::User)
158            .expect("a user key has a key id")
159    }
160
161    #[tokio::test]
162    async fn test_needs_backfill_when_server_has_no_key_id() {
163        let client = client_with_user_key(SymmetricKeyAlgorithm::XAes256Gcm, no_api_calls());
164
165        assert!(
166            client
167                .user_crypto_management()
168                .user_key_id_needs_backfill()
169                .await
170                .unwrap()
171        );
172    }
173
174    #[tokio::test]
175    async fn test_no_backfill_when_server_already_knows_the_key_id() {
176        let client = client_with_user_key(SymmetricKeyAlgorithm::XAes256Gcm, no_api_calls());
177        let key_id = user_key_id(&client);
178        client.km_state_bridge().set_user_key_id(&key_id).await;
179
180        assert!(
181            !client
182                .user_crypto_management()
183                .user_key_id_needs_backfill()
184                .await
185                .unwrap()
186        );
187    }
188
189    #[tokio::test]
190    async fn test_needs_backfill_for_a_v1_user_key() {
191        // A V1 key carries a key id as well, so it is backfilled like a V2 one.
192        let client = client_with_user_key(SymmetricKeyAlgorithm::Aes256CbcHmac, no_api_calls());
193
194        assert!(
195            client
196                .user_crypto_management()
197                .user_key_id_needs_backfill()
198                .await
199                .unwrap()
200        );
201    }
202
203    #[tokio::test]
204    async fn test_needs_backfill_without_a_user_key_errors() {
205        let client = Client::new(None);
206        client
207            .km_state_bridge()
208            .register_bridge(Box::new(InMemoryStateBridge::default()));
209
210        assert!(matches!(
211            client
212                .user_crypto_management()
213                .user_key_id_needs_backfill()
214                .await,
215            Err(KeyIdBackfillError::UserKeyNotAvailable)
216        ));
217    }
218
219    #[tokio::test]
220    async fn test_needs_backfill_without_a_state_bridge_errors() {
221        let client = Client::new(None);
222
223        assert!(matches!(
224            client
225                .user_crypto_management()
226                .user_key_id_needs_backfill()
227                .await,
228            Err(KeyIdBackfillError::StateBridgeNotRegistered)
229        ));
230    }
231
232    /// Backfills a client holding a user key of the given algorithm, and asserts the key store's
233    /// key id was both posted and stored.
234    async fn assert_backfill_posts_and_stores(algorithm: SymmetricKeyAlgorithm) {
235        // The posted id is checked against the key store's below, so the mock only records it.
236        let posted = Arc::new(std::sync::Mutex::new(None));
237        let recorder = posted.clone();
238        let api_client = ApiClient::new_mocked(|mock| {
239            mock.accounts_key_management_api
240                .expect_post_user_key_id()
241                .once()
242                .returning(move |body| {
243                    let body = body.expect("body should be Some");
244                    *recorder.lock().unwrap() = Some(body.user_key_id);
245                    Ok(())
246                });
247        });
248        let client = client_with_user_key(algorithm, api_client);
249        let expected = user_key_id(&client).to_string();
250
251        client
252            .user_crypto_management()
253            .user_key_id_backfill()
254            .await
255            .unwrap();
256
257        assert_eq!(posted.lock().unwrap().as_deref(), Some(expected.as_str()));
258        let stored = client.km_state_bridge().get_user_key_id().await.unwrap();
259        assert_eq!(stored.to_string(), expected);
260    }
261
262    #[tokio::test]
263    async fn test_backfill_posts_the_key_id_and_stores_it() {
264        assert_backfill_posts_and_stores(SymmetricKeyAlgorithm::XAes256Gcm).await;
265    }
266
267    #[tokio::test]
268    async fn test_backfill_posts_the_key_id_and_stores_it_for_a_v1_user_key() {
269        assert_backfill_posts_and_stores(SymmetricKeyAlgorithm::Aes256CbcHmac).await;
270    }
271
272    #[tokio::test]
273    async fn test_backfill_leaves_state_untouched_when_the_api_fails() {
274        let api_client = ApiClient::new_mocked(|mock| {
275            mock.accounts_key_management_api
276                .expect_post_user_key_id()
277                .once()
278                .returning(|_| {
279                    Err(bitwarden_api_api::apis::Error::Response(
280                        bitwarden_api_api::apis::ResponseContent {
281                            status: reqwest::StatusCode::BAD_REQUEST,
282                            message: "Bad Request".to_string(),
283                        },
284                    ))
285                });
286        });
287        let client = client_with_user_key(SymmetricKeyAlgorithm::XAes256Gcm, api_client);
288
289        assert!(matches!(
290            client.user_crypto_management().user_key_id_backfill().await,
291            Err(KeyIdBackfillError::Api)
292        ));
293        assert!(client.km_state_bridge().get_user_key_id().await.is_none());
294    }
295}