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