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