From f6aaecb9cf43d7dfa3ef653ff0cd117b3197308b Mon Sep 17 00:00:00 2001 From: Awiteb Date: Sun, 17 Mar 2024 11:35:47 +0300 Subject: [PATCH] refactor: Rename `Password`s `Vault`s --- src/cli/add_command.rs | 10 +- src/cli/clean_command.rs | 4 +- src/cli/edit_command.rs | 42 ++++---- src/cli/export_command.rs | 6 +- src/cli/gen_command.rs | 4 +- src/cli/import_command.rs | 30 +++--- src/cli/list_command.rs | 22 ++-- src/cli/mod.rs | 27 +++-- src/cli/remove_command.rs | 4 +- src/macros.rs | 4 +- src/password/bitwarden.rs | 12 +-- src/password/mod.rs | 29 +++--- src/password/validator.rs | 4 +- src/traits.rs | 4 +- src/vault/mod.rs | 209 ++++++++++++++++++++++++++++++++++++++ 15 files changed, 302 insertions(+), 109 deletions(-) create mode 100644 src/vault/mod.rs diff --git a/src/cli/add_command.rs b/src/cli/add_command.rs index cdc2557..46b836b 100644 --- a/src/cli/add_command.rs +++ b/src/cli/add_command.rs @@ -17,7 +17,7 @@ use clap::Args; use crate::{ - password::{Password, Passwords}, + password::{Vault, Vaults}, LprsResult, RunCommand, }; @@ -25,12 +25,12 @@ use crate::{ #[command(author, version, about, long_about = None)] pub struct Add { #[command(flatten)] - password_info: Password, + vault_info: Vault, } impl RunCommand for Add { - fn run(&self, mut password_manager: Passwords) -> LprsResult<()> { - password_manager.add_password(self.password_info.clone()); - password_manager.try_export() + fn run(&self, mut vault_manager: Vaults) -> LprsResult<()> { + vault_manager.add_vault(self.vault_info.clone()); + vault_manager.try_export() } } diff --git a/src/cli/clean_command.rs b/src/cli/clean_command.rs index 8c91892..ca49ee7 100644 --- a/src/cli/clean_command.rs +++ b/src/cli/clean_command.rs @@ -18,14 +18,14 @@ use std::fs; use clap::Args; -use crate::{password::Passwords, LprsError, LprsResult, RunCommand}; +use crate::{password::Vaults, LprsError, LprsResult, RunCommand}; #[derive(Debug, Args)] #[command(author, version, about, long_about = None)] pub struct Clean {} impl RunCommand for Clean { - fn run(&self, password_manager: Passwords) -> LprsResult<()> { + fn run(&self, password_manager: Vaults) -> LprsResult<()> { fs::write(password_manager.passwords_file, "[]").map_err(LprsError::Io) } } diff --git a/src/cli/edit_command.rs b/src/cli/edit_command.rs index 6b2885e..4336a10 100644 --- a/src/cli/edit_command.rs +++ b/src/cli/edit_command.rs @@ -19,7 +19,7 @@ use std::num::NonZeroU64; use clap::Args; use crate::{ - password::{Password, Passwords}, + password::{Vault, Vaults}, LprsError, LprsResult, RunCommand, }; @@ -30,27 +30,27 @@ pub struct Edit { index: NonZeroU64, #[arg(short, long)] - /// The new password name + /// The new vault name name: Option, #[arg(short, long)] - /// The new password username + /// The new vault username username: Option, #[arg(short, long)] /// The new password password: Option, #[arg(short, long)] - /// The new password service + /// The new vault service service: Option, #[arg(short = 'o', long)] - /// The new password note + /// The new vault note note: Option, } impl RunCommand for Edit { - fn run(&self, mut password_manager: Passwords) -> LprsResult<()> { + fn run(&self, mut password_manager: Vaults) -> LprsResult<()> { let index = self.index.get() as usize; - if let Some(password) = password_manager.passwords.get_mut(index - 1) { + if let Some(vault) = vault_manager.vaults.get_mut(index - 1) { if self.name.is_none() && self.username.is_none() && self.password.is_none() @@ -61,26 +61,18 @@ impl RunCommand for Edit { "You must edit one option at least".to_owned(), )) } else { - *password = Password { - name: self.name.as_ref().unwrap_or(&password.name).to_string(), - username: self - .username - .as_ref() - .unwrap_or(&password.username) - .to_string(), - password: self - .password - .as_ref() - .unwrap_or(&password.password) - .to_string(), - service: self.service.as_ref().or(password.service.as_ref()).cloned(), - note: self.note.as_ref().or(password.note.as_ref()).cloned(), - }; - password_manager.try_export() + *vault = Vault::::new( + self.name.as_ref().unwrap_or(&vault.name), + self.username.as_ref().unwrap_or(&vault.username), + self.password.as_ref().unwrap_or(&vault.password), + self.service.as_ref().or(vault.service.as_ref()), + self.note.as_ref().or(vault.note.as_ref()), + ); + vault_manager.try_export() } } else { - Err(LprsError::InvalidPasswordIndex(format!( - "The index `{}` is greater than the passwords count {}", + Err(LprsError::InvalidVaultIndex(format!( + "The index `{}` is greater than the vaults count {}", self.index, password_manager.passwords.len() ))) diff --git a/src/cli/export_command.rs b/src/cli/export_command.rs index 475b8b5..8c5fb31 100644 --- a/src/cli/export_command.rs +++ b/src/cli/export_command.rs @@ -19,7 +19,7 @@ use std::{fs, io::Error as IoError, io::ErrorKind as IoErrorKind, path::PathBuf} use clap::Args; use crate::{ - password::{BitWardenPasswords, Format, Passwords}, + password::{BitWardenPasswords, Format, Vaults}, LprsError, LprsResult, RunCommand, }; @@ -28,13 +28,13 @@ use crate::{ pub struct Export { /// The path to export to path: PathBuf, - /// Format to export passwords in + /// Format to export vaults in #[arg(short, long, value_name = "FORMAT", default_value_t= Format::Lprs)] format: Format, } impl RunCommand for Export { - fn run(&self, password_manager: Passwords) -> LprsResult<()> { + fn run(&self, password_manager: Vaults) -> LprsResult<()> { if self .path .extension() diff --git a/src/cli/gen_command.rs b/src/cli/gen_command.rs index afb31d5..7dd7c96 100644 --- a/src/cli/gen_command.rs +++ b/src/cli/gen_command.rs @@ -18,7 +18,7 @@ use std::num::NonZeroU64; use clap::Args; -use crate::{password::Passwords, LprsError, LprsResult, RunCommand}; +use crate::{password::Vaults, LprsError, LprsResult, RunCommand}; #[derive(Debug, Args)] #[command(author, version, about, long_about = None)] @@ -42,7 +42,7 @@ pub struct Gen { } impl RunCommand for Gen { - fn run(&self, _password_manager: Passwords) -> LprsResult<()> { + fn run(&self, _password_manager: Vaults) -> LprsResult<()> { if self.uppercase || self.lowercase || self.numbers || self.symbols { println!( "{}", diff --git a/src/cli/import_command.rs b/src/cli/import_command.rs index aa02bc3..04a54d0 100644 --- a/src/cli/import_command.rs +++ b/src/cli/import_command.rs @@ -19,7 +19,7 @@ use std::{fs::File, io::Error as IoError, io::ErrorKind as IoErrorKind, path::Pa use clap::Args; use crate::{ - password::{BitWardenPasswords, Format, Password, Passwords}, + password::{BitWardenPasswords, Format, Vault, Vaults}, LprsError, LprsResult, RunCommand, }; @@ -35,7 +35,7 @@ pub struct Import { } impl RunCommand for Import { - fn run(&self, mut password_manager: Passwords) -> LprsResult<()> { + fn run(&self, mut password_manager: Vaults) -> LprsResult<()> { if self.path.exists() { if self .path @@ -44,30 +44,30 @@ impl RunCommand for Import { { let imported_passwords_len = match self.format { Format::Lprs => { - let passwords = Passwords::try_reload( + let vaults = Vaults::try_reload( self.path.to_path_buf(), password_manager.master_password.to_vec(), )?; - let passwords_len = passwords.passwords.len(); + let vaults_len = vaults.vaults.len(); - password_manager.passwords.extend(passwords.passwords); - password_manager.try_export()?; - passwords_len + vault_manager.vaults.extend(vaults.vaults); + vault_manager.try_export()?; + vaults_len } Format::BitWarden => { - let passwords: BitWardenPasswords = + let vaults: BitWardenPasswords = serde_json::from_reader(File::open(&self.path)?)?; - let passwords_len = passwords.items.len(); + let vaults_len = vaults.items.len(); - password_manager - .passwords - .extend(passwords.items.into_iter().map(Password::from)); - password_manager.try_export()?; - passwords_len + vault_manager + .vaults + .extend(vaults.items.into_iter().map(Vault::from)); + vault_manager.try_export()?; + vaults_len } }; println!( - "{imported_passwords_len} password{s} were imported successfully", + "{imported_passwords_len} vault{s} were imported successfully", s = if imported_passwords_len >= 2 { "s" } else { "" } ); diff --git a/src/cli/list_command.rs b/src/cli/list_command.rs index 32b47e9..3015a04 100644 --- a/src/cli/list_command.rs +++ b/src/cli/list_command.rs @@ -20,7 +20,7 @@ use clap::Args; use comfy_table::Table; use regex::Regex; -use crate::{password::Passwords, LprsError, LprsResult, RunCommand}; +use crate::{password::Vaults, LprsError, LprsResult, RunCommand}; #[derive(Debug, Args)] #[command(author, version, about, long_about = None)] @@ -49,7 +49,7 @@ pub struct List { } impl RunCommand for List { - fn run(&self, password_manager: Passwords) -> LprsResult<()> { + fn run(&self, password_manager: Vaults) -> LprsResult<()> { if password_manager.passwords.is_empty() { Err(LprsError::Other( "Looks like there is no passwords to list".to_owned(), @@ -77,8 +77,8 @@ impl RunCommand for List { let re = Regex::new(self.search.as_deref().unwrap_or("."))?; table.set_header(header); - let passwords = password_manager - .passwords + let vaults = vault_manager + .vaults .iter() .enumerate() .filter(|(idx, pass)| { @@ -112,24 +112,24 @@ impl RunCommand for List { true }); - for (idx, password) in passwords { - let hide_password = "*".repeat(password.password.chars().count()); + for (idx, vault) in vaults { + let hide_password = "*".repeat(vault.password.chars().count()); let idx = (idx + 1).to_string(); let mut row = vec![ idx.as_str(), - password.name.as_str(), - password.username.as_str(), + vault.name.as_str(), + vault.username.as_str(), if self.unhide_password { - password.password.as_str() + vault.password.as_str() } else { hide_password.as_str() }, ]; if self.with_service { - row.push(password.service.as_deref().unwrap_or("Not Set")) + row.push(vault.service.as_deref().unwrap_or("Not Set")) } if self.with_note { - row.push(password.note.as_deref().unwrap_or("Not Set")) + row.push(vault.note.as_deref().unwrap_or("Not Set")) } table.add_row(row); } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index f4459e0..29b1bd4 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -19,7 +19,7 @@ use std::path::PathBuf; use clap::Parser; use crate::{ - password::{self, Passwords}, + password::{self, Vaults}, LprsError, LprsResult, RunCommand, }; @@ -47,7 +47,7 @@ crate::create_commands!( #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] pub struct Cli { - /// The passwords json file, default: $HOME/.local/share/lprs/passwords.json + /// The vaults json file #[arg(short, long)] passwords_file: Option, @@ -64,33 +64,30 @@ impl Cli { } else { crate::utils::passwords_file()? }; - log::debug!( - "Getting password file: {}", - passwords_file.to_string_lossy() - ); - let password_manager = if matches!(self.command, Commands::Clean(..) | Commands::Gen(..)) { - Passwords { + log::debug!("Getting the vaults file: {}", vaults_file.to_string_lossy()); + let vault_manager = if matches!(self.command, Commands::Clean(..) | Commands::Gen(..)) { + Vaults { passwords_file, ..Default::default() } } else { - let password = scanpw::scanpw!("Master Password: "); + let master_password = scanpw::scanpw!("Master Password: "); - if password::is_new_password_file(&passwords_file)? { - let analyzed = passwords::analyzer::analyze(&password); + if vault::is_new_vaults_file(&vaults_file)? { + let analyzed = passwords::analyzer::analyze(&master_password); if analyzed.length() < 15 { return Err(LprsError::WeakPassword( - "The password length must be beggier then 15".to_owned(), + "The master password length must be beggier then 15".to_owned(), )); } else if passwords::scorer::score(&analyzed) < 80.0 { return Err(LprsError::WeakPassword( - "Your password is not stronge enough".to_owned(), + "Your master password is not stronge enough".to_owned(), )); } } - let master_password = sha256::digest(password); - Passwords::try_reload( + let master_password = sha256::digest(master_password); + Vaults::try_reload( passwords_file, master_password.into_bytes().into_iter().take(32).collect(), )? diff --git a/src/cli/remove_command.rs b/src/cli/remove_command.rs index ac12597..6c9151b 100644 --- a/src/cli/remove_command.rs +++ b/src/cli/remove_command.rs @@ -18,7 +18,7 @@ use std::num::NonZeroU64; use clap::Args; -use crate::{password::Passwords, LprsError, LprsResult, RunCommand}; +use crate::{password::Vaults, LprsError, LprsResult, RunCommand}; #[derive(Debug, Args)] #[command(author, version, about, long_about = None)] @@ -32,7 +32,7 @@ pub struct Remove { } impl RunCommand for Remove { - fn run(&self, mut password_manager: Passwords) -> LprsResult<()> { + fn run(&self, mut password_manager: Vaults) -> LprsResult<()> { let index = (self.index.get() - 1) as usize; if index > password_manager.passwords.len() { if !self.force { diff --git a/src/macros.rs b/src/macros.rs index 80ba849..1819671 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -40,7 +40,7 @@ /// impl crate::RunCommand for TestCommands { /// fn run( /// &self, -/// password_manager: crate::password::Passwords, +/// vault_manager: crate::vault::Vaults, /// ) -> crate::LprsResult<()> { /// match self { /// Self::Test(command) => command.run(password_manager), @@ -63,7 +63,7 @@ macro_rules! create_commands { #[automatically_derived] impl $crate::RunCommand for $enum_name{ - fn run(&self, password_manager: $crate::password::Passwords) -> $crate::LprsResult<()> { + fn run(&self, password_manager: $crate::password::Vaults) -> $crate::LprsResult<()> { match self { $( Self::$varint(command) => command.run(password_manager), diff --git a/src/password/bitwarden.rs b/src/password/bitwarden.rs index b1ab0a9..c31fd8f 100644 --- a/src/password/bitwarden.rs +++ b/src/password/bitwarden.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use super::{Password, Passwords}; +use super::{Vault, Vaults}; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct BitWardenLoginData { @@ -39,7 +39,7 @@ pub struct BitWardenPasswords { pub items: Vec, } -impl From for Password { +impl From for Vault { fn from(value: BitWardenPassword) -> Self { Self { name: value.name, @@ -59,8 +59,8 @@ impl From for Password { } } -impl From for BitWardenPassword { - fn from(value: Password) -> Self { +impl From for BitWardenPassword { + fn from(value: Vault) -> Self { Self { ty: 1, name: value.name, @@ -76,8 +76,8 @@ impl From for BitWardenPassword { } } -impl From for BitWardenPasswords { - fn from(value: Passwords) -> Self { +impl From for BitWardenPasswords { + fn from(value: Vaults) -> Self { Self { encrypted: false, folders: Vec::new(), diff --git a/src/password/mod.rs b/src/password/mod.rs index a13fbe7..86ad388 100644 --- a/src/password/mod.rs +++ b/src/password/mod.rs @@ -38,7 +38,7 @@ pub enum Format { /// The password struct #[serde_with_macros::skip_serializing_none] #[derive(Clone, Debug, Deserialize, Serialize, Parser)] -pub struct Password { +pub struct Vault { /// The name of the password #[arg(short, long)] pub name: String, @@ -58,16 +58,16 @@ pub struct Password { /// The passwords manager #[derive(Default)] -pub struct Passwords { +pub struct Vaults { /// Hash of the master password pub master_password: Vec, /// The json passwords file pub passwords_file: PathBuf, /// The passwords - pub passwords: Vec, + pub passwords: Vec, } -impl Password { +impl Vault { /// Encrypt the password data pub fn encrypt(self, master_password: &[u8]) -> LprsResult { Ok(Self { @@ -103,13 +103,9 @@ impl Password { } } -impl Passwords { +impl Vaults { /// Create new Passwords instnce - pub fn new( - master_password: Vec, - passwords_file: PathBuf, - passwords: Vec, - ) -> Self { + pub fn new(master_password: Vec, passwords_file: PathBuf, passwords: Vec) -> Self { Self { master_password, passwords_file, @@ -124,18 +120,17 @@ impl Passwords { .passwords .into_iter() .map(|p| p.encrypt(&self.master_password)) - .collect::>>()?, + .collect::>>()?, ..self }) } /// Reload the passwords from the file pub fn try_reload(passwords_file: PathBuf, master_password: Vec) -> LprsResult { - let passwords = - serde_json::from_str::>(&fs::read_to_string(&passwords_file)?)? - .into_iter() - .map(|p| p.decrypt(master_password.as_slice())) - .collect::>>()?; + let passwords = serde_json::from_str::>(&fs::read_to_string(&passwords_file)?)? + .into_iter() + .map(|p| p.decrypt(master_password.as_slice())) + .collect::>>()?; Ok(Self::new(master_password, passwords_file, passwords)) } @@ -147,7 +142,7 @@ impl Passwords { } /// Add new password - pub fn add_password(&mut self, password: Password) { + pub fn add_password(&mut self, password: Vault) { self.passwords.push(password) } } diff --git a/src/password/validator.rs b/src/password/validator.rs index fb9bbf6..1fc015c 100644 --- a/src/password/validator.rs +++ b/src/password/validator.rs @@ -18,7 +18,7 @@ use std::{fs, path::Path}; use crate::LprsResult; -use super::Password; +use super::Vault; /// Return if the password file new file or not pub fn is_new_password_file(path: &Path) -> LprsResult { @@ -26,7 +26,7 @@ pub fn is_new_password_file(path: &Path) -> LprsResult { let file_content = fs::read_to_string(path)?; if !file_content.is_empty() && file_content.trim() != "[]" - && serde_json::from_str::>(&file_content).is_ok() + && serde_json::from_str::>(&file_content).is_ok() { return Ok(false); } diff --git a/src/traits.rs b/src/traits.rs index 05da877..e12a415 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -14,9 +14,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::{password::Passwords, LprsResult}; +use crate::{password::Vaults, LprsResult}; /// Trait to run the command pub trait RunCommand { - fn run(&self, password_manager: Passwords) -> LprsResult<()>; + fn run(&self, password_manager: Vaults) -> LprsResult<()>; } diff --git a/src/vault/mod.rs b/src/vault/mod.rs new file mode 100644 index 0000000..2e4bacd --- /dev/null +++ b/src/vault/mod.rs @@ -0,0 +1,209 @@ +// Lprs - A local CLI vault manager +// Copyright (C) 2024 Awiteb +// +// 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 . + +use std::{fs, marker::PhantomData, path::PathBuf}; + +use clap::{Parser, ValueEnum}; +use serde::{Deserialize, Serialize}; + +use crate::{LprsError, LprsResult}; +use vault_state::*; + +pub mod cipher; + +mod bitwarden; +mod validator; + +pub use bitwarden::*; +pub use validator::*; + +#[derive(Clone, Debug, ValueEnum)] +pub enum Format { + Lprs, + BitWarden, +} + +/// The states of the vaults +pub mod vault_state { + /// Means the vault is encrypted + #[derive(Clone, Debug, Default)] + pub struct Encrypted; + /// Means the vault is not encrypted + #[derive(Clone, Debug, Default)] + pub struct Plain; +} + +/// The vault struct +#[serde_with_macros::skip_serializing_none] +#[derive(Clone, Debug, Deserialize, Serialize, Parser)] +pub struct Vault +where + T: std::fmt::Debug + Clone, +{ + /// The name of the vault + #[arg(short, long)] + pub name: String, + /// The username + #[arg(short, long)] + pub username: String, + /// The password + #[arg(short, long)] + pub password: String, + /// The service name. e.g the website url + #[arg(short, long)] + pub service: Option, + /// Add a note to the vault + #[arg(short = 'o', long)] + pub note: Option, + + /// State phantom + #[serde(skip)] + #[arg(skip)] + phantom: PhantomData, +} + +/// The vaults manager +#[derive(Default)] +pub struct Vaults +where + T: std::fmt::Debug + Clone, +{ + /// Hash of the master password + pub master_password: Vec, + /// The json vaults file + pub vaults_file: PathBuf, + /// The vaults + pub vaults: Vec>, +} + +impl Vault +where + T: std::fmt::Debug + Clone, +{ + /// Create new [`Vault`] instance + pub fn new( + name: impl Into, + username: impl Into, + password: impl Into, + service: Option>, + note: Option>, + ) -> Self { + Self { + name: name.into(), + username: username.into(), + password: password.into(), + service: service.map(|s| s.into()), + note: note.map(|s| s.into()), + phantom: std::marker::PhantomData, + } + } +} + +impl Vault { + /// Decrypt the vault + pub fn decrypt(&self, master_password: &[u8]) -> LprsResult> { + Ok(Vault::::new( + cipher::decrypt(master_password, &self.name)?, + cipher::decrypt(master_password, &self.username)?, + cipher::decrypt(master_password, &self.password)?, + self.service + .as_ref() + .map(|url| cipher::decrypt(master_password, url)) + .transpose()?, + self.note + .as_ref() + .map(|note| cipher::decrypt(master_password, note)) + .transpose()?, + )) + } +} + +impl Vault { + /// Encrypt the vault + pub fn encrypt(&self, master_password: &[u8]) -> LprsResult> { + Ok(Vault::::new( + cipher::encrypt(master_password, &self.name)?, + cipher::encrypt(master_password, &self.username)?, + cipher::encrypt(master_password, &self.password)?, + self.service + .as_ref() + .map(|url| cipher::encrypt(master_password, url)) + .transpose()?, + self.note + .as_ref() + .map(|note| cipher::encrypt(master_password, note)) + .transpose()?, + )) + } +} + +impl Vaults +where + T: std::fmt::Debug + Clone, +{ + /// Create new [`Vaults`] instnce + pub fn new(master_password: Vec, vaults_file: PathBuf, vaults: Vec>) -> Self { + Self { + master_password, + vaults_file, + vaults, + } + } +} + +impl Vaults { + /// Encrypt the vaults + pub fn encrypt_vaults(&self) -> LprsResult>> { + self.vaults + .iter() + .map(|p| p.encrypt(&self.master_password)) + .collect() + } + + /// Reload the vaults from the file then decrypt it + pub fn try_reload(vaults_file: PathBuf, master_password: Vec) -> LprsResult { + let vaults = + serde_json::from_str::>>(&fs::read_to_string(&vaults_file)?)? + .into_iter() + .map(|p| p.decrypt(master_password.as_slice())) + .collect::>>>()?; + + Ok(Self::new(master_password, vaults_file, vaults)) + } + + /// Encrypt the vaults then export it to the file + pub fn try_export(self) -> LprsResult<()> { + fs::write( + &self.vaults_file, + serde_json::to_string(&self.encrypt_vaults()?)?, + ) + .map_err(LprsError::Io) + } + + /// Add new vault + pub fn add_vault(&mut self, vault: Vault) { + self.vaults.push(vault) + } +} + +impl ToString for Format { + fn to_string(&self) -> String { + self.to_possible_value() + .expect("There is no skiped values") + .get_name() + .to_owned() + } +}