feat: Support custom fields
All checks were successful
Rust CI / Rust CI (pull_request) Successful in 2m22s
Write changelog / write-changelog (push) Successful in 3s
Rust CI / Rust CI (push) Successful in 1m59s

This commit is contained in:
Awiteb 2024-05-09 13:28:30 +03:00
parent 0b020a1cb6
commit da568ab5e9
Signed by: awiteb
GPG key ID: 3F6B55640AA6682F
4 changed files with 111 additions and 49 deletions

View file

@ -17,6 +17,7 @@
use clap::Args; use clap::Args;
use crate::{ use crate::{
clap_parsers,
utils, utils,
vault::{Vault, Vaults}, vault::{Vault, Vaults},
LprsCommand, LprsCommand,
@ -29,16 +30,21 @@ use crate::{
/// Add command, used to add new vault to the vaults file /// Add command, used to add new vault to the vaults file
pub struct Add { pub struct Add {
#[command(flatten)] #[command(flatten)]
vault_info: Vault, vault_info: Vault,
/// The password, if there is no value for it you will prompt it /// The password, if there is no value for it you will prompt it
#[arg(short, long)] #[arg(short, long)]
#[allow(clippy::option_option)] #[allow(clippy::option_option)]
password: Option<Option<String>>, password: Option<Option<String>>,
/// 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 { impl LprsCommand for Add {
fn run(mut self, mut vault_manager: Vaults) -> LprsResult<()> { fn run(mut self, mut vault_manager: Vaults) -> LprsResult<()> {
self.vault_info.password = utils::user_password(self.password, "Vault password:")?; 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.add_vault(self.vault_info);
vault_manager.try_export() vault_manager.try_export()
} }
@ -48,9 +54,17 @@ impl LprsCommand for Add {
&& self.password.is_none() && self.password.is_none()
&& self.vault_info.service.is_none() && self.vault_info.service.is_none()
&& self.vault_info.note.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())); 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(()) Ok(())
} }
} }

View file

