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::SymmetricKeyId;
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(SymmetricKeyId::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::ApiError
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::KeyConnectorApiError
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    CryptoError,
141    #[error("Bitwarden API call failed during key connector migration")]
142    ApiError,
143    #[error("Key Connector API call failed during key connector migration")]
144    KeyConnectorApiError,
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, SymmetricKeyId::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::KeyConnectorApiError)
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(bitwarden_api_api::apis::Error::Serde(
298                        serde_json::Error::io(std::io::Error::other("API error")),
299                    ))
300                });
301        });
302
303        let key_connector_api_client =
304            bitwarden_api_key_connector::apis::ApiClient::new_mocked(|mock| {
305                mock.user_keys_api
306                    .expect_get_user_key()
307                    .once()
308                    .returning(move || {
309                        Err(bitwarden_api_key_connector::apis::Error::ResponseError(
310                            bitwarden_api_key_connector::apis::ResponseContent {
311                                status: reqwest::StatusCode::NOT_FOUND,
312                                content: "Not Found".to_string(),
313                            },
314                        ))
315                    });
316                mock.user_keys_api
317                    .expect_post_user_key()
318                    .once()
319                    .returning(move |_body| Ok(()));
320            });
321
322        let result = internal_migrate_to_key_connector(
323            &user_crypto_management_client,
324            &api_client,
325            &key_connector_api_client,
326        )
327        .await;
328
329        assert!(matches!(result, Err(MigrateToKeyConnectorError::ApiError)));
330
331        if let ApiClient::Mock(mut mock) = api_client {
332            mock.accounts_key_management_api.checkpoint();
333        }
334        if let bitwarden_api_key_connector::apis::ApiClient::Mock(mut mock) =
335            key_connector_api_client
336        {
337            mock.user_keys_api.checkpoint();
338        }
339    }
340
341    #[tokio::test]
342    async fn test_migrate_to_key_connector_user_key_not_available() {
343        let user_crypto_management_client = UserCryptoManagementClient::new(Client::new(None));
344
345        let api_client = ApiClient::new_mocked(|mock| {
346            mock.accounts_key_management_api
347                .expect_post_enroll_to_key_connector()
348                .never();
349        });
350
351        let key_connector_api_client =
352            bitwarden_api_key_connector::apis::ApiClient::new_mocked(|mock| {
353                mock.user_keys_api.expect_get_user_key().never();
354                mock.user_keys_api.expect_post_user_key().never();
355                mock.user_keys_api.expect_put_user_key().never();
356            });
357
358        let result = internal_migrate_to_key_connector(
359            &user_crypto_management_client,
360            &api_client,
361            &key_connector_api_client,
362        )
363        .await;
364
365        assert!(matches!(
366            result,
367            Err(MigrateToKeyConnectorError::UserKeyNotAvailable)
368        ));
369
370        if let ApiClient::Mock(mut mock) = api_client {
371            mock.accounts_key_management_api.checkpoint();
372        }
373        if let bitwarden_api_key_connector::apis::ApiClient::Mock(mut mock) =
374            key_connector_api_client
375        {
376            mock.user_keys_api.checkpoint();
377        }
378    }
379}