keynest 0.4.0

Simple, offline, cross-platform secrets manager written in Rust
Documentation
//! Authenticated encryption using XChaCha20-Poly1305.

use crate::crypto::KEY_LEN;

use super::SALT_LEN;
use anyhow::{Result, anyhow};
use chacha20poly1305::{
    Key, XChaCha20Poly1305, XNonce,
    aead::{Aead, KeyInit, Payload},
};
use getrandom::fill;
use zeroize::Zeroizing;

/// Length of the nonce (24 bytes for XChaCha20-Poly1305).
pub const NONCE_LEN: usize = 24;

/// Fills buffer with cryptographically secure random bytes.
fn secure_random(buf: &mut [u8]) -> Result<()> {
    fill(buf).map_err(|_| anyhow!("OS random generator unavailable"))
}

/// Generates a random salt for key derivation.
pub fn generate_salt() -> Result<[u8; SALT_LEN]> {
    let mut salt = [0u8; SALT_LEN];
    secure_random(&mut salt)?;
    Ok(salt)
}

/// Encrypts plaintext using XChaCha20-Poly1305.
///
/// # Errors
///
/// Returns an error if random number generation fails.
pub fn encrypt(key: &[u8], plaintext: &[u8], aad: &[u8]) -> Result<(Vec<u8>, Vec<u8>)> {
    if key.len() != KEY_LEN {
        return Err(anyhow!("invalid key length"));
    }

    let cipher = XChaCha20Poly1305::new(Key::from_slice(key));

    let mut nonce = vec![0u8; NONCE_LEN];
    secure_random(&mut nonce)?;

    let ciphertext = cipher
        .encrypt(
            XNonce::from_slice(&nonce),
            Payload {
                msg: plaintext,
                aad,
            },
        )
        .map_err(|_| anyhow!("encryption failed"))?;

    Ok((ciphertext, nonce))
}

/// Decrypts ciphertext using XChaCha20-Poly1305.
///
/// Returns the plaintext wrapped in `Zeroizing` for secure memory handling.
///
/// # Errors
///
/// Returns an error if:
/// - The key is incorrect
/// - The ciphertext has been tampered with
/// - The data is corrupted
pub fn decrypt(
    key: &[u8],
    nonce: &[u8],
    ciphertext: &[u8],
    aad: &[u8],
) -> Result<Zeroizing<Vec<u8>>> {
    if key.len() != KEY_LEN {
        return Err(anyhow!("invalid key length"));
    }

    if nonce.len() != NONCE_LEN {
        return Err(anyhow!("invalid nonce length"));
    }

    let cipher = XChaCha20Poly1305::new(Key::from_slice(key));

    let plaintext = cipher
        .decrypt(
            XNonce::from_slice(nonce),
            Payload {
                msg: ciphertext,
                aad,
            },
        )
        .map_err(|_| anyhow!("Invalid password or corrupted data"))?;
    Ok(Zeroizing::new(plaintext))
}