1use bitwarden_api_api::apis::ApiClient;
3use bitwarden_core::key_management::account_cryptographic_state::WrappedAccountCryptographicState;
4use bitwarden_crypto::Kdf;
5use bitwarden_error::bitwarden_error;
6use bitwarden_vault::{Cipher, Folder};
7use thiserror::Error;
8use tracing::{debug, debug_span, info};
9
10use crate::key_rotation::{
11 partial_rotateable_keyset::PartialRotateableKeyset,
12 unlock::{V1EmergencyAccessMembership, V1OrganizationMembership},
13};
14
15trait DebugMapErr<T, E: std::fmt::Debug> {
16 fn debug_map_err<E2>(self, target: E2) -> Result<T, E2>;
18}
19
20impl<T, E: std::fmt::Debug> DebugMapErr<T, E> for Result<T, E> {
21 fn debug_map_err<E2>(self, target: E2) -> Result<T, E2> {
22 self.map_err(|e| {
23 debug!(error = ?e);
24 target
25 })
26 }
27}
28
29pub(super) struct SyncedAccountData {
30 pub(super) wrapped_account_cryptographic_state: WrappedAccountCryptographicState,
31 pub(super) folders: Vec<Folder>,
32 pub(super) ciphers: Vec<Cipher>,
33 pub(super) sends: Vec<bitwarden_send::Send>,
34 pub(super) emergency_access_memberships: Vec<V1EmergencyAccessMembership>,
35 pub(super) organization_memberships: Vec<V1OrganizationMembership>,
36 pub(super) trusted_devices: Vec<PartialRotateableKeyset>,
37 pub(super) passkeys: Vec<PartialRotateableKeyset>,
38 pub(super) kdf_and_salt: Option<(Kdf, String)>,
39}
40
41#[derive(Debug, Error)]
42#[bitwarden_error(flat)]
43pub(super) enum SyncError {
44 #[error("Network error during sync")]
45 Network,
46 #[error("Failed to parse sync data")]
47 Data,
48}
49
50pub(super) struct KeyRotationData {
52 pub(super) organization_memberships: Vec<V1OrganizationMembership>,
53 pub(super) emergency_access_memberships: Vec<V1EmergencyAccessMembership>,
54 pub(super) trusted_devices: Vec<PartialRotateableKeyset>,
55 pub(super) passkeys: Vec<PartialRotateableKeyset>,
56}
57
58pub(super) async fn get_key_rotation_data(
63 api_client: &ApiClient,
64) -> Result<KeyRotationData, SyncError> {
65 let data = api_client
66 .accounts_key_management_api()
67 .get_key_rotation_data()
68 .await
69 .debug_map_err(SyncError::Network)?;
70
71 let organization_memberships = data
72 .organization_password_reset_key_data
73 .ok_or(SyncError::Data)?
74 .into_iter()
75 .map(|response| {
76 let _span = debug_span!("deserializing_organization_membership", organization_id = ?response.organization_id).entered();
77 V1OrganizationMembership::try_from(response).debug_map_err(SyncError::Data)
78 })
79 .collect::<Result<Vec<_>, _>>()?;
80
81 let emergency_access_memberships = data
82 .emergency_access_key_data
83 .ok_or(SyncError::Data)?
84 .into_iter()
85 .map(|response| {
86 let _span = debug_span!("deserializing_emergency_access_membership", emergency_access_id = ?response.id).entered();
87 V1EmergencyAccessMembership::try_from(response).debug_map_err(SyncError::Data)
88 })
89 .collect::<Result<Vec<_>, _>>()?;
90
91 let trusted_devices = data
92 .trusted_device_key_data
93 .ok_or(SyncError::Data)?
94 .into_iter()
95 .map(|response| {
96 let _span =
97 debug_span!("deserializing_trusted_device", device_id = ?response.id).entered();
98 PartialRotateableKeyset::try_from(response).debug_map_err(SyncError::Data)
99 })
100 .collect::<Result<Vec<_>, _>>()?;
101
102 let passkeys = data
103 .passkey_key_data
104 .ok_or(SyncError::Data)?
105 .into_iter()
106 .map(|response| {
107 let _span = debug_span!("deserializing_passkey", passkey_id = ?response.id).entered();
108 PartialRotateableKeyset::try_from(response).debug_map_err(SyncError::Data)
109 })
110 .collect::<Result<Vec<_>, _>>()?;
111
112 info!(
113 "Downloaded key rotation data: {} organizations, {} emergency access, {} devices, {} passkeys",
114 organization_memberships.len(),
115 emergency_access_memberships.len(),
116 trusted_devices.len(),
117 passkeys.len(),
118 );
119
120 Ok(KeyRotationData {
121 organization_memberships,
122 emergency_access_memberships,
123 trusted_devices,
124 passkeys,
125 })
126}
127
128fn parse_ciphers(
129 ciphers: Option<Vec<bitwarden_api_api::models::CipherDetailsResponseModel>>,
130) -> Result<Vec<Cipher>, SyncError> {
131 let ciphers = ciphers
132 .ok_or(SyncError::Data)?
133 .into_iter()
134 .filter(|c| c.organization_id.is_none())
135 .map(|c| {
136 let _span = debug_span!("deserializing_cipher", cipher_id = ?c.id).entered();
137 Cipher::try_from(c).debug_map_err(SyncError::Data)
138 })
139 .collect::<Result<Vec<_>, _>>()?;
140 info!("Deserialized {} ciphers", ciphers.len());
141 Ok(ciphers)
142}
143
144fn parse_folders(
145 folders: Option<Vec<bitwarden_api_api::models::FolderResponseModel>>,
146) -> Result<Vec<Folder>, SyncError> {
147 let folders = folders
148 .ok_or(SyncError::Data)?
149 .into_iter()
150 .map(|f| {
151 let _span = debug_span!("deserializing_folder", folder_id = ?f.id).entered();
152 Folder::try_from(f).debug_map_err(SyncError::Data)
153 })
154 .collect::<Result<Vec<_>, _>>()?;
155 info!("Deserialized {} folders", folders.len());
156 Ok(folders)
157}
158
159fn parse_sends(
160 sends: Option<Vec<bitwarden_api_api::models::SendResponseModel>>,
161) -> Result<Vec<bitwarden_send::Send>, SyncError> {
162 let sends = sends
163 .ok_or(SyncError::Data)?
164 .into_iter()
165 .map(|s| {
166 let _span = debug_span!("deserializing_send", send_id = ?s.id).entered();
167 bitwarden_send::Send::try_from(s).debug_map_err(SyncError::Data)
168 })
169 .collect::<Result<Vec<_>, _>>()?;
170 info!("Deserialized {} sends", sends.len());
171 Ok(sends)
172}
173
174fn from_kdf(
175 kdf: &bitwarden_api_api::models::MasterPasswordUnlockKdfResponseModel,
176) -> Result<Kdf, ()> {
177 Ok(match kdf.kdf_type {
178 bitwarden_api_api::models::KdfType::PBKDF2_SHA256 => Kdf::PBKDF2 {
179 iterations: std::num::NonZeroU32::new(kdf.iterations.try_into().debug_map_err(())?)
180 .ok_or(())?,
181 },
182 bitwarden_api_api::models::KdfType::Argon2id => {
183 let memory = kdf.memory.ok_or(())?;
184 let parallelism = kdf.parallelism.ok_or(())?;
185 Kdf::Argon2id {
186 iterations: std::num::NonZeroU32::new(kdf.iterations.try_into().debug_map_err(())?)
187 .ok_or(())?,
188 memory: std::num::NonZeroU32::new(memory.try_into().debug_map_err(())?).ok_or(())?,
189 parallelism: std::num::NonZeroU32::new(parallelism.try_into().debug_map_err(())?)
190 .ok_or(())?,
191 }
192 }
193 bitwarden_api_api::models::KdfType::__Unknown(_) => return Err(()),
194 })
195}
196
197fn parse_kdf_and_salt(
200 user_decryption: &Option<Box<bitwarden_api_api::models::UserDecryptionResponseModel>>,
201) -> Result<Option<(Kdf, String)>, SyncError> {
202 let user_decryption_options = user_decryption.as_ref().ok_or(SyncError::Data)?;
203 if let Some(master_password_unlock) = &user_decryption_options.master_password_unlock {
204 let kdf = from_kdf(&master_password_unlock.clone().kdf).debug_map_err(SyncError::Data)?;
205 let salt = master_password_unlock.clone().salt.ok_or(SyncError::Data)?;
206 debug!("Parsed password KDF and salt from sync response");
207 Ok(Some((kdf, salt)))
208 } else {
209 debug!(
210 "User does not have master password decryption options, skipping KDF and salt parsing"
211 );
212 Ok(None)
213 }
214}
215
216pub(super) async fn sync_current_account_data(
217 api_client: &ApiClient,
218) -> Result<SyncedAccountData, SyncError> {
219 info!("Syncing latest vault state from server for key rotation");
220 let sync = api_client
221 .sync_api()
222 .get(Some(true))
223 .await
224 .debug_map_err(SyncError::Network)?;
225
226 let profile = sync.profile.as_ref().ok_or(SyncError::Data)?;
227 let kdf_and_salt = parse_kdf_and_salt(&sync.user_decryption)?;
229 let account_cryptographic_state = profile.account_keys.to_owned().ok_or(SyncError::Data)?;
230 let ciphers = parse_ciphers(sync.ciphers)?;
231 let folders = parse_folders(sync.folders)?;
232 let sends = parse_sends(sync.sends)?;
233 let wrapped_account_cryptographic_state =
234 WrappedAccountCryptographicState::try_from(account_cryptographic_state.as_ref())
235 .debug_map_err(SyncError::Data)?;
236
237 info!("Syncing key rotation data (organizations, emergency access, devices, passkeys)");
240 let key_rotation_data = get_key_rotation_data(api_client).await?;
241
242 Ok(SyncedAccountData {
243 wrapped_account_cryptographic_state,
244 folders,
245 ciphers,
246 sends,
247 emergency_access_memberships: key_rotation_data.emergency_access_memberships,
248 organization_memberships: key_rotation_data.organization_memberships,
249 trusted_devices: key_rotation_data.trusted_devices,
250 passkeys: key_rotation_data.passkeys,
251 kdf_and_salt,
252 })
253}
254
255#[cfg(test)]
256mod tests {
257 use bitwarden_api_api::{
258 apis::ApiClient,
259 models::{
260 EmergencyAccessKeyDataResponseModel, FolderResponseModel, KdfType,
261 KeyRotationDataResponseModel, MasterPasswordUnlockKdfResponseModel,
262 MasterPasswordUnlockResponseModel, OrganizationPasswordResetKeyDataResponseModel,
263 PasskeyKeyDataResponseModel, PrivateKeysResponseModel, ProfileResponseModel,
264 PublicKeyEncryptionKeyPairResponseModel, SendResponseModel, SendType,
265 SyncResponseModel, TrustedDeviceKeyDataResponseModel, UserDecryptionResponseModel,
266 },
267 };
268 use bitwarden_crypto::{PublicKey, SpkiPublicKeyBytes};
269 use bitwarden_encoding::B64;
270 use bitwarden_send::SendId;
271 use bitwarden_vault::{CipherId, FolderId};
272
273 use super::*;
274
275 const TEST_ENC_STRING: &str = "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=";
276 const KEY_ENC_STRING: &str = "2.KLv/j0V4Ebs0dwyPdtt4vw==|Nczvv+DTkeP466cP/wMDnGK6W9zEIg5iHLhcuQG6s+M=|SZGsfuIAIaGZ7/kzygaVUau3LeOvJUlolENBOU+LX7g=";
277 const TEST_UNSIGNED_SHARED_KEY: &str = "4.AAAAAAAAAAAAAAAAAAAAAA==";
278
279 const TEST_RSA_PUBLIC_KEY_BYTES: &[u8] = &[
280 48, 130, 1, 34, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 130, 1, 15, 0,
281 48, 130, 1, 10, 2, 130, 1, 1, 0, 173, 4, 54, 63, 125, 12, 254, 38, 115, 34, 95, 164, 148,
282 115, 86, 140, 129, 74, 19, 70, 212, 212, 130, 163, 105, 249, 101, 120, 154, 46, 194, 250,
283 229, 242, 156, 67, 109, 179, 187, 134, 59, 235, 60, 107, 144, 163, 35, 22, 109, 230, 134,
284 243, 44, 243, 79, 84, 76, 11, 64, 56, 236, 167, 98, 26, 30, 213, 143, 105, 52, 92, 129, 92,
285 88, 22, 115, 135, 63, 215, 79, 8, 11, 183, 124, 10, 73, 231, 170, 110, 210, 178, 22, 100,
286 76, 75, 118, 202, 252, 204, 67, 204, 152, 6, 244, 208, 161, 146, 103, 225, 233, 239, 88,
287 195, 88, 150, 230, 111, 62, 142, 12, 157, 184, 155, 34, 84, 237, 111, 11, 97, 56, 152, 130,
288 14, 72, 123, 140, 47, 137, 5, 97, 166, 4, 147, 111, 23, 65, 78, 63, 208, 198, 50, 161, 39,
289 80, 143, 100, 194, 37, 252, 194, 53, 207, 166, 168, 250, 165, 121, 9, 207, 90, 36, 213,
290 211, 84, 255, 14, 205, 114, 135, 217, 137, 105, 232, 58, 169, 222, 10, 13, 138, 203, 16,
291 12, 122, 72, 227, 95, 160, 111, 54, 200, 198, 143, 156, 15, 143, 196, 50, 150, 204, 144,
292 255, 162, 248, 50, 28, 47, 66, 9, 83, 158, 67, 9, 50, 147, 174, 147, 200, 199, 238, 190,
293 248, 60, 114, 218, 32, 209, 120, 218, 17, 234, 14, 128, 192, 166, 33, 60, 73, 227, 108,
294 201, 41, 160, 81, 133, 171, 205, 221, 2, 3, 1, 0, 1,
295 ];
296
297 fn test_public_key_b64() -> String {
298 B64::from(TEST_RSA_PUBLIC_KEY_BYTES.to_vec()).to_string()
299 }
300
301 fn create_test_folder(id: uuid::Uuid) -> FolderResponseModel {
302 FolderResponseModel {
303 object: Some("folder".to_string()),
304 id: Some(id),
305 name: Some(TEST_ENC_STRING.to_string()),
306 revision_date: Some("2024-01-01T00:00:00Z".to_string()),
307 }
308 }
309
310 fn create_test_cipher(id: uuid::Uuid) -> bitwarden_api_api::models::CipherDetailsResponseModel {
311 bitwarden_api_api::models::CipherDetailsResponseModel {
312 object: Some("cipher".to_string()),
313 id: Some(id),
314 organization_id: None,
315 r#type: Some(bitwarden_api_api::models::CipherType::Login),
316 data: None,
317 partial_data: None,
318 name: Some(TEST_ENC_STRING.to_string()),
319 notes: None,
320 login: None,
321 card: None,
322 identity: None,
323 secure_note: None,
324 ssh_key: None,
325 bank_account: None,
326 drivers_license: None,
327 passport: None,
328 fields: None,
329 password_history: None,
330 attachments: None,
331 organization_use_totp: Some(false),
332 revision_date: Some("2024-01-01T00:00:00Z".to_string()),
333 creation_date: Some("2024-01-01T00:00:00Z".to_string()),
334 deleted_date: None,
335 reprompt: Some(bitwarden_api_api::models::CipherRepromptType::None),
336 key: None,
337 archived_date: None,
338 folder_id: None,
339 favorite: Some(false),
340 edit: Some(true),
341 view_password: Some(true),
342 permissions: None,
343 collection_ids: None,
344 }
345 }
346
347 fn create_test_send(id: uuid::Uuid) -> SendResponseModel {
348 SendResponseModel {
349 object: Some("send".to_string()),
350 id: Some(id),
351 access_id: Some("access_id".to_string()),
352 r#type: Some(SendType::Text),
353 name: Some(TEST_ENC_STRING.to_string()),
354 notes: None,
355 file: None,
356 text: None,
357 data: None,
358 key: Some(KEY_ENC_STRING.to_string()),
359 max_access_count: None,
360 access_count: Some(0),
361 password: None,
362 disabled: Some(false),
363 revision_date: Some("2024-01-01T00:00:00Z".to_string()),
364 expiration_date: None,
365 deletion_date: Some("2024-12-31T00:00:00Z".to_string()),
366 hide_email: Some(false),
367 auth_type: None,
368 emails: None,
369 }
370 }
371
372 fn create_test_user_decryption() -> UserDecryptionResponseModel {
373 UserDecryptionResponseModel {
374 master_password_unlock: Some(Box::new(MasterPasswordUnlockResponseModel {
375 kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
376 kdf_type: KdfType::PBKDF2_SHA256,
377 iterations: 600000,
378 memory: None,
379 parallelism: None,
380 }),
381 master_key_encrypted_user_key: None,
382 salt: Some("test_salt".to_string()),
383 contained_key_id: None,
384 })),
385 web_authn_prf_options: None,
386 v2_upgrade_token: None,
387 user_key_id: None,
388 }
389 }
390
391 fn create_test_profile(user_id: uuid::Uuid) -> ProfileResponseModel {
392 ProfileResponseModel {
393 id: Some(user_id),
394 account_keys: Some(Box::new(PrivateKeysResponseModel {
395 object: None,
396 signature_key_pair: None,
397 public_key_encryption_key_pair: Box::new(PublicKeyEncryptionKeyPairResponseModel {
398 object: None,
399 wrapped_private_key: Some(TEST_ENC_STRING.to_string()),
400 public_key: None,
401 signed_public_key: None,
402 }),
403 security_state: None,
404 })),
405 ..ProfileResponseModel::default()
406 }
407 }
408
409 fn create_test_sync_response(user_id: uuid::Uuid) -> SyncResponseModel {
410 SyncResponseModel {
411 object: Some("sync".to_string()),
412 profile: Some(Box::new(create_test_profile(user_id))),
413 folders: Some(vec![create_test_folder(uuid::Uuid::new_v4())]),
414 ciphers: Some(vec![create_test_cipher(uuid::Uuid::new_v4())]),
415 sends: Some(vec![create_test_send(uuid::Uuid::new_v4())]),
416 user_decryption: Some(Box::new(create_test_user_decryption())),
417 ..Default::default()
418 }
419 }
420
421 fn create_test_key_rotation_data_response(
422 org_id: uuid::Uuid,
423 ea_id: uuid::Uuid,
424 grantee_id: uuid::Uuid,
425 device_id: uuid::Uuid,
426 passkey_id: uuid::Uuid,
427 ) -> KeyRotationDataResponseModel {
428 KeyRotationDataResponseModel {
429 object: Some("keyRotationData".to_string()),
430 organization_password_reset_key_data: Some(vec![
431 OrganizationPasswordResetKeyDataResponseModel {
432 object: Some("organizationPasswordResetKeyData".to_string()),
433 organization_id: Some(org_id),
434 organization_name: Some("Test Org".to_string()),
435 organization_public_key: Some(test_public_key_b64()),
436 },
437 ]),
438 emergency_access_key_data: Some(vec![EmergencyAccessKeyDataResponseModel {
439 object: Some("emergencyAccessKeyData".to_string()),
440 id: Some(ea_id),
441 grantee_id: Some(grantee_id),
442 grantee_name: Some("Emergency Contact".to_string()),
443 grantee_email: Some("[email protected]".to_string()),
444 public_key: Some(test_public_key_b64()),
445 }]),
446 trusted_device_key_data: Some(vec![TrustedDeviceKeyDataResponseModel {
447 object: Some("trustedDeviceKeyData".to_string()),
448 id: Some(device_id),
449 encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
450 encrypted_user_key: Some(TEST_UNSIGNED_SHARED_KEY.to_string()),
451 }]),
452 passkey_key_data: Some(vec![PasskeyKeyDataResponseModel {
453 object: Some("passkeyKeyData".to_string()),
454 id: Some(passkey_id),
455 encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
456 encrypted_user_key: Some(TEST_UNSIGNED_SHARED_KEY.to_string()),
457 }]),
458 }
459 }
460
461 #[tokio::test]
462 async fn test_get_key_rotation_data_success() {
463 let org_id = uuid::Uuid::new_v4();
464 let ea_id = uuid::Uuid::new_v4();
465 let grantee_id = uuid::Uuid::new_v4();
466 let device_id = uuid::Uuid::new_v4();
467 let passkey_id = uuid::Uuid::new_v4();
468
469 let api_client = ApiClient::new_mocked(|mock| {
470 mock.accounts_key_management_api
471 .expect_get_key_rotation_data()
472 .once()
473 .returning(move || {
474 Ok(create_test_key_rotation_data_response(
475 org_id, ea_id, grantee_id, device_id, passkey_id,
476 ))
477 });
478 });
479
480 let data = get_key_rotation_data(&api_client).await.unwrap();
481
482 assert_eq!(data.organization_memberships.len(), 1);
483 assert_eq!(data.organization_memberships[0].organization_id, org_id);
484 assert_eq!(data.organization_memberships[0].name, "Test Org");
485
486 assert_eq!(data.emergency_access_memberships.len(), 1);
487 assert_eq!(data.emergency_access_memberships[0].id, ea_id);
488 assert_eq!(data.emergency_access_memberships[0].grantee_id, grantee_id);
489 assert_eq!(
490 data.emergency_access_memberships[0].name,
491 "Emergency Contact"
492 );
493
494 assert_eq!(data.trusted_devices.len(), 1);
495 assert_eq!(data.trusted_devices[0].id, device_id);
496 assert_eq!(
497 data.trusted_devices[0].encrypted_public_key.to_string(),
498 TEST_ENC_STRING
499 );
500 assert_eq!(
501 data.trusted_devices[0].encrypted_user_key.to_string(),
502 TEST_UNSIGNED_SHARED_KEY
503 );
504
505 assert_eq!(data.passkeys.len(), 1);
506 assert_eq!(data.passkeys[0].id, passkey_id);
507 assert_eq!(
508 data.passkeys[0].encrypted_public_key.to_string(),
509 TEST_ENC_STRING
510 );
511 assert_eq!(
512 data.passkeys[0].encrypted_user_key.to_string(),
513 TEST_UNSIGNED_SHARED_KEY
514 );
515
516 let expected_public_key = PublicKey::from_der(&SpkiPublicKeyBytes::from(
517 TEST_RSA_PUBLIC_KEY_BYTES.to_vec(),
518 ))
519 .unwrap();
520 assert_eq!(
521 data.organization_memberships[0]
522 .public_key
523 .to_der()
524 .unwrap(),
525 expected_public_key.to_der().unwrap()
526 );
527 assert_eq!(
528 data.emergency_access_memberships[0]
529 .public_key
530 .to_der()
531 .unwrap(),
532 expected_public_key.to_der().unwrap()
533 );
534
535 if let ApiClient::Mock(mut mock) = api_client {
536 mock.accounts_key_management_api.checkpoint();
537 }
538 }
539
540 #[tokio::test]
541 async fn test_get_key_rotation_data_network_error() {
542 let api_client = ApiClient::new_mocked(|mock| {
543 mock.accounts_key_management_api
544 .expect_get_key_rotation_data()
545 .once()
546 .returning(move || {
547 Err(serde_json::Error::io(std::io::Error::other("Network error")).into())
548 });
549 });
550
551 let result = get_key_rotation_data(&api_client).await;
552 assert!(matches!(result, Err(SyncError::Network)));
553
554 if let ApiClient::Mock(mut mock) = api_client {
555 mock.accounts_key_management_api.checkpoint();
556 }
557 }
558
559 #[tokio::test]
560 async fn test_get_key_rotation_data_empty_arrays_returns_empty_data() {
561 let api_client = ApiClient::new_mocked(|mock| {
562 mock.accounts_key_management_api
563 .expect_get_key_rotation_data()
564 .once()
565 .returning(move || {
566 Ok(KeyRotationDataResponseModel {
569 object: Some("keyRotationData".to_string()),
570 organization_password_reset_key_data: Some(vec![]),
571 emergency_access_key_data: Some(vec![]),
572 trusted_device_key_data: Some(vec![]),
573 passkey_key_data: Some(vec![]),
574 })
575 });
576 });
577
578 let data = get_key_rotation_data(&api_client).await.unwrap();
579
580 assert!(data.organization_memberships.is_empty());
581 assert!(data.emergency_access_memberships.is_empty());
582 assert!(data.trusted_devices.is_empty());
583 assert!(data.passkeys.is_empty());
584
585 if let ApiClient::Mock(mut mock) = api_client {
586 mock.accounts_key_management_api.checkpoint();
587 }
588 }
589
590 #[tokio::test]
591 async fn test_get_key_rotation_data_missing_field_is_data_error() {
592 let device_id = uuid::Uuid::new_v4();
593 let api_client = ApiClient::new_mocked(|mock| {
594 mock.accounts_key_management_api
595 .expect_get_key_rotation_data()
596 .once()
597 .returning(move || {
598 Ok(KeyRotationDataResponseModel {
599 object: Some("keyRotationData".to_string()),
600 organization_password_reset_key_data: Some(vec![]),
601 emergency_access_key_data: Some(vec![]),
602 trusted_device_key_data: Some(vec![TrustedDeviceKeyDataResponseModel {
603 object: Some("trustedDeviceKeyData".to_string()),
604 id: Some(device_id),
605 encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
606 encrypted_user_key: None,
608 }]),
609 passkey_key_data: Some(vec![]),
610 })
611 });
612 });
613
614 let result = get_key_rotation_data(&api_client).await;
615 assert!(matches!(result, Err(SyncError::Data)));
616
617 if let ApiClient::Mock(mut mock) = api_client {
618 mock.accounts_key_management_api.checkpoint();
619 }
620 }
621
622 #[tokio::test]
623 async fn test_get_key_rotation_data_emergency_access_name_fallback() {
624 let ea_id_email = uuid::Uuid::new_v4();
625 let ea_id_unknown = uuid::Uuid::new_v4();
626 let grantee_id = uuid::Uuid::new_v4();
627
628 let api_client = ApiClient::new_mocked(|mock| {
629 mock.accounts_key_management_api
630 .expect_get_key_rotation_data()
631 .once()
632 .returning(move || {
633 Ok(KeyRotationDataResponseModel {
634 object: Some("keyRotationData".to_string()),
635 organization_password_reset_key_data: Some(vec![]),
636 emergency_access_key_data: Some(vec![
637 EmergencyAccessKeyDataResponseModel {
638 object: Some("emergencyAccessKeyData".to_string()),
639 id: Some(ea_id_email),
640 grantee_id: Some(grantee_id),
641 grantee_name: None,
643 grantee_email: Some("[email protected]".to_string()),
644 public_key: Some(test_public_key_b64()),
645 },
646 EmergencyAccessKeyDataResponseModel {
647 object: Some("emergencyAccessKeyData".to_string()),
648 id: Some(ea_id_unknown),
649 grantee_id: Some(grantee_id),
650 grantee_name: None,
652 grantee_email: None,
653 public_key: Some(test_public_key_b64()),
654 },
655 ]),
656 trusted_device_key_data: Some(vec![]),
657 passkey_key_data: Some(vec![]),
658 })
659 });
660 });
661
662 let data = get_key_rotation_data(&api_client).await.unwrap();
663 assert_eq!(data.emergency_access_memberships.len(), 2);
664 assert_eq!(
665 data.emergency_access_memberships[0].name,
666 "[email protected]"
667 );
668 assert_eq!(data.emergency_access_memberships[1].name, "Unknown");
669
670 if let ApiClient::Mock(mut mock) = api_client {
671 mock.accounts_key_management_api.checkpoint();
672 }
673 }
674
675 #[tokio::test]
676 async fn test_sync_current_account_data_success() {
677 let user_id = uuid::Uuid::new_v4();
678 let org_id = uuid::Uuid::new_v4();
679 let ea_id = uuid::Uuid::new_v4();
680 let grantee_id = uuid::Uuid::new_v4();
681 let device_id = uuid::Uuid::new_v4();
682 let passkey_id = uuid::Uuid::new_v4();
683 let folder_id = uuid::Uuid::new_v4();
684 let cipher_id = uuid::Uuid::new_v4();
685 let send_id = uuid::Uuid::new_v4();
686
687 let api_client = ApiClient::new_mocked(|mock| {
688 mock.sync_api
689 .expect_get()
690 .once()
691 .returning(move |_exclude_domains| {
692 let mut response = create_test_sync_response(user_id);
693 response.folders = Some(vec![create_test_folder(folder_id)]);
694 response.ciphers = Some(vec![create_test_cipher(cipher_id)]);
695 response.sends = Some(vec![create_test_send(send_id)]);
696 Ok(response)
697 });
698 mock.accounts_key_management_api
699 .expect_get_key_rotation_data()
700 .once()
701 .returning(move || {
702 Ok(create_test_key_rotation_data_response(
703 org_id, ea_id, grantee_id, device_id, passkey_id,
704 ))
705 });
706 });
707
708 let result = sync_current_account_data(&api_client).await;
709 let data = result.unwrap();
710
711 assert_eq!(data.folders.len(), 1);
713 assert_eq!(data.folders[0].id, Some(FolderId::new(folder_id)));
714 assert_eq!(data.folders[0].name, TEST_ENC_STRING.parse().unwrap());
715
716 assert_eq!(data.ciphers.len(), 1);
718 assert_eq!(data.ciphers[0].id, Some(CipherId::new(cipher_id)));
719 assert_eq!(data.ciphers[0].name, Some(TEST_ENC_STRING.parse().unwrap()));
720
721 assert_eq!(data.sends.len(), 1);
723 assert_eq!(data.sends[0].id, Some(SendId::new(send_id)));
724 assert_eq!(data.sends[0].name, TEST_ENC_STRING.parse().unwrap());
725 assert_eq!(data.sends[0].key, KEY_ENC_STRING.parse().unwrap());
726
727 assert_eq!(data.organization_memberships.len(), 1);
728 assert_eq!(data.organization_memberships[0].organization_id, org_id);
729 assert_eq!(data.emergency_access_memberships.len(), 1);
730 assert_eq!(data.emergency_access_memberships[0].id, ea_id);
731 assert_eq!(data.trusted_devices.len(), 1);
732 assert_eq!(data.trusted_devices[0].id, device_id);
733 assert_eq!(data.passkeys.len(), 1);
734 assert_eq!(data.passkeys[0].id, passkey_id);
735 assert!(data.kdf_and_salt.is_some());
736 let (kdf, salt) = data.kdf_and_salt.unwrap();
737 assert_eq!(salt, "test_salt");
738 assert!(matches!(kdf, Kdf::PBKDF2 { iterations } if iterations.get() == 600000));
739 assert!(matches!(
740 data.wrapped_account_cryptographic_state,
741 WrappedAccountCryptographicState::V1 { .. }
742 ));
743
744 if let ApiClient::Mock(mut mock) = api_client {
745 mock.sync_api.checkpoint();
746 mock.accounts_key_management_api.checkpoint();
747 }
748 }
749
750 #[tokio::test]
751 async fn test_sync_current_account_data_network_error() {
752 let api_client = ApiClient::new_mocked(|mock| {
753 mock.sync_api
754 .expect_get()
755 .once()
756 .returning(move |_exclude_domains| {
757 Err(serde_json::Error::io(std::io::Error::other("API error")).into())
758 });
759 mock.accounts_key_management_api
760 .expect_get_key_rotation_data()
761 .never();
762 });
763
764 let result = sync_current_account_data(&api_client).await;
765
766 assert!(matches!(result, Err(SyncError::Network)));
767
768 if let ApiClient::Mock(mut mock) = api_client {
769 mock.sync_api.checkpoint();
770 mock.accounts_key_management_api.checkpoint();
771 }
772 }
773
774 #[test]
775 fn test_parse_ciphers_filters_organization_ciphers() {
776 let personal_cipher_id = uuid::Uuid::new_v4();
777 let organization_cipher_id = uuid::Uuid::new_v4();
778
779 let personal_cipher = create_test_cipher(personal_cipher_id);
780 let mut organization_cipher = create_test_cipher(organization_cipher_id);
781 organization_cipher.organization_id = Some(uuid::Uuid::new_v4());
782
783 let ciphers = parse_ciphers(Some(vec![personal_cipher, organization_cipher])).unwrap();
784
785 assert_eq!(ciphers.len(), 1);
786 assert_eq!(ciphers[0].id, Some(CipherId::new(personal_cipher_id)));
787 }
788}