feat: Encrypt the hole vault file #26

Merged
awiteb merged 4 commits from awiteb/fix-5 into master 2024-05-03 07:51:41 +02:00 AGit
21 changed files with 485 additions and 623 deletions

View file

@ -12,7 +12,7 @@ jobs:
runs-on: debian runs-on: debian
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: https://codeberg.org/TheAwiteb/rust-action@v1.70 - uses: https://codeberg.org/TheAwiteb/rust-action@v1.74
- name: Build the source code - name: Build the source code
run: cargo build run: cargo build
- name: Check the code format - name: Check the code format

645
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -7,28 +7,28 @@ authors = ["Awiteb <a@4rs.nl>"]
readme = "README.md" readme = "README.md"
description = "A local CLI password manager" description = "A local CLI password manager"
repository = "https://git.4rs.nl/awiteb/lprs" repository = "https://git.4rs.nl/awiteb/lprs"
rust-version = "1.70.0" rust-version = "1.74.0"
keywords = ["password", "manager", "CLI"] keywords = ["password", "manager", "CLI"]
categories = ["command-line-utilities"] categories = ["command-line-utilities"]
[dependencies] [dependencies]
base64 = "0.21.5" clap = { version = "4.5.4", features = ["derive"] }
clap = { version = "4.4.11", features = ["derive"] }
comfy-table = "7.1.0"
directories = "5.0.1" directories = "5.0.1"
log = "0.4.20" log = "0.4.21"
passwords = { version = "3.1.16", features = ["common-password"] } passwords = { version = "3.1.16", features = ["common-password"] }
pretty_env_logger = "0.5.0" pretty_env_logger = "0.5.0"
regex = "1.10.2" regex = "1.10.4"
serde = { version = "1.0.193", features = ["derive"] } serde = { version = "1.0.200", features = ["derive"] }
serde_json = "1.0.108" thiserror = "1.0.59"
serde_with_macros = "3.4.0" reqwest = {version = "0.11.27", optional = true}
sha256 = { version = "1.4.0", default-features = false } inquire = "0.7.5"
soft-aes = "0.1.0" bincode = "1.3.3"
thiserror = "1.0.51" rand = "0.8.5"
url = { version = "2.5.0", features = ["serde"] } cbc = { version = "0.1.2", features = ["alloc", "std"] }
reqwest = {version = "0.11.23", optional = true} aes = "0.8.4"
inquire = "0.7.4" sha2 = "0.10.8"
serde_json = "1.0.116"
base64 = "0.22.1"
[features] [features]
default = ["update-notify"] default = ["update-notify"]

View file

