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 name: Some(TEST_ENC_STRING.to_string()),
318 notes: None,
319 login: None,
320 card: None,
321 identity: None,
322 secure_note: None,
323 ssh_key: None,
324 bank_account: None,
325 drivers_license: None,
326 passport: None,
327 fields: None,
328 password_history: None,
329 attachments: None,
330 organization_use_totp: Some(false),
331 revision_date: Some("2024-01-01T00:00:00Z".to_string()),
332 creation_date: Some("2024-01-01T00:00:00Z".to_string()),
333 deleted_date: None,
334 reprompt: Some(bitwarden_api_api::models::CipherRepromptType::None),
335 key: None,
336 archived_date: None,
337 folder_id: None,
338 favorite: Some(false),
339 edit: Some(true),
340 view_password: Some(true),
341 permissions: None,
342 collection_ids: None,
343 }
344 }
345
346 fn create_test_send(id: uuid::Uuid) -> SendResponseModel {
347 SendResponseModel {
348 object: Some("send".to_string()),
349 id: Some(id),
350 access_id: Some("access_id".to_string()),
351 r#type: Some(SendType::Text),
352 name: Some(TEST_ENC_STRING.to_string()),
353 notes: None,
354 file: None,
355 text: None,
356 data: None,
357 key: Some(KEY_ENC_STRING.to_string()),
358 max_access_count: None,
359 access_count: Some(0),
360 password: None,
361 disabled: Some(false),
362 revision_date: Some("2024-01-01T00:00:00Z".to_string()),
363 expiration_date: None,
364 deletion_date: Some("2024-12-31T00:00:00Z".to_string()),
365 hide_email: Some(false),
366 auth_type: None,
367 emails: None,
368 }
369 }
370
371 fn create_test_user_decryption() -> UserDecryptionResponseModel {
372 UserDecryptionResponseModel {
373 master_password_unlock: Some(Box::new(MasterPasswordUnlockResponseModel {
374 kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
375 kdf_type: KdfType::PBKDF2_SHA256,
376 iterations: 600000,
377 memory: None,
378 parallelism: None,
379 }),
380 master_key_encrypted_user_key: None,
381 salt: Some("test_salt".to_string()),
382 })),
383 web_authn_prf_options: None,
384 v2_upgrade_token: None,
385 }
386 }
387
388 fn create_test_profile(user_id: uuid::Uuid) -> ProfileResponseModel {
389 ProfileResponseModel {
390 id: Some(user_id),
391 account_keys: Some(Box::new(PrivateKeysResponseModel {
392 object: None,
393 signature_key_pair: None,
394 public_key_encryption_key_pair: Box::new(PublicKeyEncryptionKeyPairResponseModel {
395 object: None,
396 wrapped_private_key: Some(TEST_ENC_STRING.to_string()),
397 public_key: None,
398 signed_public_key: None,
399 }),
400 security_state: None,
401 })),
402 ..ProfileResponseModel::default()
403 }
404 }
405
406 fn create_test_sync_response(user_id: uuid::Uuid) -> SyncResponseModel {
407 SyncResponseModel {
408 object: Some("sync".to_string()),
409 profile: Some(Box::new(create_test_profile(user_id))),
410 folders: Some(vec![create_test_folder(uuid::Uuid::new_v4())]),
411 ciphers: Some(vec![create_test_cipher(uuid::Uuid::new_v4())]),
412 sends: Some(vec![create_test_send(uuid::Uuid::new_v4())]),
413 user_decryption: Some(Box::new(create_test_user_decryption())),
414 ..Default::default()
415 }
416 }
417
418 fn create_test_key_rotation_data_response(
419 org_id: uuid::Uuid,
420 ea_id: uuid::Uuid,
421 grantee_id: uuid::Uuid,
422 device_id: uuid::Uuid,
423 passkey_id: uuid::Uuid,
424 ) -> KeyRotationDataResponseModel {
425 KeyRotationDataResponseModel {
426 object: Some("keyRotationData".to_string()),
427 organization_password_reset_key_data: Some(vec![
428 OrganizationPasswordResetKeyDataResponseModel {
429 object: Some("organizationPasswordResetKeyData".to_string()),
430 organization_id: Some(org_id),
431 organization_name: Some("Test Org".to_string()),
432 organization_public_key: Some(test_public_key_b64()),
433 },
434 ]),
435 emergency_access_key_data: Some(vec![EmergencyAccessKeyDataResponseModel {
436 object: Some("emergencyAccessKeyData".to_string()),
437 id: Some(ea_id),
438 grantee_id: Some(grantee_id),
439 grantee_name: Some("Emergency Contact".to_string()),
440 grantee_email: Some("[email protected]".to_string()),
441 public_key: Some(test_public_key_b64()),
442 }]),
443 trusted_device_key_data: Some(vec![TrustedDeviceKeyDataResponseModel {
444 object: Some("trustedDeviceKeyData".to_string()),
445 id: Some(device_id),
446 encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
447 encrypted_user_key: Some(TEST_UNSIGNED_SHARED_KEY.to_string()),
448 }]),
449 passkey_key_data: Some(vec![PasskeyKeyDataResponseModel {
450 object: Some("passkeyKeyData".to_string()),
451 id: Some(passkey_id),
452 encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
453 encrypted_user_key: Some(TEST_UNSIGNED_SHARED_KEY.to_string()),
454 }]),
455 }
456 }
457
458 #[tokio::test]
459 async fn test_get_key_rotation_data_success() {
460 let org_id = uuid::Uuid::new_v4();
461 let ea_id = uuid::Uuid::new_v4();
462 let grantee_id = uuid::Uuid::new_v4();
463 let device_id = uuid::Uuid::new_v4();
464 let passkey_id = uuid::Uuid::new_v4();
465
466 let api_client = ApiClient::new_mocked(|mock| {
467 mock.accounts_key_management_api
468 .expect_get_key_rotation_data()
469 .once()
470 .returning(move || {
471 Ok(create_test_key_rotation_data_response(
472 org_id, ea_id, grantee_id, device_id, passkey_id,
473 ))
474 });
475 });
476
477 let data = get_key_rotation_data(&api_client).await.unwrap();
478
479 assert_eq!(data.organization_memberships.len(), 1);
480 assert_eq!(data.organization_memberships[0].organization_id, org_id);
481 assert_eq!(data.organization_memberships[0].name, "Test Org");
482
483 assert_eq!(data.emergency_access_memberships.len(), 1);
484 assert_eq!(data.emergency_access_memberships[0].id, ea_id);
485 assert_eq!(data.emergency_access_memberships[0].grantee_id, grantee_id);
486 assert_eq!(
487 data.emergency_access_memberships[0].name,
488 "Emergency Contact"
489 );
490
491 assert_eq!(data.trusted_devices.len(), 1);
492 assert_eq!(data.trusted_devices[0].id, device_id);
493 assert_eq!(
494 data.trusted_devices[0].encrypted_public_key.to_string(),
495 TEST_ENC_STRING
496 );
497 assert_eq!(
498 data.trusted_devices[0].encrypted_user_key.to_string(),
499 TEST_UNSIGNED_SHARED_KEY
500 );
501
502 assert_eq!(data.passkeys.len(), 1);
503 assert_eq!(data.passkeys[0].id, passkey_id);
504 assert_eq!(
505 data.passkeys[0].encrypted_public_key.to_string(),
506 TEST_ENC_STRING
507 );
508 assert_eq!(
509 data.passkeys[0].encrypted_user_key.to_string(),
510 TEST_UNSIGNED_SHARED_KEY
511 );
512
513 let expected_public_key = PublicKey::from_der(&SpkiPublicKeyBytes::from(
514 TEST_RSA_PUBLIC_KEY_BYTES.to_vec(),
515 ))
516 .unwrap();
517 assert_eq!(
518 data.organization_memberships[0]
519 .public_key
520 .to_der()
521 .unwrap(),
522 expected_public_key.to_der().unwrap()
523 );
524 assert_eq!(
525 data.emergency_access_memberships[0]
526 .public_key
527 .to_der()
528 .unwrap(),
529 expected_public_key.to_der().unwrap()
530 );
531
532 if let ApiClient::Mock(mut mock) = api_client {
533 mock.accounts_key_management_api.checkpoint();
534 }
535 }
536
537 #[tokio::test]
538 async fn test_get_key_rotation_data_network_error() {
539 let api_client = ApiClient::new_mocked(|mock| {
540 mock.accounts_key_management_api
541 .expect_get_key_rotation_data()
542 .once()
543 .returning(move || {
544 Err(serde_json::Error::io(std::io::Error::other("Network error")).into())
545 });
546 });
547
548 let result = get_key_rotation_data(&api_client).await;
549 assert!(matches!(result, Err(SyncError::Network)));
550
551 if let ApiClient::Mock(mut mock) = api_client {
552 mock.accounts_key_management_api.checkpoint();
553 }
554 }
555
556 #[tokio::test]
557 async fn test_get_key_rotation_data_empty_arrays_returns_empty_data() {
558 let api_client = ApiClient::new_mocked(|mock| {
559 mock.accounts_key_management_api
560 .expect_get_key_rotation_data()
561 .once()
562 .returning(move || {
563 Ok(KeyRotationDataResponseModel {
566 object: Some("keyRotationData".to_string()),
567 organization_password_reset_key_data: Some(vec![]),
568 emergency_access_key_data: Some(vec![]),
569 trusted_device_key_data: Some(vec![]),
570 passkey_key_data: Some(vec![]),
571 })
572 });
573 });
574
575 let data = get_key_rotation_data(&api_client).await.unwrap();
576
577 assert!(data.organization_memberships.is_empty());
578 assert!(data.emergency_access_memberships.is_empty());
579 assert!(data.trusted_devices.is_empty());
580 assert!(data.passkeys.is_empty());
581
582 if let ApiClient::Mock(mut mock) = api_client {
583 mock.accounts_key_management_api.checkpoint();
584 }
585 }
586
587 #[tokio::test]
588 async fn test_get_key_rotation_data_missing_field_is_data_error() {
589 let device_id = uuid::Uuid::new_v4();
590 let api_client = ApiClient::new_mocked(|mock| {
591 mock.accounts_key_management_api
592 .expect_get_key_rotation_data()
593 .once()
594 .returning(move || {
595 Ok(KeyRotationDataResponseModel {
596 object: Some("keyRotationData".to_string()),
597 organization_password_reset_key_data: Some(vec![]),
598 emergency_access_key_data: Some(vec![]),
599 trusted_device_key_data: Some(vec![TrustedDeviceKeyDataResponseModel {
600 object: Some("trustedDeviceKeyData".to_string()),
601 id: Some(device_id),
602 encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
603 encrypted_user_key: None,
605 }]),
606 passkey_key_data: Some(vec![]),
607 })
608 });
609 });
610
611 let result = get_key_rotation_data(&api_client).await;
612 assert!(matches!(result, Err(SyncError::Data)));
613
614 if let ApiClient::Mock(mut mock) = api_client {
615 mock.accounts_key_management_api.checkpoint();
616 }
617 }
618
619 #[tokio::test]
620 async fn test_get_key_rotation_data_emergency_access_name_fallback() {
621 let ea_id_email = uuid::Uuid::new_v4();
622 let ea_id_unknown = uuid::Uuid::new_v4();
623 let grantee_id = uuid::Uuid::new_v4();
624
625 let api_client = ApiClient::new_mocked(|mock| {
626 mock.accounts_key_management_api
627 .expect_get_key_rotation_data()
628 .once()
629 .returning(move || {
630 Ok(KeyRotationDataResponseModel {
631 object: Some("keyRotationData".to_string()),
632 organization_password_reset_key_data: Some(vec![]),
633 emergency_access_key_data: Some(vec![
634 EmergencyAccessKeyDataResponseModel {
635 object: Some("emergencyAccessKeyData".to_string()),
636 id: Some(ea_id_email),
637 grantee_id: Some(grantee_id),
638 grantee_name: None,
640 grantee_email: Some("[email protected]".to_string()),
641 public_key: Some(test_public_key_b64()),
642 },
643 EmergencyAccessKeyDataResponseModel {
644 object: Some("emergencyAccessKeyData".to_string()),
645 id: Some(ea_id_unknown),
646 grantee_id: Some(grantee_id),
647 grantee_name: None,
649 grantee_email: None,
650 public_key: Some(test_public_key_b64()),
651 },
652 ]),
653 trusted_device_key_data: Some(vec![]),
654 passkey_key_data: Some(vec![]),
655 })
656 });
657 });
658
659 let data = get_key_rotation_data(&api_client).await.unwrap();
660 assert_eq!(data.emergency_access_memberships.len(), 2);
661 assert_eq!(
662 data.emergency_access_memberships[0].name,
663 "[email protected]"
664 );
665 assert_eq!(data.emergency_access_memberships[1].name, "Unknown");
666
667 if let ApiClient::Mock(mut mock) = api_client {
668 mock.accounts_key_management_api.checkpoint();
669 }
670 }
671
672 #[tokio::test]
673 async fn test_sync_current_account_data_success() {
674 let user_id = uuid::Uuid::new_v4();
675 let org_id = uuid::Uuid::new_v4();
676 let ea_id = uuid::Uuid::new_v4();
677 let grantee_id = uuid::Uuid::new_v4();
678 let device_id = uuid::Uuid::new_v4();
679 let passkey_id = uuid::Uuid::new_v4();
680 let folder_id = uuid::Uuid::new_v4();
681 let cipher_id = uuid::Uuid::new_v4();
682 let send_id = uuid::Uuid::new_v4();
683
684 let api_client = ApiClient::new_mocked(|mock| {
685 mock.sync_api
686 .expect_get()
687 .once()
688 .returning(move |_exclude_domains| {
689 let mut response = create_test_sync_response(user_id);
690 response.folders = Some(vec![create_test_folder(folder_id)]);
691 response.ciphers = Some(vec![create_test_cipher(cipher_id)]);
692 response.sends = Some(vec![create_test_send(send_id)]);
693 Ok(response)
694 });
695 mock.accounts_key_management_api
696 .expect_get_key_rotation_data()
697 .once()
698 .returning(move || {
699 Ok(create_test_key_rotation_data_response(
700 org_id, ea_id, grantee_id, device_id, passkey_id,
701 ))
702 });
703 });
704
705 let result = sync_current_account_data(&api_client).await;
706 let data = result.unwrap();
707
708 assert_eq!(data.folders.len(), 1);
710 assert_eq!(data.folders[0].id, Some(FolderId::new(folder_id)));
711 assert_eq!(data.folders[0].name, TEST_ENC_STRING.parse().unwrap());
712
713 assert_eq!(data.ciphers.len(), 1);
715 assert_eq!(data.ciphers[0].id, Some(CipherId::new(cipher_id)));
716 assert_eq!(data.ciphers[0].name, Some(TEST_ENC_STRING.parse().unwrap()));
717
718 assert_eq!(data.sends.len(), 1);
720 assert_eq!(data.sends[0].id, Some(SendId::new(send_id)));
721 assert_eq!(data.sends[0].name, TEST_ENC_STRING.parse().unwrap());
722 assert_eq!(data.sends[0].key, KEY_ENC_STRING.parse().unwrap());
723
724 assert_eq!(data.organization_memberships.len(), 1);
725 assert_eq!(data.organization_memberships[0].organization_id, org_id);
726 assert_eq!(data.emergency_access_memberships.len(), 1);
727 assert_eq!(data.emergency_access_memberships[0].id, ea_id);
728 assert_eq!(data.trusted_devices.len(), 1);
729 assert_eq!(data.trusted_devices[0].id, device_id);
730 assert_eq!(data.passkeys.len(), 1);
731 assert_eq!(data.passkeys[0].id, passkey_id);
732 assert!(data.kdf_and_salt.is_some());
733 let (kdf, salt) = data.kdf_and_salt.unwrap();
734 assert_eq!(salt, "test_salt");
735 assert!(matches!(kdf, Kdf::PBKDF2 { iterations } if iterations.get() == 600000));
736 assert!(matches!(
737 data.wrapped_account_cryptographic_state,
738 WrappedAccountCryptographicState::V1 { .. }
739 ));
740
741 if let ApiClient::Mock(mut mock) = api_client {
742 mock.sync_api.checkpoint();
743 mock.accounts_key_management_api.checkpoint();
744 }
745 }
746
747 #[tokio::test]
748 async fn test_sync_current_account_data_network_error() {
749 let api_client = ApiClient::new_mocked(|mock| {
750 mock.sync_api
751 .expect_get()
752 .once()
753 .returning(move |_exclude_domains| {
754 Err(serde_json::Error::io(std::io::Error::other("API error")).into())
755 });
756 mock.accounts_key_management_api
757 .expect_get_key_rotation_data()
758 .never();
759 });
760
761 let result = sync_current_account_data(&api_client).await;
762
763 assert!(matches!(result, Err(SyncError::Network)));
764
765 if let ApiClient::Mock(mut mock) = api_client {
766 mock.sync_api.checkpoint();
767 mock.accounts_key_management_api.checkpoint();
768 }
769 }
770
771 #[test]
772 fn test_parse_ciphers_filters_organization_ciphers() {
773 let personal_cipher_id = uuid::Uuid::new_v4();
774 let organization_cipher_id = uuid::Uuid::new_v4();
775
776 let personal_cipher = create_test_cipher(personal_cipher_id);
777 let mut organization_cipher = create_test_cipher(organization_cipher_id);
778 organization_cipher.organization_id = Some(uuid::Uuid::new_v4());
779
780 let ciphers = parse_ciphers(Some(vec![personal_cipher, organization_cipher])).unwrap();
781
782 assert_eq!(ciphers.len(), 1);
783 assert_eq!(ciphers[0].id, Some(CipherId::new(personal_cipher_id)));
784 }
785}