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