twoken 0.4.0

Generate One-Time Passwords from stored token secrets.
Documentation
use anyhow::{Context, Result};
use clap::Parser;
use serde::Serialize;
use twoken;
use twoken::otp::TOTP;
use twoken::store::{GenericStore, Store};
use twoken::token::{GenericToken, Token};
use twoken::yubikey::Yubikey;

static CLIPBOARD_TIMEOUT_SECONDS: f32 = 30.0;

#[derive(clap::ValueEnum, Default, Debug, Clone, Serialize)]
#[serde(rename_all = "kebab-case")]
enum TokenOption {
    #[default]
    Yubikey,
    TOTP,
}

/// Generate One-Time Passwords from stored token secrets.
///
/// Compatible with real Yubikey tokens and RFC 6238 Time-Based One-Time
/// Password tokens, with secsets stored in the configured backend. Note: A
/// token counter is updated in the password store with each Yubikey token use.
#[derive(Parser, Debug)]
#[command(version, about)]
struct Args {
    /// ID of the token in the password store
    token_id: String,

    #[arg(short, long, default_value_t, value_enum)]
    store: GenericStore,

    #[arg(short, long, default_value_t, value_enum)]
    token: TokenOption,

    /// Write generated password to stdout instead of copying it to the clipboard
    #[arg(short, long, default_value_t = false)]
    print: bool,

    /// Generate a new token and save its secrets to the password store
    #[arg(short, long, default_value_t = false)]
    new: bool,
}

fn main() -> Result<()> {
    env_logger::init_from_env(env_logger::Env::new().default_filter_or("warn"));
    let args = Args::parse();
    let mut store = args.store;

    if args.new {
        let token: twoken::token::GenericToken = match args.token {
            TokenOption::Yubikey => GenericToken::Yubikey(Yubikey::default()),
            TokenOption::TOTP => GenericToken::TOTP(TOTP::default()),
        };
        store
            .put(&args.token_id, token)
            .context("Could not store generated token in password store")?;
        return Ok(());
    }

    let token_error = format!("Could not retrieve token: {:?}", args.token_id);
    let token = match args.token {
        TokenOption::Yubikey => {
            GenericToken::Yubikey(store.get::<Yubikey>(&args.token_id).context(token_error)?)
        }
        TokenOption::TOTP => {
            GenericToken::TOTP(store.get::<TOTP>(&args.token_id).context(token_error)?)
        }
    };

    let password = token.generate_password();

    // update and store the yubikey token
    match token {
        GenericToken::Yubikey(mut key) => {
            key.usage_counter += 1;
            store
                .put(&args.token_id, key)
                .context(format!("Could not store token: {:?}", args.token_id))?;
        }
        _ => {}
    };

    if args.print {
        println!("{}", password);
    } else {
        println!(
            "Copying password to clipboard, will clear in {:.0} seconds. Press Ctrl-c to clear now.",
            CLIPBOARD_TIMEOUT_SECONDS
        );
        twoken::clipboard::set_and_clear_clipboard(&password, CLIPBOARD_TIMEOUT_SECONDS)
            .context("Could not copy password to clipboard")?;
    }

    Ok(())
}