diff --git a/src/cli/add_command.rs b/src/cli/add_command.rs index 02200b5..a14686a 100644 --- a/src/cli/add_command.rs +++ b/src/cli/add_command.rs @@ -15,9 +15,9 @@ // along with this program. If not, see . use clap::Args; -use inquire::{Password, PasswordDisplayMode}; use crate::{ + utils, vault::{Vault, Vaults}, LprsCommand, LprsError, @@ -32,31 +32,13 @@ pub struct Add { vault_info: Vault, /// The password, if there is no value for it you will prompt it #[arg(short, long)] - // FIXME: I think replacing `Option>` with custom type will be better #[allow(clippy::option_option)] password: Option>, } impl LprsCommand for Add { fn run(mut self, mut vault_manager: Vaults) -> LprsResult<()> { - match self.password { - Some(Some(password)) => { - log::debug!("User provided a password"); - self.vault_info.password = Some(password); - } - Some(None) => { - log::debug!("User didn't provide a password, prompting it"); - self.vault_info.password = Some( - Password::new("Vault password:") - .without_confirmation() - .with_formatter(&|p| "*".repeat(p.chars().count())) - .with_display_mode(PasswordDisplayMode::Masked) - .prompt()?, - ); - } - None => {} - }; - + self.vault_info.password = utils::user_password(self.password, "Vault password:")?; vault_manager.add_vault(self.vault_info); vault_manager.try_export() } diff --git a/src/cli/edit_command.rs b/src/cli/edit_command.rs index 33e0d6b..2069d82 100644 --- a/src/cli/edit_command.rs +++ b/src/cli/edit_command.rs @@ -17,9 +17,9 @@ use std::num::NonZeroU64; use clap::Args; -use inquire::{Password, PasswordDisplayMode}; use crate::{ + utils, vault::{Vault, Vaults}, LprsCommand, LprsError, @@ -41,7 +41,6 @@ pub struct Edit { username: Option, #[arg(short, long)] /// The new password, if there is no value for it you will prompt it - // FIXME: I think replacing `Option>` with custom type will be better #[allow(clippy::option_option)] password: Option>, #[arg(short, long)] @@ -65,26 +64,13 @@ impl LprsCommand for Edit { ))); }; - // Get the password from stdin or from its value if provided - let password = match self.password { - Some(Some(password)) => Some(password), - Some(None) => { - Some( - Password::new("New vault password:") - .without_confirmation() - .with_formatter(&|p| "*".repeat(p.chars().count())) - .with_display_mode(PasswordDisplayMode::Masked) - .prompt()?, - ) - } - None => None, - }; - 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()), - password.as_ref().or(vault.password.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()), ); diff --git a/src/cli/export_command.rs b/src/cli/export_command.rs index d488a97..78c436d 100644 --- a/src/cli/export_command.rs +++ b/src/cli/export_command.rs @@ -17,8 +17,10 @@ use std::{fs, io::Error as IoError, io::ErrorKind as IoErrorKind, path::PathBuf}; use clap::Args; +use sha2::Digest; use crate::{ + utils, vault::{BitWardenPasswords, Format, Vaults}, LprsCommand, LprsError, @@ -30,12 +32,17 @@ use crate::{ /// Export command, used to export the vaults in `lprs` format or `BitWarden` /// format. The exported file will be a json file. pub struct Export { + // TODO: `force` flag to write on existing file /// The path to export to - path: PathBuf, + path: PathBuf, /// Format to export vaults in #[arg(short, long, value_name = "FORMAT", default_value_t= Format::Lprs)] - format: Format, - // TODO: `force` flag to write on existing file + format: Format, + /// Encryption password of the exported vaults (in `lprs` format) + /// if there is not, will use the master password + #[arg(short = 'p', long)] + #[allow(clippy::option_option)] + encryption_password: Option>, } impl LprsCommand for Export { @@ -46,8 +53,19 @@ impl LprsCommand for Export { self.path.display(), self.format ); + + let encryption_key: Option<[u8; 32]> = + utils::user_password(self.encryption_password, "Encryption Password:")? + .map(|p| sha2::Sha256::digest(p).into()); + let exported_data = match self.format { - Format::Lprs => vault_manager.json_export()?, + Format::Lprs => { + vault_manager.json_export( + encryption_key + .as_ref() + .unwrap_or(&vault_manager.master_password), + )? + } Format::BitWarden => serde_json::to_string(&BitWardenPasswords::from(vault_manager))?, }; @@ -77,6 +95,11 @@ impl LprsCommand for Export { format!("file `{}` is a directory", self.path.display()), ))); } + if self.encryption_password.is_some() && self.format != Format::Lprs { + return Err(LprsError::Other( + "You only can to use the encryption password with `lprs` format".to_owned(), + )); + } Ok(()) } diff --git a/src/cli/import_command.rs b/src/cli/import_command.rs index 700a8a1..977b677 100644 --- a/src/cli/import_command.rs +++ b/src/cli/import_command.rs @@ -22,8 +22,10 @@ use std::{ }; use clap::Args; +use sha2::Digest; use crate::{ + utils, vault::{BitWardenPasswords, Format, Vault, Vaults}, LprsCommand, LprsError, @@ -40,7 +42,12 @@ pub struct Import { /// The format to import from #[arg(short, long, default_value_t = Format::Lprs)] - format: Format, + format: Format, + /// Decryption password of the imported vaults (in `lprs` format) + /// if there is not, will use the master password + #[arg(short = 'p', long)] + #[allow(clippy::option_option)] + decryption_password: Option>, } impl LprsCommand for Import { @@ -52,10 +59,18 @@ impl LprsCommand for Import { vault_manager.vaults_file.display() ); + let decryption_key: Option<[u8; 32]> = + utils::user_password(self.decryption_password, "Decryption password:")? + .map(|p| sha2::Sha256::digest(p).into()); + let imported_passwords_len = match self.format { Format::Lprs => { - let vaults = - Vaults::json_reload(&vault_manager.master_password, &fs::read(self.path)?)?; + let vaults = Vaults::json_reload( + decryption_key + .as_ref() + .unwrap_or(&vault_manager.master_password), + &fs::read(self.path)?, + )?; let vaults_len = vaults.len(); vault_manager.vaults.extend(vaults); @@ -81,7 +96,7 @@ impl LprsCommand for Import { } fn validate_args(&self) -> LprsResult<()> { - if self + if !self .path .extension() .is_some_and(|e| e.to_string_lossy().eq_ignore_ascii_case("json")) @@ -103,6 +118,12 @@ impl LprsCommand for Import { format!("file `{}` is a directory", self.path.display()), ))); } + if self.decryption_password.is_some() && self.format != Format::Lprs { + return Err(LprsError::Other( + "You only can to use the decryption password with `lprs` format".to_owned(), + )); + } + Ok(()) } diff --git a/src/utils.rs b/src/utils.rs index 860d797..525b0ce 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -16,7 +16,7 @@ use std::{fs, path::PathBuf}; -use inquire::{validator::Validation, PasswordDisplayMode}; +use inquire::{validator::Validation, Password, PasswordDisplayMode}; use passwords::{analyzer, scorer}; #[cfg(feature = "update-notify")] use reqwest::blocking::Client as BlockingClient; @@ -43,6 +43,35 @@ pub fn local_project_file(filename: &str) -> LprsResult { Ok(local_dir.join(filename)) } +/// Returns the user password if any +/// +/// - If the `password` is `None` will return `None` +/// - If the `password` is `Some(None)` will ask the user for a password in the +/// stdin and return it +/// - If the `password` is `Some(Some(password))` will return `Some(password)` +/// +/// ## Errors +/// - When failed to get the password from stdin +pub fn user_password( + password: Option>, + prompt_message: &str, +) -> LprsResult> { + Ok(match password { + None => None, + Some(Some(p)) => Some(p), + Some(None) => { + log::debug!("User didn't provide a password, prompting it"); + Some( + Password::new(prompt_message) + .without_confirmation() + .with_formatter(&|p| "*".repeat(p.chars().count())) + .with_display_mode(PasswordDisplayMode::Masked) + .prompt()?, + ) + } + }) +} + /// Returns the default vaults json file /// /// ## Errors diff --git a/src/vault/mod.rs b/src/vault/mod.rs index 9e995e6..47f6468 100644 --- a/src/vault/mod.rs +++ b/src/vault/mod.rs @@ -29,7 +29,7 @@ mod bitwarden; pub use bitwarden::*; -#[derive(Clone, Debug, ValueEnum)] +#[derive(Clone, Debug, ValueEnum, Eq, PartialEq)] /// The vaults format pub enum Format { /// The lprs format, which is the default format @@ -130,11 +130,9 @@ impl Vaults { /// - if the encryption failed /// /// Note: The returned string is `Vec` - pub fn json_export(&self) -> LprsResult { + pub fn json_export(&self, encryption_key: &[u8; 32]) -> LprsResult { let encrypt = |val: &str| { - LprsResult::Ok( - crate::BASE64.encode(cipher::encrypt(&self.master_password, val.as_ref())), - ) + LprsResult::Ok(crate::BASE64.encode(cipher::encrypt(encryption_key, val.as_ref()))) }; serde_json::to_string(