@ -18,37 +18,37 @@ use std::num::NonZeroU64;
use clap::Args; use clap::Args;
use crate::{ use crate::{clap_parsers, utils, vault::Vaults, LprsCommand, LprsError, LprsResult};
utils,
vault::{Vault, Vaults},
LprsCommand,
LprsError,
LprsResult,
};
#[derive(Debug, Args)] #[derive(Debug, Args)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
/// Edit command, used to edit the vault content /// Edit command, used to edit the vault content
pub struct Edit { pub struct Edit {
/// The password index. Check it from list command /// The password index. You can get it from the list command
index: NonZeroU64, index: NonZeroU64,
#[arg(short, long)] #[arg(short, long)]
/// The new vault name /// The new vault name
name: Option<String>, name: Option<String>,
#[arg(short, long)] #[arg(short, long)]
/// The new vault username /// The new vault username
username: Option<String>, username: Option<String>,
#[arg(short, long)] #[arg(short, long)]
/// The new password, if there is no value for it you will prompt it /// The new password, if there is no value for it you will prompt it
#[allow(clippy::option_option)] #[allow(clippy::option_option)]
password: Option<Option<String>>, password: Option<Option<String>>,
#[arg(short, long)] #[arg(short, long)]
/// The new vault service /// The new vault service
service: Option<String>, service: Option<String>,
#[arg(short = 'o', long)] #[arg(short = 'o', long)]
/// The new vault note /// The new vault note
note: Option<String>, note: Option<String>,
/// 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 { impl LprsCommand for Edit {
@ -65,15 +65,23 @@ impl LprsCommand for Edit {
}; };
log::info!("Applying the new values to the vault"); log::info!("Applying the new values to the vault");
*vault = Vault::new( if let Some(new_name) = self.name {
self.name.as_ref().unwrap_or(&vault.name), vault.name = new_name;
self.username.as_ref().or(vault.username.as_ref()), }
utils::user_password(self.password, "New vault password:")? if self.password.is_some() {
.as_ref() vault.password = utils::user_password(self.password, "New vault password:")?;
.or(vault.password.as_ref()), }
self.service.as_ref().or(vault.service.as_ref()), if let Some(new_username) = self.username {
self.note.as_ref().or(vault.note.as_ref()), 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() vault_manager.try_export()
} }
@ -83,11 +91,18 @@ impl LprsCommand for Edit {
&& self.password.is_none() && self.password.is_none()
&& self.service.is_none() && self.service.is_none()
&& self.note.is_none() && self.note.is_none()
&& self.custom_fields.is_empty()
{ {
return Err(LprsError::Other( return Err(LprsError::Other(
"You must edit one option at least".to_owned(), "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(()) Ok(())
} }
} }

View file

@ -42,13 +42,21 @@ pub struct BitWardenFolder {
pub name: String, pub name: String,
} }
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct BitWardenNameValue {
pub name: String,
pub value: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct BitWardenPassword { pub struct BitWardenPassword {
#[serde(rename = "type")] #[serde(rename = "type")]
pub ty: i32, pub ty: i32,
pub name: String, pub name: String,
pub login: Option<BitWardenLoginData>, pub login: Option<BitWardenLoginData>,
pub notes: Option<String>, pub notes: Option<String>,
#[serde(default)]
pub fields: Vec<BitWardenNameValue>,
} }
/// The bitwarden password struct /// The bitwarden password struct
@ -71,6 +79,11 @@ impl From<BitWardenPassword> for Vault {
.and_then(|p| p.first().map(|u| u.uri.clone())) .and_then(|p| p.first().map(|u| u.uri.clone()))
}), }),
value.notes, value.notes,
value
.fields
.into_iter()
.map(|nv| (nv.name, nv.value))
.collect(),
) )
} }
} }
@ -78,16 +91,21 @@ impl From<BitWardenPassword> for Vault {
impl From<Vault> for BitWardenPassword { impl From<Vault> for BitWardenPassword {
fn from(value: Vault) -> Self { fn from(value: Vault) -> Self {
Self { Self {
ty: 1, ty: 1,
name: value.name, name: value.name,
login: Some(BitWardenLoginData { login: Some(BitWardenLoginData {
username: value.username, username: value.username,
password: value.password, password: value.password,
uris: value uris: value
.service .service
.map(|s| vec![BitWardenUri { mt: None, uri: s }]), .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(),
} }
} }
} }

View file

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>. // along with this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
use std::{fmt, fs, path::PathBuf}; use std::{collections::BTreeMap, fmt, fs, path::PathBuf};
use base64::Engine; use base64::Engine;
use clap::{Parser, ValueEnum}; use clap::{Parser, ValueEnum};
@ -45,19 +45,22 @@ pub enum Format {
#[derive(Clone, Debug, Deserialize, Serialize, Parser)] #[derive(Clone, Debug, Deserialize, Serialize, Parser)]
pub struct Vault { pub struct Vault {
/// The name of the vault /// The name of the vault
pub name: String, pub name: String,
/// The username /// The username
#[arg(short, long)] #[arg(short, long)]
pub username: Option<String>, pub username: Option<String>,
/// The password /// The password
#[arg(skip)] #[arg(skip)]
pub password: Option<String>, pub password: Option<String>,
/// The service name. e.g the website url /// The service name. e.g the website url
#[arg(short, long)] #[arg(short, long)]
pub service: Option<String>, pub service: Option<String>,
/// Add a note to the vault /// Add a note to the vault
#[arg(short, long)] #[arg(short, long)]
pub note: Option<String>, pub note: Option<String>,
/// The vault custom fields
#[arg(skip)]
pub custom_fields: BTreeMap<String, String>,
} }
/// The vaults manager /// The vaults manager
@ -79,13 +82,15 @@ impl Vault {
password: Option<impl Into<String>>, password: Option<impl Into<String>>,
service: Option<impl Into<String>>, service: Option<impl Into<String>>,
note: Option<impl Into<String>>, note: Option<impl Into<String>>,
custom_fields: BTreeMap<String, String>,
) -> Self { ) -> Self {
Self { Self {
name: name.into(), name: name.into(),
username: username.map(Into::into), username: username.map(Into::into),
password: password.map(Into::into), password: password.map(Into::into),
service: service.map(Into::into), service: service.map(Into::into),
note: note.map(Into::into), note: note.map(Into::into),
custom_fields,
} }
} }
@ -131,9 +136,8 @@ impl Vaults {
/// ///
/// Note: The returned string is `Vec<Vault>` /// Note: The returned string is `Vec<Vault>`
pub fn json_export(&self, encryption_key: &[u8; 32]) -> LprsResult<String> { pub fn json_export(&self, encryption_key: &[u8; 32]) -> LprsResult<String> {
let encrypt = |val: &str| { let encrypt =
LprsResult::Ok(crate::BASE64.encode(cipher::encrypt(encryption_key, val.as_ref()))) |val: &str| crate::BASE64.encode(cipher::encrypt(encryption_key, val.as_ref()));
};
serde_json::to_string( serde_json::to_string(
&self &self
@ -141,11 +145,15 @@ impl Vaults {
.iter() .iter()
.map(|v| { .map(|v| {
LprsResult::Ok(Vault::new( LprsResult::Ok(Vault::new(
encrypt(&v.name)?, encrypt(&v.name),
v.username.as_ref().and_then(|u| encrypt(u).ok()), v.username.as_ref().map(|u| encrypt(u)),
v.password.as_ref().and_then(|p| encrypt(p).ok()), v.password.as_ref().map(|p| encrypt(p)),
v.service.as_ref().and_then(|s| encrypt(s).ok()), v.service.as_ref().map(|s| encrypt(s)),
v.note.as_ref().and_then(|n| encrypt(n).ok()), v.note.as_ref().map(|n| encrypt(n)),
v.custom_fields
.iter()
.map(|(key, value)| (encrypt(key), encrypt(value)))
.collect(),
)) ))
}) })
.collect::<LprsResult<Vec<_>>>()?, .collect::<LprsResult<Vec<_>>>()?,
@ -178,6 +186,10 @@ impl Vaults {
v.password.as_ref().and_then(|p| decrypt(p).ok()), v.password.as_ref().and_then(|p| decrypt(p).ok()),
v.service.as_ref().and_then(|s| decrypt(s).ok()), v.service.as_ref().and_then(|s| decrypt(s).ok()),
v.note.as_ref().and_then(|n| decrypt(n).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::<LprsResult<_>>()?,
)) ))
}) })
.collect() .collect()
@ -245,6 +257,9 @@ impl fmt::Display for Vault {
if let Some(ref note) = self.note { if let Some(ref note) = self.note {
write!(f, "\nNote:\n{note}")?; write!(f, "\nNote:\n{note}")?;
} }
for (key, value) in &self.custom_fields {
write!(f, "\n{key}: {value}")?;
}
Ok(()) Ok(())
} }