Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Timed one time passwords #10

Merged
merged 13 commits into from
May 14, 2024
Prev Previous commit
Next Next commit
Add TOTP handling
  • Loading branch information
anssip committed May 10, 2024
commit e9f19657e307decfe790a8338b2e79fb46a811b2
185 changes: 40 additions & 145 deletions src/actions.rs → src/actions/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub mod show;

use clap::Command;
use clipboard::ClipboardContext;
use clipboard::ClipboardProvider;
Expand All @@ -20,9 +22,14 @@ pub trait Action {
}

pub trait UnlockingAction: Action {
fn execute(&self) {
fn execute(&self) {
info!("Unlocking vault...");
match self.unlock() {
let result = if self.is_totp_vault() {
self.unlock_totp_vault()
} else {
self.unlock()
};
match result {
Ok(mut vault) => {
match self.run_with_vault(&mut vault) {
Ok(_) => {
Expand All @@ -39,6 +46,10 @@ pub trait UnlockingAction: Action {
}
}

fn is_totp_vault(&self) -> bool {
false
}

fn run_with_vault(&self, _: &mut Box<dyn Vault>) -> anyhow::Result<()> {
Ok(())
}
Expand All @@ -60,6 +71,14 @@ pub trait UnlockingAction: Action {
let keyfile_path = store::get_keyfile_path();
self.get_vault(&master_pwd, &filepath, keyfile_path)
}

fn unlock_totp_vault(&self) -> Result<Box<dyn Vault>> {
let stored_password = keychain::get_totp_master_password();
let master_pwd = stored_password.unwrap_or_else(|_| ui::ask_totp_master_password());
let filepath = store::get_totp_vault_path();
let keyfile_path = store::get_totp_keyfile_path();
self.get_vault(&master_pwd, &filepath, keyfile_path)
}
}

pub fn copy_to_clipboard(value: &String) {
Expand All @@ -72,6 +91,7 @@ pub enum ItemType {
Credential,
Payment,
Note,
Totp
}

impl ItemType {
Expand All @@ -82,6 +102,8 @@ impl ItemType {
ItemType::Payment
} else if *matches.get_one("notes").expect("defaulted to false by clap") {
ItemType::Note
} else if *matches.get_one("otp").expect("defaulted to false by clap") {
ItemType::Totp
} else {
ItemType::Credential
}
Expand Down Expand Up @@ -166,146 +188,11 @@ impl UnlockingAction for AddAction {
match self.item_type {
ItemType::Credential => self.add_credential(vault)?,
ItemType::Payment => self.add_payment(vault)?,
ItemType::Note => self.add_note(vault)?
};
Ok(())
}
}

pub struct ShowAction {
pub grep: Option<String>,
pub verbose: bool,
pub item_type: ItemType,
}

impl ShowAction {
pub fn new(matches: &ArgMatches) -> ShowAction {
ShowAction {
grep: matches.get_one::<String>("REGEXP").cloned(),
verbose: *matches
.get_one::<bool>("verbose")
.expect("defaulted to false by clap"),
item_type: ItemType::new_from_args(matches),
}
}
fn show_credentials(&self, vault: &mut Box<dyn Vault>) -> anyhow::Result<()> {
let grep = match &self.grep {
Some(grep) => Some(String::from(grep)),
None => panic!("-g <REGEXP> is required"),
};
let matches = find_credentials(&vault, grep).context("Failed to find matches. Invalid password? Try unlocking the vault with `passlane unlock`.")?;

if matches.len() >= 1 {
println!("Found {} matches:", matches.len());
ui::show_credentials_table(&matches, self.verbose);
if matches.len() == 1 {
copy_to_clipboard(&matches[0].password);
println!("Password copied to clipboard!", );
} else {
match ui::ask_index(
"To copy one of these passwords to clipboard, please enter a row number from the table above, or press q to exit:",
matches.len() as i16 - 1,
) {
Ok(index) => {
copy_to_clipboard(&matches[index].password);
println!("Password from index {} copied to clipboard!", index);
}
Err(message) => {
println!("{}", message);
}
}
ItemType::Note => self.add_note(vault)?,
ItemType::Totp => {
// TODO
// self.add_totp(vault);
}
}
Ok(())
}

fn show_payments(&self, vault: &mut Box<dyn Vault>) -> anyhow::Result<()> {
debug!("showing payments");
let matches = vault.find_payments();
if matches.len() == 0 {
println!("No payment cards found");
} else {
println!("Found {} payment cards:", matches.len());
ui::show_payment_cards_table(&matches, self.verbose);

if matches.len() == 1 {
let response = ui::ask("Do you want to see the card details? (y/n)");
if response == "y" {
ui::show_card(&matches[0]);
copy_to_clipboard(&matches[0].number);
println!("Card number copied to clipboard!", );
}
} else {
match ui::ask_index(
"Enter a row number from the table above, or press q to exit:",
matches.len() as i16 - 1,
) {
Ok(index) => {
ui::show_card(&matches[index]);
copy_to_clipboard(&matches[index].number);
println!("Card number copied to clipboard!");
}
Err(message) => {
println!("{}", message);
}
}
}
}
Ok(())
}

fn show_notes(&self, vault: &mut Box<dyn Vault>) -> anyhow::Result<()> {
debug!("showing notes");
let matches = vault.find_notes();
if matches.len() == 0 {
println!("No notes found");
} else {
println!("Found {} notes:", matches.len());
ui::show_notes_table(&matches, self.verbose);


if matches.len() == 1 {
let response = ui::ask("Do you want to see the full note? (y/n)");
if response == "y" {
ui::show_note(&matches[0]);
}
} else {
match ui::ask_index(
"Enter a row number from the table above, or press q to exit:",
matches.len() as i16 - 1,
) {
Ok(index) => {
ui::show_note(&matches[index]);
}
Err(message) => {
println!("{}", message);
}
}
}
}
Ok(())
}
}

fn find_credentials(
vault: &Box<dyn Vault>,
grep: Option<String>,
) -> anyhow::Result<Vec<Credential>> {
let matches = vault.grep(&grep);
if matches.is_empty() {
println!("No matches found");
}
Ok(matches)
}

impl Action for ShowAction {}

impl UnlockingAction for ShowAction {
fn run_with_vault(&self, vault: &mut Box<dyn Vault>) -> anyhow::Result<()> {
match self.item_type {
ItemType::Credential => self.show_credentials(vault)?,
ItemType::Payment => self.show_payments(vault)?,
ItemType::Note => self.show_notes(vault)?
};
Ok(())
}
Expand All @@ -326,7 +213,11 @@ impl ExportAction {
pub fn export_csv(&self, vault: &mut Box<dyn Vault>) -> anyhow::Result<i64> {
debug!("exporting to csv");
if self.item_type == ItemType::Credential {
let creds = find_credentials(&vault, None)?;
let creds = vault.grep(&None);
if creds.is_empty() {
println!("No credentials found");
return Ok(0);
}
store::write_credentials_to_csv(&self.file_path, &creds)
} else if self.item_type == ItemType::Payment {
let cards = vault.find_payments();
Expand Down Expand Up @@ -368,7 +259,7 @@ impl DeleteAction {
}

fn delete_credentials(vault: &mut Box<dyn Vault>, grep: &str) -> anyhow::Result<()> {
let matches = find_credentials(&vault, Some(String::from(grep))).context("Unable to get matches. Invalid password? Try unlocking again.")?;
let matches = vault.grep(&Some(String::from(grep)));

if matches.is_empty() {
debug!("no matches found to delete");
Expand Down Expand Up @@ -422,7 +313,7 @@ fn delete_payment(vault: &mut Box<dyn Vault>) -> anyhow::Result<()> {
) {
Ok(index) => {
if index == usize::MAX {
// ignore
// ignore
} else {
vault.delete_payment(&cards[index].id);
println!("Deleted card named '{}'!", cards[index].name);
Expand Down Expand Up @@ -456,7 +347,7 @@ fn delete_note(vault: &mut Box<dyn Vault>) -> anyhow::Result<()> {
) {
Ok(index) => {
if index == usize::MAX {
// ignore
// ignore
} else {
vault.delete_note(&notes[index].id);
println!("Deleted note with title '{}'!", notes[index].title);
Expand Down Expand Up @@ -488,6 +379,10 @@ impl UnlockingAction for DeleteAction {
ItemType::Note => {
delete_note(vault)?;
}
ItemType::Totp => {
// TODO
// delete_totp(vault);
}
};
Ok(())
}
Expand Down
Loading