chore(DX): Improve the DX #27
24 changed files with 394 additions and 135 deletions
|
@ -13,6 +13,8 @@ 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
|
||||||
|
|
|
@ -19,4 +19,11 @@ 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: ./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 }}"
|
||||||
|
|
|
@ -1,29 +1,49 @@
|
||||||
# Contributing to lprs
|
# 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
|
## 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
|
## 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
|
## 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
|
### 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>
|
<type>(<scope>): <description>
|
||||||
```
|
```
|
||||||
|
|
||||||
- The `<scope>` is optional, and the `<description>` should be a clear and concise summary of the changes.
|
- The `<scope>` is optional, and the `<description>` should be a clear and
|
||||||
- You should use the imperative, present tense (e.g., "Add feature" instead of "Added feature").
|
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:
|
- The `<type>` should be one of the following:
|
||||||
- `feat`: A new feature
|
- `feat`: A new feature
|
||||||
- `fix`: A bug fix
|
- `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
|
- `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 as documentation generation
|
- `chore`: Changes to the build process or auxiliary tools and libraries such
|
||||||
|
as documentation generation
|
||||||
|
|
||||||
#### Example
|
#### Example
|
||||||
```
|
```
|
||||||
|
@ -44,13 +65,18 @@ The title must follow [Conventional Commits](https://www.conventionalcommits.org
|
||||||
```
|
```
|
||||||
|
|
||||||
### PR description
|
### 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.
|
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. 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.
|
- 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.
|
||||||
|
@ -63,14 +89,23 @@ 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 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
|
## 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
|
## 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!
|
Happy contributing!
|
||||||
|
|
||||||
|
[Conventional Commits]: https://www.conventionalcommits.org/en/v1.0.0/
|
||||||
|
|
54
Cargo.toml
54
Cargo.toml
|
@ -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 manager"
|
description = "A local CLI password/vault 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", "manager", "CLI"]
|
keywords = ["password", "vault", "manager", "CLI"]
|
||||||
categories = ["command-line-utilities"]
|
categories = ["command-line-utilities"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
@ -35,5 +35,55 @@ 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.
|
||||||
|
|
7
Justfile
7
Justfile
|
@ -18,10 +18,9 @@ _default:
|
||||||
|
|
||||||
# Run the CI
|
# Run the CI
|
||||||
@ci: && msrv
|
@ci: && msrv
|
||||||
cargo +stable build -q
|
cargo build -q
|
||||||
cargo +stable fmt -- --check
|
cargo fmt -- --check
|
||||||
cargo +stable clippy -- -D warnings
|
cargo clippy -- -D warnings
|
||||||
cargo +{{msrv}} clippy -- -D warnings
|
|
||||||
|
|
||||||
# Check that the current MSRV is correct
|
# Check that the current MSRV is correct
|
||||||
@msrv:
|
@msrv:
|
||||||
|
|
13
rust-toolchain.toml
Normal file
13
rust-toolchain.toml
Normal 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
22
rustfmt.toml
Normal 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
|
|
@ -15,20 +15,26 @@
|
||||||
// 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, LprsError, LprsResult,
|
LprsCommand,
|
||||||
|
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)]
|
||||||
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 {
|
impl LprsCommand for Add {
|
||||||
|
@ -41,10 +47,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(
|
||||||
inquire::Password::new("Vault password:")
|
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(inquire::PasswordDisplayMode::Masked)
|
.with_display_mode(PasswordDisplayMode::Masked)
|
||||||
.prompt()?,
|
.prompt()?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,8 @@ 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)]
|
||||||
pub struct Clean {}
|
/// Clean command, used to clean the vaults file (remove all vaults)
|
||||||
|
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<()> {
|
||||||
|
|
|
@ -17,33 +17,39 @@
|
||||||
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, LprsError, LprsResult,
|
LprsCommand,
|
||||||
|
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 {
|
||||||
|
@ -62,13 +68,15 @@ 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(
|
Some(None) => {
|
||||||
inquire::Password::new("New vault password:")
|
Some(
|
||||||
.without_confirmation()
|
Password::new("New vault password:")
|
||||||
.with_formatter(&|p| "*".repeat(p.chars().count()))
|
.without_confirmation()
|
||||||
.with_display_mode(inquire::PasswordDisplayMode::Masked)
|
.with_formatter(&|p| "*".repeat(p.chars().count()))
|
||||||
.prompt()?,
|
.with_display_mode(PasswordDisplayMode::Masked)
|
||||||
),
|
.prompt()?,
|
||||||
|
)
|
||||||
|
}
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -20,14 +20,18 @@ use clap::Args;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
vault::{BitWardenPasswords, Format, Vaults},
|
vault::{BitWardenPasswords, Format, Vaults},
|
||||||
LprsCommand, LprsError, LprsResult,
|
LprsCommand,
|
||||||
|
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,
|
||||||
|
@ -51,24 +55,29 @@ 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"))
|
||||||
{
|
{
|
||||||
if !self.path.exists() {
|
return Err(LprsError::Io(IoError::new(
|
||||||
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(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +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)]
|
||||||
|
/// 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())]
|
||||||
|
@ -35,10 +36,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 {
|
||||||
|
|
|
@ -25,11 +25,15 @@ use clap::Args;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
vault::{BitWardenPasswords, Format, Vault, Vaults},
|
vault::{BitWardenPasswords, Format, Vault, Vaults},
|
||||||
LprsCommand, LprsError, LprsResult,
|
LprsCommand,
|
||||||
|
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,
|
||||||
|
@ -77,24 +81,29 @@ impl LprsCommand for Import {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_args(&self) -> LprsResult<()> {
|
fn validate_args(&self) -> LprsResult<()> {
|
||||||
if self.path.exists() {
|
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(
|
||||||
Ok(())
|
IoErrorKind::InvalidInput,
|
||||||
} else {
|
format!("file `{}` is not a json file", self.path.display()),
|
||||||
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(
|
||||||
}
|
|
||||||
} 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(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,16 +23,17 @@ 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 {
|
||||||
|
|
|
@ -20,13 +20,23 @@ 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
|
||||||
|
@ -56,20 +66,29 @@ 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");
|
||||||
|
@ -83,7 +102,7 @@ impl Cli {
|
||||||
path
|
path
|
||||||
} else {
|
} else {
|
||||||
log::info!("Using the default vaults file");
|
log::info!("Using the default vaults file");
|
||||||
crate::utils::vaults_file()?
|
utils::vaults_file()?
|
||||||
};
|
};
|
||||||
log::debug!("Vaults file: {}", vaults_file.display());
|
log::debug!("Vaults file: {}", vaults_file.display());
|
||||||
|
|
||||||
|
|
|
@ -22,11 +22,13 @@ 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 index
|
/// Force remove, will not return error if there is no password with this
|
||||||
|
/// index
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
force: bool,
|
force: bool,
|
||||||
}
|
}
|
||||||
|
@ -37,14 +39,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 {
|
||||||
return Err(LprsError::Other(
|
|
||||||
"The index is greater than the passwords counts".to_owned(),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
log::error!(
|
log::error!(
|
||||||
"The index is greater than the passwords counts, but the force flag is enabled"
|
"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 {
|
} else {
|
||||||
vault_manager.vaults.remove(index);
|
vault_manager.vaults.remove(index);
|
||||||
|
|
|
@ -14,15 +14,20 @@
|
||||||
// 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::{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)]
|
#[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("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,
|
Decryption,
|
||||||
#[error("Wrong Master Password Error: Wrong decryption password")]
|
#[error("Wrong Master Password Error: Wrong decryption password")]
|
||||||
WrongMasterPassword,
|
WrongMasterPassword,
|
||||||
|
@ -50,12 +55,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] std::io::Error),
|
Io(#[from] io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
/// Return the exit code of the 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
|
// TODO: Exit with more specific exit code
|
||||||
ExitCode::FAILURE
|
ExitCode::FAILURE
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,10 +29,7 @@
|
||||||
/// #### Output
|
/// #### Output
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// impl crate::LprsCommand for TestCommands {
|
/// impl crate::LprsCommand for TestCommands {
|
||||||
/// fn run(
|
/// fn run(&self, vault_manager: crate::vault::Vaults) -> crate::LprsResult<()> {
|
||||||
/// &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),
|
||||||
|
@ -46,7 +43,6 @@
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
|
|
||||||
/// ```
|
/// ```
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! impl_commands {
|
macro_rules! impl_commands {
|
||||||
|
|
14
src/main.rs
14
src/main.rs
|
@ -13,14 +13,21 @@
|
||||||
//
|
//
|
||||||
// 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;
|
||||||
|
@ -30,17 +37,20 @@ 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 {
|
||||||
std::env::set_var("RUST_LOG", "lprs");
|
env::set_var("RUST_LOG", "lprs");
|
||||||
}
|
}
|
||||||
pretty_env_logger::init();
|
pretty_env_logger::init();
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
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<()>;
|
||||||
|
|
51
src/utils.rs
51
src/utils.rs
|
@ -16,12 +16,19 @@
|
||||||
|
|
||||||
use std::{fs, path::PathBuf};
|
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 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())
|
||||||
|
@ -37,6 +44,10 @@ 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() {
|
||||||
|
@ -50,22 +61,26 @@ 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 = passwords::analyzer::analyze(password);
|
let analyzed = analyzer::analyze(password);
|
||||||
if analyzed.length() < 15 {
|
Ok(if analyzed.length() < 15 {
|
||||||
return Ok(Validation::Invalid(
|
Validation::Invalid("The master password length must be beggier then 15".into())
|
||||||
"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 if passwords::scorer::score(&analyzed) < 80.0 {
|
} else {
|
||||||
return Ok(Validation::Invalid(
|
Validation::Valid
|
||||||
"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 {
|
||||||
|
@ -79,13 +94,17 @@ 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(inquire::PasswordDisplayMode::Masked)
|
.with_display_mode(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;
|
||||||
|
@ -108,7 +127,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)) = reqwest::blocking::Client::new()
|
if let Ok(Ok(response)) = BlockingClient::new()
|
||||||
.get("https://crates.io/api/v1/crates/lprs")
|
.get("https://crates.io/api/v1/crates/lprs")
|
||||||
.header(
|
.header(
|
||||||
"User-Agent",
|
"User-Agent",
|
||||||
|
@ -117,8 +136,8 @@ pub fn lprs_version() -> LprsResult<Option<String>> {
|
||||||
.send()
|
.send()
|
||||||
.map(|r| r.text())
|
.map(|r| r.text())
|
||||||
{
|
{
|
||||||
let re =
|
let re = regex::Regex::new(r#""max_stable_version":"(?<version>\d+\.\d+\.\d+)""#)
|
||||||
regex::Regex::new(r#""max_stable_version":"(?<version>\d+\.\d+\.\d+)""#).unwrap();
|
.expect("The regex is correct");
|
||||||
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()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::{Vault, Vaults};
|
use super::{Vault, Vaults};
|
||||||
|
@ -6,27 +26,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>,
|
||||||
}
|
}
|
||||||
|
@ -35,8 +55,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 {
|
||||||
|
@ -58,12 +78,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 }]),
|
||||||
}),
|
}),
|
||||||
|
@ -76,8 +96,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)
|
||||||
|
|
|
@ -14,9 +14,10 @@
|
||||||
// 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};
|
||||||
|
|
||||||
|
|
|
@ -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::{fs, path::PathBuf};
|
use std::{fmt, fs, path::PathBuf};
|
||||||
|
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use clap::{Parser, ValueEnum};
|
use clap::{Parser, ValueEnum};
|
||||||
|
@ -22,6 +22,7 @@ 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;
|
||||||
|
@ -29,8 +30,14 @@ 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +46,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>,
|
||||||
|
@ -48,10 +55,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
|
||||||
|
@ -60,9 +67,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 {
|
||||||
|
@ -75,17 +82,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 std::fmt::Write;
|
use 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");
|
||||||
|
@ -119,6 +126,10 @@ 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| {
|
||||||
|
@ -147,6 +158,10 @@ 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| {
|
||||||
|
@ -172,6 +187,9 @@ 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: {}",
|
||||||
|
@ -185,6 +203,11 @@ 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)?;
|
||||||
|
|
||||||
|
@ -198,8 +221,8 @@ impl Vaults {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Format {
|
impl fmt::Display for Format {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"{}",
|
"{}",
|
||||||
|
@ -210,8 +233,8 @@ impl std::fmt::Display for Format {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Vault {
|
impl fmt::Display for Vault {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> 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}")?;
|
||||||
|
|
Loading…
Reference in a new issue