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