void-cli 0.0.4

CLI for void — anonymous encrypted source control
//! Decrypt data using a provided key.
//!
//! Takes a hex-encoded key and ciphertext, decrypts with AES-256-GCM.

use serde::Serialize;
use void_core::crypto;

use crate::output::{run_command, CliError, CliOptions};

/// Command-line arguments for decrypt.
#[derive(Debug)]
pub struct DecryptArgs {
    /// 32-byte key as hex (64 chars).
    pub key: String,
    /// Ciphertext to decrypt (hex-encoded).
    pub ciphertext: String,
}

/// JSON output for the decrypt command.
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DecryptOutput {
    /// Decrypted data (UTF-8 string).
    pub plaintext: String,
}

/// Run the decrypt command.
///
/// # Arguments
///
/// * `args` - Decrypt arguments
/// * `opts` - CLI options
pub fn run(args: DecryptArgs, opts: &CliOptions) -> Result<(), CliError> {
    run_command("decrypt", opts, |_ctx| {
        // Parse key from hex (64 hex chars = 32 bytes)
        let key_bytes = hex::decode(&args.key)
            .map_err(|e| CliError::invalid_args(format!("invalid key hex: {}", e)))?;

        if key_bytes.len() != 32 {
            return Err(CliError::invalid_args(format!(
                "key must be 32 bytes (64 hex chars), got {} bytes",
                key_bytes.len()
            )));
        }

        let key: [u8; 32] = key_bytes.try_into().unwrap();

        // Parse ciphertext from hex
        let ciphertext_bytes = hex::decode(&args.ciphertext)
            .map_err(|e| CliError::invalid_args(format!("invalid ciphertext hex: {}", e)))?;

        // Decrypt with empty AAD (matching TypeScript API)
        let decrypted = crypto::decrypt(&key, &ciphertext_bytes, b"")
            .map_err(|e| CliError::encryption_error(format!("decryption failed: {}", e)))?;

        // Convert to UTF-8 string
        let plaintext = String::from_utf8(decrypted).map_err(|e| {
            CliError::encryption_error(format!("decrypted data is not valid UTF-8: {}", e))
        })?;

        Ok(DecryptOutput { plaintext })
    })
}