refactor: Add a ecryption state to the vault

Also
- Rename `password`somthing to `vault`somthing
- Rename the `password` mod to `vault`

BREAKING-CHANGE: Moving from password to vault
ISSUE: #4
This commit is contained in:
Awiteb 2024-03-18 23:32:48 +03:00
parent f6aaecb9cf
commit 4def4aadb2
Signed by: awiteb
GPG key ID: 3F6B55640AA6682F
18 changed files with 84 additions and 223 deletions

View file

@ -17,7 +17,7 @@
use clap::Args; use clap::Args;
use crate::{ use crate::{
password::{Vault, Vaults}, vault::{vault_state::*, Vault, Vaults},
LprsResult, RunCommand, LprsResult, RunCommand,
}; };

View file

@ -18,14 +18,17 @@ use std::fs;
use clap::Args; use clap::Args;
use crate::{password::Vaults, LprsError, LprsResult, RunCommand}; use crate::{
vault::{vault_state::*, Vaults},
LprsError, LprsResult, RunCommand,
};
#[derive(Debug, Args)] #[derive(Debug, Args)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
pub struct Clean {} pub struct Clean {}
impl RunCommand for Clean { impl RunCommand for Clean {
fn run(&self, password_manager: Vaults) -> LprsResult<()> { fn run(&self, password_manager: Vaults<Plain>) -> LprsResult<()> {
fs::write(password_manager.passwords_file, "[]").map_err(LprsError::Io) fs::write(password_manager.vaults_file, "[]").map_err(LprsError::Io)
} }
} }

View file

