1use std::str::FromStr;
3
4use bitwarden_api_api::{apis::ApiClient, models::WebAuthnPrfStatus};
5use bitwarden_core::key_management::account_cryptographic_state::WrappedAccountCryptographicState;
6use bitwarden_crypto::{EncString, Kdf, PublicKey, SpkiPublicKeyBytes, UnsignedSharedKey};
7use bitwarden_encoding::B64;
8use bitwarden_error::bitwarden_error;
9use bitwarden_vault::{Cipher, Folder};
10use thiserror::Error;
11use tokio::try_join;
12use tracing::{debug, debug_span, info};
13use uuid::Uuid;
14
15use crate::key_rotation::{
16 partial_rotateable_keyset::PartialRotateableKeyset,
17 unlock::{V1EmergencyAccessMembership, V1OrganizationMembership},
18};
19
20trait DebugMapErr<T, E: std::fmt::Debug> {
21 fn debug_map_err<E2>(self, target: E2) -> Result<T, E2>;
23}
24
25impl<T, E: std::fmt::Debug> DebugMapErr<T, E> for Result<T, E> {
26 fn debug_map_err<E2>(self, target: E2) -> Result<T, E2> {
27 self.map_err(|e| {
28 debug!(error = ?e);
29 target
30 })
31 }
32}
33
34pub(super) struct SyncedAccountData {
35 pub(super) wrapped_account_cryptographic_state: WrappedAccountCryptographicState,
36 pub(super) folders: Vec<Folder>,
37 pub(super) ciphers: Vec<Cipher>,
38 pub(super) sends: Vec<bitwarden_send::Send>,
39 pub(super) emergency_access_memberships: Vec<V1EmergencyAccessMembership>,
40 pub(super) organization_memberships: Vec<V1OrganizationMembership>,
41 pub(super) trusted_devices: Vec<PartialRotateableKeyset>,
42 pub(super) passkeys: Vec<PartialRotateableKeyset>,
43 pub(super) kdf_and_salt: Option<(Kdf, String)>,
44}
45
46#[derive(Debug, Error)]
47#[bitwarden_error(flat)]
48pub(super) enum SyncError {
49 #[error("Network error during sync")]
50 NetworkError,
51 #[error("Failed to parse sync data")]
52 DataError,
53}
54
55async fn fetch_organization_public_key(
57 api_client: &ApiClient,
58 organization_id: Uuid,
59) -> Result<PublicKey, SyncError> {
60 let org_details = api_client
61 .organizations_api()
62 .get_public_key(&organization_id.to_string())
63 .await
64 .debug_map_err(SyncError::NetworkError)?
65 .public_key
66 .ok_or(SyncError::DataError)?;
67 PublicKey::from_der(&SpkiPublicKeyBytes::from(
68 B64::from_str(&org_details)
69 .debug_map_err(SyncError::DataError)?
70 .into_bytes(),
71 ))
72 .debug_map_err(SyncError::DataError)
73}
74
75pub(crate) async fn sync_orgs(
78 api_client: &ApiClient,
79) -> Result<Vec<V1OrganizationMembership>, SyncError> {
80 let organizations = api_client
81 .organizations_api()
82 .get_user()
83 .await
84 .debug_map_err(SyncError::NetworkError)?
85 .data
86 .ok_or(SyncError::DataError)?
87 .into_iter();
88 let organizations = organizations
89 .into_iter()
90 .filter(|org| org.reset_password_enrolled.unwrap_or(false))
91 .map(async |org| {
92 let id = org.id.ok_or(SyncError::DataError)?;
93 let public_key = fetch_organization_public_key(api_client, id).await?;
94 Ok(V1OrganizationMembership {
95 organization_id: id,
96 name: org.name.ok_or(SyncError::DataError)?,
97 public_key,
98 })
99 })
100 .collect::<Vec<_>>();
101
102 let mut organization_memberships = Vec::new();
104 for futures in organizations {
105 organization_memberships.push(futures.await?);
106 }
107
108 info!(
109 "Downloaded {} organization memberships",
110 organization_memberships.len()
111 );
112 Ok(organization_memberships)
113}
114
115async fn fetch_user_public_key(
117 api_client: &ApiClient,
118 user_id: Uuid,
119) -> Result<PublicKey, SyncError> {
120 let user_key_response = api_client
121 .users_api()
122 .get_public_key(user_id)
123 .await
124 .debug_map_err(SyncError::NetworkError)?;
125 let public_key_b64 = user_key_response.public_key.ok_or(SyncError::DataError)?;
126 PublicKey::from_der(&SpkiPublicKeyBytes::from(
127 B64::from_str(&public_key_b64)
128 .debug_map_err(SyncError::DataError)?
129 .into_bytes(),
130 ))
131 .debug_map_err(SyncError::DataError)
132}
133
134pub(crate) async fn sync_emergency_access(
136 api_client: &ApiClient,
137) -> Result<Vec<V1EmergencyAccessMembership>, SyncError> {
138 let emergency_access = api_client
139 .emergency_access_api()
140 .get_contacts()
141 .await
142 .debug_map_err(SyncError::NetworkError)?
143 .data
144 .ok_or(SyncError::DataError)?
145 .into_iter()
146 .map(async |ea| {
147 let user_id = ea.grantee_id.ok_or(SyncError::DataError)?;
148 let public_key = fetch_user_public_key(api_client, user_id).await?;
149 Ok(V1EmergencyAccessMembership {
150 id: ea.id.ok_or(SyncError::DataError)?,
151 name: ea.name.ok_or(SyncError::DataError)?,
152 public_key,
153 })
154 })
155 .collect::<Vec<_>>();
156
157 let mut emergency_access_memberships = Vec::new();
159 for futures in emergency_access {
160 emergency_access_memberships.push(futures.await?);
161 }
162
163 info!(
164 "Downloaded {} emergency access memberships",
165 emergency_access_memberships.len()
166 );
167 Ok(emergency_access_memberships)
168}
169
170async fn sync_passkeys(api_client: &ApiClient) -> Result<Vec<PartialRotateableKeyset>, SyncError> {
172 let passkeys = api_client
173 .web_authn_api()
174 .get()
175 .await
176 .debug_map_err(SyncError::NetworkError)?
177 .data
178 .ok_or(SyncError::DataError)?
179 .into_iter()
180 .filter(|cred| cred.prf_status == Some(WebAuthnPrfStatus::Enabled))
181 .map(|cred| {
182 Ok(PartialRotateableKeyset {
183 id: Uuid::from_str(&cred.id.ok_or(SyncError::DataError)?)
184 .debug_map_err(SyncError::DataError)?,
185 encrypted_public_key: EncString::from_str(
186 &cred.encrypted_public_key.ok_or(SyncError::DataError)?,
187 )
188 .debug_map_err(SyncError::DataError)?,
189 encrypted_user_key: UnsignedSharedKey::from_str(
190 &cred.encrypted_user_key.ok_or(SyncError::DataError)?,
191 )
192 .debug_map_err(SyncError::DataError)?,
193 })
194 })
195 .collect::<Result<Vec<_>, _>>()?;
196 info!("Downloaded {} passkeys", passkeys.len());
197 Ok(passkeys)
198}
199
200async fn sync_devices(api_client: &ApiClient) -> Result<Vec<PartialRotateableKeyset>, SyncError> {
202 let trusted_devices = api_client
203 .devices_api()
204 .get_all()
205 .await
206 .debug_map_err(SyncError::NetworkError)?
207 .data
208 .ok_or(SyncError::DataError)?
209 .into_iter()
210 .filter(|device| device.is_trusted.unwrap_or(false))
211 .map(|device| {
212 Ok(PartialRotateableKeyset {
213 id: device.id.ok_or(SyncError::DataError)?,
214 encrypted_public_key: EncString::from_str(
215 &device.encrypted_public_key.ok_or(SyncError::DataError)?,
216 )
217 .debug_map_err(SyncError::DataError)?,
218 encrypted_user_key: UnsignedSharedKey::from_str(
219 &device.encrypted_user_key.ok_or(SyncError::DataError)?,
220 )
221 .debug_map_err(SyncError::DataError)?,
222 })
223 })
224 .collect::<Result<Vec<_>, _>>()?;
225 info!("Downloaded {} trusted devices", trusted_devices.len());
226 Ok(trusted_devices)
227}
228
229fn parse_ciphers(
230 ciphers: Option<Vec<bitwarden_api_api::models::CipherDetailsResponseModel>>,
231) -> Result<Vec<Cipher>, SyncError> {
232 let ciphers = ciphers
233 .ok_or(SyncError::DataError)?
234 .into_iter()
235 .map(|c| {
236 let _span = debug_span!("deserializing_cipher", cipher_id = ?c.id).entered();
237 Cipher::try_from(c).debug_map_err(SyncError::DataError)
238 })
239 .collect::<Result<Vec<_>, _>>()?;
240 info!("Deserialized {} ciphers", ciphers.len());
241 Ok(ciphers)
242}
243
244fn parse_folders(
245 folders: Option<Vec<bitwarden_api_api::models::FolderResponseModel>>,
246) -> Result<Vec<Folder>, SyncError> {
247 let folders = folders
248 .ok_or(SyncError::DataError)?
249 .into_iter()
250 .map(|f| {
251 let _span = debug_span!("deserializing_folder", folder_id = ?f.id).entered();
252 Folder::try_from(f).debug_map_err(SyncError::DataError)
253 })
254 .collect::<Result<Vec<_>, _>>()?;
255 info!("Deserialized {} folders", folders.len());
256 Ok(folders)
257}
258
259fn parse_sends(
260 sends: Option<Vec<bitwarden_api_api::models::SendResponseModel>>,
261) -> Result<Vec<bitwarden_send::Send>, SyncError> {
262 let sends = sends
263 .ok_or(SyncError::DataError)?
264 .into_iter()
265 .map(|s| {
266 let _span = debug_span!("deserializing_send", send_id = ?s.id).entered();
267 bitwarden_send::Send::try_from(s).debug_map_err(SyncError::DataError)
268 })
269 .collect::<Result<Vec<_>, _>>()?;
270 info!("Deserialized {} sends", sends.len());
271 Ok(sends)
272}
273
274fn from_kdf(
275 kdf: &bitwarden_api_api::models::MasterPasswordUnlockKdfResponseModel,
276) -> Result<Kdf, ()> {
277 Ok(match kdf.kdf_type {
278 bitwarden_api_api::models::KdfType::PBKDF2_SHA256 => Kdf::PBKDF2 {
279 iterations: std::num::NonZeroU32::new(kdf.iterations.try_into().debug_map_err(())?)
280 .ok_or(())?,
281 },
282 bitwarden_api_api::models::KdfType::Argon2id => {
283 let memory = kdf.memory.ok_or(())?;
284 let parallelism = kdf.parallelism.ok_or(())?;
285 Kdf::Argon2id {
286 iterations: std::num::NonZeroU32::new(kdf.iterations.try_into().debug_map_err(())?)
287 .ok_or(())?,
288 memory: std::num::NonZeroU32::new(memory.try_into().debug_map_err(())?).ok_or(())?,
289 parallelism: std::num::NonZeroU32::new(parallelism.try_into().debug_map_err(())?)
290 .ok_or(())?,
291 }
292 }
293 bitwarden_api_api::models::KdfType::__Unknown(_) => return Err(()),
294 })
295}
296
297fn parse_kdf_and_salt(
300 user_decryption: &Option<Box<bitwarden_api_api::models::UserDecryptionResponseModel>>,
301) -> Result<Option<(Kdf, String)>, SyncError> {
302 let user_decryption_options = user_decryption.as_ref().ok_or(SyncError::DataError)?;
303 if let Some(master_password_unlock) = &user_decryption_options.master_password_unlock {
304 let kdf =
305 from_kdf(&master_password_unlock.clone().kdf).debug_map_err(SyncError::DataError)?;
306 let salt = master_password_unlock
307 .clone()
308 .salt
309 .ok_or(SyncError::DataError)?;
310 debug!("Parsed password KDF and salt from sync response");
311 Ok(Some((kdf, salt)))
312 } else {
313 debug!(
314 "User does not have master password decryption options, skipping KDF and salt parsing"
315 );
316 Ok(None)
317 }
318}
319
320pub(super) async fn sync_current_account_data(
321 api_client: &ApiClient,
322) -> Result<SyncedAccountData, SyncError> {
323 info!("Syncing latest vault state from server for key rotation");
324 let sync = api_client
325 .sync_api()
326 .get(Some(true))
327 .await
328 .debug_map_err(SyncError::NetworkError)?;
329
330 let profile = sync.profile.as_ref().ok_or(SyncError::DataError)?;
331 let kdf_and_salt = parse_kdf_and_salt(&sync.user_decryption)?;
333 let account_cryptographic_state = profile
334 .account_keys
335 .to_owned()
336 .ok_or(SyncError::DataError)?;
337 let ciphers = parse_ciphers(sync.ciphers)?;
338 let folders = parse_folders(sync.folders)?;
339 let sends = parse_sends(sync.sends)?;
340 let wrapped_account_cryptographic_state =
341 WrappedAccountCryptographicState::try_from(account_cryptographic_state.as_ref())
342 .debug_map_err(SyncError::DataError)?;
343
344 info!("Syncing additional data (organizations, emergency access, devices, passkeys)");
347 let (organization_memberships, emergency_access_memberships, trusted_devices, passkeys) = try_join!(
348 sync_orgs(api_client),
349 sync_emergency_access(api_client),
350 sync_devices(api_client),
351 sync_passkeys(api_client),
352 )?;
353
354 Ok(SyncedAccountData {
355 wrapped_account_cryptographic_state,
356 folders,
357 ciphers,
358 sends,
359 emergency_access_memberships,
360 organization_memberships,
361 trusted_devices,
362 passkeys,
363 kdf_and_salt,
364 })
365}
366
367#[cfg(test)]
368mod tests {
369 use bitwarden_api_api::{
370 apis::ApiClient,
371 models::{
372 DeviceAuthRequestResponseModel, DeviceAuthRequestResponseModelListResponseModel,
373 EmergencyAccessGranteeDetailsResponseModel,
374 EmergencyAccessGranteeDetailsResponseModelListResponseModel, FolderResponseModel,
375 KdfType, MasterPasswordUnlockKdfResponseModel, MasterPasswordUnlockResponseModel,
376 OrganizationPublicKeyResponseModel, PrivateKeysResponseModel,
377 ProfileOrganizationResponseModel, ProfileOrganizationResponseModelListResponseModel,
378 ProfileResponseModel, PublicKeyEncryptionKeyPairResponseModel, SendResponseModel,
379 SendType, SyncResponseModel, UserDecryptionResponseModel, UserKeyResponseModel,
380 WebAuthnCredentialResponseModel, WebAuthnCredentialResponseModelListResponseModel,
381 WebAuthnPrfStatus,
382 },
383 };
384 use bitwarden_encoding::B64;
385 use bitwarden_vault::{CipherId, FolderId};
386
387 use super::*;
388
389 const TEST_ENC_STRING: &str = "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=";
390 const KEY_ENC_STRING: &str = "2.KLv/j0V4Ebs0dwyPdtt4vw==|Nczvv+DTkeP466cP/wMDnGK6W9zEIg5iHLhcuQG6s+M=|SZGsfuIAIaGZ7/kzygaVUau3LeOvJUlolENBOU+LX7g=";
391 const TEST_UNSIGNED_SHARED_KEY: &str = "4.AAAAAAAAAAAAAAAAAAAAAA==";
392
393 const TEST_RSA_PUBLIC_KEY_BYTES: &[u8] = &[
394 48, 130, 1, 34, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 130, 1, 15, 0,
395 48, 130, 1, 10, 2, 130, 1, 1, 0, 173, 4, 54, 63, 125, 12, 254, 38, 115, 34, 95, 164, 148,
396 115, 86, 140, 129, 74, 19, 70, 212, 212, 130, 163, 105, 249, 101, 120, 154, 46, 194, 250,
397 229, 242, 156, 67, 109, 179, 187, 134, 59, 235, 60, 107, 144, 163, 35, 22, 109, 230, 134,
398 243, 44, 243, 79, 84, 76, 11, 64, 56, 236, 167, 98, 26, 30, 213, 143, 105, 52, 92, 129, 92,
399 88, 22, 115, 135, 63, 215, 79, 8, 11, 183, 124, 10, 73, 231, 170, 110, 210, 178, 22, 100,
400 76, 75, 118, 202, 252, 204, 67, 204, 152, 6, 244, 208, 161, 146, 103, 225, 233, 239, 88,
401 195, 88, 150, 230, 111, 62, 142, 12, 157, 184, 155, 34, 84, 237, 111, 11, 97, 56, 152, 130,
402 14, 72, 123, 140, 47, 137, 5, 97, 166, 4, 147, 111, 23, 65, 78, 63, 208, 198, 50, 161, 39,
403 80, 143, 100, 194, 37, 252, 194, 53, 207, 166, 168, 250, 165, 121, 9, 207, 90, 36, 213,
404 211, 84, 255, 14, 205, 114, 135, 217, 137, 105, 232, 58, 169, 222, 10, 13, 138, 203, 16,
405 12, 122, 72, 227, 95, 160, 111, 54, 200, 198, 143, 156, 15, 143, 196, 50, 150, 204, 144,
406 255, 162, 248, 50, 28, 47, 66, 9, 83, 158, 67, 9, 50, 147, 174, 147, 200, 199, 238, 190,
407 248, 60, 114, 218, 32, 209, 120, 218, 17, 234, 14, 128, 192, 166, 33, 60, 73, 227, 108,
408 201, 41, 160, 81, 133, 171, 205, 221, 2, 3, 1, 0, 1,
409 ];
410
411 fn test_public_key_b64() -> String {
412 B64::from(TEST_RSA_PUBLIC_KEY_BYTES.to_vec()).to_string()
413 }
414
415 fn create_test_folder(id: uuid::Uuid) -> FolderResponseModel {
416 FolderResponseModel {
417 object: Some("folder".to_string()),
418 id: Some(id),
419 name: Some(TEST_ENC_STRING.to_string()),
420 revision_date: Some("2024-01-01T00:00:00Z".to_string()),
421 }
422 }
423
424 fn create_test_cipher(id: uuid::Uuid) -> bitwarden_api_api::models::CipherDetailsResponseModel {
425 bitwarden_api_api::models::CipherDetailsResponseModel {
426 object: Some("cipher".to_string()),
427 id: Some(id),
428 organization_id: None,
429 r#type: Some(bitwarden_api_api::models::CipherType::Login),
430 data: None,
431 name: Some(TEST_ENC_STRING.to_string()),
432 notes: None,
433 login: None,
434 card: None,
435 identity: None,
436 secure_note: None,
437 ssh_key: None,
438 fields: None,
439 password_history: None,
440 attachments: None,
441 organization_use_totp: Some(false),
442 revision_date: Some("2024-01-01T00:00:00Z".to_string()),
443 creation_date: Some("2024-01-01T00:00:00Z".to_string()),
444 deleted_date: None,
445 reprompt: Some(bitwarden_api_api::models::CipherRepromptType::None),
446 key: None,
447 archived_date: None,
448 folder_id: None,
449 favorite: Some(false),
450 edit: Some(true),
451 view_password: Some(true),
452 permissions: None,
453 collection_ids: None,
454 }
455 }
456
457 fn create_test_send(id: uuid::Uuid) -> SendResponseModel {
458 SendResponseModel {
459 object: Some("send".to_string()),
460 id: Some(id),
461 access_id: Some("access_id".to_string()),
462 r#type: Some(SendType::Text),
463 name: Some(TEST_ENC_STRING.to_string()),
464 notes: None,
465 file: None,
466 text: None,
467 key: Some(KEY_ENC_STRING.to_string()),
468 max_access_count: None,
469 access_count: Some(0),
470 password: None,
471 disabled: Some(false),
472 revision_date: Some("2024-01-01T00:00:00Z".to_string()),
473 expiration_date: None,
474 deletion_date: Some("2024-12-31T00:00:00Z".to_string()),
475 hide_email: Some(false),
476 auth_type: None,
477 emails: None,
478 }
479 }
480
481 fn create_test_user_decryption() -> UserDecryptionResponseModel {
482 UserDecryptionResponseModel {
483 master_password_unlock: Some(Box::new(MasterPasswordUnlockResponseModel {
484 kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
485 kdf_type: KdfType::PBKDF2_SHA256,
486 iterations: 600000,
487 memory: None,
488 parallelism: None,
489 }),
490 master_key_encrypted_user_key: None,
491 salt: Some("test_salt".to_string()),
492 })),
493 web_authn_prf_options: None,
494 v2_upgrade_token: None,
495 }
496 }
497
498 fn create_test_profile(user_id: uuid::Uuid) -> ProfileResponseModel {
499 ProfileResponseModel {
500 id: Some(user_id),
501 account_keys: Some(Box::new(PrivateKeysResponseModel {
502 object: None,
503 signature_key_pair: None,
504 public_key_encryption_key_pair: Box::new(PublicKeyEncryptionKeyPairResponseModel {
505 object: None,
506 wrapped_private_key: Some(TEST_ENC_STRING.to_string()),
507 public_key: None,
508 signed_public_key: None,
509 }),
510 security_state: None,
511 })),
512 ..ProfileResponseModel::default()
513 }
514 }
515
516 fn create_test_sync_response(user_id: uuid::Uuid) -> SyncResponseModel {
517 SyncResponseModel {
518 object: Some("sync".to_string()),
519 profile: Some(Box::new(create_test_profile(user_id))),
520 folders: Some(vec![create_test_folder(uuid::Uuid::new_v4())]),
521 ciphers: Some(vec![create_test_cipher(uuid::Uuid::new_v4())]),
522 sends: Some(vec![create_test_send(uuid::Uuid::new_v4())]),
523 collections: None,
524 domains: None,
525 policies: None,
526 user_decryption: Some(Box::new(create_test_user_decryption())),
527 }
528 }
529
530 fn create_test_org_list_response(
531 org_id: uuid::Uuid,
532 ) -> ProfileOrganizationResponseModelListResponseModel {
533 ProfileOrganizationResponseModelListResponseModel {
534 object: None,
535 data: Some(vec![ProfileOrganizationResponseModel {
536 id: Some(org_id),
537 name: Some("Test Org".to_string()),
538 reset_password_enrolled: Some(true),
539 ..ProfileOrganizationResponseModel::new()
540 }]),
541 continuation_token: None,
542 }
543 }
544
545 fn create_test_org_public_key_response() -> OrganizationPublicKeyResponseModel {
546 OrganizationPublicKeyResponseModel {
547 object: None,
548 public_key: Some(test_public_key_b64()),
549 }
550 }
551
552 fn create_test_emergency_access_response(
553 ea_id: uuid::Uuid,
554 grantee_id: uuid::Uuid,
555 ) -> EmergencyAccessGranteeDetailsResponseModelListResponseModel {
556 EmergencyAccessGranteeDetailsResponseModelListResponseModel {
557 object: None,
558 data: Some(vec![EmergencyAccessGranteeDetailsResponseModel {
559 id: Some(ea_id),
560 grantee_id: Some(grantee_id),
561 name: Some("Emergency Contact".to_string()),
562 ..EmergencyAccessGranteeDetailsResponseModel::new()
563 }]),
564 continuation_token: None,
565 }
566 }
567
568 fn create_test_user_key_response() -> UserKeyResponseModel {
569 UserKeyResponseModel {
570 object: None,
571 user_id: None,
572 public_key: Some(test_public_key_b64()),
573 }
574 }
575
576 fn create_test_devices_response(
577 device_id: uuid::Uuid,
578 ) -> DeviceAuthRequestResponseModelListResponseModel {
579 DeviceAuthRequestResponseModelListResponseModel {
580 object: None,
581 data: Some(vec![DeviceAuthRequestResponseModel {
582 id: Some(device_id),
583 is_trusted: Some(true),
584 encrypted_user_key: Some(TEST_UNSIGNED_SHARED_KEY.to_string()),
585 encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
586 ..DeviceAuthRequestResponseModel::new()
587 }]),
588 continuation_token: None,
589 }
590 }
591
592 fn create_test_passkeys_response(
593 passkey_id: uuid::Uuid,
594 ) -> WebAuthnCredentialResponseModelListResponseModel {
595 WebAuthnCredentialResponseModelListResponseModel {
596 object: None,
597 data: Some(vec![WebAuthnCredentialResponseModel {
598 id: Some(passkey_id.to_string()),
599 prf_status: Some(WebAuthnPrfStatus::Enabled),
600 encrypted_user_key: Some(TEST_UNSIGNED_SHARED_KEY.to_string()),
601 encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
602 ..WebAuthnCredentialResponseModel::new()
603 }]),
604 continuation_token: None,
605 }
606 }
607
608 #[tokio::test]
609 async fn test_sync_current_account_data_success() {
610 let user_id = uuid::Uuid::new_v4();
611 let org_id = uuid::Uuid::new_v4();
612 let ea_id = uuid::Uuid::new_v4();
613 let grantee_id = uuid::Uuid::new_v4();
614 let device_id = uuid::Uuid::new_v4();
615 let passkey_id = uuid::Uuid::new_v4();
616 let folder_id = uuid::Uuid::new_v4();
617 let cipher_id = uuid::Uuid::new_v4();
618 let send_id = uuid::Uuid::new_v4();
619
620 let api_client = ApiClient::new_mocked(|mock| {
621 mock.sync_api
622 .expect_get()
623 .once()
624 .returning(move |_exclude_domains| {
625 let mut response = create_test_sync_response(user_id);
626 response.folders = Some(vec![create_test_folder(folder_id)]);
627 response.ciphers = Some(vec![create_test_cipher(cipher_id)]);
628 response.sends = Some(vec![create_test_send(send_id)]);
629 Ok(response)
630 });
631 mock.organizations_api
632 .expect_get_user()
633 .once()
634 .returning(move || Ok(create_test_org_list_response(org_id)));
635 mock.organizations_api
636 .expect_get_public_key()
637 .once()
638 .returning(move |_id| Ok(create_test_org_public_key_response()));
639 mock.emergency_access_api
640 .expect_get_contacts()
641 .once()
642 .returning(move || Ok(create_test_emergency_access_response(ea_id, grantee_id)));
643 mock.users_api
644 .expect_get_public_key()
645 .once()
646 .returning(move |_user_id| Ok(create_test_user_key_response()));
647 mock.devices_api
648 .expect_get_all()
649 .once()
650 .returning(move || Ok(create_test_devices_response(device_id)));
651 mock.web_authn_api
652 .expect_get()
653 .once()
654 .returning(move || Ok(create_test_passkeys_response(passkey_id)));
655 });
656
657 let result = sync_current_account_data(&api_client).await;
658 let data = result.unwrap();
659
660 assert_eq!(data.folders.len(), 1);
662 assert_eq!(data.folders[0].id, Some(FolderId::new(folder_id)));
663 assert_eq!(data.folders[0].name, TEST_ENC_STRING.parse().unwrap());
664
665 assert_eq!(data.ciphers.len(), 1);
667 assert_eq!(data.ciphers[0].id, Some(CipherId::new(cipher_id)));
668 assert_eq!(data.ciphers[0].name, TEST_ENC_STRING.parse().unwrap());
669
670 assert_eq!(data.sends.len(), 1);
672 assert_eq!(data.sends[0].id, Some(send_id));
673 assert_eq!(data.sends[0].name, TEST_ENC_STRING.parse().unwrap());
674 assert_eq!(data.sends[0].key, KEY_ENC_STRING.parse().unwrap());
675
676 assert_eq!(data.organization_memberships.len(), 1);
677 assert_eq!(data.organization_memberships[0].organization_id, org_id);
678 assert_eq!(data.emergency_access_memberships.len(), 1);
679 assert_eq!(data.emergency_access_memberships[0].id, ea_id);
680 assert_eq!(data.trusted_devices.len(), 1);
681 assert_eq!(data.trusted_devices[0].id, device_id);
682 assert_eq!(data.passkeys.len(), 1);
683 assert_eq!(data.passkeys[0].id, passkey_id);
684 assert!(data.kdf_and_salt.is_some());
685 let (kdf, salt) = data.kdf_and_salt.unwrap();
686 assert_eq!(salt, "test_salt");
687 assert!(matches!(kdf, Kdf::PBKDF2 { iterations } if iterations.get() == 600000));
688 assert!(matches!(
689 data.wrapped_account_cryptographic_state,
690 WrappedAccountCryptographicState::V1 { .. }
691 ));
692
693 if let ApiClient::Mock(mut mock) = api_client {
694 mock.sync_api.checkpoint();
695 mock.organizations_api.checkpoint();
696 mock.emergency_access_api.checkpoint();
697 mock.users_api.checkpoint();
698 mock.devices_api.checkpoint();
699 mock.web_authn_api.checkpoint();
700 }
701 }
702
703 #[tokio::test]
704 async fn test_sync_current_account_data_network_error() {
705 let api_client = ApiClient::new_mocked(|mock| {
706 mock.sync_api
707 .expect_get()
708 .once()
709 .returning(move |_exclude_domains| {
710 Err(bitwarden_api_api::apis::Error::Serde(
711 serde_json::Error::io(std::io::Error::other("API error")),
712 ))
713 });
714 mock.organizations_api.expect_get_user().never();
715 mock.organizations_api.expect_get_public_key().never();
716 mock.emergency_access_api.expect_get_contacts().never();
717 mock.users_api.expect_get_public_key().never();
718 mock.devices_api.expect_get_all().never();
719 mock.web_authn_api.expect_get().never();
720 });
721
722 let result = sync_current_account_data(&api_client).await;
723
724 assert!(matches!(result, Err(SyncError::NetworkError)));
725
726 if let ApiClient::Mock(mut mock) = api_client {
727 mock.sync_api.checkpoint();
728 mock.organizations_api.checkpoint();
729 mock.emergency_access_api.checkpoint();
730 mock.users_api.checkpoint();
731 mock.devices_api.checkpoint();
732 mock.web_authn_api.checkpoint();
733 }
734 }
735
736 #[tokio::test]
737 async fn test_fetch_organization_public_key_success() {
738 let org_id = uuid::Uuid::new_v4();
739 let expected_public_key_b64 = test_public_key_b64();
740
741 let api_client = ApiClient::new_mocked(|mock| {
742 let expected_public_key_b64 = expected_public_key_b64.clone();
743 mock.organizations_api
744 .expect_get_public_key()
745 .once()
746 .withf(move |id| id == org_id.to_string())
747 .returning(move |_| {
748 Ok(OrganizationPublicKeyResponseModel {
749 object: None,
750 public_key: Some(expected_public_key_b64.clone()),
751 })
752 });
753 });
754
755 let result = fetch_organization_public_key(&api_client, org_id).await;
756
757 assert!(result.is_ok());
758 let public_key = result.unwrap();
759
760 let expected_public_key = PublicKey::from_der(&SpkiPublicKeyBytes::from(
762 TEST_RSA_PUBLIC_KEY_BYTES.to_vec(),
763 ))
764 .unwrap();
765 assert_eq!(
766 public_key.to_der().unwrap(),
767 expected_public_key.to_der().unwrap()
768 );
769
770 if let ApiClient::Mock(mut mock) = api_client {
771 mock.organizations_api.checkpoint();
772 }
773 }
774
775 #[tokio::test]
776 async fn test_fetch_organization_public_key_network_error() {
777 let org_id = uuid::Uuid::new_v4();
778
779 let api_client = ApiClient::new_mocked(|mock| {
780 mock.organizations_api
781 .expect_get_public_key()
782 .once()
783 .returning(move |_| {
784 Err(bitwarden_api_api::apis::Error::Serde(
785 serde_json::Error::io(std::io::Error::other("Network error")),
786 ))
787 });
788 });
789
790 let result = fetch_organization_public_key(&api_client, org_id).await;
791
792 assert!(matches!(result, Err(SyncError::NetworkError)));
793
794 if let ApiClient::Mock(mut mock) = api_client {
795 mock.organizations_api.checkpoint();
796 }
797 }
798
799 #[tokio::test]
800 async fn test_sync_orgs_success_multiple_orgs() {
801 let org_id1 = uuid::Uuid::new_v4();
802 let org_id2 = uuid::Uuid::new_v4();
803 let org_id3 = uuid::Uuid::new_v4();
804 let org_name1 = "Organization One".to_string();
805 let org_name2 = "Organization Two".to_string();
806 let org_name3 = "Organization Three".to_string();
807 let expected_public_key_b64 = test_public_key_b64();
808
809 let api_client = ApiClient::new_mocked(|mock| {
810 let org_name1 = org_name1.clone();
811 let org_name2 = org_name2.clone();
812 let org_name3 = org_name3.clone();
813 mock.organizations_api
814 .expect_get_user()
815 .once()
816 .returning(move || {
817 Ok(ProfileOrganizationResponseModelListResponseModel {
818 object: None,
819 data: Some(vec![
820 ProfileOrganizationResponseModel {
821 id: Some(org_id1),
822 name: Some(org_name1.clone()),
823 reset_password_enrolled: Some(true),
824 ..ProfileOrganizationResponseModel::new()
825 },
826 ProfileOrganizationResponseModel {
827 id: Some(org_id2),
828 name: Some(org_name2.clone()),
829 reset_password_enrolled: Some(true),
830 ..ProfileOrganizationResponseModel::new()
831 },
832 ProfileOrganizationResponseModel {
833 id: Some(org_id3),
834 name: Some(org_name3.clone()),
835 reset_password_enrolled: Some(true),
836 ..ProfileOrganizationResponseModel::new()
837 },
838 ]),
839 continuation_token: None,
840 })
841 });
842
843 let expected_public_key_b64 = expected_public_key_b64.clone();
844 mock.organizations_api
845 .expect_get_public_key()
846 .times(3)
847 .returning(move |_| {
848 Ok(OrganizationPublicKeyResponseModel {
849 object: None,
850 public_key: Some(expected_public_key_b64.clone()),
851 })
852 });
853 });
854
855 let result = sync_orgs(&api_client).await;
856 let memberships = result.unwrap();
857
858 assert_eq!(memberships.len(), 3);
859 assert_eq!(memberships[0].organization_id, org_id1);
860 assert_eq!(memberships[0].name, org_name1);
861 assert_eq!(memberships[1].organization_id, org_id2);
862 assert_eq!(memberships[1].name, org_name2);
863 assert_eq!(memberships[2].organization_id, org_id3);
864 assert_eq!(memberships[2].name, org_name3);
865
866 let expected_public_key = PublicKey::from_der(&SpkiPublicKeyBytes::from(
868 TEST_RSA_PUBLIC_KEY_BYTES.to_vec(),
869 ))
870 .unwrap();
871 for membership in &memberships {
872 assert_eq!(
873 membership.public_key.to_der().unwrap(),
874 expected_public_key.to_der().unwrap()
875 );
876 }
877
878 if let ApiClient::Mock(mut mock) = api_client {
879 mock.organizations_api.checkpoint();
880 }
881 }
882
883 #[tokio::test]
884 async fn test_sync_orgs_network_error() {
885 let api_client = ApiClient::new_mocked(|mock| {
886 mock.organizations_api
887 .expect_get_user()
888 .once()
889 .returning(move || {
890 Err(bitwarden_api_api::apis::Error::Serde(
891 serde_json::Error::io(std::io::Error::other("Network error")),
892 ))
893 });
894
895 mock.organizations_api.expect_get_public_key().never();
896 });
897
898 let result = sync_orgs(&api_client).await;
899
900 assert!(matches!(result, Err(SyncError::NetworkError)));
901
902 if let ApiClient::Mock(mut mock) = api_client {
903 mock.organizations_api.checkpoint();
904 }
905 }
906
907 #[tokio::test]
908 async fn test_sync_orgs_public_key_fetch_fails() {
909 let org_id = uuid::Uuid::new_v4();
910
911 let api_client = ApiClient::new_mocked(|mock| {
912 mock.organizations_api
913 .expect_get_user()
914 .once()
915 .returning(move || {
916 Ok(ProfileOrganizationResponseModelListResponseModel {
917 object: None,
918 data: Some(vec![ProfileOrganizationResponseModel {
919 id: Some(org_id),
920 name: Some("Test Org".to_string()),
921 reset_password_enrolled: Some(true),
922 ..ProfileOrganizationResponseModel::new()
923 }]),
924 continuation_token: None,
925 })
926 });
927
928 mock.organizations_api
929 .expect_get_public_key()
930 .once()
931 .returning(move |_| {
932 Err(bitwarden_api_api::apis::Error::Serde(
933 serde_json::Error::io(std::io::Error::other("Network error")),
934 ))
935 });
936 });
937
938 let result = sync_orgs(&api_client).await;
939 assert!(matches!(result, Err(SyncError::NetworkError)));
940
941 if let ApiClient::Mock(mut mock) = api_client {
942 mock.organizations_api.checkpoint();
943 }
944 }
945
946 #[tokio::test]
947 async fn test_sync_passkeys_success_multiple_passkeys() {
948 let passkey_id1 = uuid::Uuid::new_v4();
949 let passkey_id2 = uuid::Uuid::new_v4();
950 let passkey_id3 = uuid::Uuid::new_v4();
951
952 let api_client = ApiClient::new_mocked(|mock| {
953 mock.web_authn_api.expect_get().once().returning(move || {
954 Ok(WebAuthnCredentialResponseModelListResponseModel {
955 object: None,
956 data: Some(vec![
957 WebAuthnCredentialResponseModel {
958 id: Some(passkey_id1.to_string()),
959 prf_status: Some(WebAuthnPrfStatus::Enabled),
960 encrypted_user_key: Some(TEST_UNSIGNED_SHARED_KEY.to_string()),
961 encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
962 ..WebAuthnCredentialResponseModel::new()
963 },
964 WebAuthnCredentialResponseModel {
965 id: Some(passkey_id2.to_string()),
966 prf_status: Some(WebAuthnPrfStatus::Enabled),
967 encrypted_user_key: Some(TEST_UNSIGNED_SHARED_KEY.to_string()),
968 encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
969 ..WebAuthnCredentialResponseModel::new()
970 },
971 WebAuthnCredentialResponseModel {
972 id: Some(passkey_id3.to_string()),
973 prf_status: Some(WebAuthnPrfStatus::Enabled),
974 encrypted_user_key: Some(TEST_UNSIGNED_SHARED_KEY.to_string()),
975 encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
976 ..WebAuthnCredentialResponseModel::new()
977 },
978 ]),
979 continuation_token: None,
980 })
981 });
982 });
983
984 let result = sync_passkeys(&api_client).await;
985 let passkeys = result.unwrap();
986
987 assert_eq!(passkeys.len(), 3);
988 assert_eq!(passkeys[0].id, passkey_id1);
989 assert_eq!(passkeys[1].id, passkey_id2);
990 assert_eq!(passkeys[2].id, passkey_id3);
991
992 for passkey in &passkeys {
994 assert_eq!(
995 passkey.encrypted_public_key.to_string(),
996 TEST_ENC_STRING.to_string()
997 );
998 assert_eq!(
999 passkey.encrypted_user_key.to_string(),
1000 TEST_UNSIGNED_SHARED_KEY.to_string()
1001 );
1002 }
1003
1004 if let ApiClient::Mock(mut mock) = api_client {
1005 mock.web_authn_api.checkpoint();
1006 }
1007 }
1008
1009 #[tokio::test]
1010 async fn test_sync_passkeys_filters_passkeys_without_prf_encryption_enabled() {
1011 let enabled_passkey_id = uuid::Uuid::new_v4();
1012 let supported_passkey_id = uuid::Uuid::new_v4();
1013 let unsupported_passkey_id = uuid::Uuid::new_v4();
1014 let no_prf_status_passkey_id = uuid::Uuid::new_v4();
1015
1016 let api_client = ApiClient::new_mocked(|mock| {
1017 mock.web_authn_api.expect_get().once().returning(move || {
1018 Ok(WebAuthnCredentialResponseModelListResponseModel {
1019 object: None,
1020 data: Some(vec![
1021 WebAuthnCredentialResponseModel {
1022 id: Some(enabled_passkey_id.to_string()),
1023 prf_status: Some(WebAuthnPrfStatus::Enabled),
1024 encrypted_user_key: Some(TEST_UNSIGNED_SHARED_KEY.to_string()),
1025 encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
1026 ..WebAuthnCredentialResponseModel::new()
1027 },
1028 WebAuthnCredentialResponseModel {
1029 id: Some(supported_passkey_id.to_string()),
1030 prf_status: Some(WebAuthnPrfStatus::Supported),
1031 encrypted_user_key: None,
1033 encrypted_public_key: None,
1034 ..WebAuthnCredentialResponseModel::new()
1035 },
1036 WebAuthnCredentialResponseModel {
1037 id: Some(unsupported_passkey_id.to_string()),
1038 prf_status: Some(WebAuthnPrfStatus::Unsupported),
1039 encrypted_user_key: None,
1040 encrypted_public_key: None,
1041 ..WebAuthnCredentialResponseModel::new()
1042 },
1043 WebAuthnCredentialResponseModel {
1044 id: Some(no_prf_status_passkey_id.to_string()),
1045 prf_status: None,
1046 encrypted_user_key: None,
1047 encrypted_public_key: None,
1048 ..WebAuthnCredentialResponseModel::new()
1049 },
1050 ]),
1051 continuation_token: None,
1052 })
1053 });
1054 });
1055
1056 let result = sync_passkeys(&api_client).await;
1057 let passkeys = result.unwrap();
1058
1059 assert_eq!(passkeys.len(), 1);
1061 assert_eq!(passkeys[0].id, enabled_passkey_id);
1062 assert_eq!(
1063 passkeys[0].encrypted_public_key.to_string(),
1064 TEST_ENC_STRING.to_string()
1065 );
1066 assert_eq!(
1067 passkeys[0].encrypted_user_key.to_string(),
1068 TEST_UNSIGNED_SHARED_KEY.to_string()
1069 );
1070
1071 if let ApiClient::Mock(mut mock) = api_client {
1072 mock.web_authn_api.checkpoint();
1073 }
1074 }
1075
1076 #[tokio::test]
1077 async fn test_sync_passkeys_network_error() {
1078 let api_client = ApiClient::new_mocked(|mock| {
1079 mock.web_authn_api.expect_get().once().returning(move || {
1080 Err(bitwarden_api_api::apis::Error::Serde(
1081 serde_json::Error::io(std::io::Error::other("Network error")),
1082 ))
1083 });
1084 });
1085
1086 let result = sync_passkeys(&api_client).await;
1087
1088 assert!(matches!(result, Err(SyncError::NetworkError)));
1089
1090 if let ApiClient::Mock(mut mock) = api_client {
1091 mock.web_authn_api.checkpoint();
1092 }
1093 }
1094
1095 #[tokio::test]
1096 async fn test_sync_devices_success_multiple_devices() {
1097 let device_id1 = uuid::Uuid::new_v4();
1098 let device_id2 = uuid::Uuid::new_v4();
1099 let device_id3 = uuid::Uuid::new_v4();
1100 let untrusted_device_id = uuid::Uuid::new_v4();
1101
1102 let api_client = ApiClient::new_mocked(|mock| {
1103 mock.devices_api.expect_get_all().once().returning(move || {
1104 Ok(DeviceAuthRequestResponseModelListResponseModel {
1105 object: None,
1106 data: Some(vec![
1107 DeviceAuthRequestResponseModel {
1108 id: Some(device_id1),
1109 is_trusted: Some(true),
1110 encrypted_user_key: Some(TEST_UNSIGNED_SHARED_KEY.to_string()),
1111 encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
1112 ..DeviceAuthRequestResponseModel::new()
1113 },
1114 DeviceAuthRequestResponseModel {
1115 id: Some(device_id2),
1116 is_trusted: Some(true),
1117 encrypted_user_key: Some(TEST_UNSIGNED_SHARED_KEY.to_string()),
1118 encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
1119 ..DeviceAuthRequestResponseModel::new()
1120 },
1121 DeviceAuthRequestResponseModel {
1122 id: Some(untrusted_device_id),
1123 is_trusted: Some(false), encrypted_user_key: Some(TEST_UNSIGNED_SHARED_KEY.to_string()),
1125 encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
1126 ..DeviceAuthRequestResponseModel::new()
1127 },
1128 DeviceAuthRequestResponseModel {
1129 id: Some(device_id3),
1130 is_trusted: Some(true),
1131 encrypted_user_key: Some(TEST_UNSIGNED_SHARED_KEY.to_string()),
1132 encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
1133 ..DeviceAuthRequestResponseModel::new()
1134 },
1135 ]),
1136 continuation_token: None,
1137 })
1138 });
1139 });
1140
1141 let result = sync_devices(&api_client).await;
1142 let devices = result.unwrap();
1143
1144 assert_eq!(devices.len(), 3);
1146 assert_eq!(devices[0].id, device_id1);
1148 assert_eq!(devices[1].id, device_id2);
1149 assert_eq!(devices[2].id, device_id3);
1150
1151 for device in &devices {
1153 assert_eq!(
1154 device.encrypted_public_key.to_string(),
1155 TEST_ENC_STRING.to_string()
1156 );
1157 assert_eq!(
1158 device.encrypted_user_key.to_string(),
1159 TEST_UNSIGNED_SHARED_KEY.to_string()
1160 );
1161 }
1162
1163 if let ApiClient::Mock(mut mock) = api_client {
1164 mock.devices_api.checkpoint();
1165 }
1166 }
1167
1168 #[tokio::test]
1169 async fn test_sync_devices_network_error() {
1170 let api_client = ApiClient::new_mocked(|mock| {
1171 mock.devices_api.expect_get_all().once().returning(move || {
1172 Err(bitwarden_api_api::apis::Error::Serde(
1173 serde_json::Error::io(std::io::Error::other("Network error")),
1174 ))
1175 });
1176 });
1177
1178 let result = sync_devices(&api_client).await;
1179
1180 assert!(matches!(result, Err(SyncError::NetworkError)));
1181
1182 if let ApiClient::Mock(mut mock) = api_client {
1183 mock.devices_api.checkpoint();
1184 }
1185 }
1186
1187 #[tokio::test]
1188 async fn test_fetch_user_public_key_success() {
1189 let user_id = uuid::Uuid::new_v4();
1190 let expected_public_key_b64 = test_public_key_b64();
1191
1192 let api_client = ApiClient::new_mocked(|mock| {
1193 let expected_public_key_b64 = expected_public_key_b64.clone();
1194 mock.users_api
1195 .expect_get_public_key()
1196 .once()
1197 .withf(move |id| id == &user_id)
1198 .returning(move |_| {
1199 Ok(UserKeyResponseModel {
1200 object: None,
1201 user_id: None,
1202 public_key: Some(expected_public_key_b64.clone()),
1203 })
1204 });
1205 });
1206
1207 let result = fetch_user_public_key(&api_client, user_id).await;
1208 let public_key = result.unwrap();
1209
1210 let expected_public_key = PublicKey::from_der(&SpkiPublicKeyBytes::from(
1212 TEST_RSA_PUBLIC_KEY_BYTES.to_vec(),
1213 ))
1214 .unwrap();
1215 assert_eq!(
1216 public_key.to_der().unwrap(),
1217 expected_public_key.to_der().unwrap()
1218 );
1219
1220 if let ApiClient::Mock(mut mock) = api_client {
1221 mock.users_api.checkpoint();
1222 }
1223 }
1224
1225 #[tokio::test]
1226 async fn test_fetch_user_public_key_network_error() {
1227 let user_id = uuid::Uuid::new_v4();
1228
1229 let api_client = ApiClient::new_mocked(|mock| {
1230 mock.users_api
1231 .expect_get_public_key()
1232 .once()
1233 .returning(move |_| {
1234 Err(bitwarden_api_api::apis::Error::Serde(
1235 serde_json::Error::io(std::io::Error::other("Network error")),
1236 ))
1237 });
1238 });
1239
1240 let result = fetch_user_public_key(&api_client, user_id).await;
1241
1242 assert!(matches!(result, Err(SyncError::NetworkError)));
1243
1244 if let ApiClient::Mock(mut mock) = api_client {
1245 mock.users_api.checkpoint();
1246 }
1247 }
1248
1249 #[tokio::test]
1250 async fn test_sync_emergency_access_success_multiple_contacts() {
1251 let ea_id1 = uuid::Uuid::new_v4();
1252 let ea_id2 = uuid::Uuid::new_v4();
1253 let ea_id3 = uuid::Uuid::new_v4();
1254 let grantee_id1 = uuid::Uuid::new_v4();
1255 let grantee_id2 = uuid::Uuid::new_v4();
1256 let grantee_id3 = uuid::Uuid::new_v4();
1257 let ea_name1 = "Contact One".to_string();
1258 let ea_name2 = "Contact Two".to_string();
1259 let ea_name3 = "Contact Three".to_string();
1260 let expected_public_key_b64 = test_public_key_b64();
1261
1262 let api_client = ApiClient::new_mocked(|mock| {
1263 let ea_name1 = ea_name1.clone();
1264 let ea_name2 = ea_name2.clone();
1265 let ea_name3 = ea_name3.clone();
1266 mock.emergency_access_api
1267 .expect_get_contacts()
1268 .once()
1269 .returning(move || {
1270 Ok(
1271 EmergencyAccessGranteeDetailsResponseModelListResponseModel {
1272 object: None,
1273 data: Some(vec![
1274 EmergencyAccessGranteeDetailsResponseModel {
1275 id: Some(ea_id1),
1276 grantee_id: Some(grantee_id1),
1277 name: Some(ea_name1.clone()),
1278 ..EmergencyAccessGranteeDetailsResponseModel::new()
1279 },
1280 EmergencyAccessGranteeDetailsResponseModel {
1281 id: Some(ea_id2),
1282 grantee_id: Some(grantee_id2),
1283 name: Some(ea_name2.clone()),
1284 ..EmergencyAccessGranteeDetailsResponseModel::new()
1285 },
1286 EmergencyAccessGranteeDetailsResponseModel {
1287 id: Some(ea_id3),
1288 grantee_id: Some(grantee_id3),
1289 name: Some(ea_name3.clone()),
1290 ..EmergencyAccessGranteeDetailsResponseModel::new()
1291 },
1292 ]),
1293 continuation_token: None,
1294 },
1295 )
1296 });
1297
1298 let expected_public_key_b64 = expected_public_key_b64.clone();
1299 mock.users_api
1300 .expect_get_public_key()
1301 .times(3)
1302 .returning(move |_| {
1303 Ok(UserKeyResponseModel {
1304 object: None,
1305 user_id: None,
1306 public_key: Some(expected_public_key_b64.clone()),
1307 })
1308 });
1309 });
1310
1311 let result = sync_emergency_access(&api_client).await;
1312 let memberships = result.unwrap();
1313
1314 assert_eq!(memberships.len(), 3);
1315 assert_eq!(memberships[0].id, ea_id1);
1316 assert_eq!(memberships[0].name, ea_name1);
1317 assert_eq!(memberships[1].id, ea_id2);
1318 assert_eq!(memberships[1].name, ea_name2);
1319 assert_eq!(memberships[2].id, ea_id3);
1320 assert_eq!(memberships[2].name, ea_name3);
1321
1322 let expected_public_key = PublicKey::from_der(&SpkiPublicKeyBytes::from(
1324 TEST_RSA_PUBLIC_KEY_BYTES.to_vec(),
1325 ))
1326 .unwrap();
1327 for membership in &memberships {
1328 assert_eq!(
1329 membership.public_key.to_der().unwrap(),
1330 expected_public_key.to_der().unwrap()
1331 );
1332 }
1333
1334 if let ApiClient::Mock(mut mock) = api_client {
1335 mock.emergency_access_api.checkpoint();
1336 mock.users_api.checkpoint();
1337 }
1338 }
1339
1340 #[tokio::test]
1341 async fn test_sync_emergency_access_network_error() {
1342 let api_client = ApiClient::new_mocked(|mock| {
1343 mock.emergency_access_api
1344 .expect_get_contacts()
1345 .once()
1346 .returning(move || {
1347 Err(bitwarden_api_api::apis::Error::Serde(
1348 serde_json::Error::io(std::io::Error::other("Network error")),
1349 ))
1350 });
1351
1352 mock.users_api.expect_get_public_key().never();
1353 });
1354
1355 let result = sync_emergency_access(&api_client).await;
1356
1357 assert!(matches!(result, Err(SyncError::NetworkError)));
1358
1359 if let ApiClient::Mock(mut mock) = api_client {
1360 mock.emergency_access_api.checkpoint();
1361 mock.users_api.checkpoint();
1362 }
1363 }
1364
1365 #[tokio::test]
1366 async fn test_sync_emergency_access_user_key_fetch_fails() {
1367 let ea_id = uuid::Uuid::new_v4();
1368 let grantee_id = uuid::Uuid::new_v4();
1369
1370 let api_client = ApiClient::new_mocked(|mock| {
1371 mock.emergency_access_api
1372 .expect_get_contacts()
1373 .once()
1374 .returning(move || {
1375 Ok(
1376 EmergencyAccessGranteeDetailsResponseModelListResponseModel {
1377 object: None,
1378 data: Some(vec![EmergencyAccessGranteeDetailsResponseModel {
1379 id: Some(ea_id),
1380 grantee_id: Some(grantee_id),
1381 name: Some("Test Contact".to_string()),
1382 ..EmergencyAccessGranteeDetailsResponseModel::new()
1383 }]),
1384 continuation_token: None,
1385 },
1386 )
1387 });
1388
1389 mock.users_api
1390 .expect_get_public_key()
1391 .once()
1392 .returning(move |_| {
1393 Err(bitwarden_api_api::apis::Error::Serde(
1394 serde_json::Error::io(std::io::Error::other("Network error")),
1395 ))
1396 });
1397 });
1398
1399 let result = sync_emergency_access(&api_client).await;
1400 assert!(matches!(result, Err(SyncError::NetworkError)));
1401
1402 if let ApiClient::Mock(mut mock) = api_client {
1403 mock.emergency_access_api.checkpoint();
1404 mock.users_api.checkpoint();
1405 }
1406 }
1407
1408 #[tokio::test]
1409 async fn test_sync_orgs_filters_non_enrolled_orgs() {
1410 let org_id_enrolled1 = uuid::Uuid::new_v4();
1411 let org_id_not_enrolled = uuid::Uuid::new_v4();
1412 let org_id_none_enrolled = uuid::Uuid::new_v4();
1413 let org_id_enrolled2 = uuid::Uuid::new_v4();
1414 let expected_public_key_b64 = test_public_key_b64();
1415
1416 let api_client = ApiClient::new_mocked(|mock| {
1417 mock.organizations_api
1418 .expect_get_user()
1419 .once()
1420 .returning(move || {
1421 Ok(ProfileOrganizationResponseModelListResponseModel {
1422 object: None,
1423 data: Some(vec![
1424 ProfileOrganizationResponseModel {
1425 id: Some(org_id_enrolled1),
1426 name: Some("Enrolled Org 1".to_string()),
1427 reset_password_enrolled: Some(true),
1428 ..ProfileOrganizationResponseModel::new()
1429 },
1430 ProfileOrganizationResponseModel {
1431 id: Some(org_id_not_enrolled),
1432 name: Some("Not Enrolled Org".to_string()),
1433 reset_password_enrolled: Some(false),
1434 ..ProfileOrganizationResponseModel::new()
1435 },
1436 ProfileOrganizationResponseModel {
1437 id: Some(org_id_none_enrolled),
1438 name: Some("None Enrolled Org".to_string()),
1439 reset_password_enrolled: None,
1440 ..ProfileOrganizationResponseModel::new()
1441 },
1442 ProfileOrganizationResponseModel {
1443 id: Some(org_id_enrolled2),
1444 name: Some("Enrolled Org 2".to_string()),
1445 reset_password_enrolled: Some(true),
1446 ..ProfileOrganizationResponseModel::new()
1447 },
1448 ]),
1449 continuation_token: None,
1450 })
1451 });
1452
1453 let expected_public_key_b64 = expected_public_key_b64.clone();
1454 mock.organizations_api
1455 .expect_get_public_key()
1456 .times(2)
1457 .returning(move |_| {
1458 Ok(OrganizationPublicKeyResponseModel {
1459 object: None,
1460 public_key: Some(expected_public_key_b64.clone()),
1461 })
1462 });
1463 });
1464
1465 let result = sync_orgs(&api_client).await;
1466 let memberships = result.unwrap();
1467
1468 assert_eq!(memberships.len(), 2);
1469 assert_eq!(memberships[0].organization_id, org_id_enrolled1);
1470 assert_eq!(memberships[0].name, "Enrolled Org 1");
1471 assert_eq!(memberships[1].organization_id, org_id_enrolled2);
1472 assert_eq!(memberships[1].name, "Enrolled Org 2");
1473
1474 if let ApiClient::Mock(mut mock) = api_client {
1475 mock.organizations_api.checkpoint();
1476 }
1477 }
1478
1479 #[tokio::test]
1480 async fn test_sync_orgs_all_not_enrolled_returns_empty() {
1481 let api_client = ApiClient::new_mocked(|mock| {
1482 mock.organizations_api
1483 .expect_get_user()
1484 .once()
1485 .returning(move || {
1486 Ok(ProfileOrganizationResponseModelListResponseModel {
1487 object: None,
1488 data: Some(vec![
1489 ProfileOrganizationResponseModel {
1490 id: Some(uuid::Uuid::new_v4()),
1491 name: Some("Org A".to_string()),
1492 reset_password_enrolled: Some(false),
1493 ..ProfileOrganizationResponseModel::new()
1494 },
1495 ProfileOrganizationResponseModel {
1496 id: Some(uuid::Uuid::new_v4()),
1497 name: Some("Org B".to_string()),
1498 reset_password_enrolled: None,
1499 ..ProfileOrganizationResponseModel::new()
1500 },
1501 ]),
1502 continuation_token: None,
1503 })
1504 });
1505
1506 mock.organizations_api.expect_get_public_key().never();
1507 });
1508
1509 let result = sync_orgs(&api_client).await;
1510 let memberships = result.unwrap();
1511
1512 assert_eq!(memberships.len(), 0);
1513
1514 if let ApiClient::Mock(mut mock) = api_client {
1515 mock.organizations_api.checkpoint();
1516 }
1517 }
1518}