@ -17,7 +17,7 @@
use clap::Args; use clap::Args;
use crate::{ use crate::{
vault::{vault_state::*, Vault, Vaults}, vault::{Vault, Vaults},
LprsCommand, LprsError, LprsResult, LprsCommand, LprsError, LprsResult,
}; };
@ -25,14 +25,14 @@ use crate::{
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
pub struct Add { pub struct Add {
#[command(flatten)] #[command(flatten)]
vault_info: Vault<Plain>, 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)]
password: Option<Option<String>>, password: Option<Option<String>>,
} }
impl LprsCommand for Add { impl LprsCommand for Add {
fn run(mut self, mut vault_manager: Vaults<Plain>) -> LprsResult<()> { fn run(mut self, mut vault_manager: Vaults) -> LprsResult<()> {
match self.password { match self.password {
Some(Some(password)) => { Some(Some(password)) => {
log::debug!("User provided a password"); log::debug!("User provided a password");

View file

@ -18,21 +18,18 @@ use std::fs;
use clap::Args; use clap::Args;
use crate::{ use crate::{vault::Vaults, LprsCommand, LprsError, LprsResult};
vault::{vault_state::*, 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 {} pub struct Clean {}
impl LprsCommand for Clean { impl LprsCommand for Clean {
fn run(self, vault_manager: Vaults<Plain>) -> LprsResult<()> { fn run(self, vault_manager: Vaults) -> LprsResult<()> {
log::info!( log::info!(
"Cleaning the vaults file: {:?}", "Cleaning the vaults file: {:?}",
vault_manager.vaults_file.display() vault_manager.vaults_file.display()
); );
fs::write(vault_manager.vaults_file, "[]").map_err(LprsError::Io) fs::write(vault_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::{
vault::{vault_state::*, Vault, Vaults}, vault::{Vault, Vaults},
LprsCommand, LprsError, LprsResult, LprsCommand, LprsError, LprsResult,
}; };
@ -47,7 +47,7 @@ pub struct Edit {
} }
impl LprsCommand for Edit { impl LprsCommand for Edit {
fn run(self, mut vault_manager: Vaults<Plain>) -> LprsResult<()> { fn run(self, mut vault_manager: Vaults) -> LprsResult<()> {
let index = self.index.get() as usize; let index = self.index.get() as usize;
log::debug!("Editing vault at index: {index}"); log::debug!("Editing vault at index: {index}");
@ -73,7 +73,7 @@ impl LprsCommand for Edit {
}; };
log::info!("Applying the new values to the vault"); log::info!("Applying the new values to the vault");
*vault = Vault::<Plain>::new( *vault = Vault::new(
self.name.as_ref().unwrap_or(&vault.name), self.name.as_ref().unwrap_or(&vault.name),
self.username.as_ref().or(vault.username.as_ref()), self.username.as_ref().or(vault.username.as_ref()),
password.as_ref().or(vault.password.as_ref()), password.as_ref().or(vault.password.as_ref()),

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::{
vault::{vault_state::*, BitWardenPasswords, Format, Vault, Vaults}, vault::{BitWardenPasswords, Format, Vaults},
LprsCommand, LprsError, LprsResult, LprsCommand, LprsError, LprsResult,
}; };
@ -31,10 +31,11 @@ pub struct Export {
/// Format to export vaults in /// Format to export vaults in
#[arg(short, long, value_name = "FORMAT", default_value_t= Format::Lprs)] #[arg(short, long, value_name = "FORMAT", default_value_t= Format::Lprs)]
format: Format, format: Format,
// TODO: `force` flag to write on existing file
} }
impl LprsCommand for Export { impl LprsCommand for Export {
fn run(self, vault_manager: Vaults<Plain>) -> LprsResult<()> { fn run(self, vault_manager: Vaults) -> LprsResult<()> {
log::debug!( log::debug!(
"Exporting vault {} to: {} with format: {}", "Exporting vault {} to: {} with format: {}",
vault_manager.vaults_file.display(), vault_manager.vaults_file.display(),
@ -42,11 +43,9 @@ impl LprsCommand for Export {
self.format self.format
); );
let exported_data = match self.format { let exported_data = match self.format {
Format::Lprs => { Format::Lprs => vault_manager.json_export()?,
serde_json::to_string::<Vec<Vault<Encrypted>>>(&vault_manager.encrypt_vaults()?) Format::BitWarden => serde_json::to_string(&BitWardenPasswords::from(vault_manager))?,
} };
Format::BitWarden => serde_json::to_string(&BitWardenPasswords::from(vault_manager)),
}?;
fs::write(&self.path, exported_data).map_err(LprsError::from) fs::write(&self.path, exported_data).map_err(LprsError::from)
} }

View file

@ -18,10 +18,7 @@ use std::num::NonZeroU64;
use clap::Args; use clap::Args;
use crate::{ use crate::{vault::Vaults, LprsCommand, LprsError, LprsResult};
vault::{vault_state::*, Vaults},
LprsCommand, LprsError, LprsResult,
};
#[derive(Debug, Args)] #[derive(Debug, Args)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
@ -45,7 +42,7 @@ pub struct Gen {
} }
impl LprsCommand for Gen { impl LprsCommand for Gen {
fn run(self, _vault_manager: Vaults<Plain>) -> LprsResult<()> { fn run(self, _vault_manager: Vaults) -> LprsResult<()> {
println!( println!(
"{}", "{}",
passwords::PasswordGenerator::new() passwords::PasswordGenerator::new()

View file

@ -14,12 +14,17 @@
// 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::File, io::Error as IoError, io::ErrorKind as IoErrorKind, path::PathBuf}; use std::{
fs::{self, File},
io::Error as IoError,
io::ErrorKind as IoErrorKind,
path::PathBuf,
};
use clap::Args; use clap::Args;
use crate::{ use crate::{
vault::{vault_state::*, BitWardenPasswords, Format, Vault, Vaults}, vault::{BitWardenPasswords, Format, Vault, Vaults},
LprsCommand, LprsError, LprsResult, LprsCommand, LprsError, LprsResult,
}; };
@ -35,7 +40,7 @@ pub struct Import {
} }
impl LprsCommand for Import { impl LprsCommand for Import {
fn run(self, mut vault_manager: Vaults<Plain>) -> LprsResult<()> { fn run(self, mut vault_manager: Vaults) -> LprsResult<()> {
log::debug!( log::debug!(
"Importing vaults from: {} with format: {} to the vault: {}", "Importing vaults from: {} with format: {} to the vault: {}",
self.path.display(), self.path.display(),
@ -45,10 +50,11 @@ impl LprsCommand for Import {
let imported_passwords_len = match self.format { let imported_passwords_len = match self.format {
Format::Lprs => { Format::Lprs => {
let vaults = Vaults::try_reload(self.path, vault_manager.master_password.to_vec())?; let vaults =
let vaults_len = vaults.vaults.len(); Vaults::json_reload(&vault_manager.master_password, &fs::read(self.path)?)?;
let vaults_len = vaults.len();
vault_manager.vaults.extend(vaults.vaults); vault_manager.vaults.extend(vaults);
vault_manager.try_export()?; vault_manager.try_export()?;
vaults_len vaults_len
} }

View file

@ -19,10 +19,7 @@ use std::num::NonZeroU64;
use clap::Args; use clap::Args;
use inquire::Select; use inquire::Select;
use crate::{ use crate::{vault::Vaults, LprsCommand, LprsError, LprsResult};
vault::{vault_state::*, Vaults},
LprsCommand, LprsError, LprsResult,
};
#[derive(Debug, Args)] #[derive(Debug, Args)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
@ -39,7 +36,7 @@ pub struct List {
} }
impl LprsCommand for List { impl LprsCommand for List {
fn run(self, vault_manager: Vaults<Plain>) -> LprsResult<()> { fn run(self, vault_manager: Vaults) -> LprsResult<()> {
if vault_manager.vaults.is_empty() { if vault_manager.vaults.is_empty() {
return Err(LprsError::Other( return Err(LprsError::Other(
"Looks like there is no vaults to list".to_owned(), "Looks like there is no vaults to list".to_owned(),

View file

@ -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::path::PathBuf; use std::{fs, path::PathBuf};
use clap::Parser; use clap::Parser;
@ -73,6 +73,13 @@ impl Cli {
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");
if let Some(parent) = path.parent() {
if parent.to_str() != Some("") && !parent.exists() {
log::info!("Creating the parent vaults file directory");
fs::create_dir_all(parent)?;
}
}
fs::File::create(&path)?;
path path
} else { } else {
log::info!("Using the default vaults file"); log::info!("Using the default vaults file");
@ -91,11 +98,9 @@ impl Cli {
} }
} else { } else {
log::info!("Reloading the vaults file"); log::info!("Reloading the vaults file");
let master_password = utils::master_password_prompt(&vaults_file)?; let master_password =
Vaults::try_reload( utils::master_password_prompt(fs::read(&vaults_file)?.is_empty())?;
vaults_file, Vaults::try_reload(vaults_file, master_password)?
master_password.into_bytes().into_iter().take(32).collect(),
)?
}; };
self.command.run(vault_manager) self.command.run(vault_manager)

View file

@ -18,10 +18,7 @@ use std::num::NonZeroU64;
use clap::Args; use clap::Args;
use crate::{ use crate::{vault::Vaults, LprsCommand, LprsError, LprsResult};
vault::{vault_state::*, Vaults},
LprsCommand, LprsError, LprsResult,
};
#[derive(Debug, Args)] #[derive(Debug, Args)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
@ -35,7 +32,7 @@ pub struct Remove {
} }
impl LprsCommand for Remove { impl LprsCommand for Remove {
fn run(self, mut vault_manager: Vaults<Plain>) -> LprsResult<()> { fn run(self, mut vault_manager: Vaults) -> LprsResult<()> {
let index = (self.index.get() - 1) as usize; let index = (self.index.get() - 1) as usize;
log::debug!("Removing vault at index: {index}"); log::debug!("Removing vault at index: {index}");

View file

@ -20,15 +20,11 @@ pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
#[error("Invalid Json Path Error: {0}")]
InvalidJsonPath(String),
#[error("Encryption Error: {0}")] #[error("Encryption Error: {0}")]
Encryption(String), Encryption(String),
#[error("Decryption Error: {0}")] #[error("Decryption Error: The given key cannot decrypt the given data. Either the data has been tampered with or the key is incorrect.")]
Decryption(String), Decryption,
#[error( #[error("Wrong Master Password Error: Wrong decryption password")]
"Wrong Master Password Error: Wrong password or you may have played with the password file"
)]
WrongMasterPassword, WrongMasterPassword,
#[error("Weak Password Error: {0}")] #[error("Weak Password Error: {0}")]
WeakPassword(String), WeakPassword(String),
@ -45,6 +41,8 @@ pub enum Error {
InvalidRegex(#[from] regex::Error), InvalidRegex(#[from] regex::Error),
#[error("UTF8 Error: {0}")] #[error("UTF8 Error: {0}")]
Utf8(#[from] FromUtf8Error), Utf8(#[from] FromUtf8Error),
#[error("Bincode Error: {0}")]
Bincode(#[from] bincode::Error),
#[error("Base64 Decode Error: {0}")] #[error("Base64 Decode Error: {0}")]
BaseDecodeError(#[from] base64::DecodeError), BaseDecodeError(#[from] base64::DecodeError),
#[error("Json Error: {0}")] #[error("Json Error: {0}")]

View file

@ -53,7 +53,7 @@ macro_rules! impl_commands {
($enum_name: ident, $($varint: ident)+) => { ($enum_name: ident, $($varint: ident)+) => {
#[automatically_derived] #[automatically_derived]
impl $crate::LprsCommand for $enum_name{ impl $crate::LprsCommand for $enum_name{
fn run(self, vault_manager: $crate::vault::Vaults<$crate::vault::vault_state::Plain>) -> $crate::LprsResult<()> { fn run(self, vault_manager: $crate::vault::Vaults) -> $crate::LprsResult<()> {
match self { match self {
$( $(
Self::$varint(command) => command.run(vault_manager), Self::$varint(command) => command.run(vault_manager),

View file

@ -14,13 +14,9 @@
// 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;
use base64::{
alphabet,
engine::{general_purpose::PAD, GeneralPurpose},
};
use clap::Parser; use clap::Parser;
use inquire::InquireError;
use std::process::ExitCode;
pub mod cli; pub mod cli;
pub mod errors; pub mod errors;
@ -30,12 +26,11 @@ pub mod vault;
mod macros; mod macros;
mod traits; mod traits;
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};
use inquire::InquireError;
pub use traits::*; pub use traits::*;
pub const STANDARDBASE: GeneralPurpose = GeneralPurpose::new(&alphabet::STANDARD, PAD); pub const DEFAULT_VAULTS_FILE: &str = "vaults.lprs";
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

@ -14,15 +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::{ use crate::{vault::Vaults, LprsResult};
vault::{vault_state::*, Vaults},
LprsResult,
};
/// Trait to work with the commands /// Trait to work with the commands
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<Plain>) -> LprsResult<()>; fn run(self, vault_manager: Vaults) -> LprsResult<()>;
/// Validate the gaiven args from the user. /// Validate the gaiven args from the user.
fn validate_args(&self) -> LprsResult<()> { fn validate_args(&self) -> LprsResult<()> {

View file

@ -14,14 +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 std::{ use std::{fs, path::PathBuf};
fs,
path::{Path, PathBuf},
};
use inquire::validator::Validation; use inquire::validator::Validation;
use sha2::Digest;
use crate::{vault, 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
pub fn local_project_file(filename: &str) -> LprsResult<PathBuf> { pub fn local_project_file(filename: &str) -> LprsResult<PathBuf> {
@ -42,11 +40,7 @@ pub fn local_project_file(filename: &str) -> LprsResult<PathBuf> {
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() {
log::info!( fs::File::create(&vaults_file)?;
"Vaults file not found, creating a new one: {:?}",
vaults_file.display()
);
fs::write(&vaults_file, "[]")?;
} }
Ok(vaults_file) Ok(vaults_file)
} }
@ -71,9 +65,9 @@ pub fn password_validator(password: &str) -> Result<Validation, inquire::CustomU
} }
/// Ask the user for the master password, then returns it /// Ask the user for the master password, then returns it
pub fn master_password_prompt(vaults_file: &Path) -> LprsResult<String> { ///
let is_new_vaults_file = vault::is_new_vaults_file(vaults_file)?; /// 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]> {
inquire::Password { inquire::Password {
message: "Master Password:", message: "Master Password:",
enable_confirmation: is_new_vaults_file, enable_confirmation: is_new_vaults_file,
@ -87,7 +81,7 @@ pub fn master_password_prompt(vaults_file: &Path) -> LprsResult<String> {
.with_formatter(&|p| "*".repeat(p.chars().count())) .with_formatter(&|p| "*".repeat(p.chars().count()))
.with_display_mode(inquire::PasswordDisplayMode::Masked) .with_display_mode(inquire::PasswordDisplayMode::Masked)
.prompt() .prompt()
.map(sha256::digest) .map(|p| sha2::Sha256::digest(p).into())
.map_err(Into::into) .map_err(Into::into)
} }

View file

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::{vault_state::*, Vault, Vaults}; use super::{Vault, Vaults};
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct BitWardenLoginData { pub struct BitWardenLoginData {
@ -39,7 +39,7 @@ pub struct BitWardenPasswords {
pub items: Vec<BitWardenPassword>, pub items: Vec<BitWardenPassword>,
} }
impl From<BitWardenPassword> for Vault<Plain> { impl From<BitWardenPassword> for Vault {
fn from(value: BitWardenPassword) -> Self { fn from(value: BitWardenPassword) -> Self {
Self::new( Self::new(
value.name, value.name,
@ -55,8 +55,8 @@ impl From<BitWardenPassword> for Vault<Plain> {
} }
} }
impl From<Vault<Plain>> for BitWardenPassword { impl From<Vault> for BitWardenPassword {
fn from(value: Vault<Plain>) -> Self { fn from(value: Vault) -> Self {
Self { Self {
ty: 1, ty: 1,
name: value.name, name: value.name,
@ -72,8 +72,8 @@ impl From<Vault<Plain>> for BitWardenPassword {
} }
} }
impl From<Vaults<Plain>> for BitWardenPasswords { impl From<Vaults> for BitWardenPasswords {
fn from(value: Vaults<Plain>) -> Self { fn from(value: Vaults) -> Self {
Self { Self {
encrypted: false, encrypted: false,
folders: Vec::new(), folders: Vec::new(),

View file

@ -14,53 +14,44 @@
// 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 base64::Engine; use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
use soft_aes::aes::{aes_dec_ecb, aes_enc_ecb}; use rand::{rngs::StdRng, Rng, SeedableRng};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::{LprsError, LprsResult}; use crate::{LprsError, LprsResult};
/// Encrypt the string with AEC ECB type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
pub fn encrypt(master_password: &[u8], data: &str) -> LprsResult<String> { type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
let padding = Some("PKCS7");
aes_enc_ecb(data.as_bytes(), master_password, padding) /// Encrypt the given data by the given key using AES-256 CBC
.map(|d| crate::STANDARDBASE.encode(d)) ///
.map_err(|err| LprsError::Encryption(err.to_string())) /// Note: The IV will be add it to the end of the ciphertext (Last 16 bytes)
} pub(crate) fn encrypt(master_password: &[u8; 32], data: &[u8]) -> Vec<u8> {
let iv: [u8; 16] = StdRng::seed_from_u64(
/// Decrypt the string with AEC ECB SystemTime::now()
pub fn decrypt(master_password: &[u8], data: &str) -> LprsResult<String> { .duration_since(UNIX_EPOCH)
let padding = Some("PKCS7"); .expect("SystemTime before UNIX EPOCH!")
.as_secs(),
aes_dec_ecb(
crate::STANDARDBASE.decode(data)?.as_slice(),
master_password,
padding,
) )
.map_err(|err| { .gen();
if err.to_string().contains("Invalid padding") {
LprsError::WrongMasterPassword let mut ciphertext =
} else { Aes256CbcEnc::new(master_password.into(), &iv.into()).encrypt_padded_vec_mut::<Pkcs7>(data);
LprsError::Decryption(err.to_string()) ciphertext.extend(&iv);
} ciphertext
})
.map(|d| String::from_utf8(d).map_err(LprsError::Utf8))?
} }
/// Encrypt if the `Option` are `Some` /// Decrypt the given data by the given key, the data should
pub fn encrypt_some( /// be encrypted by AES-256 CBC. The IV will be extraxted
master_password: &[u8], /// from the last 16 bytes.
data: Option<impl AsRef<str>>, pub(crate) fn decrypt(master_password: &[u8; 32], data: &[u8]) -> LprsResult<Vec<u8>> {
) -> LprsResult<Option<String>> { let (ciphertext, iv) = data.split_at(
data.map(|d| encrypt(master_password, d.as_ref())) data.len()
.transpose() .checked_sub(16)
} .ok_or_else(|| LprsError::Decryption)?,
);
/// Decrypt if the `Option` are `Some` Aes256CbcDec::new(master_password.into(), iv.into())
pub fn decrypt_some( .decrypt_padded_vec_mut::<Pkcs7>(ciphertext)
master_password: &[u8], .map_err(|_| LprsError::Decryption)
data: Option<impl AsRef<str>>,
) -> LprsResult<Option<String>> {
data.map(|d| decrypt(master_password, d.as_ref()))
.transpose()
} }

View file

@ -14,21 +14,19 @@
// 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, marker::PhantomData, path::PathBuf}; use std::{fs, path::PathBuf};
use base64::Engine;
use clap::{Parser, ValueEnum}; use clap::{Parser, ValueEnum};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{LprsError, LprsResult}; use crate::{LprsError, LprsResult};
use vault_state::*;
pub mod cipher; pub mod cipher;
mod bitwarden; mod bitwarden;
mod validator;
pub use bitwarden::*; pub use bitwarden::*;
pub use validator::*;
#[derive(Clone, Debug, ValueEnum)] #[derive(Clone, Debug, ValueEnum)]
pub enum Format { pub enum Format {
@ -36,23 +34,9 @@ pub enum Format {
BitWarden, 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 /// The vault struct
#[serde_with_macros::skip_serializing_none]
#[derive(Clone, Debug, Deserialize, Serialize, Parser)] #[derive(Clone, Debug, Deserialize, Serialize, Parser)]
pub struct Vault<T> pub struct Vault {
where
T: std::fmt::Debug + Clone,
{
/// The name of the vault /// The name of the vault
#[arg(short, long)] #[arg(short, long)]
pub name: String, pub name: String,
@ -68,31 +52,20 @@ where
/// Add a note to the vault /// Add a note to the vault
#[arg(short = 'o', long)] #[arg(short = 'o', long)]
pub note: Option<String>, pub note: Option<String>,
/// State phantom
#[serde(skip)]
#[arg(skip)]
phantom: PhantomData<T>,
} }
/// The vaults manager /// The vaults manager
#[derive(Default)] #[derive(Default)]
pub struct Vaults<T> pub struct Vaults {
where
T: std::fmt::Debug + Clone,
{
/// Hash of the master password /// Hash of the master password
pub master_password: Vec<u8>, pub master_password: [u8; 32],
/// The json vaults file /// The json vaults file
pub vaults_file: PathBuf, pub vaults_file: PathBuf,
/// The vaults /// The vaults
pub vaults: Vec<Vault<T>>, pub vaults: Vec<Vault>,
} }
impl<T> Vault<T> impl Vault {
where
T: std::fmt::Debug + Clone,
{
/// Create new [`Vault`] instance /// Create new [`Vault`] instance
pub fn new( pub fn new(
name: impl Into<String>, name: impl Into<String>,
@ -107,35 +80,8 @@ where
password: password.map(Into::into), password: password.map(Into::into),
service: service.map(Into::into), service: service.map(Into::into),
note: note.map(Into::into), note: note.map(Into::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_some(master_password, self.username.as_ref())?,
cipher::decrypt_some(master_password, self.password.as_ref())?,
cipher::decrypt_some(master_password, self.service.as_ref())?,
cipher::decrypt_some(master_password, self.note.as_ref())?,
))
}
}
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_some(master_password, self.username.as_ref())?,
cipher::encrypt_some(master_password, self.password.as_ref())?,
cipher::encrypt_some(master_password, self.service.as_ref())?,
cipher::encrypt_some(master_password, self.note.as_ref())?,
))
}
/// 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 {
@ -154,38 +100,75 @@ impl Vault<Plain> {
} }
} }
impl<T> Vaults<T> impl Vaults {
where
T: std::fmt::Debug + Clone,
{
/// Create new [`Vaults`] instnce /// Create new [`Vaults`] instnce
pub fn new(master_password: Vec<u8>, vaults_file: PathBuf, vaults: Vec<Vault<T>>) -> Self { pub fn new(master_password: [u8; 32], vaults_file: PathBuf, vaults: Vec<Vault>) -> Self {
Self { Self {
master_password, master_password,
vaults_file, vaults_file,
vaults, vaults,
} }
} }
}
impl Vaults<Plain> { /// Add new vault
/// Encrypt the vaults pub fn add_vault(&mut self, vault: Vault) {
pub fn encrypt_vaults(&self) -> LprsResult<Vec<Vault<Encrypted>>> { self.vaults.push(vault)
self.vaults
.iter()
.map(|p| p.encrypt(&self.master_password))
.collect()
} }
/// Reload the vaults from the file then decrypt it /// Encrypt the vaults then returns it as json.
pub fn try_reload(vaults_file: PathBuf, master_password: Vec<u8>) -> LprsResult<Self> { ///
let vaults = /// This function used to backup the vaults.
serde_json::from_str::<Vec<Vault<Encrypted>>>(&fs::read_to_string(&vaults_file)?)? ///
.into_iter() /// Note: The returned string is `Vec<Vault>`
.map(|p| p.decrypt(master_password.as_slice())) pub fn json_export(&self) -> LprsResult<String> {
.collect::<LprsResult<Vec<Vault<Plain>>>>()?; let encrypt = |val: &str| {
LprsResult::Ok(
crate::BASE64.encode(cipher::encrypt(&self.master_password, val.as_ref())),
)
};
Ok(Self::new(master_password, vaults_file, vaults)) serde_json::to_string(
&self
.vaults
.iter()
.map(|v| {
LprsResult::Ok(Vault::new(
encrypt(&v.name)?,
v.username.as_ref().and_then(|u| encrypt(u).ok()),
v.password.as_ref().and_then(|p| encrypt(p).ok()),
v.service.as_ref().and_then(|s| encrypt(s).ok()),
v.note.as_ref().and_then(|n| encrypt(n).ok()),
))
})
.collect::<LprsResult<Vec<_>>>()?,
)
.map_err(Into::into)
}
/// Reload the vaults from json data.
///
/// This function used to import backup vaults.
pub fn json_reload(master_password: &[u8; 32], json_data: &[u8]) -> LprsResult<Vec<Vault>> {
let decrypt = |val: &str| {
String::from_utf8(cipher::decrypt(
master_password,
&crate::BASE64.decode(val)?,
)?)
.map_err(|err| LprsError::Other(err.to_string()))
};
serde_json::from_slice::<Vec<Vault>>(json_data)?
.into_iter()
.map(|v| {
LprsResult::Ok(Vault::new(
decrypt(&v.name)?,
v.username.as_ref().and_then(|u| decrypt(u).ok()),
v.password.as_ref().and_then(|p| decrypt(p).ok()),
v.service.as_ref().and_then(|s| decrypt(s).ok()),
v.note.as_ref().and_then(|n| decrypt(n).ok()),
))
})
.collect()
} }
/// Encrypt the vaults then export it to the file /// Encrypt the vaults then export it to the file
@ -196,14 +179,22 @@ impl Vaults<Plain> {
); );
fs::write( fs::write(
&self.vaults_file, &self.vaults_file,
serde_json::to_string(&self.encrypt_vaults()?)?, cipher::encrypt(&self.master_password, &bincode::serialize(&self.vaults)?),
) )
.map_err(LprsError::Io) .map_err(LprsError::Io)
} }
/// Add new vault /// Reload the vaults from the file then decrypt it
pub fn add_vault(&mut self, vault: Vault<Plain>) { pub fn try_reload(vaults_file: PathBuf, master_password: [u8; 32]) -> LprsResult<Self> {
self.vaults.push(vault) let vaults_data = fs::read(&vaults_file)?;
let vaults: Vec<Vault> = if vaults_data.is_empty() {
vec![]
} else {
bincode::deserialize(&cipher::decrypt(&master_password, &vaults_data)?)?
};
Ok(Self::new(master_password, vaults_file, vaults))
} }
} }
@ -219,7 +210,7 @@ impl std::fmt::Display for Format {
} }
} }
impl<T: std::fmt::Debug + Clone> std::fmt::Display for Vault<T> { impl std::fmt::Display for Vault {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::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 {

View file

@ -1,35 +0,0 @@
// 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, path::Path};
use crate::LprsResult;
use super::{vault_state::*, Vault};
/// 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<Vault<Encrypted>>>(&file_content).is_ok()
{
return Ok(false);
}
}
Ok(true)
}