1use bitwarden_api_api::models::RotateUserKeysRequestModel;
3use bitwarden_core::key_management::{
4 KeySlotIds, V2UpgradeToken, account_cryptographic_state::WrappedAccountCryptographicState,
5};
6use bitwarden_crypto::{KeyConnectorKey, KeyStore, PublicKey, SymmetricCryptoKey};
7use serde::{Deserialize, Serialize};
8use tracing::info;
9
10use crate::{
11 UserCryptoManagementClient,
12 key_rotation::{
13 RotateUserKeysError,
14 crypto::{
15 account_cryptographic_state_to_wrapped_model, rotate_account_cryptographic_state,
16 },
17 data::{check_for_old_attachments, reencrypt_data},
18 rotation_context::make_rotation_context,
19 sync::{SyncedAccountData, 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#[bitwarden_ffi::wasm_record]
27#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
28pub enum KeyRotationMethod {
29 Password { password: String },
31 KeyConnector { key_connector_url: String },
33 Tde,
35}
36
37#[derive(Serialize, Deserialize, Clone, PartialEq)]
38#[bitwarden_ffi::wasm_record]
39#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
40pub enum UpgradeTokenAction {
41 Skip,
43 CreateIfNeeded,
46}
47
48#[derive(Serialize, Deserialize, Clone)]
49#[bitwarden_ffi::wasm_record]
50#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
51pub struct RotateUserKeysRequest {
52 pub key_rotation_method: KeyRotationMethod,
53 pub trusted_emergency_access_public_keys: Vec<PublicKey>,
54 pub trusted_organization_public_keys: Vec<PublicKey>,
57 pub upgrade_token_action: UpgradeTokenAction,
58}
59
60#[bitwarden_ffi::wasm_export]
61impl UserCryptoManagementClient {
62 pub async fn rotate_user_keys(
64 &self,
65 request: RotateUserKeysRequest,
66 ) -> Result<(), RotateUserKeysError> {
67 let api_client = &self.client.internal.get_api_configurations().api_client;
68 let key_store = self.client.internal.get_key_store();
69
70 let sync = sync_current_account_data(api_client)
71 .await
72 .map_err(|_| RotateUserKeysError::Api)?;
73
74 let wrapped_account_cryptographic_state = self
75 .regenerate_public_key_encryption_key_pair_if_needed_with_ciphers(&sync.ciphers)
76 .await
77 .map_err(|_| RotateUserKeysError::Crypto)?
78 .unwrap_or_else(|| sync.wrapped_account_cryptographic_state.clone());
79
80 let key_connector_api_client =
81 if let KeyRotationMethod::KeyConnector { key_connector_url } =
82 &request.key_rotation_method
83 {
84 Some(
85 self.client
86 .internal
87 .get_key_connector_client(key_connector_url.clone()),
88 )
89 } else {
90 None
91 };
92
93 internal_rotate_user_keys(
94 key_store,
95 api_client,
96 &self.client.km_state_bridge(),
97 key_connector_api_client.as_ref(),
98 request,
99 wrapped_account_cryptographic_state,
100 sync,
101 )
102 .await
103 }
104}
105
106struct StateUpdate {
109 user_key: SymmetricCryptoKey,
110 account_cryptographic_state: WrappedAccountCryptographicState,
111 upgrade_token: Option<V2UpgradeToken>,
112}
113
114#[bitwarden_logging::instrument(name = "rotate_user_keys", level = "info", err)]
115async fn internal_rotate_user_keys(
116 key_store: &KeyStore<KeySlotIds>,
117 api_client: &bitwarden_api_api::apis::ApiClient,
118 state_bridge: &bitwarden_core::key_management::state_bridge::StateBridgeClient,
119 key_connector_api_client: Option<&bitwarden_api_key_connector::apis::ApiClient>,
120 request: RotateUserKeysRequest,
121 wrapped_account_cryptographic_state: WrappedAccountCryptographicState,
122 sync: SyncedAccountData,
123) -> Result<(), RotateUserKeysError> {
124 check_for_old_attachments(&sync.ciphers)?;
126
127 let key_connector_key = if matches!(
130 request.key_rotation_method,
131 KeyRotationMethod::KeyConnector { .. }
132 ) {
133 let key_connector_client =
134 key_connector_api_client.ok_or(RotateUserKeysError::KeyConnectorApi)?;
135 info!("Fetching Key Connector key for key rotation");
136 let response = key_connector_client
137 .user_keys_api()
138 .get_user_key()
139 .await
140 .map_err(|_| RotateUserKeysError::KeyConnectorApi)?;
141 let key_connector_key =
142 KeyConnectorKey::try_from(response).map_err(|_| RotateUserKeysError::Crypto)?;
143 Some(key_connector_key)
144 } else {
145 None
146 };
147
148 let (post_request, state_bridge_update) = {
150 let mut ctx = key_store.context_mut();
151
152 let rotation_context = make_rotation_context(
153 &sync,
154 request.trusted_organization_public_keys.as_slice(),
155 request.trusted_emergency_access_public_keys.as_slice(),
156 request.upgrade_token_action.clone(),
157 &mut ctx,
158 )?;
159
160 info!("Rotating account cryptographic state for user key rotation");
161 let wrapped_account_cryptographic_state = rotate_account_cryptographic_state(
162 &wrapped_account_cryptographic_state,
163 &rotation_context.current_user_key_id,
164 &rotation_context.new_user_key_id,
165 &mut ctx,
166 )
167 .map_err(|_| RotateUserKeysError::Crypto)?;
168 let wrapped_account_cryptographic_state_request_model =
169 account_cryptographic_state_to_wrapped_model(
170 &wrapped_account_cryptographic_state,
171 &rotation_context.new_user_key_id,
172 &mut ctx,
173 )
174 .map_err(|_| RotateUserKeysError::Crypto)?;
175
176 info!("Re-encrypting account data for user key rotation");
177 let account_data_model = reencrypt_data(
178 sync.folders.as_slice(),
179 sync.ciphers.as_slice(),
180 sync.sends.as_slice(),
181 rotation_context.current_user_key_id,
182 rotation_context.new_user_key_id,
183 &mut ctx,
184 )
185 .map_err(|_| RotateUserKeysError::Crypto)?;
186
187 info!("Re-encrypting account primary unlock method for user key rotation");
188 let unlock_method_input = PrimaryUnlockMethod::from_key_rotation_method(
189 request.key_rotation_method,
190 &sync,
191 key_connector_key,
192 )?;
193 let unlock_method_data = reencrypt_unlock_method_data(
194 unlock_method_input,
195 rotation_context.new_user_key_id,
196 &mut ctx,
197 )
198 .map_err(|_| RotateUserKeysError::Crypto)?;
199
200 info!("Re-encrypting account common unlock data for user key rotation");
201 let common_unlock_data = reencrypt_common_unlock_data(
202 ReencryptCommonUnlockDataInput {
203 trusted_organization_keys: rotation_context.v1_organization_memberships,
204 trusted_emergency_access_keys: rotation_context.v1_emergency_access_memberships,
205 webauthn_credentials: sync.passkeys,
206 trusted_devices: sync.trusted_devices,
207 },
208 rotation_context.current_user_key_id,
209 rotation_context.new_user_key_id,
210 rotation_context.creates_v2_upgrade_token,
211 &mut ctx,
212 )
213 .map_err(|_| RotateUserKeysError::Crypto)?;
214
215 (
216 RotateUserKeysRequestModel {
217 wrapped_account_cryptographic_state: Box::new(
218 wrapped_account_cryptographic_state_request_model,
219 ),
220 account_data: Box::new(account_data_model),
221 unlock_data: Box::new(common_unlock_data.clone()),
222 unlock_method_data: Box::new(unlock_method_data),
223 new_user_key_id: ctx
225 .get_symmetric_key_id(rotation_context.new_user_key_id)
226 .map(|id| id.to_string()),
227 },
228 StateUpdate {
229 #[allow(deprecated)]
230 user_key: ctx
231 .dangerous_get_symmetric_key(rotation_context.new_user_key_id)
232 .map_err(|_| RotateUserKeysError::Crypto)?
233 .to_owned(),
234 account_cryptographic_state: wrapped_account_cryptographic_state,
235 upgrade_token: common_unlock_data
236 .v2_upgrade_token
237 .clone()
238 .map(|t| (*t).try_into())
239 .transpose()
240 .map_err(|_| RotateUserKeysError::Crypto)?,
241 },
242 )
243 };
244
245 info!("Posting rotated user account keys and data to server");
246 api_client
247 .accounts_key_management_api()
248 .rotate_user_keys(Some(post_request))
249 .await
250 .map_err(|_| RotateUserKeysError::Api)?;
251 info!("Successfully rotated user account keys and data");
252
253 if let Some(upgrade_token) = state_bridge_update.upgrade_token.as_ref() {
254 info!("Writing new cryptographic data to state");
255 state_bridge
256 .set_account_cryptographic_state(&state_bridge_update.account_cryptographic_state)
257 .await;
258 state_bridge.set_v2_upgrade_token(upgrade_token).await;
259 state_bridge
260 .set_user_key(&state_bridge_update.user_key)
261 .await;
262 }
265
266 Ok(())
267}
268
269#[cfg(test)]
270mod tests {
271 use std::str::FromStr;
272
273 use bitwarden_api_api::{apis::ApiClient, models::UnlockMethod};
274 use bitwarden_core::{
275 Client,
276 key_management::{
277 KeySlotIds, PrivateKeySlotId, SymmetricKeySlotId,
278 account_cryptographic_state::WrappedAccountCryptographicState,
279 state_bridge::{StateBridgeClient, test_support::InMemoryStateBridge},
280 },
281 };
282 use bitwarden_crypto::{
283 Decryptable, EncString, Kdf, KeyStore, PublicKeyEncryptionAlgorithm, SymmetricKeyAlgorithm,
284 UnsignedSharedKey,
285 };
286 use bitwarden_vault::{Attachment, Cipher, CipherType};
287 use chrono::DateTime;
288
289 use super::*;
290 use crate::key_rotation::{
291 partial_rotateable_keyset::PartialRotateableKeyset, unlock::V1OrganizationMembership,
292 };
293
294 fn make_state_bridge() -> StateBridgeClient {
295 let client = Client::new(None);
296 let bridge = client.km_state_bridge();
297 bridge.register_bridge(Box::new(InMemoryStateBridge::default()));
298 bridge
299 }
300
301 fn make_test_key_store_and_synced_data() -> (KeyStore<KeySlotIds>, SyncedAccountData) {
302 let store: KeyStore<KeySlotIds> = KeyStore::default();
303 let wrapped_private_key = {
304 let mut ctx = store.context_mut();
305 let user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
306 let _ = ctx.persist_symmetric_key(user_key, SymmetricKeySlotId::User);
307 let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
308 ctx.wrap_private_key(SymmetricKeySlotId::User, private_key)
309 .unwrap()
310 };
311
312 let sync = SyncedAccountData {
313 wrapped_account_cryptographic_state: WrappedAccountCryptographicState::V1 {
314 private_key: wrapped_private_key,
315 },
316 folders: vec![],
317 ciphers: vec![],
318 sends: vec![],
319 emergency_access_memberships: vec![],
320 organization_memberships: vec![],
321 trusted_devices: vec![],
322 passkeys: vec![],
323 kdf_and_salt: Some((
324 Kdf::PBKDF2 {
325 iterations: std::num::NonZeroU32::new(600000).unwrap(),
326 },
327 "test_salt".to_string(),
328 )),
329 };
330
331 (store, sync)
332 }
333
334 fn make_test_key_store_and_synced_data_with_organization()
336 -> (KeyStore<KeySlotIds>, SyncedAccountData) {
337 let (store, mut sync) = make_test_key_store_and_synced_data();
338 {
339 let mut ctx = store.context_mut();
340 let organization_private_key =
341 ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
342 sync.organization_memberships = vec![V1OrganizationMembership {
343 organization_id: uuid::Uuid::new_v4(),
344 name: "Test Org".to_string(),
345 public_key: ctx
346 .get_public_key(organization_private_key)
347 .expect("public key should exist"),
348 }];
349 }
350
351 (store, sync)
352 }
353
354 fn make_test_key_store_and_synced_data_with_trusted_devices()
355 -> (KeyStore<KeySlotIds>, SyncedAccountData, Vec<u8>) {
356 let store: KeyStore<KeySlotIds> = KeyStore::default();
357 let (trusted_device_keyset, wrapped_private_key, public_key) = {
358 let mut ctx = store.context_mut();
359 let user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
360 let _ = ctx.persist_symmetric_key(user_key, SymmetricKeySlotId::User);
361 let (trusted_device_keyset, device_private_key) =
362 PartialRotateableKeyset::make_test_keyset(SymmetricKeySlotId::User, &mut ctx);
363 let _ = ctx.persist_private_key(device_private_key, PrivateKeySlotId::UserPrivateKey);
364 let wrapped_private_key = ctx
365 .wrap_private_key(SymmetricKeySlotId::User, PrivateKeySlotId::UserPrivateKey)
366 .unwrap();
367 (
368 trusted_device_keyset,
369 wrapped_private_key,
370 ctx.get_public_key(PrivateKeySlotId::UserPrivateKey)
371 .expect("Retrieving the public key should work."),
372 )
373 };
374
375 let sync = SyncedAccountData {
376 wrapped_account_cryptographic_state: WrappedAccountCryptographicState::V1 {
377 private_key: wrapped_private_key,
378 },
379 folders: vec![],
380 ciphers: vec![],
381 sends: vec![],
382 emergency_access_memberships: vec![],
383 organization_memberships: vec![],
384 trusted_devices: vec![trusted_device_keyset],
385 passkeys: vec![],
386 kdf_and_salt: Some((
387 Kdf::PBKDF2 {
388 iterations: std::num::NonZeroU32::new(600000).unwrap(),
389 },
390 "test_salt".to_string(),
391 )),
392 };
393
394 (
395 store,
396 sync,
397 public_key
398 .to_der()
399 .expect("Generating DER serialization should work")
400 .to_vec(),
401 )
402 }
403
404 #[tokio::test]
405 async fn test_rotate_user_keys_tde_success_rotates_common_unlock_data() {
406 let (key_store, sync, public_key_der) =
407 make_test_key_store_and_synced_data_with_trusted_devices();
408 let key_store_clone = key_store.clone();
409
410 let api_client = ApiClient::new_mocked(|mock| {
411 mock.accounts_key_management_api
412 .expect_rotate_user_keys()
413 .once()
414 .returning(move |req| {
415 let req = req.expect("request body should be present");
416 assert_eq!(req.unlock_method_data.unlock_method, UnlockMethod::Tde);
417 assert!(req.unlock_method_data.master_password_unlock_data.is_none());
418 assert!(
419 req.unlock_method_data
420 .key_connector_key_wrapped_user_key
421 .is_none()
422 );
423
424 let device_unlock_data = req
425 .unlock_data
426 .device_key_unlock_data
427 .expect("device unlock data should be present");
428 assert_eq!(device_unlock_data.len(), 1);
429 let rotated_device = &device_unlock_data[0];
430
431 let encrypted_user_key: UnsignedSharedKey = rotated_device
432 .encrypted_user_key
433 .parse()
434 .expect("encrypted user key should parse");
435 let encrypted_public_key: EncString = rotated_device
436 .encrypted_public_key
437 .parse()
438 .expect("encrypted public key should parse");
439 let mut ctx = key_store_clone.context_mut();
440 let rotated_user_key_id = encrypted_user_key
441 .decapsulate(PrivateKeySlotId::UserPrivateKey, &mut ctx)
442 .expect("rotated device user key should decapsulate");
443 let decrypted_public_key: Vec<u8> = encrypted_public_key
444 .decrypt(&mut ctx, rotated_user_key_id)
445 .expect("rotated device public key should decrypt");
446 assert_eq!(decrypted_public_key, public_key_der);
447 Ok(())
448 });
449 });
450
451 let state_bridge = make_state_bridge();
452 let result = internal_rotate_user_keys(
453 &key_store,
454 &api_client,
455 &state_bridge,
456 None,
457 RotateUserKeysRequest {
458 key_rotation_method: KeyRotationMethod::Tde,
459 trusted_organization_public_keys: vec![],
460 trusted_emergency_access_public_keys: vec![],
461 upgrade_token_action: UpgradeTokenAction::Skip,
462 },
463 sync.wrapped_account_cryptographic_state.clone(),
464 sync,
465 )
466 .await;
467
468 assert!(result.is_ok());
469 if let ApiClient::Mock(mut mock) = api_client {
470 mock.accounts_key_management_api.checkpoint();
471 }
472 }
473
474 #[tokio::test]
475 async fn test_rotate_user_keys_master_password_success() {
476 let (key_store, sync) = make_test_key_store_and_synced_data();
477 let api_client = ApiClient::new_mocked(|mock| {
478 mock.accounts_key_management_api
479 .expect_rotate_user_keys()
480 .once()
481 .returning(|_| Ok(()));
482 });
483
484 let state_bridge = make_state_bridge();
485 let result = internal_rotate_user_keys(
486 &key_store,
487 &api_client,
488 &state_bridge,
489 None,
490 RotateUserKeysRequest {
491 key_rotation_method: KeyRotationMethod::Password {
492 password: "test_password".to_string(),
493 },
494 trusted_organization_public_keys: vec![],
495 trusted_emergency_access_public_keys: vec![],
496 upgrade_token_action: UpgradeTokenAction::Skip,
497 },
498 sync.wrapped_account_cryptographic_state.clone(),
499 sync,
500 )
501 .await;
502
503 assert!(result.is_ok());
504 if let ApiClient::Mock(mut mock) = api_client {
505 mock.accounts_key_management_api.checkpoint();
506 }
507 }
508
509 #[tokio::test]
510 async fn test_rotate_user_keys_post_api_failure_returns_api_error() {
511 let (key_store, sync) = make_test_key_store_and_synced_data();
512 let api_client = ApiClient::new_mocked(|mock| {
513 mock.accounts_key_management_api
514 .expect_rotate_user_keys()
515 .once()
516 .returning(|_| {
517 Err(serde_json::Error::io(std::io::Error::other("API error")).into())
518 });
519 });
520
521 let state_bridge = make_state_bridge();
522 let result = internal_rotate_user_keys(
523 &key_store,
524 &api_client,
525 &state_bridge,
526 None,
527 RotateUserKeysRequest {
528 key_rotation_method: KeyRotationMethod::Password {
529 password: "test_password".to_string(),
530 },
531 trusted_organization_public_keys: vec![],
532 trusted_emergency_access_public_keys: vec![],
533 upgrade_token_action: UpgradeTokenAction::Skip,
534 },
535 sync.wrapped_account_cryptographic_state.clone(),
536 sync,
537 )
538 .await;
539
540 assert!(matches!(result, Err(RotateUserKeysError::Api)));
541 if let ApiClient::Mock(mut mock) = api_client {
542 mock.accounts_key_management_api.checkpoint();
543 }
544 }
545
546 #[tokio::test]
547 async fn test_rotate_user_keys_upgrade_token_action_skip_omits_token() {
548 let (key_store, sync) = make_test_key_store_and_synced_data();
549 let api_client = ApiClient::new_mocked(|mock| {
550 mock.accounts_key_management_api
551 .expect_rotate_user_keys()
552 .once()
553 .returning(|req| {
554 let req = req.expect("request body should be present");
555 assert!(
556 req.unlock_data.v2_upgrade_token.is_none(),
557 "upgrade_token_action Skip, should omit the v2_upgrade_token"
558 );
559 Ok(())
560 });
561 });
562
563 let state_bridge = make_state_bridge();
564 let result = internal_rotate_user_keys(
565 &key_store,
566 &api_client,
567 &state_bridge,
568 None,
569 RotateUserKeysRequest {
570 key_rotation_method: KeyRotationMethod::Password {
571 password: "test_password".to_string(),
572 },
573 trusted_organization_public_keys: vec![],
574 trusted_emergency_access_public_keys: vec![],
575 upgrade_token_action: UpgradeTokenAction::Skip,
576 },
577 sync.wrapped_account_cryptographic_state.clone(),
578 sync,
579 )
580 .await;
581
582 assert!(result.is_ok());
583 if let ApiClient::Mock(mut mock) = api_client {
584 mock.accounts_key_management_api.checkpoint();
585 }
586 }
587
588 #[tokio::test]
589 async fn test_rotate_user_keys_upgrade_token_action_create_if_needed_includes_token() {
590 let (key_store, sync) = make_test_key_store_and_synced_data();
591 let api_client = ApiClient::new_mocked(|mock| {
592 mock.accounts_key_management_api
593 .expect_rotate_user_keys()
594 .once()
595 .returning(|req| {
596 let req = req.expect("request body should be present");
597 assert!(
598 req.unlock_data.v2_upgrade_token.is_some(),
599 "upgrade_token_action CreateIfNeeded, should include a v2_upgrade_token for V1 -> V2 rotations"
600 );
601 Ok(())
602 });
603 });
604
605 let state_bridge = make_state_bridge();
606 let result = internal_rotate_user_keys(
607 &key_store,
608 &api_client,
609 &state_bridge,
610 None,
611 RotateUserKeysRequest {
612 key_rotation_method: KeyRotationMethod::Password {
613 password: "test_password".to_string(),
614 },
615 trusted_organization_public_keys: vec![],
616 trusted_emergency_access_public_keys: vec![],
617 upgrade_token_action: UpgradeTokenAction::CreateIfNeeded,
618 },
619 sync.wrapped_account_cryptographic_state.clone(),
620 sync,
621 )
622 .await;
623
624 assert!(result.is_ok());
625 if let ApiClient::Mock(mut mock) = api_client {
626 mock.accounts_key_management_api.checkpoint();
627 }
628 }
629
630 #[tokio::test]
631 async fn test_rotate_user_keys_writes_state_when_upgrade_token_present() {
632 let (key_store, sync) = make_test_key_store_and_synced_data();
633 let api_client = ApiClient::new_mocked(|mock| {
634 mock.accounts_key_management_api
635 .expect_rotate_user_keys()
636 .once()
637 .returning(|_| Ok(()));
638 });
639
640 let state_bridge = make_state_bridge();
641 assert!(state_bridge.get_v2_upgrade_token().await.is_none());
642 assert!(
643 state_bridge
644 .get_account_cryptographic_state()
645 .await
646 .is_none()
647 );
648 assert!(state_bridge.get_user_key().await.is_none());
649
650 let result = internal_rotate_user_keys(
651 &key_store,
652 &api_client,
653 &state_bridge,
654 None,
655 RotateUserKeysRequest {
656 key_rotation_method: KeyRotationMethod::Password {
657 password: "test_password".to_string(),
658 },
659 trusted_organization_public_keys: vec![],
660 trusted_emergency_access_public_keys: vec![],
661 upgrade_token_action: UpgradeTokenAction::CreateIfNeeded,
662 },
663 sync.wrapped_account_cryptographic_state.clone(),
664 sync,
665 )
666 .await;
667
668 assert!(result.is_ok());
669 assert!(
670 state_bridge.get_v2_upgrade_token().await.is_some(),
671 "state bridge should hold the v2 upgrade token after V1 -> V2 rotation"
672 );
673 assert!(
674 state_bridge
675 .get_account_cryptographic_state()
676 .await
677 .is_some(),
678 "state bridge should hold the rotated account cryptographic state"
679 );
680 assert!(
681 state_bridge.get_user_key().await.is_some(),
682 "state bridge should hold the rotated user key"
683 );
684 if let ApiClient::Mock(mut mock) = api_client {
685 mock.accounts_key_management_api.checkpoint();
686 }
687 }
688
689 #[tokio::test]
690 async fn test_rotate_user_keys_skips_state_writes_when_no_upgrade_token() {
691 let (key_store, sync) = make_test_key_store_and_synced_data();
692 let api_client = ApiClient::new_mocked(|mock| {
693 mock.accounts_key_management_api
694 .expect_rotate_user_keys()
695 .once()
696 .returning(|_| Ok(()));
697 });
698
699 let state_bridge = make_state_bridge();
700 let result = internal_rotate_user_keys(
701 &key_store,
702 &api_client,
703 &state_bridge,
704 None,
705 RotateUserKeysRequest {
706 key_rotation_method: KeyRotationMethod::Password {
707 password: "test_password".to_string(),
708 },
709 trusted_organization_public_keys: vec![],
710 trusted_emergency_access_public_keys: vec![],
711 upgrade_token_action: UpgradeTokenAction::Skip,
712 },
713 sync.wrapped_account_cryptographic_state.clone(),
714 sync,
715 )
716 .await;
717
718 assert!(result.is_ok());
719 assert!(
720 state_bridge.get_v2_upgrade_token().await.is_none(),
721 "without an upgrade token, the state bridge must not be written"
722 );
723 assert!(
724 state_bridge
725 .get_account_cryptographic_state()
726 .await
727 .is_none()
728 );
729 assert!(state_bridge.get_user_key().await.is_none());
730 if let ApiClient::Mock(mut mock) = api_client {
731 mock.accounts_key_management_api.checkpoint();
732 }
733 }
734
735 #[tokio::test]
736 async fn test_rotate_user_keys_old_attachments_returns_error() {
737 let (key_store, mut sync) = make_test_key_store_and_synced_data();
738 let enc_string = "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=";
739
740 sync.ciphers = vec![Cipher {
742 partial_data: None,
743 id: None,
744 organization_id: None,
745 folder_id: None,
746 collection_ids: vec![],
747 r#type: CipherType::Login,
748 login: None,
749 identity: None,
750 card: None,
751 secure_note: None,
752 ssh_key: None,
753 bank_account: None,
754 drivers_license: None,
755 passport: None,
756 favorite: false,
757 reprompt: Default::default(),
758 organization_use_totp: false,
759 edit: false,
760 permissions: None,
761 view_password: false,
762 name: Some(enc_string.parse().unwrap()),
763 revision_date: DateTime::from_str("2024-01-01T00:00:00Z").unwrap(),
764 archived_date: None,
765 creation_date: DateTime::from_str("2024-01-01T00:00:00Z").unwrap(),
766 attachments: Some(vec![Attachment {
767 id: None,
768 url: None,
769 size: None,
770 size_name: None,
771 file_name: None,
772 key: None, }]),
774 fields: None,
775 key: None,
776 notes: None,
777 local_data: None,
778 password_history: None,
779 deleted_date: None,
780 data: None,
781 }];
782
783 let api_client = ApiClient::new_mocked(|mock| {
784 mock.accounts_key_management_api
786 .expect_rotate_user_keys()
787 .never();
788 });
789
790 let state_bridge = make_state_bridge();
791 let result = internal_rotate_user_keys(
792 &key_store,
793 &api_client,
794 &state_bridge,
795 None,
796 RotateUserKeysRequest {
797 key_rotation_method: KeyRotationMethod::Password {
798 password: "test_password".to_string(),
799 },
800 trusted_organization_public_keys: vec![],
801 trusted_emergency_access_public_keys: vec![],
802 upgrade_token_action: UpgradeTokenAction::Skip,
803 },
804 sync.wrapped_account_cryptographic_state.clone(),
805 sync,
806 )
807 .await;
808
809 assert!(matches!(result, Err(RotateUserKeysError::OldAttachments)));
810 if let ApiClient::Mock(mut mock) = api_client {
811 mock.accounts_key_management_api.checkpoint();
812 }
813 }
814
815 #[tokio::test]
816 async fn test_rotate_user_keys_key_connector_success() {
817 let (key_store, sync) = make_test_key_store_and_synced_data();
818
819 let key_connector_key = KeyConnectorKey::make();
820 let key_connector_api_client = bitwarden_api_key_connector::apis::ApiClient::new_mocked(
821 |mock| {
822 let key_connector_key_clone = key_connector_key.clone();
823 mock.user_keys_api
824 .expect_get_user_key()
825 .once()
826 .returning(move || {
827 let encoded: bitwarden_encoding::B64 =
828 key_connector_key_clone.clone().into();
829 Ok(
830 bitwarden_api_key_connector::models::user_key_response_model::UserKeyResponseModel {
831 key: encoded.to_string(),
832 },
833 )
834 });
835 },
836 );
837
838 let api_client = ApiClient::new_mocked(|mock| {
839 mock.accounts_key_management_api
840 .expect_rotate_user_keys()
841 .once()
842 .returning(|req| {
843 let req = req.expect("request body should be present");
844 assert!(
845 req.unlock_method_data
846 .key_connector_key_wrapped_user_key
847 .is_some(),
848 "key_connector_key_wrapped_user_key should be set for KC rotation"
849 );
850 assert!(
851 req.unlock_method_data.master_password_unlock_data.is_none(),
852 "master_password_unlock_data should be None for KC rotation"
853 );
854 Ok(())
855 });
856 });
857
858 let state_bridge = make_state_bridge();
859 let result = internal_rotate_user_keys(
860 &key_store,
861 &api_client,
862 &state_bridge,
863 Some(&key_connector_api_client),
864 RotateUserKeysRequest {
865 key_rotation_method: KeyRotationMethod::KeyConnector {
866 key_connector_url: "https://kc.example.com".to_string(),
867 },
868 trusted_organization_public_keys: vec![],
869 trusted_emergency_access_public_keys: vec![],
870 upgrade_token_action: UpgradeTokenAction::Skip,
871 },
872 sync.wrapped_account_cryptographic_state.clone(),
873 sync,
874 )
875 .await;
876
877 assert!(result.is_ok());
878 if let ApiClient::Mock(mut mock) = api_client {
879 mock.accounts_key_management_api.checkpoint();
880 }
881 if let bitwarden_api_key_connector::apis::ApiClient::Mock(mut mock) =
882 key_connector_api_client
883 {
884 mock.user_keys_api.checkpoint();
885 }
886 }
887
888 #[tokio::test]
889 async fn test_rotate_user_keys_key_connector_api_failure() {
890 let (key_store, sync) = make_test_key_store_and_synced_data();
891
892 let key_connector_api_client =
893 bitwarden_api_key_connector::apis::ApiClient::new_mocked(|mock| {
894 mock.user_keys_api
895 .expect_get_user_key()
896 .once()
897 .returning(move || {
898 Err(bitwarden_api_key_connector::apis::Error::ResponseError(
899 bitwarden_api_key_connector::apis::ResponseContent {
900 status: reqwest::StatusCode::INTERNAL_SERVER_ERROR,
901 content: "Server Error".to_string(),
902 },
903 ))
904 });
905 });
906
907 let api_client = ApiClient::new_mocked(|mock| {
908 mock.accounts_key_management_api
909 .expect_rotate_user_keys()
910 .never();
911 });
912
913 let state_bridge = make_state_bridge();
914 let result = internal_rotate_user_keys(
915 &key_store,
916 &api_client,
917 &state_bridge,
918 Some(&key_connector_api_client),
919 RotateUserKeysRequest {
920 key_rotation_method: KeyRotationMethod::KeyConnector {
921 key_connector_url: "https://kc.example.com".to_string(),
922 },
923 trusted_organization_public_keys: vec![],
924 trusted_emergency_access_public_keys: vec![],
925 upgrade_token_action: UpgradeTokenAction::Skip,
926 },
927 sync.wrapped_account_cryptographic_state.clone(),
928 sync,
929 )
930 .await;
931
932 assert!(matches!(result, Err(RotateUserKeysError::KeyConnectorApi)));
933 if let ApiClient::Mock(mut mock) = api_client {
934 mock.accounts_key_management_api.checkpoint();
935 }
936 if let bitwarden_api_key_connector::apis::ApiClient::Mock(mut mock) =
937 key_connector_api_client
938 {
939 mock.user_keys_api.checkpoint();
940 }
941 }
942
943 #[tokio::test]
944 async fn test_rotate_user_keys_v1_to_v2_upgrade_defers_organization_account_recovery() {
945 let (key_store, sync) = make_test_key_store_and_synced_data_with_organization();
946 let organization_id = sync.organization_memberships[0].organization_id;
947
948 let api_client = ApiClient::new_mocked(|mock| {
949 mock.accounts_key_management_api
950 .expect_rotate_user_keys()
951 .once()
952 .returning(move |req| {
953 let req = req.expect("request body should be present");
954 assert!(
955 req.unlock_data.v2_upgrade_token.is_some(),
956 "without the token, admins cannot update account recovery"
957 );
958
959 let account_recovery = req
960 .unlock_data
961 .organization_account_recovery_unlock_data
962 .as_ref()
963 .expect("account recovery data should be present");
964 assert_eq!(account_recovery.len(), 1);
965 assert_eq!(account_recovery[0].organization_id, organization_id);
966 assert!(
967 account_recovery[0].reset_password_key.is_none(),
968 "account recovery is left for organization admins to update"
969 );
970 Ok(())
971 });
972 });
973
974 let state_bridge = make_state_bridge();
975 let result = internal_rotate_user_keys(
976 &key_store,
977 &api_client,
978 &state_bridge,
979 None,
980 RotateUserKeysRequest {
981 key_rotation_method: KeyRotationMethod::Password {
982 password: "test_password".to_string(),
983 },
984 trusted_organization_public_keys: vec![],
985 trusted_emergency_access_public_keys: vec![],
986 upgrade_token_action: UpgradeTokenAction::CreateIfNeeded,
987 },
988 sync.wrapped_account_cryptographic_state.clone(),
989 sync,
990 )
991 .await;
992
993 assert!(result.is_ok());
994 if let ApiClient::Mock(mut mock) = api_client {
995 mock.accounts_key_management_api.checkpoint();
996 }
997 }
998
999 #[tokio::test]
1000 async fn test_rotate_user_keys_v1_to_v2_upgrade_ignores_trusted_organization_public_keys() {
1001 let (key_store, sync) = make_test_key_store_and_synced_data_with_organization();
1002 let organization_public_key = sync.organization_memberships[0].public_key.clone();
1003
1004 let api_client = ApiClient::new_mocked(|mock| {
1005 mock.accounts_key_management_api
1006 .expect_rotate_user_keys()
1007 .once()
1008 .returning(move |req| {
1009 let req = req.expect("request body should be present");
1010 let account_recovery = req
1011 .unlock_data
1012 .organization_account_recovery_unlock_data
1013 .as_ref()
1014 .expect("account recovery data should be present");
1015 assert_eq!(account_recovery.len(), 1);
1016 assert!(
1017 account_recovery[0].reset_password_key.is_none(),
1018 "trusting a key does not change who updates account recovery"
1019 );
1020 Ok(())
1021 });
1022 });
1023
1024 let state_bridge = make_state_bridge();
1025 let result = internal_rotate_user_keys(
1026 &key_store,
1027 &api_client,
1028 &state_bridge,
1029 None,
1030 RotateUserKeysRequest {
1031 key_rotation_method: KeyRotationMethod::Password {
1032 password: "test_password".to_string(),
1033 },
1034 trusted_organization_public_keys: vec![organization_public_key],
1036 trusted_emergency_access_public_keys: vec![],
1037 upgrade_token_action: UpgradeTokenAction::CreateIfNeeded,
1038 },
1039 sync.wrapped_account_cryptographic_state.clone(),
1040 sync,
1041 )
1042 .await;
1043
1044 assert!(result.is_ok());
1045 if let ApiClient::Mock(mut mock) = api_client {
1046 mock.accounts_key_management_api.checkpoint();
1047 }
1048 }
1049
1050 #[tokio::test]
1051 async fn test_rotate_user_keys_untrusted_organization_without_upgrade_token_returns_error() {
1052 let (key_store, sync) = make_test_key_store_and_synced_data_with_organization();
1053
1054 let api_client = ApiClient::new_mocked(|mock| {
1055 mock.accounts_key_management_api
1056 .expect_rotate_user_keys()
1057 .never();
1058 });
1059
1060 let state_bridge = make_state_bridge();
1061 let result = internal_rotate_user_keys(
1062 &key_store,
1063 &api_client,
1064 &state_bridge,
1065 None,
1066 RotateUserKeysRequest {
1067 key_rotation_method: KeyRotationMethod::Password {
1068 password: "test_password".to_string(),
1069 },
1070 trusted_organization_public_keys: vec![],
1071 trusted_emergency_access_public_keys: vec![],
1072 upgrade_token_action: UpgradeTokenAction::Skip,
1073 },
1074 sync.wrapped_account_cryptographic_state.clone(),
1075 sync,
1076 )
1077 .await;
1078
1079 assert!(matches!(result, Err(RotateUserKeysError::UntrustedKey)));
1080 if let ApiClient::Mock(mut mock) = api_client {
1081 mock.accounts_key_management_api.checkpoint();
1082 }
1083 }
1084}