anubis-age 1.4.0

Post-quantum secure encryption library with hybrid X25519+ML-KEM-1024 mode (internal dependency for anubis-rage)
Documentation
//! AEAD (Authenticated Encryption with Associated Data) abstraction.
//!
//! This module provides a unified interface for AEAD ciphers, supporting both
//! ChaCha20-Poly1305 (default) and AES-256-GCM-SIV (FIPS mode).

use std::io;

/// AEAD cipher abstraction
pub trait Aead {
    /// Encrypt plaintext with the given key and nonce
    fn encrypt(&self, key: &[u8; 32], nonce: &[u8], plaintext: &[u8]) -> io::Result<Vec<u8>>;

    /// Decrypt ciphertext with the given key and nonce
    fn decrypt(&self, key: &[u8; 32], nonce: &[u8], ciphertext: &[u8]) -> io::Result<Vec<u8>>;

    /// Get the nonce size for this cipher
    fn nonce_size(&self) -> usize;

    /// Get the tag size for this cipher
    fn tag_size(&self) -> usize;
}

/// ChaCha20-Poly1305 AEAD cipher (RFC 7539) - Default
pub struct ChaCha20Poly1305Aead;

impl Aead for ChaCha20Poly1305Aead {
    fn encrypt(&self, key: &[u8; 32], nonce: &[u8], plaintext: &[u8]) -> io::Result<Vec<u8>> {
        use chacha20poly1305::{
            aead::{Aead as AeadTrait, KeyInit, Payload},
            ChaCha20Poly1305, Nonce,
        };

        let cipher = ChaCha20Poly1305::new(key.into());
        let nonce = Nonce::from_slice(nonce);

        cipher
            .encrypt(
                nonce,
                Payload {
                    msg: plaintext,
                    aad: b"",
                },
            )
            .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "encryption failed"))
    }

    fn decrypt(&self, key: &[u8; 32], nonce: &[u8], ciphertext: &[u8]) -> io::Result<Vec<u8>> {
        use chacha20poly1305::{
            aead::{Aead as AeadTrait, KeyInit, Payload},
            ChaCha20Poly1305, Nonce,
        };

        let cipher = ChaCha20Poly1305::new(key.into());
        let nonce = Nonce::from_slice(nonce);

        cipher
            .decrypt(
                nonce,
                Payload {
                    msg: ciphertext,
                    aad: b"",
                },
            )
            .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "decryption failed"))
    }

    fn nonce_size(&self) -> usize {
        12 // ChaCha20-Poly1305 uses 96-bit nonces
    }

    fn tag_size(&self) -> usize {
        16 // Poly1305 produces 128-bit tags
    }
}

/// AES-256-GCM-SIV AEAD cipher (RFC 8452) - Default (FIPS 140-3 compliant)
pub struct Aes256GcmSivAead;

impl Aead for Aes256GcmSivAead {
    fn encrypt(&self, key: &[u8; 32], nonce: &[u8], plaintext: &[u8]) -> io::Result<Vec<u8>> {
        use aes_gcm_siv::{
            aead::{Aead as AeadTrait, KeyInit, Payload},
            Aes256GcmSiv, Nonce,
        };

        let cipher = Aes256GcmSiv::new(key.into());
        let nonce = Nonce::from_slice(nonce);

        cipher
            .encrypt(
                nonce,
                Payload {
                    msg: plaintext,
                    aad: b"",
                },
            )
            .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "encryption failed"))
    }

    fn decrypt(&self, key: &[u8; 32], nonce: &[u8], ciphertext: &[u8]) -> io::Result<Vec<u8>> {
        use aes_gcm_siv::{
            aead::{Aead as AeadTrait, KeyInit, Payload},
            Aes256GcmSiv, Nonce,
        };

        let cipher = Aes256GcmSiv::new(key.into());
        let nonce = Nonce::from_slice(nonce);

        cipher
            .decrypt(
                nonce,
                Payload {
                    msg: ciphertext,
                    aad: b"",
                },
            )
            .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "decryption failed"))
    }

    fn nonce_size(&self) -> usize {
        12 // AES-GCM-SIV uses 96-bit nonces
    }

    fn tag_size(&self) -> usize {
        16 // GCM-SIV produces 128-bit tags
    }
}

/// Get the default AEAD cipher (AES-256-GCM-SIV for FIPS 140-3 compliance)
pub fn default_aead() -> Box<dyn Aead> {
    Box::new(Aes256GcmSivAead)
}

/// Get the AEAD cipher name
pub fn aead_name() -> &'static str {
    "AES-256-GCM-SIV"
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_chacha20poly1305_round_trip() {
        let aead = ChaCha20Poly1305Aead;
        let key = [0u8; 32];
        let nonce = [0u8; 12];
        let plaintext = b"Hello, post-quantum world!";

        let ciphertext = aead.encrypt(&key, &nonce, plaintext).unwrap();
        assert_ne!(ciphertext.as_slice(), plaintext);
        assert_eq!(ciphertext.len(), plaintext.len() + aead.tag_size());

        let decrypted = aead.decrypt(&key, &nonce, &ciphertext).unwrap();
        assert_eq!(decrypted.as_slice(), plaintext);
    }

    #[test]
    fn test_aes256gcmsiv_round_trip() {
        let aead = Aes256GcmSivAead;
        let key = [0u8; 32];
        let nonce = [0u8; 12];
        let plaintext = b"FIPS 140-3 compliant encryption!";

        let ciphertext = aead.encrypt(&key, &nonce, plaintext).unwrap();
        assert_ne!(ciphertext.as_slice(), plaintext);
        assert_eq!(ciphertext.len(), plaintext.len() + aead.tag_size());

        let decrypted = aead.decrypt(&key, &nonce, &ciphertext).unwrap();
        assert_eq!(decrypted.as_slice(), plaintext);
    }

    #[test]
    fn test_default_aead() {
        let aead = default_aead();
        assert_eq!(aead.nonce_size(), 12);
        assert_eq!(aead.tag_size(), 16);
    }

    #[test]
    fn test_aead_name() {
        assert_eq!(aead_name(), "AES-256-GCM-SIV");
    }
}