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;
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    /// 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#[bitwarden_ffi::wasm_record]
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#[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    /// 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#[bitwarden_ffi::wasm_export]
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        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    /// Moves this unlocked client from the V1 user key to the V2 key it just rotated to. Without
118    /// this the SDK keeps the V1 key in memory until the next sync, while state already holds V2.
119    #[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        // The server already rotated, so failing the call would make the client retry a
131        // rotation that cannot be repeated. The next sync re-initializes the client.
132        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/// What a V1 to V2 rotation hands the rotating client to re-initialize itself from.
142#[cfg_attr(not(any(feature = "uniffi", feature = "wasm")), allow(dead_code))]
143struct V2Upgrade {
144    account_cryptographic_state: WrappedAccountCryptographicState,
145    upgrade_token: V2UpgradeToken,
146}
147
148/// Data that needs to be written to local state after the key rotation
149/// was successfully posted to the server
150struct 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    // Fail early if any cipher has old attachments that would become irrecoverable
167    check_for_old_attachments(&sync.ciphers)?;
168
169    // For Key Connector users, fetch the existing KC key from the KC server.
170    // This must happen before the synchronous key store scope below.
171    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    // Create a separate scope so that the mutable context is not held across the await point
191    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                // Only V2 (COSE-encoded) user keys carry a key id; V1 rotations omit the field.
266                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    // Important: A full sync MUST be triggered after the key rotation to make sure all unlock
308    // methods are accurate
309
310    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    /// Builds a V1 account that is enrolled in account recovery for a single organization.
382    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        // Add a cipher with an old attachment (key is None)
788        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, // Old attachment - no per-attachment key
820            }]),
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            // Rotation API should never be called
832            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                // Older callers may still send organization keys. That must keep working.
1082                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}