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