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