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