feat: Support export and import with different password #30
6 changed files with 91 additions and 52 deletions
|
@ -15,9 +15,9 @@
|
||||||
// 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 clap::Args;
|
use clap::Args;
|
||||||
use inquire::{Password, PasswordDisplayMode};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
utils,
|
||||||
vault::{Vault, Vaults},
|
vault::{Vault, Vaults},
|
||||||
LprsCommand,
|
LprsCommand,
|
||||||
LprsError,
|
LprsError,
|
||||||
|
@ -32,31 +32,13 @@ pub struct Add {
|
||||||
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)]
|
||||||
// FIXME: I think replacing `Option<Option<String>>` with custom type will be better
|
|
||||||
#[allow(clippy::option_option)]
|
#[allow(clippy::option_option)]
|
||||||
password: Option<Option<String>>,
|
password: Option<Option<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<()> {
|
||||||
match self.password {
|
self.vault_info.password = utils::user_password(self.password, "Vault 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 => {}
|
|
||||||
};
|
|
||||||
|
|
||||||
vault_manager.add_vault(self.vault_info);
|
vault_manager.add_vault(self.vault_info);
|
||||||
vault_manager.try_export()
|
vault_manager.try_export()
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +17,9 @@
|
||||||
use std::num::NonZeroU64;
|
use std::num::NonZeroU64;
|
||||||
|
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
use inquire::{Password, PasswordDisplayMode};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
utils,
|
||||||
vault::{Vault, Vaults},
|
vault::{Vault, Vaults},
|
||||||
LprsCommand,
|
LprsCommand,
|
||||||
LprsError,
|
LprsError,
|
||||||
|
@ -41,7 +41,6 @@ pub struct Edit {
|
||||||
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
|
||||||
// FIXME: I think replacing `Option<Option<String>>` with custom type will be better
|
|
||||||
#[allow(clippy::option_option)]
|
#[allow(clippy::option_option)]
|
||||||
password: Option<Option<String>>,
|
password: Option<Option<String>>,
|
||||||
#[arg(short, long)]
|
#[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");
|
log::info!("Applying the new values to the vault");
|
||||||
*vault = Vault::new(
|
*vault = Vault::new(
|
||||||
self.name.as_ref().unwrap_or(&vault.name),
|
self.name.as_ref().unwrap_or(&vault.name),
|
||||||
self.username.as_ref().or(vault.username.as_ref()),
|
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.service.as_ref().or(vault.service.as_ref()),
|
||||||
self.note.as_ref().or(vault.note.as_ref()),
|
self.note.as_ref().or(vault.note.as_ref()),
|
||||||
);
|
);
|
||||||
|
|
|
@ -17,8 +17,10 @@
|
||||||
use std::{fs, io::Error as IoError, io::ErrorKind as IoErrorKind, path::PathBuf};
|
use std::{fs, io::Error as IoError, io::ErrorKind as IoErrorKind, path::PathBuf};
|
||||||
|
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
|
use sha2::Digest;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
utils,
|
||||||
vault::{BitWardenPasswords, Format, Vaults},
|
vault::{BitWardenPasswords, Format, Vaults},
|
||||||
LprsCommand,
|
LprsCommand,
|
||||||
LprsError,
|
LprsError,
|
||||||
|
@ -30,12 +32,17 @@ use crate::{
|
||||||
/// Export command, used to export the vaults in `lprs` format or `BitWarden`
|
/// Export command, used to export the vaults in `lprs` format or `BitWarden`
|
||||||
/// format. The exported file will be a json file.
|
/// format. The exported file will be a json file.
|
||||||
pub struct Export {
|
pub struct Export {
|
||||||
|
// TODO: `force` flag to write on existing file
|
||||||
/// The path to export to
|
/// The path to export to
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
/// Format to export vaults in
|
/// Format to export vaults in
|
||||||
#[arg(short, long, value_name = "FORMAT", default_value_t= Format::Lprs)]
|
#[arg(short, long, value_name = "FORMAT", default_value_t= Format::Lprs)]
|
||||||
format: Format,
|
format: Format,
|
||||||
// TODO: `force` flag to write on existing file
|
/// 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<Option<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LprsCommand for Export {
|
impl LprsCommand for Export {
|
||||||
|
@ -46,8 +53,19 @@ impl LprsCommand for Export {
|
||||||
self.path.display(),
|
self.path.display(),
|
||||||
self.format
|
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 {
|
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))?,
|
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()),
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,8 +22,10 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
|
use sha2::Digest;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
utils,
|
||||||
vault::{BitWardenPasswords, Format, Vault, Vaults},
|
vault::{BitWardenPasswords, Format, Vault, Vaults},
|
||||||
LprsCommand,
|
LprsCommand,
|
||||||
LprsError,
|
LprsError,
|
||||||
|
@ -41,6 +43,11 @@ pub struct Import {
|
||||||
/// The format to import from
|
/// The format to import from
|
||||||
#[arg(short, long, default_value_t = Format::Lprs)]
|
#[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<Option<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LprsCommand for Import {
|
impl LprsCommand for Import {
|
||||||
|
@ -52,10 +59,18 @@ impl LprsCommand for Import {
|
||||||
vault_manager.vaults_file.display()
|
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 {
|
let imported_passwords_len = match self.format {
|
||||||
Format::Lprs => {
|
Format::Lprs => {
|
||||||
let vaults =
|
let vaults = Vaults::json_reload(
|
||||||
Vaults::json_reload(&vault_manager.master_password, &fs::read(self.path)?)?;
|
decryption_key
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&vault_manager.master_password),
|
||||||
|
&fs::read(self.path)?,
|
||||||
|
)?;
|
||||||
let vaults_len = vaults.len();
|
let vaults_len = vaults.len();
|
||||||
|
|
||||||
vault_manager.vaults.extend(vaults);
|
vault_manager.vaults.extend(vaults);
|
||||||
|
@ -81,7 +96,7 @@ impl LprsCommand for Import {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_args(&self) -> LprsResult<()> {
|
fn validate_args(&self) -> LprsResult<()> {
|
||||||
if self
|
if !self
|
||||||
.path
|
.path
|
||||||
.extension()
|
.extension()
|
||||||
.is_some_and(|e| e.to_string_lossy().eq_ignore_ascii_case("json"))
|
.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()),
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
31
src/utils.rs
31
src/utils.rs
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
use std::{fs, path::PathBuf};
|
use std::{fs, path::PathBuf};
|
||||||
|
|
||||||
use inquire::{validator::Validation, PasswordDisplayMode};
|
use inquire::{validator::Validation, Password, PasswordDisplayMode};
|
||||||
use passwords::{analyzer, scorer};
|
use passwords::{analyzer, scorer};
|
||||||
#[cfg(feature = "update-notify")]
|
#[cfg(feature = "update-notify")]
|
||||||
use reqwest::blocking::Client as BlockingClient;
|
use reqwest::blocking::Client as BlockingClient;
|
||||||
|
@ -43,6 +43,35 @@ pub fn local_project_file(filename: &str) -> LprsResult<PathBuf> {
|
||||||
Ok(local_dir.join(filename))
|
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<Option<String>>,
|
||||||
|
prompt_message: &str,
|
||||||
|
) -> LprsResult<Option<String>> {
|
||||||
|
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
|
/// Returns the default vaults json file
|
||||||
///
|
///
|
||||||
/// ## Errors
|
/// ## Errors
|
||||||
|
|
|
@ -29,7 +29,7 @@ mod bitwarden;
|
||||||
|
|
||||||
pub use bitwarden::*;
|
pub use bitwarden::*;
|
||||||
|
|
||||||
#[derive(Clone, Debug, ValueEnum)]
|
#[derive(Clone, Debug, ValueEnum, Eq, PartialEq)]
|
||||||
/// The vaults format
|
/// The vaults format
|
||||||
pub enum Format {
|
pub enum Format {
|
||||||
/// The lprs format, which is the default format
|
/// The lprs format, which is the default format
|
||||||
|
@ -130,11 +130,9 @@ impl Vaults {
|
||||||
/// - if the encryption failed
|
/// - if the encryption failed
|
||||||
///
|
///
|
||||||
/// Note: The returned string is `Vec<Vault>`
|
/// Note: The returned string is `Vec<Vault>`
|
||||||
pub fn json_export(&self) -> LprsResult<String> {
|
pub fn json_export(&self, encryption_key: &[u8; 32]) -> LprsResult<String> {
|
||||||
let encrypt = |val: &str| {
|
let encrypt = |val: &str| {
|
||||||
LprsResult::Ok(
|
LprsResult::Ok(crate::BASE64.encode(cipher::encrypt(encryption_key, val.as_ref())))
|
||||||
crate::BASE64.encode(cipher::encrypt(&self.master_password, val.as_ref())),
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
serde_json::to_string(
|
serde_json::to_string(
|
||||||
|
|
Loading…
Reference in a new issue