//
// Syd: rock-solid application kernel
// src/utils/syd-key.rs: Utility to generate encryption keys and save to keyrings(7)
//
// Copyright (c) 2024, 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{
    io::{stdin, Cursor},
    os::unix::ffi::OsStrExt,
    process::ExitCode,
    str::from_utf8,
};

use btoi::btoi;
use nix::errno::Errno;
use rpassword::{prompt_password, read_password_from_bufread};
use syd::hash::{
    add_key, hash, HashAlgorithm, Key, KeySerial, KEY_SIZE, KEY_SPEC_PROCESS_KEYRING,
    KEY_SPEC_SESSION_KEYRING, KEY_SPEC_THREAD_KEYRING, KEY_SPEC_USER_KEYRING,
    KEY_SPEC_USER_SESSION_KEYRING,
};
use zeroize::Zeroize;

// Set global allocator to GrapheneOS allocator.
#[cfg(all(
    not(coverage),
    not(feature = "prof"),
    not(target_os = "android"),
    not(target_arch = "riscv64"),
    target_page_size_4k,
    target_pointer_width = "64"
))]
#[global_allocator]
static GLOBAL: hardened_malloc::HardenedMalloc = hardened_malloc::HardenedMalloc;

// Set global allocator to tcmalloc if profiling is enabled.
#[cfg(feature = "prof")]
#[global_allocator]
static GLOBAL: tcmalloc::TCMalloc = tcmalloc::TCMalloc;

syd::main! {
    use lexopt::prelude::*;

    syd::set_sigpipe_dfl()?;

    // Parse CLI options.
    let mut opt_desc = None;
    let mut opt_type = None;
    let mut opt_ring = None;
    let mut opt_pass = false;
    let mut opt_pinp = false;

    let mut parser = lexopt::Parser::from_env();
    while let Some(arg) = parser.next()? {
        match arg {
            Short('h') => {
                help();
                return Ok(ExitCode::SUCCESS);
            }
            Short('d') => opt_desc = Some(from_utf8(parser.value()?.as_bytes())?.to_string()),
            Short('t') => opt_type = Some(from_utf8(parser.value()?.as_bytes())?.to_string()),
            Short('k') => opt_ring = Some(keyspec2serial(parser.value()?.as_bytes())?),
            Short('p') => opt_pass = true,
            Short('P') => opt_pinp = true,
            _ => return Err(arg.unexpected().into()),
        }
    }

    if opt_pass && opt_pinp {
        eprintln!("syd-key: -p and -P are mutually exclusive!");
        return Err(Errno::EINVAL.into());
    }

    let keydesc = opt_desc.unwrap_or_else(|| "SYD-3-CRYPT".to_string());
    let keytype = opt_type.unwrap_or_else(|| "user".to_string());
    let keyring = opt_ring.unwrap_or(KEY_SPEC_USER_KEYRING);
    let key_id = {
        let key = if opt_pass { // -p
            let mut pass = prompt_password("Passphrase: ")?;
            let hash = hash(Cursor::new(pass.as_bytes()), HashAlgorithm::Sha256)?;
            pass.zeroize();

            assert_eq!(hash.len(), KEY_SIZE,
                "BUG: syd::hash::hash produced {} bytes (expected {KEY_SIZE}), report a bug!",
                hash.len());
            let hash = hash.into_boxed_slice();
            #[expect(clippy::disallowed_methods)]
            let hash: Box<[u8; KEY_SIZE]> = hash.try_into().unwrap();

            Key::new(*hash)
        } else if opt_pinp { // -P
            let mut stdin = stdin().lock();
            let mut pass = read_password_from_bufread(&mut stdin)?;
            let hash = hash(Cursor::new(pass.as_bytes()), HashAlgorithm::Sha256)?;
            pass.zeroize();

            assert_eq!(hash.len(), KEY_SIZE,
                "BUG: syd::hash::hash produced {} bytes (expected {KEY_SIZE}), report a bug!",
                hash.len());
            let hash = hash.into_boxed_slice();
            #[expect(clippy::disallowed_methods)]
            let hash: Box<[u8; KEY_SIZE]> = hash.try_into().unwrap();

            Key::new(*hash)
        } else { // Default: getrandom(2) with GRND_RANDOM.
            Key::random()?
        };
        add_key(&keytype, &keydesc, key.as_ref(), keyring)?
    }; // Key zeroized on drop.

    println!("{key_id}");
    Ok(ExitCode::SUCCESS)
}

fn help() {
    println!("Usage: syd-key [-hpP] [-d keydesc] [-t keytype] [-k keyring]");
    println!("Utility to generate encryption keys and save to keyrings(7)");
    println!("Options:");
    println!("  -h           Print this help message and exit.");
    println!("  -p           Read passphrase from controlling TTY but NOT stdin(3)!");
    println!("               Hash passphrase using SHA3-256 to generate encryption key.");
    println!("               Default is to generate key using getrandom(2) with GRND_RANDOM flag.");
    println!("  -P           Read passphrase from stdin(3) rather than TTY.");
    println!("  -d keydesc   Specify alternative key description. Default is 'SYD-3-CRYPT'.");
    println!("  -t keytype   Specify alternative key type. Default is 'user'.");
    println!("  -k keyring   Specify alternative key type. Default is 'KEY_SPEC_USER_KEYRING'.");
    println!(
        "               May be exactly one of thread, process, session, user or user-session."
    );
    println!("               May also be a 32-bit decimal number specifying a keyring ID.");
}

fn keyspec2serial(spec: &[u8]) -> Result<KeySerial, Errno> {
    match spec {
        b"thread" => Ok(KEY_SPEC_THREAD_KEYRING),
        b"process" => Ok(KEY_SPEC_PROCESS_KEYRING),
        b"session" => Ok(KEY_SPEC_SESSION_KEYRING),
        b"user" => Ok(KEY_SPEC_USER_KEYRING),
        b"user-session" => Ok(KEY_SPEC_USER_SESSION_KEYRING),
        other => {
            // Parse numeric keyring id into KeySerial using btoi.
            btoi::<KeySerial>(other).map_err(|_| Errno::EINVAL)
        }
    }
}
