Skip to main content

bitwarden_user_crypto_management/key_rotation/
sync.rs

1//! Functionality for syncing the latest account data from the server
2use bitwarden_api_api::apis::ApiClient;
3use bitwarden_core::key_management::account_cryptographic_state::WrappedAccountCryptographicState;
4use bitwarden_crypto::Kdf;
5use bitwarden_error::bitwarden_error;
6use bitwarden_vault::{Cipher, Folder};
7use thiserror::Error;
8use tracing::{debug, debug_span, info};
9
10use crate::key_rotation::{
11    partial_rotateable_keyset::PartialRotateableKeyset,
12    unlock::{V1EmergencyAccessMembership, V1OrganizationMembership},
13};
14
15trait DebugMapErr<T, E: std::fmt::Debug> {
16    /// Logs the error using `tracing::debug` and maps it to a new error type
17    fn debug_map_err<E2>(self, target: E2) -> Result<T, E2>;
18}
19
20impl<T, E: std::fmt::Debug> DebugMapErr<T, E> for Result<T, E> {
21    fn debug_map_err<E2>(self, target: E2) -> Result<T, E2> {
22        self.map_err(|e| {
23            debug!(error = ?e);
24            target
25        })
26    }
27}
28
29pub(super) struct SyncedAccountData {
30    pub(super) wrapped_account_cryptographic_state: WrappedAccountCryptographicState,
31    pub(super) folders: Vec<Folder>,
32    pub(super) ciphers: Vec<Cipher>,
33    pub(super) sends: Vec<bitwarden_send::Send>,
34    pub(super) emergency_access_memberships: Vec<V1EmergencyAccessMembership>,
35    pub(super) organization_memberships: Vec<V1OrganizationMembership>,
36    pub(super) trusted_devices: Vec<PartialRotateableKeyset>,
37    pub(super) passkeys: Vec<PartialRotateableKeyset>,
38    pub(super) kdf_and_salt: Option<(Kdf, String)>,
39}
40
41#[derive(Debug, Error)]
42#[bitwarden_error(flat)]
43pub(super) enum SyncError {
44    #[error("Network error during sync")]
45    Network,
46    #[error("Failed to parse sync data")]
47    Data,
48}
49
50/// The account keys data needed for key rotation, fetched from the key rotation data endpoint.
51pub(super) struct KeyRotationData {
52    pub(super) organization_memberships: Vec<V1OrganizationMembership>,
53    pub(super) emergency_access_memberships: Vec<V1EmergencyAccessMembership>,
54    pub(super) trusted_devices: Vec<PartialRotateableKeyset>,
55    pub(super) passkeys: Vec<PartialRotateableKeyset>,
56}
57
58/// Download the key rotation data from the server. This is the public keys for the
59/// password reset enrolled organizations and emergency-access grantees, and the encrypted keysets
60/// for the trusted devices and PRF-enabled passkeys. The server filters these to only the entries
61/// that participate in key rotation, so no client-side filtering is required.
62pub(super) async fn get_key_rotation_data(
63    api_client: &ApiClient,
64) -> Result<KeyRotationData, SyncError> {
65    let data = api_client
66        .accounts_key_management_api()
67        .get_key_rotation_data()
68        .await
69        .debug_map_err(SyncError::Network)?;
70
71    let organization_memberships = data
72        .organization_password_reset_key_data
73        .ok_or(SyncError::Data)?
74        .into_iter()
75        .map(|response| {
76            let _span = debug_span!("deserializing_organization_membership", organization_id = ?response.organization_id).entered();
77            V1OrganizationMembership::try_from(response).debug_map_err(SyncError::Data)
78        })
79        .collect::<Result<Vec<_>, _>>()?;
80
81    let emergency_access_memberships = data
82        .emergency_access_key_data
83        .ok_or(SyncError::Data)?
84        .into_iter()
85        .map(|response| {
86            let _span = debug_span!("deserializing_emergency_access_membership", emergency_access_id = ?response.id).entered();
87            V1EmergencyAccessMembership::try_from(response).debug_map_err(SyncError::Data)
88        })
89        .collect::<Result<Vec<_>, _>>()?;
90
91    let trusted_devices = data
92        .trusted_device_key_data
93        .ok_or(SyncError::Data)?
94        .into_iter()
95        .map(|response| {
96            let _span =
97                debug_span!("deserializing_trusted_device", device_id = ?response.id).entered();
98            PartialRotateableKeyset::try_from(response).debug_map_err(SyncError::Data)
99        })
100        .collect::<Result<Vec<_>, _>>()?;
101
102    let passkeys = data
103        .passkey_key_data
104        .ok_or(SyncError::Data)?
105        .into_iter()
106        .map(|response| {
107            let _span = debug_span!("deserializing_passkey", passkey_id = ?response.id).entered();
108            PartialRotateableKeyset::try_from(response).debug_map_err(SyncError::Data)
109        })
110        .collect::<Result<Vec<_>, _>>()?;
111
112    info!(
113        "Downloaded key rotation data: {} organizations, {} emergency access, {} devices, {} passkeys",
114        organization_memberships.len(),
115        emergency_access_memberships.len(),
116        trusted_devices.len(),
117        passkeys.len(),
118    );
119
120    Ok(KeyRotationData {
121        organization_memberships,
122        emergency_access_memberships,
123        trusted_devices,
124        passkeys,
125    })
126}
127
128fn parse_ciphers(
129    ciphers: Option<Vec<bitwarden_api_api::models::CipherDetailsResponseModel>>,
130) -> Result<Vec<Cipher>, SyncError> {
131    let ciphers = ciphers
132        .ok_or(SyncError::Data)?
133        .into_iter()
134        .filter(|c| c.organization_id.is_none())
135        .map(|c| {
136            let _span = debug_span!("deserializing_cipher", cipher_id = ?c.id).entered();
137            Cipher::try_from(c).debug_map_err(SyncError::Data)
138        })
139        .collect::<Result<Vec<_>, _>>()?;
140    info!("Deserialized {} ciphers", ciphers.len());
141    Ok(ciphers)
142}
143
144fn parse_folders(
145    folders: Option<Vec<bitwarden_api_api::models::FolderResponseModel>>,
146) -> Result<Vec<Folder>, SyncError> {
147    let folders = folders
148        .ok_or(SyncError::Data)?
149        .into_iter()
150        .map(|f| {
151            let _span = debug_span!("deserializing_folder", folder_id = ?f.id).entered();
152            Folder::try_from(f).debug_map_err(SyncError::Data)
153        })
154        .collect::<Result<Vec<_>, _>>()?;
155    info!("Deserialized {} folders", folders.len());
156    Ok(folders)
157}
158
159fn parse_sends(
160    sends: Option<Vec<bitwarden_api_api::models::SendResponseModel>>,
161) -> Result<Vec<bitwarden_send::Send>, SyncError> {
162    let sends = sends
163        .ok_or(SyncError::Data)?
164        .into_iter()
165        .map(|s| {
166            let _span = debug_span!("deserializing_send", send_id = ?s.id).entered();
167            bitwarden_send::Send::try_from(s).debug_map_err(SyncError::Data)
168        })
169        .collect::<Result<Vec<_>, _>>()?;
170    info!("Deserialized {} sends", sends.len());
171    Ok(sends)
172}
173
174fn from_kdf(
175    kdf: &bitwarden_api_api::models::MasterPasswordUnlockKdfResponseModel,
176) -> Result<Kdf, ()> {
177    Ok(match kdf.kdf_type {
178        bitwarden_api_api::models::KdfType::PBKDF2_SHA256 => Kdf::PBKDF2 {
179            iterations: std::num::NonZeroU32::new(kdf.iterations.try_into().debug_map_err(())?)
180                .ok_or(())?,
181        },
182        bitwarden_api_api::models::KdfType::Argon2id => {
183            let memory = kdf.memory.ok_or(())?;
184            let parallelism = kdf.parallelism.ok_or(())?;
185            Kdf::Argon2id {
186                iterations: std::num::NonZeroU32::new(kdf.iterations.try_into().debug_map_err(())?)
187                    .ok_or(())?,
188                memory: std::num::NonZeroU32::new(memory.try_into().debug_map_err(())?).ok_or(())?,
189                parallelism: std::num::NonZeroU32::new(parallelism.try_into().debug_map_err(())?)
190                    .ok_or(())?,
191            }
192        }
193        bitwarden_api_api::models::KdfType::__Unknown(_) => return Err(()),
194    })
195}
196
197/// Parses the user's KDF and salt from the sync response. If the user is not a master-password
198/// user, returns Ok(None)
199fn parse_kdf_and_salt(
200    user_decryption: &Option<Box<bitwarden_api_api::models::UserDecryptionResponseModel>>,
201) -> Result<Option<(Kdf, String)>, SyncError> {
202    let user_decryption_options = user_decryption.as_ref().ok_or(SyncError::Data)?;
203    if let Some(master_password_unlock) = &user_decryption_options.master_password_unlock {
204        let kdf = from_kdf(&master_password_unlock.clone().kdf).debug_map_err(SyncError::Data)?;
205        let salt = master_password_unlock.clone().salt.ok_or(SyncError::Data)?;
206        debug!("Parsed password KDF and salt from sync response");
207        Ok(Some((kdf, salt)))
208    } else {
209        debug!(
210            "User does not have master password decryption options, skipping KDF and salt parsing"
211        );
212        Ok(None)
213    }
214}
215
216pub(super) async fn sync_current_account_data(
217    api_client: &ApiClient,
218) -> Result<SyncedAccountData, SyncError> {
219    info!("Syncing latest vault state from server for key rotation");
220    let sync = api_client
221        .sync_api()
222        .get(Some(true))
223        .await
224        .debug_map_err(SyncError::Network)?;
225
226    let profile = sync.profile.as_ref().ok_or(SyncError::Data)?;
227    // This is optional for master-password-users!
228    let kdf_and_salt = parse_kdf_and_salt(&sync.user_decryption)?;
229    let account_cryptographic_state = profile.account_keys.to_owned().ok_or(SyncError::Data)?;
230    let ciphers = parse_ciphers(sync.ciphers)?;
231    let folders = parse_folders(sync.folders)?;
232    let sends = parse_sends(sync.sends)?;
233    let wrapped_account_cryptographic_state =
234        WrappedAccountCryptographicState::try_from(account_cryptographic_state.as_ref())
235            .debug_map_err(SyncError::Data)?;
236
237    // Get the key rotation data (organizations, emergency access, devices, passkeys) in a single
238    // request. The server filters these down to the entries that participate in key rotation.
239    info!("Syncing key rotation data (organizations, emergency access, devices, passkeys)");
240    let key_rotation_data = get_key_rotation_data(api_client).await?;
241
242    Ok(SyncedAccountData {
243        wrapped_account_cryptographic_state,
244        folders,
245        ciphers,
246        sends,
247        emergency_access_memberships: key_rotation_data.emergency_access_memberships,
248        organization_memberships: key_rotation_data.organization_memberships,
249        trusted_devices: key_rotation_data.trusted_devices,
250        passkeys: key_rotation_data.passkeys,
251        kdf_and_salt,
252    })
253}
254
255#[cfg(test)]
256mod tests {
257    use bitwarden_api_api::{
258        apis::ApiClient,
259        models::{
260            EmergencyAccessKeyDataResponseModel, FolderResponseModel, KdfType,
261            KeyRotationDataResponseModel, MasterPasswordUnlockKdfResponseModel,
262            MasterPasswordUnlockResponseModel, OrganizationPasswordResetKeyDataResponseModel,
263            PasskeyKeyDataResponseModel, PrivateKeysResponseModel, ProfileResponseModel,
264            PublicKeyEncryptionKeyPairResponseModel, SendResponseModel, SendType,
265            SyncResponseModel, TrustedDeviceKeyDataResponseModel, UserDecryptionResponseModel,
266        },
267    };
268    use bitwarden_crypto::{PublicKey, SpkiPublicKeyBytes};
269    use bitwarden_encoding::B64;
270    use bitwarden_send::SendId;
271    use bitwarden_vault::{CipherId, FolderId};
272
273    use super::*;
274
275    const TEST_ENC_STRING: &str = "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=";
276    const KEY_ENC_STRING: &str = "2.KLv/j0V4Ebs0dwyPdtt4vw==|Nczvv+DTkeP466cP/wMDnGK6W9zEIg5iHLhcuQG6s+M=|SZGsfuIAIaGZ7/kzygaVUau3LeOvJUlolENBOU+LX7g=";
277    const TEST_UNSIGNED_SHARED_KEY: &str = "4.AAAAAAAAAAAAAAAAAAAAAA==";
278
279    const TEST_RSA_PUBLIC_KEY_BYTES: &[u8] = &[
280        48, 130, 1, 34, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 130, 1, 15, 0,
281        48, 130, 1, 10, 2, 130, 1, 1, 0, 173, 4, 54, 63, 125, 12, 254, 38, 115, 34, 95, 164, 148,
282        115, 86, 140, 129, 74, 19, 70, 212, 212, 130, 163, 105, 249, 101, 120, 154, 46, 194, 250,
283        229, 242, 156, 67, 109, 179, 187, 134, 59, 235, 60, 107, 144, 163, 35, 22, 109, 230, 134,
284        243, 44, 243, 79, 84, 76, 11, 64, 56, 236, 167, 98, 26, 30, 213, 143, 105, 52, 92, 129, 92,
285        88, 22, 115, 135, 63, 215, 79, 8, 11, 183, 124, 10, 73, 231, 170, 110, 210, 178, 22, 100,
286        76, 75, 118, 202, 252, 204, 67, 204, 152, 6, 244, 208, 161, 146, 103, 225, 233, 239, 88,
287        195, 88, 150, 230, 111, 62, 142, 12, 157, 184, 155, 34, 84, 237, 111, 11, 97, 56, 152, 130,
288        14, 72, 123, 140, 47, 137, 5, 97, 166, 4, 147, 111, 23, 65, 78, 63, 208, 198, 50, 161, 39,
289        80, 143, 100, 194, 37, 252, 194, 53, 207, 166, 168, 250, 165, 121, 9, 207, 90, 36, 213,
290        211, 84, 255, 14, 205, 114, 135, 217, 137, 105, 232, 58, 169, 222, 10, 13, 138, 203, 16,
291        12, 122, 72, 227, 95, 160, 111, 54, 200, 198, 143, 156, 15, 143, 196, 50, 150, 204, 144,
292        255, 162, 248, 50, 28, 47, 66, 9, 83, 158, 67, 9, 50, 147, 174, 147, 200, 199, 238, 190,
293        248, 60, 114, 218, 32, 209, 120, 218, 17, 234, 14, 128, 192, 166, 33, 60, 73, 227, 108,
294        201, 41, 160, 81, 133, 171, 205, 221, 2, 3, 1, 0, 1,
295    ];
296
297    fn test_public_key_b64() -> String {
298        B64::from(TEST_RSA_PUBLIC_KEY_BYTES.to_vec()).to_string()
299    }
300
301    fn create_test_folder(id: uuid::Uuid) -> FolderResponseModel {
302        FolderResponseModel {
303            object: Some("folder".to_string()),
304            id: Some(id),
305            name: Some(TEST_ENC_STRING.to_string()),
306            revision_date: Some("2024-01-01T00:00:00Z".to_string()),
307        }
308    }
309
310    fn create_test_cipher(id: uuid::Uuid) -> bitwarden_api_api::models::CipherDetailsResponseModel {
311        bitwarden_api_api::models::CipherDetailsResponseModel {
312            object: Some("cipher".to_string()),
313            id: Some(id),
314            organization_id: None,
315            r#type: Some(bitwarden_api_api::models::CipherType::Login),
316            data: None,
317            name: Some(TEST_ENC_STRING.to_string()),
318            notes: None,
319            login: None,
320            card: None,
321            identity: None,
322            secure_note: None,
323            ssh_key: None,
324            bank_account: None,
325            drivers_license: None,
326            passport: None,
327            fields: None,
328            password_history: None,
329            attachments: None,
330            organization_use_totp: Some(false),
331            revision_date: Some("2024-01-01T00:00:00Z".to_string()),
332            creation_date: Some("2024-01-01T00:00:00Z".to_string()),
333            deleted_date: None,
334            reprompt: Some(bitwarden_api_api::models::CipherRepromptType::None),
335            key: None,
336            archived_date: None,
337            folder_id: None,
338            favorite: Some(false),
339            edit: Some(true),
340            view_password: Some(true),
341            permissions: None,
342            collection_ids: None,
343        }
344    }
345
346    fn create_test_send(id: uuid::Uuid) -> SendResponseModel {
347        SendResponseModel {
348            object: Some("send".to_string()),
349            id: Some(id),
350            access_id: Some("access_id".to_string()),
351            r#type: Some(SendType::Text),
352            name: Some(TEST_ENC_STRING.to_string()),
353            notes: None,
354            file: None,
355            text: None,
356            key: Some(KEY_ENC_STRING.to_string()),
357            max_access_count: None,
358            access_count: Some(0),
359            password: None,
360            disabled: Some(false),
361            revision_date: Some("2024-01-01T00:00:00Z".to_string()),
362            expiration_date: None,
363            deletion_date: Some("2024-12-31T00:00:00Z".to_string()),
364            hide_email: Some(false),
365            auth_type: None,
366            emails: None,
367        }
368    }
369
370    fn create_test_user_decryption() -> UserDecryptionResponseModel {
371        UserDecryptionResponseModel {
372            master_password_unlock: Some(Box::new(MasterPasswordUnlockResponseModel {
373                kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
374                    kdf_type: KdfType::PBKDF2_SHA256,
375                    iterations: 600000,
376                    memory: None,
377                    parallelism: None,
378                }),
379                master_key_encrypted_user_key: None,
380                salt: Some("test_salt".to_string()),
381            })),
382            web_authn_prf_options: None,
383            v2_upgrade_token: None,
384        }
385    }
386
387    fn create_test_profile(user_id: uuid::Uuid) -> ProfileResponseModel {
388        ProfileResponseModel {
389            id: Some(user_id),
390            account_keys: Some(Box::new(PrivateKeysResponseModel {
391                object: None,
392                signature_key_pair: None,
393                public_key_encryption_key_pair: Box::new(PublicKeyEncryptionKeyPairResponseModel {
394                    object: None,
395                    wrapped_private_key: Some(TEST_ENC_STRING.to_string()),
396                    public_key: None,
397                    signed_public_key: None,
398                }),
399                security_state: None,
400            })),
401            ..ProfileResponseModel::default()
402        }
403    }
404
405    fn create_test_sync_response(user_id: uuid::Uuid) -> SyncResponseModel {
406        SyncResponseModel {
407            object: Some("sync".to_string()),
408            profile: Some(Box::new(create_test_profile(user_id))),
409            folders: Some(vec![create_test_folder(uuid::Uuid::new_v4())]),
410            ciphers: Some(vec![create_test_cipher(uuid::Uuid::new_v4())]),
411            sends: Some(vec![create_test_send(uuid::Uuid::new_v4())]),
412            user_decryption: Some(Box::new(create_test_user_decryption())),
413            ..Default::default()
414        }
415    }
416
417    fn create_test_key_rotation_data_response(
418        org_id: uuid::Uuid,
419        ea_id: uuid::Uuid,
420        grantee_id: uuid::Uuid,
421        device_id: uuid::Uuid,
422        passkey_id: uuid::Uuid,
423    ) -> KeyRotationDataResponseModel {
424        KeyRotationDataResponseModel {
425            object: Some("keyRotationData".to_string()),
426            organization_password_reset_key_data: Some(vec![
427                OrganizationPasswordResetKeyDataResponseModel {
428                    object: Some("organizationPasswordResetKeyData".to_string()),
429                    organization_id: Some(org_id),
430                    organization_name: Some("Test Org".to_string()),
431                    organization_public_key: Some(test_public_key_b64()),
432                },
433            ]),
434            emergency_access_key_data: Some(vec![EmergencyAccessKeyDataResponseModel {
435                object: Some("emergencyAccessKeyData".to_string()),
436                id: Some(ea_id),
437                grantee_id: Some(grantee_id),
438                grantee_name: Some("Emergency Contact".to_string()),
439                grantee_email: Some("[email protected]".to_string()),
440                public_key: Some(test_public_key_b64()),
441            }]),
442            trusted_device_key_data: Some(vec![TrustedDeviceKeyDataResponseModel {
443                object: Some("trustedDeviceKeyData".to_string()),
444                id: Some(device_id),
445                encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
446                encrypted_user_key: Some(TEST_UNSIGNED_SHARED_KEY.to_string()),
447            }]),
448            passkey_key_data: Some(vec![PasskeyKeyDataResponseModel {
449                object: Some("passkeyKeyData".to_string()),
450                id: Some(passkey_id),
451                encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
452                encrypted_user_key: Some(TEST_UNSIGNED_SHARED_KEY.to_string()),
453            }]),
454        }
455    }
456
457    #[tokio::test]
458    async fn test_get_key_rotation_data_success() {
459        let org_id = uuid::Uuid::new_v4();
460        let ea_id = uuid::Uuid::new_v4();
461        let grantee_id = uuid::Uuid::new_v4();
462        let device_id = uuid::Uuid::new_v4();
463        let passkey_id = uuid::Uuid::new_v4();
464
465        let api_client = ApiClient::new_mocked(|mock| {
466            mock.accounts_key_management_api
467                .expect_get_key_rotation_data()
468                .once()
469                .returning(move || {
470                    Ok(create_test_key_rotation_data_response(
471                        org_id, ea_id, grantee_id, device_id, passkey_id,
472                    ))
473                });
474        });
475
476        let data = get_key_rotation_data(&api_client).await.unwrap();
477
478        assert_eq!(data.organization_memberships.len(), 1);
479        assert_eq!(data.organization_memberships[0].organization_id, org_id);
480        assert_eq!(data.organization_memberships[0].name, "Test Org");
481
482        assert_eq!(data.emergency_access_memberships.len(), 1);
483        assert_eq!(data.emergency_access_memberships[0].id, ea_id);
484        assert_eq!(data.emergency_access_memberships[0].grantee_id, grantee_id);
485        assert_eq!(
486            data.emergency_access_memberships[0].name,
487            "Emergency Contact"
488        );
489
490        assert_eq!(data.trusted_devices.len(), 1);
491        assert_eq!(data.trusted_devices[0].id, device_id);
492        assert_eq!(
493            data.trusted_devices[0].encrypted_public_key.to_string(),
494            TEST_ENC_STRING
495        );
496        assert_eq!(
497            data.trusted_devices[0].encrypted_user_key.to_string(),
498            TEST_UNSIGNED_SHARED_KEY
499        );
500
501        assert_eq!(data.passkeys.len(), 1);
502        assert_eq!(data.passkeys[0].id, passkey_id);
503        assert_eq!(
504            data.passkeys[0].encrypted_public_key.to_string(),
505            TEST_ENC_STRING
506        );
507        assert_eq!(
508            data.passkeys[0].encrypted_user_key.to_string(),
509            TEST_UNSIGNED_SHARED_KEY
510        );
511
512        let expected_public_key = PublicKey::from_der(&SpkiPublicKeyBytes::from(
513            TEST_RSA_PUBLIC_KEY_BYTES.to_vec(),
514        ))
515        .unwrap();
516        assert_eq!(
517            data.organization_memberships[0]
518                .public_key
519                .to_der()
520                .unwrap(),
521            expected_public_key.to_der().unwrap()
522        );
523        assert_eq!(
524            data.emergency_access_memberships[0]
525                .public_key
526                .to_der()
527                .unwrap(),
528            expected_public_key.to_der().unwrap()
529        );
530
531        if let ApiClient::Mock(mut mock) = api_client {
532            mock.accounts_key_management_api.checkpoint();
533        }
534    }
535
536    #[tokio::test]
537    async fn test_get_key_rotation_data_network_error() {
538        let api_client = ApiClient::new_mocked(|mock| {
539            mock.accounts_key_management_api
540                .expect_get_key_rotation_data()
541                .once()
542                .returning(move || {
543                    Err(serde_json::Error::io(std::io::Error::other("Network error")).into())
544                });
545        });
546
547        let result = get_key_rotation_data(&api_client).await;
548        assert!(matches!(result, Err(SyncError::Network)));
549
550        if let ApiClient::Mock(mut mock) = api_client {
551            mock.accounts_key_management_api.checkpoint();
552        }
553    }
554
555    #[tokio::test]
556    async fn test_get_key_rotation_data_empty_arrays_returns_empty_data() {
557        let api_client = ApiClient::new_mocked(|mock| {
558            mock.accounts_key_management_api
559                .expect_get_key_rotation_data()
560                .once()
561                .returning(move || {
562                    // The server returns the entity arrays as present but empty when the user
563                    // has no entries that participate in key rotation.
564                    Ok(KeyRotationDataResponseModel {
565                        object: Some("keyRotationData".to_string()),
566                        organization_password_reset_key_data: Some(vec![]),
567                        emergency_access_key_data: Some(vec![]),
568                        trusted_device_key_data: Some(vec![]),
569                        passkey_key_data: Some(vec![]),
570                    })
571                });
572        });
573
574        let data = get_key_rotation_data(&api_client).await.unwrap();
575
576        assert!(data.organization_memberships.is_empty());
577        assert!(data.emergency_access_memberships.is_empty());
578        assert!(data.trusted_devices.is_empty());
579        assert!(data.passkeys.is_empty());
580
581        if let ApiClient::Mock(mut mock) = api_client {
582            mock.accounts_key_management_api.checkpoint();
583        }
584    }
585
586    #[tokio::test]
587    async fn test_get_key_rotation_data_missing_field_is_data_error() {
588        let device_id = uuid::Uuid::new_v4();
589        let api_client = ApiClient::new_mocked(|mock| {
590            mock.accounts_key_management_api
591                .expect_get_key_rotation_data()
592                .once()
593                .returning(move || {
594                    Ok(KeyRotationDataResponseModel {
595                        object: Some("keyRotationData".to_string()),
596                        organization_password_reset_key_data: Some(vec![]),
597                        emergency_access_key_data: Some(vec![]),
598                        trusted_device_key_data: Some(vec![TrustedDeviceKeyDataResponseModel {
599                            object: Some("trustedDeviceKeyData".to_string()),
600                            id: Some(device_id),
601                            encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
602                            // The required encrypted user key is missing.
603                            encrypted_user_key: None,
604                        }]),
605                        passkey_key_data: Some(vec![]),
606                    })
607                });
608        });
609
610        let result = get_key_rotation_data(&api_client).await;
611        assert!(matches!(result, Err(SyncError::Data)));
612
613        if let ApiClient::Mock(mut mock) = api_client {
614            mock.accounts_key_management_api.checkpoint();
615        }
616    }
617
618    #[tokio::test]
619    async fn test_get_key_rotation_data_emergency_access_name_fallback() {
620        let ea_id_email = uuid::Uuid::new_v4();
621        let ea_id_unknown = uuid::Uuid::new_v4();
622        let grantee_id = uuid::Uuid::new_v4();
623
624        let api_client = ApiClient::new_mocked(|mock| {
625            mock.accounts_key_management_api
626                .expect_get_key_rotation_data()
627                .once()
628                .returning(move || {
629                    Ok(KeyRotationDataResponseModel {
630                        object: Some("keyRotationData".to_string()),
631                        organization_password_reset_key_data: Some(vec![]),
632                        emergency_access_key_data: Some(vec![
633                            EmergencyAccessKeyDataResponseModel {
634                                object: Some("emergencyAccessKeyData".to_string()),
635                                id: Some(ea_id_email),
636                                grantee_id: Some(grantee_id),
637                                // No name set, so the email is used as the display name.
638                                grantee_name: None,
639                                grantee_email: Some("[email protected]".to_string()),
640                                public_key: Some(test_public_key_b64()),
641                            },
642                            EmergencyAccessKeyDataResponseModel {
643                                object: Some("emergencyAccessKeyData".to_string()),
644                                id: Some(ea_id_unknown),
645                                grantee_id: Some(grantee_id),
646                                // Neither name nor email is set, so "Unknown" is used.
647                                grantee_name: None,
648                                grantee_email: None,
649                                public_key: Some(test_public_key_b64()),
650                            },
651                        ]),
652                        trusted_device_key_data: Some(vec![]),
653                        passkey_key_data: Some(vec![]),
654                    })
655                });
656        });
657
658        let data = get_key_rotation_data(&api_client).await.unwrap();
659        assert_eq!(data.emergency_access_memberships.len(), 2);
660        assert_eq!(
661            data.emergency_access_memberships[0].name,
662            "[email protected]"
663        );
664        assert_eq!(data.emergency_access_memberships[1].name, "Unknown");
665
666        if let ApiClient::Mock(mut mock) = api_client {
667            mock.accounts_key_management_api.checkpoint();
668        }
669    }
670
671    #[tokio::test]
672    async fn test_sync_current_account_data_success() {
673        let user_id = uuid::Uuid::new_v4();
674        let org_id = uuid::Uuid::new_v4();
675        let ea_id = uuid::Uuid::new_v4();
676        let grantee_id = uuid::Uuid::new_v4();
677        let device_id = uuid::Uuid::new_v4();
678        let passkey_id = uuid::Uuid::new_v4();
679        let folder_id = uuid::Uuid::new_v4();
680        let cipher_id = uuid::Uuid::new_v4();
681        let send_id = uuid::Uuid::new_v4();
682
683        let api_client = ApiClient::new_mocked(|mock| {
684            mock.sync_api
685                .expect_get()
686                .once()
687                .returning(move |_exclude_domains| {
688                    let mut response = create_test_sync_response(user_id);
689                    response.folders = Some(vec![create_test_folder(folder_id)]);
690                    response.ciphers = Some(vec![create_test_cipher(cipher_id)]);
691                    response.sends = Some(vec![create_test_send(send_id)]);
692                    Ok(response)
693                });
694            mock.accounts_key_management_api
695                .expect_get_key_rotation_data()
696                .once()
697                .returning(move || {
698                    Ok(create_test_key_rotation_data_response(
699                        org_id, ea_id, grantee_id, device_id, passkey_id,
700                    ))
701                });
702        });
703
704        let result = sync_current_account_data(&api_client).await;
705        let data = result.unwrap();
706
707        // Verify folders
708        assert_eq!(data.folders.len(), 1);
709        assert_eq!(data.folders[0].id, Some(FolderId::new(folder_id)));
710        assert_eq!(data.folders[0].name, TEST_ENC_STRING.parse().unwrap());
711
712        // Verify ciphers
713        assert_eq!(data.ciphers.len(), 1);
714        assert_eq!(data.ciphers[0].id, Some(CipherId::new(cipher_id)));
715        assert_eq!(data.ciphers[0].name, Some(TEST_ENC_STRING.parse().unwrap()));
716
717        // Verify sends
718        assert_eq!(data.sends.len(), 1);
719        assert_eq!(data.sends[0].id, Some(SendId::new(send_id)));
720        assert_eq!(data.sends[0].name, TEST_ENC_STRING.parse().unwrap());
721        assert_eq!(data.sends[0].key, KEY_ENC_STRING.parse().unwrap());
722
723        assert_eq!(data.organization_memberships.len(), 1);
724        assert_eq!(data.organization_memberships[0].organization_id, org_id);
725        assert_eq!(data.emergency_access_memberships.len(), 1);
726        assert_eq!(data.emergency_access_memberships[0].id, ea_id);
727        assert_eq!(data.trusted_devices.len(), 1);
728        assert_eq!(data.trusted_devices[0].id, device_id);
729        assert_eq!(data.passkeys.len(), 1);
730        assert_eq!(data.passkeys[0].id, passkey_id);
731        assert!(data.kdf_and_salt.is_some());
732        let (kdf, salt) = data.kdf_and_salt.unwrap();
733        assert_eq!(salt, "test_salt");
734        assert!(matches!(kdf, Kdf::PBKDF2 { iterations } if iterations.get() == 600000));
735        assert!(matches!(
736            data.wrapped_account_cryptographic_state,
737            WrappedAccountCryptographicState::V1 { .. }
738        ));
739
740        if let ApiClient::Mock(mut mock) = api_client {
741            mock.sync_api.checkpoint();
742            mock.accounts_key_management_api.checkpoint();
743        }
744    }
745
746    #[tokio::test]
747    async fn test_sync_current_account_data_network_error() {
748        let api_client = ApiClient::new_mocked(|mock| {
749            mock.sync_api
750                .expect_get()
751                .once()
752                .returning(move |_exclude_domains| {
753                    Err(serde_json::Error::io(std::io::Error::other("API error")).into())
754                });
755            mock.accounts_key_management_api
756                .expect_get_key_rotation_data()
757                .never();
758        });
759
760        let result = sync_current_account_data(&api_client).await;
761
762        assert!(matches!(result, Err(SyncError::Network)));
763
764        if let ApiClient::Mock(mut mock) = api_client {
765            mock.sync_api.checkpoint();
766            mock.accounts_key_management_api.checkpoint();
767        }
768    }
769
770    #[test]
771    fn test_parse_ciphers_filters_organization_ciphers() {
772        let personal_cipher_id = uuid::Uuid::new_v4();
773        let organization_cipher_id = uuid::Uuid::new_v4();
774
775        let personal_cipher = create_test_cipher(personal_cipher_id);
776        let mut organization_cipher = create_test_cipher(organization_cipher_id);
777        organization_cipher.organization_id = Some(uuid::Uuid::new_v4());
778
779        let ciphers = parse_ciphers(Some(vec![personal_cipher, organization_cipher])).unwrap();
780
781        assert_eq!(ciphers.len(), 1);
782        assert_eq!(ciphers[0].id, Some(CipherId::new(personal_cipher_id)));
783    }
784}