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