1use bitwarden_api_api::models::RotateUserKeysRequestModel;
3use bitwarden_core::key_management::KeySlotIds;
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_wrapped_model,
17 data::reencrypt_data,
18 rotation_context::make_rotation_context,
19 sync::sync_current_account_data,
20 unlock::{ReencryptCommonUnlockDataInput, reencrypt_common_unlock_data},
21 unlock_method::{PrimaryUnlockMethod, reencrypt_unlock_method_data},
22 },
23};
24
25#[derive(Serialize, Deserialize, Clone)]
26#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
27#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
28pub enum KeyRotationMethod {
29 Password { password: String },
31 KeyConnector,
35 Tde,
39}
40
41#[derive(Serialize, Deserialize, Clone)]
42#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
43#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
44pub struct RotateUserKeysRequest {
45 pub key_rotation_method: KeyRotationMethod,
46 pub trusted_emergency_access_public_keys: Vec<PublicKey>,
47 pub trusted_organization_public_keys: Vec<PublicKey>,
48}
49
50#[cfg_attr(feature = "wasm", wasm_bindgen)]
51impl UserCryptoManagementClient {
52 pub async fn rotate_user_keys(
54 &self,
55 request: RotateUserKeysRequest,
56 ) -> Result<(), RotateUserKeysError> {
57 let api_client = &self.client.internal.get_api_configurations().api_client;
58 let key_store = self.client.internal.get_key_store();
59 internal_rotate_user_keys(key_store, api_client, request).await
60 }
61}
62
63#[instrument(name = "rotate_user_keys", level = "info", skip_all, err)]
64async fn internal_rotate_user_keys(
65 key_store: &KeyStore<KeySlotIds>,
66 api_client: &bitwarden_api_api::apis::ApiClient,
67 request: RotateUserKeysRequest,
68) -> Result<(), RotateUserKeysError> {
69 match &request.key_rotation_method {
71 KeyRotationMethod::KeyConnector => {
72 return Err(RotateUserKeysError::UnimplementedKeyRotationMethod);
73 }
74 KeyRotationMethod::Tde => {
75 return Err(RotateUserKeysError::UnimplementedKeyRotationMethod);
76 }
77 KeyRotationMethod::Password { .. } => {}
78 }
79
80 let sync = sync_current_account_data(api_client)
81 .await
82 .map_err(|_| RotateUserKeysError::ApiError)?;
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 &mut ctx,
93 )?;
94
95 info!("Rotating account cryptographic state for user key rotation");
96 let wrapped_account_cryptographic_state_request_model =
97 rotate_account_cryptographic_state_to_wrapped_model(
98 &sync.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::CryptoError)?;
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::CryptoError)?;
115
116 info!("Re-encrypting account primary unlock method for user key rotation");
117 let unlock_method_input =
118 PrimaryUnlockMethod::from_key_rotation_method(request.key_rotation_method, &sync)
119 .map_err(|_| RotateUserKeysError::ApiError)?;
120 let unlock_method_data = reencrypt_unlock_method_data(
121 unlock_method_input,
122 rotation_context.new_user_key_id,
123 &mut ctx,
124 )
125 .map_err(|_| RotateUserKeysError::CryptoError)?;
126
127 info!("Re-encrypting account common unlock data for user key rotation");
128 let common_unlock_data = reencrypt_common_unlock_data(
129 ReencryptCommonUnlockDataInput {
130 trusted_organization_keys: rotation_context.v1_organization_memberships,
131 trusted_emergency_access_keys: rotation_context.v1_emergency_access_memberships,
132 webauthn_credentials: sync.passkeys,
133 trusted_devices: sync.trusted_devices,
134 },
135 rotation_context.current_user_key_id,
136 rotation_context.new_user_key_id,
137 &mut ctx,
138 )
139 .map_err(|_| RotateUserKeysError::CryptoError)?;
140
141 RotateUserKeysRequestModel {
142 wrapped_account_cryptographic_state: Box::new(
143 wrapped_account_cryptographic_state_request_model,
144 ),
145 account_data: Box::new(account_data_model),
146 unlock_data: Box::new(common_unlock_data),
147 unlock_method_data: Box::new(unlock_method_data),
148 }
149 };
150
151 info!("Posting rotated user account keys and data to server");
152 api_client
153 .accounts_key_management_api()
154 .rotate_user_keys(Some(post_request))
155 .await
156 .map_err(|_| RotateUserKeysError::ApiError)?;
157 info!("Successfully rotated user account keys and data");
158 Ok(())
159}
160
161#[cfg(test)]
162mod tests {
163 use bitwarden_api_api::{
164 apis::ApiClient,
165 models::{
166 DeviceAuthRequestResponseModelListResponseModel,
167 EmergencyAccessGranteeDetailsResponseModelListResponseModel, KdfType,
168 MasterPasswordUnlockKdfResponseModel, MasterPasswordUnlockResponseModel,
169 PrivateKeysResponseModel, ProfileOrganizationResponseModelListResponseModel,
170 ProfileResponseModel, PublicKeyEncryptionKeyPairResponseModel, SyncResponseModel,
171 UserDecryptionResponseModel, WebAuthnCredentialResponseModelListResponseModel,
172 },
173 };
174 use bitwarden_core::key_management::{KeySlotIds, SymmetricKeySlotId};
175 use bitwarden_crypto::{KeyStore, PublicKeyEncryptionAlgorithm, SymmetricKeyAlgorithm};
176
177 use super::*;
178
179 fn make_test_key_store_and_sync_response() -> (KeyStore<KeySlotIds>, SyncResponseModel) {
180 let store: KeyStore<KeySlotIds> = KeyStore::default();
181 let wrapped_private_key = {
182 let mut ctx = store.context_mut();
183 let user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
184 let _ = ctx.persist_symmetric_key(user_key, SymmetricKeySlotId::User);
185 let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
186 ctx.wrap_private_key(SymmetricKeySlotId::User, private_key)
187 .unwrap()
188 };
189
190 let sync_response = SyncResponseModel {
191 object: Some("sync".to_string()),
192 profile: Some(Box::new(ProfileResponseModel {
193 id: Some(uuid::Uuid::new_v4()),
194 account_keys: Some(Box::new(PrivateKeysResponseModel {
195 object: None,
196 signature_key_pair: None,
197 public_key_encryption_key_pair: Box::new(
198 PublicKeyEncryptionKeyPairResponseModel {
199 object: None,
200 wrapped_private_key: Some(wrapped_private_key.to_string()),
201 public_key: None,
202 signed_public_key: None,
203 },
204 ),
205 security_state: None,
206 })),
207 ..ProfileResponseModel::default()
208 })),
209 folders: Some(vec![]),
210 ciphers: Some(vec![]),
211 sends: Some(vec![]),
212 collections: None,
213 domains: None,
214 policies: None,
215 user_decryption: Some(Box::new(UserDecryptionResponseModel {
216 master_password_unlock: Some(Box::new(MasterPasswordUnlockResponseModel {
217 kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
218 kdf_type: KdfType::PBKDF2_SHA256,
219 iterations: 600000,
220 memory: None,
221 parallelism: None,
222 }),
223 master_key_encrypted_user_key: None,
224 salt: Some("test_salt".to_string()),
225 })),
226 web_authn_prf_options: None,
227 v2_upgrade_token: None,
228 })),
229 };
230
231 (store, sync_response)
232 }
233
234 fn mock_empty_sync_calls(mock: &mut bitwarden_api_api::apis::ApiClientMock) {
235 mock.organizations_api
236 .expect_get_user()
237 .once()
238 .returning(|| {
239 Ok(ProfileOrganizationResponseModelListResponseModel {
240 object: None,
241 data: Some(vec![]),
242 continuation_token: None,
243 })
244 });
245 mock.emergency_access_api
246 .expect_get_contacts()
247 .once()
248 .returning(|| {
249 Ok(
250 EmergencyAccessGranteeDetailsResponseModelListResponseModel {
251 object: None,
252 data: Some(vec![]),
253 continuation_token: None,
254 },
255 )
256 });
257 mock.devices_api.expect_get_all().once().returning(|| {
258 Ok(DeviceAuthRequestResponseModelListResponseModel {
259 object: None,
260 data: Some(vec![]),
261 continuation_token: None,
262 })
263 });
264 mock.web_authn_api.expect_get().once().returning(|| {
265 Ok(WebAuthnCredentialResponseModelListResponseModel {
266 object: None,
267 data: Some(vec![]),
268 continuation_token: None,
269 })
270 });
271 }
272
273 #[tokio::test]
274 async fn test_rotate_user_keys_key_connector_returns_unimplemented() {
275 let key_store: KeyStore<KeySlotIds> = KeyStore::default();
276 let api_client = ApiClient::new_mocked(|mock| {
277 mock.sync_api.expect_get().never();
278 mock.accounts_key_management_api
279 .expect_rotate_user_keys()
280 .never();
281 });
282
283 let result = internal_rotate_user_keys(
284 &key_store,
285 &api_client,
286 RotateUserKeysRequest {
287 key_rotation_method: KeyRotationMethod::KeyConnector,
288 trusted_organization_public_keys: vec![],
289 trusted_emergency_access_public_keys: vec![],
290 },
291 )
292 .await;
293
294 assert!(matches!(
295 result,
296 Err(RotateUserKeysError::UnimplementedKeyRotationMethod)
297 ));
298 if let ApiClient::Mock(mut mock) = api_client {
299 mock.sync_api.checkpoint();
300 mock.accounts_key_management_api.checkpoint();
301 }
302 }
303
304 #[tokio::test]
305 async fn test_rotate_user_keys_tde_returns_unimplemented() {
306 let key_store: KeyStore<KeySlotIds> = KeyStore::default();
307 let api_client = ApiClient::new_mocked(|mock| {
308 mock.sync_api.expect_get().never();
309 mock.accounts_key_management_api
310 .expect_rotate_user_keys()
311 .never();
312 });
313
314 let result = internal_rotate_user_keys(
315 &key_store,
316 &api_client,
317 RotateUserKeysRequest {
318 key_rotation_method: KeyRotationMethod::Tde,
319 trusted_organization_public_keys: vec![],
320 trusted_emergency_access_public_keys: vec![],
321 },
322 )
323 .await;
324
325 assert!(matches!(
326 result,
327 Err(RotateUserKeysError::UnimplementedKeyRotationMethod)
328 ));
329 if let ApiClient::Mock(mut mock) = api_client {
330 mock.sync_api.checkpoint();
331 mock.accounts_key_management_api.checkpoint();
332 }
333 }
334
335 #[tokio::test]
336 async fn test_rotate_user_keys_api_failure_returns_api_error() {
337 let key_store: KeyStore<KeySlotIds> = KeyStore::default();
338 let api_client = ApiClient::new_mocked(|mock| {
339 mock.sync_api.expect_get().once().returning(|_| {
340 Err(bitwarden_api_api::apis::Error::Serde(
341 serde_json::Error::io(std::io::Error::other("network error")),
342 ))
343 });
344 mock.accounts_key_management_api
345 .expect_rotate_user_keys()
346 .never();
347 });
348
349 let result = internal_rotate_user_keys(
350 &key_store,
351 &api_client,
352 RotateUserKeysRequest {
353 key_rotation_method: KeyRotationMethod::Password {
354 password: "test".to_string(),
355 },
356 trusted_organization_public_keys: vec![],
357 trusted_emergency_access_public_keys: vec![],
358 },
359 )
360 .await;
361
362 assert!(matches!(result, Err(RotateUserKeysError::ApiError)));
363 if let ApiClient::Mock(mut mock) = api_client {
364 mock.sync_api.checkpoint();
365 mock.accounts_key_management_api.checkpoint();
366 }
367 }
368
369 #[tokio::test]
370 async fn test_rotate_user_keys_master_password_success() {
371 let (key_store, sync_response) = make_test_key_store_and_sync_response();
372 let api_client = ApiClient::new_mocked(|mock| {
373 mock.sync_api
374 .expect_get()
375 .once()
376 .returning(move |_| Ok(sync_response.clone()));
377 mock_empty_sync_calls(mock);
378 mock.accounts_key_management_api
379 .expect_rotate_user_keys()
380 .once()
381 .returning(|_| Ok(()));
382 });
383
384 let result = internal_rotate_user_keys(
385 &key_store,
386 &api_client,
387 RotateUserKeysRequest {
388 key_rotation_method: KeyRotationMethod::Password {
389 password: "test_password".to_string(),
390 },
391 trusted_organization_public_keys: vec![],
392 trusted_emergency_access_public_keys: vec![],
393 },
394 )
395 .await;
396
397 assert!(result.is_ok());
398 if let ApiClient::Mock(mut mock) = api_client {
399 mock.sync_api.checkpoint();
400 mock.organizations_api.checkpoint();
401 mock.emergency_access_api.checkpoint();
402 mock.devices_api.checkpoint();
403 mock.web_authn_api.checkpoint();
404 mock.accounts_key_management_api.checkpoint();
405 }
406 }
407
408 #[tokio::test]
409 async fn test_rotate_user_keys_post_api_failure_returns_api_error() {
410 let (key_store, sync_response) = make_test_key_store_and_sync_response();
411 let api_client = ApiClient::new_mocked(|mock| {
412 mock.sync_api
413 .expect_get()
414 .once()
415 .returning(move |_| Ok(sync_response.clone()));
416 mock_empty_sync_calls(mock);
417 mock.accounts_key_management_api
418 .expect_rotate_user_keys()
419 .once()
420 .returning(|_| {
421 Err(bitwarden_api_api::apis::Error::Serde(
422 serde_json::Error::io(std::io::Error::other("API error")),
423 ))
424 });
425 });
426
427 let result = internal_rotate_user_keys(
428 &key_store,
429 &api_client,
430 RotateUserKeysRequest {
431 key_rotation_method: KeyRotationMethod::Password {
432 password: "test_password".to_string(),
433 },
434 trusted_organization_public_keys: vec![],
435 trusted_emergency_access_public_keys: vec![],
436 },
437 )
438 .await;
439
440 assert!(matches!(result, Err(RotateUserKeysError::ApiError)));
441 if let ApiClient::Mock(mut mock) = api_client {
442 mock.sync_api.checkpoint();
443 mock.organizations_api.checkpoint();
444 mock.emergency_access_api.checkpoint();
445 mock.devices_api.checkpoint();
446 mock.web_authn_api.checkpoint();
447 mock.accounts_key_management_api.checkpoint();
448 }
449 }
450}