Compare commits
No commits in common. "6f6966d5b25b2b5047081304f7597fe80ec95387" and "0fe7a2fac3c4caf18d88ebd9a4fb3d9e595d0aaa" have entirely different histories.
6f6966d5b2
...
0fe7a2fac3
21 changed files with 624 additions and 486 deletions
|
@ -12,7 +12,7 @@ jobs:
|
|||
runs-on: debian
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: https://codeberg.org/TheAwiteb/rust-action@v1.74
|
||||
- uses: https://codeberg.org/TheAwiteb/rust-action@v1.70
|
||||
- name: Build the source code
|
||||
run: cargo build
|
||||
- name: Check the code format
|
||||
|
|
641
Cargo.lock
generated
641
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
30
Cargo.toml
30
Cargo.toml
|
@ -7,28 +7,28 @@ authors = ["Awiteb <a@4rs.nl>"]
|
|||
readme = "README.md"
|
||||
description = "A local CLI password manager"
|
||||
repository = "https://git.4rs.nl/awiteb/lprs"
|
||||
rust-version = "1.74.0"
|
||||
rust-version = "1.70.0"
|
||||
keywords = ["password", "manager", "CLI"]
|
||||
categories = ["command-line-utilities"]
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.4", features = ["derive"] }
|
||||
base64 = "0.21.5"
|
||||
clap = { version = "4.4.11", features = ["derive"] }
|
||||
comfy-table = "7.1.0"
|
||||
directories = "5.0.1"
|
||||
log = "0.4.21"
|
||||
log = "0.4.20"
|
||||
passwords = { version = "3.1.16", features = ["common-password"] }
|
||||
pretty_env_logger = "0.5.0"
|
||||
regex = "1.10.4"
|
||||
serde = { version = "1.0.200", features = ["derive"] }
|
||||
thiserror = "1.0.59"
|
||||
reqwest = {version = "0.11.27", optional = true}
|
||||
inquire = "0.7.5"
|
||||
bincode = "1.3.3"
|
||||
rand = "0.8.5"
|
||||
cbc = { version = "0.1.2", features = ["alloc", "std"] }
|
||||
aes = "0.8.4"
|
||||
sha2 = "0.10.8"
|
||||
serde_json = "1.0.116"
|
||||
base64 = "0.22.1"
|
||||
regex = "1.10.2"
|
||||
serde = { version = "1.0.193", features = ["derive"] }
|
||||
serde_json = "1.0.108"
|
||||
serde_with_macros = "3.4.0"
|
||||
sha256 = { version = "1.4.0", default-features = false }
|
||||
soft-aes = "0.1.0"
|
||||
thiserror = "1.0.51"
|
||||
url = { version = "2.5.0", features = ["serde"] }
|
||||
reqwest = {version = "0.11.23", optional = true}
|
||||
inquire = "0.7.4"
|
||||
|
||||
[features]
|
||||
default = ["update-notify"]
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
use clap::Args;
|
||||
|
||||
use crate::{
|
||||
vault::{Vault, Vaults},
|
||||
vault::{vault_state::*, Vault, Vaults},
|
||||
LprsCommand, LprsError, LprsResult,
|
||||
};
|
||||
|
||||
|
@ -25,14 +25,14 @@ use crate::{
|
|||
#[command(author, version, about, long_about = None)]
|
||||
pub struct Add {
|
||||
#[command(flatten)]
|
||||
vault_info: Vault,
|
||||
vault_info: Vault<Plain>,
|
||||
/// The password, if there is no value for it you will prompt it
|
||||
#[arg(short, long)]
|
||||
password: Option<Option<String>>,
|
||||
}
|
||||
|
||||
impl LprsCommand for Add {
|
||||
fn run(mut self, mut vault_manager: Vaults) -> LprsResult<()> {
|
||||
fn run(mut self, mut vault_manager: Vaults<Plain>) -> LprsResult<()> {
|
||||
match self.password {
|
||||
Some(Some(password)) => {
|
||||
log::debug!("User provided a password");
|
||||
|
|
|
@ -18,18 +18,21 @@ use std::fs;
|
|||
|
||||
use clap::Args;
|
||||
|
||||
use crate::{vault::Vaults, LprsCommand, LprsError, LprsResult};
|
||||
use crate::{
|
||||
vault::{vault_state::*, Vaults},
|
||||
LprsCommand, LprsError, LprsResult,
|
||||
};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
pub struct Clean {}
|
||||
|
||||
impl LprsCommand for Clean {
|
||||
fn run(self, vault_manager: Vaults) -> LprsResult<()> {
|
||||
fn run(self, vault_manager: Vaults<Plain>) -> LprsResult<()> {
|
||||
log::info!(
|
||||
"Cleaning the vaults file: {:?}",
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ use std::num::NonZeroU64;
|
|||
use clap::Args;
|
||||
|
||||
use crate::{
|
||||
vault::{Vault, Vaults},
|
||||
vault::{vault_state::*, Vault, Vaults},
|
||||
LprsCommand, LprsError, LprsResult,
|
||||
};
|
||||
|
||||
|
@ -47,7 +47,7 @@ pub struct Edit {
|
|||
}
|
||||
|
||||
impl LprsCommand for Edit {
|
||||
fn run(self, mut vault_manager: Vaults) -> LprsResult<()> {
|
||||
fn run(self, mut vault_manager: Vaults<Plain>) -> LprsResult<()> {
|
||||
let index = self.index.get() as usize;
|
||||
log::debug!("Editing vault at index: {index}");
|
||||
|
||||
|
@ -73,7 +73,7 @@ impl LprsCommand for Edit {
|
|||
};
|
||||
|
||||
log::info!("Applying the new values to the vault");
|
||||
*vault = Vault::new(
|
||||
*vault = Vault::<Plain>::new(
|
||||
self.name.as_ref().unwrap_or(&vault.name),
|
||||
self.username.as_ref().or(vault.username.as_ref()),
|
||||
password.as_ref().or(vault.password.as_ref()),
|
||||
|
|
|
@ -19,7 +19,7 @@ use std::{fs, io::Error as IoError, io::ErrorKind as IoErrorKind, path::PathBuf}
|
|||
use clap::Args;
|
||||
|
||||
use crate::{
|
||||
vault::{BitWardenPasswords, Format, Vaults},
|
||||
vault::{vault_state::*, BitWardenPasswords, Format, Vault, Vaults},
|
||||
LprsCommand, LprsError, LprsResult,
|
||||
};
|
||||
|
||||
|
@ -31,11 +31,10 @@ pub struct Export {
|
|||
/// Format to export vaults in
|
||||
#[arg(short, long, value_name = "FORMAT", default_value_t= Format::Lprs)]
|
||||
format: Format,
|
||||
// TODO: `force` flag to write on existing file
|
||||
}
|
||||
|
||||
impl LprsCommand for Export {
|
||||
fn run(self, vault_manager: Vaults) -> LprsResult<()> {
|
||||
fn run(self, vault_manager: Vaults<Plain>) -> LprsResult<()> {
|
||||
log::debug!(
|
||||
"Exporting vault {} to: {} with format: {}",
|
||||
vault_manager.vaults_file.display(),
|
||||
|
@ -43,9 +42,11 @@ impl LprsCommand for Export {
|
|||
self.format
|
||||
);
|
||||
let exported_data = match self.format {
|
||||
Format::Lprs => vault_manager.json_export()?,
|
||||
Format::BitWarden => serde_json::to_string(&BitWardenPasswords::from(vault_manager))?,
|
||||
};
|
||||
Format::Lprs => {
|
||||
serde_json::to_string::<Vec<Vault<Encrypted>>>(&vault_manager.encrypt_vaults()?)
|
||||
}
|
||||
Format::BitWarden => serde_json::to_string(&BitWardenPasswords::from(vault_manager)),
|
||||
}?;
|
||||
|
||||
fs::write(&self.path, exported_data).map_err(LprsError::from)
|
||||
}
|
||||
|
|
|
@ -18,7 +18,10 @@ use std::num::NonZeroU64;
|
|||
|
||||
use clap::Args;
|
||||
|
||||
use crate::{vault::Vaults, LprsCommand, LprsError, LprsResult};
|
||||
use crate::{
|
||||
vault::{vault_state::*, Vaults},
|
||||
LprsCommand, LprsError, LprsResult,
|
||||
};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
|
@ -42,7 +45,7 @@ pub struct Gen {
|
|||
}
|
||||
|
||||
impl LprsCommand for Gen {
|
||||
fn run(self, _vault_manager: Vaults) -> LprsResult<()> {
|
||||
fn run(self, _vault_manager: Vaults<Plain>) -> LprsResult<()> {
|
||||
println!(
|
||||
"{}",
|
||||
passwords::PasswordGenerator::new()
|
||||
|
|
|
@ -14,17 +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 std::{
|
||||
fs::{self, File},
|
||||
io::Error as IoError,
|
||||
io::ErrorKind as IoErrorKind,
|
||||
path::PathBuf,
|
||||
};
|
||||
use std::{fs::File, io::Error as IoError, io::ErrorKind as IoErrorKind, path::PathBuf};
|
||||
|
||||
use clap::Args;
|
||||
|
||||
use crate::{
|
||||
vault::{BitWardenPasswords, Format, Vault, Vaults},
|
||||
vault::{vault_state::*, BitWardenPasswords, Format, Vault, Vaults},
|
||||
LprsCommand, LprsError, LprsResult,
|
||||
};
|
||||
|
||||
|
@ -40,7 +35,7 @@ pub struct Import {
|
|||
}
|
||||
|
||||
impl LprsCommand for Import {
|
||||
fn run(self, mut vault_manager: Vaults) -> LprsResult<()> {
|
||||
fn run(self, mut vault_manager: Vaults<Plain>) -> LprsResult<()> {
|
||||
log::debug!(
|
||||
"Importing vaults from: {} with format: {} to the vault: {}",
|
||||
self.path.display(),
|
||||
|
@ -50,11 +45,10 @@ impl LprsCommand for Import {
|
|||
|
||||
let imported_passwords_len = match self.format {
|
||||
Format::Lprs => {
|
||||
let vaults =
|
||||
Vaults::json_reload(&vault_manager.master_password, &fs::read(self.path)?)?;
|
||||
let vaults_len = vaults.len();
|
||||
let vaults = Vaults::try_reload(self.path, vault_manager.master_password.to_vec())?;
|
||||
let vaults_len = vaults.vaults.len();
|
||||
|
||||
vault_manager.vaults.extend(vaults);
|
||||
vault_manager.vaults.extend(vaults.vaults);
|
||||
vault_manager.try_export()?;
|
||||
vaults_len
|
||||
}
|
||||
|
|
|
@ -19,7 +19,10 @@ use std::num::NonZeroU64;
|
|||
use clap::Args;
|
||||
use inquire::Select;
|
||||
|
||||
use crate::{vault::Vaults, LprsCommand, LprsError, LprsResult};
|
||||
use crate::{
|
||||
vault::{vault_state::*, Vaults},
|
||||
LprsCommand, LprsError, LprsResult,
|
||||
};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
|
@ -36,7 +39,7 @@ pub struct List {
|
|||
}
|
||||
|
||||
impl LprsCommand for List {
|
||||
fn run(self, vault_manager: Vaults) -> LprsResult<()> {
|
||||
fn run(self, vault_manager: Vaults<Plain>) -> LprsResult<()> {
|
||||
if vault_manager.vaults.is_empty() {
|
||||
return Err(LprsError::Other(
|
||||
"Looks like there is no vaults to list".to_owned(),
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
// 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 std::path::PathBuf;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
|
@ -73,13 +73,6 @@ impl Cli {
|
|||
pub fn run(self) -> LprsResult<()> {
|
||||
let vaults_file = if let Some(path) = self.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
|
||||
} else {
|
||||
log::info!("Using the default vaults file");
|
||||
|
@ -98,9 +91,11 @@ impl Cli {
|
|||
}
|
||||
} else {
|
||||
log::info!("Reloading the vaults file");
|
||||
let master_password =
|
||||
utils::master_password_prompt(fs::read(&vaults_file)?.is_empty())?;
|
||||
Vaults::try_reload(vaults_file, master_password)?
|
||||
let master_password = utils::master_password_prompt(&vaults_file)?;
|
||||
Vaults::try_reload(
|
||||
vaults_file,
|
||||
master_password.into_bytes().into_iter().take(32).collect(),
|
||||
)?
|
||||
};
|
||||
|
||||
self.command.run(vault_manager)
|
||||
|
|
|
@ -18,7 +18,10 @@ use std::num::NonZeroU64;
|
|||
|
||||
use clap::Args;
|
||||
|
||||
use crate::{vault::Vaults, LprsCommand, LprsError, LprsResult};
|
||||
use crate::{
|
||||
vault::{vault_state::*, Vaults},
|
||||
LprsCommand, LprsError, LprsResult,
|
||||
};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
|
@ -32,7 +35,7 @@ pub struct Remove {
|
|||
}
|
||||
|
||||
impl LprsCommand for Remove {
|
||||
fn run(self, mut vault_manager: Vaults) -> LprsResult<()> {
|
||||
fn run(self, mut vault_manager: Vaults<Plain>) -> LprsResult<()> {
|
||||
let index = (self.index.get() - 1) as usize;
|
||||
log::debug!("Removing vault at index: {index}");
|
||||
|
||||
|
|
|
@ -20,11 +20,15 @@ pub type Result<T> = std::result::Result<T, Error>;
|
|||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Invalid Json Path Error: {0}")]
|
||||
InvalidJsonPath(String),
|
||||
#[error("Encryption Error: {0}")]
|
||||
Encryption(String),
|
||||
#[error("Decryption Error: The given key cannot decrypt the given data. Either the data has been tampered with or the key is incorrect.")]
|
||||
Decryption,
|
||||
#[error("Wrong Master Password Error: Wrong decryption password")]
|
||||
#[error("Decryption Error: {0}")]
|
||||
Decryption(String),
|
||||
#[error(
|
||||
"Wrong Master Password Error: Wrong password or you may have played with the password file"
|
||||
)]
|
||||
WrongMasterPassword,
|
||||
#[error("Weak Password Error: {0}")]
|
||||
WeakPassword(String),
|
||||
|
@ -41,8 +45,6 @@ pub enum Error {
|
|||
InvalidRegex(#[from] regex::Error),
|
||||
#[error("UTF8 Error: {0}")]
|
||||
Utf8(#[from] FromUtf8Error),
|
||||
#[error("Bincode Error: {0}")]
|
||||
Bincode(#[from] bincode::Error),
|
||||
#[error("Base64 Decode Error: {0}")]
|
||||
BaseDecodeError(#[from] base64::DecodeError),
|
||||
#[error("Json Error: {0}")]
|
||||
|
|
|
@ -53,7 +53,7 @@ macro_rules! impl_commands {
|
|||
($enum_name: ident, $($varint: ident)+) => {
|
||||
#[automatically_derived]
|
||||
impl $crate::LprsCommand for $enum_name{
|
||||
fn run(self, vault_manager: $crate::vault::Vaults) -> $crate::LprsResult<()> {
|
||||
fn run(self, vault_manager: $crate::vault::Vaults<$crate::vault::vault_state::Plain>) -> $crate::LprsResult<()> {
|
||||
match self {
|
||||
$(
|
||||
Self::$varint(command) => command.run(vault_manager),
|
||||
|
|
13
src/main.rs
13
src/main.rs
|
@ -14,10 +14,14 @@
|
|||
// 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 clap::Parser;
|
||||
use inquire::InquireError;
|
||||
use std::process::ExitCode;
|
||||
|
||||
use base64::{
|
||||
alphabet,
|
||||
engine::{general_purpose::PAD, GeneralPurpose},
|
||||
};
|
||||
use clap::Parser;
|
||||
|
||||
pub mod cli;
|
||||
pub mod errors;
|
||||
pub mod utils;
|
||||
|
@ -26,11 +30,12 @@ pub mod vault;
|
|||
mod macros;
|
||||
mod traits;
|
||||
|
||||
pub use base64::engine::general_purpose::STANDARD as BASE64;
|
||||
pub use errors::{Error as LprsError, Result as LprsResult};
|
||||
use inquire::InquireError;
|
||||
pub use traits::*;
|
||||
|
||||
pub const DEFAULT_VAULTS_FILE: &str = "vaults.lprs";
|
||||
pub const STANDARDBASE: GeneralPurpose = GeneralPurpose::new(&alphabet::STANDARD, PAD);
|
||||
pub const DEFAULT_VAULTS_FILE: &str = "vaults.json";
|
||||
|
||||
#[cfg(feature = "update-notify")]
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
|
|
@ -14,12 +14,15 @@
|
|||
// 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::{vault::Vaults, LprsResult};
|
||||
use crate::{
|
||||
vault::{vault_state::*, Vaults},
|
||||
LprsResult,
|
||||
};
|
||||
|
||||
/// Trait to work with the commands
|
||||
pub trait LprsCommand {
|
||||
/// Run the command, should do all the logic, even the export
|
||||
fn run(self, vault_manager: Vaults) -> LprsResult<()>;
|
||||
fn run(self, vault_manager: Vaults<Plain>) -> LprsResult<()>;
|
||||
|
||||
/// Validate the gaiven args from the user.
|
||||
fn validate_args(&self) -> LprsResult<()> {
|
||||
|
|
22
src/utils.rs
22
src/utils.rs
|
@ -14,12 +14,14 @@
|
|||
// 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 std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use inquire::validator::Validation;
|
||||
use sha2::Digest;
|
||||
|
||||
use crate::{LprsError, LprsResult};
|
||||
use crate::{vault, LprsError, LprsResult};
|
||||
|
||||
/// Returns the local project dir joined with the given file name
|
||||
pub fn local_project_file(filename: &str) -> LprsResult<PathBuf> {
|
||||
|
@ -40,7 +42,11 @@ pub fn local_project_file(filename: &str) -> LprsResult<PathBuf> {
|
|||
pub fn vaults_file() -> LprsResult<PathBuf> {
|
||||
let vaults_file = local_project_file(crate::DEFAULT_VAULTS_FILE)?;
|
||||
if !vaults_file.exists() {
|
||||
fs::File::create(&vaults_file)?;
|
||||
log::info!(
|
||||
"Vaults file not found, creating a new one: {:?}",
|
||||
vaults_file.display()
|
||||
);
|
||||
fs::write(&vaults_file, "[]")?;
|
||||
}
|
||||
Ok(vaults_file)
|
||||
}
|
||||
|
@ -65,9 +71,9 @@ pub fn password_validator(password: &str) -> Result<Validation, inquire::CustomU
|
|||
}
|
||||
|
||||
/// Ask the user for the master password, then returns it
|
||||
///
|
||||
/// Return's the password as 32 bytes after hash it (256 bit)
|
||||
pub fn master_password_prompt(is_new_vaults_file: bool) -> LprsResult<[u8; 32]> {
|
||||
pub fn master_password_prompt(vaults_file: &Path) -> LprsResult<String> {
|
||||
let is_new_vaults_file = vault::is_new_vaults_file(vaults_file)?;
|
||||
|
||||
inquire::Password {
|
||||
message: "Master Password:",
|
||||
enable_confirmation: is_new_vaults_file,
|
||||
|
@ -81,7 +87,7 @@ pub fn master_password_prompt(is_new_vaults_file: bool) -> LprsResult<[u8; 32]>
|
|||
.with_formatter(&|p| "*".repeat(p.chars().count()))
|
||||
.with_display_mode(inquire::PasswordDisplayMode::Masked)
|
||||
.prompt()
|
||||
.map(|p| sha2::Sha256::digest(p).into())
|
||||
.map(sha256::digest)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{Vault, Vaults};
|
||||
use super::{vault_state::*, Vault, Vaults};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct BitWardenLoginData {
|
||||
|
@ -39,7 +39,7 @@ pub struct BitWardenPasswords {
|
|||
pub items: Vec<BitWardenPassword>,
|
||||
}
|
||||
|
||||
impl From<BitWardenPassword> for Vault {
|
||||
impl From<BitWardenPassword> for Vault<Plain> {
|
||||
fn from(value: BitWardenPassword) -> Self {
|
||||
Self::new(
|
||||
value.name,
|
||||
|
@ -55,8 +55,8 @@ impl From<BitWardenPassword> for Vault {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Vault> for BitWardenPassword {
|
||||
fn from(value: Vault) -> Self {
|
||||
impl From<Vault<Plain>> for BitWardenPassword {
|
||||
fn from(value: Vault<Plain>) -> Self {
|
||||
Self {
|
||||
ty: 1,
|
||||
name: value.name,
|
||||
|
@ -72,8 +72,8 @@ impl From<Vault> for BitWardenPassword {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Vaults> for BitWardenPasswords {
|
||||
fn from(value: Vaults) -> Self {
|
||||
impl From<Vaults<Plain>> for BitWardenPasswords {
|
||||
fn from(value: Vaults<Plain>) -> Self {
|
||||
Self {
|
||||
encrypted: false,
|
||||
folders: Vec::new(),
|
||||
|
|
|
@ -14,44 +14,53 @@
|
|||
// 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 aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
|
||||
use rand::{rngs::StdRng, Rng, SeedableRng};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use base64::Engine;
|
||||
use soft_aes::aes::{aes_dec_ecb, aes_enc_ecb};
|
||||
|
||||
use crate::{LprsError, LprsResult};
|
||||
|
||||
type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
|
||||
type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
|
||||
/// Encrypt the string with AEC ECB
|
||||
pub fn encrypt(master_password: &[u8], data: &str) -> LprsResult<String> {
|
||||
let padding = Some("PKCS7");
|
||||
|
||||
/// Encrypt the given data by the given key using AES-256 CBC
|
||||
///
|
||||
/// 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(
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("SystemTime before UNIX EPOCH!")
|
||||
.as_secs(),
|
||||
aes_enc_ecb(data.as_bytes(), master_password, padding)
|
||||
.map(|d| crate::STANDARDBASE.encode(d))
|
||||
.map_err(|err| LprsError::Encryption(err.to_string()))
|
||||
}
|
||||
|
||||
/// Decrypt the string with AEC ECB
|
||||
pub fn decrypt(master_password: &[u8], data: &str) -> LprsResult<String> {
|
||||
let padding = Some("PKCS7");
|
||||
|
||||
aes_dec_ecb(
|
||||
crate::STANDARDBASE.decode(data)?.as_slice(),
|
||||
master_password,
|
||||
padding,
|
||||
)
|
||||
.gen();
|
||||
|
||||
let mut ciphertext =
|
||||
Aes256CbcEnc::new(master_password.into(), &iv.into()).encrypt_padded_vec_mut::<Pkcs7>(data);
|
||||
ciphertext.extend(&iv);
|
||||
ciphertext
|
||||
.map_err(|err| {
|
||||
if err.to_string().contains("Invalid padding") {
|
||||
LprsError::WrongMasterPassword
|
||||
} else {
|
||||
LprsError::Decryption(err.to_string())
|
||||
}
|
||||
})
|
||||
.map(|d| String::from_utf8(d).map_err(LprsError::Utf8))?
|
||||
}
|
||||
|
||||
/// Decrypt the given data by the given key, the data should
|
||||
/// be encrypted by AES-256 CBC. The IV will be extraxted
|
||||
/// from the last 16 bytes.
|
||||
pub(crate) fn decrypt(master_password: &[u8; 32], data: &[u8]) -> LprsResult<Vec<u8>> {
|
||||
let (ciphertext, iv) = data.split_at(
|
||||
data.len()
|
||||
.checked_sub(16)
|
||||
.ok_or_else(|| LprsError::Decryption)?,
|
||||
);
|
||||
|
||||
Aes256CbcDec::new(master_password.into(), iv.into())
|
||||
.decrypt_padded_vec_mut::<Pkcs7>(ciphertext)
|
||||
.map_err(|_| LprsError::Decryption)
|
||||
/// Encrypt if the `Option` are `Some`
|
||||
pub fn encrypt_some(
|
||||
master_password: &[u8],
|
||||
data: Option<impl AsRef<str>>,
|
||||
) -> LprsResult<Option<String>> {
|
||||
data.map(|d| encrypt(master_password, d.as_ref()))
|
||||
.transpose()
|
||||
}
|
||||
|
||||
/// Decrypt if the `Option` are `Some`
|
||||
pub fn decrypt_some(
|
||||
master_password: &[u8],
|
||||
data: Option<impl AsRef<str>>,
|
||||
) -> LprsResult<Option<String>> {
|
||||
data.map(|d| decrypt(master_password, d.as_ref()))
|
||||
.transpose()
|
||||
}
|
||||
|
|
169
src/vault/mod.rs
169
src/vault/mod.rs
|
@ -14,19 +14,21 @@
|
|||
// 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 std::{fs, marker::PhantomData, path::PathBuf};
|
||||
|
||||
use base64::Engine;
|
||||
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 {
|
||||
|
@ -34,9 +36,23 @@ pub enum Format {
|
|||
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 {
|
||||
pub struct Vault<T>
|
||||
where
|
||||
T: std::fmt::Debug + Clone,
|
||||
{
|
||||
/// The name of the vault
|
||||
#[arg(short, long)]
|
||||
pub name: String,
|
||||
|
@ -52,20 +68,31 @@ pub struct Vault {
|
|||
/// 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 {
|
||||
pub struct Vaults<T>
|
||||
where
|
||||
T: std::fmt::Debug + Clone,
|
||||
{
|
||||
/// Hash of the master password
|
||||
pub master_password: [u8; 32],
|
||||
pub master_password: Vec<u8>,
|
||||
/// The json vaults file
|
||||
pub vaults_file: PathBuf,
|
||||
/// The vaults
|
||||
pub vaults: Vec<Vault>,
|
||||
pub vaults: Vec<Vault<T>>,
|
||||
}
|
||||
|
||||
impl Vault {
|
||||
impl<T> Vault<T>
|
||||
where
|
||||
T: std::fmt::Debug + Clone,
|
||||
{
|
||||
/// Create new [`Vault`] instance
|
||||
pub fn new(
|
||||
name: impl Into<String>,
|
||||
|
@ -80,8 +107,35 @@ impl Vault {
|
|||
password: password.map(Into::into),
|
||||
service: service.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
|
||||
pub fn list_name(&self) -> String {
|
||||
|
@ -100,77 +154,40 @@ impl Vault {
|
|||
}
|
||||
}
|
||||
|
||||
impl Vaults {
|
||||
impl<T> Vaults<T>
|
||||
where
|
||||
T: std::fmt::Debug + Clone,
|
||||
{
|
||||
/// Create new [`Vaults`] instnce
|
||||
pub fn new(master_password: [u8; 32], vaults_file: PathBuf, vaults: Vec<Vault>) -> Self {
|
||||
pub fn new(master_password: Vec<u8>, vaults_file: PathBuf, vaults: Vec<Vault<T>>) -> Self {
|
||||
Self {
|
||||
master_password,
|
||||
vaults_file,
|
||||
vaults,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add new vault
|
||||
pub fn add_vault(&mut self, vault: Vault) {
|
||||
self.vaults.push(vault)
|
||||
}
|
||||
|
||||
/// Encrypt the vaults then returns it as json.
|
||||
///
|
||||
/// This function used to backup the vaults.
|
||||
///
|
||||
/// Note: The returned string is `Vec<Vault>`
|
||||
pub fn json_export(&self) -> LprsResult<String> {
|
||||
let encrypt = |val: &str| {
|
||||
LprsResult::Ok(
|
||||
crate::BASE64.encode(cipher::encrypt(&self.master_password, val.as_ref())),
|
||||
)
|
||||
};
|
||||
|
||||
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()),
|
||||
))
|
||||
})
|
||||
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<()> {
|
||||
log::debug!(
|
||||
|
@ -179,22 +196,14 @@ impl Vaults {
|
|||
);
|
||||
fs::write(
|
||||
&self.vaults_file,
|
||||
cipher::encrypt(&self.master_password, &bincode::serialize(&self.vaults)?),
|
||||
serde_json::to_string(&self.encrypt_vaults()?)?,
|
||||
)
|
||||
.map_err(LprsError::Io)
|
||||
}
|
||||
|
||||
/// Reload the vaults from the file then decrypt it
|
||||
pub fn try_reload(vaults_file: PathBuf, master_password: [u8; 32]) -> LprsResult<Self> {
|
||||
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))
|
||||
/// Add new vault
|
||||
pub fn add_vault(&mut self, vault: Vault<Plain>) {
|
||||
self.vaults.push(vault)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -210,7 +219,7 @@ impl std::fmt::Display for Format {
|
|||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Vault {
|
||||
impl<T: std::fmt::Debug + Clone> std::fmt::Display for Vault<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Name: {}", self.name)?;
|
||||
if let Some(ref username) = self.username {
|
||||
|
|
35
src/vault/validator.rs
Normal file
35
src/vault/validator.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
// 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)
|
||||
}
|
Loading…
Reference in a new issue