use super::*;
use crate::encryption::key_chain::StaticKeyChain;
use crate::table::block::BlockType;
const TEST_KEY: [u8; 32] = [0x42; 32];
const TEST_KEY_OTHER: [u8; 32] = [0x55; 32];
fn id() -> BlockIdentity {
BlockIdentity {
table_id: 0x1234_5678_9ABC_DEF0,
block_type: BlockType::Data,
dict_id: 0,
window_log: 0,
}
}
fn ctx() -> EncryptionContext {
EncryptionContext::v1(0, SuiteId::Aes256Gcm, 0, 0)
}
fn chain() -> StaticKeyChain {
StaticKeyChain::new().with_key(0, TEST_KEY)
}
#[test]
fn parse_metadata_is_key_free_and_matches_seal() {
let plaintext = b"forensic payload bytes";
let sealed = encrypt_block(plaintext, &id(), &ctx(), &chain()).unwrap();
let meta = parse_encrypted_block_metadata(&sealed).unwrap();
assert_eq!(meta.format_version, 1);
assert_eq!(meta.key_epoch, 0);
assert_eq!(meta.suite_id, SuiteId::Aes256Gcm);
assert_eq!(meta.block_type, u8::from(BlockType::Data));
assert_eq!(meta.compression_type, 0);
assert_eq!(meta.dict_id, 0);
assert_eq!(meta.window_log, 0);
assert!(meta.ciphertext_len > 0, "body must carry ciphertext");
let chacha_ctx = EncryptionContext::v1(0, SuiteId::ChaCha20Poly1305, 0, 0);
let sealed_cc = encrypt_block(plaintext, &id(), &chacha_ctx, &chain()).unwrap();
let meta_cc = parse_encrypted_block_metadata(&sealed_cc).unwrap();
assert_eq!(meta_cc.suite_id, SuiteId::ChaCha20Poly1305);
assert!(parse_encrypted_block_metadata(b"not a frame").is_err());
assert!(parse_encrypted_block_metadata(&sealed[..10]).is_err());
}
#[test]
fn parse_metadata_rejects_truncated_body() {
let sealed = encrypt_block(b"forensic payload bytes", &id(), &ctx(), &chain()).unwrap();
let cut = 8 + METADATA_PAYLOAD_LEN_V1 as usize + 8;
assert!(
sealed.len() > cut,
"test setup: sealed block must extend past the body header",
);
#[expect(
clippy::expect_used,
reason = "test asserts the truncated frame is rejected"
)]
let err = parse_encrypted_block_metadata(&sealed[..cut])
.expect_err("truncated body must be rejected");
assert!(
matches!(err, DecryptError::MalformedBodyFrame(_)),
"expected MalformedBodyFrame for a truncated body, got {err:?}",
);
}
#[test]
fn reconstruct_aad_matches_seal_with_correct_table_id() {
let sealed = encrypt_block(b"forensic payload bytes", &id(), &ctx(), &chain()).unwrap();
let expected = build(&ctx(), &id());
let got = reconstruct_block_aad(&sealed, id().table_id).unwrap();
assert_eq!(got.len(), AAD_LEN);
assert_eq!(
got, expected,
"reconstructed AAD must match the sealing AAD"
);
let other = reconstruct_block_aad(&sealed, id().table_id ^ 1).unwrap();
assert_ne!(got, other, "table_id must affect the reconstructed AAD");
assert!(reconstruct_block_aad(b"not a frame", 0).is_err());
}
#[test]
fn roundtrip_aes_recovers_plaintext() {
let plaintext = b"the quick brown fox";
let sealed = encrypt_block(plaintext, &id(), &ctx(), &chain()).unwrap();
let recovered = decrypt_block(&sealed, &id(), &chain()).unwrap();
assert_eq!(&recovered.plaintext[..], plaintext);
assert_eq!(recovered.compression_type, 0);
assert_eq!(recovered.dict_id, 0);
assert_eq!(recovered.window_log, 0);
}
#[test]
fn roundtrip_chacha_recovers_plaintext() {
let plaintext = b"the quick brown fox";
let chacha_ctx = EncryptionContext::v1(0, SuiteId::ChaCha20Poly1305, 0, 0);
let sealed = encrypt_block(plaintext, &id(), &chacha_ctx, &chain()).unwrap();
let recovered = decrypt_block(&sealed, &id(), &chain()).unwrap();
assert_eq!(&recovered.plaintext[..], plaintext);
}
#[test]
fn wrong_key_in_chain_surfaces_aead_failure() {
let plaintext = b"the quick brown fox";
let sealed = encrypt_block(plaintext, &id(), &ctx(), &chain()).unwrap();
let wrong = StaticKeyChain::new().with_key(0, TEST_KEY_OTHER);
let err = decrypt_block(&sealed, &id(), &wrong).unwrap_err();
assert!(
matches!(err, DecryptError::AeadVerificationFailed),
"expected AeadVerificationFailed, got {err:?}",
);
}
#[test]
fn missing_key_epoch_surfaces_unknown_key_epoch() {
let plaintext = b"the quick brown fox";
let sealed = encrypt_block(plaintext, &id(), &ctx(), &chain()).unwrap();
let no_epoch_zero = StaticKeyChain::new().with_key(1, TEST_KEY);
let err = decrypt_block(&sealed, &id(), &no_epoch_zero).unwrap_err();
assert!(
matches!(err, DecryptError::UnknownKeyEpoch { key_epoch: 0 }),
"expected UnknownKeyEpoch {{ key_epoch: 0 }}, got {err:?}",
);
}
#[test]
fn cross_identity_substitution_surfaces_aead_failure() {
let plaintext = b"the quick brown fox";
let sealed = encrypt_block(plaintext, &id(), &ctx(), &chain()).unwrap();
let mut wrong_id = id();
wrong_id.table_id ^= 0x1; let err = decrypt_block(&sealed, &wrong_id, &chain()).unwrap_err();
assert!(matches!(err, DecryptError::AeadVerificationFailed));
}
#[test]
fn trailing_bytes_after_body_are_rejected() {
let plaintext = b"the quick brown fox";
let mut sealed = encrypt_block(plaintext, &id(), &ctx(), &chain()).unwrap();
assert!(decrypt_block(&sealed, &id(), &chain()).is_ok());
sealed.extend_from_slice(&[0xAB, 0xCD, 0xEF, 0x01]);
let err = decrypt_block(&sealed, &id(), &chain()).unwrap_err();
assert!(
matches!(err, DecryptError::MalformedBodyFrame(_)),
"expected MalformedBodyFrame for trailing bytes, got {err:?}",
);
}
#[test]
fn truncated_input_surfaces_malformed_metadata() {
let plaintext = b"the quick brown fox";
let sealed = encrypt_block(plaintext, &id(), &ctx(), &chain()).unwrap();
let truncated = &sealed[..6];
let err = decrypt_block(truncated, &id(), &chain()).unwrap_err();
assert!(matches!(err, DecryptError::MalformedMetadataFrame(_)));
}
#[test]
fn encrypt_block_rejects_empty_plaintext() {
let err = encrypt_block(&[], &id(), &ctx(), &chain()).unwrap_err();
assert!(
matches!(err, crate::Error::Encrypt(_)),
"expected Error::Encrypt for empty plaintext, got {err:?}",
);
}
#[test]
fn encrypt_block_rejects_unknown_block_flags_bit() {
let mut c = ctx();
c.block_flags = 0x10; let err = encrypt_block(b"payload", &id(), &c, &chain()).unwrap_err();
assert!(
matches!(err, crate::Error::Encrypt(_)),
"expected Error::Encrypt for unknown BlockFlags bit, got {err:?}",
);
}
#[test]
fn invalid_window_log_surfaces_malformed_metadata() {
let plaintext = b"the quick brown fox";
let mut sealed = encrypt_block(plaintext, &id(), &ctx(), &chain()).unwrap();
sealed[17] = 9; let err = decrypt_block(&sealed, &id(), &chain()).unwrap_err();
assert!(
matches!(err, DecryptError::MalformedMetadataFrame(_)),
"expected MalformedMetadataFrame for WindowLog=9, got {err:?}",
);
}
#[test]
fn oversized_body_payload_len_rejected_before_alloc() {
let plaintext = b"the quick brown fox";
let mut sealed = encrypt_block(plaintext, &id(), &ctx(), &chain()).unwrap();
let metadata_frame_len = 8 + METADATA_PAYLOAD_LEN_V1 as usize;
let body_payload_len_at = metadata_frame_len + 4;
sealed[body_payload_len_at..body_payload_len_at + 4].copy_from_slice(&u32::MAX.to_le_bytes());
let err = decrypt_block(&sealed, &id(), &chain()).unwrap_err();
assert!(
matches!(err, DecryptError::MalformedBodyFrame(_)),
"expected MalformedBodyFrame for oversized BodyFrame PayloadLen, got {err:?}",
);
}
#[test]
fn unknown_block_flags_bit_rejected_before_aead() {
let plaintext = b"the quick brown fox";
let mut sealed = encrypt_block(plaintext, &id(), &ctx(), &chain()).unwrap();
const BLOCK_FLAGS_AT: usize = 8 + 10;
sealed[BLOCK_FLAGS_AT] |= 0x10;
let err = decrypt_block(&sealed, &id(), &chain()).unwrap_err();
assert!(
matches!(err, DecryptError::MalformedMetadataFrame(_)),
"expected MalformedMetadataFrame for unknown BlockFlags bit, got {err:?}",
);
}