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::KeySlotIds;
4use bitwarden_crypto::{KeyConnectorKey, KeyStore, PublicKey};
5use serde::{Deserialize, Serialize};
6use tracing::{info, instrument};
7#[cfg(feature = "wasm")]
8use tsify::Tsify;
9#[cfg(feature = "wasm")]
10use wasm_bindgen::prelude::*;
11
12use crate::{
13    UserCryptoManagementClient,
14    key_rotation::{
15        RotateUserKeysError,
16        crypto::rotate_account_cryptographic_state_to_wrapped_model,
17        data::{check_for_old_attachments, reencrypt_data},
18        rotation_context::make_rotation_context,
19        sync::sync_current_account_data,
20        unlock::{ReencryptCommonUnlockDataInput, reencrypt_common_unlock_data},
21        unlock_method::{PrimaryUnlockMethod, reencrypt_unlock_method_data},
22    },
23};
24
25#[derive(Serialize, Deserialize, Clone)]
26#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
27#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
28pub enum KeyRotationMethod {
29    /// Master password user, key rotation without a password change.
30    Password { password: String },
31    /// Key Connector user, key rotation without a password change.
32    KeyConnector { key_connector_url: String },
33    /// TDE user, key rotation without a password change.
34    /// NOTE: This is not yet implemented and will return a
35    /// RotateUserKeysError::UnimplementedKeyRotationMethod error if used.
36    Tde,
37}
38
39#[derive(Serialize, Deserialize, Clone)]
40#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
41#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
42pub enum UpgradeTokenAction {
43    /// Skip creating and sending an upgrade token to the server. This will be the default behavior
44    /// if the field is omitted.
45    Skip,
46    /// Creates an upgrade token for V1 -> V2 key rotations.
47    /// For V2 -> V2 rotations, no upgrade token is needed.
48    CreateIfNeeded,
49}
50
51#[derive(Serialize, Deserialize, Clone)]
52#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
53#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
54pub struct RotateUserKeysRequest {
55    pub key_rotation_method: KeyRotationMethod,
56    pub trusted_emergency_access_public_keys: Vec<PublicKey>,
57    pub trusted_organization_public_keys: Vec<PublicKey>,
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    #[cfg_attr(feature = "wasm", tsify(optional))]
60    pub upgrade_token_action: Option<UpgradeTokenAction>,
61}
62
63#[cfg_attr(feature = "wasm", wasm_bindgen)]
64impl UserCryptoManagementClient {
65    /// Rotates the user's encryption keys without a password change.
66    pub async fn rotate_user_keys(
67        &self,
68        request: RotateUserKeysRequest,
69    ) -> Result<(), RotateUserKeysError> {
70        let api_client = &self.client.internal.get_api_configurations().api_client;
71        let key_store = self.client.internal.get_key_store();
72
73        let key_connector_api_client =
74            if let KeyRotationMethod::KeyConnector { key_connector_url } =
75                &request.key_rotation_method
76            {
77                Some(
78                    self.client
79                        .internal
80                        .get_key_connector_client(key_connector_url.clone()),
81                )
82            } else {
83                None
84            };
85
86        internal_rotate_user_keys(
87            key_store,
88            api_client,
89            key_connector_api_client.as_ref(),
90            request,
91        )
92        .await
93    }
94}
95
96#[instrument(name = "rotate_user_keys", level = "info", skip_all, err)]
97async fn internal_rotate_user_keys(
98    key_store: &KeyStore<KeySlotIds>,
99    api_client: &bitwarden_api_api::apis::ApiClient,
100    key_connector_api_client: Option<&bitwarden_api_key_connector::apis::ApiClient>,
101    request: RotateUserKeysRequest,
102) -> Result<(), RotateUserKeysError> {
103    if matches!(request.key_rotation_method, KeyRotationMethod::Tde) {
104        return Err(RotateUserKeysError::UnimplementedKeyRotationMethod);
105    }
106
107    let sync = sync_current_account_data(api_client)
108        .await
109        .map_err(|_| RotateUserKeysError::Api)?;
110
111    // Fail early if any cipher has old attachments that would become irrecoverable
112    check_for_old_attachments(&sync.ciphers)?;
113
114    // For Key Connector users, fetch the existing KC key from the KC server.
115    // This must happen before the synchronous key store scope below.
116    let key_connector_key = if matches!(
117        request.key_rotation_method,
118        KeyRotationMethod::KeyConnector { .. }
119    ) {
120        let key_connector_client =
121            key_connector_api_client.ok_or(RotateUserKeysError::KeyConnectorApi)?;
122        info!("Fetching Key Connector key for key rotation");
123        let response = key_connector_client
124            .user_keys_api()
125            .get_user_key()
126            .await
127            .map_err(|_| RotateUserKeysError::KeyConnectorApi)?;
128        let key_connector_key =
129            KeyConnectorKey::try_from(response).map_err(|_| RotateUserKeysError::Crypto)?;
130        Some(key_connector_key)
131    } else {
132        None
133    };
134
135    // Create a separate scope so that the mutable context is not held across the await point
136    let post_request = {
137        let mut ctx = key_store.context_mut();
138
139        let rotation_context = make_rotation_context(
140            &sync,
141            request.trusted_organization_public_keys.as_slice(),
142            request.trusted_emergency_access_public_keys.as_slice(),
143            &mut ctx,
144        )?;
145
146        info!("Rotating account cryptographic state for user key rotation");
147        let wrapped_account_cryptographic_state_request_model =
148            rotate_account_cryptographic_state_to_wrapped_model(
149                &sync.wrapped_account_cryptographic_state,
150                &rotation_context.current_user_key_id,
151                &rotation_context.new_user_key_id,
152                &mut ctx,
153            )
154            .map_err(|_| RotateUserKeysError::Crypto)?;
155
156        info!("Re-encrypting account data for user key rotation");
157        let account_data_model = reencrypt_data(
158            sync.folders.as_slice(),
159            sync.ciphers.as_slice(),
160            sync.sends.as_slice(),
161            rotation_context.current_user_key_id,
162            rotation_context.new_user_key_id,
163            &mut ctx,
164        )
165        .map_err(|_| RotateUserKeysError::Crypto)?;
166
167        info!("Re-encrypting account primary unlock method for user key rotation");
168        let unlock_method_input = PrimaryUnlockMethod::from_key_rotation_method(
169            request.key_rotation_method,
170            &sync,
171            key_connector_key,
172        )?;
173        let unlock_method_data = reencrypt_unlock_method_data(
174            unlock_method_input,
175            rotation_context.new_user_key_id,
176            &mut ctx,
177        )
178        .map_err(|_| RotateUserKeysError::Crypto)?;
179
180        info!("Re-encrypting account common unlock data for user key rotation");
181        let common_unlock_data = reencrypt_common_unlock_data(
182            ReencryptCommonUnlockDataInput {
183                trusted_organization_keys: rotation_context.v1_organization_memberships,
184                trusted_emergency_access_keys: rotation_context.v1_emergency_access_memberships,
185                webauthn_credentials: sync.passkeys,
186                trusted_devices: sync.trusted_devices,
187            },
188            rotation_context.current_user_key_id,
189            rotation_context.new_user_key_id,
190            request
191                .upgrade_token_action
192                .unwrap_or(UpgradeTokenAction::Skip),
193            &mut ctx,
194        )
195        .map_err(|_| RotateUserKeysError::Crypto)?;
196
197        RotateUserKeysRequestModel {
198            wrapped_account_cryptographic_state: Box::new(
199                wrapped_account_cryptographic_state_request_model,
200            ),
201            account_data: Box::new(account_data_model),
202            unlock_data: Box::new(common_unlock_data),
203            unlock_method_data: Box::new(unlock_method_data),
204        }
205    };
206
207    info!("Posting rotated user account keys and data to server");
208    api_client
209        .accounts_key_management_api()
210        .rotate_user_keys(Some(post_request))
211        .await
212        .map_err(|_| RotateUserKeysError::Api)?;
213    info!("Successfully rotated user account keys and data");
214    Ok(())
215}
216
217#[cfg(test)]
218mod tests {
219    use bitwarden_api_api::{
220        apis::ApiClient,
221        models::{
222            DeviceAuthRequestResponseModelListResponseModel,
223            EmergencyAccessGranteeDetailsResponseModelListResponseModel, KdfType,
224            MasterPasswordUnlockKdfResponseModel, MasterPasswordUnlockResponseModel,
225            PrivateKeysResponseModel, ProfileOrganizationResponseModelListResponseModel,
226            ProfileResponseModel, PublicKeyEncryptionKeyPairResponseModel, SyncResponseModel,
227            UserDecryptionResponseModel, WebAuthnCredentialResponseModelListResponseModel,
228        },
229    };
230    use bitwarden_core::key_management::{KeySlotIds, SymmetricKeySlotId};
231    use bitwarden_crypto::{KeyStore, PublicKeyEncryptionAlgorithm, SymmetricKeyAlgorithm};
232
233    use super::*;
234
235    fn make_test_key_store_and_sync_response() -> (KeyStore<KeySlotIds>, SyncResponseModel) {
236        let store: KeyStore<KeySlotIds> = KeyStore::default();
237        let wrapped_private_key = {
238            let mut ctx = store.context_mut();
239            let user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
240            let _ = ctx.persist_symmetric_key(user_key, SymmetricKeySlotId::User);
241            let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
242            ctx.wrap_private_key(SymmetricKeySlotId::User, private_key)
243                .unwrap()
244        };
245
246        let sync_response = SyncResponseModel {
247            object: Some("sync".to_string()),
248            profile: Some(Box::new(ProfileResponseModel {
249                id: Some(uuid::Uuid::new_v4()),
250                account_keys: Some(Box::new(PrivateKeysResponseModel {
251                    object: None,
252                    signature_key_pair: None,
253                    public_key_encryption_key_pair: Box::new(
254                        PublicKeyEncryptionKeyPairResponseModel {
255                            object: None,
256                            wrapped_private_key: Some(wrapped_private_key.to_string()),
257                            public_key: None,
258                            signed_public_key: None,
259                        },
260                    ),
261                    security_state: None,
262                })),
263                ..ProfileResponseModel::default()
264            })),
265            folders: Some(vec![]),
266            ciphers: Some(vec![]),
267            sends: Some(vec![]),
268            user_decryption: Some(Box::new(UserDecryptionResponseModel {
269                master_password_unlock: Some(Box::new(MasterPasswordUnlockResponseModel {
270                    kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
271                        kdf_type: KdfType::PBKDF2_SHA256,
272                        iterations: 600000,
273                        memory: None,
274                        parallelism: None,
275                    }),
276                    master_key_encrypted_user_key: None,
277                    salt: Some("test_salt".to_string()),
278                })),
279                web_authn_prf_options: None,
280                v2_upgrade_token: None,
281            })),
282            ..Default::default()
283        };
284
285        (store, sync_response)
286    }
287
288    fn mock_empty_sync_calls(mock: &mut bitwarden_api_api::apis::ApiClientMock) {
289        mock.organizations_api
290            .expect_get_user()
291            .once()
292            .returning(|| {
293                Ok(ProfileOrganizationResponseModelListResponseModel {
294                    object: None,
295                    data: Some(vec![]),
296                    continuation_token: None,
297                })
298            });
299        mock.emergency_access_api
300            .expect_get_contacts()
301            .once()
302            .returning(|| {
303                Ok(
304                    EmergencyAccessGranteeDetailsResponseModelListResponseModel {
305                        object: None,
306                        data: Some(vec![]),
307                        continuation_token: None,
308                    },
309                )
310            });
311        mock.devices_api.expect_get_all().once().returning(|| {
312            Ok(DeviceAuthRequestResponseModelListResponseModel {
313                object: None,
314                data: Some(vec![]),
315                continuation_token: None,
316            })
317        });
318        mock.web_authn_api.expect_get().once().returning(|| {
319            Ok(WebAuthnCredentialResponseModelListResponseModel {
320                object: None,
321                data: Some(vec![]),
322                continuation_token: None,
323            })
324        });
325    }
326
327    #[tokio::test]
328    async fn test_rotate_user_keys_tde_returns_unimplemented() {
329        let key_store: KeyStore<KeySlotIds> = KeyStore::default();
330        let api_client = ApiClient::new_mocked(|mock| {
331            mock.sync_api.expect_get().never();
332            mock.accounts_key_management_api
333                .expect_rotate_user_keys()
334                .never();
335        });
336
337        let result = internal_rotate_user_keys(
338            &key_store,
339            &api_client,
340            None,
341            RotateUserKeysRequest {
342                key_rotation_method: KeyRotationMethod::Tde,
343                trusted_organization_public_keys: vec![],
344                trusted_emergency_access_public_keys: vec![],
345                upgrade_token_action: None,
346            },
347        )
348        .await;
349
350        assert!(matches!(
351            result,
352            Err(RotateUserKeysError::UnimplementedKeyRotationMethod)
353        ));
354        if let ApiClient::Mock(mut mock) = api_client {
355            mock.sync_api.checkpoint();
356            mock.accounts_key_management_api.checkpoint();
357        }
358    }
359
360    #[tokio::test]
361    async fn test_rotate_user_keys_api_failure_returns_api_error() {
362        let key_store: KeyStore<KeySlotIds> = KeyStore::default();
363        let api_client = ApiClient::new_mocked(|mock| {
364            mock.sync_api.expect_get().once().returning(|_| {
365                Err(serde_json::Error::io(std::io::Error::other("network error")).into())
366            });
367            mock.accounts_key_management_api
368                .expect_rotate_user_keys()
369                .never();
370        });
371
372        let result = internal_rotate_user_keys(
373            &key_store,
374            &api_client,
375            None,
376            RotateUserKeysRequest {
377                key_rotation_method: KeyRotationMethod::Password {
378                    password: "test".to_string(),
379                },
380                trusted_organization_public_keys: vec![],
381                trusted_emergency_access_public_keys: vec![],
382                upgrade_token_action: None,
383            },
384        )
385        .await;
386
387        assert!(matches!(result, Err(RotateUserKeysError::Api)));
388        if let ApiClient::Mock(mut mock) = api_client {
389            mock.sync_api.checkpoint();
390            mock.accounts_key_management_api.checkpoint();
391        }
392    }
393
394    #[tokio::test]
395    async fn test_rotate_user_keys_master_password_success() {
396        let (key_store, sync_response) = make_test_key_store_and_sync_response();
397        let api_client = ApiClient::new_mocked(|mock| {
398            mock.sync_api
399                .expect_get()
400                .once()
401                .returning(move |_| Ok(sync_response.clone()));
402            mock_empty_sync_calls(mock);
403            mock.accounts_key_management_api
404                .expect_rotate_user_keys()
405                .once()
406                .returning(|_| Ok(()));
407        });
408
409        let result = internal_rotate_user_keys(
410            &key_store,
411            &api_client,
412            None,
413            RotateUserKeysRequest {
414                key_rotation_method: KeyRotationMethod::Password {
415                    password: "test_password".to_string(),
416                },
417                trusted_organization_public_keys: vec![],
418                trusted_emergency_access_public_keys: vec![],
419                upgrade_token_action: None,
420            },
421        )
422        .await;
423
424        assert!(result.is_ok());
425        if let ApiClient::Mock(mut mock) = api_client {
426            mock.sync_api.checkpoint();
427            mock.organizations_api.checkpoint();
428            mock.emergency_access_api.checkpoint();
429            mock.devices_api.checkpoint();
430            mock.web_authn_api.checkpoint();
431            mock.accounts_key_management_api.checkpoint();
432        }
433    }
434
435    #[tokio::test]
436    async fn test_rotate_user_keys_post_api_failure_returns_api_error() {
437        let (key_store, sync_response) = make_test_key_store_and_sync_response();
438        let api_client = ApiClient::new_mocked(|mock| {
439            mock.sync_api
440                .expect_get()
441                .once()
442                .returning(move |_| Ok(sync_response.clone()));
443            mock_empty_sync_calls(mock);
444            mock.accounts_key_management_api
445                .expect_rotate_user_keys()
446                .once()
447                .returning(|_| {
448                    Err(serde_json::Error::io(std::io::Error::other("API error")).into())
449                });
450        });
451
452        let result = internal_rotate_user_keys(
453            &key_store,
454            &api_client,
455            None,
456            RotateUserKeysRequest {
457                key_rotation_method: KeyRotationMethod::Password {
458                    password: "test_password".to_string(),
459                },
460                trusted_organization_public_keys: vec![],
461                trusted_emergency_access_public_keys: vec![],
462                upgrade_token_action: None,
463            },
464        )
465        .await;
466
467        assert!(matches!(result, Err(RotateUserKeysError::Api)));
468        if let ApiClient::Mock(mut mock) = api_client {
469            mock.sync_api.checkpoint();
470            mock.organizations_api.checkpoint();
471            mock.emergency_access_api.checkpoint();
472            mock.devices_api.checkpoint();
473            mock.web_authn_api.checkpoint();
474            mock.accounts_key_management_api.checkpoint();
475        }
476    }
477
478    #[tokio::test]
479    async fn test_rotate_user_keys_upgrade_token_action_none_omits_token() {
480        let (key_store, sync_response) = make_test_key_store_and_sync_response();
481        let api_client = ApiClient::new_mocked(|mock| {
482            mock.sync_api
483                .expect_get()
484                .once()
485                .returning(move |_| Ok(sync_response.clone()));
486            mock_empty_sync_calls(mock);
487            mock.accounts_key_management_api
488                .expect_rotate_user_keys()
489                .once()
490                .returning(|req| {
491                    let req = req.expect("request body should be present");
492                    assert!(
493                        req.unlock_data.v2_upgrade_token.is_none(),
494                        "upgrade_token_action None, should omit the v2_upgrade_token"
495                    );
496                    Ok(())
497                });
498        });
499
500        let result = internal_rotate_user_keys(
501            &key_store,
502            &api_client,
503            None,
504            RotateUserKeysRequest {
505                key_rotation_method: KeyRotationMethod::Password {
506                    password: "test_password".to_string(),
507                },
508                trusted_organization_public_keys: vec![],
509                trusted_emergency_access_public_keys: vec![],
510                upgrade_token_action: None,
511            },
512        )
513        .await;
514
515        assert!(result.is_ok());
516        if let ApiClient::Mock(mut mock) = api_client {
517            mock.sync_api.checkpoint();
518            mock.organizations_api.checkpoint();
519            mock.emergency_access_api.checkpoint();
520            mock.devices_api.checkpoint();
521            mock.web_authn_api.checkpoint();
522            mock.accounts_key_management_api.checkpoint();
523        }
524    }
525
526    #[tokio::test]
527    async fn test_rotate_user_keys_upgrade_token_action_skip_omits_token() {
528        let (key_store, sync_response) = make_test_key_store_and_sync_response();
529        let api_client = ApiClient::new_mocked(|mock| {
530            mock.sync_api
531                .expect_get()
532                .once()
533                .returning(move |_| Ok(sync_response.clone()));
534            mock_empty_sync_calls(mock);
535            mock.accounts_key_management_api
536                .expect_rotate_user_keys()
537                .once()
538                .returning(|req| {
539                    let req = req.expect("request body should be present");
540                    assert!(
541                        req.unlock_data.v2_upgrade_token.is_none(),
542                        "upgrade_token_action Skip, should omit the v2_upgrade_token"
543                    );
544                    Ok(())
545                });
546        });
547
548        let result = internal_rotate_user_keys(
549            &key_store,
550            &api_client,
551            None,
552            RotateUserKeysRequest {
553                key_rotation_method: KeyRotationMethod::Password {
554                    password: "test_password".to_string(),
555                },
556                trusted_organization_public_keys: vec![],
557                trusted_emergency_access_public_keys: vec![],
558                upgrade_token_action: Some(UpgradeTokenAction::Skip),
559            },
560        )
561        .await;
562
563        assert!(result.is_ok());
564        if let ApiClient::Mock(mut mock) = api_client {
565            mock.sync_api.checkpoint();
566            mock.organizations_api.checkpoint();
567            mock.emergency_access_api.checkpoint();
568            mock.devices_api.checkpoint();
569            mock.web_authn_api.checkpoint();
570            mock.accounts_key_management_api.checkpoint();
571        }
572    }
573
574    #[tokio::test]
575    async fn test_rotate_user_keys_upgrade_token_action_create_if_needed_includes_token() {
576        let (key_store, sync_response) = make_test_key_store_and_sync_response();
577        let api_client = ApiClient::new_mocked(|mock| {
578            mock.sync_api
579                .expect_get()
580                .once()
581                .returning(move |_| Ok(sync_response.clone()));
582            mock_empty_sync_calls(mock);
583            mock.accounts_key_management_api
584                .expect_rotate_user_keys()
585                .once()
586                .returning(|req| {
587                    let req = req.expect("request body should be present");
588                    assert!(
589                        req.unlock_data.v2_upgrade_token.is_some(),
590                        "upgrade_token_action CreateIfNeeded, should include a v2_upgrade_token for V1 -> V2 rotations"
591                    );
592                    Ok(())
593                });
594        });
595
596        let result = internal_rotate_user_keys(
597            &key_store,
598            &api_client,
599            None,
600            RotateUserKeysRequest {
601                key_rotation_method: KeyRotationMethod::Password {
602                    password: "test_password".to_string(),
603                },
604                trusted_organization_public_keys: vec![],
605                trusted_emergency_access_public_keys: vec![],
606                upgrade_token_action: Some(UpgradeTokenAction::CreateIfNeeded),
607            },
608        )
609        .await;
610
611        assert!(result.is_ok());
612        if let ApiClient::Mock(mut mock) = api_client {
613            mock.sync_api.checkpoint();
614            mock.organizations_api.checkpoint();
615            mock.emergency_access_api.checkpoint();
616            mock.devices_api.checkpoint();
617            mock.web_authn_api.checkpoint();
618            mock.accounts_key_management_api.checkpoint();
619        }
620    }
621
622    #[tokio::test]
623    async fn test_rotate_user_keys_old_attachments_returns_error() {
624        use bitwarden_api_api::models::{
625            AttachmentResponseModel, CipherDetailsResponseModel, CipherType,
626        };
627
628        let (key_store, mut sync_response) = make_test_key_store_and_sync_response();
629        let enc_string = "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=";
630
631        // Add a cipher with an old attachment (key is None)
632        sync_response.ciphers = Some(vec![CipherDetailsResponseModel {
633            id: Some(uuid::Uuid::new_v4()),
634            organization_id: None,
635            r#type: Some(CipherType::Login),
636            name: Some(enc_string.to_string()),
637            revision_date: Some("2024-01-01T00:00:00Z".to_string()),
638            creation_date: Some("2024-01-01T00:00:00Z".to_string()),
639            attachments: Some(vec![AttachmentResponseModel {
640                id: Some("att1".to_string()),
641                file_name: Some(enc_string.to_string()),
642                key: None, // Old attachment - no per-attachment key
643                ..AttachmentResponseModel::new()
644            }]),
645            ..CipherDetailsResponseModel::new()
646        }]);
647
648        let api_client = ApiClient::new_mocked(|mock| {
649            mock.sync_api
650                .expect_get()
651                .once()
652                .returning(move |_| Ok(sync_response.clone()));
653            mock_empty_sync_calls(mock);
654            // Rotation API should never be called
655            mock.accounts_key_management_api
656                .expect_rotate_user_keys()
657                .never();
658        });
659
660        let result = internal_rotate_user_keys(
661            &key_store,
662            &api_client,
663            None,
664            RotateUserKeysRequest {
665                key_rotation_method: KeyRotationMethod::Password {
666                    password: "test_password".to_string(),
667                },
668                trusted_organization_public_keys: vec![],
669                trusted_emergency_access_public_keys: vec![],
670                upgrade_token_action: None,
671            },
672        )
673        .await;
674
675        assert!(matches!(result, Err(RotateUserKeysError::OldAttachments)));
676        if let ApiClient::Mock(mut mock) = api_client {
677            mock.sync_api.checkpoint();
678            mock.organizations_api.checkpoint();
679            mock.emergency_access_api.checkpoint();
680            mock.devices_api.checkpoint();
681            mock.web_authn_api.checkpoint();
682            mock.accounts_key_management_api.checkpoint();
683        }
684    }
685
686    #[tokio::test]
687    async fn test_rotate_user_keys_key_connector_success() {
688        let (key_store, sync_response) = make_test_key_store_and_sync_response();
689
690        let key_connector_key = KeyConnectorKey::make();
691        let key_connector_api_client = bitwarden_api_key_connector::apis::ApiClient::new_mocked(
692            |mock| {
693                let key_connector_key_clone = key_connector_key.clone();
694                mock.user_keys_api
695                    .expect_get_user_key()
696                    .once()
697                    .returning(move || {
698                        let encoded: bitwarden_encoding::B64 =
699                            key_connector_key_clone.clone().into();
700                        Ok(
701                            bitwarden_api_key_connector::models::user_key_response_model::UserKeyResponseModel {
702                                key: encoded.to_string(),
703                            },
704                        )
705                    });
706            },
707        );
708
709        let api_client = ApiClient::new_mocked(|mock| {
710            mock.sync_api
711                .expect_get()
712                .once()
713                .returning(move |_| Ok(sync_response.clone()));
714            mock_empty_sync_calls(mock);
715            mock.accounts_key_management_api
716                .expect_rotate_user_keys()
717                .once()
718                .returning(|req| {
719                    let req = req.expect("request body should be present");
720                    assert!(
721                        req.unlock_method_data
722                            .key_connector_key_wrapped_user_key
723                            .is_some(),
724                        "key_connector_key_wrapped_user_key should be set for KC rotation"
725                    );
726                    assert!(
727                        req.unlock_method_data.master_password_unlock_data.is_none(),
728                        "master_password_unlock_data should be None for KC rotation"
729                    );
730                    Ok(())
731                });
732        });
733
734        let result = internal_rotate_user_keys(
735            &key_store,
736            &api_client,
737            Some(&key_connector_api_client),
738            RotateUserKeysRequest {
739                key_rotation_method: KeyRotationMethod::KeyConnector {
740                    key_connector_url: "https://kc.example.com".to_string(),
741                },
742                trusted_organization_public_keys: vec![],
743                trusted_emergency_access_public_keys: vec![],
744                upgrade_token_action: None,
745            },
746        )
747        .await;
748
749        assert!(result.is_ok());
750        if let ApiClient::Mock(mut mock) = api_client {
751            mock.sync_api.checkpoint();
752            mock.organizations_api.checkpoint();
753            mock.emergency_access_api.checkpoint();
754            mock.devices_api.checkpoint();
755            mock.web_authn_api.checkpoint();
756            mock.accounts_key_management_api.checkpoint();
757        }
758        if let bitwarden_api_key_connector::apis::ApiClient::Mock(mut mock) =
759            key_connector_api_client
760        {
761            mock.user_keys_api.checkpoint();
762        }
763    }
764
765    #[tokio::test]
766    async fn test_rotate_user_keys_key_connector_api_failure() {
767        let (key_store, sync_response) = make_test_key_store_and_sync_response();
768
769        let key_connector_api_client =
770            bitwarden_api_key_connector::apis::ApiClient::new_mocked(|mock| {
771                mock.user_keys_api
772                    .expect_get_user_key()
773                    .once()
774                    .returning(move || {
775                        Err(bitwarden_api_key_connector::apis::Error::ResponseError(
776                            bitwarden_api_key_connector::apis::ResponseContent {
777                                status: reqwest::StatusCode::INTERNAL_SERVER_ERROR,
778                                content: "Server Error".to_string(),
779                            },
780                        ))
781                    });
782            });
783
784        let api_client = ApiClient::new_mocked(|mock| {
785            mock.sync_api
786                .expect_get()
787                .once()
788                .returning(move |_| Ok(sync_response.clone()));
789            mock_empty_sync_calls(mock);
790            mock.accounts_key_management_api
791                .expect_rotate_user_keys()
792                .never();
793        });
794
795        let result = internal_rotate_user_keys(
796            &key_store,
797            &api_client,
798            Some(&key_connector_api_client),
799            RotateUserKeysRequest {
800                key_rotation_method: KeyRotationMethod::KeyConnector {
801                    key_connector_url: "https://kc.example.com".to_string(),
802                },
803                trusted_organization_public_keys: vec![],
804                trusted_emergency_access_public_keys: vec![],
805                upgrade_token_action: None,
806            },
807        )
808        .await;
809
810        assert!(matches!(result, Err(RotateUserKeysError::KeyConnectorApi)));
811        if let ApiClient::Mock(mut mock) = api_client {
812            mock.sync_api.checkpoint();
813            mock.organizations_api.checkpoint();
814            mock.emergency_access_api.checkpoint();
815            mock.devices_api.checkpoint();
816            mock.web_authn_api.checkpoint();
817            mock.accounts_key_management_api.checkpoint();
818        }
819        if let bitwarden_api_key_connector::apis::ApiClient::Mock(mut mock) =
820            key_connector_api_client
821        {
822            mock.user_keys_api.checkpoint();
823        }
824    }
825}