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(())
}