reverse_resonance_id 0.1.0

Self-checking symmetric tokens based on reversing squared user identifiers.
Documentation
#![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 {
    /// Generate a token using one of the supported schemes.
    Make(MakeArgs),
    /// Validate an existing token.
    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 {
    /// Token scheme to use.
    #[arg(long, value_enum)]
    scheme: SchemeArg,
    /// User identifier to encode.
    user_id: u64,
    /// Secret key in hex for HMAC scheme.
    #[arg(long)]
    key: Option<String>,
    /// HMAC tag length (hex chars, even, 8..=64).
    #[arg(long, default_value_t = 16)]
    tag_len: usize,
    /// Salt length in bytes for salt-iter scheme.
    #[arg(long, default_value_t = 4)]
    salt_len: usize,
    /// Iteration count for salt-iter scheme.
    #[arg(long, default_value_t = 2048)]
    iters: usize,
}

#[derive(Args, Debug)]
struct VerifyArgs {
    /// Token scheme to verify against.
    #[arg(long, value_enum)]
    scheme: SchemeArg,
    /// Token string to validate.
    token: String,
    /// Secret key in hex for HMAC scheme.
    #[arg(long)]
    key: Option<String>,
    /// HMAC tag length (hex chars, even, 8..=64).
    #[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}"),
    }
}