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::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::ApiError)?;
65
66 let post_request = {
68 let mut ctx = key_store.context_mut();
69
70 let rotation_context = make_rotation_context(
71 &sync,
72 request.trusted_organization_public_keys.as_slice(),
73 request.trusted_emergency_access_public_keys.as_slice(),
74 &mut ctx,
75 )?;
76
77 info!("Rotating account cryptographic state for user key rotation");
78 let account_keys_model = rotate_account_cryptographic_state_to_request_model(
79 &sync.wrapped_account_cryptographic_state,
80 &rotation_context.current_user_key_id,
81 &rotation_context.new_user_key_id,
82 &mut ctx,
83 )
84 .map_err(|_| RotateUserKeysError::CryptoError)?;
85
86 info!("Re-encrypting account data for user key rotation");
87 let account_data_model = reencrypt_data(
88 sync.folders.as_slice(),
89 sync.ciphers.as_slice(),
90 sync.sends.as_slice(),
91 rotation_context.current_user_key_id,
92 rotation_context.new_user_key_id,
93 &mut ctx,
94 )
95 .map_err(|_| RotateUserKeysError::CryptoError)?;
96
97 info!("Re-encrypting account unlock data for user key rotation");
98 let (kdf, salt) = sync.kdf_and_salt.ok_or(RotateUserKeysError::ApiError)?;
99 let unlock_data_model = reencrypt_master_password_change_unlock_data(
100 ReencryptMasterPasswordChangeAndUnlockInput {
101 password: request.password,
102 hint: request.hint,
103 kdf: kdf.clone(),
104 salt: salt.clone(),
105 common_unlock_data: ReencryptCommonUnlockDataInput {
106 trusted_devices: sync.trusted_devices,
107 webauthn_credentials: sync.passkeys,
108 trusted_organization_keys: rotation_context.v1_organization_memberships,
109 trusted_emergency_access_keys: rotation_context.v1_emergency_access_memberships,
110 },
111 },
112 rotation_context.current_user_key_id,
113 rotation_context.new_user_key_id,
114 &mut ctx,
115 )
116 .map_err(|_| RotateUserKeysError::CryptoError)?;
117
118 let old_master_password_authentication_data =
119 MasterPasswordAuthenticationData::derive(&request.old_password, &kdf, &salt)
120 .map_err(|_| RotateUserKeysError::CryptoError)?;
121
122 RotateUserAccountKeysAndDataRequestModel {
123 old_master_key_authentication_hash: Some(
124 old_master_password_authentication_data
125 .master_password_authentication_hash
126 .to_string(),
127 ),
128 account_keys: Box::new(account_keys_model),
129 account_data: Box::new(account_data_model),
130 account_unlock_data: Box::new(unlock_data_model),
131 }
132 };
133
134 info!("Posting rotated user account keys and data to server");
135 api_client
136 .accounts_key_management_api()
137 .password_change_and_rotate_user_account_keys(Some(post_request))
138 .await
139 .map_err(|_| RotateUserKeysError::ApiError)?;
140 info!("Successfully rotated user account keys and data");
141 Ok(())
142}
143
144#[cfg(test)]
145mod tests {
146 use bitwarden_api_api::{
147 apis::ApiClient,
148 models::{
149 DeviceAuthRequestResponseModelListResponseModel,
150 EmergencyAccessGranteeDetailsResponseModelListResponseModel, KdfType,
151 MasterPasswordUnlockKdfResponseModel, MasterPasswordUnlockResponseModel,
152 PrivateKeysResponseModel, ProfileOrganizationResponseModelListResponseModel,
153 ProfileResponseModel, PublicKeyEncryptionKeyPairResponseModel, SyncResponseModel,
154 UserDecryptionResponseModel, WebAuthnCredentialResponseModelListResponseModel,
155 },
156 };
157 use bitwarden_core::key_management::{KeySlotIds, SymmetricKeySlotId};
158 use bitwarden_crypto::{KeyStore, PublicKeyEncryptionAlgorithm, SymmetricKeyAlgorithm};
159
160 use super::*;
161
162 fn make_test_key_store_and_sync_response() -> (KeyStore<KeySlotIds>, SyncResponseModel) {
163 let store: KeyStore<KeySlotIds> = KeyStore::default();
164 let wrapped_private_key = {
165 let mut ctx = store.context_mut();
166 let user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
167 let _ = ctx.persist_symmetric_key(user_key, SymmetricKeySlotId::User);
168 let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
169 ctx.wrap_private_key(SymmetricKeySlotId::User, private_key)
170 .unwrap()
171 };
172
173 let sync_response = SyncResponseModel {
174 object: Some("sync".to_string()),
175 profile: Some(Box::new(ProfileResponseModel {
176 id: Some(uuid::Uuid::new_v4()),
177 account_keys: Some(Box::new(PrivateKeysResponseModel {
178 object: None,
179 signature_key_pair: None,
180 public_key_encryption_key_pair: Box::new(
181 PublicKeyEncryptionKeyPairResponseModel {
182 object: None,
183 wrapped_private_key: Some(wrapped_private_key.to_string()),
184 public_key: None,
185 signed_public_key: None,
186 },
187 ),
188 security_state: None,
189 })),
190 ..ProfileResponseModel::default()
191 })),
192 folders: Some(vec![]),
193 ciphers: Some(vec![]),
194 sends: Some(vec![]),
195 collections: None,
196 domains: None,
197 policies: None,
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 };
213
214 (store, sync_response)
215 }
216
217 fn mock_empty_sync_calls(mock: &mut bitwarden_api_api::apis::ApiClientMock) {
218 mock.organizations_api
219 .expect_get_user()
220 .once()
221 .returning(|| {
222 Ok(ProfileOrganizationResponseModelListResponseModel {
223 object: None,
224 data: Some(vec![]),
225 continuation_token: None,
226 })
227 });
228 mock.emergency_access_api
229 .expect_get_contacts()
230 .once()
231 .returning(|| {
232 Ok(
233 EmergencyAccessGranteeDetailsResponseModelListResponseModel {
234 object: None,
235 data: Some(vec![]),
236 continuation_token: None,
237 },
238 )
239 });
240 mock.devices_api.expect_get_all().once().returning(|| {
241 Ok(DeviceAuthRequestResponseModelListResponseModel {
242 object: None,
243 data: Some(vec![]),
244 continuation_token: None,
245 })
246 });
247 mock.web_authn_api.expect_get().once().returning(|| {
248 Ok(WebAuthnCredentialResponseModelListResponseModel {
249 object: None,
250 data: Some(vec![]),
251 continuation_token: None,
252 })
253 });
254 }
255
256 #[tokio::test]
257 async fn test_password_change_and_rotate_user_keys_sync_api_failure_returns_api_error() {
258 let store: KeyStore<KeySlotIds> = KeyStore::default();
259 let api_client = ApiClient::new_mocked(|mock| {
260 mock.sync_api.expect_get().once().returning(|_| {
261 Err(bitwarden_api_api::apis::Error::Serde(
262 serde_json::Error::io(std::io::Error::other("network error")),
263 ))
264 });
265 mock.accounts_key_management_api
266 .expect_password_change_and_rotate_user_account_keys()
267 .never();
268 });
269
270 let result = internal_password_change_and_rotate_user_keys(
271 &store,
272 &api_client,
273 PasswordChangeAndRotateUserKeysRequest {
274 old_password: "old_password".to_string(),
275 password: "new_password".to_string(),
276 hint: None,
277 trusted_organization_public_keys: vec![],
278 trusted_emergency_access_public_keys: vec![],
279 },
280 )
281 .await;
282
283 assert!(matches!(result, Err(RotateUserKeysError::ApiError)));
284 if let ApiClient::Mock(mut mock) = api_client {
285 mock.sync_api.checkpoint();
286 mock.accounts_key_management_api.checkpoint();
287 }
288 }
289
290 #[tokio::test]
291 async fn test_password_change_and_rotate_user_keys_missing_kdf_returns_api_error() {
292 let (key_store, mut sync_response) = make_test_key_store_and_sync_response();
293 if let Some(user_decryption) = sync_response.user_decryption.as_mut() {
295 user_decryption.master_password_unlock = None;
296 }
297 let api_client = ApiClient::new_mocked(|mock| {
298 mock.sync_api
299 .expect_get()
300 .once()
301 .returning(move |_| Ok(sync_response.clone()));
302 mock_empty_sync_calls(mock);
303 mock.accounts_key_management_api
304 .expect_password_change_and_rotate_user_account_keys()
305 .never();
306 });
307
308 let result = internal_password_change_and_rotate_user_keys(
309 &key_store,
310 &api_client,
311 PasswordChangeAndRotateUserKeysRequest {
312 old_password: "old_password".to_string(),
313 password: "new_password".to_string(),
314 hint: None,
315 trusted_organization_public_keys: vec![],
316 trusted_emergency_access_public_keys: vec![],
317 },
318 )
319 .await;
320
321 assert!(matches!(result, Err(RotateUserKeysError::ApiError)));
322 if let ApiClient::Mock(mut mock) = api_client {
323 mock.sync_api.checkpoint();
324 mock.organizations_api.checkpoint();
325 mock.emergency_access_api.checkpoint();
326 mock.devices_api.checkpoint();
327 mock.web_authn_api.checkpoint();
328 mock.accounts_key_management_api.checkpoint();
329 }
330 }
331
332 #[tokio::test]
333 async fn test_password_change_and_rotate_user_keys_success() {
334 let (key_store, sync_response) = make_test_key_store_and_sync_response();
335 let api_client = ApiClient::new_mocked(|mock| {
336 mock.sync_api
337 .expect_get()
338 .once()
339 .returning(move |_| Ok(sync_response.clone()));
340 mock_empty_sync_calls(mock);
341 mock.accounts_key_management_api
342 .expect_password_change_and_rotate_user_account_keys()
343 .once()
344 .returning(|_| Ok(()));
345 });
346
347 let result = internal_password_change_and_rotate_user_keys(
348 &key_store,
349 &api_client,
350 PasswordChangeAndRotateUserKeysRequest {
351 old_password: "old_password".to_string(),
352 password: "new_password".to_string(),
353 hint: None,
354 trusted_organization_public_keys: vec![],
355 trusted_emergency_access_public_keys: vec![],
356 },
357 )
358 .await;
359
360 assert!(result.is_ok());
361 if let ApiClient::Mock(mut mock) = api_client {
362 mock.sync_api.checkpoint();
363 mock.organizations_api.checkpoint();
364 mock.emergency_access_api.checkpoint();
365 mock.devices_api.checkpoint();
366 mock.web_authn_api.checkpoint();
367 mock.accounts_key_management_api.checkpoint();
368 }
369 }
370
371 #[tokio::test]
372 async fn test_password_change_and_rotate_user_keys_post_api_failure_returns_api_error() {
373 let (key_store, sync_response) = make_test_key_store_and_sync_response();
374 let api_client = ApiClient::new_mocked(|mock| {
375 mock.sync_api
376 .expect_get()
377 .once()
378 .returning(move |_| Ok(sync_response.clone()));
379 mock_empty_sync_calls(mock);
380 mock.accounts_key_management_api
381 .expect_password_change_and_rotate_user_account_keys()
382 .once()
383 .returning(|_| {
384 Err(bitwarden_api_api::apis::Error::Serde(
385 serde_json::Error::io(std::io::Error::other("API error")),
386 ))
387 });
388 });
389
390 let result = internal_password_change_and_rotate_user_keys(
391 &key_store,
392 &api_client,
393 PasswordChangeAndRotateUserKeysRequest {
394 old_password: "old_password".to_string(),
395 password: "new_password".to_string(),
396 hint: None,
397 trusted_organization_public_keys: vec![],
398 trusted_emergency_access_public_keys: vec![],
399 },
400 )
401 .await;
402
403 assert!(matches!(result, Err(RotateUserKeysError::ApiError)));
404 if let ApiClient::Mock(mut mock) = api_client {
405 mock.sync_api.checkpoint();
406 mock.organizations_api.checkpoint();
407 mock.emergency_access_api.checkpoint();
408 mock.devices_api.checkpoint();
409 mock.web_authn_api.checkpoint();
410 mock.accounts_key_management_api.checkpoint();
411 }
412 }
413}