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