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