First source commit

This commit is contained in:
TheAwiteb 2023-12-23 22:59:25 +03:00
parent 758bb79967
commit 018c8c63b2
No known key found for this signature in database
GPG key ID: ABF818BD15DC2D34
14 changed files with 1720 additions and 0 deletions

1120
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

32
Cargo.toml Normal file
View file

@ -0,0 +1,32 @@
[package]
name = "passrs"
version = "0.1.0"
edition = "2021"
license = "GPL-3.0-only"
authors = ["Awiteb <awiteb@hotmail.com>"]
readme = "README.md"
description = "Local CLI password manager"
repository = "https://github.com/TheAwiteb/passrs"
rust-version = "1.70.0"
keywords = ["password", "manager", "CLI"]
categories = ["command-line-utilities"]
[dependencies]
base64 = "0.21.5"
clap = { version = "4.4.11", features = ["derive"] }
comfy-table = "7.1.0"
directories = "5.0.1"
log = "0.4.20"
passwords = { version = "3.1.16", features = ["common-password"] }
pretty_env_logger = "0.5.0"
regex = "1.10.2"
scanpw = "1.0.0"
serde = { version = "1.0.193", features = ["derive"] }
serde_json = "1.0.108"
sha256 = { version = "1.4.0", default-features = false }
soft-aes = "0.1.0"
thiserror = "1.0.51"
url = { version = "2.5.0", features = ["serde"] }
[profile.release]
strip = true # Automatically strip symbols from the binary.

20
src/cli/add_command.rs Normal file
View file

@ -0,0 +1,20 @@
use clap::Args;
use crate::{
password::{Password, Passwords},
PassrsResult, RunCommand,
};
#[derive(Debug, Args)]
#[command(author, version, about, long_about = None)]
pub struct Add {
#[command(flatten)]
password_info: Password,
}
impl RunCommand for Add {
fn run(&self, mut password_manager: Passwords) -> PassrsResult<()> {
password_manager.add_password(self.password_info.clone());
password_manager.try_export()
}
}

15
src/cli/clean_command.rs Normal file
View file

@ -0,0 +1,15 @@
use std::fs;
use clap::Args;
use crate::{password::Passwords, PassrsError, PassrsResult, RunCommand};
#[derive(Debug, Args)]
#[command(author, version, about, long_about = None)]
pub struct Clean {}
impl RunCommand for Clean {
fn run(&self, password_manager: Passwords) -> PassrsResult<()> {
fs::write(password_manager.passwords_file, "[]").map_err(PassrsError::Io)
}
}

120
src/cli/list_command.rs Normal file
View file

@ -0,0 +1,120 @@
use clap::Args;
use comfy_table::Table;
use regex::Regex;
use crate::{password::Passwords, PassrsError, PassrsResult, RunCommand};
#[derive(Debug, Args)]
#[command(author, version, about, long_about = None)]
pub struct List {
/// Show the clean password
#[arg(short = 'p', long)]
unhide_password: bool,
/// Show the service of the password and search in it if you search
#[arg(short = 's', long)]
with_service: bool,
/// Show the note of the password and search in it if you search
#[arg(short = 'n', long)]
with_note: bool,
/// Return the password with spesifc index
#[arg(short, long, value_name = "INDEX")]
get: Option<usize>,
/// Search and display only matching passwords.
///
/// The name and username will be searched. And service and note if included
#[arg(short = 'e', long, value_name = "TEXT")]
search: Option<String>,
/// Enable regex in the search
#[arg(short, long)]
regex: bool,
}
impl RunCommand for List {
fn run(&self, password_manager: Passwords) -> PassrsResult<()> {
if password_manager.passwords.is_empty() {
println!("Looks like there is no passwords to list")
} else {
if self.get.is_some() && self.search.is_some() {
return Err(PassrsError::ArgsConflict(
"You cannot use `--get` arg with `--search` arg".to_owned(),
));
}
if self.regex && self.search.is_none() {
return Err(PassrsError::ArgsConflict(
"You cannot use `--regex` without `--search` arg".to_owned(),
));
}
let mut table = Table::new();
let mut header = vec!["Index", "Name", "Username", "Password"];
if self.with_service {
header.push("Service");
}
if self.with_note {
header.push("Note");
}
let re = Regex::new(self.search.as_deref().unwrap_or("."))?;
table.set_header(header);
let passwords = password_manager
.passwords
.iter()
.enumerate()
.filter(|(idx, pass)| {
if let Some(index) = self.get {
return (idx + 1) == index;
}
if let Some(ref pattern) = self.search {
if self.regex {
return re.is_match(&pass.name)
|| re.is_match(&pass.username)
|| (self.with_service
&& pass.service.as_ref().is_some_and(|s| re.is_match(s)))
|| (self.with_note
&& pass.note.as_ref().is_some_and(|n| re.is_match(n)));
} else {
let pattern = pattern.to_lowercase();
return pass.name.to_lowercase().contains(&pattern)
|| pass.username.to_lowercase().contains(&pattern)
|| (self.with_service
&& pass
.service
.as_ref()
.is_some_and(|s| s.to_lowercase().contains(&pattern)))
|| (self.with_note
&& pass
.note
.as_ref()
.is_some_and(|n| n.to_lowercase().contains(&pattern)));
}
}
true
});
for (idx, password) in passwords {
let hide_password = "*".repeat(password.password.chars().count());
let idx = (idx + 1).to_string();
let mut row = vec![
idx.as_str(),
password.name.as_str(),
password.username.as_str(),
if self.unhide_password {
password.password.as_str()
} else {
hide_password.as_str()
},
];
if self.with_service {
row.push(password.service.as_deref().unwrap_or("Not Set"))
}
if self.with_note {
row.push(password.note.as_deref().unwrap_or("Not Set"))
}
table.add_row(row);
}
println!("{table}");
}
Ok(())
}
}

