1use bitwarden_api_api::models::RotateUserAccountKeysAndDataRequestModel;
3use bitwarden_core::key_management::{KeySlotIds, MasterPasswordAuthenticationData};
4use bitwarden_crypto::{KeyStore, PublicKey};
5use serde::{Deserialize, Serialize};
6use tracing::{info, instrument};
7#[cfg(feature = "wasm")]
8use tsify::Tsify;
9#[cfg(feature = "wasm")]
10use wasm_bindgen::prelude::*;
11
12use crate::{
13 UserCryptoManagementClient,
14 key_rotation::{
15 RotateUserKeysError,
16 crypto::rotate_account_cryptographic_state_to_request_model,
17 data::{check_for_old_attachments, reencrypt_data},
18 rotation_context::make_rotation_context,
19 sync::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#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
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#[cfg_attr(feature = "wasm", wasm_bindgen)]
39impl UserCryptoManagementClient {
40 pub async fn password_change_and_rotate_user_keys(
42 &self,
43 request: PasswordChangeAndRotateUserKeysRequest,
44 ) -> Result<(), RotateUserKeysError> {
45 let api_client = &self.client.internal.get_api_configurations().api_client;
46 let key_store = self.client.internal.get_key_store();
47 internal_password_change_and_rotate_user_keys(key_store, api_client, request).await
48 }
49}
50
51#[instrument(
52 name = "password_change_and_rotate_user_keys",
53 level = "info",
54 skip_all,
55 err
56)]
57async fn internal_password_change_and_rotate_user_keys(
58 key_store: &KeyStore<KeySlotIds>,
59 api_client: &bitwarden_api_api::apis::ApiClient,
60 request: PasswordChangeAndRotateUserKeysRequest,
61) -> Result<(), RotateUserKeysError> {
62 let sync = sync_current_account_data(api_client)
63 .await
64 .map_err(|_| RotateUserKeysError::Api)?;
65
66 check_for_old_attachments(&sync.ciphers)?;
68
69 let post_request = {
71 let mut ctx = key_store.context_mut();
72
73 let rotation_context = make_rotation_context(
74 &sync,
75 request.trusted_organization_public_keys.as_slice(),
76 request.trusted_emergency_access_public_keys.as_slice(),
77 &mut ctx,
78 )?;
79
80 info!("Rotating account cryptographic state for user key rotation");
81 let account_keys_model = rotate_account_cryptographic_state_to_request_model(
82 &sync.wrapped_account_cryptographic_state,
83 &rotation_context.current_user_key_id,
84 &rotation_context.new_user_key_id,
85 &mut ctx,
86 )
87 .map_err(|_| RotateUserKeysError::Crypto)?;
88
89 info!("Re-encrypting account data for user key rotation");
90 let account_data_model = reencrypt_data(
91 sync.folders.as_slice(),
92 sync.ciphers.as_slice(),
93 sync.sends.as_slice(),
94 rotation_context.current_user_key_id,
95 rotation_context.new_user_key_id,
96 &mut ctx,
97 )
98 .map_err(|_| RotateUserKeysError::Crypto)?;
99
100 info!("Re-encrypting account unlock data for user key rotation");
101 let (kdf, salt) = sync.kdf_and_salt.ok_or(RotateUserKeysError::Api)?;
102 let unlock_data_model = reencrypt_master_password_change_unlock_data(
103 ReencryptMasterPasswordChangeAndUnlockInput {
104 password: request.password,
105 hint: request.hint,
106 kdf: kdf.clone(),
107 salt: salt.clone(),
108 common_unlock_data: ReencryptCommonUnlockDataInput {
109 trusted_devices: sync.trusted_devices,
110 webauthn_credentials: sync.passkeys,
111 trusted_organization_keys: rotation_context.v1_organization_memberships,
112 trusted_emergency_access_keys: rotation_context.v1_emergency_access_memberships,
113 },
114 },
115 rotation_context.current_user_key_id,
116 rotation_context.new_user_key_id,
117 &mut ctx,
118 )
119 .map_err(|_| RotateUserKeysError::Crypto)?;
120
121 let old_master_password_authentication_data =
122 MasterPasswordAuthenticationData::derive(&request.old_password, &kdf, &salt)
123 .map_err(|_| RotateUserKeysError::Crypto)?;
124
125 RotateUserAccountKeysAndDataRequestModel {
126 old_master_key_authentication_hash: Some(
127 old_master_password_authentication_data
128 .master_password_authentication_hash
129 .to_string(),
130 ),
131 account_keys: Box::new(account_keys_model),
132 account_data: Box::new(account_data_model),
133 account_unlock_data: Box::new(unlock_data_model),
134 }
135 };
136
137 info!("Posting rotated user account keys and data to server");
138 api_client
139 .accounts_key_management_api()
140 .password_change_and_rotate_user_account_keys(Some(post_request))
141 .await
142 .map_err(|_| RotateUserKeysError::Api)?;
143 info!("Successfully rotated user account keys and data");
144 Ok(())
145}
146
147#[cfg(test)]
148mod tests {
149 use bitwarden_api_api::{
150 apis::ApiClient,
151 models::{
152 DeviceAuthRequestResponseModelListResponseModel,
153 EmergencyAccessGranteeDetailsResponseModelListResponseModel, KdfType,
154 MasterPasswordUnlockKdfResponseModel, MasterPasswordUnlockResponseModel,
155 PrivateKeysResponseModel, ProfileOrganizationResponseModelListResponseModel,
156 ProfileResponseModel, PublicKeyEncryptionKeyPairResponseModel, SyncResponseModel,
157 UserDecryptionResponseModel, WebAuthnCredentialResponseModelListResponseModel,
158 },
159 };
160 use bitwarden_core::key_management::{KeySlotIds, SymmetricKeySlotId};
161 use bitwarden_crypto::{KeyStore, PublicKeyEncryptionAlgorithm, SymmetricKeyAlgorithm};
162
163 use super::*;
164
165 fn make_test_key_store_and_sync_response() -> (KeyStore<KeySlotIds>, SyncResponseModel) {
166 let store: KeyStore<KeySlotIds> = KeyStore::default();
167 let wrapped_private_key = {
168 let mut ctx = store.context_mut();
169 let user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
170 let _ = ctx.persist_symmetric_key(user_key, SymmetricKeySlotId::User);
171 let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
172 ctx.wrap_private_key(SymmetricKeySlotId::User, private_key)
173 .unwrap()
174 };
175
176 let sync_response = SyncResponseModel {
177 object: Some("sync".to_string()),
178 profile: Some(Box::new(ProfileResponseModel {
179 id: Some(uuid::Uuid::new_v4()),
180 account_keys: Some(Box::new(PrivateKeysResponseModel {
181 object: None,
182 signature_key_pair: None,
183 public_key_encryption_key_pair: Box::new(
184 PublicKeyEncryptionKeyPairResponseModel {
185 object: None,
186 wrapped_private_key: Some(wrapped_private_key.to_string()),
187 public_key: None,
188 signed_public_key: None,
189 },
190 ),
191 security_state: None,
192 })),
193 ..ProfileResponseModel::default()
194 })),
195 folders: Some(vec![]),
196 ciphers: Some(vec![]),
197 sends: Some(vec![]),
198 user_decryption: Some(Box::new(UserDecryptionResponseModel {
199 master_password_unlock: Some(Box::new(MasterPasswordUnlockResponseModel {
200 kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
201 kdf_type: KdfType::PBKDF2_SHA256,
202 iterations: 600000,
203 memory: None,
204 parallelism: None,
205 }),
206 master_key_encrypted_user_key: None,
207 salt: Some("test_salt".to_string()),
208 })),
209 web_authn_prf_options: None,
210 v2_upgrade_token: None,
211 })),
212 ..Default::default()
213 };
214
215 (store, sync_response)
216 }
217
218 fn mock_empty_sync_calls(mock: &mut bitwarden_api_api::apis::ApiClientMock) {
219 mock.organizations_api
220 .expect_get_user()
221 .once()
222 .returning(|| {
223 Ok(ProfileOrganizationResponseModelListResponseModel {
224 object: None,
225 data: Some(vec![]),
226 continuation_token: None,
227 })
228 });
229 mock.emergency_access_api
230 .expect_get_contacts()
231 .once()
232 .returning(|| {
233 Ok(
234 EmergencyAccessGranteeDetailsResponseModelListResponseModel {
235 object: None,
236 data: Some(vec![]),
237 continuation_token: None,
238 },
239 )
240 });
241 mock.devices_api.expect_get_all().once().returning(|| {
242 Ok(DeviceAuthRequestResponseModelListResponseModel {
243 object: None,
244 data: Some(vec![]),
245 continuation_token: None,
246 })
247 });
248 mock.web_authn_api.expect_get().once().returning(|| {
249 Ok(WebAuthnCredentialResponseModelListResponseModel {
250 object: None,
251 data: Some(vec![]),
252 continuation_token: None,
253 })
254 });
255 }
256
257 #[tokio::test]
258 async fn test_password_change_and_rotate_user_keys_sync_api_failure_returns_api_error() {
259 let store: KeyStore<KeySlotIds> = KeyStore::default();
260 let api_client = ApiClient::new_mocked(|mock| {
261 mock.sync_api.expect_get().once().returning(|_| {
262 Err(serde_json::Error::io(std::io::Error::other("network error")).into())
263 });
264 mock.accounts_key_management_api
265 .expect_password_change_and_rotate_user_account_keys()
266 .never();
267 });
268
269 let result = internal_password_change_and_rotate_user_keys(
270 &store,
271 &api_client,
272 PasswordChangeAndRotateUserKeysRequest {
273 old_password: "old_password".to_string(),
274 password: "new_password".to_string(),
275 hint: None,
276 trusted_organization_public_keys: vec![],
277 trusted_emergency_access_public_keys: vec![],
278 },
279 )
280 .await;
281
282 assert!(matches!(result, Err(RotateUserKeysError::Api)));
283 if let ApiClient::Mock(mut mock) = api_client {
284 mock.sync_api.checkpoint();
285 mock.accounts_key_management_api.checkpoint();
286 }
287 }
288
289 #[tokio::test]
290 async fn test_password_change_and_rotate_user_keys_missing_kdf_returns_api_error() {
291 let (key_store, mut sync_response) = make_test_key_store_and_sync_response();
292 if let Some(user_decryption) = sync_response.user_decryption.as_mut() {
294 user_decryption.master_password_unlock = None;
295 }
296 let api_client = ApiClient::new_mocked(|mock| {
297 mock.sync_api
298 .expect_get()
299 .once()
300 .returning(move |_| Ok(sync_response.clone()));
301 mock_empty_sync_calls(mock);
302 mock.accounts_key_management_api
303 .expect_password_change_and_rotate_user_account_keys()
304 .never();
305 });
306
307 let result = internal_password_change_and_rotate_user_keys(
308 &key_store,
309 &api_client,
310 PasswordChangeAndRotateUserKeysRequest {
311 old_password: "old_password".to_string(),
312 password: "new_password".to_string(),
313 hint: None,
314 trusted_organization_public_keys: vec![],
315 trusted_emergency_access_public_keys: vec![],
316 },
317 )
318 .await;
319
320 assert!(matches!(result, Err(RotateUserKeysError::Api)));
321 if let ApiClient::Mock(mut mock) = api_client {
322 mock.sync_api.checkpoint();
323 mock.organizations_api.checkpoint();
324 mock.emergency_access_api.checkpoint();
325 mock.devices_api.checkpoint();
326 mock.web_authn_api.checkpoint();
327 mock.accounts_key_management_api.checkpoint();
328 }
329 }
330
331 #[tokio::test]
332 async fn test_password_change_and_rotate_user_keys_success() {
333 let (key_store, sync_response) = make_test_key_store_and_sync_response();
334 let api_client = ApiClient::new_mocked(|mock| {
335 mock.sync_api
336 .expect_get()
337 .once()
338 .returning(move |_| Ok(sync_response.clone()));
339 mock_empty_sync_calls(mock);
340 mock.accounts_key_management_api
341 .expect_password_change_and_rotate_user_account_keys()
342 .once()
343 .returning(|_| Ok(()));
344 });
345
346 let result = internal_password_change_and_rotate_user_keys(
347 &key_store,
348 &api_client,
349 PasswordChangeAndRotateUserKeysRequest {
350 old_password: "old_password".to_string(),
351 password: "new_password".to_string(),
352 hint: None,
353 trusted_organization_public_keys: vec![],
354 trusted_emergency_access_public_keys: vec![],
355 },
356 )
357 .await;
358
359 assert!(result.is_ok());
360 if let ApiClient::Mock(mut mock) = api_client {
361 mock.sync_api.checkpoint();
362 mock.organizations_api.checkpoint();
363 mock.emergency_access_api.checkpoint();
364 mock.devices_api.checkpoint();
365 mock.web_authn_api.checkpoint();
366 mock.accounts_key_management_api.checkpoint();
367 }
368 }
369
370 #[tokio::test]
371 async fn test_password_change_and_rotate_user_keys_post_api_failure_returns_api_error() {
372 let (key_store, sync_response) = make_test_key_store_and_sync_response();
373 let api_client = ApiClient::new_mocked(|mock| {
374 mock.sync_api
375 .expect_get()
376 .once()
377 .returning(move |_| Ok(sync_response.clone()));
378 mock_empty_sync_calls(mock);
379 mock.accounts_key_management_api
380 .expect_password_change_and_rotate_user_account_keys()
381 .once()
382 .returning(|_| {
383 Err(serde_json::Error::io(std::io::Error::other("API error")).into())
384 });
385 });
386
387 let result = internal_password_change_and_rotate_user_keys(
388 &key_store,
389 &api_client,
390 PasswordChangeAndRotateUserKeysRequest {
391 old_password: "old_password".to_string(),
392 password: "new_password".to_string(),
393 hint: None,
394 trusted_organization_public_keys: vec![],
395 trusted_emergency_access_public_keys: vec![],
396 },
397 )
398 .await;
399
400 assert!(matches!(result, Err(RotateUserKeysError::Api)));
401 if let ApiClient::Mock(mut mock) = api_client {
402 mock.sync_api.checkpoint();
403 mock.organizations_api.checkpoint();
404 mock.emergency_access_api.checkpoint();
405 mock.devices_api.checkpoint();
406 mock.web_authn_api.checkpoint();
407 mock.accounts_key_management_api.checkpoint();
408 }
409 }
410
411 #[tokio::test]
412 async fn test_password_change_and_rotate_old_attachments_returns_error() {
413 use bitwarden_api_api::models::{
414 AttachmentResponseModel, CipherDetailsResponseModel, CipherType,
415 };
416
417 let (key_store, mut sync_response) = make_test_key_store_and_sync_response();
418 let enc_string = "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=";
419
420 sync_response.ciphers = Some(vec![CipherDetailsResponseModel {
422 id: Some(uuid::Uuid::new_v4()),
423 organization_id: None,
424 r#type: Some(CipherType::Login),
425 name: Some(enc_string.to_string()),
426 revision_date: Some("2024-01-01T00:00:00Z".to_string()),
427 creation_date: Some("2024-01-01T00:00:00Z".to_string()),
428 attachments: Some(vec![AttachmentResponseModel {
429 id: Some("att1".to_string()),
430 file_name: Some(enc_string.to_string()),
431 key: None, ..AttachmentResponseModel::new()
433 }]),
434 ..CipherDetailsResponseModel::new()
435 }]);
436
437 let api_client = ApiClient::new_mocked(|mock| {
438 mock.sync_api
439 .expect_get()
440 .once()
441 .returning(move |_| Ok(sync_response.clone()));
442 mock_empty_sync_calls(mock);
443 mock.accounts_key_management_api
445 .expect_password_change_and_rotate_user_account_keys()
446 .never();
447 });
448
449 let result = internal_password_change_and_rotate_user_keys(
450 &key_store,
451 &api_client,
452 PasswordChangeAndRotateUserKeysRequest {
453 old_password: "old_password".to_string(),
454 password: "new_password".to_string(),
455 hint: None,
456 trusted_organization_public_keys: vec![],
457 trusted_emergency_access_public_keys: vec![],
458 },
459 )
460 .await;
461
462 assert!(matches!(result, Err(RotateUserKeysError::OldAttachments)));
463 if let ApiClient::Mock(mut mock) = api_client {
464 mock.sync_api.checkpoint();
465 mock.organizations_api.checkpoint();
466 mock.emergency_access_api.checkpoint();
467 mock.devices_api.checkpoint();
468 mock.web_authn_api.checkpoint();
469 mock.accounts_key_management_api.checkpoint();
470 }
471 }
472}