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::{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::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    /// NOTE: This is not yet implemented and will return a
33    /// RotateUserKeysError::UnimplementedKeyRotationMethod error if used.
34    KeyConnector,
35    /// TDE user, key rotation without a password change.
36    /// NOTE: This is not yet implemented and will return a
37    /// RotateUserKeysError::UnimplementedKeyRotationMethod error if used.
38    Tde,
39}
40
41#[derive(Serialize, Deserialize, Clone)]
42#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
43#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
44pub struct RotateUserKeysRequest {
45    pub key_rotation_method: KeyRotationMethod,
46    pub trusted_emergency_access_public_keys: Vec<PublicKey>,
47    pub trusted_organization_public_keys: Vec<PublicKey>,
48}
49
50#[cfg_attr(feature = "wasm", wasm_bindgen)]
51impl UserCryptoManagementClient {
52    /// Rotates the user's encryption keys without a password change.
53    pub async fn rotate_user_keys(
54        &self,
55        request: RotateUserKeysRequest,
56    ) -> Result<(), RotateUserKeysError> {
57        let api_client = &self.client.internal.get_api_configurations().api_client;
58        let key_store = self.client.internal.get_key_store();
59        internal_rotate_user_keys(key_store, api_client, request).await
60    }
61}
62
63#[instrument(name = "rotate_user_keys", level = "info", skip_all, err)]
64async fn internal_rotate_user_keys(
65    key_store: &KeyStore<KeySlotIds>,
66    api_client: &bitwarden_api_api::apis::ApiClient,
67    request: RotateUserKeysRequest,
68) -> Result<(), RotateUserKeysError> {
69    // This guard should be removed once other key rotation methods are implemented.
70    match &request.key_rotation_method {
71        KeyRotationMethod::KeyConnector => {
72            return Err(RotateUserKeysError::UnimplementedKeyRotationMethod);
73        }
74        KeyRotationMethod::Tde => {
75            return Err(RotateUserKeysError::UnimplementedKeyRotationMethod);
76        }
77        KeyRotationMethod::Password { .. } => {}
78    }
79
80    let sync = sync_current_account_data(api_client)
81        .await
82        .map_err(|_| RotateUserKeysError::ApiError)?;
83
84    // Create a separate scope so that the mutable context is not held across the await point
85    let post_request = {
86        let mut ctx = key_store.context_mut();
87
88        let rotation_context = make_rotation_context(
89            &sync,
90            request.trusted_organization_public_keys.as_slice(),
91            request.trusted_emergency_access_public_keys.as_slice(),
92            &mut ctx,
93        )?;
94
95        info!("Rotating account cryptographic state for user key rotation");
96        let wrapped_account_cryptographic_state_request_model =
97            rotate_account_cryptographic_state_to_wrapped_model(
98                &sync.wrapped_account_cryptographic_state,
99                &rotation_context.current_user_key_id,
100                &rotation_context.new_user_key_id,
101                &mut ctx,
102            )
103            .map_err(|_| RotateUserKeysError::CryptoError)?;
104
105        info!("Re-encrypting account data for user key rotation");
106        let account_data_model = reencrypt_data(
107            sync.folders.as_slice(),
108            sync.ciphers.as_slice(),
109            sync.sends.as_slice(),
110            rotation_context.current_user_key_id,
111            rotation_context.new_user_key_id,
112            &mut ctx,
113        )
114        .map_err(|_| RotateUserKeysError::CryptoError)?;
115
116        info!("Re-encrypting account primary unlock method for user key rotation");
117        let unlock_method_input =
118            PrimaryUnlockMethod::from_key_rotation_method(request.key_rotation_method, &sync)
119                .map_err(|_| RotateUserKeysError::ApiError)?;
120        let unlock_method_data = reencrypt_unlock_method_data(
121            unlock_method_input,
122            rotation_context.new_user_key_id,
123            &mut ctx,
124        )
125        .map_err(|_| RotateUserKeysError::CryptoError)?;
126
127        info!("Re-encrypting account common unlock data for user key rotation");
128        let common_unlock_data = reencrypt_common_unlock_data(
129            ReencryptCommonUnlockDataInput {
130                trusted_organization_keys: rotation_context.v1_organization_memberships,
131                trusted_emergency_access_keys: rotation_context.v1_emergency_access_memberships,
132                webauthn_credentials: sync.passkeys,
133                trusted_devices: sync.trusted_devices,
134            },
135            rotation_context.current_user_key_id,
136            rotation_context.new_user_key_id,
137            &mut ctx,
138        )
139        .map_err(|_| RotateUserKeysError::CryptoError)?;
140
141        RotateUserKeysRequestModel {
142            wrapped_account_cryptographic_state: Box::new(
143                wrapped_account_cryptographic_state_request_model,
144            ),
145            account_data: Box::new(account_data_model),
146            unlock_data: Box::new(common_unlock_data),
147            unlock_method_data: Box::new(unlock_method_data),
148        }
149    };
150
151    info!("Posting rotated user account keys and data to server");
152    api_client
153        .accounts_key_management_api()
154        .rotate_user_keys(Some(post_request))
155        .await
156        .map_err(|_| RotateUserKeysError::ApiError)?;
157    info!("Successfully rotated user account keys and data");
158    Ok(())
159}
160
161#[cfg(test)]
162mod tests {
163    use bitwarden_api_api::{
164        apis::ApiClient,
165        models::{
166            DeviceAuthRequestResponseModelListResponseModel,
167            EmergencyAccessGranteeDetailsResponseModelListResponseModel, KdfType,
168            MasterPasswordUnlockKdfResponseModel, MasterPasswordUnlockResponseModel,
169            PrivateKeysResponseModel, ProfileOrganizationResponseModelListResponseModel,
170            ProfileResponseModel, PublicKeyEncryptionKeyPairResponseModel, SyncResponseModel,
171            UserDecryptionResponseModel, WebAuthnCredentialResponseModelListResponseModel,
172        },
173    };
174    use bitwarden_core::key_management::{KeySlotIds, SymmetricKeySlotId};
175    use bitwarden_crypto::{KeyStore, PublicKeyEncryptionAlgorithm, SymmetricKeyAlgorithm};
176
177    use super::*;
178
179    fn make_test_key_store_and_sync_response() -> (KeyStore<KeySlotIds>, SyncResponseModel) {
180        let store: KeyStore<KeySlotIds> = KeyStore::default();
181        let wrapped_private_key = {
182            let mut ctx = store.context_mut();
183            let user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
184            let _ = ctx.persist_symmetric_key(user_key, SymmetricKeySlotId::User);
185            let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
186            ctx.wrap_private_key(SymmetricKeySlotId::User, private_key)
187                .unwrap()
188        };
189
190        let sync_response = SyncResponseModel {
191            object: Some("sync".to_string()),
192            profile: Some(Box::new(ProfileResponseModel {
193                id: Some(uuid::Uuid::new_v4()),
194                account_keys: Some(Box::new(PrivateKeysResponseModel {
195                    object: None,
196                    signature_key_pair: None,
197                    public_key_encryption_key_pair: Box::new(
198                        PublicKeyEncryptionKeyPairResponseModel {
199                            object: None,
200                            wrapped_private_key: Some(wrapped_private_key.to_string()),
201                            public_key: None,
202                            signed_public_key: None,
203                        },
204                    ),
205                    security_state: None,
206                })),
207                ..ProfileResponseModel::default()
208            })),
209            folders: Some(vec![]),
210            ciphers: Some(vec![]),
211            sends: Some(vec![]),
212            collections: None,
213            domains: None,
214            policies: None,
215            user_decryption: Some(Box::new(UserDecryptionResponseModel {
216                master_password_unlock: Some(Box::new(MasterPasswordUnlockResponseModel {
217                    kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
218                        kdf_type: KdfType::PBKDF2_SHA256,
219                        iterations: 600000,
220                        memory: None,
221                        parallelism: None,
222                    }),
223                    master_key_encrypted_user_key: None,
224                    salt: Some("test_salt".to_string()),
225                })),
226                web_authn_prf_options: None,
227                v2_upgrade_token: None,
228            })),
229        };
230
231        (store, sync_response)
232    }
233
234    fn mock_empty_sync_calls(mock: &mut bitwarden_api_api::apis::ApiClientMock) {
235        mock.organizations_api
236            .expect_get_user()
237            .once()
238            .returning(|| {
239                Ok(ProfileOrganizationResponseModelListResponseModel {
240                    object: None,
241                    data: Some(vec![]),
242                    continuation_token: None,
243                })
244            });
245        mock.emergency_access_api
246            .expect_get_contacts()
247            .once()
248            .returning(|| {
249                Ok(
250                    EmergencyAccessGranteeDetailsResponseModelListResponseModel {
251                        object: None,
252                        data: Some(vec![]),
253                        continuation_token: None,
254                    },
255                )
256            });
257        mock.devices_api.expect_get_all().once().returning(|| {
258            Ok(DeviceAuthRequestResponseModelListResponseModel {
259                object: None,
260                data: Some(vec![]),
261                continuation_token: None,
262            })
263        });
264        mock.web_authn_api.expect_get().once().returning(|| {
265            Ok(WebAuthnCredentialResponseModelListResponseModel {
266                object: None,
267                data: Some(vec![]),
268                continuation_token: None,
269            })
270        });
271    }
272
273    #[tokio::test]
274    async fn test_rotate_user_keys_key_connector_returns_unimplemented() {
275        let key_store: KeyStore<KeySlotIds> = KeyStore::default();
276        let api_client = ApiClient::new_mocked(|mock| {
277            mock.sync_api.expect_get().never();
278            mock.accounts_key_management_api
279                .expect_rotate_user_keys()
280                .never();
281        });
282
283        let result = internal_rotate_user_keys(
284            &key_store,
285            &api_client,
286            RotateUserKeysRequest {
287                key_rotation_method: KeyRotationMethod::KeyConnector,
288                trusted_organization_public_keys: vec![],
289                trusted_emergency_access_public_keys: vec![],
290            },
291        )
292        .await;
293
294        assert!(matches!(
295            result,
296            Err(RotateUserKeysError::UnimplementedKeyRotationMethod)
297        ));
298        if let ApiClient::Mock(mut mock) = api_client {
299            mock.sync_api.checkpoint();
300            mock.accounts_key_management_api.checkpoint();
301        }
302    }
303
304    #[tokio::test]
305    async fn test_rotate_user_keys_tde_returns_unimplemented() {
306        let key_store: KeyStore<KeySlotIds> = KeyStore::default();
307        let api_client = ApiClient::new_mocked(|mock| {
308            mock.sync_api.expect_get().never();
309            mock.accounts_key_management_api
310                .expect_rotate_user_keys()
311                .never();
312        });
313
314        let result = internal_rotate_user_keys(
315            &key_store,
316            &api_client,
317            RotateUserKeysRequest {
318                key_rotation_method: KeyRotationMethod::Tde,
319                trusted_organization_public_keys: vec![],
320                trusted_emergency_access_public_keys: vec![],
321            },
322        )
323        .await;
324
325        assert!(matches!(
326            result,
327            Err(RotateUserKeysError::UnimplementedKeyRotationMethod)
328        ));
329        if let ApiClient::Mock(mut mock) = api_client {
330            mock.sync_api.checkpoint();
331            mock.accounts_key_management_api.checkpoint();
332        }
333    }
334
335    #[tokio::test]
336    async fn test_rotate_user_keys_api_failure_returns_api_error() {
337        let key_store: KeyStore<KeySlotIds> = KeyStore::default();
338        let api_client = ApiClient::new_mocked(|mock| {
339            mock.sync_api.expect_get().once().returning(|_| {
340                Err(bitwarden_api_api::apis::Error::Serde(
341                    serde_json::Error::io(std::io::Error::other("network error")),
342                ))
343            });
344            mock.accounts_key_management_api
345                .expect_rotate_user_keys()
346                .never();
347        });
348
349        let result = internal_rotate_user_keys(
350            &key_store,
351            &api_client,
352            RotateUserKeysRequest {
353                key_rotation_method: KeyRotationMethod::Password {
354                    password: "test".to_string(),
355                },
356                trusted_organization_public_keys: vec![],
357                trusted_emergency_access_public_keys: vec![],
358            },
359        )
360        .await;
361
362        assert!(matches!(result, Err(RotateUserKeysError::ApiError)));
363        if let ApiClient::Mock(mut mock) = api_client {
364            mock.sync_api.checkpoint();
365            mock.accounts_key_management_api.checkpoint();
366        }
367    }
368
369    #[tokio::test]
370    async fn test_rotate_user_keys_master_password_success() {
371        let (key_store, sync_response) = make_test_key_store_and_sync_response();
372        let api_client = ApiClient::new_mocked(|mock| {
373            mock.sync_api
374                .expect_get()
375                .once()
376                .returning(move |_| Ok(sync_response.clone()));
377            mock_empty_sync_calls(mock);
378            mock.accounts_key_management_api
379                .expect_rotate_user_keys()
380                .once()
381                .returning(|_| Ok(()));
382        });
383
384        let result = internal_rotate_user_keys(
385            &key_store,
386            &api_client,
387            RotateUserKeysRequest {
388                key_rotation_method: KeyRotationMethod::Password {
389                    password: "test_password".to_string(),
390                },
391                trusted_organization_public_keys: vec![],
392                trusted_emergency_access_public_keys: vec![],
393            },
394        )
395        .await;
396
397        assert!(result.is_ok());
398        if let ApiClient::Mock(mut mock) = api_client {
399            mock.sync_api.checkpoint();
400            mock.organizations_api.checkpoint();
401            mock.emergency_access_api.checkpoint();
402            mock.devices_api.checkpoint();
403            mock.web_authn_api.checkpoint();
404            mock.accounts_key_management_api.checkpoint();
405        }
406    }
407
408    #[tokio::test]
409    async fn test_rotate_user_keys_post_api_failure_returns_api_error() {
410        let (key_store, sync_response) = make_test_key_store_and_sync_response();
411        let api_client = ApiClient::new_mocked(|mock| {
412            mock.sync_api
413                .expect_get()
414                .once()
415                .returning(move |_| Ok(sync_response.clone()));
416            mock_empty_sync_calls(mock);
417            mock.accounts_key_management_api
418                .expect_rotate_user_keys()
419                .once()
420                .returning(|_| {
421                    Err(bitwarden_api_api::apis::Error::Serde(
422                        serde_json::Error::io(std::io::Error::other("API error")),
423                    ))
424                });
425        });
426
427        let result = internal_rotate_user_keys(
428            &key_store,
429            &api_client,
430            RotateUserKeysRequest {
431                key_rotation_method: KeyRotationMethod::Password {
432                    password: "test_password".to_string(),
433                },
434                trusted_organization_public_keys: vec![],
435                trusted_emergency_access_public_keys: vec![],
436            },
437        )
438        .await;
439
440        assert!(matches!(result, Err(RotateUserKeysError::ApiError)));
441        if let ApiClient::Mock(mut mock) = api_client {
442            mock.sync_api.checkpoint();
443            mock.organizations_api.checkpoint();
444            mock.emergency_access_api.checkpoint();
445            mock.devices_api.checkpoint();
446            mock.web_authn_api.checkpoint();
447            mock.accounts_key_management_api.checkpoint();
448        }
449    }
450}