#![forbid(unsafe_code)]
use clap::{Args, Parser, Subcommand, ValueEnum};
use reverse_resonance_id::{
make_token_baseline, make_token_hmac, make_token_salt_iter, validate_token_baseline,
validate_token_hmac, validate_token_salt_iter,
};
use serde::Serialize;
use std::process::ExitCode;
use std::time::Instant;
#[derive(Parser, Debug)]
#[command(author, version, about = "reverse_resonance_id CLI", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug)]
enum Command {
Make(MakeArgs),
Verify(VerifyArgs),
}
#[derive(Copy, Clone, Debug, ValueEnum)]
enum SchemeArg {
Baseline,
Hmac,
#[clap(name = "salt-iter")]
SaltIter,
}
impl SchemeArg {
fn as_str(self) -> &'static str {
match self {
SchemeArg::Baseline => "baseline",
SchemeArg::Hmac => "hmac",
SchemeArg::SaltIter => "salt-iter",
}
}
}
#[derive(Args, Debug)]
struct MakeArgs {
#[arg(long, value_enum)]
scheme: SchemeArg,
user_id: u64,
#[arg(long)]
key: Option<String>,
#[arg(long, default_value_t = 16)]
tag_len: usize,
#[arg(long, default_value_t = 4)]
salt_len: usize,
#[arg(long, default_value_t = 2048)]
iters: usize,
}
#[derive(Args, Debug)]
struct VerifyArgs {
#[arg(long, value_enum)]
scheme: SchemeArg,
token: String,
#[arg(long)]
key: Option<String>,
#[arg(long, default_value_t = 16)]
tag_len: usize,
}
#[derive(Serialize)]
struct CliResponse {
token: String,
scheme: String,
gen_time_ms: f64,
}
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(err) => {
eprintln!("{err}");
ExitCode::FAILURE
}
}
}
fn run() -> Result<(), String> {
let cli = Cli::parse();
match cli.command {
Command::Make(args) => handle_make(args),
Command::Verify(args) => handle_verify(args),
}
}
fn handle_make(args: MakeArgs) -> Result<(), String> {
let start = Instant::now();
let token = match args.scheme {
SchemeArg::Baseline => make_token_baseline(args.user_id).map_err(|e| e.to_string())?,
SchemeArg::Hmac => {
let key_hex = args
.key
.as_ref()
.ok_or_else(|| "HMAC scheme requires --key <HEX>".to_string())?;
let key = parse_hex(key_hex)?;
make_token_hmac(args.user_id, &key, args.tag_len).map_err(|e| e.to_string())?
}
SchemeArg::SaltIter => make_token_salt_iter(args.user_id, args.salt_len, args.iters)
.map_err(|e| e.to_string())?,
};
let elapsed = start.elapsed();
emit_response(token.as_str(), args.scheme, elapsed);
Ok(())
}
fn handle_verify(args: VerifyArgs) -> Result<(), String> {
let start = Instant::now();
let ok = match args.scheme {
SchemeArg::Baseline => validate_token_baseline(&args.token),
SchemeArg::Hmac => {
let key_hex = args
.key
.as_ref()
.ok_or_else(|| "HMAC verification requires --key <HEX>".to_string())?;
let key = parse_hex(key_hex)?;
validate_token_hmac(&args.token, &key, args.tag_len)
}
SchemeArg::SaltIter => validate_token_salt_iter(&args.token),
};
let elapsed = start.elapsed();
if !ok {
return Err("token verification failed".to_string());
}
emit_response(&args.token, args.scheme, elapsed);
Ok(())
}
fn parse_hex(input: &str) -> Result<Vec<u8>, String> {
hex::decode(input).map_err(|err| format!("invalid hex: {err}"))
}
fn emit_response(token: &str, scheme: SchemeArg, elapsed: std::time::Duration) {
let response = CliResponse {
token: token.to_owned(),
scheme: scheme.as_str().to_owned(),
gen_time_ms: elapsed.as_secs_f64() * 1_000.0,
};
match serde_json::to_string(&response) {
Ok(json) => println!("{json}"),
Err(err) => eprintln!("failed to serialize JSON output: {err}"),
}
}