bitwarden_vault/
sync.rs

1use bitwarden_api_api::models::{
2    DomainsResponseModel, ProfileOrganizationResponseModel, ProfileResponseModel, SyncResponseModel,
3};
4use bitwarden_core::{
5    client::encryption_settings::EncryptionSettingsError, require, Client, MissingFieldError,
6};
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9use uuid::Uuid;
10
11use crate::{Cipher, Collection, Folder, GlobalDomains, VaultParseError};
12
13#[derive(Debug, Error)]
14pub enum SyncError {
15    #[error(transparent)]
16    Api(#[from] bitwarden_core::ApiError),
17    #[error(transparent)]
18    MissingField(#[from] MissingFieldError),
19    #[error(transparent)]
20    VaultParse(#[from] VaultParseError),
21    #[error(transparent)]
22    EncryptionSettings(#[from] EncryptionSettingsError),
23}
24
25#[allow(missing_docs)]
26#[derive(Serialize, Deserialize, Debug)]
27#[serde(rename_all = "camelCase", deny_unknown_fields)]
28pub struct SyncRequest {
29    /// Exclude the subdomains from the response, defaults to false
30    pub exclude_subdomains: Option<bool>,
31}
32
33pub(crate) async fn sync(client: &Client, input: &SyncRequest) -> Result<SyncResponse, SyncError> {
34    let config = client.internal.get_api_configurations().await;
35    let sync = bitwarden_api_api::apis::sync_api::sync_get(&config.api, input.exclude_subdomains)
36        .await
37        .map_err(|e| SyncError::Api(e.into()))?;
38
39    let org_keys: Vec<_> = require!(sync.profile.as_ref())
40        .organizations
41        .as_deref()
42        .unwrap_or_default()
43        .iter()
44        .filter_map(|o| o.id.zip(o.key.as_deref().and_then(|k| k.parse().ok())))
45        .collect();
46
47    client.internal.initialize_org_crypto(org_keys)?;
48
49    SyncResponse::process_response(sync)
50}
51
52#[derive(Serialize, Deserialize, Debug)]
53#[serde(rename_all = "camelCase", deny_unknown_fields)]
54pub struct ProfileResponse {
55    pub id: Uuid,
56    pub name: String,
57    pub email: String,
58
59    //key: String,
60    //private_key: String,
61    pub organizations: Vec<ProfileOrganizationResponse>,
62}
63
64#[derive(Serialize, Deserialize, Debug)]
65#[serde(rename_all = "camelCase", deny_unknown_fields)]
66pub struct ProfileOrganizationResponse {
67    pub id: Uuid,
68}
69
70#[derive(Serialize, Deserialize, Debug)]
71#[serde(rename_all = "camelCase", deny_unknown_fields)]
72pub struct DomainResponse {
73    pub equivalent_domains: Vec<Vec<String>>,
74    pub global_equivalent_domains: Vec<GlobalDomains>,
75}
76
77#[allow(missing_docs)]
78#[derive(Serialize, Deserialize, Debug)]
79#[serde(rename_all = "camelCase", deny_unknown_fields)]
80pub struct SyncResponse {
81    /// Data about the user, including their encryption keys and the organizations they are a part
82    /// of
83    pub profile: ProfileResponse,
84    pub folders: Vec<Folder>,
85    pub collections: Vec<Collection>,
86    /// List of ciphers accessible by the user
87    pub ciphers: Vec<Cipher>,
88    pub domains: Option<DomainResponse>,
89    //pub policies: Vec<Policy>,
90    //pub sends: Vec<Send>,
91}
92
93impl SyncResponse {
94    pub(crate) fn process_response(response: SyncResponseModel) -> Result<SyncResponse, SyncError> {
95        let profile = require!(response.profile);
96        let ciphers = require!(response.ciphers);
97
98        fn try_into_iter<In, InItem, Out, OutItem>(iter: In) -> Result<Out, InItem::Error>
99        where
100            In: IntoIterator<Item = InItem>,
101            InItem: TryInto<OutItem>,
102            Out: FromIterator<OutItem>,
103        {
104            iter.into_iter().map(|i| i.try_into()).collect()
105        }
106
107        Ok(SyncResponse {
108            profile: ProfileResponse::process_response(*profile)?,
109            folders: try_into_iter(require!(response.folders))?,
110            collections: try_into_iter(require!(response.collections))?,
111            ciphers: try_into_iter(ciphers)?,
112            domains: response.domains.map(|d| (*d).try_into()).transpose()?,
113            //policies: try_into_iter(require!(response.policies))?,
114            //sends: try_into_iter(require!(response.sends))?,
115        })
116    }
117}
118
119impl ProfileOrganizationResponse {
120    fn process_response(
121        response: ProfileOrganizationResponseModel,
122    ) -> Result<ProfileOrganizationResponse, MissingFieldError> {
123        Ok(ProfileOrganizationResponse {
124            id: require!(response.id),
125        })
126    }
127}
128
129impl ProfileResponse {
130    fn process_response(
131        response: ProfileResponseModel,
132    ) -> Result<ProfileResponse, MissingFieldError> {
133        Ok(ProfileResponse {
134            id: require!(response.id),
135            name: require!(response.name),
136            email: require!(response.email),
137            //key: response.key,
138            //private_key: response.private_key,
139            organizations: response
140                .organizations
141                .unwrap_or_default()
142                .into_iter()
143                .map(ProfileOrganizationResponse::process_response)
144                .collect::<Result<_, _>>()?,
145        })
146    }
147}
148
149impl TryFrom<DomainsResponseModel> for DomainResponse {
150    type Error = SyncError;
151
152    fn try_from(value: DomainsResponseModel) -> Result<Self, Self::Error> {
153        Ok(Self {
154            equivalent_domains: value.equivalent_domains.unwrap_or_default(),
155            global_equivalent_domains: value
156                .global_equivalent_domains
157                .unwrap_or_default()
158                .into_iter()
159                .map(|s| s.try_into())
160                .collect::<Result<Vec<GlobalDomains>, _>>()?,
161        })
162    }
163}