Skip to main content

bitwarden_user_crypto_management/key_rotation/
rotate_user_keys.rs

1//! Client implementation for rotating user keys without a password change.
2use 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    /// Master password user, key rotation without a password change.
30    Password { password: String },
31    /// Key Connector user, key rotation without a password change.
32    KeyConnector { key_connector_url: String },
33    /// TDE user, key rotation without a password change.
34    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 creating and sending an upgrade token to the server.
42    Skip,
43    /// Creates an upgrade token for V1 -> V2 key rotations.
44    /// For V2 -> V2 rotations, no upgrade token is needed.
45    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    /// Organization public keys the user confirmed as trusted. Only needed when the rotation
55    /// shares the new user key for account recovery.
56    pub trusted_organization_public_keys: Vec<PublicKey>,
57    pub upgrade_token_action: UpgradeTokenAction,
58}
59
60#[bitwarden_ffi::wasm_export]
61impl UserCryptoManagementClient {
62    /// Rotates the user's encryption keys without a password change.
63    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
106/// Data that needs to be written to local state after the key rotation
107/// was successfully posted to the server
108struct 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    // Fail early if any cipher has old attachments that would become irrecoverable
125    check_for_old_attachments(&sync.ciphers)?;
126
127    // For Key Connector users, fetch the existing KC key from the KC server.
128    // This must happen before the synchronous key store scope below.
129    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    // Create a separate scope so that the mutable context is not held across the await point
149    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                // Only V2 (COSE-encoded) user keys carry a key id; V1 rotations omit the field.
224                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        // Important: A full sync MUST be triggered after the key rotation to make sure all unlock
263        // methods are accurate
264    }
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    /// Builds a V1 account that is enrolled in account recovery for a single organization.
335    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        // Add a cipher with an old attachment (key is None)
741        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, // Old attachment - no per-attachment key
773            }]),
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            // Rotation API should never be called
785            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                // Older callers may still send organization keys. That must keep working.
1035                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}