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, instrument};
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#[instrument(
77 name = "password_change_and_rotate_user_keys",
78 level = "info",
79 skip_all,
80 err
81)]
82async fn internal_password_change_and_rotate_user_keys(
83 key_store: &KeyStore<KeySlotIds>,
84 api_client: &bitwarden_api_api::apis::ApiClient,
85 request: PasswordChangeAndRotateUserKeysRequest,
86 wrapped_account_cryptographic_state: WrappedAccountCryptographicState,
87 sync: SyncedAccountData,
88) -> Result<(), RotateUserKeysError> {
89 check_for_old_attachments(&sync.ciphers)?;
91
92 let post_request = {
94 let mut ctx = key_store.context_mut();
95
96 let rotation_context = make_rotation_context(
97 &sync,
98 request.trusted_organization_public_keys.as_slice(),
99 request.trusted_emergency_access_public_keys.as_slice(),
100 &mut ctx,
101 )?;
102
103 info!("Rotating account cryptographic state for user key rotation");
104 let account_keys_model = rotate_account_cryptographic_state_to_request_model(
105 &wrapped_account_cryptographic_state,
106 &rotation_context.current_user_key_id,
107 &rotation_context.new_user_key_id,
108 &mut ctx,
109 )
110 .map_err(|_| RotateUserKeysError::Crypto)?;
111
112 info!("Re-encrypting account data for user key rotation");
113 let account_data_model = reencrypt_data(
114 sync.folders.as_slice(),
115 sync.ciphers.as_slice(),
116 sync.sends.as_slice(),
117 rotation_context.current_user_key_id,
118 rotation_context.new_user_key_id,
119 &mut ctx,
120 )
121 .map_err(|_| RotateUserKeysError::Crypto)?;
122
123 info!("Re-encrypting account unlock data for user key rotation");
124 let (kdf, salt) = sync.kdf_and_salt.ok_or(RotateUserKeysError::Api)?;
125 let unlock_data_model = reencrypt_master_password_change_unlock_data(
126 ReencryptMasterPasswordChangeAndUnlockInput {
127 password: request.password,
128 hint: request.hint,
129 kdf: kdf.clone(),
130 salt: salt.clone(),
131 common_unlock_data: ReencryptCommonUnlockDataInput {
132 trusted_devices: sync.trusted_devices,
133 webauthn_credentials: sync.passkeys,
134 trusted_organization_keys: rotation_context.v1_organization_memberships,
135 trusted_emergency_access_keys: rotation_context.v1_emergency_access_memberships,
136 },
137 },
138 rotation_context.current_user_key_id,
139 rotation_context.new_user_key_id,
140 &mut ctx,
141 )
142 .map_err(|_| RotateUserKeysError::Crypto)?;
143
144 let old_master_password_authentication_data =
145 MasterPasswordAuthenticationData::derive(&request.old_password, &kdf, &salt)
146 .map_err(|_| RotateUserKeysError::Crypto)?;
147
148 RotateUserAccountKeysAndDataRequestModel {
149 old_master_key_authentication_hash: Some(
150 old_master_password_authentication_data
151 .master_password_authentication_hash
152 .to_string(),
153 ),
154 account_keys: Box::new(account_keys_model),
155 account_data: Box::new(account_data_model),
156 account_unlock_data: Box::new(unlock_data_model),
157 }
158 };
159
160 info!("Posting rotated user account keys and data to server");
161 api_client
162 .accounts_key_management_api()
163 .password_change_and_rotate_user_account_keys(Some(post_request))
164 .await
165 .map_err(|_| RotateUserKeysError::Api)?;
166 info!("Successfully rotated user account keys and data");
167 Ok(())
168}
169
170#[cfg(test)]
171mod tests {
172 use std::str::FromStr;
173
174 use bitwarden_api_api::apis::ApiClient;
175 use bitwarden_core::key_management::{
176 KeySlotIds, SymmetricKeySlotId,
177 account_cryptographic_state::WrappedAccountCryptographicState,
178 };
179 use bitwarden_crypto::{Kdf, KeyStore, PublicKeyEncryptionAlgorithm, SymmetricKeyAlgorithm};
180 use bitwarden_vault::{Attachment, Cipher, CipherType};
181 use chrono::DateTime;
182
183 use super::*;
184
185 fn make_test_key_store_and_synced_data() -> (KeyStore<KeySlotIds>, SyncedAccountData) {
186 let store: KeyStore<KeySlotIds> = KeyStore::default();
187 let wrapped_private_key = {
188 let mut ctx = store.context_mut();
189 let user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
190 let _ = ctx.persist_symmetric_key(user_key, SymmetricKeySlotId::User);
191 let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
192 ctx.wrap_private_key(SymmetricKeySlotId::User, private_key)
193 .unwrap()
194 };
195
196 let sync = SyncedAccountData {
197 wrapped_account_cryptographic_state: WrappedAccountCryptographicState::V1 {
198 private_key: wrapped_private_key,
199 },
200 folders: vec![],
201 ciphers: vec![],
202 sends: vec![],
203 emergency_access_memberships: vec![],
204 organization_memberships: vec![],
205 trusted_devices: vec![],
206 passkeys: vec![],
207 kdf_and_salt: Some((
208 Kdf::PBKDF2 {
209 iterations: std::num::NonZeroU32::new(600000).unwrap(),
210 },
211 "test_salt".to_string(),
212 )),
213 };
214
215 (store, sync)
216 }
217
218 #[tokio::test]
219 async fn test_password_change_and_rotate_user_keys_missing_kdf_returns_api_error() {
220 let (key_store, mut sync) = make_test_key_store_and_synced_data();
221 sync.kdf_and_salt = None;
222
223 let api_client = ApiClient::new_mocked(|mock| {
224 mock.accounts_key_management_api
225 .expect_password_change_and_rotate_user_account_keys()
226 .never();
227 });
228
229 let result = internal_password_change_and_rotate_user_keys(
230 &key_store,
231 &api_client,
232 PasswordChangeAndRotateUserKeysRequest {
233 old_password: "old_password".to_string(),
234 password: "new_password".to_string(),
235 hint: None,
236 trusted_organization_public_keys: vec![],
237 trusted_emergency_access_public_keys: vec![],
238 },
239 sync.wrapped_account_cryptographic_state.clone(),
240 sync,
241 )
242 .await;
243
244 assert!(matches!(result, Err(RotateUserKeysError::Api)));
245 if let ApiClient::Mock(mut mock) = api_client {
246 mock.accounts_key_management_api.checkpoint();
247 }
248 }
249
250 #[tokio::test]
251 async fn test_password_change_and_rotate_user_keys_success() {
252 let (key_store, sync) = make_test_key_store_and_synced_data();
253 let api_client = ApiClient::new_mocked(|mock| {
254 mock.accounts_key_management_api
255 .expect_password_change_and_rotate_user_account_keys()
256 .once()
257 .returning(|_| Ok(()));
258 });
259
260 let result = internal_password_change_and_rotate_user_keys(
261 &key_store,
262 &api_client,
263 PasswordChangeAndRotateUserKeysRequest {
264 old_password: "old_password".to_string(),
265 password: "new_password".to_string(),
266 hint: None,
267 trusted_organization_public_keys: vec![],
268 trusted_emergency_access_public_keys: vec![],
269 },
270 sync.wrapped_account_cryptographic_state.clone(),
271 sync,
272 )
273 .await;
274
275 assert!(result.is_ok());
276 if let ApiClient::Mock(mut mock) = api_client {
277 mock.accounts_key_management_api.checkpoint();
278 }
279 }
280
281 #[tokio::test]
282 async fn test_password_change_and_rotate_user_keys_post_api_failure_returns_api_error() {
283 let (key_store, sync) = make_test_key_store_and_synced_data();
284 let api_client = ApiClient::new_mocked(|mock| {
285 mock.accounts_key_management_api
286 .expect_password_change_and_rotate_user_account_keys()
287 .once()
288 .returning(|_| {
289 Err(serde_json::Error::io(std::io::Error::other("API error")).into())
290 });
291 });
292
293 let result = internal_password_change_and_rotate_user_keys(
294 &key_store,
295 &api_client,
296 PasswordChangeAndRotateUserKeysRequest {
297 old_password: "old_password".to_string(),
298 password: "new_password".to_string(),
299 hint: None,
300 trusted_organization_public_keys: vec![],
301 trusted_emergency_access_public_keys: vec![],
302 },
303 sync.wrapped_account_cryptographic_state.clone(),
304 sync,
305 )
306 .await;
307
308 assert!(matches!(result, Err(RotateUserKeysError::Api)));
309 if let ApiClient::Mock(mut mock) = api_client {
310 mock.accounts_key_management_api.checkpoint();
311 }
312 }
313
314 #[tokio::test]
315 async fn test_password_change_and_rotate_old_attachments_returns_error() {
316 let (key_store, mut sync) = make_test_key_store_and_synced_data();
317 let enc_string = "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=";
318
319 sync.ciphers = vec![Cipher {
321 id: None,
322 organization_id: None,
323 folder_id: None,
324 collection_ids: vec![],
325 r#type: CipherType::Login,
326 login: None,
327 identity: None,
328 card: None,
329 secure_note: None,
330 ssh_key: None,
331 bank_account: None,
332 drivers_license: None,
333 passport: None,
334 favorite: false,
335 reprompt: Default::default(),
336 organization_use_totp: false,
337 edit: false,
338 permissions: None,
339 view_password: false,
340 name: enc_string.parse().unwrap(),
341 revision_date: DateTime::from_str("2024-01-01T00:00:00Z").unwrap(),
342 archived_date: None,
343 creation_date: DateTime::from_str("2024-01-01T00:00:00Z").unwrap(),
344 attachments: Some(vec![Attachment {
345 id: None,
346 url: None,
347 size: None,
348 size_name: None,
349 file_name: None,
350 key: None, }]),
352 fields: None,
353 key: None,
354 notes: None,
355 local_data: None,
356 password_history: None,
357 deleted_date: None,
358 data: None,
359 }];
360
361 let api_client = ApiClient::new_mocked(|mock| {
362 mock.accounts_key_management_api
364 .expect_password_change_and_rotate_user_account_keys()
365 .never();
366 });
367
368 let result = internal_password_change_and_rotate_user_keys(
369 &key_store,
370 &api_client,
371 PasswordChangeAndRotateUserKeysRequest {
372 old_password: "old_password".to_string(),
373 password: "new_password".to_string(),
374 hint: None,
375 trusted_organization_public_keys: vec![],
376 trusted_emergency_access_public_keys: vec![],
377 },
378 sync.wrapped_account_cryptographic_state.clone(),
379 sync,
380 )
381 .await;
382
383 assert!(matches!(result, Err(RotateUserKeysError::OldAttachments)));
384 if let ApiClient::Mock(mut mock) = api_client {
385 mock.accounts_key_management_api.checkpoint();
386 }
387 }
388}