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