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