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            data: None,
357            key: Some(KEY_ENC_STRING.to_string()),
358            max_access_count: None,
359            access_count: Some(0),
360            password: None,
361            disabled: Some(false),
362            revision_date: Some("2024-01-01T00:00:00Z".to_string()),
363            expiration_date: None,
364            deletion_date: Some("2024-12-31T00:00:00Z".to_string()),
365            hide_email: Some(false),
366            auth_type: None,
367            emails: None,
368        }
369    }
370
371    fn create_test_user_decryption() -> UserDecryptionResponseModel {
372        UserDecryptionResponseModel {
373            master_password_unlock: Some(Box::new(MasterPasswordUnlockResponseModel {
374                kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
375                    kdf_type: KdfType::PBKDF2_SHA256,
376                    iterations: 600000,
377                    memory: None,
378                    parallelism: None,
379                }),
380                master_key_encrypted_user_key: None,
381                salt: Some("test_salt".to_string()),
382            })),
383            web_authn_prf_options: None,
384            v2_upgrade_token: None,
385        }
386    }
387
388    fn create_test_profile(user_id: uuid::Uuid) -> ProfileResponseModel {
389        ProfileResponseModel {
390            id: Some(user_id),
391            account_keys: Some(Box::new(PrivateKeysResponseModel {
392                object: None,
393                signature_key_pair: None,
394                public_key_encryption_key_pair: Box::new(PublicKeyEncryptionKeyPairResponseModel {
395                    object: None,
396                    wrapped_private_key: Some(TEST_ENC_STRING.to_string()),
397                    public_key: None,
398                    signed_public_key: None,
399                }),
400                security_state: None,
401            })),
402            ..ProfileResponseModel::default()
403        }
404    }
405
406    fn create_test_sync_response(user_id: uuid::Uuid) -> SyncResponseModel {
407        SyncResponseModel {
408            object: Some("sync".to_string()),
409            profile: Some(Box::new(create_test_profile(user_id))),
410            folders: Some(vec![create_test_folder(uuid::Uuid::new_v4())]),
411            ciphers: Some(vec![create_test_cipher(uuid::Uuid::new_v4())]),
412            sends: Some(vec![create_test_send(uuid::Uuid::new_v4())]),
413            user_decryption: Some(Box::new(create_test_user_decryption())),
414            ..Default::default()
415        }
416    }
417
418    fn create_test_key_rotation_data_response(
419        org_id: uuid::Uuid,
420        ea_id: uuid::Uuid,
421        grantee_id: uuid::Uuid,
422        device_id: uuid::Uuid,
423        passkey_id: uuid::Uuid,
424    ) -> KeyRotationDataResponseModel {
425        KeyRotationDataResponseModel {
426            object: Some("keyRotationData".to_string()),
427            organization_password_reset_key_data: Some(vec![
428                OrganizationPasswordResetKeyDataResponseModel {
429                    object: Some("organizationPasswordResetKeyData".to_string()),
430                    organization_id: Some(org_id),
431                    organization_name: Some("Test Org".to_string()),
432                    organization_public_key: Some(test_public_key_b64()),
433                },
434            ]),
435            emergency_access_key_data: Some(vec![EmergencyAccessKeyDataResponseModel {
436                object: Some("emergencyAccessKeyData".to_string()),
437                id: Some(ea_id),
438                grantee_id: Some(grantee_id),
439                grantee_name: Some("Emergency Contact".to_string()),
440                grantee_email: Some("[email protected]".to_string()),
441                public_key: Some(test_public_key_b64()),
442            }]),
443            trusted_device_key_data: Some(vec![TrustedDeviceKeyDataResponseModel {
444                object: Some("trustedDeviceKeyData".to_string()),
445                id: Some(device_id),
446                encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
447                encrypted_user_key: Some(TEST_UNSIGNED_SHARED_KEY.to_string()),
448            }]),
449            passkey_key_data: Some(vec![PasskeyKeyDataResponseModel {
450                object: Some("passkeyKeyData".to_string()),
451                id: Some(passkey_id),
452                encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
453                encrypted_user_key: Some(TEST_UNSIGNED_SHARED_KEY.to_string()),
454            }]),
455        }
456    }
457
458    #[tokio::test]
459    async fn test_get_key_rotation_data_success() {
460        let org_id = uuid::Uuid::new_v4();
461        let ea_id = uuid::Uuid::new_v4();
462        let grantee_id = uuid::Uuid::new_v4();
463        let device_id = uuid::Uuid::new_v4();
464        let passkey_id = uuid::Uuid::new_v4();
465
466        let api_client = ApiClient::new_mocked(|mock| {
467            mock.accounts_key_management_api
468                .expect_get_key_rotation_data()
469                .once()
470                .returning(move || {
471                    Ok(create_test_key_rotation_data_response(
472                        org_id, ea_id, grantee_id, device_id, passkey_id,
473                    ))
474                });
475        });
476
477        let data = get_key_rotation_data(&api_client).await.unwrap();
478
479        assert_eq!(data.organization_memberships.len(), 1);
480        assert_eq!(data.organization_memberships[0].organization_id, org_id);
481        assert_eq!(data.organization_memberships[0].name, "Test Org");
482
483        assert_eq!(data.emergency_access_memberships.len(), 1);
484        assert_eq!(data.emergency_access_memberships[0].id, ea_id);
485        assert_eq!(data.emergency_access_memberships[0].grantee_id, grantee_id);
486        assert_eq!(
487            data.emergency_access_memberships[0].name,
488            "Emergency Contact"
489        );
490
491        assert_eq!(data.trusted_devices.len(), 1);
492        assert_eq!(data.trusted_devices[0].id, device_id);
493        assert_eq!(
494            data.trusted_devices[0].encrypted_public_key.to_string(),
495            TEST_ENC_STRING
496        );
497        assert_eq!(
498            data.trusted_devices[0].encrypted_user_key.to_string(),
499            TEST_UNSIGNED_SHARED_KEY
500        );
501
502        assert_eq!(data.passkeys.len(), 1);
503        assert_eq!(data.passkeys[0].id, passkey_id);
504        assert_eq!(
505            data.passkeys[0].encrypted_public_key.to_string(),
506            TEST_ENC_STRING
507        );
508        assert_eq!(
509            data.passkeys[0].encrypted_user_key.to_string(),
510            TEST_UNSIGNED_SHARED_KEY
511        );
512
513        let expected_public_key = PublicKey::from_der(&SpkiPublicKeyBytes::from(
514            TEST_RSA_PUBLIC_KEY_BYTES.to_vec(),
515        ))
516        .unwrap();
517        assert_eq!(
518            data.organization_memberships[0]
519                .public_key
520                .to_der()
521                .unwrap(),
522            expected_public_key.to_der().unwrap()
523        );
524        assert_eq!(
525            data.emergency_access_memberships[0]
526                .public_key
527                .to_der()
528                .unwrap(),
529            expected_public_key.to_der().unwrap()
530        );
531
532        if let ApiClient::Mock(mut mock) = api_client {
533            mock.accounts_key_management_api.checkpoint();
534        }
535    }
536
537    #[tokio::test]
538    async fn test_get_key_rotation_data_network_error() {
539        let api_client = ApiClient::new_mocked(|mock| {
540            mock.accounts_key_management_api
541                .expect_get_key_rotation_data()
542                .once()
543                .returning(move || {
544                    Err(serde_json::Error::io(std::io::Error::other("Network error")).into())
545                });
546        });
547
548        let result = get_key_rotation_data(&api_client).await;
549        assert!(matches!(result, Err(SyncError::Network)));
550
551        if let ApiClient::Mock(mut mock) = api_client {
552            mock.accounts_key_management_api.checkpoint();
553        }
554    }
555
556    #[tokio::test]
557    async fn test_get_key_rotation_data_empty_arrays_returns_empty_data() {
558        let api_client = ApiClient::new_mocked(|mock| {
559            mock.accounts_key_management_api
560                .expect_get_key_rotation_data()
561                .once()
562                .returning(move || {
563                    // The server returns the entity arrays as present but empty when the user
564                    // has no entries that participate in key rotation.
565                    Ok(KeyRotationDataResponseModel {
566                        object: Some("keyRotationData".to_string()),
567                        organization_password_reset_key_data: Some(vec![]),
568                        emergency_access_key_data: Some(vec![]),
569                        trusted_device_key_data: Some(vec![]),
570                        passkey_key_data: Some(vec![]),
571                    })
572                });
573        });
574
575        let data = get_key_rotation_data(&api_client).await.unwrap();
576
577        assert!(data.organization_memberships.is_empty());
578        assert!(data.emergency_access_memberships.is_empty());
579        assert!(data.trusted_devices.is_empty());
580        assert!(data.passkeys.is_empty());
581
582        if let ApiClient::Mock(mut mock) = api_client {
583            mock.accounts_key_management_api.checkpoint();
584        }
585    }
586
587    #[tokio::test]
588    async fn test_get_key_rotation_data_missing_field_is_data_error() {
589        let device_id = uuid::Uuid::new_v4();
590        let api_client = ApiClient::new_mocked(|mock| {
591            mock.accounts_key_management_api
592                .expect_get_key_rotation_data()
593                .once()
594                .returning(move || {
595                    Ok(KeyRotationDataResponseModel {
596                        object: Some("keyRotationData".to_string()),
597                        organization_password_reset_key_data: Some(vec![]),
598                        emergency_access_key_data: Some(vec![]),
599                        trusted_device_key_data: Some(vec![TrustedDeviceKeyDataResponseModel {
600                            object: Some("trustedDeviceKeyData".to_string()),
601                            id: Some(device_id),
602                            encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
603                            // The required encrypted user key is missing.
604                            encrypted_user_key: None,
605                        }]),
606                        passkey_key_data: Some(vec![]),
607                    })
608                });
609        });
610
611        let result = get_key_rotation_data(&api_client).await;
612        assert!(matches!(result, Err(SyncError::Data)));
613
614        if let ApiClient::Mock(mut mock) = api_client {
615            mock.accounts_key_management_api.checkpoint();
616        }
617    }
618
619    #[tokio::test]
620    async fn test_get_key_rotation_data_emergency_access_name_fallback() {
621        let ea_id_email = uuid::Uuid::new_v4();
622        let ea_id_unknown = uuid::Uuid::new_v4();
623        let grantee_id = uuid::Uuid::new_v4();
624
625        let api_client = ApiClient::new_mocked(|mock| {
626            mock.accounts_key_management_api
627                .expect_get_key_rotation_data()
628                .once()
629                .returning(move || {
630                    Ok(KeyRotationDataResponseModel {
631                        object: Some("keyRotationData".to_string()),
632                        organization_password_reset_key_data: Some(vec![]),
633                        emergency_access_key_data: Some(vec![
634                            EmergencyAccessKeyDataResponseModel {
635                                object: Some("emergencyAccessKeyData".to_string()),
636                                id: Some(ea_id_email),
637                                grantee_id: Some(grantee_id),
638                                // No name set, so the email is used as the display name.
639                                grantee_name: None,
640                                grantee_email: Some("[email protected]".to_string()),
641                                public_key: Some(test_public_key_b64()),
642                            },
643                            EmergencyAccessKeyDataResponseModel {
644                                object: Some("emergencyAccessKeyData".to_string()),
645                                id: Some(ea_id_unknown),
646                                grantee_id: Some(grantee_id),
647                                // Neither name nor email is set, so "Unknown" is used.
648                                grantee_name: None,
649                                grantee_email: None,
650                                public_key: Some(test_public_key_b64()),
651                            },
652                        ]),
653                        trusted_device_key_data: Some(vec![]),
654                        passkey_key_data: Some(vec![]),
655                    })
656                });
657        });
658
659        let data = get_key_rotation_data(&api_client).await.unwrap();
660        assert_eq!(data.emergency_access_memberships.len(), 2);
661        assert_eq!(
662            data.emergency_access_memberships[0].name,
663            "[email protected]"
664        );
665        assert_eq!(data.emergency_access_memberships[1].name, "Unknown");
666
667        if let ApiClient::Mock(mut mock) = api_client {
668            mock.accounts_key_management_api.checkpoint();
669        }
670    }
671
672    #[tokio::test]
673    async fn test_sync_current_account_data_success() {
674        let user_id = uuid::Uuid::new_v4();
675        let org_id = uuid::Uuid::new_v4();
676        let ea_id = uuid::Uuid::new_v4();
677        let grantee_id = uuid::Uuid::new_v4();
678        let device_id = uuid::Uuid::new_v4();
679        let passkey_id = uuid::Uuid::new_v4();
680        let folder_id = uuid::Uuid::new_v4();
681        let cipher_id = uuid::Uuid::new_v4();
682        let send_id = uuid::Uuid::new_v4();
683
684        let api_client = ApiClient::new_mocked(|mock| {
685            mock.sync_api
686                .expect_get()
687                .once()
688                .returning(move |_exclude_domains| {
689                    let mut response = create_test_sync_response(user_id);
690                    response.folders = Some(vec![create_test_folder(folder_id)]);
691                    response.ciphers = Some(vec![create_test_cipher(cipher_id)]);
692                    response.sends = Some(vec![create_test_send(send_id)]);
693                    Ok(response)
694                });
695            mock.accounts_key_management_api
696                .expect_get_key_rotation_data()
697                .once()
698                .returning(move || {
699                    Ok(create_test_key_rotation_data_response(
700                        org_id, ea_id, grantee_id, device_id, passkey_id,
701                    ))
702                });
703        });
704
705        let result = sync_current_account_data(&api_client).await;
706        let data = result.unwrap();
707
708        // Verify folders
709        assert_eq!(data.folders.len(), 1);
710        assert_eq!(data.folders[0].id, Some(FolderId::new(folder_id)));
711        assert_eq!(data.folders[0].name, TEST_ENC_STRING.parse().unwrap());
712
713        // Verify ciphers
714        assert_eq!(data.ciphers.len(), 1);
715        assert_eq!(data.ciphers[0].id, Some(CipherId::new(cipher_id)));
716        assert_eq!(data.ciphers[0].name, Some(TEST_ENC_STRING.parse().unwrap()));
717
718        // Verify sends
719        assert_eq!(data.sends.len(), 1);
720        assert_eq!(data.sends[0].id, Some(SendId::new(send_id)));
721        assert_eq!(data.sends[0].name, TEST_ENC_STRING.parse().unwrap());
722        assert_eq!(data.sends[0].key, KEY_ENC_STRING.parse().unwrap());
723
724        assert_eq!(data.organization_memberships.len(), 1);
725        assert_eq!(data.organization_memberships[0].organization_id, org_id);
726        assert_eq!(data.emergency_access_memberships.len(), 1);
727        assert_eq!(data.emergency_access_memberships[0].id, ea_id);
728        assert_eq!(data.trusted_devices.len(), 1);
729        assert_eq!(data.trusted_devices[0].id, device_id);
730        assert_eq!(data.passkeys.len(), 1);
731        assert_eq!(data.passkeys[0].id, passkey_id);
732        assert!(data.kdf_and_salt.is_some());
733        let (kdf, salt) = data.kdf_and_salt.unwrap();
734        assert_eq!(salt, "test_salt");
735        assert!(matches!(kdf, Kdf::PBKDF2 { iterations } if iterations.get() == 600000));
736        assert!(matches!(
737            data.wrapped_account_cryptographic_state,
738            WrappedAccountCryptographicState::V1 { .. }
739        ));
740
741        if let ApiClient::Mock(mut mock) = api_client {
742            mock.sync_api.checkpoint();
743            mock.accounts_key_management_api.checkpoint();
744        }
745    }
746
747    #[tokio::test]
748    async fn test_sync_current_account_data_network_error() {
749        let api_client = ApiClient::new_mocked(|mock| {
750            mock.sync_api
751                .expect_get()
752                .once()
753                .returning(move |_exclude_domains| {
754                    Err(serde_json::Error::io(std::io::Error::other("API error")).into())
755                });
756            mock.accounts_key_management_api
757                .expect_get_key_rotation_data()
758                .never();
759        });
760
761        let result = sync_current_account_data(&api_client).await;
762
763        assert!(matches!(result, Err(SyncError::Network)));
764
765        if let ApiClient::Mock(mut mock) = api_client {
766            mock.sync_api.checkpoint();
767            mock.accounts_key_management_api.checkpoint();
768        }
769    }
770
771    #[test]
772    fn test_parse_ciphers_filters_organization_ciphers() {
773        let personal_cipher_id = uuid::Uuid::new_v4();
774        let organization_cipher_id = uuid::Uuid::new_v4();
775
776        let personal_cipher = create_test_cipher(personal_cipher_id);
777        let mut organization_cipher = create_test_cipher(organization_cipher_id);
778        organization_cipher.organization_id = Some(uuid::Uuid::new_v4());
779
780        let ciphers = parse_ciphers(Some(vec![personal_cipher, organization_cipher])).unwrap();
781
782        assert_eq!(ciphers.len(), 1);
783        assert_eq!(ciphers[0].id, Some(CipherId::new(personal_cipher_id)));
784    }
785}