Skip to main content

bitwarden_user_crypto_management/
key_connector_migration.rs

1//! Client operations for migrating an initialized account to Key Connector unlock.
2
3use bitwarden_api_api::models::KeyConnectorEnrollmentRequestModel;
4use bitwarden_api_key_connector::models::user_key_request_model::UserKeyKeyRequestModel;
5use bitwarden_core::key_management::SymmetricKeySlotId;
6use bitwarden_crypto::{EncString, KeyConnectorKey};
7use bitwarden_encoding::B64;
8use bitwarden_error::bitwarden_error;
9use thiserror::Error;
10use tracing::{error, info};
11#[cfg(feature = "wasm")]
12use wasm_bindgen::prelude::*;
13
14use crate::UserCryptoManagementClient;
15
16#[cfg_attr(feature = "wasm", wasm_bindgen)]
17impl UserCryptoManagementClient {
18    /// Migrates an initialized account to Key Connector unlock.
19    ///
20    /// Requires the client to be unlocked so the current user key is available in memory.
21    pub async fn migrate_to_key_connector(
22        &self,
23        key_connector_url: String,
24    ) -> Result<(), MigrateToKeyConnectorError> {
25        let internal = &self.client.internal;
26        let api_configuration = internal.get_api_configurations();
27        let key_connector_api_client = internal.get_key_connector_client(key_connector_url);
28
29        internal_migrate_to_key_connector(
30            self,
31            &api_configuration.api_client,
32            &key_connector_api_client,
33        )
34        .await
35    }
36}
37
38async fn internal_migrate_to_key_connector(
39    user_crypto_management_client: &UserCryptoManagementClient,
40    api_client: &bitwarden_api_api::apis::ApiClient,
41    key_connector_api_client: &bitwarden_api_key_connector::apis::ApiClient,
42) -> Result<(), MigrateToKeyConnectorError> {
43    // A key-connector-migration does the following:
44    // 1. Make a new key-connector-key. This is a randomly sampled symmetric key
45    // 2. Wrap the user's current user-key with the key-connector-key
46    // 3. Post the key-connector-key to the key-connector
47    // 4. Post the wrapped user-key to the server. This will replace the existing "master key
48    //    wrapped user-key".
49    //
50    // If the user-key is missing, we do not post the key-connector-key to the key-connector,
51    // and instead return early.
52
53    // Step 1: Make a new key-connector-key
54    let key_connector_key = KeyConnectorKey::make();
55
56    // Step 2: Wrap the user's current user key with the key connector key
57    let key_connector_key_wrapped_user_key = {
58        let key_store = user_crypto_management_client
59            .client
60            .internal
61            .get_key_store();
62        let ctx = key_store.context();
63        key_connector_key
64            .wrap_user_key(SymmetricKeySlotId::User, &ctx)
65            .map_err(|_| MigrateToKeyConnectorError::UserKeyNotAvailable)?
66    };
67
68    // Step 3: Post the key connector key to the key connector server
69    info!("Posting key connector key to key connector server");
70    post_key_connector_key_to_key_connector(key_connector_api_client, key_connector_key).await?;
71
72    // Step 4: Post the wrapped user key to the server and enroll the user into key connector
73    info!("Posting wrapped user key for key connector migration");
74    enroll_user_into_key_connector(api_client, key_connector_key_wrapped_user_key).await?;
75
76    info!("Successfully migrated account to key connector unlock");
77    Ok(())
78}
79
80async fn enroll_user_into_key_connector(
81    api_client: &bitwarden_api_api::apis::ApiClient,
82    key_connector_key_wrapped_user_key: EncString,
83) -> Result<(), MigrateToKeyConnectorError> {
84    let request = KeyConnectorEnrollmentRequestModel {
85        key_connector_key_wrapped_user_key: Some(key_connector_key_wrapped_user_key.to_string()),
86    };
87
88    api_client
89        .accounts_key_management_api()
90        .post_enroll_to_key_connector(Some(request))
91        .await
92        .map_err(|e| {
93            error!("Failed to post key connector migration request: {e:?}");
94            MigrateToKeyConnectorError::Api
95        })
96}
97
98async fn post_key_connector_key_to_key_connector(
99    key_connector_api_client: &bitwarden_api_key_connector::apis::ApiClient,
100    key_connector_key: KeyConnectorKey,
101) -> Result<(), MigrateToKeyConnectorError> {
102    let encoded_key_connector_key: B64 = key_connector_key.into();
103    let request = UserKeyKeyRequestModel {
104        key: encoded_key_connector_key.to_string(),
105    };
106
107    // Key-connector doesn't support PUT if the key does not exist, so
108    // in this case we GET, then POST/PUT depending on the response.
109    let result = if key_connector_api_client
110        .user_keys_api()
111        .get_user_key()
112        .await
113        .is_ok()
114    {
115        info!("User's key connector key exists, updating");
116        key_connector_api_client
117            .user_keys_api()
118            .put_user_key(request)
119            .await
120    } else {
121        info!("User's key connector key does not exist, creating");
122        key_connector_api_client
123            .user_keys_api()
124            .post_user_key(request)
125            .await
126    };
127
128    result.map_err(|e| {
129        error!("Failed to post key connector key to key connector server: {e:?}");
130        MigrateToKeyConnectorError::KeyConnectorApi
131    })
132}
133
134#[derive(Debug, Error)]
135#[bitwarden_error(flat)]
136pub enum MigrateToKeyConnectorError {
137    #[error("Current user key is not available")]
138    UserKeyNotAvailable,
139    #[error("Cryptographic error during key connector migration")]
140    Crypto,
141    #[error("Bitwarden API call failed during key connector migration")]
142    Api,
143    #[error("Key Connector API call failed during key connector migration")]
144    KeyConnectorApi,
145}
146
147#[cfg(test)]
148mod tests {
149    use bitwarden_api_api::apis::ApiClient;
150    use bitwarden_core::Client;
151    use bitwarden_crypto::EncString;
152
153    use super::*;
154
155    fn unlocked_client() -> UserCryptoManagementClient {
156        let client = Client::new(None);
157        {
158            let key_store = client.internal.get_key_store();
159            let mut ctx = key_store.context_mut();
160            let local_user_key =
161                ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
162            let _ = ctx.persist_symmetric_key(local_user_key, SymmetricKeySlotId::User);
163        }
164
165        UserCryptoManagementClient::new(client)
166    }
167
168    #[tokio::test]
169    async fn test_migrate_to_key_connector_success() {
170        let user_crypto_management_client = unlocked_client();
171
172        let api_client = ApiClient::new_mocked(|mock| {
173            mock.accounts_key_management_api
174                .expect_post_enroll_to_key_connector()
175                .once()
176                .returning(move |body| {
177                    let body = body.expect("body should be Some");
178                    let wrapped_key = body
179                        .key_connector_key_wrapped_user_key
180                        .expect("key_connector_key_wrapped_user_key should be Some");
181                    wrapped_key
182                        .parse::<EncString>()
183                        .expect("key_connector_key_wrapped_user_key should be a valid EncString");
184                    Ok(())
185                });
186        });
187
188        let key_connector_api_client =
189            bitwarden_api_key_connector::apis::ApiClient::new_mocked(|mock| {
190                mock.user_keys_api
191                    .expect_get_user_key()
192                    .once()
193                    .returning(move || {
194                        Err(bitwarden_api_key_connector::apis::Error::ResponseError(
195                            bitwarden_api_key_connector::apis::ResponseContent {
196                                status: reqwest::StatusCode::NOT_FOUND,
197                                content: "Not Found".to_string(),
198                            },
199                        ))
200                    });
201                mock.user_keys_api
202                    .expect_post_user_key()
203                    .once()
204                    .returning(move |_body| Ok(()));
205            });
206
207        let result = internal_migrate_to_key_connector(
208            &user_crypto_management_client,
209            &api_client,
210            &key_connector_api_client,
211        )
212        .await;
213
214        assert!(result.is_ok());
215
216        if let ApiClient::Mock(mut mock) = api_client {
217            mock.accounts_key_management_api.checkpoint();
218        }
219        if let bitwarden_api_key_connector::apis::ApiClient::Mock(mut mock) =
220            key_connector_api_client
221        {
222            mock.user_keys_api.checkpoint();
223        }
224    }
225
226    #[tokio::test]
227    async fn test_migrate_to_key_connector_key_connector_api_failure() {
228        let user_crypto_management_client = unlocked_client();
229
230        let api_client = ApiClient::new_mocked(|mock| {
231            mock.accounts_key_management_api
232                .expect_post_enroll_to_key_connector()
233                .never();
234        });
235
236        let key_connector_api_client =
237            bitwarden_api_key_connector::apis::ApiClient::new_mocked(|mock| {
238                mock.user_keys_api
239                    .expect_get_user_key()
240                    .once()
241                    .returning(move || {
242                        Err(bitwarden_api_key_connector::apis::Error::ResponseError(
243                            bitwarden_api_key_connector::apis::ResponseContent {
244                                status: reqwest::StatusCode::NOT_FOUND,
245                                content: "Not Found".to_string(),
246                            },
247                        ))
248                    });
249                mock.user_keys_api
250                    .expect_post_user_key()
251                    .once()
252                    .returning(move |_body| {
253                        Err(bitwarden_api_key_connector::apis::Error::Serde(
254                            serde_json::Error::io(std::io::Error::other("API error")),
255                        ))
256                    });
257            });
258
259        let result = internal_migrate_to_key_connector(
260            &user_crypto_management_client,
261            &api_client,
262            &key_connector_api_client,
263        )
264        .await;
265
266        assert!(matches!(
267            result,
268            Err(MigrateToKeyConnectorError::KeyConnectorApi)
269        ));
270
271        if let ApiClient::Mock(mut mock) = api_client {
272            mock.accounts_key_management_api.checkpoint();
273        }
274        if let bitwarden_api_key_connector::apis::ApiClient::Mock(mut mock) =
275            key_connector_api_client
276        {
277            mock.user_keys_api.checkpoint();
278        }
279    }
280
281    #[tokio::test]
282    async fn test_migrate_to_key_connector_api_failure() {
283        let user_crypto_management_client = unlocked_client();
284
285        let api_client = ApiClient::new_mocked(|mock| {
286            mock.accounts_key_management_api
287                .expect_post_enroll_to_key_connector()
288                .once()
289                .returning(move |body| {
290                    let body = body.expect("body should be Some");
291                    let wrapped_key = body
292                        .key_connector_key_wrapped_user_key
293                        .expect("key_connector_key_wrapped_user_key should be Some");
294                    wrapped_key
295                        .parse::<EncString>()
296                        .expect("key_connector_key_wrapped_user_key should be a valid EncString");
297                    Err(serde_json::Error::io(std::io::Error::other("API error")).into())
298                });
299        });
300
301        let key_connector_api_client =
302            bitwarden_api_key_connector::apis::ApiClient::new_mocked(|mock| {
303                mock.user_keys_api
304                    .expect_get_user_key()
305                    .once()
306                    .returning(move || {
307                        Err(bitwarden_api_key_connector::apis::Error::ResponseError(
308                            bitwarden_api_key_connector::apis::ResponseContent {
309                                status: reqwest::StatusCode::NOT_FOUND,
310                                content: "Not Found".to_string(),
311                            },
312                        ))
313                    });
314                mock.user_keys_api
315                    .expect_post_user_key()
316                    .once()
317                    .returning(move |_body| Ok(()));
318            });
319
320        let result = internal_migrate_to_key_connector(
321            &user_crypto_management_client,
322            &api_client,
323            &key_connector_api_client,
324        )
325        .await;
326
327        assert!(matches!(result, Err(MigrateToKeyConnectorError::Api)));
328
329        if let ApiClient::Mock(mut mock) = api_client {
330            mock.accounts_key_management_api.checkpoint();
331        }
332        if let bitwarden_api_key_connector::apis::ApiClient::Mock(mut mock) =
333            key_connector_api_client
334        {
335            mock.user_keys_api.checkpoint();
336        }
337    }
338
339    #[tokio::test]
340    async fn test_migrate_to_key_connector_user_key_not_available() {
341        let user_crypto_management_client = UserCryptoManagementClient::new(Client::new(None));
342
343        let api_client = ApiClient::new_mocked(|mock| {
344            mock.accounts_key_management_api
345                .expect_post_enroll_to_key_connector()
346                .never();
347        });
348
349        let key_connector_api_client =
350            bitwarden_api_key_connector::apis::ApiClient::new_mocked(|mock| {
351                mock.user_keys_api.expect_get_user_key().never();
352                mock.user_keys_api.expect_post_user_key().never();
353                mock.user_keys_api.expect_put_user_key().never();
354            });
355
356        let result = internal_migrate_to_key_connector(
357            &user_crypto_management_client,
358            &api_client,
359            &key_connector_api_client,
360        )
361        .await;
362
363        assert!(matches!(
364            result,
365            Err(MigrateToKeyConnectorError::UserKeyNotAvailable)
366        ));
367
368        if let ApiClient::Mock(mut mock) = api_client {
369            mock.accounts_key_management_api.checkpoint();
370        }
371        if let bitwarden_api_key_connector::apis::ApiClient::Mock(mut mock) =
372            key_connector_api_client
373        {
374            mock.user_keys_api.checkpoint();
375        }
376    }
377}