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