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