73
src/cli/mod.rs Normal file
View file

@ -0,0 +1,73 @@
use std::path::PathBuf;
use clap::Parser;
use crate::{
password::{self, Passwords},
PassrsError, PassrsResult, RunCommand,
};
pub mod add_command;
pub mod clean_command;
pub mod list_command;
crate::create_commands!(
enum Commands
"Add new password", Add => add_command::Add
"List your password and search", List => list_command::List
"Clean the password file", Clean => clean_command::Clean
// TODO: Edit command
// TODO: Delete command
// TODO: Export command
// TODO: Import command
);
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub struct Cli {
/// The passwords json file, default: $HOME/.local/share/passrs/passwords.json
#[arg(short, long)]
passwords_file: Option<PathBuf>,
// TODO: verbose flag
#[command(subcommand)]
command: Commands,
}
impl Cli {
/// Run the cli
pub fn run(self) -> PassrsResult<()> {
let passwords_file = if let Some(ref path) = self.passwords_file {
path.clone()
} else {
crate::utils::passwords_file()?
};
log::debug!(
"Getting password file: {}",
passwords_file.to_string_lossy()
);
let password = scanpw::scanpw!("Master Password: ");
if password::is_new_password_file(&passwords_file)? {
let analyzed = passwords::analyzer::analyze(&password);
if analyzed.length() < 15 {
return Err(PassrsError::WeakPassword(
"The password length must be beggier then 15".to_owned(),
));
} else if passwords::scorer::score(&analyzed) < 80.0 {
return Err(PassrsError::WeakPassword(
"Your password is not stronge enough".to_owned(),
));
}
}
let master_password = sha256::digest(password);
let password_manager = Passwords::try_reload(
passwords_file,
master_password.into_bytes().into_iter().take(32).collect(),
)?;
self.command.run(password_manager)?;
Ok(())
}
}

41
src/errors.rs Normal file
View file

