Skip to main content

bitwarden_user_crypto_management/key_rotation/
sync.rs

1//! Functionality for syncing the latest account data from the server
2use 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    /// Logs the error using `tracing::debug` and maps it to a new error type
25    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
58/// Fetch the public key for a single organization
59async 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
78// Download the public keys for the organizations for which reset password is enrolled, since these
79// are not included in the sync
80pub(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    // Await all fetches
106    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
118/// Fetch the public key for a user (used for emergency access)
119async 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
137/// Download the emergency access memberships and their public keys
138pub(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                // The name can be null if a user does not set a name.
161                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    // Await all fetches
170    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
182/// Sync the user's passkeys
183async 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
212/// Sync the user's trusted devices
213async 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
310/// Parses the user's KDF and salt from the sync response. If the user is not a master-password
311/// user, returns Ok(None)
312fn 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    // This is optional for master-password-users!
345    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    // Concurrently sync organization memberships, emergency access memberships, trusted devices,
358    // and passkeys
359    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        // Verify folders
677        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        // Verify ciphers
682        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        // Verify sends
687        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        // Verify the public key was correctly parsed from DER format
792        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        // Verify all public keys are correctly parsed
898        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        // Verify encrypted data is correctly parsed
1024        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                            // Non-enabled passkeys may not contain encryption material.
1063                            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        // Only passkeys with PRF encryption enabled should be included.
1091        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), // Not trusted
1155                            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        // Verify only trusted devices are returned (3 out of 4)
1176        assert_eq!(devices.len(), 3);
1177        // Verify each device's ID (untrusted device should not be included)
1178        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        // Verify encrypted data is correctly parsed
1183        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        // Verify the public key was correctly parsed from DER format
1242        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        // Verify all public keys are correctly parsed
1357        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                // Only called for the 3 contacts that pass the filter.
1505                .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        // Only Confirmed, RecoveryInitiated, and RecoveryApproved should be included.
1519        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}