chore(DX): Improve the DX #27

Merged
awiteb merged 9 commits from awiteb/improve-dx into master 2024-05-06 23:19:20 +02:00 AGit
24 changed files with 394 additions and 135 deletions

View file

@ -13,6 +13,8 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: https://codeberg.org/TheAwiteb/rust-action@v1.74
- name: Check MSRV
run: cargo +1.74 build
- name: Build the source code
run: cargo build
- name: Check the code format

View file

@ -19,4 +19,11 @@ jobs:
mv git-sumi-x86_64-unknown-linux-gnu/git-sumi git-sumi
chmod +x git-sumi
- name: Run git-sumi
run: ./git-sumi "${{ github.event.pull_request.title }}"
run: |
# Check if the PR are WIP or not (Start with WIP: or wip:)
IS_WIP=$(echo ${{ github.event.pull_request.title }} | grep -i "^WIP:" | wc -l)
if [ $IS_WIP -eq 1 ]; then
echo "Is Work In Progress PR"
exit 0
fi
./git-sumi "${{ github.event.pull_request.title }}"

View file

@ -1,29 +1,49 @@
# Contributing to lprs
Thank you for your interest in contributing to lprs! We welcome contributions from the community to help improve the project.
Thank you for your interest in contributing to lprs! We welcome contributions
from the community to help improve the project.
## Reporting Issues
If you encounter any issues or bugs while using lprs, please open a new issue on the Forgejo repository. When reporting an issue, please provide as much detail as possible, including steps to reproduce the issue and any relevant error messages.
If you encounter any issues or bugs while using lprs, please open a new issue on
the Forgejo repository. When reporting an issue, please provide as much detail as
possible, including steps to reproduce the issue and any relevant error messages.
## Feature Requests
If you have a feature request or an idea for improving lprs, we encourage you to open a new issue on the Forgejo repository. Please describe the feature or improvement in detail and provide any relevant context or examples.
If you have a feature request or an idea for improving lprs, we encourage you to
open a new issue on the Forgejo repository. Please describe the feature or
improvement in detail and provide any relevant context or examples.
## Writing Code
Before you start writing code, please open a new issue first to discuss the proposed changes. This will help ensure that your contribution is aligned with the project's goals and that you are not duplicating work that is already in progress or has been completed by someone else.
Before you start writing code, please open a new issue first to discuss the proposed
changes. This will help ensure that your contribution is aligned with the project's
goals and that you are not duplicating work that is already in progress or has
been completed by someone else.
### Rust Version
In the lprs project, we always try to stay on the lowest MSRV possible for
compatibility, but the development process relies on the nightly release to get
the latest rust-analyzer and rustfmt features.
You can check the nightly version used in the project in the `rust-toolchain` file.
And the MSRV in the `Cargo.toml` file.
### PR title
Your PR will squash and merge, and your PR title will be used as the commit message. Please make sure your PR title is clear and concise.
Your PR will squash and merge, and your PR title will be used as the commit message.
Please make sure your PR title is clear and concise.
The title must follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) format. This means that the title should be in the following format:
The title must follow [Conventional Commits] format. This means that the title
should be in the following format:
```
<type>(<scope>): <description>
```
- The `<scope>` is optional, and the `<description>` should be a clear and concise summary of the changes.
- You should use the imperative, present tense (e.g., "Add feature" instead of "Added feature").
- The `<scope>` is optional, and the `<description>` should be a clear and
concise summary of the changes.
- You should use the imperative, present tense
(e.g., "Add feature" instead of "Added feature").
- The `<type>` should be one of the following:
- `feat`: A new feature
- `fix`: A bug fix
@ -35,7 +55,8 @@ The title must follow [Conventional Commits](https://www.conventionalcommits.org
- `security`: Changes that affect the security of the code
- `perf`: A code change that improves performance
- `test`: Adding missing tests or correcting existing tests
- `chore`: Changes to the build process or auxiliary tools and libraries such as documentation generation
- `chore`: Changes to the build process or auxiliary tools and libraries such
as documentation generation
#### Example
```
@ -44,13 +65,18 @@ The title must follow [Conventional Commits](https://www.conventionalcommits.org
```
### PR description
Your PR description should provide a clear and concise summary of the changes you have made. It should also include any relevant context or background information that will help the project maintainers understand the purpose of the changes. Make sure to reference the issue that your PR is addressing, and note any breaking changes that your PR introduces.
Your PR description should provide a clear and concise summary of the changes you
have made. It should also include any relevant context or background information
that will help the project maintainers understand the purpose of the changes.
Make sure to reference the issue that your PR is addressing, and note any breaking
changes that your PR introduces.
Make sure to explain why you made the changes not just what changes you made.
### Code Style
Please follow the existing code style and conventions used in the lprs project. This includes:
Please follow the existing code style and conventions used in the lprs project.
This includes:
- Using Rust's official formatting tool, `rustfmt`, to format your code.
- Writing clear and concise code with meaningful variable and function names.
@ -63,14 +89,23 @@ Run the CI before submitting your code. You can run the CI with the following co
just ci
```
This will run the tests and check the code formatting. If the CI fail, please fix the issues before submitting your code.
This will run the tests and check the code formatting. If the CI fail, please
fix the issues before submitting your code.
## Code Review
All contributions to lprs will go through a code review process. This ensures that the code meets the project's standards and maintains its quality. Please be open to feedback and suggestions from the project maintainers during the code review process.
All contributions to lprs will go through a code review process.
This ensures that the code meets the project's standards and maintains its quality.
Please be open to feedback and suggestions from the project maintainers during
the code review process.
## License
By contributing to lprs, you agree that your contributions will be licensed under the project's [LICENSE](LICENSE) file. This means that you are granting lprs the right to use, modify, and distribute your contributions under the terms of the license. wich is GPL-3.0 License.
By contributing to lprs, you agree that your contributions will be licensed under
the project's [license](LICENSE). This means that you are granting lprs the
right to use, modify, and distribute your contributions under the terms of the
license. Which is GPL-3.0 License.
Happy contributing!
[Conventional Commits]: https://www.conventionalcommits.org/en/v1.0.0/

View file

@ -5,10 +5,10 @@ edition = "2021"
license = "GPL-3.0-only"
authors = ["Awiteb <a@4rs.nl>"]
readme = "README.md"
description = "A local CLI password manager"
description = "A local CLI password/vault manager"
repository = "https://git.4rs.nl/awiteb/lprs"
rust-version = "1.74.0"
keywords = ["password", "manager", "CLI"]
keywords = ["password", "vault", "manager", "CLI"]
categories = ["command-line-utilities"]
[dependencies]
@ -35,5 +35,55 @@ default = ["update-notify"]
update-notify = ["reqwest/blocking"]
reqwest = ["dep:reqwest"]
[lints.rust]
unsafe_code = "forbid"
missing_docs = "warn"
[lints.clippy]
# I know is huge, but I like to be explicit, it also provides
# a better DX for new contributors (Make it easier to understand the codebase).
# Also, this is a general linting configuration, it's not specific to this project.
wildcard_imports = "deny"
manual_let_else = "deny"
match_bool = "deny"
match_on_vec_items = "deny"
or_fun_call = "deny"
panic = "deny"
unwrap_used = "deny"
missing_assert_message = "warn"
missing_const_for_fn = "warn"
missing_errors_doc = "warn"
absolute_paths = "warn"
cast_lossless = "warn"
clone_on_ref_ptr = "warn"
cloned_instead_of_copied = "warn"
dbg_macro = "warn"
default_trait_access = "warn"
empty_enum_variants_with_brackets = "warn"
empty_line_after_doc_comments = "warn"
empty_line_after_outer_attr = "warn"
empty_structs_with_brackets = "warn"
enum_glob_use = "warn"
equatable_if_let = "warn"
explicit_iter_loop = "warn"
filetype_is_file = "warn"
filter_map_next = "warn"
flat_map_option = "warn"
float_cmp = "warn"
format_push_string = "warn"
future_not_send = "warn"
if_not_else = "warn"
if_then_some_else_none = "warn"
implicit_clone = "warn"
inconsistent_struct_constructor = "warn"
indexing_slicing = "warn"
iter_filter_is_ok = "warn"
iter_filter_is_some = "warn"
iter_not_returning_iterator = "warn"
manual_is_variant_and = "warn"
option_if_let_else = "warn"
option_option = "warn"
[profile.release]
strip = true # Automatically strip symbols from the binary.

View file

@ -18,10 +18,9 @@ _default:
# Run the CI
@ci: && msrv
cargo +stable build -q
cargo +stable fmt -- --check
cargo +stable clippy -- -D warnings
cargo +{{msrv}} clippy -- -D warnings
cargo build -q
cargo fmt -- --check
cargo clippy -- -D warnings
# Check that the current MSRV is correct
@msrv:

13
rust-toolchain.toml Normal file
View file

@ -0,0 +1,13 @@
[toolchain]
# We use nightly in development only, the project will always be compliant with
# the latest stable release and the MSRV as defined in `Cargo.toml` file.
channel = "nightly-2024-05-05"
components = [
"rustc",
"cargo",
"rust-std",
"rust-src",
"rustfmt",
"rust-analyzer",
"clippy",
]

22
rustfmt.toml Normal file
View file

@ -0,0 +1,22 @@
unstable_features = true
version = "Two"
blank_lines_upper_bound = 2
combine_control_expr = false
wrap_comments = true
condense_wildcard_suffixes = true
edition = "2021"
enum_discrim_align_threshold = 20
force_multiline_blocks = true
format_code_in_doc_comments = true
format_generated_files = false
format_macro_matchers = true
format_strings = true
imports_layout = "HorizontalVertical"
newline_style = "Unix"
normalize_comments = true
reorder_impl_items = true
group_imports = "StdExternalCrate"
single_line_let_else_max_width = 0
struct_field_align_threshold = 20
use_try_shorthand = true

View file

@ -15,20 +15,26 @@
// along with this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
use clap::Args;
use inquire::{Password, PasswordDisplayMode};
use crate::{
vault::{Vault, Vaults},
LprsCommand, LprsError, LprsResult,
LprsCommand,
LprsError,
LprsResult,
};
#[derive(Debug, Args)]
#[command(author, version, about, long_about = None)]
/// Add command, used to add new vault to the vaults file
pub struct Add {
#[command(flatten)]
vault_info: Vault,
/// The password, if there is no value for it you will prompt it
#[arg(short, long)]
password: Option<Option<String>>,
// FIXME: I think replacing `Option<Option<String>>` with custom type will be better
#[allow(clippy::option_option)]
password: Option<Option<String>>,
}
impl LprsCommand for Add {
@ -41,10 +47,10 @@ impl LprsCommand for Add {
Some(None) => {
log::debug!("User didn't provide a password, prompting it");
self.vault_info.password = Some(
inquire::Password::new("Vault password:")
Password::new("Vault password:")
.without_confirmation()
.with_formatter(&|p| "*".repeat(p.chars().count()))
.with_display_mode(inquire::PasswordDisplayMode::Masked)
.with_display_mode(PasswordDisplayMode::Masked)
.prompt()?,
);
}

View file

@ -22,7 +22,8 @@ use crate::{vault::Vaults, LprsCommand, LprsError, LprsResult};
#[derive(Debug, Args)]
#[command(author, version, about, long_about = None)]
pub struct Clean {}
/// Clean command, used to clean the vaults file (remove all vaults)
pub struct Clean;
impl LprsCommand for Clean {
fn run(self, vault_manager: Vaults) -> LprsResult<()> {

View file

@ -17,33 +17,39 @@
use std::num::NonZeroU64;
use clap::Args;
use inquire::{Password, PasswordDisplayMode};
use crate::{
vault::{Vault, Vaults},
LprsCommand, LprsError, LprsResult,
LprsCommand,
LprsError,
LprsResult,
};
#[derive(Debug, Args)]
#[command(author, version, about, long_about = None)]
/// Edit command, used to edit the vault content
pub struct Edit {
/// The password index. Check it from list command
index: NonZeroU64,
#[arg(short, long)]
/// The new vault name
name: Option<String>,
name: Option<String>,
#[arg(short, long)]
/// The new vault username
username: Option<String>,
#[arg(short, long)]
/// The new password, if there is no value for it you will prompt it
// FIXME: I think replacing `Option<Option<String>>` with custom type will be better
#[allow(clippy::option_option)]
password: Option<Option<String>>,
#[arg(short, long)]
/// The new vault service
service: Option<String>,
service: Option<String>,
#[arg(short = 'o', long)]
/// The new vault note
note: Option<String>,
note: Option<String>,
}
impl LprsCommand for Edit {
@ -62,13 +68,15 @@ impl LprsCommand for Edit {
// Get the password from stdin or from its value if provided
let password = match self.password {
Some(Some(password)) => Some(password),
Some(None) => Some(
inquire::Password::new("New vault password:")
.without_confirmation()
.with_formatter(&|p| "*".repeat(p.chars().count()))
.with_display_mode(inquire::PasswordDisplayMode::Masked)
.prompt()?,
),
Some(None) => {
Some(
Password::new("New vault password:")
.without_confirmation()
.with_formatter(&|p| "*".repeat(p.chars().count()))
.with_display_mode(PasswordDisplayMode::Masked)
.prompt()?,
)
}
None => None,
};

View file

@ -20,14 +20,18 @@ use clap::Args;
use crate::{
vault::{BitWardenPasswords, Format, Vaults},
LprsCommand, LprsError, LprsResult,
LprsCommand,
LprsError,
LprsResult,
};
#[derive(Debug, Args)]
#[command(author, version, about, long_about = None)]
/// Export command, used to export the vaults in `lprs` format or `BitWarden`
/// format. The exported file will be a json file.
pub struct Export {
/// The path to export to
path: PathBuf,
path: PathBuf,
/// Format to export vaults in
#[arg(short, long, value_name = "FORMAT", default_value_t= Format::Lprs)]
format: Format,
@ -51,24 +55,29 @@ impl LprsCommand for Export {
}
fn validate_args(&self) -> LprsResult<()> {
if self
if !self
.path
.extension()
.is_some_and(|e| e.to_string_lossy().eq_ignore_ascii_case("json"))
{
if !self.path.exists() {
Ok(())
} else {
Err(LprsError::Io(IoError::new(
IoErrorKind::AlreadyExists,
format!("file `{}` is already exists", self.path.display()),
)))
}
} else {
Err(LprsError::Io(IoError::new(
return Err(LprsError::Io(IoError::new(
IoErrorKind::InvalidInput,
format!("file `{}` is not a json file", self.path.display()),
)))
)));
}
if self.path.exists() {
return Err(LprsError::Io(IoError::new(
IoErrorKind::AlreadyExists,
format!("file `{}` is already exists", self.path.display()),
)));
}
if self.path.is_dir() {
return Err(LprsError::Io(IoError::new(
IoErrorKind::InvalidInput,
format!("file `{}` is a directory", self.path.display()),
)));
}
Ok(())
}
}

View file

@ -22,6 +22,7 @@ use crate::{vault::Vaults, LprsCommand, LprsError, LprsResult};
#[derive(Debug, Args)]
#[command(author, version, about, long_about = None)]
/// Generate command, used to generate a password
pub struct Gen {
/// The password length
#[arg(default_value_t = NonZeroU64::new(18).unwrap())]
@ -35,10 +36,10 @@ pub struct Gen {
lowercase: bool,
/// With numbers (0-9)
#[arg(short, long)]
numbers: bool,
numbers: bool,
/// With symbols (!,# ...)
#[arg(short, long)]
symbols: bool,
symbols: bool,
}
impl LprsCommand for Gen {

View file

@ -25,11 +25,15 @@ use clap::Args;
use crate::{
vault::{BitWardenPasswords, Format, Vault, Vaults},
LprsCommand, LprsError, LprsResult,
LprsCommand,
LprsError,
LprsResult,
};
#[derive(Debug, Args)]
#[command(author, version, about, long_about = None)]
/// Import command, used to import vaults from the exported files, `lprs` or
/// `BitWarden`
pub struct Import {
/// The file path to import from
path: PathBuf,
@ -77,24 +81,29 @@ impl LprsCommand for Import {
}
fn validate_args(&self) -> LprsResult<()> {
if self.path.exists() {
if self
.path
.extension()
.is_some_and(|e| e.to_string_lossy().eq_ignore_ascii_case("json"))
{
Ok(())
} else {
Err(LprsError::Io(IoError::new(
IoErrorKind::InvalidInput,
format!("file `{}` is not a json file", self.path.display()),
)))
}
} else {
Err(LprsError::Io(IoError::new(
if self
.path
.extension()
.is_some_and(|e| e.to_string_lossy().eq_ignore_ascii_case("json"))
{
return Err(LprsError::Io(IoError::new(
IoErrorKind::InvalidInput,
format!("file `{}` is not a json file", self.path.display()),
)));
}
if !self.path.exists() {
return Err(LprsError::Io(IoError::new(
IoErrorKind::NotFound,
format!("file `{}` not found", self.path.display()),
)))
)));
}
if self.path.is_dir() {
return Err(LprsError::Io(IoError::new(
IoErrorKind::InvalidInput,
format!("file `{}` is a directory", self.path.display()),
)));
}
Ok(())
}
}

View file

@ -23,16 +23,17 @@ use crate::{vault::Vaults, LprsCommand, LprsError, LprsResult};
#[derive(Debug, Args)]
#[command(author, version, about, long_about = None)]
/// List command, used to list the vaults and search
pub struct List {
/// Return the password with spesifc index
#[arg(short, long, value_name = "INDEX")]
get: Option<NonZeroU64>,
get: Option<NonZeroU64>,
/// Filter the select list
#[arg(short, long, value_name = "TEXT")]
filter: Option<String>,
/// Enable regex when use `--filter` option
#[arg(short, long)]
regex: bool,
regex: bool,
}
impl LprsCommand for List {

View file

@ -20,13 +20,23 @@ use clap::Parser;
use crate::{impl_commands, utils, vault::Vaults, LprsCommand, LprsResult};
/// Add command, used to add new vault to the vaults file
pub mod add_command;
/// Clean command, used to clean the vaults file (remove all vaults)
pub mod clean_command;
/// Edit command, used to edit the vault content
pub mod edit_command;
/// Export command, used to export the vaults
/// in `lprs` format or `BitWarden` format
pub mod export_command;
/// Generate command, used to generate a password
pub mod gen_command;
/// Import command, used to import vaults from the exported files, `lprs` or
/// `BitWarden`
pub mod import_command;
/// List command, used to list the vaults and search
pub mod list_command;
/// Remove command, used to remove vault from the vaults file
pub mod remove_command;
/// The lprs commands
@ -56,20 +66,29 @@ impl_commands!(Commands, Add Remove List Clean Edit Gen Export Import);
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
/// The lprs cli, manage the CLI arguments and run the commands
pub struct Cli {
/// The vaults json file
#[arg(short = 'f', long)]
pub vaults_file: Option<PathBuf>,
/// Show the logs in the stdout
#[arg(short, long)]
pub verbose: bool,
pub verbose: bool,
#[command(subcommand)]
/// The provided command to run
pub command: Commands,
}
impl Cli {
/// Run the cli
///
/// # Errors
/// - If can't get the default vaults file
/// - If the vaults file can't be created
/// - If the user provide a worng CLI arguments
/// - If failed to write in the vaults file
/// - (errors from the commands)
pub fn run(self) -> LprsResult<()> {
let vaults_file = if let Some(path) = self.vaults_file {
log::info!("Using the given vaults file");
@ -83,7 +102,7 @@ impl Cli {
path
} else {
log::info!("Using the default vaults file");
crate::utils::vaults_file()?
utils::vaults_file()?
};
log::debug!("Vaults file: {}", vaults_file.display());

View file

@ -22,11 +22,13 @@ use crate::{vault::Vaults, LprsCommand, LprsError, LprsResult};
#[derive(Debug, Args)]
#[command(author, version, about, long_about = None)]
/// Remove command, used to remove a vault from the vaults file
pub struct Remove {
/// The password index
index: NonZeroU64,
/// Force remove, will not return error if there is no password with this index
/// Force remove, will not return error if there is no password with this
/// index
#[arg(short, long)]
force: bool,
}
@ -37,14 +39,14 @@ impl LprsCommand for Remove {
log::debug!("Removing vault at index: {index}");
if index > vault_manager.vaults.len() {
if !self.force {
return Err(LprsError::Other(
"The index is greater than the passwords counts".to_owned(),
));
} else {
if self.force {
log::error!(
"The index is greater than the passwords counts, but the force flag is enabled"
);
} else {
return Err(LprsError::Other(
"The index is greater than the passwords counts".to_owned(),
));
}
} else {
vault_manager.vaults.remove(index);

View file

@ -14,15 +14,20 @@
// 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::{process::ExitCode, string::FromUtf8Error};
use std::{io, process::ExitCode, result, string::FromUtf8Error};
pub type Result<T> = std::result::Result<T, Error>;
/// The result type used in the whole project
pub type Result<T> = result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[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.")]
#[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")]
WrongMasterPassword,
@ -50,12 +55,12 @@ pub enum Error {
#[error("Project Folder Error: {0}")]
ProjectDir(String),
#[error("IO Error: {0}")]
Io(#[from] std::io::Error),
Io(#[from] io::Error),
}
impl Error {
/// Return the exit code of the error
pub fn exit_code(&self) -> ExitCode {
pub const fn exit_code(&self) -> ExitCode {
// TODO: Exit with more specific exit code
ExitCode::FAILURE
}

View file

@ -29,10 +29,7 @@
/// #### Output
/// ```rust
/// impl crate::LprsCommand for TestCommands {
/// fn run(
/// &self,
/// vault_manager: crate::vault::Vaults,
/// ) -> crate::LprsResult<()> {
/// fn run(&self, vault_manager: crate::vault::Vaults) -> crate::LprsResult<()> {
/// match self {
/// Self::Test(command) => command.run(vault_manager),
/// Self::Some(command) => command.run(vault_manager),
@ -46,7 +43,6 @@
/// }
/// }
/// }
/// ```
#[macro_export]
macro_rules! impl_commands {

View file

@ -13,14 +13,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>.
#![doc = include_str!("../README.md")]
use std::env;
use std::process::ExitCode;
use clap::Parser;
use inquire::InquireError;
use std::process::ExitCode;
/// The main module of the lprs crate, contains the cli and the commands.
pub mod cli;
/// The errors module, contains the errors and the result type.
pub mod errors;
/// The utils module, contains the utility functions of all the modules.
pub mod utils;
/// The vault module, contains the vault struct and the vaults manager.
pub mod vault;
mod macros;
@ -30,17 +37,20 @@ pub use base64::engine::general_purpose::STANDARD as BASE64;
pub use errors::{Error as LprsError, Result as LprsResult};
pub use traits::*;
/// The default vaults file name. Used to store the vaults.
pub const DEFAULT_VAULTS_FILE: &str = "vaults.lprs";
#[cfg(feature = "update-notify")]
/// The version of the lprs crate.
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg(feature = "update-notify")]
/// The last version check file. Used to store the last version check time.
pub const LAST_VERSION_CHECK_FILE: &str = ".last_version_check";
fn main() -> ExitCode {
let lprs_cli = cli::Cli::parse();
if lprs_cli.verbose {
std::env::set_var("RUST_LOG", "lprs");
env::set_var("RUST_LOG", "lprs");
}
pretty_env_logger::init();

View file

@ -17,6 +17,7 @@
use crate::{vault::Vaults, LprsResult};
/// Trait to work with the commands
#[allow(clippy::missing_errors_doc)]
pub trait LprsCommand {
/// Run the command, should do all the logic, even the export
fn run(self, vault_manager: Vaults) -> LprsResult<()>;

View file

@ -16,12 +16,19 @@
use std::{fs, path::PathBuf};
use inquire::validator::Validation;
use inquire::{validator::Validation, PasswordDisplayMode};
use passwords::{analyzer, scorer};
#[cfg(feature = "update-notify")]
use reqwest::blocking::Client as BlockingClient;
use sha2::Digest;
use crate::{LprsError, LprsResult};
/// Returns the local project dir joined with the given file name
///
/// ## Errors
/// - If the project dir can't be extracted from the OS
/// - If the local project dir can't be created
pub fn local_project_file(filename: &str) -> LprsResult<PathBuf> {
let local_dir = directories::ProjectDirs::from("", "", "lprs")
.map(|d| d.data_local_dir().to_path_buf())
@ -37,6 +44,10 @@ pub fn local_project_file(filename: &str) -> LprsResult<PathBuf> {
}
/// Returns the default vaults json file
///
/// ## Errors
/// - If the project dir can't be extracted from the OS
/// - If the vaults file can't be created
pub fn vaults_file() -> LprsResult<PathBuf> {
let vaults_file = local_project_file(crate::DEFAULT_VAULTS_FILE)?;
if !vaults_file.exists() {
@ -50,22 +61,26 @@ pub fn vaults_file() -> LprsResult<PathBuf> {
/// ## To pass
/// - The length must be higher than 14 (>=15)
/// - Its score must be greater than 80.0
///
/// ## Errors
/// - There is no errors, just the return type of inquire validator must be
/// Result<Validation, inquire::CustomUserError>
pub fn password_validator(password: &str) -> Result<Validation, inquire::CustomUserError> {
let analyzed = passwords::analyzer::analyze(password);
if analyzed.length() < 15 {
return Ok(Validation::Invalid(
"The master password length must be beggier then 15".into(),
));
} else if passwords::scorer::score(&analyzed) < 80.0 {
return Ok(Validation::Invalid(
"Your master password is not stronge enough".into(),
));
}
Ok(Validation::Valid)
let analyzed = analyzer::analyze(password);
Ok(if analyzed.length() < 15 {
Validation::Invalid("The master password length must be beggier then 15".into())
} else if scorer::score(&analyzed) < 80.0 {
Validation::Invalid("Your master password is not stronge enough".into())
} else {
Validation::Valid
})
}
/// Ask the user for the master password, then returns it
///
/// ## Errors
/// - Can't read the password from the user
///
/// 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 {
@ -79,13 +94,17 @@ pub fn master_password_prompt(is_new_vaults_file: bool) -> LprsResult<[u8; 32]>
..inquire::Password::new("")
}
.with_formatter(&|p| "*".repeat(p.chars().count()))
.with_display_mode(inquire::PasswordDisplayMode::Masked)
.with_display_mode(PasswordDisplayMode::Masked)
.prompt()
.map(|p| sha2::Sha256::digest(p).into())
.map_err(Into::into)
}
/// Retuns the current lprs version from `crates.io`
///
/// ## Errors
/// - The project dir can't be extracted from the OS
/// - If the last version check file can't be created
#[cfg(feature = "update-notify")]
pub fn lprs_version() -> LprsResult<Option<String>> {
use std::time::SystemTime;
@ -108,7 +127,7 @@ pub fn lprs_version() -> LprsResult<Option<String>> {
// Check if the last check is before one hour or not
if (current_time - last_check) >= (60 * 60) || current_time == last_check {
if let Ok(Ok(response)) = reqwest::blocking::Client::new()
if let Ok(Ok(response)) = BlockingClient::new()
.get("https://crates.io/api/v1/crates/lprs")
.header(
"User-Agent",
@ -117,8 +136,8 @@ pub fn lprs_version() -> LprsResult<Option<String>> {
.send()
.map(|r| r.text())
{
let re =
regex::Regex::new(r#""max_stable_version":"(?<version>\d+\.\d+\.\d+)""#).unwrap();
let re = regex::Regex::new(r#""max_stable_version":"(?<version>\d+\.\d+\.\d+)""#)
.expect("The regex is correct");
if let Some(cap) = re.captures(&response) {
return Ok(cap.name("version").map(|m| m.as_str().to_string()));
}

View file

@ -1,3 +1,23 @@
// 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>.
// This file is not important, it is just a struct that is used to serialize and
// deserialize the vaults from and to the BitWarden format.
#![allow(missing_docs)]
use serde::{Deserialize, Serialize};
use super::{Vault, Vaults};
@ -6,27 +26,27 @@ use super::{Vault, Vaults};
pub struct BitWardenLoginData {
pub username: Option<String>,
pub password: Option<String>,
pub uris: Option<Vec<BitWardenUri>>,
pub uris: Option<Vec<BitWardenUri>>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct BitWardenUri {
#[serde(rename = "match")]
pub mt: Option<i32>,
pub mt: Option<i32>,
pub uri: String,
}
#[derive(Default, Deserialize, Serialize)]
pub struct BitWardenFolder {
pub id: String,
pub id: String,
pub name: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct BitWardenPassword {
#[serde(rename = "type")]
pub ty: i32,
pub name: String,
pub ty: i32,
pub name: String,
pub login: Option<BitWardenLoginData>,
pub notes: Option<String>,
}
@ -35,8 +55,8 @@ pub struct BitWardenPassword {
#[derive(Default, Deserialize, Serialize)]
pub struct BitWardenPasswords {
pub encrypted: bool,
pub folders: Vec<BitWardenFolder>,
pub items: Vec<BitWardenPassword>,
pub folders: Vec<BitWardenFolder>,
pub items: Vec<BitWardenPassword>,
}
impl From<BitWardenPassword> for Vault {
@ -58,12 +78,12 @@ impl From<BitWardenPassword> for Vault {
impl From<Vault> for BitWardenPassword {
fn from(value: Vault) -> Self {
Self {
ty: 1,
name: value.name,
ty: 1,
name: value.name,
login: Some(BitWardenLoginData {
username: value.username,
password: value.password,
uris: value
uris: value
.service
.map(|s| vec![BitWardenUri { mt: None, uri: s }]),
}),
@ -76,8 +96,8 @@ impl From<Vaults> for BitWardenPasswords {
fn from(value: Vaults) -> Self {
Self {
encrypted: false,
folders: Vec::new(),
items: value
folders: Vec::new(),
items: value
.vaults
.into_iter()
.map(BitWardenPassword::from)

View file

@ -14,9 +14,10 @@
// 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::time::{SystemTime, UNIX_EPOCH};
use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
use rand::{rngs::StdRng, Rng, SeedableRng};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::{LprsError, LprsResult};

View file

@ -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::{fmt, fs, path::PathBuf};
use base64::Engine;
use clap::{Parser, ValueEnum};
@ -22,6 +22,7 @@ use serde::{Deserialize, Serialize};
use crate::{LprsError, LprsResult};
/// The chiper module, used to encrypt and decrypt the vaults
pub mod cipher;
mod bitwarden;
@ -29,8 +30,14 @@ mod bitwarden;
pub use bitwarden::*;
#[derive(Clone, Debug, ValueEnum)]
/// The vaults format
pub enum Format {
/// The lprs format, which is the default format
/// and is is the result of the serialization/deserialization of the Vaults
/// struct
Lprs,
/// The BitWarden format, which is the result of the
/// serialization/deserialization of the BitWardenPasswords struct
BitWarden,
}
@ -39,7 +46,7 @@ pub enum Format {
pub struct Vault {
/// The name of the vault
#[arg(short, long)]
pub name: String,
pub name: String,
/// The username
#[arg(short, long)]
pub username: Option<String>,
@ -48,10 +55,10 @@ pub struct Vault {
pub password: Option<String>,
/// The service name. e.g the website url
#[arg(short, long)]
pub service: Option<String>,
pub service: Option<String>,
/// Add a note to the vault
#[arg(short = 'o', long)]
pub note: Option<String>,
pub note: Option<String>,
}
/// The vaults manager
@ -60,9 +67,9 @@ pub struct Vaults {
/// Hash of the master password
pub master_password: [u8; 32],
/// The json vaults file
pub vaults_file: PathBuf,
pub vaults_file: PathBuf,
/// The vaults
pub vaults: Vec<Vault>,
pub vaults: Vec<Vault>,
}
impl Vault {
@ -75,17 +82,17 @@ impl Vault {
note: Option<impl Into<String>>,
) -> Self {
Self {
name: name.into(),
name: name.into(),
username: username.map(Into::into),
password: password.map(Into::into),
service: service.map(Into::into),
note: note.map(Into::into),
service: service.map(Into::into),
note: note.map(Into::into),
}
}
/// Return the name of the vault with the service if there
pub fn list_name(&self) -> String {
use std::fmt::Write;
use fmt::Write;
let mut list_name = self.name.clone();
if let Some(ref username) = self.username {
write!(&mut list_name, " <{username}>").expect("String never fail");
@ -119,6 +126,10 @@ impl Vaults {
///
/// This function used to backup the vaults.
///
/// ## Errors
/// - If the serialization failed
/// - if the encryption failed
///
/// Note: The returned string is `Vec<Vault>`
pub fn json_export(&self) -> LprsResult<String> {
let encrypt = |val: &str| {
@ -147,6 +158,10 @@ impl Vaults {
/// Reload the vaults from json data.
///
/// ## Errors
/// - If base64 decoding failed (of the vault field encrypted data)
/// - If decryption failed (wrong master password or the data is corrupted)
///
/// 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| {
@ -172,6 +187,9 @@ impl Vaults {
}
/// Encrypt the vaults then export it to the file
///
/// ## Errors
/// - Writing to the file failed
pub fn try_export(self) -> LprsResult<()> {
log::debug!(
"Trying to export the vaults to the file: {}",
@ -185,6 +203,11 @@ impl Vaults {
}
/// Reload the vaults from the file then decrypt it
///
/// ## Errors
/// - Reading the file failed
/// - Decryption failed (wrong master password or the data is corrupted)
/// - Bytecode deserialization failed (the data is corrupted)
pub fn try_reload(vaults_file: PathBuf, master_password: [u8; 32]) -> LprsResult<Self> {
let vaults_data = fs::read(&vaults_file)?;
@ -198,8 +221,8 @@ impl Vaults {
}
}
impl std::fmt::Display for Format {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl fmt::Display for Format {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
@ -210,8 +233,8 @@ impl std::fmt::Display for Format {
}
}
impl std::fmt::Display for Vault {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl fmt::Display for Vault {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Name: {}", self.name)?;
if let Some(ref username) = self.username {
write!(f, "\nUsername: {username}")?;