use aes_gcm::{
Aes256Gcm,
aead::{Aead, AeadCore, KeyInit, OsRng, generic_array::GenericArray},
};
use zeroize::Zeroizing;
use crate::crypto::CryptoError;
pub struct EncryptedSecret {
pub ciphertext: Vec<u8>,
pub nonce: [u8; 12],
}
pub fn encrypt(key: &[u8; 32], plaintext: &[u8]) -> Result<EncryptedSecret, CryptoError> {
let cipher = Aes256Gcm::new_from_slice(key).expect("key is &[u8; 32]: length is always valid");
let nonce_ga = Aes256Gcm::generate_nonce(&mut OsRng);
let ciphertext = cipher
.encrypt(&nonce_ga, plaintext)
.map_err(|_| CryptoError::EncryptionFailed)?;
let nonce: [u8; 12] = nonce_ga
.as_slice()
.try_into()
.expect("generate_nonce always returns exactly 12 bytes");
Ok(EncryptedSecret { ciphertext, nonce })
}
pub fn decrypt(
key: &[u8; 32],
ciphertext: &[u8],
nonce: &[u8],
) -> Result<Zeroizing<Vec<u8>>, CryptoError> {
if nonce.len() != 12 {
return Err(CryptoError::InvalidNonce);
}
let cipher = Aes256Gcm::new_from_slice(key).expect("key is &[u8; 32]: length is always valid");
let nonce = GenericArray::from_slice(nonce);
let plaintext = cipher
.decrypt(nonce, ciphertext)
.map_err(|_| CryptoError::DecryptionFailed)?;
Ok(Zeroizing::new(plaintext))
}
#[cfg(test)]
mod tests {
use super::*;
const KEY_A: [u8; 32] = [0u8; 32];
const KEY_B: [u8; 32] = [1u8; 32];
#[test]
fn encrypt_produces_ciphertext() {
let result = encrypt(&KEY_A, b"hello").expect("encrypt must succeed");
assert_ne!(
result.ciphertext,
b"hello".to_vec(),
"ciphertext must differ from plaintext"
);
assert!(
!result.ciphertext.windows(5).any(|w| w == b"hello"),
"plaintext must not appear as a substring of ciphertext"
);
assert_eq!(result.nonce.len(), 12, "nonce must be exactly 12 bytes");
}
#[test]
fn decrypt_round_trips() {
let secret = encrypt(&KEY_A, b"hello").expect("encrypt must succeed");
let plaintext = decrypt(&KEY_A, &secret.ciphertext, &secret.nonce)
.expect("decrypt must succeed with matching key");
assert_eq!(plaintext.as_slice(), b"hello");
}
#[test]
fn wrong_key_fails() {
let secret = encrypt(&KEY_A, b"hello").expect("encrypt must succeed");
let result = decrypt(&KEY_B, &secret.ciphertext, &secret.nonce);
assert!(
matches!(result, Err(CryptoError::DecryptionFailed)),
"wrong key must return DecryptionFailed, got: {:?}",
result
);
}
#[test]
fn tampered_ciphertext_fails() {
let mut secret = encrypt(&KEY_A, b"hello").expect("encrypt must succeed");
secret.ciphertext[0] ^= 0xFF; let result = decrypt(&KEY_A, &secret.ciphertext, &secret.nonce);
assert!(
matches!(result, Err(CryptoError::DecryptionFailed)),
"tampered ciphertext must return DecryptionFailed, got: {:?}",
result
);
}
#[test]
fn empty_plaintext_succeeds() {
let secret = encrypt(&KEY_A, b"").expect("encrypt must succeed for empty plaintext");
let plaintext = decrypt(&KEY_A, &secret.ciphertext, &secret.nonce)
.expect("decrypt must succeed for empty plaintext");
assert_eq!(plaintext.as_slice(), b"");
}
#[test]
fn nonce_uniqueness() {
let s1 = encrypt(&KEY_A, b"hello").expect("first encrypt must succeed");
let s2 = encrypt(&KEY_A, b"hello").expect("second encrypt must succeed");
assert_ne!(
s1.nonce, s2.nonce,
"each encrypt call must generate a unique nonce"
);
}
#[test]
fn invalid_nonce_length() {
let secret = encrypt(&KEY_A, b"hello").expect("encrypt must succeed");
let result_short = decrypt(&KEY_A, &secret.ciphertext, &[0u8; 11]);
assert!(
matches!(result_short, Err(CryptoError::InvalidNonce)),
"11-byte nonce must return InvalidNonce, got: {:?}",
result_short
);
let result_long = decrypt(&KEY_A, &secret.ciphertext, &[0u8; 13]);
assert!(
matches!(result_long, Err(CryptoError::InvalidNonce)),
"13-byte nonce must return InvalidNonce, got: {:?}",
result_long
);
}
#[test]
fn zeroize_plaintext() {
use std::mem::ManuallyDrop;
use zeroize::Zeroize;
let secret = encrypt(&KEY_A, b"secret data").expect("encrypt must succeed");
let mut decrypted = ManuallyDrop::new(
decrypt(&KEY_A, &secret.ciphertext, &secret.nonce).expect("decrypt must succeed"),
);
let ptr = decrypted.as_ptr();
let original_len = decrypted.len();
(*decrypted).zeroize();
let zeroed = unsafe { std::slice::from_raw_parts(ptr, original_len) };
assert!(
zeroed.iter().all(|&b| b == 0),
"plaintext bytes must be zeroed after zeroize"
);
unsafe { ManuallyDrop::drop(&mut decrypted) };
}
}