diff --git a/src/cli/add_command.rs b/src/cli/add_command.rs index a14686a..a75019e 100644 --- a/src/cli/add_command.rs +++ b/src/cli/add_command.rs @@ -17,6 +17,7 @@ use clap::Args; use crate::{ + clap_parsers, utils, vault::{Vault, Vaults}, LprsCommand, @@ -29,16 +30,21 @@ use crate::{ /// Add command, used to add new vault to the vaults file pub struct Add { #[command(flatten)] - vault_info: Vault, + vault_info: Vault, /// The password, if there is no value for it you will prompt it #[arg(short, long)] #[allow(clippy::option_option)] - password: Option>, + password: Option>, + /// Add a custom field to the vault + #[arg(name = "KEY=VALUE", short = 'c', long = "custom")] + #[arg(value_parser = clap_parsers::kv_parser)] + custom_fields: Vec<(String, String)>, } impl LprsCommand for Add { fn run(mut self, mut vault_manager: Vaults) -> LprsResult<()> { self.vault_info.password = utils::user_password(self.password, "Vault password:")?; + self.vault_info.custom_fields = self.custom_fields.into_iter().collect(); vault_manager.add_vault(self.vault_info); vault_manager.try_export() } @@ -48,9 +54,17 @@ impl LprsCommand for Add { && self.password.is_none() && self.vault_info.service.is_none() && self.vault_info.note.is_none() + && self.custom_fields.is_empty() { return Err(LprsError::Other("You can't add empty vault".to_owned())); } + + if let Some(duplicated_key) = utils::get_duplicated_field(&self.custom_fields) { + return Err(LprsError::Other(format!( + "Duplication error: The custom key `{duplicated_key}` is duplicate" + ))); + } + Ok(()) } } diff --git a/src/cli/edit_command.rs b/src/cli/edit_command.rs index 2069d82..7a81388 100644 --- a/src/cli/edit_command.rs +++ b/src/cli/edit_command.rs @@ -18,37 +18,37 @@ use std::num::NonZeroU64; use clap::Args; -use crate::{ - utils, - vault::{Vault, Vaults}, - LprsCommand, - LprsError, - LprsResult, -}; +use crate::{clap_parsers, utils, vault::Vaults, LprsCommand, LprsError, LprsResult}; #[derive(Debug, Args)] #[command(author, version, about, long_about = None)] /// Edit command, used to edit the vault content pub struct Edit { - /// The password index. Check it from list command + /// The password index. You can get it from the list command index: NonZeroU64, #[arg(short, long)] /// The new vault name - name: Option, + name: Option, #[arg(short, long)] /// The new vault username - username: Option, + username: Option, #[arg(short, long)] /// The new password, if there is no value for it you will prompt it #[allow(clippy::option_option)] - password: Option>, + password: Option>, #[arg(short, long)] /// The new vault service - service: Option, + service: Option, #[arg(short = 'o', long)] /// The new vault note - note: Option, + note: Option, + /// The custom field, make its value empty to delete it + /// + /// If the custom field not exist will created it, if it's will update it + #[arg(name = "KEY=VALUE", short = 'c', long = "custom")] + #[arg(value_parser = clap_parsers::kv_parser)] + pub custom_fields: Vec<(String, String)>, } impl LprsCommand for Edit { @@ -65,15 +65,23 @@ impl LprsCommand for Edit { }; log::info!("Applying the new values to the vault"); - *vault = Vault::new( - self.name.as_ref().unwrap_or(&vault.name), - self.username.as_ref().or(vault.username.as_ref()), - utils::user_password(self.password, "New vault password:")? - .as_ref() - .or(vault.password.as_ref()), - self.service.as_ref().or(vault.service.as_ref()), - self.note.as_ref().or(vault.note.as_ref()), - ); + if let Some(new_name) = self.name { + vault.name = new_name; + } + if self.password.is_some() { + vault.password = utils::user_password(self.password, "New vault password:")?; + } + if let Some(new_username) = self.username { + vault.username = Some(new_username); + } + if let Some(new_service) = self.service { + vault.service = Some(new_service); + } + if let Some(new_note) = self.note { + vault.note = Some(new_note); + } + utils::apply_custom_fields(&mut vault.custom_fields, self.custom_fields); + vault_manager.try_export() } @@ -83,11 +91,18 @@ impl LprsCommand for Edit { && self.password.is_none() && self.service.is_none() && self.note.is_none() + && self.custom_fields.is_empty() { return Err(LprsError::Other( "You must edit one option at least".to_owned(), )); } + if let Some(duplicated_key) = utils::get_duplicated_field(&self.custom_fields) { + return Err(LprsError::Other(format!( + "Duplication error: The custom key `{duplicated_key}` is duplicate" + ))); + } + Ok(()) } } diff --git a/src/vault/bitwarden.rs b/src/vault/bitwarden.rs index c91fc15..36d6e2e 100644 --- a/src/vault/bitwarden.rs +++ b/src/vault/bitwarden.rs @@ -42,13 +42,21 @@ pub struct BitWardenFolder { pub name: String, } +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct BitWardenNameValue { + pub name: String, + pub value: String, +} + #[derive(Clone, Debug, Deserialize, Serialize)] pub struct BitWardenPassword { #[serde(rename = "type")] - pub ty: i32, - pub name: String, - pub login: Option, - pub notes: Option, + pub ty: i32, + pub name: String, + pub login: Option, + pub notes: Option, + #[serde(default)] + pub fields: Vec, } /// The bitwarden password struct @@ -71,6 +79,11 @@ impl From for Vault { .and_then(|p| p.first().map(|u| u.uri.clone())) }), value.notes, + value + .fields + .into_iter() + .map(|nv| (nv.name, nv.value)) + .collect(), ) } } @@ -78,16 +91,21 @@ impl From for Vault { impl From for BitWardenPassword { fn from(value: Vault) -> Self { Self { - ty: 1, - name: value.name, - login: Some(BitWardenLoginData { + ty: 1, + name: value.name, + login: Some(BitWardenLoginData { username: value.username, password: value.password, uris: value .service .map(|s| vec![BitWardenUri { mt: None, uri: s }]), }), - notes: value.note, + notes: value.note, + fields: value + .custom_fields + .into_iter() + .map(|(name, value)| BitWardenNameValue { name, value }) + .collect(), } } } diff --git a/src/vault/mod.rs b/src/vault/mod.rs index 47f6468..627ef20 100644 --- a/src/vault/mod.rs +++ b/src/vault/mod.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::{fmt, fs, path::PathBuf}; +use std::{collections::BTreeMap, fmt, fs, path::PathBuf}; use base64::Engine; use clap::{Parser, ValueEnum}; @@ -45,19 +45,22 @@ pub enum Format { #[derive(Clone, Debug, Deserialize, Serialize, Parser)] pub struct Vault { /// The name of the vault - pub name: String, + pub name: String, /// The username #[arg(short, long)] - pub username: Option, + pub username: Option, /// The password #[arg(skip)] - pub password: Option, + pub password: Option, /// The service name. e.g the website url #[arg(short, long)] - pub service: Option, + pub service: Option, /// Add a note to the vault #[arg(short, long)] - pub note: Option, + pub note: Option, + /// The vault custom fields + #[arg(skip)] + pub custom_fields: BTreeMap, } /// The vaults manager @@ -79,13 +82,15 @@ impl Vault { password: Option>, service: Option>, note: Option>, + custom_fields: BTreeMap, ) -> Self { Self { - name: name.into(), + name: name.into(), username: username.map(Into::into), password: password.map(Into::into), - service: service.map(Into::into), - note: note.map(Into::into), + service: service.map(Into::into), + note: note.map(Into::into), + custom_fields, } } @@ -131,9 +136,8 @@ impl Vaults { /// /// Note: The returned string is `Vec` pub fn json_export(&self, encryption_key: &[u8; 32]) -> LprsResult { - let encrypt = |val: &str| { - LprsResult::Ok(crate::BASE64.encode(cipher::encrypt(encryption_key, val.as_ref()))) - }; + let encrypt = + |val: &str| crate::BASE64.encode(cipher::encrypt(encryption_key, val.as_ref())); serde_json::to_string( &self @@ -141,11 +145,15 @@ impl Vaults { .iter() .map(|v| { LprsResult::Ok(Vault::new( - encrypt(&v.name)?, - v.username.as_ref().and_then(|u| encrypt(u).ok()), - v.password.as_ref().and_then(|p| encrypt(p).ok()), - v.service.as_ref().and_then(|s| encrypt(s).ok()), - v.note.as_ref().and_then(|n| encrypt(n).ok()), + encrypt(&v.name), + v.username.as_ref().map(|u| encrypt(u)), + v.password.as_ref().map(|p| encrypt(p)), + v.service.as_ref().map(|s| encrypt(s)), + v.note.as_ref().map(|n| encrypt(n)), + v.custom_fields + .iter() + .map(|(key, value)| (encrypt(key), encrypt(value))) + .collect(), )) }) .collect::>>()?, @@ -178,6 +186,10 @@ impl Vaults { v.password.as_ref().and_then(|p| decrypt(p).ok()), v.service.as_ref().and_then(|s| decrypt(s).ok()), v.note.as_ref().and_then(|n| decrypt(n).ok()), + v.custom_fields + .into_iter() + .map(|(key, value)| LprsResult::Ok((decrypt(&key)?, decrypt(&value)?))) + .collect::>()?, )) }) .collect() @@ -245,6 +257,9 @@ impl fmt::Display for Vault { if let Some(ref note) = self.note { write!(f, "\nNote:\n{note}")?; } + for (key, value) in &self.custom_fields { + write!(f, "\n{key}: {value}")?; + } Ok(()) }