From 4b87dd0770fa51fdcc031b227e293b66ed041ad7 Mon Sep 17 00:00:00 2001 From: Awiteb Date: Thu, 9 May 2024 13:25:44 +0300 Subject: [PATCH 1/4] chore: Add `ArgParse` error --- src/errors.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/errors.rs b/src/errors.rs index e425f56..b64d9c6 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -38,6 +38,8 @@ pub enum Error { #[error("Invalid Vault Index Error: {0}")] InvalidVaultIndex(String), #[error("{0}")] + ArgParse(String), + #[error("{0}")] Other(String), #[error("CLI error: {0}")] -- 2.45.2 From 71ca10e865c4b42fa050a00e97ccf10768308211 Mon Sep 17 00:00:00 2001 From: Awiteb Date: Thu, 9 May 2024 13:26:08 +0300 Subject: [PATCH 2/4] chore: A clap parser to parse key value args --- src/clap_parsers.rs | 30 ++++++++++++++++++++++++++++++ src/main.rs | 2 ++ 2 files changed, 32 insertions(+) create mode 100644 src/clap_parsers.rs diff --git a/src/clap_parsers.rs b/src/clap_parsers.rs new file mode 100644 index 0000000..e2bdacd --- /dev/null +++ b/src/clap_parsers.rs @@ -0,0 +1,30 @@ +// Lprs - A local CLI vault manager +// Copyright (C) 2024 Awiteb +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use crate::{LprsError, LprsResult}; + +/// Parse the key & value arguments. +/// ## Errors +/// - If the argument value syntax not `key=value` +pub fn kv_parser(value: &str) -> LprsResult<(String, String)> { + if let Some((key, value)) = value.split_once('=') { + Ok((key.trim().to_owned(), value.trim().to_owned())) + } else { + Err(LprsError::ArgParse( + "There is no value, the syntax is `KEY=VALUE`".to_owned(), + )) + } +} diff --git a/src/main.rs b/src/main.rs index ba59a86..dc27b93 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,8 @@ use std::process::ExitCode; use clap::Parser; use inquire::InquireError; +/// A set of clap vaule parsers used to parse some CLI arguments +pub mod clap_parsers; /// The main module of the lprs crate, contains the cli and the commands. pub mod cli; /// The errors module, contains the errors and the result type. -- 2.45.2 From 0b020a1cb61710b8e8e05ea71ff9dcf73b3accf4 Mon Sep 17 00:00:00 2001 From: Awiteb Date: Thu, 9 May 2024 13:28:06 +0300 Subject: [PATCH 3/4] chore: Create new utils for custom fields Is `get_duplicated_field` and `apply_custom_fields` utils --- src/utils.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/utils.rs b/src/utils.rs index 525b0ce..fc2a979 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use std::collections::BTreeMap; use std::{fs, path::PathBuf}; use inquire::{validator::Validation, Password, PasswordDisplayMode}; @@ -174,3 +175,33 @@ pub fn lprs_version() -> LprsResult> { } Ok(None) } + +/// Returns the duplicated field from the custom field (unprocessed fields) +pub fn get_duplicated_field(fields: &[(String, String)]) -> Option<&str> { + fields.iter().find_map(|(key, _)| { + if fields.iter().filter(|(k, _)| key == k).count() > 1 { + return Some(key.as_str()); + } + None + }) +} + +/// Apply the edited fields to the vault fields. +/// This will: +/// - Add the field if it's not in the fields map. +/// - Update the field if it's in the map. +/// - Remove the field if its value is empty string. +pub fn apply_custom_fields( + fields: &mut BTreeMap, + edited_fields: Vec<(String, String)>, +) { + for (key, value) in edited_fields { + if fields.contains_key(&key) && value.is_empty() { + fields.remove(&key); + } else { + // The field is not there or its value not empty, + // so add it or update its value + fields.insert(key, value); + } + } +} -- 2.45.2 From da568ab5e9414ef77831066eb9b09621c0fedaee Mon Sep 17 00:00:00 2001 From: Awiteb Date: Thu, 9 May 2024 13:28:30 +0300 Subject: [PATCH 4/4] feat: Support custom fields --- src/cli/add_command.rs | 18 +++++++++++-- src/cli/edit_command.rs | 59 ++++++++++++++++++++++++++--------------- src/vault/bitwarden.rs | 34 ++++++++++++++++++------ src/vault/mod.rs | 49 ++++++++++++++++++++++------------ 4 files changed, 111 insertions(+), 49 deletions(-) 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(()) } -- 2.45.2