chore: password refactor #11

Merged
awiteb merged 4 commits from password-refactor into master 2024-03-19 08:21:21 +01:00
20 changed files with 385 additions and 334 deletions

View file

@ -1,6 +1,6 @@
# Lprs
Lprs is a local password manager designed to securely store and manage your passwords.
Lprs is a local vault manager designed to securely store and manage your vaults.
### MSRV
The Minimum Supported Rust Version (MSRV) is `1.70.0`.
@ -31,7 +31,7 @@ cargo uninstall lprs
## Usage
Lprs provides a command-line interface for managing your passwords. The following commands are available:
Lprs provides a command-line interface for managing your vaults. The following commands are available:
```
A local CLI password manager
@ -39,23 +39,20 @@ A local CLI password manager
Usage: lprs [OPTIONS] <COMMAND>
Commands:
add Add new password
remove Remove password
list List your password and search
clean Clean the password file
edit Edit the password content
gen Generate password
export Export the passwords
import Import passwords
add Add new vault
remove Remove vault
list List your vaults and search
clean Clean the vaults file
edit Edit the vault content
gen Generate a password
export Export the vaults
import Import vaults
help Print this message or the help of the given subcommand(s)
Options:
-p, --passwords-file <PASSWORDS_FILE>
The passwords json file, default: $HOME/.local/share/lprs/passwords.json
-h, --help
Print help
-V, --version
Print version
-v, --vaults-file <VAULTS_FILE> The vaults json file
-h, --help Print help
-V, --version Print version
```
### Example
@ -78,7 +75,7 @@ Master Password: ***************
### Backup
It is important to regularly backup your passwords to prevent data loss. Lprs does not provide an automatic backup feature. To backup your passwords, you can use the `export` command provided by Lprs. This command allows you to export your encrypted passwords to a json file, which you can then manually backup to a secure location.
It is important to regularly backup your vaults to prevent data loss. Lprs does not provide an automatic backup feature. To backup your vaults, you can use the `export` command provided by Lprs. This command allows you to export your encrypted vaults to a json file, which you can then manually backup to a secure location.
#### Formats
The format of the exported file can be specified using the `--format` option. The following formats are supported:

View file

@ -1,4 +1,4 @@
// Lprs - A local CLI password manager
// 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
@ -17,7 +17,7 @@
use clap::Args;
use crate::{
password::{Password, Passwords},
vault::{vault_state::*, 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<Plain>,
}
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<Plain>) -> LprsResult<()> {
vault_manager.add_vault(self.vault_info.clone());
vault_manager.try_export()
}
}

View file

@ -1,4 +1,4 @@
// Lprs - A local CLI password manager
// 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
@ -18,14 +18,17 @@ use std::fs;
use clap::Args;
use crate::{password::Passwords, LprsError, LprsResult, RunCommand};
use crate::{
vault::{vault_state::*, 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<()> {
fs::write(password_manager.passwords_file, "[]").map_err(LprsError::Io)
fn run(&self, vault_manager: Vaults<Plain>) -> LprsResult<()> {
fs::write(vault_manager.vaults_file, "[]").map_err(LprsError::Io)
}
}

View file

@ -1,4 +1,4 @@
// Lprs - A local CLI password manager
// 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
@ -19,7 +19,7 @@ use std::num::NonZeroU64;
use clap::Args;
use crate::{
password::{Password, Passwords},
vault::{vault_state::*, 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<String>,
#[arg(short, long)]
/// The new password username
/// The new vault username
username: Option<String>,
#[arg(short, long)]
/// The new password
password: Option<String>,
#[arg(short, long)]
/// The new password service
/// The new vault service
service: Option<String>,
#[arg(short = 'o', long)]
/// The new password note
/// The new vault note
note: Option<String>,
}
impl RunCommand for Edit {
fn run(&self, mut password_manager: Passwords) -> LprsResult<()> {
fn run(&self, mut vault_manager: Vaults<Plain>) -> 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,28 +61,20 @@ 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::<Plain>::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()
vault_manager.vaults.len()
)))
}
}

View file

