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::{
4    KeySlotIds, MasterPasswordAuthenticationData,
5    account_cryptographic_state::WrappedAccountCryptographicState,
6};
7use bitwarden_crypto::{KeyStore, PublicKey};
8use serde::{Deserialize, Serialize};
9use tracing::{info, instrument};
10#[cfg(feature = "wasm")]
11use tsify::Tsify;
12#[cfg(feature = "wasm")]
13use wasm_bindgen::prelude::*;
14
15use crate::{
16    UserCryptoManagementClient,
17    key_rotation::{
18        RotateUserKeysError,
19        crypto::rotate_account_cryptographic_state_to_request_model,
20        data::{check_for_old_attachments, reencrypt_data},
21        rotation_context::make_rotation_context,
22        sync::{SyncedAccountData, sync_current_account_data},
23        unlock::{
24            ReencryptCommonUnlockDataInput, ReencryptMasterPasswordChangeAndUnlockInput,
25            reencrypt_master_password_change_unlock_data,
26        },
27    },
28};
29
30#[derive(Serialize, Deserialize, Clone)]
31#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
32#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
33pub struct PasswordChangeAndRotateUserKeysRequest {
34    pub old_password: String,
35    pub password: String,
36    pub hint: Option<String>,
37    pub trusted_emergency_access_public_keys: Vec<PublicKey>,
38    pub trusted_organization_public_keys: Vec<PublicKey>,
39}
40
41#[cfg_attr(feature = "wasm", wasm_bindgen)]
42impl UserCryptoManagementClient {
43    /// Combines a password change and user key rotation into a single request.
44    ///
45    /// Before rotating, this checks whether the user's public key encryption key pair needs
46    /// regeneration and fixes it if necessary. This ensures that key rotation can proceed even
47    /// if the existing private key is corrupt.
48    pub async fn password_change_and_rotate_user_keys(
49        &self,
50        request: PasswordChangeAndRotateUserKeysRequest,
51    ) -> Result<(), RotateUserKeysError> {
52        let api_client = &self.client.internal.get_api_configurations().api_client;
53        let key_store = self.client.internal.get_key_store();
54
55        let sync = sync_current_account_data(api_client)
56            .await
57            .map_err(|_| RotateUserKeysError::Api)?;
58
59        let wrapped_account_cryptographic_state = self
60            .regenerate_public_key_encryption_key_pair_if_needed_with_ciphers(&sync.ciphers)
61            .await
62            .map_err(|_| RotateUserKeysError::Crypto)?
63            .unwrap_or_else(|| sync.wrapped_account_cryptographic_state.clone());
64
65        internal_password_change_and_rotate_user_keys(
66            key_store,
67            api_client,
68            request,
69            wrapped_account_cryptographic_state,
70            sync,
71        )
72        .await
73    }
74}
75
76#[instrument(
77    name = "password_change_and_rotate_user_keys",
78    level = "info",
79    skip_all,
80    err
81)]
82async fn internal_password_change_and_rotate_user_keys(
83    key_store: &KeyStore<KeySlotIds>,
84    api_client: &bitwarden_api_api::apis::ApiClient,
85    request: PasswordChangeAndRotateUserKeysRequest,
86    wrapped_account_cryptographic_state: WrappedAccountCryptographicState,
87    sync: SyncedAccountData,
88) -> Result<(), RotateUserKeysError> {
89    // Fail early if any cipher has old attachments that would become irrecoverable
90    check_for_old_attachments(&sync.ciphers)?;
91
92    // Create a separate scope so that the mutable context is not held across the await point
93    let post_request = {
94        let mut ctx = key_store.context_mut();
95
96        let rotation_context = make_rotation_context(
97            &sync,
98            request.trusted_organization_public_keys.as_slice(),
99            request.trusted_emergency_access_public_keys.as_slice(),
100            &mut ctx,
101        )?;
102
103        info!("Rotating account cryptographic state for user key rotation");
104        let account_keys_model = rotate_account_cryptographic_state_to_request_model(
105            &wrapped_account_cryptographic_state,
106            &rotation_context.current_user_key_id,
107            &rotation_context.new_user_key_id,
108            &mut ctx,
109        )
110        .map_err(|_| RotateUserKeysError::Crypto)?;
111
112        info!("Re-encrypting account data for user key rotation");
113        let account_data_model = reencrypt_data(
114            sync.folders.as_slice(),
115            sync.ciphers.as_slice(),
116            sync.sends.as_slice(),
117            rotation_context.current_user_key_id,
118            rotation_context.new_user_key_id,
119            &mut ctx,
120        )
121        .map_err(|_| RotateUserKeysError::Crypto)?;
122
123        info!("Re-encrypting account unlock data for user key rotation");
124        let (kdf, salt) = sync.kdf_and_salt.ok_or(RotateUserKeysError::Api)?;
125        let unlock_data_model = reencrypt_master_password_change_unlock_data(
126            ReencryptMasterPasswordChangeAndUnlockInput {
127                password: request.password,
128                hint: request.hint,
129                kdf: kdf.clone(),
130                salt: salt.clone(),
131                common_unlock_data: ReencryptCommonUnlockDataInput {
132                    trusted_devices: sync.trusted_devices,
133                    webauthn_credentials: sync.passkeys,
134                    trusted_organization_keys: rotation_context.v1_organization_memberships,
135                    trusted_emergency_access_keys: rotation_context.v1_emergency_access_memberships,
136                },
137            },
138            rotation_context.current_user_key_id,
139            rotation_context.new_user_key_id,
140            &mut ctx,
141        )
142        .map_err(|_| RotateUserKeysError::Crypto)?;
143
144        let old_master_password_authentication_data =
145            MasterPasswordAuthenticationData::derive(&request.old_password, &kdf, &salt)
146                .map_err(|_| RotateUserKeysError::Crypto)?;
147
148        RotateUserAccountKeysAndDataRequestModel {
149            old_master_key_authentication_hash: Some(
150                old_master_password_authentication_data
151                    .master_password_authentication_hash
152                    .to_string(),
153            ),
154            account_keys: Box::new(account_keys_model),
155            account_data: Box::new(account_data_model),
156            account_unlock_data: Box::new(unlock_data_model),
157        }
158    };
159
160    info!("Posting rotated user account keys and data to server");
161    api_client
162        .accounts_key_management_api()
163        .password_change_and_rotate_user_account_keys(Some(post_request))
164        .await
165        .map_err(|_| RotateUserKeysError::Api)?;
166    info!("Successfully rotated user account keys and data");
167    Ok(())
168}
169
170#[cfg(test)]
171mod tests {
172    use std::str::FromStr;
173
174    use bitwarden_api_api::apis::ApiClient;
175    use bitwarden_core::key_management::{
176        KeySlotIds, SymmetricKeySlotId,
177        account_cryptographic_state::WrappedAccountCryptographicState,
178    };
179    use bitwarden_crypto::{Kdf, KeyStore, PublicKeyEncryptionAlgorithm, SymmetricKeyAlgorithm};
180    use bitwarden_vault::{Attachment, Cipher, CipherType};
181    use chrono::DateTime;
182
183    use super::*;
184
185    fn make_test_key_store_and_synced_data() -> (KeyStore<KeySlotIds>, SyncedAccountData) {
186        let store: KeyStore<KeySlotIds> = KeyStore::default();
187        let wrapped_private_key = {
188            let mut ctx = store.context_mut();
189            let user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
190            let _ = ctx.persist_symmetric_key(user_key, SymmetricKeySlotId::User);
191            let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
192            ctx.wrap_private_key(SymmetricKeySlotId::User, private_key)
193                .unwrap()
194        };
195
196        let sync = SyncedAccountData {
197            wrapped_account_cryptographic_state: WrappedAccountCryptographicState::V1 {
198                private_key: wrapped_private_key,
199            },
200            folders: vec![],
201            ciphers: vec![],
202            sends: vec![],
203            emergency_access_memberships: vec![],
204            organization_memberships: vec![],
205            trusted_devices: vec![],
206            passkeys: vec![],
207            kdf_and_salt: Some((
208                Kdf::PBKDF2 {
209                    iterations: std::num::NonZeroU32::new(600000).unwrap(),
210                },
211                "test_salt".to_string(),
212            )),
213        };
214
215        (store, sync)
216    }
217
218    #[tokio::test]
219    async fn test_password_change_and_rotate_user_keys_missing_kdf_returns_api_error() {
220        let (key_store, mut sync) = make_test_key_store_and_synced_data();
221        sync.kdf_and_salt = None;
222
223        let api_client = ApiClient::new_mocked(|mock| {
224            mock.accounts_key_management_api
225                .expect_password_change_and_rotate_user_account_keys()
226                .never();
227        });
228
229        let result = internal_password_change_and_rotate_user_keys(
230            &key_store,
231            &api_client,
232            PasswordChangeAndRotateUserKeysRequest {
233                old_password: "old_password".to_string(),
234                password: "new_password".to_string(),
235                hint: None,
236                trusted_organization_public_keys: vec![],
237                trusted_emergency_access_public_keys: vec![],
238            },
239            sync.wrapped_account_cryptographic_state.clone(),
240            sync,
241        )
242        .await;
243
244        assert!(matches!(result, Err(RotateUserKeysError::Api)));
245        if let ApiClient::Mock(mut mock) = api_client {
246            mock.accounts_key_management_api.checkpoint();
247        }
248    }
249
250    #[tokio::test]
251    async fn test_password_change_and_rotate_user_keys_success() {
252        let (key_store, sync) = make_test_key_store_and_synced_data();
253        let api_client = ApiClient::new_mocked(|mock| {
254            mock.accounts_key_management_api
255                .expect_password_change_and_rotate_user_account_keys()
256                .once()
257                .returning(|_| Ok(()));
258        });
259
260        let result = internal_password_change_and_rotate_user_keys(
261            &key_store,
262            &api_client,
263            PasswordChangeAndRotateUserKeysRequest {
264                old_password: "old_password".to_string(),
265                password: "new_password".to_string(),
266                hint: None,
267                trusted_organization_public_keys: vec![],
268                trusted_emergency_access_public_keys: vec![],
269            },
270            sync.wrapped_account_cryptographic_state.clone(),
271            sync,
272        )
273        .await;
274
275        assert!(result.is_ok());
276        if let ApiClient::Mock(mut mock) = api_client {
277            mock.accounts_key_management_api.checkpoint();
278        }
279    }
280
281    #[tokio::test]
282    async fn test_password_change_and_rotate_user_keys_post_api_failure_returns_api_error() {
283        let (key_store, sync) = make_test_key_store_and_synced_data();
284        let api_client = ApiClient::new_mocked(|mock| {
285            mock.accounts_key_management_api
286                .expect_password_change_and_rotate_user_account_keys()
287                .once()
288                .returning(|_| {
289                    Err(serde_json::Error::io(std::io::Error::other("API error")).into())
290                });
291        });
292
293        let result = internal_password_change_and_rotate_user_keys(
294            &key_store,
295            &api_client,
296            PasswordChangeAndRotateUserKeysRequest {
297                old_password: "old_password".to_string(),
298                password: "new_password".to_string(),
299                hint: None,
300                trusted_organization_public_keys: vec![],
301                trusted_emergency_access_public_keys: vec![],
302            },
303            sync.wrapped_account_cryptographic_state.clone(),
304            sync,
305        )
306        .await;
307
308        assert!(matches!(result, Err(RotateUserKeysError::Api)));
309        if let ApiClient::Mock(mut mock) = api_client {
310            mock.accounts_key_management_api.checkpoint();
311        }
312    }
313
314    #[tokio::test]
315    async fn test_password_change_and_rotate_old_attachments_returns_error() {
316        let (key_store, mut sync) = make_test_key_store_and_synced_data();
317        let enc_string = "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=";
318
319        // Add a cipher with an old attachment (key is None)
320        sync.ciphers = vec![Cipher {
321            id: None,
322            organization_id: None,
323            folder_id: None,
324            collection_ids: vec![],
325            r#type: CipherType::Login,
326            login: None,
327            identity: None,
328            card: None,
329            secure_note: None,
330            ssh_key: None,
331            bank_account: None,
332            drivers_license: None,
333            passport: None,
334            favorite: false,
335            reprompt: Default::default(),
336            organization_use_totp: false,
337            edit: false,
338            permissions: None,
339            view_password: false,
340            name: enc_string.parse().unwrap(),
341            revision_date: DateTime::from_str("2024-01-01T00:00:00Z").unwrap(),
342            archived_date: None,
343            creation_date: DateTime::from_str("2024-01-01T00:00:00Z").unwrap(),
344            attachments: Some(vec![Attachment {
345                id: None,
346                url: None,
347                size: None,
348                size_name: None,
349                file_name: None,
350                key: None, // Old attachment - no per-attachment key
351            }]),
352            fields: None,
353            key: None,
354            notes: None,
355            local_data: None,
356            password_history: None,
357            deleted_date: None,
358            data: None,
359        }];
360
361        let api_client = ApiClient::new_mocked(|mock| {
362            // Rotation API should never be called
363            mock.accounts_key_management_api
364                .expect_password_change_and_rotate_user_account_keys()
365                .never();
366        });
367
368        let result = internal_password_change_and_rotate_user_keys(
369            &key_store,
370            &api_client,
371            PasswordChangeAndRotateUserKeysRequest {
372                old_password: "old_password".to_string(),
373                password: "new_password".to_string(),
374                hint: None,
375                trusted_organization_public_keys: vec![],
376                trusted_emergency_access_public_keys: vec![],
377            },
378            sync.wrapped_account_cryptographic_state.clone(),
379            sync,
380        )
381        .await;
382
383        assert!(matches!(result, Err(RotateUserKeysError::OldAttachments)));
384        if let ApiClient::Mock(mut mock) = api_client {
385            mock.accounts_key_management_api.checkpoint();
386        }
387    }
388}