#![cfg(feature = "encryption")]
use super::aad::{AAD_LEN, SuiteId};
use super::error::DecryptError;
pub const TAG_LEN: usize = 16;
pub fn encrypt_in_place(
suite: SuiteId,
key: &[u8; 32],
nonce: &[u8],
aad: &[u8; AAD_LEN],
plaintext: &mut [u8],
) -> crate::Result<[u8; TAG_LEN]> {
use aes_gcm::aead::{AeadInOut, KeyInit, Nonce};
match suite {
SuiteId::Aes256Gcm => {
let cipher = aes_gcm::Aes256Gcm::new(key.into());
let nonce = Nonce::<aes_gcm::Aes256Gcm>::try_from(nonce)
.map_err(|_| crate::Error::Encrypt("AES-256-GCM nonce length mismatch"))?;
let tag = cipher
.encrypt_inout_detached(&nonce, aad, plaintext.into())
.map_err(|_| crate::Error::Encrypt("AES-256-GCM encryption failed"))?;
let mut out = [0u8; TAG_LEN];
out.copy_from_slice(&tag);
Ok(out)
}
SuiteId::ChaCha20Poly1305 => {
let cipher = chacha20poly1305::ChaCha20Poly1305::new(key.into());
let nonce = Nonce::<chacha20poly1305::ChaCha20Poly1305>::try_from(nonce)
.map_err(|_| crate::Error::Encrypt("ChaCha20-Poly1305 nonce length mismatch"))?;
let tag = cipher
.encrypt_inout_detached(&nonce, aad, plaintext.into())
.map_err(|_| crate::Error::Encrypt("ChaCha20-Poly1305 encryption failed"))?;
let mut out = [0u8; TAG_LEN];
out.copy_from_slice(&tag);
Ok(out)
}
}
}
pub fn decrypt_in_place(
suite: SuiteId,
key: &[u8; 32],
nonce: &[u8],
aad: &[u8; AAD_LEN],
tag: &[u8; TAG_LEN],
ciphertext: &mut [u8],
) -> Result<(), DecryptError> {
use aes_gcm::aead::{AeadInOut, KeyInit, Nonce, Tag};
match suite {
SuiteId::Aes256Gcm => {
let cipher = aes_gcm::Aes256Gcm::new(key.into());
let nonce = Nonce::<aes_gcm::Aes256Gcm>::try_from(nonce)
.map_err(|_| DecryptError::MalformedBodyFrame("nonce length != suite nonce_len"))?;
let aead_tag = Tag::<aes_gcm::Aes256Gcm>::try_from(&tag[..])
.map_err(|_| DecryptError::MalformedBodyFrame("tag length != 16"))?;
cipher
.decrypt_inout_detached(&nonce, aad, ciphertext.into(), &aead_tag)
.map_err(|_| DecryptError::AeadVerificationFailed)
}
SuiteId::ChaCha20Poly1305 => {
let cipher = chacha20poly1305::ChaCha20Poly1305::new(key.into());
let nonce = Nonce::<chacha20poly1305::ChaCha20Poly1305>::try_from(nonce)
.map_err(|_| DecryptError::MalformedBodyFrame("nonce length != suite nonce_len"))?;
let aead_tag = Tag::<chacha20poly1305::ChaCha20Poly1305>::try_from(&tag[..])
.map_err(|_| DecryptError::MalformedBodyFrame("tag length != 16"))?;
cipher
.decrypt_inout_detached(&nonce, aad, ciphertext.into(), &aead_tag)
.map_err(|_| DecryptError::AeadVerificationFailed)
}
}
}
#[cfg(test)]
#[expect(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
reason = "test code"
)]
mod tests {
use super::*;
use crate::encryption::aad::{BlockType, EncryptionContext, build};
use crate::table::block::BlockIdentity as TableBlockIdentity;
const TEST_KEY: [u8; 32] = [0x42; 32];
const TEST_KEY_OTHER: [u8; 32] = [0x55; 32];
fn test_aad(suite: SuiteId) -> [u8; AAD_LEN] {
let ctx = EncryptionContext::v1(
7, suite, 0, 0, );
let identity = TableBlockIdentity::for_test(0xAB, BlockType::Data);
build(&ctx, &identity)
}
fn test_nonce() -> [u8; 12] {
[
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C,
]
}
fn roundtrip_for(suite: SuiteId) {
let plaintext = b"hello AAD-bound world";
let aad = test_aad(suite);
let nonce = test_nonce();
let mut buf = plaintext.to_vec();
let tag = encrypt_in_place(suite, &TEST_KEY, &nonce, &aad, &mut buf)
.expect("encrypt should succeed");
assert_ne!(&buf[..], &plaintext[..]);
decrypt_in_place(suite, &TEST_KEY, &nonce, &aad, &tag, &mut buf)
.expect("decrypt should succeed with matching key + aad");
assert_eq!(&buf[..], &plaintext[..]);
}
#[test]
fn aes256gcm_roundtrip_recovers_plaintext() {
roundtrip_for(SuiteId::Aes256Gcm);
}
#[test]
fn chacha20poly1305_roundtrip_recovers_plaintext() {
roundtrip_for(SuiteId::ChaCha20Poly1305);
}
fn wrong_key_fails_for(suite: SuiteId) {
let aad = test_aad(suite);
let nonce = test_nonce();
let mut buf = b"abc".to_vec();
let tag = encrypt_in_place(suite, &TEST_KEY, &nonce, &aad, &mut buf).unwrap();
let err = decrypt_in_place(suite, &TEST_KEY_OTHER, &nonce, &aad, &tag, &mut buf)
.expect_err("wrong key must fail");
assert!(matches!(err, DecryptError::AeadVerificationFailed));
}
#[test]
fn aes256gcm_wrong_key_fails_with_aead_verification_failed() {
wrong_key_fails_for(SuiteId::Aes256Gcm);
}
#[test]
fn chacha20poly1305_wrong_key_fails_with_aead_verification_failed() {
wrong_key_fails_for(SuiteId::ChaCha20Poly1305);
}
fn tampered_aad_fails_for(suite: SuiteId) {
let aad = test_aad(suite);
let nonce = test_nonce();
let mut buf = b"abc".to_vec();
let tag = encrypt_in_place(suite, &TEST_KEY, &nonce, &aad, &mut buf).unwrap();
let mut bad_aad = aad;
bad_aad[5] ^= 0x01;
let err = decrypt_in_place(suite, &TEST_KEY, &nonce, &bad_aad, &tag, &mut buf)
.expect_err("AAD tamper must fail");
assert!(matches!(err, DecryptError::AeadVerificationFailed));
}
#[test]
fn aes256gcm_tampered_aad_byte_fails_verification() {
tampered_aad_fails_for(SuiteId::Aes256Gcm);
}
#[test]
fn chacha20poly1305_tampered_aad_byte_fails_verification() {
tampered_aad_fails_for(SuiteId::ChaCha20Poly1305);
}
fn tampered_ciphertext_fails_for(suite: SuiteId) {
let aad = test_aad(suite);
let nonce = test_nonce();
let mut buf = b"abcdef".to_vec();
let tag = encrypt_in_place(suite, &TEST_KEY, &nonce, &aad, &mut buf).unwrap();
buf[0] ^= 0x01;
let err = decrypt_in_place(suite, &TEST_KEY, &nonce, &aad, &tag, &mut buf)
.expect_err("ciphertext tamper must fail");
assert!(matches!(err, DecryptError::AeadVerificationFailed));
}
#[test]
fn aes256gcm_tampered_ciphertext_fails_verification() {
tampered_ciphertext_fails_for(SuiteId::Aes256Gcm);
}
#[test]
fn chacha20poly1305_tampered_ciphertext_fails_verification() {
tampered_ciphertext_fails_for(SuiteId::ChaCha20Poly1305);
}
#[test]
fn wrong_nonce_length_returns_malformed_body_frame_error() {
let aad = test_aad(SuiteId::Aes256Gcm);
let mut buf = b"abc".to_vec();
let short_nonce = [0u8; 11];
let bogus_tag = [0u8; TAG_LEN];
let err = decrypt_in_place(
SuiteId::Aes256Gcm,
&TEST_KEY,
&short_nonce,
&aad,
&bogus_tag,
&mut buf,
)
.expect_err("wrong nonce length must fail");
assert!(matches!(err, DecryptError::MalformedBodyFrame(_)));
}
#[test]
fn cross_suite_decrypt_fails_verification() {
let aes_aad = test_aad(SuiteId::Aes256Gcm);
let nonce = test_nonce();
let mut buf = b"abc".to_vec();
let tag =
encrypt_in_place(SuiteId::Aes256Gcm, &TEST_KEY, &nonce, &aes_aad, &mut buf).unwrap();
let err = decrypt_in_place(
SuiteId::ChaCha20Poly1305,
&TEST_KEY,
&nonce,
&aes_aad,
&tag,
&mut buf,
)
.expect_err("cross-suite decrypt must fail");
assert!(matches!(err, DecryptError::AeadVerificationFailed));
}
}