@ -19,7 +19,7 @@ use std::num::NonZeroU64;
use clap::Args; use clap::Args;
use crate::{ use crate::{
password::{Vault, Vaults}, vault::{vault_state::*, Vault, Vaults},
LprsError, LprsResult, RunCommand, LprsError, LprsResult, RunCommand,
}; };
@ -47,7 +47,7 @@ pub struct Edit {
} }
impl RunCommand for Edit { impl RunCommand for Edit {
fn run(&self, mut password_manager: Vaults) -> LprsResult<()> { fn run(&self, mut password_manager: Vaults<Plain>) -> LprsResult<()> {
let index = self.index.get() as usize; let index = self.index.get() as usize;
if let Some(vault) = vault_manager.vaults.get_mut(index - 1) { if let Some(vault) = vault_manager.vaults.get_mut(index - 1) {
@ -74,7 +74,7 @@ impl RunCommand for Edit {
Err(LprsError::InvalidVaultIndex(format!( Err(LprsError::InvalidVaultIndex(format!(
"The index `{}` is greater than the vaults count {}", "The index `{}` is greater than the vaults count {}",
self.index, self.index,
password_manager.passwords.len() password_manager.vaults.len()
))) )))
} }
} }

View file

@ -19,7 +19,7 @@ use std::{fs, io::Error as IoError, io::ErrorKind as IoErrorKind, path::PathBuf}
use clap::Args; use clap::Args;
use crate::{ use crate::{
password::{BitWardenPasswords, Format, Vaults}, vault::{vault_state::*, BitWardenPasswords, Format, Vault, Vaults},
LprsError, LprsResult, RunCommand, LprsError, LprsResult, RunCommand,
}; };
@ -34,7 +34,7 @@ pub struct Export {
} }
impl RunCommand for Export { impl RunCommand for Export {
fn run(&self, password_manager: Vaults) -> LprsResult<()> { fn run(&self, password_manager: Vaults<Plain>) -> LprsResult<()> {
if self if self
.path .path
.extension() .extension()
@ -42,7 +42,9 @@ impl RunCommand for Export {
{ {
if !self.path.exists() { if !self.path.exists() {
let exported_data = match self.format { let exported_data = match self.format {
Format::Lprs => serde_json::to_string(&password_manager.encrypt()?.passwords), Format::Lprs => serde_json::to_string::<Vec<Vault<Encrypted>>>(
&password_manager.encrypt_vaults()?,
),
Format::BitWarden => { Format::BitWarden => {
serde_json::to_string(&BitWardenPasswords::from(password_manager)) serde_json::to_string(&BitWardenPasswords::from(password_manager))
} }

View file

@ -18,7 +18,10 @@ use std::num::NonZeroU64;
use clap::Args; use clap::Args;
use crate::{password::Vaults, LprsError, LprsResult, RunCommand}; use crate::{
vault::{vault_state::*, Vaults},
LprsError, LprsResult, RunCommand,
};
#[derive(Debug, Args)] #[derive(Debug, Args)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
@ -42,7 +45,7 @@ pub struct Gen {
} }
impl RunCommand for Gen { impl RunCommand for Gen {
fn run(&self, _password_manager: Vaults) -> LprsResult<()> { fn run(&self, _password_manager: Vaults<Plain>) -> LprsResult<()> {
if self.uppercase || self.lowercase || self.numbers || self.symbols { if self.uppercase || self.lowercase || self.numbers || self.symbols {
println!( println!(
"{}", "{}",

View file

@ -19,7 +19,7 @@ use std::{fs::File, io::Error as IoError, io::ErrorKind as IoErrorKind, path::Pa
use clap::Args; use clap::Args;
use crate::{ use crate::{
password::{BitWardenPasswords, Format, Vault, Vaults}, vault::{vault_state::*, BitWardenPasswords, Format, Vault, Vaults},
LprsError, LprsResult, RunCommand, LprsError, LprsResult, RunCommand,
}; };
@ -35,7 +35,7 @@ pub struct Import {
} }
impl RunCommand for Import { impl RunCommand for Import {
fn run(&self, mut password_manager: Vaults) -> LprsResult<()> { fn run(&self, mut password_manager: Vaults<Plain>) -> LprsResult<()> {
if self.path.exists() { if self.path.exists() {
if self if self
.path .path

View file

@ -20,7 +20,10 @@ use clap::Args;
use comfy_table::Table; use comfy_table::Table;
use regex::Regex; use regex::Regex;
use crate::{password::Vaults, LprsError, LprsResult, RunCommand}; use crate::{
vault::{vault_state::*, Vaults},
LprsError, LprsResult, RunCommand,
};
#[derive(Debug, Args)] #[derive(Debug, Args)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
@ -49,8 +52,8 @@ pub struct List {
} }
impl RunCommand for List { impl RunCommand for List {
fn run(&self, password_manager: Vaults) -> LprsResult<()> { fn run(&self, password_manager: Vaults<Plain>) -> LprsResult<()> {
if password_manager.passwords.is_empty() { if password_manager.vaults.is_empty() {
Err(LprsError::Other( Err(LprsError::Other(
"Looks like there is no passwords to list".to_owned(), "Looks like there is no passwords to list".to_owned(),
)) ))

View file

@ -19,7 +19,7 @@ use std::path::PathBuf;
use clap::Parser; use clap::Parser;
use crate::{ use crate::{
password::{self, Vaults}, vault::{self, Vaults},
LprsError, LprsResult, RunCommand, LprsError, LprsResult, RunCommand,
}; };
@ -34,14 +34,14 @@ pub mod remove_command;
crate::create_commands!( crate::create_commands!(
enum Commands enum Commands
"Add new password", Add => add_command::Add "Add new vault", Add => add_command::Add
"Remove password", Remove => remove_command::Remove "Remove vault", Remove => remove_command::Remove
"List your password and search", List => list_command::List "List your vaults and search", List => list_command::List
"Clean the password file", Clean => clean_command::Clean "Clean the vaults file", Clean => clean_command::Clean
"Edit the password content", Edit => edit_command::Edit "Edit the vault content", Edit => edit_command::Edit
"Generate password", Gen => gen_command::Gen "Generate a password", Gen => gen_command::Gen
"Export the passwords", Export => export_command::Export "Export the vaults", Export => export_command::Export
"Import passwords", Import => import_command::Import "Import vaults", Import => import_command::Import
); );
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
@ -49,7 +49,7 @@ crate::create_commands!(
pub struct Cli { pub struct Cli {
/// The vaults json file /// The vaults json file
#[arg(short, long)] #[arg(short, long)]
passwords_file: Option<PathBuf>, vaults_file: Option<PathBuf>,
// TODO: verbose flag // TODO: verbose flag
#[command(subcommand)] #[command(subcommand)]
@ -59,15 +59,15 @@ pub struct Cli {
impl Cli { impl Cli {
/// Run the cli /// Run the cli
pub fn run(self) -> LprsResult<()> { pub fn run(self) -> LprsResult<()> {
let passwords_file = if let Some(ref path) = self.passwords_file { let vaults_file = if let Some(ref path) = self.vaults_file {
path.clone() path.clone()
} else { } else {
crate::utils::passwords_file()? crate::utils::vaults_file()?
}; };
log::debug!("Getting the vaults file: {}", vaults_file.to_string_lossy()); log::debug!("Getting the vaults file: {}", vaults_file.to_string_lossy());
let vault_manager = if matches!(self.command, Commands::Clean(..) | Commands::Gen(..)) { let vault_manager = if matches!(self.command, Commands::Clean(..) | Commands::Gen(..)) {
Vaults { Vaults {
passwords_file, vaults_file,
..Default::default() ..Default::default()
} }
} else { } else {
@ -88,7 +88,7 @@ impl Cli {
let master_password = sha256::digest(master_password); let master_password = sha256::digest(master_password);
Vaults::try_reload( Vaults::try_reload(
passwords_file, vaults_file,
master_password.into_bytes().into_iter().take(32).collect(), master_password.into_bytes().into_iter().take(32).collect(),
)? )?
}; };

View file

@ -18,7 +18,10 @@ use std::num::NonZeroU64;
use clap::Args; use clap::Args;
use crate::{password::Vaults, LprsError, LprsResult, RunCommand}; use crate::{
vault::{vault_state::*, Vaults},
LprsError, LprsResult, RunCommand,
};
#[derive(Debug, Args)] #[derive(Debug, Args)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
@ -32,16 +35,16 @@ pub struct Remove {
} }
impl RunCommand for Remove { impl RunCommand for Remove {
fn run(&self, mut password_manager: Vaults) -> LprsResult<()> { fn run(&self, mut password_manager: Vaults<Plain>) -> LprsResult<()> {
let index = (self.index.get() - 1) as usize; let index = (self.index.get() - 1) as usize;
if index > password_manager.passwords.len() { if index > password_manager.vaults.len() {
if !self.force { if !self.force {
return Err(LprsError::Other( return Err(LprsError::Other(
"The index is greater than the passwords counts".to_owned(), "The index is greater than the passwords counts".to_owned(),
)); ));
} }
} else { } else {
password_manager.passwords.remove(index); password_manager.vaults.remove(index);
password_manager.try_export()?; password_manager.try_export()?;
} }
Ok(()) Ok(())

View file

@ -34,8 +34,8 @@ pub enum Error {
WeakPassword(String), WeakPassword(String),
#[error("Args Conflict Error: {0}")] #[error("Args Conflict Error: {0}")]
ArgsConflict(String), ArgsConflict(String),
#[error("Invalid Password Index Error: {0}")] #[error("Invalid Vault Index Error: {0}")]
InvalidPasswordIndex(String), InvalidVaultIndex(String),
#[error("{0}")] #[error("{0}")]
Other(String), Other(String),
@ -56,6 +56,7 @@ pub enum 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 fn exit_code(&self) -> ExitCode {
// TODO: Exit with more specific exit code
ExitCode::FAILURE ExitCode::FAILURE
} }
} }

View file

@ -63,7 +63,7 @@ macro_rules! create_commands {
#[automatically_derived] #[automatically_derived]
impl $crate::RunCommand for $enum_name{ impl $crate::RunCommand for $enum_name{
fn run(&self, password_manager: $crate::password::Vaults) -> $crate::LprsResult<()> { fn run(&self, password_manager: $crate::vault::Vaults<$crate::vault::vault_state::Plain>) -> $crate::LprsResult<()> {
match self { match self {
$( $(
Self::$varint(command) => command.run(password_manager), Self::$varint(command) => command.run(password_manager),

View file

@ -24,8 +24,8 @@ use clap::Parser;
pub mod cli; pub mod cli;
pub mod errors; pub mod errors;
pub mod password;
pub mod utils; pub mod utils;
pub mod vault;
mod macros; mod macros;
mod traits; mod traits;
@ -34,7 +34,7 @@ pub use errors::{Error as LprsError, Result as LprsResult};
pub use traits::*; pub use traits::*;
pub const STANDARDBASE: GeneralPurpose = GeneralPurpose::new(&alphabet::STANDARD, PAD); pub const STANDARDBASE: GeneralPurpose = GeneralPurpose::new(&alphabet::STANDARD, PAD);
pub const DEFAULT_PASSWORD_FILE: &str = "passwords.json"; pub const DEFAULT_VAULTS_FILE: &str = "vaults.json";
#[cfg(feature = "update-notify")] #[cfg(feature = "update-notify")]
pub const VERSION: &str = env!("CARGO_PKG_VERSION"); pub const VERSION: &str = env!("CARGO_PKG_VERSION");

View file

@ -1,157 +0,0 @@
// Lprs - A local CLI password 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>.
use std::{fs, path::PathBuf};
use clap::{Parser, ValueEnum};
use serde::{Deserialize, Serialize};
use crate::{LprsError, LprsResult};
pub mod cipher;
mod bitwarden;
mod validator;
pub use bitwarden::*;
pub use validator::*;
#[derive(Clone, Debug, ValueEnum)]
pub enum Format {
Lprs,
BitWarden,
}
/// The password struct
#[serde_with_macros::skip_serializing_none]
#[derive(Clone, Debug, Deserialize, Serialize, Parser)]
pub struct Vault {
/// The name of the password
#[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<String>,
/// The note of the password
#[arg(short = 'o', long)]
pub note: Option<String>,
}
/// The passwords manager
#[derive(Default)]
pub struct Vaults {
/// Hash of the master password
pub master_password: Vec<u8>,
/// The json passwords file
pub passwords_file: PathBuf,
/// The passwords
pub passwords: Vec<Vault>,
}
impl Vault {
/// Encrypt the password data
pub fn encrypt(self, master_password: &[u8]) -> LprsResult<Self> {
Ok(Self {
name: cipher::encrypt(master_password, &self.name)?,
username: cipher::encrypt(master_password, &self.username)?,
password: cipher::encrypt(master_password, &self.password)?,
service: self
.service
.map(|url| cipher::encrypt(master_password, &url))
.transpose()?,
note: self
.note
.map(|note| cipher::encrypt(master_password, &note))
.transpose()?,
})
}
/// Decrypt the password data
pub fn decrypt(self, master_password: &[u8]) -> LprsResult<Self> {
Ok(Self {
name: cipher::decrypt(master_password, &self.name)?,
username: cipher::decrypt(master_password, &self.username)?,
password: cipher::decrypt(master_password, &self.password)?,
service: self
.service
.map(|url| cipher::decrypt(master_password, &url))
.transpose()?,
note: self
.note
.map(|note| cipher::decrypt(master_password, &note))
.transpose()?,
})
}
}
impl Vaults {
/// Create new Passwords instnce
pub fn new(master_password: Vec<u8>, passwords_file: PathBuf, passwords: Vec<Vault>) -> Self {
Self {
master_password,
passwords_file,
passwords,
}
}
/// Encrypt the passwords
pub fn encrypt(self) -> LprsResult<Self> {
Ok(Self {
passwords: self
.passwords
.into_iter()
.map(|p| p.encrypt(&self.master_password))
.collect::<LprsResult<Vec<Vault>>>()?,
..self
})
}
/// Reload the passwords from the file
pub fn try_reload(passwords_file: PathBuf, master_password: Vec<u8>) -> LprsResult<Self> {
let passwords = serde_json::from_str::<Vec<Vault>>(&fs::read_to_string(&passwords_file)?)?
.into_iter()
.map(|p| p.decrypt(master_password.as_slice()))
.collect::<LprsResult<Vec<Vault>>>()?;
Ok(Self::new(master_password, passwords_file, passwords))
}
/// Export the passwords to the file
pub fn try_export(self) -> LprsResult<()> {
let path = self.passwords_file.to_path_buf();
fs::write(path, serde_json::to_string(&self.encrypt()?.passwords)?).map_err(LprsError::Io)
}
/// Add new password
pub fn add_password(&mut self, password: Vault) {
self.passwords.push(password)
}
}
impl ToString for Format {
fn to_string(&self) -> String {
self.to_possible_value()
.expect("There is no skiped values")
.get_name()
.to_owned()
}
}

View file

@ -14,9 +14,12 @@
// 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 crate::{password::Vaults, LprsResult}; use crate::{
vault::{vault_state::*, Vaults},
LprsResult,
};
/// Trait to run the command /// Trait to run the command
pub trait RunCommand { pub trait RunCommand {
fn run(&self, password_manager: Vaults) -> LprsResult<()>; fn run(&self, password_manager: Vaults<Plain>) -> LprsResult<()>;
} }

View file

@ -31,13 +31,13 @@ pub fn local_project_file(filename: &str) -> LprsResult<PathBuf> {
Ok(local_dir.join(filename)) Ok(local_dir.join(filename))
} }
/// Returns the default passwords json file /// Returns the default vaults json file
pub fn passwords_file() -> LprsResult<PathBuf> { pub fn vaults_file() -> LprsResult<PathBuf> {
let password_file = local_project_file(crate::DEFAULT_PASSWORD_FILE)?; let vaults_file = local_project_file(crate::DEFAULT_VAULTS_FILE)?;
if !password_file.exists() { if !vaults_file.exists() {
fs::write(&password_file, "[]")?; fs::write(&vaults_file, "[]")?;
} }
Ok(password_file) Ok(vaults_file)
} }
/// Retuns the current lprs version from `crates.io` /// Retuns the current lprs version from `crates.io`

View file

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::{Vault, Vaults}; use super::{vault_state::*, Vault, Vaults};
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct BitWardenLoginData { pub struct BitWardenLoginData {
@ -39,28 +39,28 @@ pub struct BitWardenPasswords {
pub items: Vec<BitWardenPassword>, pub items: Vec<BitWardenPassword>,
} }
impl From<BitWardenPassword> for Vault { impl From<BitWardenPassword> for Vault<Plain> {
fn from(value: BitWardenPassword) -> Self { fn from(value: BitWardenPassword) -> Self {
Self { Self::new(
name: value.name, value.name,
username: value value
.login .login
.as_ref() .as_ref()
.map_or_else(String::new, |l| l.username.to_owned().unwrap_or_default()), .map_or_else(String::new, |l| l.username.to_owned().unwrap_or_default()),
password: value value
.login .login
.as_ref() .as_ref()
.map_or_else(String::new, |l| l.password.to_owned().unwrap_or_default()), .map_or_else(String::new, |l| l.password.to_owned().unwrap_or_default()),
service: value value
.login .login
.and_then(|l| l.uris.and_then(|p| p.first().map(|u| u.uri.clone()))), .and_then(|l| l.uris.and_then(|p| p.first().map(|u| u.uri.clone()))),
note: value.notes, value.notes,
} )
} }
} }
impl From<Vault> for BitWardenPassword { impl From<Vault<Plain>> for BitWardenPassword {
fn from(value: Vault) -> Self { fn from(value: Vault<Plain>) -> Self {
Self { Self {
ty: 1, ty: 1,
name: value.name, name: value.name,
@ -76,13 +76,13 @@ impl From<Vault> for BitWardenPassword {
} }
} }
impl From<Vaults> for BitWardenPasswords { impl From<Vaults<Plain>> for BitWardenPasswords {
fn from(value: Vaults) -> Self { fn from(value: Vaults<Plain>) -> Self {
Self { Self {
encrypted: false, encrypted: false,
folders: Vec::new(), folders: Vec::new(),
items: value items: value
.passwords .vaults
.into_iter() .into_iter()
.map(BitWardenPassword::from) .map(BitWardenPassword::from)
.collect(), .collect(),

View file

@ -18,15 +18,15 @@ use std::{fs, path::Path};
use crate::LprsResult; use crate::LprsResult;
use super::Vault; use super::{vault_state::*, Vault};
/// Return if the password file new file or not /// Return if the vaults file new file or not
pub fn is_new_password_file(path: &Path) -> LprsResult<bool> { pub fn is_new_vaults_file(path: &Path) -> LprsResult<bool> {
if path.exists() { if path.exists() {
let file_content = fs::read_to_string(path)?; let file_content = fs::read_to_string(path)?;
if !file_content.is_empty() if !file_content.is_empty()
&& file_content.trim() != "[]" && file_content.trim() != "[]"
&& serde_json::from_str::<Vec<Vault>>(&file_content).is_ok() && serde_json::from_str::<Vec<Vault<Encrypted>>>(&file_content).is_ok()
{ {
return Ok(false); return Ok(false);
} }