use std::collections::HashMap;
use std::io::{Read, Write};
use basil::stream::{
AeadSuite, CekSource, LocalSeedCekRecovery, MlKemSuite, decrypt_aead, decrypt_ml_kem,
encrypt_aead, encrypt_ml_kem,
};
use zeroize::Zeroizing;
type CliError = Box<dyn std::error::Error>;
const DEFAULT_CLI_CHUNK_SIZE: usize = 64;
fn main() {
if let Err(err) = run() {
eprintln!("stream_cli: {err}");
std::process::exit(1);
}
}
fn run() -> Result<(), CliError> {
let mut args = std::env::args();
let _bin = args.next();
let command = args.next().ok_or("missing subcommand")?;
let flags = parse_flags(args)?;
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
let mut input = Vec::new();
std::io::stdin().read_to_end(&mut input)?;
let mut output = Vec::new();
match command.as_str() {
"encrypt" => {
let suite = parse_aead_suite(flag(&flags, "suite")?)?;
let key = parse_cek(flag(&flags, "key")?)?;
let chunk = chunk_size(&flags)?;
rt.block_on(async {
encrypt_aead(
input.as_slice(),
&mut output,
suite,
CekSource::Provided(key),
chunk,
)
.await
.map(|_| ())
})?;
}
"decrypt" => {
let key = parse_cek(flag(&flags, "key")?)?;
rt.block_on(decrypt_aead(input.as_slice(), &mut output, &key))?;
}
"mlkem-encrypt" => {
let suite = parse_ml_kem_suite(flag(&flags, "suite")?)?;
let public = hex::decode(flag(&flags, "pubkey")?)?;
let chunk = chunk_size(&flags)?;
rt.block_on(encrypt_ml_kem(
input.as_slice(),
&mut output,
suite,
&public,
chunk,
))?;
}
"mlkem-decrypt" => {
let suite = parse_ml_kem_suite(flag(&flags, "suite")?)?;
let seed = hex::decode(flag(&flags, "seed")?)?;
let recovery = LocalSeedCekRecovery::new(seed, suite);
rt.block_on(decrypt_ml_kem(input.as_slice(), &mut output, &recovery))?;
}
other => return Err(format!("unknown subcommand: {other}").into()),
}
std::io::stdout().write_all(&output)?;
std::io::stdout().flush()?;
Ok(())
}
fn parse_flags(mut args: std::env::Args) -> Result<HashMap<String, String>, CliError> {
let mut flags = HashMap::new();
while let Some(key) = args.next() {
let name = key
.strip_prefix("--")
.ok_or_else(|| format!("expected --flag, got {key}"))?;
let value = args
.next()
.ok_or_else(|| format!("flag --{name} needs a value"))?;
flags.insert(name.to_owned(), value);
}
Ok(flags)
}
fn flag<'a>(flags: &'a HashMap<String, String>, name: &str) -> Result<&'a str, CliError> {
flags
.get(name)
.map(String::as_str)
.ok_or_else(|| format!("missing required flag --{name}").into())
}
fn chunk_size(flags: &HashMap<String, String>) -> Result<usize, CliError> {
match flags.get("chunk-size") {
Some(value) => Ok(value.parse()?),
None => Ok(DEFAULT_CLI_CHUNK_SIZE),
}
}
fn parse_cek(value: &str) -> Result<Zeroizing<[u8; 32]>, CliError> {
let bytes = hex::decode(value)?;
let array: [u8; 32] = bytes
.as_slice()
.try_into()
.map_err(|_| "key must be 32 bytes (64 hex chars)")?;
Ok(Zeroizing::new(array))
}
fn parse_aead_suite(name: &str) -> Result<AeadSuite, CliError> {
match name {
"aes256gcm" | "aes-256-gcm" => Ok(AeadSuite::Aes256Gcm),
"chacha20poly1305" | "chacha20-poly1305" => Ok(AeadSuite::ChaCha20Poly1305),
other => Err(format!("unknown AEAD suite: {other}").into()),
}
}
fn parse_ml_kem_suite(name: &str) -> Result<MlKemSuite, CliError> {
match name {
"mlkem512" | "ml-kem-512" => Ok(MlKemSuite::MlKem512),
"mlkem768" | "ml-kem-768" => Ok(MlKemSuite::MlKem768),
"mlkem1024" | "ml-kem-1024" => Ok(MlKemSuite::MlKem1024),
other => Err(format!("unknown ML-KEM suite: {other}").into()),
}
}