1use bitwarden_api_api::models::RotateUserKeysRequestModel;
3use bitwarden_core::key_management::KeySlotIds;
4use bitwarden_crypto::{KeyConnectorKey, KeyStore, PublicKey};
5use serde::{Deserialize, Serialize};
6use tracing::{info, instrument};
7#[cfg(feature = "wasm")]
8use tsify::Tsify;
9#[cfg(feature = "wasm")]
10use wasm_bindgen::prelude::*;
11
12use crate::{
13 UserCryptoManagementClient,
14 key_rotation::{
15 RotateUserKeysError,
16 crypto::rotate_account_cryptographic_state_to_wrapped_model,
17 data::{check_for_old_attachments, reencrypt_data},
18 rotation_context::make_rotation_context,
19 sync::sync_current_account_data,
20 unlock::{ReencryptCommonUnlockDataInput, reencrypt_common_unlock_data},
21 unlock_method::{PrimaryUnlockMethod, reencrypt_unlock_method_data},
22 },
23};
24
25#[derive(Serialize, Deserialize, Clone)]
26#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
27#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
28pub enum KeyRotationMethod {
29 Password { password: String },
31 KeyConnector { key_connector_url: String },
33 Tde,
37}
38
39#[derive(Serialize, Deserialize, Clone)]
40#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
41#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
42pub enum UpgradeTokenAction {
43 Skip,
46 CreateIfNeeded,
49}
50
51#[derive(Serialize, Deserialize, Clone)]
52#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
53#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
54pub struct RotateUserKeysRequest {
55 pub key_rotation_method: KeyRotationMethod,
56 pub trusted_emergency_access_public_keys: Vec<PublicKey>,
57 pub trusted_organization_public_keys: Vec<PublicKey>,
58 #[serde(default, skip_serializing_if = "Option::is_none")]
59 #[cfg_attr(feature = "wasm", tsify(optional))]
60 pub upgrade_token_action: Option<UpgradeTokenAction>,
61}
62
63#[cfg_attr(feature = "wasm", wasm_bindgen)]
64impl UserCryptoManagementClient {
65 pub async fn rotate_user_keys(
67 &self,
68 request: RotateUserKeysRequest,
69 ) -> Result<(), RotateUserKeysError> {
70 let api_client = &self.client.internal.get_api_configurations().api_client;
71 let key_store = self.client.internal.get_key_store();
72
73 let key_connector_api_client =
74 if let KeyRotationMethod::KeyConnector { key_connector_url } =
75 &request.key_rotation_method
76 {
77 Some(
78 self.client
79 .internal
80 .get_key_connector_client(key_connector_url.clone()),
81 )
82 } else {
83 None
84 };
85
86 internal_rotate_user_keys(
87 key_store,
88 api_client,
89 key_connector_api_client.as_ref(),
90 request,
91 )
92 .await
93 }
94}
95
96#[instrument(name = "rotate_user_keys", level = "info", skip_all, err)]
97async fn internal_rotate_user_keys(
98 key_store: &KeyStore<KeySlotIds>,
99 api_client: &bitwarden_api_api::apis::ApiClient,
100 key_connector_api_client: Option<&bitwarden_api_key_connector::apis::ApiClient>,
101 request: RotateUserKeysRequest,
102) -> Result<(), RotateUserKeysError> {
103 if matches!(request.key_rotation_method, KeyRotationMethod::Tde) {
104 return Err(RotateUserKeysError::UnimplementedKeyRotationMethod);
105 }
106
107 let sync = sync_current_account_data(api_client)
108 .await
109 .map_err(|_| RotateUserKeysError::Api)?;
110
111 check_for_old_attachments(&sync.ciphers)?;
113
114 let key_connector_key = if matches!(
117 request.key_rotation_method,
118 KeyRotationMethod::KeyConnector { .. }
119 ) {
120 let key_connector_client =
121 key_connector_api_client.ok_or(RotateUserKeysError::KeyConnectorApi)?;
122 info!("Fetching Key Connector key for key rotation");
123 let response = key_connector_client
124 .user_keys_api()
125 .get_user_key()
126 .await
127 .map_err(|_| RotateUserKeysError::KeyConnectorApi)?;
128 let key_connector_key =
129 KeyConnectorKey::try_from(response).map_err(|_| RotateUserKeysError::Crypto)?;
130 Some(key_connector_key)
131 } else {
132 None
133 };
134
135 let post_request = {
137 let mut ctx = key_store.context_mut();
138
139 let rotation_context = make_rotation_context(
140 &sync,
141 request.trusted_organization_public_keys.as_slice(),
142 request.trusted_emergency_access_public_keys.as_slice(),
143 &mut ctx,
144 )?;
145
146 info!("Rotating account cryptographic state for user key rotation");
147 let wrapped_account_cryptographic_state_request_model =
148 rotate_account_cryptographic_state_to_wrapped_model(
149 &sync.wrapped_account_cryptographic_state,
150 &rotation_context.current_user_key_id,
151 &rotation_context.new_user_key_id,
152 &mut ctx,
153 )
154 .map_err(|_| RotateUserKeysError::Crypto)?;
155
156 info!("Re-encrypting account data for user key rotation");
157 let account_data_model = reencrypt_data(
158 sync.folders.as_slice(),
159 sync.ciphers.as_slice(),
160 sync.sends.as_slice(),
161 rotation_context.current_user_key_id,
162 rotation_context.new_user_key_id,
163 &mut ctx,
164 )
165 .map_err(|_| RotateUserKeysError::Crypto)?;
166
167 info!("Re-encrypting account primary unlock method for user key rotation");
168 let unlock_method_input = PrimaryUnlockMethod::from_key_rotation_method(
169 request.key_rotation_method,
170 &sync,
171 key_connector_key,
172 )?;
173 let unlock_method_data = reencrypt_unlock_method_data(
174 unlock_method_input,
175 rotation_context.new_user_key_id,
176 &mut ctx,
177 )
178 .map_err(|_| RotateUserKeysError::Crypto)?;
179
180 info!("Re-encrypting account common unlock data for user key rotation");
181 let common_unlock_data = reencrypt_common_unlock_data(
182 ReencryptCommonUnlockDataInput {
183 trusted_organization_keys: rotation_context.v1_organization_memberships,
184 trusted_emergency_access_keys: rotation_context.v1_emergency_access_memberships,
185 webauthn_credentials: sync.passkeys,
186 trusted_devices: sync.trusted_devices,
187 },
188 rotation_context.current_user_key_id,
189 rotation_context.new_user_key_id,
190 request
191 .upgrade_token_action
192 .unwrap_or(UpgradeTokenAction::Skip),
193 &mut ctx,
194 )
195 .map_err(|_| RotateUserKeysError::Crypto)?;
196
197 RotateUserKeysRequestModel {
198 wrapped_account_cryptographic_state: Box::new(
199 wrapped_account_cryptographic_state_request_model,
200 ),
201 account_data: Box::new(account_data_model),
202 unlock_data: Box::new(common_unlock_data),
203 unlock_method_data: Box::new(unlock_method_data),
204 }
205 };
206
207 info!("Posting rotated user account keys and data to server");
208 api_client
209 .accounts_key_management_api()
210 .rotate_user_keys(Some(post_request))
211 .await
212 .map_err(|_| RotateUserKeysError::Api)?;
213 info!("Successfully rotated user account keys and data");
214 Ok(())
215}
216
217#[cfg(test)]
218mod tests {
219 use bitwarden_api_api::{
220 apis::ApiClient,
221 models::{
222 DeviceAuthRequestResponseModelListResponseModel,
223 EmergencyAccessGranteeDetailsResponseModelListResponseModel, KdfType,
224 MasterPasswordUnlockKdfResponseModel, MasterPasswordUnlockResponseModel,
225 PrivateKeysResponseModel, ProfileOrganizationResponseModelListResponseModel,
226 ProfileResponseModel, PublicKeyEncryptionKeyPairResponseModel, SyncResponseModel,
227 UserDecryptionResponseModel, WebAuthnCredentialResponseModelListResponseModel,
228 },
229 };
230 use bitwarden_core::key_management::{KeySlotIds, SymmetricKeySlotId};
231 use bitwarden_crypto::{KeyStore, PublicKeyEncryptionAlgorithm, SymmetricKeyAlgorithm};
232
233 use super::*;
234
235 fn make_test_key_store_and_sync_response() -> (KeyStore<KeySlotIds>, SyncResponseModel) {
236 let store: KeyStore<KeySlotIds> = KeyStore::default();
237 let wrapped_private_key = {
238 let mut ctx = store.context_mut();
239 let user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
240 let _ = ctx.persist_symmetric_key(user_key, SymmetricKeySlotId::User);
241 let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
242 ctx.wrap_private_key(SymmetricKeySlotId::User, private_key)
243 .unwrap()
244 };
245
246 let sync_response = SyncResponseModel {
247 object: Some("sync".to_string()),
248 profile: Some(Box::new(ProfileResponseModel {
249 id: Some(uuid::Uuid::new_v4()),
250 account_keys: Some(Box::new(PrivateKeysResponseModel {
251 object: None,
252 signature_key_pair: None,
253 public_key_encryption_key_pair: Box::new(
254 PublicKeyEncryptionKeyPairResponseModel {
255 object: None,
256 wrapped_private_key: Some(wrapped_private_key.to_string()),
257 public_key: None,
258 signed_public_key: None,
259 },
260 ),
261 security_state: None,
262 })),
263 ..ProfileResponseModel::default()
264 })),
265 folders: Some(vec![]),
266 ciphers: Some(vec![]),
267 sends: Some(vec![]),
268 user_decryption: Some(Box::new(UserDecryptionResponseModel {
269 master_password_unlock: Some(Box::new(MasterPasswordUnlockResponseModel {
270 kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
271 kdf_type: KdfType::PBKDF2_SHA256,
272 iterations: 600000,
273 memory: None,
274 parallelism: None,
275 }),
276 master_key_encrypted_user_key: None,
277 salt: Some("test_salt".to_string()),
278 })),
279 web_authn_prf_options: None,
280 v2_upgrade_token: None,
281 })),
282 ..Default::default()
283 };
284
285 (store, sync_response)
286 }
287
288 fn mock_empty_sync_calls(mock: &mut bitwarden_api_api::apis::ApiClientMock) {
289 mock.organizations_api
290 .expect_get_user()
291 .once()
292 .returning(|| {
293 Ok(ProfileOrganizationResponseModelListResponseModel {
294 object: None,
295 data: Some(vec![]),
296 continuation_token: None,
297 })
298 });
299 mock.emergency_access_api
300 .expect_get_contacts()
301 .once()
302 .returning(|| {
303 Ok(
304 EmergencyAccessGranteeDetailsResponseModelListResponseModel {
305 object: None,
306 data: Some(vec![]),
307 continuation_token: None,
308 },
309 )
310 });
311 mock.devices_api.expect_get_all().once().returning(|| {
312 Ok(DeviceAuthRequestResponseModelListResponseModel {
313 object: None,
314 data: Some(vec![]),
315 continuation_token: None,
316 })
317 });
318 mock.web_authn_api.expect_get().once().returning(|| {
319 Ok(WebAuthnCredentialResponseModelListResponseModel {
320 object: None,
321 data: Some(vec![]),
322 continuation_token: None,
323 })
324 });
325 }
326
327 #[tokio::test]
328 async fn test_rotate_user_keys_tde_returns_unimplemented() {
329 let key_store: KeyStore<KeySlotIds> = KeyStore::default();
330 let api_client = ApiClient::new_mocked(|mock| {
331 mock.sync_api.expect_get().never();
332 mock.accounts_key_management_api
333 .expect_rotate_user_keys()
334 .never();
335 });
336
337 let result = internal_rotate_user_keys(
338 &key_store,
339 &api_client,
340 None,
341 RotateUserKeysRequest {
342 key_rotation_method: KeyRotationMethod::Tde,
343 trusted_organization_public_keys: vec![],
344 trusted_emergency_access_public_keys: vec![],
345 upgrade_token_action: None,
346 },
347 )
348 .await;
349
350 assert!(matches!(
351 result,
352 Err(RotateUserKeysError::UnimplementedKeyRotationMethod)
353 ));
354 if let ApiClient::Mock(mut mock) = api_client {
355 mock.sync_api.checkpoint();
356 mock.accounts_key_management_api.checkpoint();
357 }
358 }
359
360 #[tokio::test]
361 async fn test_rotate_user_keys_api_failure_returns_api_error() {
362 let key_store: KeyStore<KeySlotIds> = KeyStore::default();
363 let api_client = ApiClient::new_mocked(|mock| {
364 mock.sync_api.expect_get().once().returning(|_| {
365 Err(serde_json::Error::io(std::io::Error::other("network error")).into())
366 });
367 mock.accounts_key_management_api
368 .expect_rotate_user_keys()
369 .never();
370 });
371
372 let result = internal_rotate_user_keys(
373 &key_store,
374 &api_client,
375 None,
376 RotateUserKeysRequest {
377 key_rotation_method: KeyRotationMethod::Password {
378 password: "test".to_string(),
379 },
380 trusted_organization_public_keys: vec![],
381 trusted_emergency_access_public_keys: vec![],
382 upgrade_token_action: None,
383 },
384 )
385 .await;
386
387 assert!(matches!(result, Err(RotateUserKeysError::Api)));
388 if let ApiClient::Mock(mut mock) = api_client {
389 mock.sync_api.checkpoint();
390 mock.accounts_key_management_api.checkpoint();
391 }
392 }
393
394 #[tokio::test]
395 async fn test_rotate_user_keys_master_password_success() {
396 let (key_store, sync_response) = make_test_key_store_and_sync_response();
397 let api_client = ApiClient::new_mocked(|mock| {
398 mock.sync_api
399 .expect_get()
400 .once()
401 .returning(move |_| Ok(sync_response.clone()));
402 mock_empty_sync_calls(mock);
403 mock.accounts_key_management_api
404 .expect_rotate_user_keys()
405 .once()
406 .returning(|_| Ok(()));
407 });
408
409 let result = internal_rotate_user_keys(
410 &key_store,
411 &api_client,
412 None,
413 RotateUserKeysRequest {
414 key_rotation_method: KeyRotationMethod::Password {
415 password: "test_password".to_string(),
416 },
417 trusted_organization_public_keys: vec![],
418 trusted_emergency_access_public_keys: vec![],
419 upgrade_token_action: None,
420 },
421 )
422 .await;
423
424 assert!(result.is_ok());
425 if let ApiClient::Mock(mut mock) = api_client {
426 mock.sync_api.checkpoint();
427 mock.organizations_api.checkpoint();
428 mock.emergency_access_api.checkpoint();
429 mock.devices_api.checkpoint();
430 mock.web_authn_api.checkpoint();
431 mock.accounts_key_management_api.checkpoint();
432 }
433 }
434
435 #[tokio::test]
436 async fn test_rotate_user_keys_post_api_failure_returns_api_error() {
437 let (key_store, sync_response) = make_test_key_store_and_sync_response();
438 let api_client = ApiClient::new_mocked(|mock| {
439 mock.sync_api
440 .expect_get()
441 .once()
442 .returning(move |_| Ok(sync_response.clone()));
443 mock_empty_sync_calls(mock);
444 mock.accounts_key_management_api
445 .expect_rotate_user_keys()
446 .once()
447 .returning(|_| {
448 Err(serde_json::Error::io(std::io::Error::other("API error")).into())
449 });
450 });
451
452 let result = internal_rotate_user_keys(
453 &key_store,
454 &api_client,
455 None,
456 RotateUserKeysRequest {
457 key_rotation_method: KeyRotationMethod::Password {
458 password: "test_password".to_string(),
459 },
460 trusted_organization_public_keys: vec![],
461 trusted_emergency_access_public_keys: vec![],
462 upgrade_token_action: None,
463 },
464 )
465 .await;
466
467 assert!(matches!(result, Err(RotateUserKeysError::Api)));
468 if let ApiClient::Mock(mut mock) = api_client {
469 mock.sync_api.checkpoint();
470 mock.organizations_api.checkpoint();
471 mock.emergency_access_api.checkpoint();
472 mock.devices_api.checkpoint();
473 mock.web_authn_api.checkpoint();
474 mock.accounts_key_management_api.checkpoint();
475 }
476 }
477
478 #[tokio::test]
479 async fn test_rotate_user_keys_upgrade_token_action_none_omits_token() {
480 let (key_store, sync_response) = make_test_key_store_and_sync_response();
481 let api_client = ApiClient::new_mocked(|mock| {
482 mock.sync_api
483 .expect_get()
484 .once()
485 .returning(move |_| Ok(sync_response.clone()));
486 mock_empty_sync_calls(mock);
487 mock.accounts_key_management_api
488 .expect_rotate_user_keys()
489 .once()
490 .returning(|req| {
491 let req = req.expect("request body should be present");
492 assert!(
493 req.unlock_data.v2_upgrade_token.is_none(),
494 "upgrade_token_action None, should omit the v2_upgrade_token"
495 );
496 Ok(())
497 });
498 });
499
500 let result = internal_rotate_user_keys(
501 &key_store,
502 &api_client,
503 None,
504 RotateUserKeysRequest {
505 key_rotation_method: KeyRotationMethod::Password {
506 password: "test_password".to_string(),
507 },
508 trusted_organization_public_keys: vec![],
509 trusted_emergency_access_public_keys: vec![],
510 upgrade_token_action: None,
511 },
512 )
513 .await;
514
515 assert!(result.is_ok());
516 if let ApiClient::Mock(mut mock) = api_client {
517 mock.sync_api.checkpoint();
518 mock.organizations_api.checkpoint();
519 mock.emergency_access_api.checkpoint();
520 mock.devices_api.checkpoint();
521 mock.web_authn_api.checkpoint();
522 mock.accounts_key_management_api.checkpoint();
523 }
524 }
525
526 #[tokio::test]
527 async fn test_rotate_user_keys_upgrade_token_action_skip_omits_token() {
528 let (key_store, sync_response) = make_test_key_store_and_sync_response();
529 let api_client = ApiClient::new_mocked(|mock| {
530 mock.sync_api
531 .expect_get()
532 .once()
533 .returning(move |_| Ok(sync_response.clone()));
534 mock_empty_sync_calls(mock);
535 mock.accounts_key_management_api
536 .expect_rotate_user_keys()
537 .once()
538 .returning(|req| {
539 let req = req.expect("request body should be present");
540 assert!(
541 req.unlock_data.v2_upgrade_token.is_none(),
542 "upgrade_token_action Skip, should omit the v2_upgrade_token"
543 );
544 Ok(())
545 });
546 });
547
548 let result = internal_rotate_user_keys(
549 &key_store,
550 &api_client,
551 None,
552 RotateUserKeysRequest {
553 key_rotation_method: KeyRotationMethod::Password {
554 password: "test_password".to_string(),
555 },
556 trusted_organization_public_keys: vec![],
557 trusted_emergency_access_public_keys: vec![],
558 upgrade_token_action: Some(UpgradeTokenAction::Skip),
559 },
560 )
561 .await;
562
563 assert!(result.is_ok());
564 if let ApiClient::Mock(mut mock) = api_client {
565 mock.sync_api.checkpoint();
566 mock.organizations_api.checkpoint();
567 mock.emergency_access_api.checkpoint();
568 mock.devices_api.checkpoint();
569 mock.web_authn_api.checkpoint();
570 mock.accounts_key_management_api.checkpoint();
571 }
572 }
573
574 #[tokio::test]
575 async fn test_rotate_user_keys_upgrade_token_action_create_if_needed_includes_token() {
576 let (key_store, sync_response) = make_test_key_store_and_sync_response();
577 let api_client = ApiClient::new_mocked(|mock| {
578 mock.sync_api
579 .expect_get()
580 .once()
581 .returning(move |_| Ok(sync_response.clone()));
582 mock_empty_sync_calls(mock);
583 mock.accounts_key_management_api
584 .expect_rotate_user_keys()
585 .once()
586 .returning(|req| {
587 let req = req.expect("request body should be present");
588 assert!(
589 req.unlock_data.v2_upgrade_token.is_some(),
590 "upgrade_token_action CreateIfNeeded, should include a v2_upgrade_token for V1 -> V2 rotations"
591 );
592 Ok(())
593 });
594 });
595
596 let result = internal_rotate_user_keys(
597 &key_store,
598 &api_client,
599 None,
600 RotateUserKeysRequest {
601 key_rotation_method: KeyRotationMethod::Password {
602 password: "test_password".to_string(),
603 },
604 trusted_organization_public_keys: vec![],
605 trusted_emergency_access_public_keys: vec![],
606 upgrade_token_action: Some(UpgradeTokenAction::CreateIfNeeded),
607 },
608 )
609 .await;
610
611 assert!(result.is_ok());
612 if let ApiClient::Mock(mut mock) = api_client {
613 mock.sync_api.checkpoint();
614 mock.organizations_api.checkpoint();
615 mock.emergency_access_api.checkpoint();
616 mock.devices_api.checkpoint();
617 mock.web_authn_api.checkpoint();
618 mock.accounts_key_management_api.checkpoint();
619 }
620 }
621
622 #[tokio::test]
623 async fn test_rotate_user_keys_old_attachments_returns_error() {
624 use bitwarden_api_api::models::{
625 AttachmentResponseModel, CipherDetailsResponseModel, CipherType,
626 };
627
628 let (key_store, mut sync_response) = make_test_key_store_and_sync_response();
629 let enc_string = "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=";
630
631 sync_response.ciphers = Some(vec![CipherDetailsResponseModel {
633 id: Some(uuid::Uuid::new_v4()),
634 organization_id: None,
635 r#type: Some(CipherType::Login),
636 name: Some(enc_string.to_string()),
637 revision_date: Some("2024-01-01T00:00:00Z".to_string()),
638 creation_date: Some("2024-01-01T00:00:00Z".to_string()),
639 attachments: Some(vec![AttachmentResponseModel {
640 id: Some("att1".to_string()),
641 file_name: Some(enc_string.to_string()),
642 key: None, ..AttachmentResponseModel::new()
644 }]),
645 ..CipherDetailsResponseModel::new()
646 }]);
647
648 let api_client = ApiClient::new_mocked(|mock| {
649 mock.sync_api
650 .expect_get()
651 .once()
652 .returning(move |_| Ok(sync_response.clone()));
653 mock_empty_sync_calls(mock);
654 mock.accounts_key_management_api
656 .expect_rotate_user_keys()
657 .never();
658 });
659
660 let result = internal_rotate_user_keys(
661 &key_store,
662 &api_client,
663 None,
664 RotateUserKeysRequest {
665 key_rotation_method: KeyRotationMethod::Password {
666 password: "test_password".to_string(),
667 },
668 trusted_organization_public_keys: vec![],
669 trusted_emergency_access_public_keys: vec![],
670 upgrade_token_action: None,
671 },
672 )
673 .await;
674
675 assert!(matches!(result, Err(RotateUserKeysError::OldAttachments)));
676 if let ApiClient::Mock(mut mock) = api_client {
677 mock.sync_api.checkpoint();
678 mock.organizations_api.checkpoint();
679 mock.emergency_access_api.checkpoint();
680 mock.devices_api.checkpoint();
681 mock.web_authn_api.checkpoint();
682 mock.accounts_key_management_api.checkpoint();
683 }
684 }
685
686 #[tokio::test]
687 async fn test_rotate_user_keys_key_connector_success() {
688 let (key_store, sync_response) = make_test_key_store_and_sync_response();
689
690 let key_connector_key = KeyConnectorKey::make();
691 let key_connector_api_client = bitwarden_api_key_connector::apis::ApiClient::new_mocked(
692 |mock| {
693 let key_connector_key_clone = key_connector_key.clone();
694 mock.user_keys_api
695 .expect_get_user_key()
696 .once()
697 .returning(move || {
698 let encoded: bitwarden_encoding::B64 =
699 key_connector_key_clone.clone().into();
700 Ok(
701 bitwarden_api_key_connector::models::user_key_response_model::UserKeyResponseModel {
702 key: encoded.to_string(),
703 },
704 )
705 });
706 },
707 );
708
709 let api_client = ApiClient::new_mocked(|mock| {
710 mock.sync_api
711 .expect_get()
712 .once()
713 .returning(move |_| Ok(sync_response.clone()));
714 mock_empty_sync_calls(mock);
715 mock.accounts_key_management_api
716 .expect_rotate_user_keys()
717 .once()
718 .returning(|req| {
719 let req = req.expect("request body should be present");
720 assert!(
721 req.unlock_method_data
722 .key_connector_key_wrapped_user_key
723 .is_some(),
724 "key_connector_key_wrapped_user_key should be set for KC rotation"
725 );
726 assert!(
727 req.unlock_method_data.master_password_unlock_data.is_none(),
728 "master_password_unlock_data should be None for KC rotation"
729 );
730 Ok(())
731 });
732 });
733
734 let result = internal_rotate_user_keys(
735 &key_store,
736 &api_client,
737 Some(&key_connector_api_client),
738 RotateUserKeysRequest {
739 key_rotation_method: KeyRotationMethod::KeyConnector {
740 key_connector_url: "https://kc.example.com".to_string(),
741 },
742 trusted_organization_public_keys: vec![],
743 trusted_emergency_access_public_keys: vec![],
744 upgrade_token_action: None,
745 },
746 )
747 .await;
748
749 assert!(result.is_ok());
750 if let ApiClient::Mock(mut mock) = api_client {
751 mock.sync_api.checkpoint();
752 mock.organizations_api.checkpoint();
753 mock.emergency_access_api.checkpoint();
754 mock.devices_api.checkpoint();
755 mock.web_authn_api.checkpoint();
756 mock.accounts_key_management_api.checkpoint();
757 }
758 if let bitwarden_api_key_connector::apis::ApiClient::Mock(mut mock) =
759 key_connector_api_client
760 {
761 mock.user_keys_api.checkpoint();
762 }
763 }
764
765 #[tokio::test]
766 async fn test_rotate_user_keys_key_connector_api_failure() {
767 let (key_store, sync_response) = make_test_key_store_and_sync_response();
768
769 let key_connector_api_client =
770 bitwarden_api_key_connector::apis::ApiClient::new_mocked(|mock| {
771 mock.user_keys_api
772 .expect_get_user_key()
773 .once()
774 .returning(move || {
775 Err(bitwarden_api_key_connector::apis::Error::ResponseError(
776 bitwarden_api_key_connector::apis::ResponseContent {
777 status: reqwest::StatusCode::INTERNAL_SERVER_ERROR,
778 content: "Server Error".to_string(),
779 },
780 ))
781 });
782 });
783
784 let api_client = ApiClient::new_mocked(|mock| {
785 mock.sync_api
786 .expect_get()
787 .once()
788 .returning(move |_| Ok(sync_response.clone()));
789 mock_empty_sync_calls(mock);
790 mock.accounts_key_management_api
791 .expect_rotate_user_keys()
792 .never();
793 });
794
795 let result = internal_rotate_user_keys(
796 &key_store,
797 &api_client,
798 Some(&key_connector_api_client),
799 RotateUserKeysRequest {
800 key_rotation_method: KeyRotationMethod::KeyConnector {
801 key_connector_url: "https://kc.example.com".to_string(),
802 },
803 trusted_organization_public_keys: vec![],
804 trusted_emergency_access_public_keys: vec![],
805 upgrade_token_action: None,
806 },
807 )
808 .await;
809
810 assert!(matches!(result, Err(RotateUserKeysError::KeyConnectorApi)));
811 if let ApiClient::Mock(mut mock) = api_client {
812 mock.sync_api.checkpoint();
813 mock.organizations_api.checkpoint();
814 mock.emergency_access_api.checkpoint();
815 mock.devices_api.checkpoint();
816 mock.web_authn_api.checkpoint();
817 mock.accounts_key_management_api.checkpoint();
818 }
819 if let bitwarden_api_key_connector::apis::ApiClient::Mock(mut mock) =
820 key_connector_api_client
821 {
822 mock.user_keys_api.checkpoint();
823 }
824 }
825}