Compare commits

..

No commits in common. "3436e616f8d59f87012781e4e9d01d067c7e77ad" and "f8086b24ff04e6cb60e4136b7fc93d47202ba173" have entirely different histories.

24 changed files with 135 additions and 394 deletions

View file

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

View file

@ -19,11 +19,4 @@ jobs:
mv git-sumi-x86_64-unknown-linux-gnu/git-sumi git-sumi mv git-sumi-x86_64-unknown-linux-gnu/git-sumi git-sumi
chmod +x git-sumi chmod +x git-sumi
- name: Run git-sumi - name: Run git-sumi
run: | run: ./git-sumi "${{ github.event.pull_request.title }}"
# 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,49 +1,29 @@
# Contributing to lprs # Contributing to lprs
Thank you for your interest in contributing to lprs! We welcome contributions Thank you for your interest in contributing to lprs! We welcome contributions from the community to help improve the project.
from the community to help improve the project.
## Reporting Issues ## Reporting Issues
If you encounter any issues or bugs while using lprs, please open a new issue on 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.
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 ## Feature Requests
If you have a feature request or an idea for improving lprs, we encourage you to 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.
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 ## Writing Code
Before you start writing code, please open a new issue first to discuss the proposed 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.
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 ### PR title
Your PR will squash and merge, and your PR title will be used as the commit message. 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.
Please make sure your PR title is clear and concise.
The title must follow [Conventional Commits] format. This means that the title 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:
should be in the following format:
``` ```
<type>(<scope>): <description> <type>(<scope>): <description>
``` ```
- The `<scope>` is optional, and the `<description>` should be a clear and - The `<scope>` is optional, and the `<description>` should be a clear and concise summary of the changes.
concise summary of the changes. - You should use the imperative, present tense (e.g., "Add feature" instead of "Added feature").
- You should use the imperative, present tense
(e.g., "Add feature" instead of "Added feature").
- The `<type>` should be one of the following: - The `<type>` should be one of the following:
- `feat`: A new feature - `feat`: A new feature
- `fix`: A bug fix - `fix`: A bug fix
@ -55,8 +35,7 @@ should be in the following format:
- `security`: Changes that affect the security of the code - `security`: Changes that affect the security of the code
- `perf`: A code change that improves performance - `perf`: A code change that improves performance
- `test`: Adding missing tests or correcting existing tests - `test`: Adding missing tests or correcting existing tests
- `chore`: Changes to the build process or auxiliary tools and libraries such - `chore`: Changes to the build process or auxiliary tools and libraries such as documentation generation
as documentation generation
#### Example #### Example
``` ```
@ -65,18 +44,13 @@ should be in the following format:
``` ```
### PR description ### PR description
Your PR description should provide a clear and concise summary of the changes you 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.
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. Make sure to explain why you made the changes not just what changes you made.
### Code Style ### Code Style
Please follow the existing code style and conventions used in the lprs project. Please follow the existing code style and conventions used in the lprs project. This includes:
This includes:
- Using Rust's official formatting tool, `rustfmt`, to format your code. - Using Rust's official formatting tool, `rustfmt`, to format your code.
- Writing clear and concise code with meaningful variable and function names. - Writing clear and concise code with meaningful variable and function names.
@ -89,23 +63,14 @@ Run the CI before submitting your code. You can run the CI with the following co
just ci just ci
``` ```
This will run the tests and check the code formatting. If the CI fail, please This will run the tests and check the code formatting. If the CI fail, please fix the issues before submitting your code.
fix the issues before submitting your code.
## Code Review ## Code Review
All contributions to lprs will go through a 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.
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 ## License
By contributing to lprs, you agree that your contributions will be licensed under 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.
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! 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" license = "GPL-3.0-only"
authors = ["Awiteb <a@4rs.nl>"] authors = ["Awiteb <a@4rs.nl>"]
readme = "README.md" readme = "README.md"
description = "A local CLI password/vault manager" description = "A local CLI password manager"
repository = "https://git.4rs.nl/awiteb/lprs" repository = "https://git.4rs.nl/awiteb/lprs"
rust-version = "1.74.0" rust-version = "1.74.0"
keywords = ["password", "vault", "manager", "CLI"] keywords = ["password", "manager", "CLI"]
categories = ["command-line-utilities"] categories = ["command-line-utilities"]
[dependencies] [dependencies]
@ -35,55 +35,5 @@ default = ["update-notify"]
update-notify = ["reqwest/blocking"] update-notify = ["reqwest/blocking"]
reqwest = ["dep:reqwest"] 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] [profile.release]
strip = true # Automatically strip symbols from the binary. strip = true # Automatically strip symbols from the binary.