@ -1,4 +1,4 @@
// Lprs - A local CLI password manager
// 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
@ -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},
vault::{vault_state::*, BitWardenPasswords, Format, Vault, 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, vault_manager: Vaults<Plain>) -> LprsResult<()> {
if self
.path
.extension()
@ -42,9 +42,11 @@ impl RunCommand for Export {
{
if !self.path.exists() {
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>>>(
&vault_manager.encrypt_vaults()?,
),
Format::BitWarden => {
serde_json::to_string(&BitWardenPasswords::from(password_manager))
serde_json::to_string(&BitWardenPasswords::from(vault_manager))
}
}?;

View file

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

View file

@ -1,4 +1,4 @@
// Lprs - A local CLI password manager
// 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
@ -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},
vault::{vault_state::*, 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 vault_manager: Vaults<Plain>) -> 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(),
vault_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 { "" }
);

View file

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

View file

@ -1,4 +1,4 @@
// Lprs - A local CLI password manager
// 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
@ -19,7 +19,7 @@ use std::path::PathBuf;
use clap::Parser;
use crate::{
password::{self, Passwords},
vault::{self, Vaults},
LprsError, LprsResult, RunCommand,
};
@ -34,22 +34,22 @@ pub mod remove_command;
crate::create_commands!(
enum Commands
"Add new password", Add => add_command::Add
"Remove password", Remove => remove_command::Remove
"List your password and search", List => list_command::List
"Clean the password file", Clean => clean_command::Clean
"Edit the password content", Edit => edit_command::Edit
"Generate password", Gen => gen_command::Gen
"Export the passwords", Export => export_command::Export
"Import passwords", Import => import_command::Import
"Add new vault", Add => add_command::Add
"Remove vault", Remove => remove_command::Remove
"List your vaults and search", List => list_command::List
"Clean the vaults file", Clean => clean_command::Clean
"Edit the vault content", Edit => edit_command::Edit
"Generate a password", Gen => gen_command::Gen
"Export the vaults", Export => export_command::Export
"Import vaults", Import => import_command::Import
);
#[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<PathBuf>,
vaults_file: Option<PathBuf>,
// TODO: verbose flag
#[command(subcommand)]
@ -59,43 +59,40 @@ pub struct Cli {
impl Cli {
/// Run the cli
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()
} else {
crate::utils::passwords_file()?
crate::utils::vaults_file()?
};
log::debug!(
"Getting password file: {}",
passwords_file.to_string_lossy()
);
let password_manager = if matches!(self.command, Commands::Clean(..) | Commands::Gen(..)) {
Passwords {
passwords_file,
log::debug!("Getting the vaults file: {}", vaults_file.to_string_lossy());
let vault_manager = if matches!(self.command, Commands::Clean(..) | Commands::Gen(..)) {
Vaults {
vaults_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(
passwords_file,
let master_password = sha256::digest(master_password);
Vaults::try_reload(
vaults_file,
master_password.into_bytes().into_iter().take(32).collect(),
)?
};
self.command.run(password_manager)?;
self.command.run(vault_manager)?;
Ok(())
}

View file

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

View file

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

View file

@ -1,4 +1,4 @@
// Lprs - A local CLI password manager
// 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
@ -40,11 +40,11 @@
/// 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),
/// Self::Some(command) => command.run(password_manager),
/// Self::Test(command) => command.run(vault_manager),
/// Self::Some(command) => command.run(vault_manager),
/// }
/// }
/// }
@ -63,10 +63,10 @@ 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, vault_manager: $crate::vault::Vaults<$crate::vault::vault_state::Plain>) -> $crate::LprsResult<()> {
match self {
$(
Self::$varint(command) => command.run(password_manager),
Self::$varint(command) => command.run(vault_manager),
)+
}
}

View file

@ -1,4 +1,4 @@
// Lprs - A local CLI password manager
// 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
@ -24,8 +24,8 @@ use clap::Parser;
pub mod cli;
pub mod errors;
pub mod password;
pub mod utils;
pub mod vault;
mod macros;
mod traits;
@ -34,7 +34,7 @@ pub use errors::{Error as LprsError, Result as LprsResult};
pub use traits::*;
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")]
pub const VERSION: &str = env!("CARGO_PKG_VERSION");

View file

@ -1,162 +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 Password {
/// 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 Passwords {
/// Hash of the master password
pub master_password: Vec<u8>,
/// The json passwords file
pub passwords_file: PathBuf,
/// The passwords
pub passwords: Vec<Password>,
}
impl Password {
/// 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 Passwords {
/// Create new Passwords instnce
pub fn new(
master_password: Vec<u8>,
passwords_file: PathBuf,
passwords: Vec<Password>,
) -> 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<Password>>>()?,
..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<Password>>(&fs::read_to_string(&passwords_file)?)?
.into_iter()
.map(|p| p.decrypt(master_password.as_slice()))
.collect::<LprsResult<Vec<Password>>>()?;
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: Password) {
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

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
// Lprs - A local CLI password manager
// 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

209
src/vault/mod.rs Normal file
View file

@ -0,0 +1,209 @@
// 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>.
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<T>
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<String>,
/// Add a note to the vault
#[arg(short = 'o', long)]
pub note: Option<String>,
/// State phantom
#[serde(skip)]
#[arg(skip)]
phantom: PhantomData<T>,
}
/// The vaults manager
#[derive(Default)]
pub struct Vaults<T>
where
T: std::fmt::Debug + Clone,
{
/// Hash of the master password
pub master_password: Vec<u8>,
/// The json vaults file
pub vaults_file: PathBuf,
/// The vaults
pub vaults: Vec<Vault<T>>,
}
impl<T> Vault<T>
where
T: std::fmt::Debug + Clone,
{
/// Create new [`Vault`] instance
pub fn new(
name: impl Into<String>,
username: impl Into<String>,
password: impl Into<String>,
service: Option<impl Into<String>>,
note: Option<impl Into<String>>,
) -> 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<Encrypted> {
/// Decrypt the vault
pub fn decrypt(&self, master_password: &[u8]) -> LprsResult<Vault<Plain>> {
Ok(Vault::<Plain>::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<Plain> {
/// Encrypt the vault
pub fn encrypt(&self, master_password: &[u8]) -> LprsResult<Vault<Encrypted>> {
Ok(Vault::<Encrypted>::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<T> Vaults<T>
where
T: std::fmt::Debug + Clone,
{
/// Create new [`Vaults`] instnce
pub fn new(master_password: Vec<u8>, vaults_file: PathBuf, vaults: Vec<Vault<T>>) -> Self {
Self {
master_password,
vaults_file,
vaults,
}
}
}
impl Vaults<Plain> {
/// Encrypt the vaults
pub fn encrypt_vaults(&self) -> LprsResult<Vec<Vault<Encrypted>>> {
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<u8>) -> LprsResult<Self> {
let vaults =
serde_json::from_str::<Vec<Vault<Encrypted>>>(&fs::read_to_string(&vaults_file)?)?
.into_iter()
.map(|p| p.decrypt(master_password.as_slice()))
.collect::<LprsResult<Vec<Vault<Plain>>>>()?;
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<Plain>) {
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()
}
}

View file

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