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, instrument};
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    pub trusted_organization_public_keys: Vec<PublicKey>,
59    pub upgrade_token_action: UpgradeTokenAction,
60}
61
62#[cfg_attr(feature = "wasm", wasm_bindgen)]
63impl UserCryptoManagementClient {
64    /// Rotates the user's encryption keys without a password change.
65    pub async fn rotate_user_keys(
66        &self,
67        request: RotateUserKeysRequest,
68    ) -> Result<(), RotateUserKeysError> {
69        let api_client = &self.client.internal.get_api_configurations().api_client;
70        let key_store = self.client.internal.get_key_store();
71
72        let sync = sync_current_account_data(api_client)
73            .await
74            .map_err(|_| RotateUserKeysError::Api)?;
75
76        let wrapped_account_cryptographic_state = self
77            .regenerate_public_key_encryption_key_pair_if_needed_with_ciphers(&sync.ciphers)
78            .await
79            .map_err(|_| RotateUserKeysError::Crypto)?
80            .unwrap_or_else(|| sync.wrapped_account_cryptographic_state.clone());
81
82        let key_connector_api_client =
83            if let KeyRotationMethod::KeyConnector { key_connector_url } =
84                &request.key_rotation_method
85            {
86                Some(
87                    self.client
88                        .internal
89                        .get_key_connector_client(key_connector_url.clone()),
90                )
91            } else {
92                None
93            };
94
95        internal_rotate_user_keys(
96            key_store,
97            api_client,
98            &self.client.km_state_bridge(),
99            key_connector_api_client.as_ref(),
100            request,
101            wrapped_account_cryptographic_state,
102            sync,
103        )
104        .await
105    }
106}
107
108/// Data that needs to be written to local state after the key rotation
109/// was successfully posted to the server
110struct StateUpdate {
111    user_key: SymmetricCryptoKey,
112    account_cryptographic_state: WrappedAccountCryptographicState,
113    upgrade_token: Option<V2UpgradeToken>,
114}
115
116#[instrument(name = "rotate_user_keys", level = "info", skip_all, err)]
117async fn internal_rotate_user_keys(
118    key_store: &KeyStore<KeySlotIds>,
119    api_client: &bitwarden_api_api::apis::ApiClient,
120    state_bridge: &bitwarden_core::key_management::state_bridge::StateBridgeClient,
121    key_connector_api_client: Option<&bitwarden_api_key_connector::apis::ApiClient>,
122    request: RotateUserKeysRequest,
123    wrapped_account_cryptographic_state: WrappedAccountCryptographicState,
124    sync: SyncedAccountData,
125) -> Result<(), RotateUserKeysError> {
126    // Fail early if any cipher has old attachments that would become irrecoverable
127    check_for_old_attachments(&sync.ciphers)?;
128
129    // For Key Connector users, fetch the existing KC key from the KC server.
130    // This must happen before the synchronous key store scope below.
131    let key_connector_key = if matches!(
132        request.key_rotation_method,
133        KeyRotationMethod::KeyConnector { .. }
134    ) {
135        let key_connector_client =
136            key_connector_api_client.ok_or(RotateUserKeysError::KeyConnectorApi)?;
137        info!("Fetching Key Connector key for key rotation");
138        let response = key_connector_client
139            .user_keys_api()
140            .get_user_key()
141            .await
142            .map_err(|_| RotateUserKeysError::KeyConnectorApi)?;
143        let key_connector_key =
144            KeyConnectorKey::try_from(response).map_err(|_| RotateUserKeysError::Crypto)?;
145        Some(key_connector_key)
146    } else {
147        None
148    };
149
150    // Create a separate scope so that the mutable context is not held across the await point
151    let (post_request, state_bridge_update) = {
152        let mut ctx = key_store.context_mut();
153
154        let rotation_context = make_rotation_context(
155            &sync,
156            request.trusted_organization_public_keys.as_slice(),
157            request.trusted_emergency_access_public_keys.as_slice(),
158            &mut ctx,
159        )?;
160
161        info!("Rotating account cryptographic state for user key rotation");
162        let wrapped_account_cryptographic_state = rotate_account_cryptographic_state(
163            &wrapped_account_cryptographic_state,
164            &rotation_context.current_user_key_id,
165            &rotation_context.new_user_key_id,
166            &mut ctx,
167        )
168        .map_err(|_| RotateUserKeysError::Crypto)?;
169        let wrapped_account_cryptographic_state_request_model =
170            account_cryptographic_state_to_wrapped_model(
171                &wrapped_account_cryptographic_state,
172                &rotation_context.new_user_key_id,
173                &mut ctx,
174            )
175            .map_err(|_| RotateUserKeysError::Crypto)?;
176
177        info!("Re-encrypting account data for user key rotation");
178        let account_data_model = reencrypt_data(
179            sync.folders.as_slice(),
180            sync.ciphers.as_slice(),
181            sync.sends.as_slice(),
182            rotation_context.current_user_key_id,
183            rotation_context.new_user_key_id,
184            &mut ctx,
185        )
186        .map_err(|_| RotateUserKeysError::Crypto)?;
187
188        info!("Re-encrypting account primary unlock method for user key rotation");
189        let unlock_method_input = PrimaryUnlockMethod::from_key_rotation_method(
190            request.key_rotation_method,
191            &sync,
192            key_connector_key,
193        )?;
194        let unlock_method_data = reencrypt_unlock_method_data(
195            unlock_method_input,
196            rotation_context.new_user_key_id,
197            &mut ctx,
198        )
199        .map_err(|_| RotateUserKeysError::Crypto)?;
200
201        info!("Re-encrypting account common unlock data for user key rotation");
202        let common_unlock_data = reencrypt_common_unlock_data(
203            ReencryptCommonUnlockDataInput {
204                trusted_organization_keys: rotation_context.v1_organization_memberships,
205                trusted_emergency_access_keys: rotation_context.v1_emergency_access_memberships,
206                webauthn_credentials: sync.passkeys,
207                trusted_devices: sync.trusted_devices,
208            },
209            rotation_context.current_user_key_id,
210            rotation_context.new_user_key_id,
211            request.upgrade_token_action,
212            &mut ctx,
213        )
214        .map_err(|_| RotateUserKeysError::Crypto)?;
215
216        (
217            RotateUserKeysRequestModel {
218                wrapped_account_cryptographic_state: Box::new(
219                    wrapped_account_cryptographic_state_request_model,
220                ),
221                account_data: Box::new(account_data_model),
222                unlock_data: Box::new(common_unlock_data.clone()),
223                unlock_method_data: Box::new(unlock_method_data),
224            },
225            StateUpdate {
226                #[allow(deprecated)]
227                user_key: ctx
228                    .dangerous_get_symmetric_key(rotation_context.new_user_key_id)
229                    .map_err(|_| RotateUserKeysError::Crypto)?
230                    .to_owned(),
231                account_cryptographic_state: wrapped_account_cryptographic_state,
232                upgrade_token: common_unlock_data
233                    .v2_upgrade_token
234                    .clone()
235                    .map(|t| (*t).try_into())
236                    .transpose()
237                    .map_err(|_| RotateUserKeysError::Crypto)?,
238            },
239        )
240    };
241
242    info!("Posting rotated user account keys and data to server");
243    api_client
244        .accounts_key_management_api()
245        .rotate_user_keys(Some(post_request))
246        .await
247        .map_err(|_| RotateUserKeysError::Api)?;
248    info!("Successfully rotated user account keys and data");
249
250    if let Some(upgrade_token) = state_bridge_update.upgrade_token.as_ref() {
251        info!("Writing new cryptographic data to state");
252        state_bridge
253            .set_account_cryptographic_state(&state_bridge_update.account_cryptographic_state)
254            .await;
255        state_bridge.set_v2_upgrade_token(upgrade_token).await;
256        state_bridge
257            .set_user_key(&state_bridge_update.user_key)
258            .await;
259        // Important: A full sync MUST be triggered after the key rotation to make sure all unlock
260        // methods are accurate
261    }
262
263    Ok(())
264}
265
266#[cfg(test)]
267mod tests {
268    use std::str::FromStr;
269
270    use bitwarden_api_api::{apis::ApiClient, models::UnlockMethod};
271    use bitwarden_core::{
272        Client,
273        key_management::{
274            KeySlotIds, PrivateKeySlotId, SymmetricKeySlotId,
275            account_cryptographic_state::WrappedAccountCryptographicState,
276            state_bridge::{StateBridgeClient, test_support::InMemoryStateBridge},
277        },
278    };
279    use bitwarden_crypto::{
280        Decryptable, EncString, Kdf, KeyStore, PublicKeyEncryptionAlgorithm, SymmetricKeyAlgorithm,
281        UnsignedSharedKey,
282    };
283    use bitwarden_vault::{Attachment, Cipher, CipherType};
284    use chrono::DateTime;
285
286    use super::*;
287    use crate::key_rotation::partial_rotateable_keyset::PartialRotateableKeyset;
288
289    fn make_state_bridge() -> StateBridgeClient {
290        let client = Client::new(None);
291        let bridge = client.km_state_bridge();
292        bridge.register_bridge(Box::new(InMemoryStateBridge::default()));
293        bridge
294    }
295
296    fn make_test_key_store_and_synced_data() -> (KeyStore<KeySlotIds>, SyncedAccountData) {
297        let store: KeyStore<KeySlotIds> = KeyStore::default();
298        let wrapped_private_key = {
299            let mut ctx = store.context_mut();
300            let user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
301            let _ = ctx.persist_symmetric_key(user_key, SymmetricKeySlotId::User);
302            let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
303            ctx.wrap_private_key(SymmetricKeySlotId::User, private_key)
304                .unwrap()
305        };
306
307        let sync = SyncedAccountData {
308            wrapped_account_cryptographic_state: WrappedAccountCryptographicState::V1 {
309                private_key: wrapped_private_key,
310            },
311            folders: vec![],
312            ciphers: vec![],
313            sends: vec![],
314            emergency_access_memberships: vec![],
315            organization_memberships: vec![],
316            trusted_devices: vec![],
317            passkeys: vec![],
318            kdf_and_salt: Some((
319                Kdf::PBKDF2 {
320                    iterations: std::num::NonZeroU32::new(600000).unwrap(),
321                },
322                "test_salt".to_string(),
323            )),
324        };
325
326        (store, sync)
327    }
328
329    fn make_test_key_store_and_synced_data_with_trusted_devices()
330    -> (KeyStore<KeySlotIds>, SyncedAccountData, Vec<u8>) {
331        let store: KeyStore<KeySlotIds> = KeyStore::default();
332        let (trusted_device_keyset, wrapped_private_key, public_key) = {
333            let mut ctx = store.context_mut();
334            let user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
335            let _ = ctx.persist_symmetric_key(user_key, SymmetricKeySlotId::User);
336            let (trusted_device_keyset, device_private_key) =
337                PartialRotateableKeyset::make_test_keyset(SymmetricKeySlotId::User, &mut ctx);
338            let _ = ctx.persist_private_key(device_private_key, PrivateKeySlotId::UserPrivateKey);
339            let wrapped_private_key = ctx
340                .wrap_private_key(SymmetricKeySlotId::User, PrivateKeySlotId::UserPrivateKey)
341                .unwrap();
342            (
343                trusted_device_keyset,
344                wrapped_private_key,
345                ctx.get_public_key(PrivateKeySlotId::UserPrivateKey)
346                    .expect("Retrieving the public key should work."),
347            )
348        };
349
350        let sync = SyncedAccountData {
351            wrapped_account_cryptographic_state: WrappedAccountCryptographicState::V1 {
352                private_key: wrapped_private_key,
353            },
354            folders: vec![],
355            ciphers: vec![],
356            sends: vec![],
357            emergency_access_memberships: vec![],
358            organization_memberships: vec![],
359            trusted_devices: vec![trusted_device_keyset],
360            passkeys: vec![],
361            kdf_and_salt: Some((
362                Kdf::PBKDF2 {
363                    iterations: std::num::NonZeroU32::new(600000).unwrap(),
364                },
365                "test_salt".to_string(),
366            )),
367        };
368
369        (
370            store,
371            sync,
372            public_key
373                .to_der()
374                .expect("Generating DER serialization should work")
375                .to_vec(),
376        )
377    }
378
379    #[tokio::test]
380    async fn test_rotate_user_keys_tde_success_rotates_common_unlock_data() {
381        let (key_store, sync, public_key_der) =
382            make_test_key_store_and_synced_data_with_trusted_devices();
383        let key_store_clone = key_store.clone();
384
385        let api_client = ApiClient::new_mocked(|mock| {
386            mock.accounts_key_management_api
387                .expect_rotate_user_keys()
388                .once()
389                .returning(move |req| {
390                    let req = req.expect("request body should be present");
391                    assert_eq!(req.unlock_method_data.unlock_method, UnlockMethod::Tde);
392                    assert!(req.unlock_method_data.master_password_unlock_data.is_none());
393                    assert!(
394                        req.unlock_method_data
395                            .key_connector_key_wrapped_user_key
396                            .is_none()
397                    );
398
399                    let device_unlock_data = req
400                        .unlock_data
401                        .device_key_unlock_data
402                        .expect("device unlock data should be present");
403                    assert_eq!(device_unlock_data.len(), 1);
404                    let rotated_device = &device_unlock_data[0];
405
406                    let encrypted_user_key: UnsignedSharedKey = rotated_device
407                        .encrypted_user_key
408                        .parse()
409                        .expect("encrypted user key should parse");
410                    let encrypted_public_key: EncString = rotated_device
411                        .encrypted_public_key
412                        .parse()
413                        .expect("encrypted public key should parse");
414                    let mut ctx = key_store_clone.context_mut();
415                    let rotated_user_key_id = encrypted_user_key
416                        .decapsulate(PrivateKeySlotId::UserPrivateKey, &mut ctx)
417                        .expect("rotated device user key should decapsulate");
418                    let decrypted_public_key: Vec<u8> = encrypted_public_key
419                        .decrypt(&mut ctx, rotated_user_key_id)
420                        .expect("rotated device public key should decrypt");
421                    assert_eq!(decrypted_public_key, public_key_der);
422                    Ok(())
423                });
424        });
425
426        let state_bridge = make_state_bridge();
427        let result = internal_rotate_user_keys(
428            &key_store,
429            &api_client,
430            &state_bridge,
431            None,
432            RotateUserKeysRequest {
433                key_rotation_method: KeyRotationMethod::Tde,
434                trusted_organization_public_keys: vec![],
435                trusted_emergency_access_public_keys: vec![],
436                upgrade_token_action: UpgradeTokenAction::Skip,
437            },
438            sync.wrapped_account_cryptographic_state.clone(),
439            sync,
440        )
441        .await;
442
443        assert!(result.is_ok());
444        if let ApiClient::Mock(mut mock) = api_client {
445            mock.accounts_key_management_api.checkpoint();
446        }
447    }
448
449    #[tokio::test]
450    async fn test_rotate_user_keys_master_password_success() {
451        let (key_store, sync) = make_test_key_store_and_synced_data();
452        let api_client = ApiClient::new_mocked(|mock| {
453            mock.accounts_key_management_api
454                .expect_rotate_user_keys()
455                .once()
456                .returning(|_| Ok(()));
457        });
458
459        let state_bridge = make_state_bridge();
460        let result = internal_rotate_user_keys(
461            &key_store,
462            &api_client,
463            &state_bridge,
464            None,
465            RotateUserKeysRequest {
466                key_rotation_method: KeyRotationMethod::Password {
467                    password: "test_password".to_string(),
468                },
469                trusted_organization_public_keys: vec![],
470                trusted_emergency_access_public_keys: vec![],
471                upgrade_token_action: UpgradeTokenAction::Skip,
472            },
473            sync.wrapped_account_cryptographic_state.clone(),
474            sync,
475        )
476        .await;
477
478        assert!(result.is_ok());
479        if let ApiClient::Mock(mut mock) = api_client {
480            mock.accounts_key_management_api.checkpoint();
481        }
482    }
483
484    #[tokio::test]
485    async fn test_rotate_user_keys_post_api_failure_returns_api_error() {
486        let (key_store, sync) = make_test_key_store_and_synced_data();
487        let api_client = ApiClient::new_mocked(|mock| {
488            mock.accounts_key_management_api
489                .expect_rotate_user_keys()
490                .once()
491                .returning(|_| {
492                    Err(serde_json::Error::io(std::io::Error::other("API error")).into())
493                });
494        });
495
496        let state_bridge = make_state_bridge();
497        let result = internal_rotate_user_keys(
498            &key_store,
499            &api_client,
500            &state_bridge,
501            None,
502            RotateUserKeysRequest {
503                key_rotation_method: KeyRotationMethod::Password {
504                    password: "test_password".to_string(),
505                },
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!(matches!(result, Err(RotateUserKeysError::Api)));
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_upgrade_token_action_skip_omits_token() {
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(|req| {
529                    let req = req.expect("request body should be present");
530                    assert!(
531                        req.unlock_data.v2_upgrade_token.is_none(),
532                        "upgrade_token_action Skip, should omit the v2_upgrade_token"
533                    );
534                    Ok(())
535                });
536        });
537
538        let state_bridge = make_state_bridge();
539        let result = internal_rotate_user_keys(
540            &key_store,
541            &api_client,
542            &state_bridge,
543            None,
544            RotateUserKeysRequest {
545                key_rotation_method: KeyRotationMethod::Password {
546                    password: "test_password".to_string(),
547                },
548                trusted_organization_public_keys: vec![],
549                trusted_emergency_access_public_keys: vec![],
550                upgrade_token_action: UpgradeTokenAction::Skip,
551            },
552            sync.wrapped_account_cryptographic_state.clone(),
553            sync,
554        )
555        .await;
556
557        assert!(result.is_ok());
558        if let ApiClient::Mock(mut mock) = api_client {
559            mock.accounts_key_management_api.checkpoint();
560        }
561    }
562
563    #[tokio::test]
564    async fn test_rotate_user_keys_upgrade_token_action_create_if_needed_includes_token() {
565        let (key_store, sync) = make_test_key_store_and_synced_data();
566        let api_client = ApiClient::new_mocked(|mock| {
567            mock.accounts_key_management_api
568                .expect_rotate_user_keys()
569                .once()
570                .returning(|req| {
571                    let req = req.expect("request body should be present");
572                    assert!(
573                        req.unlock_data.v2_upgrade_token.is_some(),
574                        "upgrade_token_action CreateIfNeeded, should include a v2_upgrade_token for V1 -> V2 rotations"
575                    );
576                    Ok(())
577                });
578        });
579
580        let state_bridge = make_state_bridge();
581        let result = internal_rotate_user_keys(
582            &key_store,
583            &api_client,
584            &state_bridge,
585            None,
586            RotateUserKeysRequest {
587                key_rotation_method: KeyRotationMethod::Password {
588                    password: "test_password".to_string(),
589                },
590                trusted_organization_public_keys: vec![],
591                trusted_emergency_access_public_keys: vec![],
592                upgrade_token_action: UpgradeTokenAction::CreateIfNeeded,
593            },
594            sync.wrapped_account_cryptographic_state.clone(),
595            sync,
596        )
597        .await;
598
599        assert!(result.is_ok());
600        if let ApiClient::Mock(mut mock) = api_client {
601            mock.accounts_key_management_api.checkpoint();
602        }
603    }
604
605    #[tokio::test]
606    async fn test_rotate_user_keys_writes_state_when_upgrade_token_present() {
607        let (key_store, sync) = make_test_key_store_and_synced_data();
608        let api_client = ApiClient::new_mocked(|mock| {
609            mock.accounts_key_management_api
610                .expect_rotate_user_keys()
611                .once()
612                .returning(|_| Ok(()));
613        });
614
615        let state_bridge = make_state_bridge();
616        assert!(state_bridge.get_v2_upgrade_token().await.is_none());
617        assert!(
618            state_bridge
619                .get_account_cryptographic_state()
620                .await
621                .is_none()
622        );
623        assert!(state_bridge.get_user_key().await.is_none());
624
625        let result = internal_rotate_user_keys(
626            &key_store,
627            &api_client,
628            &state_bridge,
629            None,
630            RotateUserKeysRequest {
631                key_rotation_method: KeyRotationMethod::Password {
632                    password: "test_password".to_string(),
633                },
634                trusted_organization_public_keys: vec![],
635                trusted_emergency_access_public_keys: vec![],
636                upgrade_token_action: UpgradeTokenAction::CreateIfNeeded,
637            },
638            sync.wrapped_account_cryptographic_state.clone(),
639            sync,
640        )
641        .await;
642
643        assert!(result.is_ok());
644        assert!(
645            state_bridge.get_v2_upgrade_token().await.is_some(),
646            "state bridge should hold the v2 upgrade token after V1 -> V2 rotation"
647        );
648        assert!(
649            state_bridge
650                .get_account_cryptographic_state()
651                .await
652                .is_some(),
653            "state bridge should hold the rotated account cryptographic state"
654        );
655        assert!(
656            state_bridge.get_user_key().await.is_some(),
657            "state bridge should hold the rotated user key"
658        );
659        if let ApiClient::Mock(mut mock) = api_client {
660            mock.accounts_key_management_api.checkpoint();
661        }
662    }
663
664    #[tokio::test]
665    async fn test_rotate_user_keys_skips_state_writes_when_no_upgrade_token() {
666        let (key_store, sync) = make_test_key_store_and_synced_data();
667        let api_client = ApiClient::new_mocked(|mock| {
668            mock.accounts_key_management_api
669                .expect_rotate_user_keys()
670                .once()
671                .returning(|_| Ok(()));
672        });
673
674        let state_bridge = make_state_bridge();
675        let result = internal_rotate_user_keys(
676            &key_store,
677            &api_client,
678            &state_bridge,
679            None,
680            RotateUserKeysRequest {
681                key_rotation_method: KeyRotationMethod::Password {
682                    password: "test_password".to_string(),
683                },
684                trusted_organization_public_keys: vec![],
685                trusted_emergency_access_public_keys: vec![],
686                upgrade_token_action: UpgradeTokenAction::Skip,
687            },
688            sync.wrapped_account_cryptographic_state.clone(),
689            sync,
690        )
691        .await;
692
693        assert!(result.is_ok());
694        assert!(
695            state_bridge.get_v2_upgrade_token().await.is_none(),
696            "without an upgrade token, the state bridge must not be written"
697        );
698        assert!(
699            state_bridge
700                .get_account_cryptographic_state()
701                .await
702                .is_none()
703        );
704        assert!(state_bridge.get_user_key().await.is_none());
705        if let ApiClient::Mock(mut mock) = api_client {
706            mock.accounts_key_management_api.checkpoint();
707        }
708    }
709
710    #[tokio::test]
711    async fn test_rotate_user_keys_old_attachments_returns_error() {
712        let (key_store, mut sync) = make_test_key_store_and_synced_data();
713        let enc_string = "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=";
714
715        // Add a cipher with an old attachment (key is None)
716        sync.ciphers = vec![Cipher {
717            id: None,
718            organization_id: None,
719            folder_id: None,
720            collection_ids: vec![],
721            r#type: CipherType::Login,
722            login: None,
723            identity: None,
724            card: None,
725            secure_note: None,
726            ssh_key: None,
727            bank_account: None,
728            drivers_license: None,
729            passport: None,
730            favorite: false,
731            reprompt: Default::default(),
732            organization_use_totp: false,
733            edit: false,
734            permissions: None,
735            view_password: false,
736            name: enc_string.parse().unwrap(),
737            revision_date: DateTime::from_str("2024-01-01T00:00:00Z").unwrap(),
738            archived_date: None,
739            creation_date: DateTime::from_str("2024-01-01T00:00:00Z").unwrap(),
740            attachments: Some(vec![Attachment {
741                id: None,
742                url: None,
743                size: None,
744                size_name: None,
745                file_name: None,
746                key: None, // Old attachment - no per-attachment key
747            }]),
748            fields: None,
749            key: None,
750            notes: None,
751            local_data: None,
752            password_history: None,
753            deleted_date: None,
754            data: None,
755        }];
756
757        let api_client = ApiClient::new_mocked(|mock| {
758            // Rotation API should never be called
759            mock.accounts_key_management_api
760                .expect_rotate_user_keys()
761                .never();
762        });
763
764        let state_bridge = make_state_bridge();
765        let result = internal_rotate_user_keys(
766            &key_store,
767            &api_client,
768            &state_bridge,
769            None,
770            RotateUserKeysRequest {
771                key_rotation_method: KeyRotationMethod::Password {
772                    password: "test_password".to_string(),
773                },
774                trusted_organization_public_keys: vec![],
775                trusted_emergency_access_public_keys: vec![],
776                upgrade_token_action: UpgradeTokenAction::Skip,
777            },
778            sync.wrapped_account_cryptographic_state.clone(),
779            sync,
780        )
781        .await;
782
783        assert!(matches!(result, Err(RotateUserKeysError::OldAttachments)));
784        if let ApiClient::Mock(mut mock) = api_client {
785            mock.accounts_key_management_api.checkpoint();
786        }
787    }
788
789    #[tokio::test]
790    async fn test_rotate_user_keys_key_connector_success() {
791        let (key_store, sync) = make_test_key_store_and_synced_data();
792
793        let key_connector_key = KeyConnectorKey::make();
794        let key_connector_api_client = bitwarden_api_key_connector::apis::ApiClient::new_mocked(
795            |mock| {
796                let key_connector_key_clone = key_connector_key.clone();
797                mock.user_keys_api
798                    .expect_get_user_key()
799                    .once()
800                    .returning(move || {
801                        let encoded: bitwarden_encoding::B64 =
802                            key_connector_key_clone.clone().into();
803                        Ok(
804                            bitwarden_api_key_connector::models::user_key_response_model::UserKeyResponseModel {
805                                key: encoded.to_string(),
806                            },
807                        )
808                    });
809            },
810        );
811
812        let api_client = ApiClient::new_mocked(|mock| {
813            mock.accounts_key_management_api
814                .expect_rotate_user_keys()
815                .once()
816                .returning(|req| {
817                    let req = req.expect("request body should be present");
818                    assert!(
819                        req.unlock_method_data
820                            .key_connector_key_wrapped_user_key
821                            .is_some(),
822                        "key_connector_key_wrapped_user_key should be set for KC rotation"
823                    );
824                    assert!(
825                        req.unlock_method_data.master_password_unlock_data.is_none(),
826                        "master_password_unlock_data should be None for KC rotation"
827                    );
828                    Ok(())
829                });
830        });
831
832        let state_bridge = make_state_bridge();
833        let result = internal_rotate_user_keys(
834            &key_store,
835            &api_client,
836            &state_bridge,
837            Some(&key_connector_api_client),
838            RotateUserKeysRequest {
839                key_rotation_method: KeyRotationMethod::KeyConnector {
840                    key_connector_url: "https://kc.example.com".to_string(),
841                },
842                trusted_organization_public_keys: vec![],
843                trusted_emergency_access_public_keys: vec![],
844                upgrade_token_action: UpgradeTokenAction::Skip,
845            },
846            sync.wrapped_account_cryptographic_state.clone(),
847            sync,
848        )
849        .await;
850
851        assert!(result.is_ok());
852        if let ApiClient::Mock(mut mock) = api_client {
853            mock.accounts_key_management_api.checkpoint();
854        }
855        if let bitwarden_api_key_connector::apis::ApiClient::Mock(mut mock) =
856            key_connector_api_client
857        {
858            mock.user_keys_api.checkpoint();
859        }
860    }
861
862    #[tokio::test]
863    async fn test_rotate_user_keys_key_connector_api_failure() {
864        let (key_store, sync) = make_test_key_store_and_synced_data();
865
866        let key_connector_api_client =
867            bitwarden_api_key_connector::apis::ApiClient::new_mocked(|mock| {
868                mock.user_keys_api
869                    .expect_get_user_key()
870                    .once()
871                    .returning(move || {
872                        Err(bitwarden_api_key_connector::apis::Error::ResponseError(
873                            bitwarden_api_key_connector::apis::ResponseContent {
874                                status: reqwest::StatusCode::INTERNAL_SERVER_ERROR,
875                                content: "Server Error".to_string(),
876                            },
877                        ))
878                    });
879            });
880
881        let api_client = ApiClient::new_mocked(|mock| {
882            mock.accounts_key_management_api
883                .expect_rotate_user_keys()
884                .never();
885        });
886
887        let state_bridge = make_state_bridge();
888        let result = internal_rotate_user_keys(
889            &key_store,
890            &api_client,
891            &state_bridge,
892            Some(&key_connector_api_client),
893            RotateUserKeysRequest {
894                key_rotation_method: KeyRotationMethod::KeyConnector {
895                    key_connector_url: "https://kc.example.com".to_string(),
896                },
897                trusted_organization_public_keys: vec![],
898                trusted_emergency_access_public_keys: vec![],
899                upgrade_token_action: UpgradeTokenAction::Skip,
900            },
901            sync.wrapped_account_cryptographic_state.clone(),
902            sync,
903        )
904        .await;
905
906        assert!(matches!(result, Err(RotateUserKeysError::KeyConnectorApi)));
907        if let ApiClient::Mock(mut mock) = api_client {
908            mock.accounts_key_management_api.checkpoint();
909        }
910        if let bitwarden_api_key_connector::apis::ApiClient::Mock(mut mock) =
911            key_connector_api_client
912        {
913            mock.user_keys_api.checkpoint();
914        }
915    }
916}