View file

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

View file

@ -1,13 +0,0 @@
[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",
]

View file

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

View file

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

View file

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

View file

@ -20,18 +20,14 @@ use clap::Args;
use crate::{ use crate::{
vault::{BitWardenPasswords, Format, Vaults}, vault::{BitWardenPasswords, Format, Vaults},
LprsCommand, LprsCommand, LprsError, LprsResult,
LprsError,
LprsResult,
}; };
#[derive(Debug, Args)] #[derive(Debug, Args)]
#[command(author, version, about, long_about = None)] #[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 { pub struct Export {
/// The path to export to /// The path to export to
path: PathBuf, path: PathBuf,
/// Format to export vaults in /// Format to export vaults in
#[arg(short, long, value_name = "FORMAT", default_value_t= Format::Lprs)] #[arg(short, long, value_name = "FORMAT", default_value_t= Format::Lprs)]
format: Format, format: Format,
@ -55,29 +51,24 @@ impl LprsCommand for Export {
} }
fn validate_args(&self) -> LprsResult<()> { fn validate_args(&self) -> LprsResult<()> {
if !self if self
.path .path
.extension() .extension()
.is_some_and(|e| e.to_string_lossy().eq_ignore_ascii_case("json")) .is_some_and(|e| e.to_string_lossy().eq_ignore_ascii_case("json"))
{ {
return Err(LprsError::Io(IoError::new( 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(
IoErrorKind::InvalidInput, IoErrorKind::InvalidInput,
format!("file `{}` is not a json file", self.path.display()), 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,7 +22,6 @@ use crate::{vault::Vaults, LprsCommand, LprsError, LprsResult};
#[derive(Debug, Args)] #[derive(Debug, Args)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
/// Generate command, used to generate a password
pub struct Gen { pub struct Gen {
/// The password length /// The password length
#[arg(default_value_t = NonZeroU64::new(18).unwrap())] #[arg(default_value_t = NonZeroU64::new(18).unwrap())]
@ -36,10 +35,10 @@ pub struct Gen {
lowercase: bool, lowercase: bool,
/// With numbers (0-9) /// With numbers (0-9)
#[arg(short, long)] #[arg(short, long)]
numbers: bool, numbers: bool,
/// With symbols (!,# ...) /// With symbols (!,# ...)
#[arg(short, long)] #[arg(short, long)]
symbols: bool, symbols: bool,
} }
impl LprsCommand for Gen { impl LprsCommand for Gen {

View file

@ -25,15 +25,11 @@ use clap::Args;
use crate::{ use crate::{
vault::{BitWardenPasswords, Format, Vault, Vaults}, vault::{BitWardenPasswords, Format, Vault, Vaults},
LprsCommand, LprsCommand, LprsError, LprsResult,
LprsError,
LprsResult,
}; };
#[derive(Debug, Args)] #[derive(Debug, Args)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
/// Import command, used to import vaults from the exported files, `lprs` or
/// `BitWarden`
pub struct Import { pub struct Import {
/// The file path to import from /// The file path to import from
path: PathBuf, path: PathBuf,
@ -81,29 +77,24 @@ impl LprsCommand for Import {
} }
fn validate_args(&self) -> LprsResult<()> { fn validate_args(&self) -> LprsResult<()> {
if self if self.path.exists() {
.path if self
.extension() .path
.is_some_and(|e| e.to_string_lossy().eq_ignore_ascii_case("json")) .extension()
{ .is_some_and(|e| e.to_string_lossy().eq_ignore_ascii_case("json"))
return Err(LprsError::Io(IoError::new( {
IoErrorKind::InvalidInput, Ok(())
format!("file `{}` is not a json file", self.path.display()), } else {
))); Err(LprsError::Io(IoError::new(
} IoErrorKind::InvalidInput,
if !self.path.exists() { format!("file `{}` is not a json file", self.path.display()),
return Err(LprsError::Io(IoError::new( )))
}
} else {
Err(LprsError::Io(IoError::new(
IoErrorKind::NotFound, IoErrorKind::NotFound,
format!("file `{}` not found", self.path.display()), 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,17 +23,16 @@ use crate::{vault::Vaults, LprsCommand, LprsError, LprsResult};
#[derive(Debug, Args)] #[derive(Debug, Args)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
/// List command, used to list the vaults and search
pub struct List { pub struct List {
/// Return the password with spesifc index /// Return the password with spesifc index
#[arg(short, long, value_name = "INDEX")] #[arg(short, long, value_name = "INDEX")]
get: Option<NonZeroU64>, get: Option<NonZeroU64>,
/// Filter the select list /// Filter the select list
#[arg(short, long, value_name = "TEXT")] #[arg(short, long, value_name = "TEXT")]
filter: Option<String>, filter: Option<String>,
/// Enable regex when use `--filter` option /// Enable regex when use `--filter` option
#[arg(short, long)] #[arg(short, long)]
regex: bool, regex: bool,
} }
impl LprsCommand for List { impl LprsCommand for List {

View file

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

View file

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

View file

@ -14,20 +14,15 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>. // along with this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
use std::{io, process::ExitCode, result, string::FromUtf8Error}; use std::{process::ExitCode, string::FromUtf8Error};
/// The result type used in the whole project pub type Result<T> = std::result::Result<T, Error>;
pub type Result<T> = result::Result<T, Error>;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error { pub enum Error {
#[error("Encryption Error: {0}")] #[error("Encryption Error: {0}")]
Encryption(String), Encryption(String),
#[error( #[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: The given key cannot decrypt the given data. Either the data has been \
tampered with or the key is incorrect."
)]
Decryption, Decryption,
#[error("Wrong Master Password Error: Wrong decryption password")] #[error("Wrong Master Password Error: Wrong decryption password")]
WrongMasterPassword, WrongMasterPassword,
@ -55,12 +50,12 @@ pub enum Error {
#[error("Project Folder Error: {0}")] #[error("Project Folder Error: {0}")]
ProjectDir(String), ProjectDir(String),
#[error("IO Error: {0}")] #[error("IO Error: {0}")]
Io(#[from] io::Error), Io(#[from] std::io::Error),
} }
impl Error { impl Error {
/// Return the exit code of the error /// Return the exit code of the error
pub const fn exit_code(&self) -> ExitCode { pub fn exit_code(&self) -> ExitCode {
// TODO: Exit with more specific exit code // TODO: Exit with more specific exit code
ExitCode::FAILURE ExitCode::FAILURE
} }

View file

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

View file

@ -13,21 +13,14 @@
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>. // along with this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
#![doc = include_str!("../README.md")]
use std::env;
use std::process::ExitCode;
use clap::Parser; use clap::Parser;
use inquire::InquireError; use inquire::InquireError;
use std::process::ExitCode;
/// The main module of the lprs crate, contains the cli and the commands.
pub mod cli; pub mod cli;
/// The errors module, contains the errors and the result type.
pub mod errors; pub mod errors;
/// The utils module, contains the utility functions of all the modules.
pub mod utils; pub mod utils;
/// The vault module, contains the vault struct and the vaults manager.
pub mod vault; pub mod vault;
mod macros; mod macros;
@ -37,20 +30,17 @@ pub use base64::engine::general_purpose::STANDARD as BASE64;
pub use errors::{Error as LprsError, Result as LprsResult}; pub use errors::{Error as LprsError, Result as LprsResult};
pub use traits::*; pub use traits::*;
/// The default vaults file name. Used to store the vaults.
pub const DEFAULT_VAULTS_FILE: &str = "vaults.lprs"; pub const DEFAULT_VAULTS_FILE: &str = "vaults.lprs";
#[cfg(feature = "update-notify")] #[cfg(feature = "update-notify")]
/// The version of the lprs crate.
pub const VERSION: &str = env!("CARGO_PKG_VERSION"); pub const VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg(feature = "update-notify")] #[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"; pub const LAST_VERSION_CHECK_FILE: &str = ".last_version_check";
fn main() -> ExitCode { fn main() -> ExitCode {
let lprs_cli = cli::Cli::parse(); let lprs_cli = cli::Cli::parse();
if lprs_cli.verbose { if lprs_cli.verbose {
env::set_var("RUST_LOG", "lprs"); std::env::set_var("RUST_LOG", "lprs");
} }
pretty_env_logger::init(); pretty_env_logger::init();

View file

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

View file

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

View file

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

View file

@ -14,10 +14,9 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>. // along with this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
use std::time::{SystemTime, UNIX_EPOCH};
use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit}; use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
use rand::{rngs::StdRng, Rng, SeedableRng}; use rand::{rngs::StdRng, Rng, SeedableRng};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::{LprsError, LprsResult}; use crate::{LprsError, LprsResult};

View file

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