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