@ -0,0 +1,41 @@
use std::{process::ExitCode, string::FromUtf8Error};
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: {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),
#[error("Args Conflict Error: {0}")]
ArgsConflict(String),
#[error("Invalid Regex: {0}")]
InvalidRegex(#[from] regex::Error),
#[error("UTF8 Error: {0}")]
Utf8(#[from] FromUtf8Error),
#[error("Base64 Decode Error: {0}")]
BaseDecodeError(#[from] base64::DecodeError),
#[error("Json Error: {0}")]
Json(#[from] serde_json::Error),
#[error("Project Folder Error: {0}")]
ProjectDir(String),
#[error("IO Error: {0}")]
Io(#[from] std::io::Error),
}
impl Error {
/// Return the exit code of the error
pub fn exit_code(&self) -> ExitCode {
ExitCode::FAILURE
}
}

59
src/macros.rs Normal file
View file

@ -0,0 +1,59 @@
/// Creates commands macro, to create the `Commands` enum and impl `RunCommand` to it.
///
/// ### Notes:
/// The `$command` must impl `RunCommand` trait
///
/// ### Example:
/// ```rust
/// create_commands!(
/// enum TestCommands
/// "Test command", Test => TestArgs
/// "Do something", Some => SomeArgs
/// );
/// ```
/// #### Output
/// ```rust
/// ///The passrs commands
/// pub enum TestCommands {
/// ///Test command
/// Test(TestArgs),
/// ///Do something
/// Some(SomeArgs),
/// }
///
/// impl crate::RunCommand for TestCommands {
/// fn run(
/// &self,
/// password_manager: crate::password::Passwords,
/// ) -> crate::PassrsResult<()> {
/// match self {
/// Self::Test(command) => command.run(password_manager),
/// Self::Some(command) => command.run(password_manager),
/// }
/// }
/// }
/// ```
#[macro_export]
macro_rules! create_commands {
(enum $enum_name: ident $($doc:tt, $varint: ident => $command: ty)+) => {
#[doc = "The passrs commands"]
#[derive(Debug, clap::Subcommand)]
pub enum $enum_name {
$(
#[doc = $doc]
$varint($command),
)+
}
#[automatically_derived]
impl $crate::RunCommand for $enum_name{
fn run(&self, password_manager: $crate::password::Passwords) -> $crate::PassrsResult<()> {
match self {
$(
Self::$varint(command) => command.run(password_manager),
)+
}
}
}
};
}

32
src/main.rs Normal file
View file

@ -0,0 +1,32 @@
use std::process::ExitCode;
use base64::{
alphabet,
engine::{general_purpose::PAD, GeneralPurpose},
};
use clap::Parser;
pub mod cli;
pub mod errors;
pub mod password;
pub mod utils;
mod macros;
mod traits;
pub use {macros::*, traits::*};
pub use errors::{Error as PassrsError, Result as PassrsResult};
pub const STANDARDBASE: GeneralPurpose = GeneralPurpose::new(&alphabet::STANDARD, PAD);
fn main() -> ExitCode {
pretty_env_logger::init();
if let Err(err) = cli::Cli::parse().run() {
eprintln!("{err}");
err.exit_code()
} else {
ExitCode::SUCCESS
}
}

32
src/password/cipher.rs Normal file
View file

@ -0,0 +1,32 @@
use base64::Engine;
use soft_aes::aes::{aes_dec_ecb, aes_enc_ecb};
use crate::{PassrsError, PassrsResult};
/// Encrypt the string with AEC ECB
pub fn encrypt(master_password: &[u8], data: &str) -> PassrsResult<String> {
let padding = Some("PKCS7");
aes_enc_ecb(data.as_bytes(), master_password, padding)
.map(|d| crate::STANDARDBASE.encode(d))
.map_err(|err| PassrsError::Encryption(err.to_string()))
}
/// Decrypt the string with AEC ECB
pub fn decrypt(master_password: &[u8], data: &str) -> PassrsResult<String> {
let padding = Some("PKCS7");
aes_dec_ecb(
crate::STANDARDBASE.decode(data)?.as_slice(),
master_password,
padding,
)
.map_err(|err| {
if err.to_string().contains("Invalid padding") {
PassrsError::WrongMasterPassword
} else {
PassrsError::Decryption(err.to_string())
}
})
.map(|d| String::from_utf8(d).map_err(PassrsError::Utf8))?
}

129
src/password/mod.rs Normal file
View file

@ -0,0 +1,129 @@
use std::{fs, path::PathBuf};
use clap::Parser;
use serde::{Deserialize, Serialize};
use crate::{PassrsError, PassrsResult};
pub mod cipher;
mod validator;
pub use validator::*;
/// The passwords manager
#[derive(Deserialize, Serialize)]
pub struct Passwords {
/// Hash of the master password
#[serde(skip)]
pub master_password: Vec<u8>,
/// The json passwords file
#[serde(skip)]
pub passwords_file: PathBuf,
/// The passwords
pub passwords: Vec<Password>,
}
/// The password struct
#[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>,
}
impl Password {
/// Encrypt the password data
pub fn encrypt(self, master_password: &[u8]) -> PassrsResult<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]) -> PassrsResult<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
fn encrypt(self) -> PassrsResult<Self> {
Ok(Self {
passwords: self
.passwords
.into_iter()
.map(|p| p.encrypt(&self.master_password))
.collect::<PassrsResult<Vec<Password>>>()?,
..self
})
}
/// Reload the passwords from the file
pub fn try_reload(passwords_file: PathBuf, master_password: Vec<u8>) -> PassrsResult<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::<PassrsResult<Vec<Password>>>()?;
Ok(Self::new(master_password, passwords_file, passwords))
}
/// Export the passwords to the file
pub fn try_export(self) -> PassrsResult<()> {
let path = self.passwords_file.to_path_buf();
fs::write(path, serde_json::to_string(&self.encrypt()?.passwords)?).map_err(PassrsError::Io)
}
/// Add new password
pub fn add_password(&mut self, password: Password) {
self.passwords.push(password)
}
}

19
src/password/validator.rs Normal file
View file

@ -0,0 +1,19 @@
use std::{fs, path::Path};
use crate::PassrsResult;
use super::Password;
/// Return if the password file new file or not
pub fn is_new_password_file(path: &Path) -> PassrsResult<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()
{
return Ok(false);
}
}
Ok(true)
}

6
src/traits.rs Normal file
View file

@ -0,0 +1,6 @@
use crate::{password::Passwords, PassrsResult};
/// Trait to run the command
pub trait RunCommand {
fn run(&self, password_manager: Passwords) -> PassrsResult<()>;
}

22
src/utils.rs Normal file
View file

@ -0,0 +1,22 @@
use std::{fs, path::PathBuf};
use crate::{PassrsError, PassrsResult};
/// Return the default passwords json file
pub fn passwords_file() -> PassrsResult<PathBuf> {
if let Some(path) = directories::ProjectDirs::from("", "", "passrs")
.map(|d| d.data_local_dir().to_path_buf().join("passwords.json"))
{
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
if !path.exists() {
fs::write(&path, "[]")?;
}
Ok(path)
} else {
Err(PassrsError::ProjectDir(
"Can't extract the project_dir from this OS".to_owned(),
))
}
}