Skip to main content

bitwarden_user_crypto_management/key_rotation/
sync.rs

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