bitwarden_user_crypto_management/
key_connector_migration.rs1use 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 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 let key_connector_key = KeyConnectorKey::make();
55
56 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 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 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 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}