First source commit
This commit is contained in:
parent
758bb79967
commit
018c8c63b2
14 changed files with 1720 additions and 0 deletions
1120
Cargo.lock
generated
Normal file
1120
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
32
Cargo.toml
Normal file
32
Cargo.toml
Normal 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
20
src/cli/add_command.rs
Normal 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
15
src/cli/clean_command.rs
Normal 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
120
src/cli/list_command.rs
Normal 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
73
src/cli/mod.rs
Normal 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
41
src/errors.rs
Normal 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
59
src/macros.rs
Normal 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
32
src/main.rs
Normal 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
32
src/password/cipher.rs
Normal 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
129
src/password/mod.rs
Normal 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, ¬e))
|
||||||
|
.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, ¬e))
|
||||||
|
.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
19
src/password/validator.rs
Normal 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
6
src/traits.rs
Normal 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
22
src/utils.rs
Normal 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(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue