use crate::prelude::*; use std::io::{Read as _, Write as _}; use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; #[derive( serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq, )] pub struct Entry { pub id: String, pub org_id: Option, pub folder: Option, pub folder_id: Option, pub name: String, pub data: EntryData, pub fields: Vec, pub notes: Option, pub history: Vec, } #[derive(serde::Serialize, Debug, Clone, Eq, PartialEq)] pub struct Uri { pub uri: String, pub match_type: Option, } // backwards compatibility impl<'de> serde::Deserialize<'de> for Uri { fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { struct StringOrUri; impl<'de> serde::de::Visitor<'de> for StringOrUri { type Value = Uri; fn expecting( &self, formatter: &mut std::fmt::Formatter, ) -> std::fmt::Result { formatter.write_str("uri") } fn visit_str( self, value: &str, ) -> std::result::Result where E: serde::de::Error, { Ok(Uri { uri: value.to_string(), match_type: None, }) } fn visit_map( self, mut map: M, ) -> std::result::Result where M: serde::de::MapAccess<'de>, { let mut uri = None; let mut match_type = None; while let Some(key) = map.next_key()? { match key { "uri" => { if uri.is_some() { return Err( serde::de::Error::duplicate_field("uri"), ); } uri = Some(map.next_value()?); } "match_type" => { if match_type.is_some() { return Err( serde::de::Error::duplicate_field( "match_type", ), ); } match_type = map.next_value()?; } _ => { return Err(serde::de::Error::unknown_field( key, &["uri", "match_type"], )) } } } uri.map_or_else( || Err(serde::de::Error::missing_field("uri")), |uri| Ok(Self::Value { uri, match_type }), ) } } deserializer.deserialize_any(StringOrUri) } } #[derive( serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq, )] pub enum EntryData { Login { username: Option, password: Option, totp: Option, uris: Vec, }, Card { cardholder_name: Option, number: Option, brand: Option, exp_month: Option, exp_year: Option, code: Option, }, Identity { title: Option, first_name: Option, middle_name: Option, last_name: Option, address1: Option, address2: Option, address3: Option, city: Option, state: Option, postal_code: Option, country: Option, phone: Option, email: Option, ssn: Option, license_number: Option, passport_number: Option, username: Option, }, SecureNote, } #[derive( serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq, )] pub struct Field { pub ty: crate::api::FieldType, pub name: Option, pub value: Option, pub linked_id: Option, } #[derive( serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq, )] pub struct HistoryEntry { pub last_used_date: String, pub password: String, } #[derive(serde::Serialize, serde::Deserialize, Default, Debug)] pub struct Db { pub access_token: Option, pub refresh_token: Option, pub kdf: Option, pub iterations: Option, pub memory: Option, pub parallelism: Option, pub protected_key: Option, pub protected_private_key: Option, pub protected_org_keys: std::collections::HashMap, pub entries: Vec, } impl Db { #[must_use] pub fn new() -> Self { Self::default() } pub fn load(server: &str, email: &str) -> Result { let file = crate::dirs::db_file(server, email); let mut fh = std::fs::File::open(&file).map_err(|source| Error::LoadDb { source, file: file.clone(), })?; let mut json = String::new(); fh.read_to_string(&mut json) .map_err(|source| Error::LoadDb { source, file: file.clone(), })?; let slf: Self = serde_json::from_str(&json) .map_err(|source| Error::LoadDbJson { source, file })?; Ok(slf) } pub async fn load_async(server: &str, email: &str) -> Result { let file = crate::dirs::db_file(server, email); let mut fh = tokio::fs::File::open(&file).await.map_err(|source| { Error::LoadDbAsync { source, file: file.clone(), } })?; let mut json = String::new(); fh.read_to_string(&mut json).await.map_err(|source| { Error::LoadDbAsync { source, file: file.clone(), } })?; let slf: Self = serde_json::from_str(&json) .map_err(|source| Error::LoadDbJson { source, file })?; Ok(slf) } // XXX need to make this atomic pub fn save(&self, server: &str, email: &str) -> Result<()> { let file = crate::dirs::db_file(server, email); // unwrap is safe here because Self::filename is explicitly // constructed as a filename in a directory std::fs::create_dir_all(file.parent().unwrap()).map_err( |source| Error::SaveDb { source, file: file.clone(), }, )?; let mut fh = std::fs::File::create(&file).map_err(|source| Error::SaveDb { source, file: file.clone(), })?; fh.write_all( serde_json::to_string(self) .map_err(|source| Error::SaveDbJson { source, file: file.clone(), })? .as_bytes(), ) .map_err(|source| Error::SaveDb { source, file })?; Ok(()) } // XXX need to make this atomic pub async fn save_async(&self, server: &str, email: &str) -> Result<()> { let file = crate::dirs::db_file(server, email); // unwrap is safe here because Self::filename is explicitly // constructed as a filename in a directory tokio::fs::create_dir_all(file.parent().unwrap()) .await .map_err(|source| Error::SaveDbAsync { source, file: file.clone(), })?; let mut fh = tokio::fs::File::create(&file).await.map_err(|source| { Error::SaveDbAsync { source, file: file.clone(), } })?; fh.write_all( serde_json::to_string(self) .map_err(|source| Error::SaveDbJson { source, file: file.clone(), })? .as_bytes(), ) .await .map_err(|source| Error::SaveDbAsync { source, file })?; Ok(()) } pub fn remove(server: &str, email: &str) -> Result<()> { let file = crate::dirs::db_file(server, email); let res = std::fs::remove_file(&file); if let Err(e) = &res { if e.kind() == std::io::ErrorKind::NotFound { return Ok(()); } } res.map_err(|source| Error::RemoveDb { source, file })?; Ok(()) } #[must_use] pub fn needs_login(&self) -> bool { self.access_token.is_none() || self.refresh_token.is_none() || self.iterations.is_none() || self.kdf.is_none() || self.protected_key.is_none() } }