use chrono::offset::Utc;
use clap::Parser;
use ykoath_protocol::{calculate_all, YubiKey};
#[derive(Parser)]
struct Opts {
#[clap(subcommand)]
command: Command,
}
#[derive(Parser)]
enum Command {
Code {
#[clap(long)]
single: bool,
query: Option<String>,
},
List,
}
fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
let opts = Opts::parse();
let mut buf = Vec::new();
let yubikey = YubiKey::connect(&mut buf)?;
yubikey.select(&mut buf)?;
match opts.command {
Command::Code { single, query } => {
fn challenge() -> [u8; 8] {
(Utc::now().timestamp() / 30).to_be_bytes()
}
let mut responses = yubikey.calculate_all(true, &challenge(), &mut buf)?;
responses.sort_unstable_by_key(|response| response.name);
if let Some(query) = query {
responses.retain(|response| {
response
.name
.windows(query.len())
.any(|window| window == query.as_bytes())
});
}
if let [calculate_all::Response { name, inner, .. }] = responses[..] {
let name = name.to_vec();
let code = match inner {
calculate_all::Inner::Response(response) => response.code(),
calculate_all::Inner::Hotp => todo!(),
calculate_all::Inner::Touch => {
eprintln!("Touch YubiKey ...");
let response = yubikey.calculate(true, &name, &challenge(), &mut buf)?;
response.code()
}
};
if single {
println!("{code}");
} else {
println!("{}\t{code}", name.escape_ascii());
};
} else {
anyhow::ensure!(
!single,
"Multiple matches, please make the query more specific.",
);
for response in responses {
let code = match response.inner {
calculate_all::Inner::Response(response) => Some(response.code()),
calculate_all::Inner::Hotp | calculate_all::Inner::Touch => None,
};
if let Some(code) = code {
println!("{}\t{code}", response.name.escape_ascii());
} else {
println!("{}\t-", response.name.escape_ascii());
}
}
}
}
Command::List => {
let mut responses = yubikey.list(&mut buf)?;
responses.sort_unstable_by_key(|response| response.name);
for response in responses {
println!("{}", response.name.escape_ascii());
}
}
}
Ok(())
}