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