#![deny(unsafe_code)]
#![deny(missing_docs)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::panic)]
pub mod aes_gcm;
#[cfg(not(feature = "fips"))]
pub mod chacha20poly1305;
pub const NONCE_LEN: usize = 12;
pub const TAG_LEN: usize = 16;
pub const AES_GCM_128_KEY_LEN: usize = 16;
pub const AES_GCM_256_KEY_LEN: usize = 32;
pub const CHACHA20_POLY1305_KEY_LEN: usize = 32;
pub type Nonce = [u8; NONCE_LEN];
pub type Tag = [u8; TAG_LEN];
mod sealed {
pub trait Sealed {}
impl Sealed for super::aes_gcm::AesGcm128 {}
impl Sealed for super::aes_gcm::AesGcm256 {}
#[cfg(not(feature = "fips"))]
impl Sealed for super::chacha20poly1305::ChaCha20Poly1305Cipher {}
}
pub trait AeadCipher: sealed::Sealed {
const KEY_LEN: usize;
fn new(key: &[u8]) -> Result<Self, AeadError>
where
Self: Sized;
fn generate_nonce() -> Nonce;
#[must_use = "AEAD ciphertext + tag must be transmitted to the receiver"]
fn encrypt(
&self,
nonce: &Nonce,
plaintext: &[u8],
aad: Option<&[u8]>,
) -> Result<(Vec<u8>, Tag), AeadError>;
fn seal(
&self,
plaintext: &[u8],
aad: Option<&[u8]>,
) -> Result<(Nonce, Vec<u8>, Tag), AeadError> {
let nonce = Self::generate_nonce();
let (ciphertext, tag) = self.encrypt(&nonce, plaintext, aad)?;
Ok((nonce, ciphertext, tag))
}
fn decrypt(
&self,
nonce: &Nonce,
ciphertext: &[u8],
tag: &Tag,
aad: Option<&[u8]>,
) -> Result<zeroize::Zeroizing<Vec<u8>>, AeadError>;
}
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum AeadError {
#[error("Invalid key length")]
InvalidKeyLength,
#[error("Invalid nonce length")]
InvalidNonceLength,
#[error(
"Weak key rejected by AEAD constructor (likely uninitialised memory or unset \
configuration field). Generate a fresh key via \
`latticearc::primitives::security::generate_secure_random_bytes(32)`, or — for KAT \
replay only — enable the `kat-test-vectors` Cargo feature and call \
`AeadCipher::new_allow_weak_key`."
)]
WeakKey,
#[error("Encryption failed: {0}")]
EncryptionFailed(String),
#[error("Decryption failed: {0}")]
DecryptionFailed(String),
}
#[inline]
#[must_use]
pub(crate) fn is_all_zero_key(key: &[u8]) -> bool {
crate::primitives::ct::is_all_zero_bytes(key)
}
#[must_use]
pub fn verify_tag_constant_time(expected: &Tag, actual: &Tag) -> bool {
use subtle::ConstantTimeEq;
expected.ct_eq(actual).into()
}
pub fn zeroize_data(data: &mut [u8]) {
use zeroize::Zeroize;
data.zeroize();
}
#[cfg(not(feature = "fips"))]
pub use self::chacha20poly1305::{ChaCha20Poly1305Cipher, XChaCha20Poly1305Cipher};
#[cfg(test)]
mod tests {
use super::*;
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
use subtle::ConstantTimeEq;
let len_eq = a.len().ct_eq(&b.len());
let mut result = len_eq;
for (x, y) in a.iter().zip(b.iter()) {
result &= x.ct_eq(y);
}
result.into()
}
#[test]
fn test_constant_time_eq_equal_succeeds() {
assert!(constant_time_eq(b"hello", b"hello"));
assert!(constant_time_eq(b"", b""));
assert!(constant_time_eq(&[0u8; 32], &[0u8; 32]));
}
#[test]
fn test_constant_time_eq_not_equal_succeeds() {
assert!(!constant_time_eq(b"hello", b"world"));
assert!(!constant_time_eq(b"short", b"longer"));
assert!(!constant_time_eq(b"a", b""));
}
#[test]
fn test_aead_constants_succeeds() {
assert_eq!(NONCE_LEN, 12);
assert_eq!(TAG_LEN, 16);
assert_eq!(AES_GCM_128_KEY_LEN, 16);
assert_eq!(AES_GCM_256_KEY_LEN, 32);
assert_eq!(CHACHA20_POLY1305_KEY_LEN, 32);
}
#[test]
fn test_aead_error_display_fails() {
let err = AeadError::InvalidKeyLength;
assert_eq!(format!("{err}"), "Invalid key length");
let err = AeadError::InvalidNonceLength;
assert_eq!(format!("{err}"), "Invalid nonce length");
let err = AeadError::EncryptionFailed("test".to_string());
assert_eq!(format!("{err}"), "Encryption failed: test");
let err = AeadError::DecryptionFailed("oops".to_string());
assert_eq!(format!("{err}"), "Decryption failed: oops");
let err = AeadError::WeakKey;
let msg = format!("{err}");
assert!(
msg.contains("Weak key rejected by AEAD constructor"),
"WeakKey Display must lead with the diagnosis"
);
assert!(
msg.contains("generate_secure_random_bytes"),
"WeakKey Display must point at the production remediation"
);
assert!(
msg.contains("kat-test-vectors"),
"WeakKey Display must point at the KAT escape hatch"
);
}
#[test]
fn test_aead_encryption_failed_resource_limit_path_is_wired() {
use crate::primitives::resource_limits::validate_encryption_size;
let result = validate_encryption_size(usize::MAX);
assert!(
result.is_err(),
"validate_encryption_size(usize::MAX) must reject; \
AEAD encrypt's map_err relies on this to produce \
AeadError::EncryptionFailed"
);
let wrapped = AeadError::EncryptionFailed("plaintext exceeds resource limits".to_string());
assert!(matches!(wrapped, AeadError::EncryptionFailed(_)));
}
#[test]
fn test_nonce_and_tag_types_succeeds() {
let nonce: Nonce = [0u8; NONCE_LEN];
assert_eq!(nonce.len(), 12);
let tag: Tag = [0u8; TAG_LEN];
assert_eq!(tag.len(), 16);
}
#[test]
fn test_invalid_nonce_length_is_structurally_unreachable() {
const _: () = {
let _: Nonce = [0u8; NONCE_LEN];
};
assert_eq!(NONCE_LEN, 12);
let err = AeadError::InvalidNonceLength;
assert!(matches!(err, AeadError::InvalidNonceLength));
}
}