chore(DX): Improve the DX #27
16 changed files with 182 additions and 67 deletions
|
@ -15,6 +15,7 @@
|
||||||
// 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::{
|
||||||
vault::{Vault, Vaults},
|
vault::{Vault, Vaults},
|
||||||
|
@ -23,11 +24,14 @@ use crate::{
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
#[derive(Debug, Args)]
|
||||||
#[command(author, version, about, long_about = None)]
|
#[command(author, version, about, long_about = None)]
|
||||||
|
/// 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)]
|
||||||
|
// FIXME: I think replacing `Option<Option<String>>` with custom type will be better
|
||||||
|
#[allow(clippy::option_option)]
|
||||||
password: Option<Option<String>>,
|
password: Option<Option<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,10 +45,10 @@ impl LprsCommand for Add {
|
||||||
Some(None) => {
|
Some(None) => {
|
||||||
log::debug!("User didn't provide a password, prompting it");
|
log::debug!("User didn't provide a password, prompting it");
|
||||||
self.vault_info.password = Some(
|
self.vault_info.password = Some(
|
||||||
inquire::Password::new("Vault password:")
|
Password::new("Vault password:")
|
||||||
.without_confirmation()
|
.without_confirmation()
|
||||||
.with_formatter(&|p| "*".repeat(p.chars().count()))
|
.with_formatter(&|p| "*".repeat(p.chars().count()))
|
||||||
.with_display_mode(inquire::PasswordDisplayMode::Masked)
|
.with_display_mode(PasswordDisplayMode::Masked)
|
||||||
.prompt()?,
|
.prompt()?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,8 @@ use crate::{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)]
|
||||||
pub struct Clean {}
|
/// Clean command, used to clean the vaults file (remove all vaults)
|
||||||
|
pub struct Clean;
|
||||||
|
|
||||||
impl LprsCommand for Clean {
|
impl LprsCommand for Clean {
|
||||||
fn run(self, vault_manager: Vaults) -> LprsResult<()> {
|
fn run(self, vault_manager: Vaults) -> LprsResult<()> {
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
use std::num::NonZeroU64;
|
use std::num::NonZeroU64;
|
||||||
|
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
|
use inquire::{Password, PasswordDisplayMode};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
vault::{Vault, Vaults},
|
vault::{Vault, Vaults},
|
||||||
|
@ -25,6 +26,7 @@ use crate::{
|
||||||
|
|
||||||
#[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
|
||||||
pub struct Edit {
|
pub struct Edit {
|
||||||
/// The password index. Check it from list command
|
/// The password index. Check it from list command
|
||||||
index: NonZeroU64,
|
index: NonZeroU64,
|
||||||
|
@ -37,6 +39,8 @@ 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)]
|
||||||
password: Option<Option<String>>,
|
password: Option<Option<String>>,
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
/// The new vault service
|
/// The new vault service
|
||||||
|
@ -63,10 +67,10 @@ impl LprsCommand for Edit {
|
||||||
let password = match self.password {
|
let password = match self.password {
|
||||||
Some(Some(password)) => Some(password),
|
Some(Some(password)) => Some(password),
|
||||||
Some(None) => Some(
|
Some(None) => Some(
|
||||||
inquire::Password::new("New vault password:")
|
Password::new("New vault password:")
|
||||||
.without_confirmation()
|
.without_confirmation()
|
||||||
.with_formatter(&|p| "*".repeat(p.chars().count()))
|
.with_formatter(&|p| "*".repeat(p.chars().count()))
|
||||||
.with_display_mode(inquire::PasswordDisplayMode::Masked)
|
.with_display_mode(PasswordDisplayMode::Masked)
|
||||||
.prompt()?,
|
.prompt()?,
|
||||||
),
|
),
|
||||||
None => None,
|
None => None,
|
||||||
|
|
|
@ -25,6 +25,8 @@ use crate::{
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
#[derive(Debug, Args)]
|
||||||
#[command(author, version, about, long_about = None)]
|
#[command(author, version, about, long_about = None)]
|
||||||
|
/// Export command, used to export the vaults in `lprs` format or `BitWarden` format.
|
||||||
|
/// The exported file will be a json file.
|
||||||
pub struct Export {
|
pub struct Export {
|
||||||
/// The path to export to
|
/// The path to export to
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
|
@ -51,24 +53,29 @@ impl LprsCommand for Export {
|
||||||
}
|
}
|
||||||
|
|
||||||
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"))
|
||||||
{
|
{
|
||||||
if !self.path.exists() {
|
return Err(LprsError::Io(IoError::new(
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(LprsError::Io(IoError::new(
|
|
||||||
IoErrorKind::AlreadyExists,
|
|
||||||
format!("file `{}` is already exists", self.path.display()),
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(LprsError::Io(IoError::new(
|
|
||||||
IoErrorKind::InvalidInput,
|
IoErrorKind::InvalidInput,
|
||||||
format!("file `{}` is not a json file", self.path.display()),
|
format!("file `{}` is not a json file", self.path.display()),
|
||||||
)))
|
)));
|
||||||
}
|
}
|
||||||
|
if self.path.exists() {
|
||||||
|
return Err(LprsError::Io(IoError::new(
|
||||||
|
IoErrorKind::AlreadyExists,
|
||||||
|
format!("file `{}` is already exists", self.path.display()),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if self.path.is_dir() {
|
||||||
|
return Err(LprsError::Io(IoError::new(
|
||||||
|
IoErrorKind::InvalidInput,
|
||||||
|
format!("file `{}` is a directory", self.path.display()),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ use crate::{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)]
|
||||||
|
/// Generate command, used to generate a password
|
||||||
pub struct Gen {
|
pub struct Gen {
|
||||||
/// The password length
|
/// The password length
|
||||||
#[arg(default_value_t = NonZeroU64::new(18).unwrap())]
|
#[arg(default_value_t = NonZeroU64::new(18).unwrap())]
|
||||||
|
|
|
@ -30,6 +30,7 @@ use crate::{
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
#[derive(Debug, Args)]
|
||||||
#[command(author, version, about, long_about = None)]
|
#[command(author, version, about, long_about = None)]
|
||||||
|
/// Import command, used to import vaults from the exported files, `lprs` or `BitWarden`
|
||||||
pub struct Import {
|
pub struct Import {
|
||||||
/// The file path to import from
|
/// The file path to import from
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
|
@ -77,24 +78,29 @@ impl LprsCommand for Import {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_args(&self) -> LprsResult<()> {
|
fn validate_args(&self) -> LprsResult<()> {
|
||||||
if self.path.exists() {
|
|
||||||
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"))
|
||||||
{
|
{
|
||||||
Ok(())
|
return Err(LprsError::Io(IoError::new(
|
||||||
} else {
|
|
||||||
Err(LprsError::Io(IoError::new(
|
|
||||||
IoErrorKind::InvalidInput,
|
IoErrorKind::InvalidInput,
|
||||||
format!("file `{}` is not a json file", self.path.display()),
|
format!("file `{}` is not a json file", self.path.display()),
|
||||||
)))
|
)));
|
||||||
}
|
}
|
||||||
} else {
|
if !self.path.exists() {
|
||||||
Err(LprsError::Io(IoError::new(
|
return Err(LprsError::Io(IoError::new(
|
||||||
IoErrorKind::NotFound,
|
IoErrorKind::NotFound,
|
||||||
format!("file `{}` not found", self.path.display()),
|
format!("file `{}` not found", self.path.display()),
|
||||||
)))
|
)));
|
||||||
}
|
}
|
||||||
|
if self.path.is_dir() {
|
||||||
|
return Err(LprsError::Io(IoError::new(
|
||||||
|
IoErrorKind::InvalidInput,
|
||||||
|
format!("file `{}` is a directory", self.path.display()),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ use crate::{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)]
|
||||||
|
/// List command, used to list the vaults and search
|
||||||
pub struct List {
|
pub struct List {
|
||||||
/// Return the password with spesifc index
|
/// Return the password with spesifc index
|
||||||
#[arg(short, long, value_name = "INDEX")]
|
#[arg(short, long, value_name = "INDEX")]
|
||||||
|
|
|
@ -20,13 +20,22 @@ use clap::Parser;
|
||||||
|
|
||||||
use crate::{impl_commands, utils, vault::Vaults, LprsCommand, LprsResult};
|
use crate::{impl_commands, utils, vault::Vaults, LprsCommand, LprsResult};
|
||||||
|
|
||||||
|
/// Add command, used to add new vault to the vaults file
|
||||||
pub mod add_command;
|
pub mod add_command;
|
||||||
|
/// Clean command, used to clean the vaults file (remove all vaults)
|
||||||
pub mod clean_command;
|
pub mod clean_command;
|
||||||
|
/// Edit command, used to edit the vault content
|
||||||
pub mod edit_command;
|
pub mod edit_command;
|
||||||
|
/// Export command, used to export the vaults
|
||||||
|
/// in `lprs` format or `BitWarden` format
|
||||||
pub mod export_command;
|
pub mod export_command;
|
||||||
|
/// Generate command, used to generate a password
|
||||||
pub mod gen_command;
|
pub mod gen_command;
|
||||||
|
/// Import command, used to import vaults from the exported files, `lprs` or `BitWarden`
|
||||||
pub mod import_command;
|
pub mod import_command;
|
||||||
|
/// List command, used to list the vaults and search
|
||||||
pub mod list_command;
|
pub mod list_command;
|
||||||
|
/// Remove command, used to remove vault from the vaults file
|
||||||
pub mod remove_command;
|
pub mod remove_command;
|
||||||
|
|
||||||
/// The lprs commands
|
/// The lprs commands
|
||||||
|
@ -56,6 +65,7 @@ impl_commands!(Commands, Add Remove List Clean Edit Gen Export Import);
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(author, version, about, long_about = None)]
|
#[command(author, version, about, long_about = None)]
|
||||||
|
/// The lprs cli, manage the CLI arguments and run the commands
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
/// The vaults json file
|
/// The vaults json file
|
||||||
#[arg(short = 'f', long)]
|
#[arg(short = 'f', long)]
|
||||||
|
@ -65,11 +75,19 @@ pub struct Cli {
|
||||||
pub verbose: bool,
|
pub verbose: bool,
|
||||||
|
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
|
/// The provided command to run
|
||||||
pub command: Commands,
|
pub command: Commands,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cli {
|
impl Cli {
|
||||||
/// Run the cli
|
/// Run the cli
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// - If can't get the default vaults file
|
||||||
|
/// - If the vaults file can't be created
|
||||||
|
/// - If the user provide a worng CLI arguments
|
||||||
|
/// - If failed to write in the vaults file
|
||||||
|
/// - (errors from the commands)
|
||||||
pub fn run(self) -> LprsResult<()> {
|
pub fn run(self) -> LprsResult<()> {
|
||||||
let vaults_file = if let Some(path) = self.vaults_file {
|
let vaults_file = if let Some(path) = self.vaults_file {
|
||||||
log::info!("Using the given vaults file");
|
log::info!("Using the given vaults file");
|
||||||
|
@ -83,7 +101,7 @@ impl Cli {
|
||||||
path
|
path
|
||||||
} else {
|
} else {
|
||||||
log::info!("Using the default vaults file");
|
log::info!("Using the default vaults file");
|
||||||
crate::utils::vaults_file()?
|
utils::vaults_file()?
|
||||||
};
|
};
|
||||||
log::debug!("Vaults file: {}", vaults_file.display());
|
log::debug!("Vaults file: {}", vaults_file.display());
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ use crate::{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)]
|
||||||
|
/// Remove command, used to remove a vault from the vaults file
|
||||||
pub struct Remove {
|
pub struct Remove {
|
||||||
/// The password index
|
/// The password index
|
||||||
index: NonZeroU64,
|
index: NonZeroU64,
|
||||||
|
@ -37,14 +38,14 @@ impl LprsCommand for Remove {
|
||||||
log::debug!("Removing vault at index: {index}");
|
log::debug!("Removing vault at index: {index}");
|
||||||
|
|
||||||
if index > vault_manager.vaults.len() {
|
if index > vault_manager.vaults.len() {
|
||||||
if !self.force {
|
if self.force {
|
||||||
return Err(LprsError::Other(
|
|
||||||
"The index is greater than the passwords counts".to_owned(),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
log::error!(
|
log::error!(
|
||||||
"The index is greater than the passwords counts, but the force flag is enabled"
|
"The index is greater than the passwords counts, but the force flag is enabled"
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
return Err(LprsError::Other(
|
||||||
|
"The index is greater than the passwords counts".to_owned(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
vault_manager.vaults.remove(index);
|
vault_manager.vaults.remove(index);
|
||||||
|
|
|
@ -14,11 +14,13 @@
|
||||||
// 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::{process::ExitCode, string::FromUtf8Error};
|
use std::{io, process::ExitCode, result, string::FromUtf8Error};
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
/// The result type used in the whole project
|
||||||
|
pub type Result<T> = result::Result<T, Error>;
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
#[allow(missing_docs)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("Encryption Error: {0}")]
|
#[error("Encryption Error: {0}")]
|
||||||
Encryption(String),
|
Encryption(String),
|
||||||
|
@ -50,12 +52,12 @@ pub enum Error {
|
||||||
#[error("Project Folder Error: {0}")]
|
#[error("Project Folder Error: {0}")]
|
||||||
ProjectDir(String),
|
ProjectDir(String),
|
||||||
#[error("IO Error: {0}")]
|
#[error("IO Error: {0}")]
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
/// Return the exit code of the error
|
/// Return the exit code of the error
|
||||||
pub fn exit_code(&self) -> ExitCode {
|
pub const fn exit_code(&self) -> ExitCode {
|
||||||
// TODO: Exit with more specific exit code
|
// TODO: Exit with more specific exit code
|
||||||
ExitCode::FAILURE
|
ExitCode::FAILURE
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,6 @@
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
|
|
||||||
/// ```
|
/// ```
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! impl_commands {
|
macro_rules! impl_commands {
|
||||||
|
|
11
src/main.rs
11
src/main.rs
|
@ -13,14 +13,20 @@
|
||||||
//
|
//
|
||||||
// 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>.
|
||||||
|
#![doc = include_str!("../README.md")]
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use inquire::InquireError;
|
use inquire::InquireError;
|
||||||
|
use std::env;
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
|
/// The main module of the lprs crate, contains the cli and the commands.
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
|
/// The errors module, contains the errors and the result type.
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
|
/// The utils module, contains the utility functions of all the modules.
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
/// The vault module, contains the vault struct and the vaults manager.
|
||||||
pub mod vault;
|
pub mod vault;
|
||||||
|
|
||||||
mod macros;
|
mod macros;
|
||||||
|
@ -30,17 +36,20 @@ pub use base64::engine::general_purpose::STANDARD as BASE64;
|
||||||
pub use errors::{Error as LprsError, Result as LprsResult};
|
pub use errors::{Error as LprsError, Result as LprsResult};
|
||||||
pub use traits::*;
|
pub use traits::*;
|
||||||
|
|
||||||
|
/// The default vaults file name. Used to store the vaults.
|
||||||
pub const DEFAULT_VAULTS_FILE: &str = "vaults.lprs";
|
pub const DEFAULT_VAULTS_FILE: &str = "vaults.lprs";
|
||||||
|
|
||||||
#[cfg(feature = "update-notify")]
|
#[cfg(feature = "update-notify")]
|
||||||
|
/// The version of the lprs crate.
|
||||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
#[cfg(feature = "update-notify")]
|
#[cfg(feature = "update-notify")]
|
||||||
|
/// The last version check file. Used to store the last version check time.
|
||||||
pub const LAST_VERSION_CHECK_FILE: &str = ".last_version_check";
|
pub const LAST_VERSION_CHECK_FILE: &str = ".last_version_check";
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
let lprs_cli = cli::Cli::parse();
|
let lprs_cli = cli::Cli::parse();
|
||||||
if lprs_cli.verbose {
|
if lprs_cli.verbose {
|
||||||
std::env::set_var("RUST_LOG", "lprs");
|
env::set_var("RUST_LOG", "lprs");
|
||||||
}
|
}
|
||||||
pretty_env_logger::init();
|
pretty_env_logger::init();
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
use crate::{vault::Vaults, LprsResult};
|
use crate::{vault::Vaults, LprsResult};
|
||||||
|
|
||||||
/// Trait to work with the commands
|
/// Trait to work with the commands
|
||||||
|
#[allow(clippy::missing_errors_doc)]
|
||||||
pub trait LprsCommand {
|
pub trait LprsCommand {
|
||||||
/// Run the command, should do all the logic, even the export
|
/// Run the command, should do all the logic, even the export
|
||||||
fn run(self, vault_manager: Vaults) -> LprsResult<()>;
|
fn run(self, vault_manager: Vaults) -> LprsResult<()>;
|
||||||
|
|
52
src/utils.rs
52
src/utils.rs
|
@ -16,12 +16,20 @@
|
||||||
|
|
||||||
use std::{fs, path::PathBuf};
|
use std::{fs, path::PathBuf};
|
||||||
|
|
||||||
use inquire::validator::Validation;
|
use inquire::{validator::Validation, PasswordDisplayMode};
|
||||||
|
use passwords::{analyzer, scorer};
|
||||||
use sha2::Digest;
|
use sha2::Digest;
|
||||||
|
|
||||||
|
#[cfg(feature = "update-notify")]
|
||||||
|
use reqwest::blocking::Client as BlockingClient;
|
||||||
|
|
||||||
use crate::{LprsError, LprsResult};
|
use crate::{LprsError, LprsResult};
|
||||||
|
|
||||||
/// Returns the local project dir joined with the given file name
|
/// Returns the local project dir joined with the given file name
|
||||||
|
///
|
||||||
|
/// ## Errors
|
||||||
|
/// - If the project dir can't be extracted from the OS
|
||||||
|
/// - If the local project dir can't be created
|
||||||
pub fn local_project_file(filename: &str) -> LprsResult<PathBuf> {
|
pub fn local_project_file(filename: &str) -> LprsResult<PathBuf> {
|
||||||
let local_dir = directories::ProjectDirs::from("", "", "lprs")
|
let local_dir = directories::ProjectDirs::from("", "", "lprs")
|
||||||
.map(|d| d.data_local_dir().to_path_buf())
|
.map(|d| d.data_local_dir().to_path_buf())
|
||||||
|
@ -37,6 +45,10 @@ pub fn local_project_file(filename: &str) -> LprsResult<PathBuf> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the default vaults json file
|
/// Returns the default vaults json file
|
||||||
|
///
|
||||||
|
/// ## Errors
|
||||||
|
/// - If the project dir can't be extracted from the OS
|
||||||
|
/// - If the vaults file can't be created
|
||||||
pub fn vaults_file() -> LprsResult<PathBuf> {
|
pub fn vaults_file() -> LprsResult<PathBuf> {
|
||||||
let vaults_file = local_project_file(crate::DEFAULT_VAULTS_FILE)?;
|
let vaults_file = local_project_file(crate::DEFAULT_VAULTS_FILE)?;
|
||||||
if !vaults_file.exists() {
|
if !vaults_file.exists() {
|
||||||
|
@ -50,22 +62,26 @@ pub fn vaults_file() -> LprsResult<PathBuf> {
|
||||||
/// ## To pass
|
/// ## To pass
|
||||||
/// - The length must be higher than 14 (>=15)
|
/// - The length must be higher than 14 (>=15)
|
||||||
/// - Its score must be greater than 80.0
|
/// - Its score must be greater than 80.0
|
||||||
|
///
|
||||||
|
/// ## Errors
|
||||||
|
/// - There is no errors, just the return type of inquire validator
|
||||||
|
/// must be Result<Validation, inquire::CustomUserError>
|
||||||
pub fn password_validator(password: &str) -> Result<Validation, inquire::CustomUserError> {
|
pub fn password_validator(password: &str) -> Result<Validation, inquire::CustomUserError> {
|
||||||
let analyzed = passwords::analyzer::analyze(password);
|
let analyzed = analyzer::analyze(password);
|
||||||
if analyzed.length() < 15 {
|
Ok(if analyzed.length() < 15 {
|
||||||
return Ok(Validation::Invalid(
|
Validation::Invalid("The master password length must be beggier then 15".into())
|
||||||
"The master password length must be beggier then 15".into(),
|
} else if scorer::score(&analyzed) < 80.0 {
|
||||||
));
|
Validation::Invalid("Your master password is not stronge enough".into())
|
||||||
} else if passwords::scorer::score(&analyzed) < 80.0 {
|
} else {
|
||||||
return Ok(Validation::Invalid(
|
Validation::Valid
|
||||||
"Your master password is not stronge enough".into(),
|
})
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(Validation::Valid)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ask the user for the master password, then returns it
|
/// Ask the user for the master password, then returns it
|
||||||
///
|
///
|
||||||
|
/// ## Errors
|
||||||
|
/// - Can't read the password from the user
|
||||||
|
///
|
||||||
/// Return's the password as 32 bytes after hash it (256 bit)
|
/// Return's the password as 32 bytes after hash it (256 bit)
|
||||||
pub fn master_password_prompt(is_new_vaults_file: bool) -> LprsResult<[u8; 32]> {
|
pub fn master_password_prompt(is_new_vaults_file: bool) -> LprsResult<[u8; 32]> {
|
||||||
inquire::Password {
|
inquire::Password {
|
||||||
|
@ -79,13 +95,17 @@ pub fn master_password_prompt(is_new_vaults_file: bool) -> LprsResult<[u8; 32]>
|
||||||
..inquire::Password::new("")
|
..inquire::Password::new("")
|
||||||
}
|
}
|
||||||
.with_formatter(&|p| "*".repeat(p.chars().count()))
|
.with_formatter(&|p| "*".repeat(p.chars().count()))
|
||||||
.with_display_mode(inquire::PasswordDisplayMode::Masked)
|
.with_display_mode(PasswordDisplayMode::Masked)
|
||||||
.prompt()
|
.prompt()
|
||||||
.map(|p| sha2::Sha256::digest(p).into())
|
.map(|p| sha2::Sha256::digest(p).into())
|
||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retuns the current lprs version from `crates.io`
|
/// Retuns the current lprs version from `crates.io`
|
||||||
|
///
|
||||||
|
/// ## Errors
|
||||||
|
/// - The project dir can't be extracted from the OS
|
||||||
|
/// - If the last version check file can't be created
|
||||||
#[cfg(feature = "update-notify")]
|
#[cfg(feature = "update-notify")]
|
||||||
pub fn lprs_version() -> LprsResult<Option<String>> {
|
pub fn lprs_version() -> LprsResult<Option<String>> {
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
@ -108,7 +128,7 @@ pub fn lprs_version() -> LprsResult<Option<String>> {
|
||||||
|
|
||||||
// Check if the last check is before one hour or not
|
// Check if the last check is before one hour or not
|
||||||
if (current_time - last_check) >= (60 * 60) || current_time == last_check {
|
if (current_time - last_check) >= (60 * 60) || current_time == last_check {
|
||||||
if let Ok(Ok(response)) = reqwest::blocking::Client::new()
|
if let Ok(Ok(response)) = BlockingClient::new()
|
||||||
.get("https://crates.io/api/v1/crates/lprs")
|
.get("https://crates.io/api/v1/crates/lprs")
|
||||||
.header(
|
.header(
|
||||||
"User-Agent",
|
"User-Agent",
|
||||||
|
@ -117,8 +137,8 @@ pub fn lprs_version() -> LprsResult<Option<String>> {
|
||||||
.send()
|
.send()
|
||||||
.map(|r| r.text())
|
.map(|r| r.text())
|
||||||
{
|
{
|
||||||
let re =
|
let re = regex::Regex::new(r#""max_stable_version":"(?<version>\d+\.\d+\.\d+)""#)
|
||||||
regex::Regex::new(r#""max_stable_version":"(?<version>\d+\.\d+\.\d+)""#).unwrap();
|
.expect("The regex is correct");
|
||||||
if let Some(cap) = re.captures(&response) {
|
if let Some(cap) = re.captures(&response) {
|
||||||
return Ok(cap.name("version").map(|m| m.as_str().to_string()));
|
return Ok(cap.name("version").map(|m| m.as_str().to_string()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,23 @@
|
||||||
|
// Lprs - A local CLI vault manager
|
||||||
|
// Copyright (C) 2024 Awiteb <a@4rs.nl>
|
||||||
|
//
|
||||||
|
// 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 <https://www.gnu.org/licenses/gpl-3.0.html>.
|
||||||
|
|
||||||
|
// This file is not important, it is just a struct that is used to serialize and deserialize the vaults
|
||||||
|
// from and to the BitWarden format.
|
||||||
|
#![allow(missing_docs)]
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::{Vault, Vaults};
|
use super::{Vault, Vaults};
|
||||||
|
|
|
@ -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::{fs, path::PathBuf};
|
use std::{fmt, fs, path::PathBuf};
|
||||||
|
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use clap::{Parser, ValueEnum};
|
use clap::{Parser, ValueEnum};
|
||||||
|
@ -22,6 +22,7 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{LprsError, LprsResult};
|
use crate::{LprsError, LprsResult};
|
||||||
|
|
||||||
|
/// The chiper module, used to encrypt and decrypt the vaults
|
||||||
pub mod cipher;
|
pub mod cipher;
|
||||||
|
|
||||||
mod bitwarden;
|
mod bitwarden;
|
||||||
|
@ -29,8 +30,12 @@ mod bitwarden;
|
||||||
pub use bitwarden::*;
|
pub use bitwarden::*;
|
||||||
|
|
||||||
#[derive(Clone, Debug, ValueEnum)]
|
#[derive(Clone, Debug, ValueEnum)]
|
||||||
|
/// The vaults format
|
||||||
pub enum Format {
|
pub enum Format {
|
||||||
|
/// The lprs format, which is the default format
|
||||||
|
/// and is is the result of the serialization/deserialization of the Vaults struct
|
||||||
Lprs,
|
Lprs,
|
||||||
|
/// The BitWarden format, which is the result of the serialization/deserialization of the BitWardenPasswords struct
|
||||||
BitWarden,
|
BitWarden,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,7 +90,7 @@ impl Vault {
|
||||||
|
|
||||||
/// Return the name of the vault with the service if there
|
/// Return the name of the vault with the service if there
|
||||||
pub fn list_name(&self) -> String {
|
pub fn list_name(&self) -> String {
|
||||||
use std::fmt::Write;
|
use fmt::Write;
|
||||||
let mut list_name = self.name.clone();
|
let mut list_name = self.name.clone();
|
||||||
if let Some(ref username) = self.username {
|
if let Some(ref username) = self.username {
|
||||||
write!(&mut list_name, " <{username}>").expect("String never fail");
|
write!(&mut list_name, " <{username}>").expect("String never fail");
|
||||||
|
@ -119,6 +124,10 @@ impl Vaults {
|
||||||
///
|
///
|
||||||
/// This function used to backup the vaults.
|
/// This function used to backup the vaults.
|
||||||
///
|
///
|
||||||
|
/// ## Errors
|
||||||
|
/// - If the serialization 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) -> LprsResult<String> {
|
||||||
let encrypt = |val: &str| {
|
let encrypt = |val: &str| {
|
||||||
|
@ -147,6 +156,10 @@ impl Vaults {
|
||||||
|
|
||||||
/// Reload the vaults from json data.
|
/// Reload the vaults from json data.
|
||||||
///
|
///
|
||||||
|
/// ## Errors
|
||||||
|
/// - If base64 decoding failed (of the vault field encrypted data)
|
||||||
|
/// - If decryption failed (wrong master password or the data is corrupted)
|
||||||
|
///
|
||||||
/// This function used to import backup vaults.
|
/// This function used to import backup vaults.
|
||||||
pub fn json_reload(master_password: &[u8; 32], json_data: &[u8]) -> LprsResult<Vec<Vault>> {
|
pub fn json_reload(master_password: &[u8; 32], json_data: &[u8]) -> LprsResult<Vec<Vault>> {
|
||||||
let decrypt = |val: &str| {
|
let decrypt = |val: &str| {
|
||||||
|
@ -172,6 +185,9 @@ impl Vaults {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encrypt the vaults then export it to the file
|
/// Encrypt the vaults then export it to the file
|
||||||
|
///
|
||||||
|
/// ## Errors
|
||||||
|
/// - Writing to the file failed
|
||||||
pub fn try_export(self) -> LprsResult<()> {
|
pub fn try_export(self) -> LprsResult<()> {
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"Trying to export the vaults to the file: {}",
|
"Trying to export the vaults to the file: {}",
|
||||||
|
@ -185,6 +201,11 @@ impl Vaults {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reload the vaults from the file then decrypt it
|
/// Reload the vaults from the file then decrypt it
|
||||||
|
///
|
||||||
|
/// ## Errors
|
||||||
|
/// - Reading the file failed
|
||||||
|
/// - Decryption failed (wrong master password or the data is corrupted)
|
||||||
|
/// - Bytecode deserialization failed (the data is corrupted)
|
||||||
pub fn try_reload(vaults_file: PathBuf, master_password: [u8; 32]) -> LprsResult<Self> {
|
pub fn try_reload(vaults_file: PathBuf, master_password: [u8; 32]) -> LprsResult<Self> {
|
||||||
let vaults_data = fs::read(&vaults_file)?;
|
let vaults_data = fs::read(&vaults_file)?;
|
||||||
|
|
||||||
|
@ -198,8 +219,8 @@ impl Vaults {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Format {
|
impl fmt::Display for Format {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"{}",
|
"{}",
|
||||||
|
@ -210,8 +231,8 @@ impl std::fmt::Display for Format {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Vault {
|
impl fmt::Display for Vault {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "Name: {}", self.name)?;
|
write!(f, "Name: {}", self.name)?;
|
||||||
if let Some(ref username) = self.username {
|
if let Some(ref username) = self.username {
|
||||||
write!(f, "\nUsername: {username}")?;
|
write!(f, "\nUsername: {username}")?;
|
||||||
|
|
Loading…
Reference in a new issue