Skip to main content

bwx/
db.rs

1use crate::prelude::*;
2
3use std::io::{Read as _, Write as _};
4
5use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
6
7#[derive(
8    serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq,
9)]
10pub struct Entry {
11    pub id: String,
12    pub org_id: Option<String>,
13    pub folder: Option<String>,
14    pub folder_id: Option<String>,
15    pub name: String,
16    pub data: EntryData,
17    pub fields: Vec<Field>,
18    pub notes: Option<String>,
19    pub history: Vec<HistoryEntry>,
20    pub key: Option<String>,
21    pub master_password_reprompt: crate::api::CipherRepromptType,
22}
23
24impl Entry {
25    pub fn master_password_reprompt(&self) -> bool {
26        self.master_password_reprompt != crate::api::CipherRepromptType::None
27    }
28}
29
30#[derive(serde::Serialize, Debug, Clone, Eq, PartialEq)]
31pub struct Uri {
32    pub uri: String,
33    pub match_type: Option<crate::api::UriMatchType>,
34}
35
36// backwards compatibility
37impl<'de> serde::Deserialize<'de> for Uri {
38    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
39    where
40        D: serde::Deserializer<'de>,
41    {
42        struct StringOrUri;
43        impl<'de> serde::de::Visitor<'de> for StringOrUri {
44            type Value = Uri;
45
46            fn expecting(
47                &self,
48                formatter: &mut std::fmt::Formatter,
49            ) -> std::fmt::Result {
50                formatter.write_str("uri")
51            }
52
53            fn visit_str<E>(
54                self,
55                value: &str,
56            ) -> std::result::Result<Self::Value, E>
57            where
58                E: serde::de::Error,
59            {
60                Ok(Uri {
61                    uri: value.to_string(),
62                    match_type: None,
63                })
64            }
65
66            fn visit_map<M>(
67                self,
68                mut map: M,
69            ) -> std::result::Result<Self::Value, M::Error>
70            where
71                M: serde::de::MapAccess<'de>,
72            {
73                let mut uri = None;
74                let mut match_type = None;
75                while let Some(key) = map.next_key()? {
76                    match key {
77                        "uri" => {
78                            if uri.is_some() {
79                                return Err(
80                                    serde::de::Error::duplicate_field("uri"),
81                                );
82                            }
83                            uri = Some(map.next_value()?);
84                        }
85                        "match_type" => {
86                            if match_type.is_some() {
87                                return Err(
88                                    serde::de::Error::duplicate_field(
89                                        "match_type",
90                                    ),
91                                );
92                            }
93                            match_type = map.next_value()?;
94                        }
95                        _ => {
96                            return Err(serde::de::Error::unknown_field(
97                                key,
98                                &["uri", "match_type"],
99                            ))
100                        }
101                    }
102                }
103
104                uri.map_or_else(
105                    || Err(serde::de::Error::missing_field("uri")),
106                    |uri| Ok(Self::Value { uri, match_type }),
107                )
108            }
109        }
110
111        deserializer.deserialize_any(StringOrUri)
112    }
113}
114
115#[derive(
116    serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq,
117)]
118pub enum EntryData {
119    Login {
120        username: Option<String>,
121        password: Option<String>,
122        totp: Option<String>,
123        uris: Vec<Uri>,
124    },
125    Card {
126        cardholder_name: Option<String>,
127        number: Option<String>,
128        brand: Option<String>,
129        exp_month: Option<String>,
130        exp_year: Option<String>,
131        code: Option<String>,
132    },
133    Identity {
134        title: Option<String>,
135        first_name: Option<String>,
136        middle_name: Option<String>,
137        last_name: Option<String>,
138        address1: Option<String>,
139        address2: Option<String>,
140        address3: Option<String>,
141        city: Option<String>,
142        state: Option<String>,
143        postal_code: Option<String>,
144        country: Option<String>,
145        phone: Option<String>,
146        email: Option<String>,
147        ssn: Option<String>,
148        license_number: Option<String>,
149        passport_number: Option<String>,
150        username: Option<String>,
151    },
152    SecureNote,
153    SshKey {
154        private_key: Option<String>,
155        public_key: Option<String>,
156        fingerprint: Option<String>,
157    },
158}
159
160#[derive(
161    serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq,
162)]
163pub struct Field {
164    pub ty: Option<crate::api::FieldType>,
165    pub name: Option<String>,
166    pub value: Option<String>,
167    pub linked_id: Option<crate::api::LinkedIdType>,
168}
169
170#[derive(
171    serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq,
172)]
173pub struct HistoryEntry {
174    pub last_used_date: String,
175    pub password: String,
176}
177
178#[derive(serde::Serialize, serde::Deserialize, Default, Debug)]
179pub struct Db {
180    pub access_token: Option<String>,
181    pub refresh_token: Option<String>,
182
183    pub kdf: Option<crate::api::KdfType>,
184    pub iterations: Option<u32>,
185    pub memory: Option<u32>,
186    pub parallelism: Option<u32>,
187    pub protected_key: Option<String>,
188    pub protected_private_key: Option<String>,
189    pub protected_org_keys: std::collections::HashMap<String, String>,
190
191    pub entries: Vec<Entry>,
192}
193
194impl Db {
195    pub fn new() -> Self {
196        Self::default()
197    }
198
199    pub fn load(server: &str, email: &str) -> Result<Self> {
200        let file = crate::dirs::db_file(server, email);
201        let mut fh =
202            std::fs::File::open(&file).map_err(|source| Error::LoadDb {
203                source,
204                file: file.clone(),
205            })?;
206        let mut json = String::new();
207        fh.read_to_string(&mut json)
208            .map_err(|source| Error::LoadDb {
209                source,
210                file: file.clone(),
211            })?;
212        let slf: Self = serde_json::from_str(&json)
213            .map_err(|source| Error::LoadDbJson { source, file })?;
214        Ok(slf)
215    }
216
217    pub async fn load_async(server: &str, email: &str) -> Result<Self> {
218        let file = crate::dirs::db_file(server, email);
219        let mut fh =
220            tokio::fs::File::open(&file).await.map_err(|source| {
221                Error::LoadDbAsync {
222                    source,
223                    file: file.clone(),
224                }
225            })?;
226        let mut json = String::new();
227        fh.read_to_string(&mut json).await.map_err(|source| {
228            Error::LoadDbAsync {
229                source,
230                file: file.clone(),
231            }
232        })?;
233        let slf: Self = serde_json::from_str(&json)
234            .map_err(|source| Error::LoadDbJson { source, file })?;
235        Ok(slf)
236    }
237
238    // XXX need to make this atomic
239    pub fn save(&self, server: &str, email: &str) -> Result<()> {
240        use std::os::unix::fs::{OpenOptionsExt as _, PermissionsExt as _};
241        let file = crate::dirs::db_file(server, email);
242        // unwrap is safe here because Self::filename is explicitly
243        // constructed as a filename in a directory
244        std::fs::create_dir_all(file.parent().unwrap()).map_err(
245            |source| Error::SaveDb {
246                source,
247                file: file.clone(),
248            },
249        )?;
250        let mut fh = std::fs::OpenOptions::new()
251            .write(true)
252            .create(true)
253            .truncate(true)
254            .mode(0o600)
255            .open(&file)
256            .map_err(|source| Error::SaveDb {
257                source,
258                file: file.clone(),
259            })?;
260        fh.set_permissions(std::fs::Permissions::from_mode(0o600))
261            .map_err(|source| Error::SaveDb {
262                source,
263                file: file.clone(),
264            })?;
265        fh.write_all(
266            serde_json::to_string(self)
267                .map_err(|source| Error::SaveDbJson {
268                    source,
269                    file: file.clone(),
270                })?
271                .as_bytes(),
272        )
273        .map_err(|source| Error::SaveDb { source, file })?;
274        Ok(())
275    }
276
277    // XXX need to make this atomic
278    pub async fn save_async(&self, server: &str, email: &str) -> Result<()> {
279        use std::os::unix::fs::PermissionsExt as _;
280        let file = crate::dirs::db_file(server, email);
281        // unwrap is safe here because Self::filename is explicitly
282        // constructed as a filename in a directory
283        tokio::fs::create_dir_all(file.parent().unwrap())
284            .await
285            .map_err(|source| Error::SaveDbAsync {
286                source,
287                file: file.clone(),
288            })?;
289        let mut fh = tokio::fs::OpenOptions::new()
290            .write(true)
291            .create(true)
292            .truncate(true)
293            .mode(0o600)
294            .open(&file)
295            .await
296            .map_err(|source| Error::SaveDbAsync {
297                source,
298                file: file.clone(),
299            })?;
300        fh.set_permissions(std::fs::Permissions::from_mode(0o600))
301            .await
302            .map_err(|source| Error::SaveDbAsync {
303                source,
304                file: file.clone(),
305            })?;
306        fh.write_all(
307            serde_json::to_string(self)
308                .map_err(|source| Error::SaveDbJson {
309                    source,
310                    file: file.clone(),
311                })?
312                .as_bytes(),
313        )
314        .await
315        .map_err(|source| Error::SaveDbAsync { source, file })?;
316        Ok(())
317    }
318
319    pub fn remove(server: &str, email: &str) -> Result<()> {
320        let file = crate::dirs::db_file(server, email);
321        let res = std::fs::remove_file(&file);
322        if let Err(e) = &res {
323            if e.kind() == std::io::ErrorKind::NotFound {
324                return Ok(());
325            }
326        }
327        res.map_err(|source| Error::RemoveDb { source, file })?;
328        Ok(())
329    }
330
331    pub fn needs_login(&self) -> bool {
332        self.access_token.is_none()
333            || self.refresh_token.is_none()
334            || self.iterations.is_none()
335            || self.kdf.is_none()
336            || self.protected_key.is_none()
337    }
338}