Skip to main content

bitwarden_user_crypto_management/key_rotation/
password_change_and_rotate_user_keys.rs

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