From 960090ee8a65e8fbe137cbb9d55355ea9814f6f1 Mon Sep 17 00:00:00 2001 From: Alberto Coscia Date: Tue, 17 Jul 2018 17:43:52 +0200 Subject: [PATCH] Allow to use `add` subcommands without interactive mode * All fields can be provided as argument * Interactive mode is enabled if no argument is provided * `OtpRecord` and `Record` now implement a `::new()` method * `Record` is now guaranteed to provide a value for the `password` file --- src/cli/args.rs | 113 +++++++++++++++++++++++++++++++++++++--- src/cli/otp/add.rs | 22 +++++++- src/cli/otp/mod.rs | 1 + src/cli/password/add.rs | 42 +++++++++++---- src/cli/password/get.rs | 13 ++--- src/cli/password/mod.rs | 1 + src/lib/error.rs | 4 -- src/lib/types.rs | 49 +++++++++++++++-- 8 files changed, 210 insertions(+), 35 deletions(-) diff --git a/src/cli/args.rs b/src/cli/args.rs index 88ba7be..fd87667 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -1,5 +1,6 @@ use cli; use failure::Error; +use lib::types::{HmacAlgorithm, OtpRecord, Record}; use lib::{error, utils}; use std::env; use std::path::PathBuf; @@ -52,8 +53,39 @@ pub enum Command { #[derive(Debug, StructOpt)] pub enum OtpCommand { #[structopt(name = "add")] - /// Add an OTP generator to a vault - Add, + /// Add an OTP secret to a vault. Interactive mode if no argument is provided + Add { + #[structopt( + requires = "secret", + long = "totp", + takes_value = false, + group = "algo", + conflicts_with = "hotp" + )] + /// Use TOTP as the generation algorithm + totp: bool, + #[structopt(requires = "secret", long = "hotp", takes_value = false, group = "algo")] + /// Use HOTP as the generation algorithm + hotp: bool, + /// A label for this secret + #[structopt(requires = "secret")] + record: Option, + #[structopt(requires = "algo")] + /// The secret + secret: Option, + #[structopt(requires = "secret", long = "issuer")] + /// The issuer of this secret + issuer: Option, + #[structopt(requires = "secret", long = "hmac", default_value = "SHA1")] + /// The HMAC algorithm to use to generate tokens + algorithm: HmacAlgorithm, + #[structopt(requires = "secret", long = "digits", default_value = "6")] + /// The token length + digits: u32, + #[structopt(requires = "secret", long = "period", default_value = "30")] + /// Token validity in seconds + period: u64, + }, #[structopt(name = "import")] /// Import an OTP generator to a vault using an `otpauth://` URI ImportUrl { @@ -81,8 +113,24 @@ pub enum OtpCommand { #[derive(Debug, StructOpt)] pub enum PasswordCommand { #[structopt(name = "add")] - /// Add a password to a vault - Add, + /// Add a password to a vault. Interactive mode if no argument is provided + Add { + #[structopt(requires = "password")] + /// A label for this password + record: Option, + #[structopt()] + /// The password + password: Option, + #[structopt(short = "u", long = "username", requires = "password")] + /// The username associated with this password + username: Option, + #[structopt(long = "email", requires = "password")] + /// The email associated with this password + email: Option, + #[structopt(long = "home", requires = "password")] + /// The homepage for this service + home: Option, + }, #[structopt(name = "rm")] /// Remove a record from a vault Remove { @@ -123,7 +171,26 @@ pub fn match_args(sigil: Sigil) -> Result<(), Error> { Command::Touch { force } => cli::touch::touch_vault(&vault?, &key?, force), Command::List { disclose } => cli::list::list_vault(&vault?, disclose), Command::Password { cmd } => match cmd { - PasswordCommand::Add => cli::password::add_record(&vault?, &key?, ctx?), + PasswordCommand::Add { + record, + password, + username, + email, + home, + } => { + if record.is_some() && password.is_some() { + // Safe unwraps because we checked them before and they are required args + cli::password::add_record( + &vault?, + &key?, + ctx?, + Record::new(password.unwrap(), username, email, home), + record.unwrap(), + ) + } else { + cli::password::add_record_interactive(&vault?, &key?, ctx?) + } + } PasswordCommand::Remove { record } => { cli::password::remove_record(&vault?, &key?, ctx?, record) } @@ -133,7 +200,41 @@ pub fn match_args(sigil: Sigil) -> Result<(), Error> { PasswordCommand::Generate { chars } => cli::password::generate_password(chars), }, Command::Otp { cmd } => match cmd { - OtpCommand::Add => cli::otp::add_record(&vault?, &key?, ctx?), + OtpCommand::Add { + totp, + hotp, + issuer, + record, + secret, + algorithm, + digits, + period, + } => { + if secret.is_some() && record.is_some() { + // Safe unwraps because we checked them before and they are required args + if totp { + cli::otp::add_record( + &vault?, + &key?, + ctx?, + OtpRecord::new_totp(secret.unwrap(), issuer, algorithm, digits, period), + record.unwrap(), + ) + } else if hotp { + cli::otp::add_record( + &vault?, + &key?, + ctx?, + OtpRecord::new_hotp(secret.unwrap(), issuer, algorithm, digits), + record.unwrap(), + ) + } else { + unreachable!() + } + } else { + cli::otp::add_record_interactive(&vault?, &key?, ctx?) + } + } OtpCommand::ImportUrl { url } => cli::otp::import_url(&vault?, &key?, ctx?, &url), OtpCommand::GetToken { record, counter } => { cli::otp::get_token(&vault?, ctx?, record, counter) diff --git a/src/cli/otp/add.rs b/src/cli/otp/add.rs index 1201508..196dacd 100644 --- a/src/cli/otp/add.rs +++ b/src/cli/otp/add.rs @@ -4,7 +4,21 @@ use lib::types::{HmacAlgorithm, OtpRecord}; use lib::{error, utils}; use std::path::PathBuf; -/// Adds an OTP record to the specified vault +pub fn add_record( + vault_path: &PathBuf, + key: &str, + mut ctx: Context, + record: OtpRecord, + record_id: String, +) -> Result<(), Error> { + let mut vault = utils::read_vault(&vault_path, &mut ctx).unwrap(); + vault.add_otp_record(record, record_id)?; + utils::write_vault(&vault_path, &vault, &mut ctx, &key).unwrap(); + + Ok(()) +} + +/// Adds an OTP record to the specified vault using an interactive dialog /** * Blueprint * 1. Get the OTP record kind from the user (allow Hotp and Totp) @@ -26,7 +40,11 @@ use std::path::PathBuf; * 5. `read_vault`, `vault::add_otp_record`, `write_vault`, bail on error */ // TODO Compact question boilerplate -pub fn add_record(vault_path: &PathBuf, key: &str, mut ctx: Context) -> Result<(), Error> { +pub fn add_record_interactive( + vault_path: &PathBuf, + key: &str, + mut ctx: Context, +) -> Result<(), Error> { tracepoint!(); // (1) diff --git a/src/cli/otp/mod.rs b/src/cli/otp/mod.rs index 3285e41..e43b558 100644 --- a/src/cli/otp/mod.rs +++ b/src/cli/otp/mod.rs @@ -4,6 +4,7 @@ mod remove; mod token; pub use self::add::add_record; +pub use self::add::add_record_interactive; pub use self::import::import_url; pub use self::remove::remove_record; pub use self::token::get_token; diff --git a/src/cli/password/add.rs b/src/cli/password/add.rs index b8b44d6..d816da2 100644 --- a/src/cli/password/add.rs +++ b/src/cli/password/add.rs @@ -4,7 +4,31 @@ use lib::types::Record; use lib::{error, utils}; use std::path::PathBuf; -/// Adds a password record to the specified vault +/// Adds the provided password record to the specified vault +/** + * Blueprint + * 1. `read_vault`, `vault::add_record`, `write_vault`, bail on error + */ +pub fn add_record( + vault_path: &PathBuf, + key: &str, + mut ctx: Context, + record: Record, + record_id: String, +) -> Result<(), Error> { + tracepoint!(); + + // (1) + // TODO These unwraps are due to the fact that the errors cannot be made + // into failure::Error's. Find a workaround + let mut vault = utils::read_vault(&vault_path, &mut ctx).unwrap(); + vault.add_record(record, record_id)?; + utils::write_vault(&vault_path, &vault, &mut ctx, &key).unwrap(); + + Ok(()) +} + +/// Adds a password record to the specified vault using an interactive dialog /** * Blueprint * 1. Get the information necessary to construct a record from the user or from @@ -17,10 +41,10 @@ use std::path::PathBuf; * e) Account password: mandatory * 2. Construct a `Record` * 3. Get a record ID from the user, bail if not provided - * 4. `read_vault`, `Vault::add_record`, `write_vault`, bail on error + * 4. `add_record` */ // TODO Compact question boilerplate -pub fn add_record(vault_path: &PathBuf, key: &str, mut ctx: Context) -> Result<(), Error> { +pub fn add_record_interactive(vault_path: &PathBuf, key: &str, ctx: Context) -> Result<(), Error> { tracepoint!(); // (1.a) @@ -40,7 +64,7 @@ pub fn add_record(vault_path: &PathBuf, key: &str, mut ctx: Context) -> Result<( }; // (1.c) - let username = question!("What is the username associated with this password? [None] ")?; + let username = question!("What is the username associated with this password? ")?; let username = username.trim(); let username = if username.is_empty() { None @@ -69,7 +93,7 @@ pub fn add_record(vault_path: &PathBuf, key: &str, mut ctx: Context) -> Result<( username, home, email, - password: Some(password.to_owned()), + password: password.to_owned(), }; // (3) @@ -81,14 +105,10 @@ pub fn add_record(vault_path: &PathBuf, key: &str, mut ctx: Context) -> Result<( let mut record_id = record_id.trim().to_owned(); if record_id.is_empty() { record_id = record_id_default; - } + }; // (4) - // TODO These unwraps are due to the fact that the errors cannot be made - // into failure::Error's. Find a workaround - let mut vault = utils::read_vault(&vault_path, &mut ctx).unwrap(); - vault.add_record(record, record_id)?; - utils::write_vault(&vault_path, &vault, &mut ctx, &key).unwrap(); + add_record(&vault_path, &key, ctx, record, record_id)?; Ok(()) } diff --git a/src/cli/password/get.rs b/src/cli/password/get.rs index b93a792..7628032 100644 --- a/src/cli/password/get.rs +++ b/src/cli/password/get.rs @@ -1,6 +1,5 @@ use failure::Error; use gpgme::Context; -use lib::error; use lib::utils; use std::path::PathBuf; @@ -8,7 +7,7 @@ use std::path::PathBuf; /** * Blueprint * 1. `read_vault`, `vault.get_record`, bail on error - * 2. Return the `password` field or bail if it is not defined + * 2. Return the `password` field */ pub fn get_password( vault_path: &PathBuf, @@ -22,11 +21,7 @@ pub fn get_password( let record = vault.get_record(record_id)?; // (2) - match record.password { - Some(ref password) => { - println!("{}", password); - Ok(()) - } - None => Err(error::NoSuchField("password".to_string()))?, - } + println!("{}", record.password); + + Ok(()) } diff --git a/src/cli/password/mod.rs b/src/cli/password/mod.rs index 2895ec9..9076928 100644 --- a/src/cli/password/mod.rs +++ b/src/cli/password/mod.rs @@ -4,6 +4,7 @@ mod get; mod remove; pub use self::add::add_record; +pub use self::add::add_record_interactive; pub use self::generate::generate_password; pub use self::get::get_password; pub use self::remove::remove_record; diff --git a/src/lib/error.rs b/src/lib/error.rs index 326a20d..88ea97b 100644 --- a/src/lib/error.rs +++ b/src/lib/error.rs @@ -22,10 +22,6 @@ pub struct NoKeyError(); #[fail(display = "A mandatory argument was not provided")] pub struct MandatoryArgumentAbsentError(); -#[derive(Debug, Fail)] -#[fail(display = "Failed to find field {} on record", 0)] -pub struct NoSuchField(pub String); - #[derive(Debug, Fail)] #[fail(display = "Failed to create a GPG cryptographic context")] pub struct GgpContextCreationFailed(); diff --git a/src/lib/types.rs b/src/lib/types.rs index 0fb1adf..18295ae 100644 --- a/src/lib/types.rs +++ b/src/lib/types.rs @@ -117,11 +117,25 @@ fn tree_add_element(buf: &mut String, item: &str, depth: usize) { pub struct Record { pub username: Option, pub email: Option, - pub password: Option, + pub password: String, pub home: Option, } impl Record { + pub fn new( + password: String, + username: Option, + email: Option, + home: Option, + ) -> Record { + Record { + password, + username, + email, + home, + } + } + pub fn display(&self, disclose: bool, depth: usize) -> String { let mut buf = String::new(); @@ -146,10 +160,10 @@ impl Record { depth, ); } - if self.password.is_some() && disclose { + if disclose { tree_add_element( &mut buf, - &format!("Password: {}", self.password.clone().unwrap()), + &format!("Password: {}", self.password.clone()), depth, ); } @@ -208,6 +222,35 @@ impl HmacAlgorithm { } impl OtpRecord { + pub fn new_totp( + secret: String, + issuer: Option, + algorithm: HmacAlgorithm, + digits: u32, + period: u64, + ) -> OtpRecord { + OtpRecord::Totp { + secret, + issuer, + algorithm, + digits, + period, + } + } + pub fn new_hotp( + secret: String, + issuer: Option, + algorithm: HmacAlgorithm, + digits: u32, + ) -> OtpRecord { + OtpRecord::Hotp { + secret, + issuer, + algorithm, + digits, + } + } + /// Generate a token for this record. `counter` is required for Otp::Hotp /// and ignored by Otp::Totp pub fn generate_token(&self, counter: Option) -> Result {