feat: Support TOTP #45
14 changed files with 241 additions and 37 deletions
47
Cargo.lock
generated
47
Cargo.lock
generated
|
@ -106,6 +106,12 @@ dependencies = [
|
||||||
"rustc-demangle",
|
"rustc-demangle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base32"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.21.7"
|
version = "0.21.7"
|
||||||
|
@ -329,6 +335,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer",
|
"block-buffer",
|
||||||
"crypto-common",
|
"crypto-common",
|
||||||
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -531,6 +538,15 @@ version = "0.3.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hmac"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.2.12"
|
version = "0.2.12"
|
||||||
|
@ -725,6 +741,7 @@ name = "lprs"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
|
"base32",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bincode",
|
"bincode",
|
||||||
"cbc",
|
"cbc",
|
||||||
|
@ -742,6 +759,7 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"totp-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1165,6 +1183,17 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha1"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.8"
|
version = "0.10.8"
|
||||||
|
@ -1243,6 +1272,12 @@ version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "subtle"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.60"
|
version = "2.0.60"
|
||||||
|
@ -1374,6 +1409,18 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "totp-lite"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8e43134db17199f7f721803383ac5854edd0d3d523cc34dba321d6acfbe76c3"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
"hmac",
|
||||||
|
"sha1",
|
||||||
|
"sha2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower-service"
|
name = "tower-service"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
|
|
|
@ -30,6 +30,8 @@ sha2 = "0.10.8"
|
||||||
serde_json = "1.0.116"
|
serde_json = "1.0.116"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
clap_complete = "4.5.2"
|
clap_complete = "4.5.2"
|
||||||
|
totp-lite = "2.0.1"
|
||||||
|
base32 = "0.4.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["update-notify"]
|
default = ["update-notify"]
|
||||||
|
|
|
@ -31,10 +31,14 @@ use crate::{
|
||||||
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 you will prompt it
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
#[allow(clippy::option_option)]
|
#[allow(clippy::option_option)]
|
||||||
password: Option<Option<String>>,
|
password: Option<Option<String>>,
|
||||||
|
/// The TOTP secret, if there is no value you will prompt it
|
||||||
|
#[arg(short, long)]
|
||||||
|
#[allow(clippy::option_option)]
|
||||||
|
totp_secret: Option<Option<String>>,
|
||||||
/// Add a custom field to the vault
|
/// Add a custom field to the vault
|
||||||
#[arg(name = "KEY=VALUE", short = 'c', long = "custom")]
|
#[arg(name = "KEY=VALUE", short = 'c', long = "custom")]
|
||||||
#[arg(value_parser = clap_parsers::kv_parser)]
|
#[arg(value_parser = clap_parsers::kv_parser)]
|
||||||
|
@ -51,7 +55,8 @@ impl LprsCommand for Add {
|
||||||
fn run(mut self, mut vault_manager: Vaults) -> LprsResult<()> {
|
fn run(mut self, mut vault_manager: Vaults) -> LprsResult<()> {
|
||||||
if !self.vault_info.is_empty() {
|
if !self.vault_info.is_empty() {
|
||||||
self.vault_info.name = self.vault_info.name.trim().to_string();
|
self.vault_info.name = self.vault_info.name.trim().to_string();
|
||||||
self.vault_info.password = utils::user_password(self.password, "Vault password:")?;
|
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.custom_fields = self.custom_fields.into_iter().collect();
|
self.vault_info.custom_fields = self.custom_fields.into_iter().collect();
|
||||||
vault_manager.add_vault(self.vault_info);
|
vault_manager.add_vault(self.vault_info);
|
||||||
vault_manager.try_export()?;
|
vault_manager.try_export()?;
|
||||||
|
@ -71,6 +76,13 @@ impl LprsCommand for Add {
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if self
|
||||||
|
.custom_fields
|
||||||
|
.iter()
|
||||||
|
.any(|(k, _)| k.starts_with(crate::RESERVED_FIELD_PREFIX))
|
||||||
|
{
|
||||||
|
return Err(LprsError::ReservedPrefix(crate::RESERVED_FIELD_PREFIX));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,31 +28,35 @@ pub struct Edit {
|
||||||
|
|
||||||
#[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
|
||||||
#[allow(clippy::option_option)]
|
#[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>,
|
||||||
|
/// The TOTP secret, if there is no value you will prompt it
|
||||||
|
#[arg(short, long)]
|
||||||
|
#[allow(clippy::option_option)]
|
||||||
|
totp_secret: Option<Option<String>>,
|
||||||
/// The custom field, make its value empty to delete it
|
/// The custom field, make its value empty to delete it
|
||||||
///
|
///
|
||||||
/// If the custom field not exist will created it, if it's will update it
|
/// If the custom field not exist will created it, if it's will update it
|
||||||
#[arg(name = "KEY=VALUE", short = 'c', long = "custom")]
|
#[arg(name = "KEY=VALUE", short = 'c', long = "custom")]
|
||||||
#[arg(value_parser = clap_parsers::kv_parser)]
|
#[arg(value_parser = clap_parsers::kv_parser)]
|
||||||
pub custom_fields: Vec<(String, String)>,
|
custom_fields: Vec<(String, String)>,
|
||||||
/// Force edit, will not return error if there is a problem with the args.
|
/// Force edit, will not return error if there is a problem with the args.
|
||||||
///
|
///
|
||||||
/// For example, duplication in the custom fields and try to editing nothing
|
/// For example, duplication in the custom fields and try to editing nothing
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
force: bool,
|
force: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LprsCommand for Edit {
|
impl LprsCommand for Edit {
|
||||||
|
@ -73,7 +77,10 @@ impl LprsCommand for Edit {
|
||||||
vault.name = new_name;
|
vault.name = new_name;
|
||||||
}
|
}
|
||||||
if self.password.is_some() {
|
if self.password.is_some() {
|
||||||
vault.password = utils::user_password(self.password, "New vault password:")?;
|
vault.password = utils::user_secret(self.password, "New vault password:")?;
|
||||||
|
}
|
||||||
|
if self.totp_secret.is_some() {
|
||||||
|
vault.totp_secret = utils::user_secret(self.totp_secret, "TOTP Secret:")?;
|
||||||
}
|
}
|
||||||
if let Some(new_username) = self.username {
|
if let Some(new_username) = self.username {
|
||||||
vault.username = Some(new_username);
|
vault.username = Some(new_username);
|
||||||
|
@ -109,6 +116,13 @@ impl LprsCommand for Edit {
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if self
|
||||||
|
.custom_fields
|
||||||
|
.iter()
|
||||||
|
.any(|(k, _)| k.starts_with(crate::RESERVED_FIELD_PREFIX))
|
||||||
|
{
|
||||||
|
return Err(LprsError::ReservedPrefix(crate::RESERVED_FIELD_PREFIX));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,7 @@ impl LprsCommand for Export {
|
||||||
);
|
);
|
||||||
|
|
||||||
let encryption_key: Option<[u8; 32]> =
|
let encryption_key: Option<[u8; 32]> =
|
||||||
utils::user_password(self.encryption_password, "Encryption Password:")?
|
utils::user_secret(self.encryption_password, "Encryption Password:")?
|
||||||
.map(|p| sha2::Sha256::digest(p).into());
|
.map(|p| sha2::Sha256::digest(p).into());
|
||||||
|
|
||||||
let exported_data = match self.format {
|
let exported_data = match self.format {
|
||||||
|
|
|
@ -20,7 +20,7 @@ use clap::Args;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
utils,
|
utils,
|
||||||
vault::{Vault, Vaults},
|
vault::{cipher, Vault, Vaults},
|
||||||
LprsCommand,
|
LprsCommand,
|
||||||
LprsError,
|
LprsError,
|
||||||
LprsResult,
|
LprsResult,
|
||||||
|
@ -34,6 +34,8 @@ enum VaultGetField {
|
||||||
Password,
|
Password,
|
||||||
Service,
|
Service,
|
||||||
Note,
|
Note,
|
||||||
|
TotpSecret,
|
||||||
|
TotpCode,
|
||||||
Custom(String),
|
Custom(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,6 +50,8 @@ impl FromStr for VaultGetField {
|
||||||
"password" => Self::Password,
|
"password" => Self::Password,
|
||||||
"service" => Self::Service,
|
"service" => Self::Service,
|
||||||
"note" => Self::Note,
|
"note" => Self::Note,
|
||||||
|
"totp_secret" => Self::TotpSecret,
|
||||||
|
"totp_code" => Self::TotpCode,
|
||||||
_ => Self::Custom(input.to_owned()),
|
_ => Self::Custom(input.to_owned()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -63,6 +67,8 @@ impl VaultGetField {
|
||||||
Self::Password => vault.password.as_deref(),
|
Self::Password => vault.password.as_deref(),
|
||||||
Self::Service => vault.service.as_deref(),
|
Self::Service => vault.service.as_deref(),
|
||||||
Self::Note => vault.note.as_deref(),
|
Self::Note => vault.note.as_deref(),
|
||||||
|
Self::TotpSecret => vault.totp_secret.as_deref(),
|
||||||
|
Self::TotpCode => None,
|
||||||
Self::Custom(custom_field) => vault.custom_fields.get(custom_field).map(|x| x.as_str()),
|
Self::Custom(custom_field) => vault.custom_fields.get(custom_field).map(|x| x.as_str()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,6 +82,8 @@ impl VaultGetField {
|
||||||
Self::Password => "password",
|
Self::Password => "password",
|
||||||
Self::Service => "service",
|
Self::Service => "service",
|
||||||
Self::Note => "note",
|
Self::Note => "note",
|
||||||
|
Self::TotpSecret => "totp_secret",
|
||||||
|
Self::TotpCode => "totp_code",
|
||||||
Self::Custom(field) => field.as_str(),
|
Self::Custom(field) => field.as_str(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -90,8 +98,10 @@ pub struct Get {
|
||||||
location: String,
|
location: String,
|
||||||
/// A Specific field to get.
|
/// A Specific field to get.
|
||||||
///
|
///
|
||||||
/// Can be [name,username,password,service,note,"string"] where the string
|
/// Can be [name, username, password, service, note, totp_secret, totp_code,
|
||||||
/// means a custom field
|
/// "string"]
|
||||||
|
///
|
||||||
|
/// where the string means a custom field
|
||||||
#[arg(value_parser = VaultGetField::from_str)]
|
#[arg(value_parser = VaultGetField::from_str)]
|
||||||
field: Option<VaultGetField>,
|
field: Option<VaultGetField>,
|
||||||
}
|
}
|
||||||
|
@ -106,13 +116,25 @@ impl LprsCommand for Get {
|
||||||
print!("{index}");
|
print!("{index}");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
if field == VaultGetField::TotpCode {
|
||||||
|
if let Some(ref totp_secret) = vault.totp_secret {
|
||||||
|
let totp_code = cipher::totp_now(totp_secret, &vault.totp_hash)?.0;
|
||||||
|
print!("{totp_code}");
|
||||||
|
return Ok(());
|
||||||
|
} else {
|
||||||
|
return Err(LprsError::Other(
|
||||||
|
"There is no TOTP secret to get TOTP code".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(value) = field.get_from_vault(vault) {
|
if let Some(value) = field.get_from_vault(vault) {
|
||||||
print!("{value}")
|
print!("{value}")
|
||||||
} else {
|
} else {
|
||||||
return Err(LprsError::Other(format!(
|
return Err(LprsError::Other(format!(
|
||||||
"There is no value for `{}`",
|
"There is no value for `{}` at \"{}\" vault",
|
||||||
field.as_str()
|
field.as_str(),
|
||||||
|
vault.name
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -60,7 +60,7 @@ impl LprsCommand for Import {
|
||||||
);
|
);
|
||||||
|
|
||||||
let decryption_key: Option<[u8; 32]> =
|
let decryption_key: Option<[u8; 32]> =
|
||||||
utils::user_password(self.decryption_password, "Decryption password:")?
|
utils::user_secret(self.decryption_password, "Decryption password:")?
|
||||||
.map(|p| sha2::Sha256::digest(p).into());
|
.map(|p| sha2::Sha256::digest(p).into());
|
||||||
|
|
||||||
let imported_passwords_len = match self.format {
|
let imported_passwords_len = match self.format {
|
||||||
|
@ -71,6 +71,14 @@ impl LprsCommand for Import {
|
||||||
.unwrap_or(&vault_manager.master_password),
|
.unwrap_or(&vault_manager.master_password),
|
||||||
&fs::read(self.path)?,
|
&fs::read(self.path)?,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
if vaults.iter().any(|v| {
|
||||||
|
v.custom_fields
|
||||||
|
.iter()
|
||||||
|
.any(|(k, _)| k.starts_with(crate::RESERVED_FIELD_PREFIX))
|
||||||
|
}) {
|
||||||
|
return Err(LprsError::ReservedPrefix(crate::RESERVED_FIELD_PREFIX));
|
||||||
|
}
|
||||||
let vaults_len = vaults.len();
|
let vaults_len = vaults.len();
|
||||||
|
|
||||||
vault_manager.vaults.extend(vaults);
|
vault_manager.vaults.extend(vaults);
|
||||||
|
|
|
@ -17,7 +17,13 @@
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
use inquire::{InquireError, Select};
|
use inquire::{InquireError, Select};
|
||||||
|
|
||||||
use crate::{vault::Vaults, LprsCommand, LprsError, LprsResult};
|
use crate::{
|
||||||
|
vault::{cipher, Vaults},
|
||||||
|
LprsCommand,
|
||||||
|
LprsError,
|
||||||
|
LprsResult,
|
||||||
|
RESERVED_FIELD_PREFIX,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
#[derive(Debug, Args)]
|
||||||
#[command(author, version, about, long_about = None)]
|
#[command(author, version, about, long_about = None)]
|
||||||
|
@ -35,7 +41,7 @@ pub struct List {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LprsCommand for List {
|
impl LprsCommand for List {
|
||||||
fn run(self, vault_manager: Vaults) -> LprsResult<()> {
|
fn run(self, mut vault_manager: Vaults) -> LprsResult<()> {
|
||||||
if vault_manager.vaults.is_empty() {
|
if vault_manager.vaults.is_empty() {
|
||||||
return Err(LprsError::Other(
|
return Err(LprsError::Other(
|
||||||
"Looks like there is no vaults to list".to_owned(),
|
"Looks like there is no vaults to list".to_owned(),
|
||||||
|
@ -95,13 +101,20 @@ impl LprsCommand for List {
|
||||||
|
|
||||||
log::debug!("The user selected the vault at index: {vault_idx}");
|
log::debug!("The user selected the vault at index: {vault_idx}");
|
||||||
|
|
||||||
println!(
|
let vault = vault_manager
|
||||||
"{}",
|
.vaults
|
||||||
vault_manager
|
.get_mut(vault_idx - 1)
|
||||||
.vaults
|
.expect("The index is correct");
|
||||||
.get(vault_idx - 1)
|
|
||||||
.expect("The index is correct")
|
if let Some(ref totp_secret) = vault.totp_secret {
|
||||||
);
|
let (code, remaining) = cipher::totp_now(totp_secret, &vault.totp_hash)?;
|
||||||
|
vault.custom_fields.insert(
|
||||||
|
format!("{RESERVED_FIELD_PREFIX}TOTP Code"),
|
||||||
|
format!("{code} ({remaining}s remaining)"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{vault}",);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -39,6 +39,13 @@ pub enum Error {
|
||||||
InvalidVaultIndex(String),
|
InvalidVaultIndex(String),
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
ArgParse(String),
|
ArgParse(String),
|
||||||
|
#[error(
|
||||||
|
"Reserved Prefix Error: Sorry, but the following prefix is reserved and cannot be used in \
|
||||||
|
custom fields {0}"
|
||||||
|
)]
|
||||||
|
ReservedPrefix(&'static str),
|
||||||
|
#[error("Base32 Error: {0}")]
|
||||||
|
Base32(String),
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
Other(String),
|
Other(String),
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,8 @@ 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.
|
/// 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";
|
||||||
|
/// The prefix of the reserved custom fields
|
||||||
|
const RESERVED_FIELD_PREFIX: &str = ".lprsfield.";
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
let lprs_cli = cli::Cli::parse();
|
let lprs_cli = cli::Cli::parse();
|
||||||
|
|
18
src/utils.rs
18
src/utils.rs
|
@ -45,24 +45,24 @@ pub fn local_project_file(filename: &str) -> LprsResult<PathBuf> {
|
||||||
Ok(local_dir.join(filename))
|
Ok(local_dir.join(filename))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the user password if any
|
/// Returns the user secret if any
|
||||||
///
|
///
|
||||||
/// - If the `password` is `None` will return `None`
|
/// - If the `secret` is `None` will return `None`
|
||||||
/// - If the `password` is `Some(None)` will ask the user for a password in the
|
/// - If the `secret` is `Some(None)` will ask the user for a secret in the
|
||||||
/// stdin and return it
|
/// stdin and return it
|
||||||
/// - If the `password` is `Some(Some(password))` will return `Some(password)`
|
/// - If the `secret` is `Some(Some(secret))` will return `Some(secret)`
|
||||||
///
|
///
|
||||||
/// ## Errors
|
/// ## Errors
|
||||||
/// - When failed to get the password from stdin
|
/// - When failed to get the secret from stdin
|
||||||
pub fn user_password(
|
pub fn user_secret(
|
||||||
password: Option<Option<String>>,
|
secret: Option<Option<String>>,
|
||||||
prompt_message: &str,
|
prompt_message: &str,
|
||||||
) -> LprsResult<Option<String>> {
|
) -> LprsResult<Option<String>> {
|
||||||
Ok(match password {
|
Ok(match secret {
|
||||||
None => None,
|
None => None,
|
||||||
Some(Some(p)) => Some(p),
|
Some(Some(p)) => Some(p),
|
||||||
Some(None) => {
|
Some(None) => {
|
||||||
log::debug!("User didn't provide a password, prompting it");
|
log::debug!("User didn't provide a secret, prompting it");
|
||||||
Some(
|
Some(
|
||||||
Password::new(prompt_message)
|
Password::new(prompt_message)
|
||||||
.without_confirmation()
|
.without_confirmation()
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::{Vault, Vaults};
|
use super::{cipher::TotpHash, Vault, Vaults};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
pub struct BitWardenLoginData {
|
pub struct BitWardenLoginData {
|
||||||
|
@ -84,6 +84,8 @@ impl From<BitWardenPassword> for Vault {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|nv| (nv.name, nv.value))
|
.map(|nv| (nv.name, nv.value))
|
||||||
.collect(),
|
.collect(),
|
||||||
|
None::<String>,
|
||||||
|
TotpHash::default(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,13 +17,63 @@
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
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 base32::Alphabet as Base32Alphabet;
|
||||||
|
use clap::ValueEnum;
|
||||||
use rand::{rngs::StdRng, Rng, SeedableRng};
|
use rand::{rngs::StdRng, Rng, SeedableRng};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{LprsError, LprsResult};
|
use crate::{LprsError, LprsResult};
|
||||||
|
|
||||||
type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
|
type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
|
||||||
type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
|
type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
|
||||||
|
|
||||||
|
#[derive(Default, Clone, Debug, ValueEnum, Eq, PartialEq, Deserialize, Serialize)]
|
||||||
|
/// The TOTP hash functions
|
||||||
|
pub enum TotpHash {
|
||||||
|
/// Sha1 hash function
|
||||||
|
#[default]
|
||||||
|
Sha1,
|
||||||
|
/// Sha256 hash function
|
||||||
|
Sha256,
|
||||||
|
/// Sha512 hash function
|
||||||
|
Sha512,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Create the TOTP code of the current time
|
||||||
|
///
|
||||||
|
/// ## Errors
|
||||||
|
/// - If the given `secret_base32` are vaild base32
|
||||||
|
pub fn totp_now(secret_base32: &str, hash_function: &TotpHash) -> LprsResult<(String, u8)> {
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.expect("SystemTime before UNIX EPOCH!")
|
||||||
|
.as_secs();
|
||||||
|
let remaining = 30 - (now % 30) as u8;
|
||||||
|
let secret = base32::decode(Base32Alphabet::RFC4648 { padding: true }, secret_base32)
|
||||||
|
.ok_or_else(|| LprsError::Base32("Can't decode the TOTP secret".to_owned()))?;
|
||||||
|
Ok(match hash_function {
|
||||||
|
TotpHash::Sha1 => {
|
||||||
|
(
|
||||||
|
totp_lite::totp_custom::<totp_lite::Sha1>(30, 6, &secret, now),
|
||||||
|
remaining,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TotpHash::Sha256 => {
|
||||||
|
(
|
||||||
|
totp_lite::totp_custom::<totp_lite::Sha256>(30, 6, &secret, now),
|
||||||
|
remaining,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TotpHash::Sha512 => {
|
||||||
|
(
|
||||||
|
totp_lite::totp_custom::<totp_lite::Sha512>(30, 6, &secret, now),
|
||||||
|
remaining,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Encrypt the given data by the given key using AES-256 CBC
|
/// Encrypt the given data by the given key using AES-256 CBC
|
||||||
///
|
///
|
||||||
/// Note: The IV will be add it to the end of the ciphertext (Last 16 bytes)
|
/// Note: The IV will be add it to the end of the ciphertext (Last 16 bytes)
|
||||||
|
|
|
@ -29,6 +29,8 @@ mod bitwarden;
|
||||||
|
|
||||||
pub use bitwarden::*;
|
pub use bitwarden::*;
|
||||||
|
|
||||||
|
use self::cipher::TotpHash;
|
||||||
|
|
||||||
#[derive(Clone, Debug, ValueEnum, Eq, PartialEq)]
|
#[derive(Clone, Debug, ValueEnum, Eq, PartialEq)]
|
||||||
/// The vaults format
|
/// The vaults format
|
||||||
pub enum Format {
|
pub enum Format {
|
||||||
|
@ -61,6 +63,12 @@ pub struct Vault {
|
||||||
/// The vault custom fields
|
/// The vault custom fields
|
||||||
#[arg(skip)]
|
#[arg(skip)]
|
||||||
pub custom_fields: BTreeMap<String, String>,
|
pub custom_fields: BTreeMap<String, String>,
|
||||||
|
/// The TOTP secret
|
||||||
|
#[arg(skip)]
|
||||||
|
pub totp_secret: Option<String>,
|
||||||
|
/// The TOTP hash function
|
||||||
|
#[arg(long, value_name = "HASH_FUNCTION", value_enum, default_value_t)]
|
||||||
|
pub totp_hash: TotpHash,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The vaults manager
|
/// The vaults manager
|
||||||
|
@ -76,6 +84,7 @@ pub struct Vaults {
|
||||||
|
|
||||||
impl Vault {
|
impl Vault {
|
||||||
/// Create new [`Vault`] instance
|
/// Create new [`Vault`] instance
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
name: impl Into<String>,
|
name: impl Into<String>,
|
||||||
username: Option<impl Into<String>>,
|
username: Option<impl Into<String>>,
|
||||||
|
@ -83,6 +92,8 @@ impl Vault {
|
||||||
service: Option<impl Into<String>>,
|
service: Option<impl Into<String>>,
|
||||||
note: Option<impl Into<String>>,
|
note: Option<impl Into<String>>,
|
||||||
custom_fields: BTreeMap<String, String>,
|
custom_fields: BTreeMap<String, String>,
|
||||||
|
totp_secret: Option<impl Into<String>>,
|
||||||
|
totp_hash: TotpHash,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: name.into(),
|
name: name.into(),
|
||||||
|
@ -91,6 +102,8 @@ impl Vault {
|
||||||
service: service.map(Into::into),
|
service: service.map(Into::into),
|
||||||
note: note.map(Into::into),
|
note: note.map(Into::into),
|
||||||
custom_fields,
|
custom_fields,
|
||||||
|
totp_secret: totp_secret.map(Into::into),
|
||||||
|
totp_hash,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,6 +176,8 @@ impl Vaults {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(key, value)| (encrypt(key), encrypt(value)))
|
.map(|(key, value)| (encrypt(key), encrypt(value)))
|
||||||
.collect(),
|
.collect(),
|
||||||
|
v.totp_secret.as_ref().map(|t| encrypt(t)),
|
||||||
|
v.totp_hash.clone(),
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.collect::<LprsResult<Vec<_>>>()?,
|
.collect::<LprsResult<Vec<_>>>()?,
|
||||||
|
@ -199,6 +214,8 @@ impl Vaults {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(key, value)| LprsResult::Ok((decrypt(&key)?, decrypt(&value)?)))
|
.map(|(key, value)| LprsResult::Ok((decrypt(&key)?, decrypt(&value)?)))
|
||||||
.collect::<LprsResult<_>>()?,
|
.collect::<LprsResult<_>>()?,
|
||||||
|
v.totp_secret.as_ref().and_then(|t| decrypt(t).ok()),
|
||||||
|
v.totp_hash,
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
@ -266,8 +283,16 @@ impl fmt::Display for Vault {
|
||||||
if let Some(ref note) = self.note {
|
if let Some(ref note) = self.note {
|
||||||
write!(f, "\nNote:\n{note}")?;
|
write!(f, "\nNote:\n{note}")?;
|
||||||
}
|
}
|
||||||
|
if let Some(ref totp_secret) = self.totp_secret {
|
||||||
|
write!(f, "\nTOTP Secret: {totp_secret}")?;
|
||||||
|
}
|
||||||
for (key, value) in &self.custom_fields {
|
for (key, value) in &self.custom_fields {
|
||||||
write!(f, "\n{key}: {value}")?;
|
write!(
|
||||||
|
f,
|
||||||
|
"\n{}: {value}",
|
||||||
|
key.strip_prefix(crate::RESERVED_FIELD_PREFIX)
|
||||||
|
.unwrap_or(key)
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
Loading…
Reference in a new issue