From ced363a37f6f64282ca1a1fb022aa3d030edff79 Mon Sep 17 00:00:00 2001 From: Awiteb Date: Thu, 16 May 2024 22:41:20 +0200 Subject: [PATCH] feat: Support changing master password (#50) Reviewed-on: https://git.4rs.nl/awiteb/lprs/pulls/50 Co-authored-by: Awiteb Co-committed-by: Awiteb --- src/cli/add_command.rs | 5 +-- src/cli/change_master_password_command.rs | 39 +++++++++++++++++++++++ src/cli/edit_command.rs | 4 +-- src/cli/export_command.rs | 2 +- src/cli/import_command.rs | 2 +- src/cli/mod.rs | 6 +++- src/utils.rs | 13 +++++--- 7 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 src/cli/change_master_password_command.rs diff --git a/src/cli/add_command.rs b/src/cli/add_command.rs index 8c3fa8d..07cd536 100644 --- a/src/cli/add_command.rs +++ b/src/cli/add_command.rs @@ -55,8 +55,9 @@ impl LprsCommand for Add { fn run(mut self, mut vault_manager: Vaults) -> LprsResult<()> { if !self.vault_info.is_empty() { self.vault_info.name = self.vault_info.name.trim().to_string(); - self.vault_info.password = utils::user_secret(self.password, "Vault password:")?; - self.vault_info.totp_secret = utils::user_secret(self.totp_secret, "TOTP Secret:")?; + self.vault_info.password = utils::user_secret(self.password, "Vault password:", false)?; + self.vault_info.totp_secret = + utils::user_secret(self.totp_secret, "TOTP Secret:", false)?; self.vault_info.custom_fields = self.custom_fields.into_iter().collect(); vault_manager.add_vault(self.vault_info); vault_manager.try_export()?; diff --git a/src/cli/change_master_password_command.rs b/src/cli/change_master_password_command.rs new file mode 100644 index 0000000..8fd249d --- /dev/null +++ b/src/cli/change_master_password_command.rs @@ -0,0 +1,39 @@ +// Lprs - A local CLI vaults manager. For human and machine use +// Copyright (C) 2024 Awiteb +// +// 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 . + +use clap::Args; +use sha2::{Digest, Sha256}; + +use crate::{utils, vault::Vaults, LprsCommand, LprsResult}; + +#[derive(Debug, Args)] +/// Change master password, reencrypt the vaults with new password +pub struct ChangeMasterPassword { + /// The new master password, if there is no value for it you will prompt it + #[allow(clippy::option_option)] + new_password: Option, +} + +impl LprsCommand for ChangeMasterPassword { + fn run(self, mut vault_manager: Vaults) -> LprsResult<()> { + vault_manager.master_password = + utils::user_secret(Some(self.new_password), "New master password:", true)? + .map(|s| Sha256::digest(s).into()) + .expect("We wrap it in `Some`, so is will return a secret"); + vault_manager.try_export()?; + Ok(()) + } +} diff --git a/src/cli/edit_command.rs b/src/cli/edit_command.rs index 81e3c88..8cf4a86 100644 --- a/src/cli/edit_command.rs +++ b/src/cli/edit_command.rs @@ -77,10 +77,10 @@ impl LprsCommand for Edit { vault.name = new_name; } if self.password.is_some() { - vault.password = utils::user_secret(self.password, "New vault password:")?; + vault.password = utils::user_secret(self.password, "New vault password:", false)?; } if self.totp_secret.is_some() { - vault.totp_secret = utils::user_secret(self.totp_secret, "TOTP Secret:")?; + vault.totp_secret = utils::user_secret(self.totp_secret, "TOTP Secret:", false)?; } if let Some(new_username) = self.username { vault.username = Some(new_username); diff --git a/src/cli/export_command.rs b/src/cli/export_command.rs index dcebf62..65fbf8f 100644 --- a/src/cli/export_command.rs +++ b/src/cli/export_command.rs @@ -55,7 +55,7 @@ impl LprsCommand for Export { ); let encryption_key: Option<[u8; 32]> = - utils::user_secret(self.encryption_password, "Encryption Password:")? + utils::user_secret(self.encryption_password, "Encryption Password:", false)? .map(|p| sha2::Sha256::digest(p).into()); let exported_data = match self.format { diff --git a/src/cli/import_command.rs b/src/cli/import_command.rs index 4c11e8f..cce9bda 100644 --- a/src/cli/import_command.rs +++ b/src/cli/import_command.rs @@ -60,7 +60,7 @@ impl LprsCommand for Import { ); let decryption_key: Option<[u8; 32]> = - utils::user_secret(self.decryption_password, "Decryption password:")? + utils::user_secret(self.decryption_password, "Decryption password:", false)? .map(|p| sha2::Sha256::digest(p).into()); let imported_passwords_len = match self.format { diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 3cfb68f..0eee915 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -23,6 +23,8 @@ use crate::{impl_commands, utils, vault::Vaults, LprsCommand, LprsResult}; /// Add command, used to add new vault to the vaults file pub mod add_command; +/// Change master password, reencrypt the vaults with new password +pub mod change_master_password_command; /// Clean command, used to clean the vaults file (remove all vaults) pub mod clean_command; /// Generate shell completion @@ -67,11 +69,13 @@ pub enum Commands { Export(export_command::Export), /// Import vaults Import(import_command::Import), + /// Change master password, reencrypt the vaults with new password + ChangeMasterPassword(change_master_password_command::ChangeMasterPassword), /// Generate shell completion Completion(completion_command::Completion), } -impl_commands!(Commands, Add Remove List Clean Edit Gen Get Export Import Completion); +impl_commands!(Commands, Add Remove List Clean Edit Gen Get Export Import ChangeMasterPassword Completion); #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] diff --git a/src/utils.rs b/src/utils.rs index 5577a04..61b49ee 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -57,6 +57,7 @@ pub fn local_project_file(filename: &str) -> LprsResult { pub fn user_secret( secret: Option>, prompt_message: &str, + confirmation: bool, ) -> LprsResult> { Ok(match secret { None => None, @@ -64,11 +65,13 @@ pub fn user_secret( Some(None) => { log::debug!("User didn't provide a secret, prompting it"); Some( - Password::new(prompt_message) - .without_confirmation() - .with_formatter(&|p| "*".repeat(p.chars().count())) - .with_display_mode(PasswordDisplayMode::Masked) - .prompt()?, + Password { + enable_confirmation: confirmation, + ..Password::new(prompt_message) + .with_formatter(&|p| "*".repeat(p.chars().count())) + .with_display_mode(PasswordDisplayMode::Masked) + } + .prompt()?, ) } })