Skip to main content

bw/vault/
sync.rs

1use bitwarden_api_api::models::{
2    DomainsResponseModel, ProfileOrganizationResponseModel, ProfileResponseModel, SyncResponseModel,
3};
4use bitwarden_collections::{collection::Collection, error::CollectionsParseError};
5use bitwarden_core::{
6    Client, MissingFieldError, NotAuthenticatedError, OrganizationId, UserId,
7    client::encryption_settings::EncryptionSettingsError,
8    key_management::{MasterPasswordError, UserDecryptionData},
9    require,
10};
11use bitwarden_vault::{Cipher, Folder, GlobalDomains, VaultParseError};
12use serde::{Deserialize, Serialize};
13use thiserror::Error;
14
15#[derive(Debug, Error)]
16pub enum SyncError {
17    #[error(transparent)]
18    Api(#[from] bitwarden_core::ApiError),
19    #[error(transparent)]
20    MissingField(#[from] MissingFieldError),
21    #[error(transparent)]
22    VaultParse(#[from] VaultParseError),
23    #[error(transparent)]
24    CollectionParse(#[from] CollectionsParseError),
25    #[error(transparent)]
26    EncryptionSettings(#[from] EncryptionSettingsError),
27    #[error(transparent)]
28    MasterPassword(#[from] MasterPasswordError),
29    #[error(transparent)]
30    NotAuthenticated(#[from] NotAuthenticatedError),
31}
32
33#[allow(missing_docs)]
34#[derive(Serialize, Deserialize, Debug)]
35#[serde(rename_all = "camelCase", deny_unknown_fields)]
36pub struct SyncRequest {
37    /// Exclude the subdomains from the response, defaults to false
38    pub exclude_subdomains: Option<bool>,
39}
40
41pub(crate) async fn sync(client: &Client, input: &SyncRequest) -> Result<SyncResponse, SyncError> {
42    let config = client.internal.get_api_configurations().await;
43    let sync = config
44        .api_client
45        .sync_api()
46        .get(input.exclude_subdomains)
47        .await
48        .map_err(|e| SyncError::Api(e.into()))?;
49
50    let master_password_unlock = sync
51        .user_decryption
52        .as_deref()
53        .map(UserDecryptionData::try_from)
54        .transpose()?
55        .and_then(|user_decryption| user_decryption.master_password_unlock);
56    if let Some(master_password_unlock) = master_password_unlock {
57        client
58            .internal
59            .set_user_master_password_unlock(master_password_unlock)?;
60    }
61
62    let org_keys: Vec<_> = require!(sync.profile.as_ref())
63        .organizations
64        .as_deref()
65        .unwrap_or_default()
66        .iter()
67        .filter_map(|o| o.id.zip(o.key.as_deref().and_then(|k| k.parse().ok())))
68        .map(|(id, key)| (OrganizationId::new(id), key))
69        .collect();
70
71    client.internal.initialize_org_crypto(org_keys)?;
72
73    SyncResponse::process_response(sync)
74}
75
76#[derive(Serialize, Deserialize, Debug)]
77#[serde(rename_all = "camelCase", deny_unknown_fields)]
78pub struct ProfileResponse {
79    pub id: UserId,
80    pub name: String,
81    pub email: String,
82
83    //key: String,
84    //private_key: String,
85    pub organizations: Vec<ProfileOrganizationResponse>,
86}
87
88#[derive(Serialize, Deserialize, Debug)]
89#[serde(rename_all = "camelCase", deny_unknown_fields)]
90pub struct ProfileOrganizationResponse {
91    pub id: OrganizationId,
92}
93
94#[derive(Serialize, Deserialize, Debug)]
95#[serde(rename_all = "camelCase", deny_unknown_fields)]
96pub struct DomainResponse {
97    pub equivalent_domains: Vec<Vec<String>>,
98    pub global_equivalent_domains: Vec<GlobalDomains>,
99}
100
101#[allow(missing_docs)]
102#[derive(Serialize, Deserialize, Debug)]
103#[serde(rename_all = "camelCase", deny_unknown_fields)]
104pub struct SyncResponse {
105    /// Data about the user, including their encryption keys and the organizations they are a part
106    /// of
107    pub profile: ProfileResponse,
108    pub folders: Vec<Folder>,
109    pub collections: Vec<Collection>,
110    /// List of ciphers accessible by the user
111    pub ciphers: Vec<Cipher>,
112    pub domains: Option<DomainResponse>,
113    //pub policies: Vec<Policy>,
114    //pub sends: Vec<Send>,
115}
116
117impl SyncResponse {
118    pub(crate) fn process_response(response: SyncResponseModel) -> Result<SyncResponse, SyncError> {
119        let profile = require!(response.profile);
120        let ciphers = require!(response.ciphers);
121
122        fn try_into_iter<In, InItem, Out, OutItem>(iter: In) -> Result<Out, InItem::Error>
123        where
124            In: IntoIterator<Item = InItem>,
125            InItem: TryInto<OutItem>,
126            Out: FromIterator<OutItem>,
127        {
128            iter.into_iter().map(|i| i.try_into()).collect()
129        }
130
131        Ok(SyncResponse {
132            profile: ProfileResponse::process_response(*profile)?,
133            folders: try_into_iter(require!(response.folders))?,
134            collections: try_into_iter(require!(response.collections))?,
135            ciphers: try_into_iter(ciphers)?,
136            domains: response.domains.map(|d| (*d).try_into()).transpose()?,
137            //policies: try_into_iter(require!(response.policies))?,
138            //sends: try_into_iter(require!(response.sends))?,
139        })
140    }
141}
142
143impl ProfileOrganizationResponse {
144    fn process_response(
145        response: ProfileOrganizationResponseModel,
146    ) -> Result<ProfileOrganizationResponse, MissingFieldError> {
147        Ok(ProfileOrganizationResponse {
148            id: OrganizationId::new(require!(response.id)),
149        })
150    }
151}
152
153impl ProfileResponse {
154    fn process_response(
155        response: ProfileResponseModel,
156    ) -> Result<ProfileResponse, MissingFieldError> {
157        Ok(ProfileResponse {
158            id: UserId::new(require!(response.id)),
159            name: require!(response.name),
160            email: require!(response.email),
161            //key: response.key,
162            //private_key: response.private_key,
163            organizations: response
164                .organizations
165                .unwrap_or_default()
166                .into_iter()
167                .map(ProfileOrganizationResponse::process_response)
168                .collect::<Result<_, _>>()?,
169        })
170    }
171}
172
173impl TryFrom<DomainsResponseModel> for DomainResponse {
174    type Error = SyncError;
175
176    fn try_from(value: DomainsResponseModel) -> Result<Self, Self::Error> {
177        Ok(Self {
178            equivalent_domains: value.equivalent_domains.unwrap_or_default(),
179            global_equivalent_domains: value
180                .global_equivalent_domains
181                .unwrap_or_default()
182                .into_iter()
183                .map(|s| s.try_into())
184                .collect::<Result<Vec<GlobalDomains>, _>>()?,
185        })
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use std::num::NonZeroU32;
192
193    use bitwarden_api_api::models::{
194        KdfType, MasterPasswordUnlockKdfResponseModel, MasterPasswordUnlockResponseModel,
195        UserDecryptionResponseModel,
196    };
197    use bitwarden_core::{
198        ClientSettings, DeviceType,
199        key_management::{
200            MasterPasswordUnlockData, SymmetricKeyId, UserKeyState,
201            account_cryptographic_state::WrappedAccountCryptographicState,
202            crypto::{InitOrgCryptoRequest, InitUserCryptoMethod, InitUserCryptoRequest},
203        },
204    };
205    use bitwarden_crypto::{EncString, Kdf, UnsignedSharedKey};
206    use bitwarden_test::{MemoryRepository, start_api_mock};
207    use wiremock::{Mock, MockServer, Request, ResponseTemplate, matchers};
208
209    use super::*;
210
211    const TEST_USER_NAME: &str = "Test User";
212    const TEST_USER_EMAIL: &str = "[email protected]";
213    const TEST_USER_PASSWORD: &str = "asdfasdfasdf";
214    const TEST_USER_ID: &str = "060000fb-0922-4dd3-b170-6e15cb5df8c8";
215    const TEST_ACCOUNT_USER_KEY: &str = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=";
216    const TEST_ACCOUNT_PRIVATE_KEY: &str = "2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4=";
217    const TEST_ACCOUNT_ORGANIZATION_ID: &str = "1bc9ac1e-f5aa-45f2-94bf-b181009709b8";
218    const TEST_ACCOUNT_ORGANIZATION_KEY: &str = "4.rY01mZFXHOsBAg5Fq4gyXuklWfm6mQASm42DJpx05a+e2mmp+P5W6r54WU2hlREX0uoTxyP91bKKwickSPdCQQ58J45LXHdr9t2uzOYyjVzpzebFcdMw1eElR9W2DW8wEk9+mvtWvKwu7yTebzND+46y1nRMoFydi5zPVLSlJEf81qZZ4Uh1UUMLwXz+NRWfixnGXgq2wRq1bH0n3mqDhayiG4LJKgGdDjWXC8W8MMXDYx24SIJrJu9KiNEMprJE+XVF9nQVNijNAjlWBqkDpsfaWTUfeVLRLctfAqW1blsmIv4RQ91PupYJZDNc8nO9ZTF3TEVM+2KHoxzDJrLs2Q==";
219
220    fn create_profile_response(user_id: UserId) -> ProfileResponseModel {
221        ProfileResponseModel {
222            id: Some(user_id.into()),
223            name: Some(TEST_USER_NAME.to_string()),
224            email: Some(TEST_USER_EMAIL.to_string()),
225            organizations: Some(vec![]),
226            ..ProfileResponseModel::new()
227        }
228    }
229
230    fn create_sync_response(user_id: UserId) -> SyncResponseModel {
231        SyncResponseModel {
232            profile: Some(Box::new(create_profile_response(user_id))),
233            folders: Some(vec![]),
234            collections: Some(vec![]),
235            ciphers: Some(vec![]),
236            ..SyncResponseModel::new()
237        }
238    }
239
240    async fn setup_sync_client(
241        response: SyncResponseModel,
242        user_crypto_request: InitUserCryptoRequest,
243        org_crypto_request: Option<InitOrgCryptoRequest>,
244    ) -> (MockServer, Client) {
245        let (server, api_config) = start_api_mock(vec![
246            Mock::given(matchers::path("/sync"))
247                .respond_with(move |_: &Request| {
248                    ResponseTemplate::new(200).set_body_json(response.to_owned())
249                })
250                .expect(1),
251        ])
252        .await;
253
254        let client = Client::new(Some(ClientSettings {
255            identity_url: api_config.base_path.clone(),
256            api_url: api_config.base_path,
257            user_agent: api_config.user_agent.unwrap(),
258            device_type: DeviceType::SDK,
259            device_identifier: None,
260            bitwarden_client_version: None,
261            bitwarden_package_type: None,
262        }));
263
264        let repository = MemoryRepository::<UserKeyState>::default();
265        client
266            .platform()
267            .state()
268            .register_client_managed(std::sync::Arc::new(repository));
269
270        client
271            .crypto()
272            .initialize_user_crypto(user_crypto_request)
273            .await
274            .unwrap();
275
276        if let Some(org_crypto_request) = org_crypto_request {
277            client
278                .crypto()
279                .initialize_org_crypto(org_crypto_request)
280                .await
281                .unwrap();
282        }
283
284        (server, client)
285    }
286
287    fn make_user_crypto_request() -> InitUserCryptoRequest {
288        InitUserCryptoRequest {
289            user_id: Some(TEST_USER_ID.parse().unwrap()),
290            kdf_params: Kdf::default_pbkdf2(),
291            email: TEST_USER_EMAIL.to_string(),
292            account_cryptographic_state: WrappedAccountCryptographicState::V1 {
293                private_key: TEST_ACCOUNT_PRIVATE_KEY.parse().unwrap(),
294            },
295            method: InitUserCryptoMethod::MasterPasswordUnlock {
296                password: TEST_USER_PASSWORD.to_string(),
297                master_password_unlock: MasterPasswordUnlockData {
298                    kdf: Kdf::default_pbkdf2(),
299                    master_key_wrapped_user_key: TEST_ACCOUNT_USER_KEY.parse().unwrap(),
300                    salt: TEST_USER_EMAIL.to_string(),
301                },
302            },
303        }
304    }
305
306    #[tokio::test]
307    async fn test_sync_user_empty_vault_no_organizations() {
308        let user_id: UserId = TEST_USER_ID.parse().unwrap();
309        let organization_id: OrganizationId = TEST_ACCOUNT_ORGANIZATION_ID
310            .parse()
311            .expect("Invalid organization ID");
312        let user_crypto_request = make_user_crypto_request();
313        let (_server, client) =
314            setup_sync_client(create_sync_response(user_id), user_crypto_request, None).await;
315
316        let sync_request = SyncRequest {
317            exclude_subdomains: Some(false),
318        };
319
320        let sync_response = sync(&client, &sync_request).await;
321        assert!(sync_response.is_ok());
322
323        let sync_response = sync_response.unwrap();
324        assert_eq!(sync_response.profile.id, user_id);
325        assert_eq!(sync_response.profile.name, TEST_USER_NAME);
326        assert_eq!(sync_response.profile.email, TEST_USER_EMAIL);
327        assert!(sync_response.profile.organizations.is_empty());
328        assert!(sync_response.ciphers.is_empty());
329        assert!(sync_response.folders.is_empty());
330        assert!(sync_response.collections.is_empty());
331        assert!(sync_response.domains.is_none());
332        assert!(
333            !client
334                .internal
335                .get_key_store()
336                .context()
337                .has_symmetric_key(SymmetricKeyId::Organization(organization_id))
338        );
339    }
340
341    #[tokio::test]
342    async fn test_sync_user_with_organization() {
343        let user_id = UserId::new(uuid::uuid!(TEST_USER_ID));
344        let organization_id: OrganizationId = TEST_ACCOUNT_ORGANIZATION_ID
345            .parse()
346            .expect("Invalid organization ID");
347        let organization_key: UnsignedSharedKey = TEST_ACCOUNT_ORGANIZATION_KEY
348            .parse()
349            .expect("Invalid organization key");
350        let user_crypto_request = make_user_crypto_request();
351        let response = SyncResponseModel {
352            profile: Some(Box::new(ProfileResponseModel {
353                organizations: Some(vec![ProfileOrganizationResponseModel {
354                    id: Some(organization_id.into()),
355                    key: Some(organization_key.to_string()),
356                    ..ProfileOrganizationResponseModel::new()
357                }]),
358                ..create_profile_response(user_id)
359            })),
360            ..create_sync_response(user_id)
361        };
362        let (_server, client) = setup_sync_client(response, user_crypto_request, None).await;
363
364        let sync_request = SyncRequest {
365            exclude_subdomains: Some(false),
366        };
367
368        let sync_response = sync(&client, &sync_request).await;
369        assert!(sync_response.is_ok());
370
371        let sync_response = sync_response.unwrap();
372        assert_eq!(sync_response.profile.id, user_id);
373        assert_eq!(sync_response.profile.name, TEST_USER_NAME);
374        assert_eq!(sync_response.profile.email, TEST_USER_EMAIL);
375        assert_eq!(sync_response.profile.organizations.len(), 1);
376        let organization = sync_response.profile.organizations.first().unwrap();
377        assert_eq!(organization.id, organization_id);
378        assert!(sync_response.ciphers.is_empty());
379        assert!(sync_response.folders.is_empty());
380        assert!(sync_response.collections.is_empty());
381        assert!(sync_response.domains.is_none());
382        assert!(
383            client
384                .internal
385                .get_key_store()
386                .context()
387                .has_symmetric_key(SymmetricKeyId::Organization(organization_id))
388        );
389    }
390
391    #[tokio::test]
392    async fn test_sync_user_with_decryption_options_master_password_unlock() {
393        let user_id = UserId::new(uuid::uuid!(TEST_USER_ID));
394        let user_key: EncString = TEST_ACCOUNT_USER_KEY.parse().expect("Invalid user key");
395        let user_crypto_request = make_user_crypto_request();
396        let response = SyncResponseModel {
397            user_decryption: Some(Box::new(UserDecryptionResponseModel {
398                master_password_unlock: Some(Box::new(MasterPasswordUnlockResponseModel {
399                    kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
400                        kdf_type: KdfType::Argon2id,
401                        iterations: 4,
402                        memory: Some(65),
403                        parallelism: Some(5),
404                    }),
405                    salt: Some(TEST_USER_EMAIL.to_string()),
406                    master_key_encrypted_user_key: Some(user_key.to_string()),
407                })),
408                web_authn_prf_options: None,
409                v2_upgrade_token: None,
410            })),
411            ..create_sync_response(user_id)
412        };
413        let (_server, client) = setup_sync_client(response, user_crypto_request, None).await;
414
415        let sync_request = SyncRequest {
416            exclude_subdomains: Some(false),
417        };
418
419        let sync_response = sync(&client, &sync_request).await;
420        assert!(sync_response.is_ok());
421
422        assert_eq!(
423            client.internal.get_kdf().unwrap(),
424            Kdf::Argon2id {
425                iterations: NonZeroU32::new(4).unwrap(),
426                memory: NonZeroU32::new(65).unwrap(),
427                parallelism: NonZeroU32::new(5).unwrap(),
428            }
429        );
430    }
431}