1use 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 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 check_for_old_attachments(&sync.ciphers)?;
86
87 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 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, }]),
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 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}