syd 3.57.0

rock-solid application kernel
Documentation
//
// Syd: rock-solid application kernel
// src/utils/syd-aes.rs: AES-CTR Encryption and Decryption Utility
//
// Copyright (c) 2024, 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{
    mem::MaybeUninit,
    os::unix::ffi::OsStrExt,
    process::ExitCode,
    time::{Duration, Instant},
};

use btoi::btoi;
use ctr::cipher::StreamCipher;
use nix::unistd::isatty;
use syd::{
    compat::slice_assume_init_mut,
    config::IO_BUF_SIZE,
    cookie::safe_read2,
    err::SydResult,
    hash::{aes_ctr, hex_decode, CryptKey, KeySerial, IV},
    io::write_all,
    retry::retry_on_eintr,
};
use zeroize::Zeroizing;

// 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;

fn process_data(encrypting: bool, key_id: KeySerial, iv: IV, verbose: bool) -> SydResult<()> {
    // Read key into locked, zeroized memory and key CTR cipher.
    let keys = CryptKey::load(key_id, key_id)?;
    let mut cipher = aes_ctr(keys.enc(), &iv)?;

    let stdin = std::io::stdin();
    let stdout = std::io::stdout();

    let stime = Instant::now();
    let mut ltime = stime;
    let mut nbytes: u64 = 0;
    let mut nwrite: u64 = 0;
    let verbose = verbose && isatty(std::io::stderr()).unwrap_or(false);

    // SAFETY: Zeroize buffer which holds plaintext or ciphertext.
    let mut buf: Zeroizing<[MaybeUninit<u8>; IO_BUF_SIZE]> =
        Zeroizing::new([MaybeUninit::uninit(); IO_BUF_SIZE]);

    loop {
        let nread = match retry_on_eintr(|| safe_read2(&stdin, &mut buf[..]))? {
            0 => break,
            nread => nread,
        };

        // SAFETY: safe_read2 initialized the first "nread" bytes.
        let chunk = unsafe { slice_assume_init_mut(&mut buf[..nread]) };

        // AES-CTR is symmetric, encryption and decryption are one operation.
        cipher.apply_keystream(chunk);
        write_all(&stdout, &*chunk)?;

        nbytes = nbytes.saturating_add(nread as u64);
        nwrite = nwrite.saturating_add(1);

        if verbose {
            let now = Instant::now();
            if now.duration_since(ltime) >= Duration::from_millis(500) {
                let elapsed = stime.elapsed();
                let speed = nbytes as f64 / elapsed.as_secs_f64();
                let output = format!(
                    "{} bytes ({:.2} GB, {:.2} GiB) processed, {:.2?} s, {:.2} MB/s",
                    nbytes,
                    nbytes as f64 / 1_000_000_000.0,
                    nbytes as f64 / (1 << 30) as f64,
                    elapsed,
                    speed / (1 << 20) as f64
                );
                eprint!("\r\x1B[K{output}");
                ltime = now;
            }
        }
    }

    if verbose {
        let action = if encrypting { "encrypted" } else { "decrypted" };
        let elapsed = stime.elapsed();
        eprintln!(
            "\n{nwrite} records {action}.\n{} bytes ({:.2} GB, {:.2} GiB) processed, {:.5?} s, {:.2} MB/s",
            nbytes,
            nbytes as f64 / 1_000_000_000.0,
            nbytes as f64 / (1 << 30) as f64,
            elapsed,
            nbytes as f64 / elapsed.as_secs_f64() / (1 << 20) as f64
        );
    }

    Ok(())
}

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

    syd::set_sigpipe_dfl()?;

    // Parse CLI options.
    let mut opt_encrypt = None;
    let mut opt_key_id = None;
    let mut opt_iv_hex = None;
    let mut opt_verbose = false;

    let mut parser = lexopt::Parser::from_env();
    while let Some(arg) = parser.next()? {
        match arg {
            Short('h') => {
                help();
                return Ok(ExitCode::SUCCESS);
            }
            Short('v') => opt_verbose = true,
            Short('e') => opt_encrypt = Some(true),
            Short('d') => opt_encrypt = Some(false),
            Short('k') => opt_key_id = Some(btoi::<KeySerial>(parser.value()?.as_bytes())?),
            Short('i') => opt_iv_hex = Some(parser.value()?.parse::<String>()?),
            _ => return Err(arg.unexpected().into()),
        }
    }

    let is_enc = if let Some(is_enc) = opt_encrypt {
        is_enc
    } else {
        eprintln!("syd-aes: Error: -e or -d options are required.");
        help();
        return Ok(ExitCode::FAILURE);
    };

    let key_id = if let Some(key_id) = opt_key_id {
        key_id
    } else {
        eprintln!("syd-aes: Error: -k option is required.");
        help();
        return Ok(ExitCode::FAILURE);
    };

    if opt_iv_hex.is_none() {
        eprintln!("syd-aes: Error: -i option is required.");
        help();
        return Ok(ExitCode::FAILURE);
    }
    let iv = match opt_iv_hex
        .and_then(|hex| hex_decode(hex.as_bytes()).ok())
        .and_then(|vec| vec.as_slice().try_into().ok())
    {
        Some(iv) => IV::new(iv),
        None => {
            eprintln!("syd-aes: Error: IV must be valid hex, and 128 bits (16 bytes) in length!");
            return Ok(ExitCode::FAILURE);
        }
    };

    process_data(is_enc, key_id, iv, opt_verbose).map(|_| ExitCode::SUCCESS)
}

fn help() {
    println!("Usage: syd-aes [-h] -e|-d -k <key-serial> -i <iv-hex>");
    println!("AES-CTR Encryption and Decryption Utility");
    println!("Reads from standard input and writes to standard output.");
    println!("  -h        Print this help message and exit.");
    println!("  -v        Enable verbose mode.");
    println!("  -e        Encrypt the input data.");
    println!("  -d        Decrypt the input data.");
    println!("  -k <key>  Key serial ID for keyrings(7) (32-bit integer)");
    println!("            Key must have read permission.");
    println!("  -i <iv>   Hex-encoded IV (128 bits)");
}