Skip to content

Commit

Permalink
Allow to use add subcommands without interactive mode
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ALCC01 committed Jul 17, 2018
1 parent af45ad6 commit 960090e
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 35 deletions.
113 changes: 107 additions & 6 deletions src/cli/args.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<String>,
#[structopt(requires = "algo")]
/// The secret
secret: Option<String>,
#[structopt(requires = "secret", long = "issuer")]
/// The issuer of this secret
issuer: Option<String>,
#[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 {
Expand Down Expand Up @@ -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<String>,
#[structopt()]
/// The password
password: Option<String>,
#[structopt(short = "u", long = "username", requires = "password")]
/// The username associated with this password
username: Option<String>,
#[structopt(long = "email", requires = "password")]
/// The email associated with this password
email: Option<String>,
#[structopt(long = "home", requires = "password")]
/// The homepage for this service
home: Option<String>,
},
#[structopt(name = "rm")]
/// Remove a record from a vault
Remove {
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
Expand Down
22 changes: 20 additions & 2 deletions src/cli/otp/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions src/cli/otp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
42 changes: 31 additions & 11 deletions src/cli/password/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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(())
}
Expand Down
13 changes: 4 additions & 9 deletions src/cli/password/get.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
use failure::Error;
use gpgme::Context;
use lib::error;
use lib::utils;
use std::path::PathBuf;

/// Returns a password from a record
/**
* 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,
Expand All @@ -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(())
}
1 change: 1 addition & 0 deletions src/cli/password/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
4 changes: 0 additions & 4 deletions src/lib/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
49 changes: 46 additions & 3 deletions src/lib/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,25 @@ fn tree_add_element(buf: &mut String, item: &str, depth: usize) {
pub struct Record {
pub username: Option<String>,
pub email: Option<String>,
pub password: Option<String>,
pub password: String,
pub home: Option<String>,
}

impl Record {
pub fn new(
password: String,
username: Option<String>,
email: Option<String>,
home: Option<String>,
) -> Record {
Record {
password,
username,
email,
home,
}
}

pub fn display(&self, disclose: bool, depth: usize) -> String {
let mut buf = String::new();

Expand All @@ -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,
);
}
Expand Down Expand Up @@ -208,6 +222,35 @@ impl HmacAlgorithm {
}

impl OtpRecord {
pub fn new_totp(
secret: String,
issuer: Option<String>,
algorithm: HmacAlgorithm,
digits: u32,
period: u64,
) -> OtpRecord {
OtpRecord::Totp {
secret,
issuer,
algorithm,
digits,
period,
}
}
pub fn new_hotp(
secret: String,
issuer: Option<String>,
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<u64>) -> Result<String, OtpError> {
Expand Down

0 comments on commit 960090e

Please sign in to comment.