#![deny(unsafe_code)]
#![deny(missing_docs)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::panic)]
use crate::hybrid::kem_hybrid::{self, EncapsulatedKey, HybridKemPublicKey, HybridKemSecretKey};
use crate::log_crypto_operation_error;
use crate::primitives::aead::aes_gcm::AesGcm256;
use crate::primitives::aead::{AeadCipher, NONCE_LEN, TAG_LEN};
use crate::primitives::kdf::hkdf::hkdf;
use crate::unified_api::logging::op;
use thiserror::Error;
use zeroize::Zeroizing;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum HybridEncryptionError {
#[error("KEM error: {0}")]
KemError(String),
#[error("Encryption error: {0}")]
EncryptionError(String),
#[error("Decryption error: {0}")]
DecryptionError(String),
#[error("Key derivation error: {0}")]
KdfError(String),
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Key length error: expected {expected}, got {actual}")]
KeyLengthError {
expected: usize,
actual: usize,
},
}
#[derive(Debug, Clone)]
pub struct HybridCiphertext {
kem_ciphertext: Vec<u8>,
ecdh_ephemeral_pk: Vec<u8>,
symmetric_ciphertext: Vec<u8>,
nonce: Vec<u8>,
tag: Vec<u8>,
}
impl HybridCiphertext {
#[must_use]
pub fn new(
kem_ciphertext: Vec<u8>,
ecdh_ephemeral_pk: Vec<u8>,
symmetric_ciphertext: Vec<u8>,
nonce: Vec<u8>,
tag: Vec<u8>,
) -> Self {
Self { kem_ciphertext, ecdh_ephemeral_pk, symmetric_ciphertext, nonce, tag }
}
#[must_use]
pub fn kem_ciphertext(&self) -> &[u8] {
&self.kem_ciphertext
}
#[must_use]
pub fn ecdh_ephemeral_pk(&self) -> &[u8] {
&self.ecdh_ephemeral_pk
}
#[must_use]
pub fn symmetric_ciphertext(&self) -> &[u8] {
&self.symmetric_ciphertext
}
#[must_use]
pub fn nonce(&self) -> &[u8] {
&self.nonce
}
#[must_use]
pub fn tag(&self) -> &[u8] {
&self.tag
}
pub fn kem_ciphertext_mut(&mut self) -> &mut Vec<u8> {
&mut self.kem_ciphertext
}
pub fn ecdh_ephemeral_pk_mut(&mut self) -> &mut Vec<u8> {
&mut self.ecdh_ephemeral_pk
}
pub fn symmetric_ciphertext_mut(&mut self) -> &mut Vec<u8> {
&mut self.symmetric_ciphertext
}
pub fn nonce_mut(&mut self) -> &mut Vec<u8> {
&mut self.nonce
}
pub fn tag_mut(&mut self) -> &mut Vec<u8> {
&mut self.tag
}
}
#[derive(Debug, Clone)]
pub struct HybridEncryptionContext {
pub info: Vec<u8>,
pub aad: Vec<u8>,
}
impl Default for HybridEncryptionContext {
fn default() -> Self {
Self { info: crate::types::domains::HYBRID_ENCRYPTION_INFO.to_vec(), aad: vec![] }
}
}
pub fn derive_encryption_key(
shared_secret: &[u8],
context: &HybridEncryptionContext,
) -> Result<Zeroizing<[u8; 32]>, HybridEncryptionError> {
if shared_secret.len() != 32 && shared_secret.len() != 64 {
return Err(HybridEncryptionError::KdfError(
"Shared secret must be 32 bytes (ML-KEM) or 64 bytes (hybrid)".to_string(),
));
}
if context.info.len() > u32::MAX as usize || context.aad.len() > u32::MAX as usize {
return Err(HybridEncryptionError::KdfError(
"HKDF info or AAD field exceeds 2^32 bytes".to_string(),
));
}
let info_len = context.info.len();
let aad_len = context.aad.len();
let total = 4usize.saturating_add(info_len).saturating_add(4).saturating_add(aad_len);
let mut info = Vec::with_capacity(total);
let info_len_u32 = u32::try_from(info_len).map_err(|_e| {
HybridEncryptionError::KdfError("HKDF info field exceeds 2^32 bytes".to_string())
})?;
let aad_len_u32 = u32::try_from(aad_len).map_err(|_e| {
HybridEncryptionError::KdfError("HKDF AAD field exceeds 2^32 bytes".to_string())
})?;
info.extend_from_slice(&info_len_u32.to_be_bytes());
info.extend_from_slice(&context.info);
info.extend_from_slice(&aad_len_u32.to_be_bytes());
info.extend_from_slice(&context.aad);
let hkdf_result = hkdf(shared_secret, None, Some(&info), 32).map_err(|_e| {
log_crypto_operation_error!(op::HYBRID_DERIVE_KEY, "HKDF failed");
HybridEncryptionError::KdfError("KDF failed".to_string())
})?;
let mut key = Zeroizing::new([0u8; 32]);
key.copy_from_slice(hkdf_result.key());
Ok(key)
}
pub fn encrypt_hybrid(
hybrid_pk: &HybridKemPublicKey,
plaintext: &[u8],
context: Option<&HybridEncryptionContext>,
) -> Result<HybridCiphertext, HybridEncryptionError> {
let default_ctx = HybridEncryptionContext::default();
let ctx = context.unwrap_or(&default_ctx);
let opaque_kem = || HybridEncryptionError::KemError("encapsulation failed".to_string());
let opaque_enc = || HybridEncryptionError::EncryptionError("encryption failed".to_string());
let encapsulated = kem_hybrid::encapsulate(hybrid_pk).map_err(|_e| {
log_crypto_operation_error!(op::HYBRID_ENCRYPT, "KEM encapsulation failed");
opaque_kem()
})?;
let encryption_key = derive_encryption_key(encapsulated.shared_secret(), ctx)?;
let nonce_bytes = AesGcm256::generate_nonce();
let cipher = AesGcm256::new(&*encryption_key).map_err(|_e| {
log_crypto_operation_error!(op::HYBRID_ENCRYPT, "AES-256 init failed");
opaque_enc()
})?;
let (ciphertext, tag) =
cipher.encrypt(&nonce_bytes, plaintext, Some(&ctx.aad)).map_err(|_e| {
log_crypto_operation_error!(op::HYBRID_ENCRYPT, "AES-GCM seal failed");
opaque_enc()
})?;
Ok(HybridCiphertext::new(
encapsulated.ml_kem_ct().to_vec(),
encapsulated.ecdh_pk().to_vec(),
ciphertext,
nonce_bytes.to_vec(),
tag.to_vec(),
))
}
pub fn decrypt_hybrid(
hybrid_sk: &HybridKemSecretKey,
ciphertext: &HybridCiphertext,
context: Option<&HybridEncryptionContext>,
) -> Result<Zeroizing<Vec<u8>>, HybridEncryptionError> {
let default_ctx = HybridEncryptionContext::default();
let ctx = context.unwrap_or(&default_ctx);
let expected_ct_size = hybrid_sk.security_level().ciphertext_size();
if ciphertext.kem_ciphertext().len() != expected_ct_size {
return Err(HybridEncryptionError::InvalidInput(format!(
"{} ciphertext must be {} bytes, got {}",
hybrid_sk.security_level().name(),
expected_ct_size,
ciphertext.kem_ciphertext().len()
)));
}
if ciphertext.ecdh_ephemeral_pk().len() != 32 {
return Err(HybridEncryptionError::InvalidInput(
"X25519 ephemeral public key must be 32 bytes".to_string(),
));
}
if ciphertext.nonce().len() != 12 {
return Err(HybridEncryptionError::InvalidInput("Nonce must be 12 bytes".to_string()));
}
if ciphertext.tag().len() != 16 {
return Err(HybridEncryptionError::InvalidInput("Tag must be 16 bytes".to_string()));
}
let encapsulated = EncapsulatedKey::new(
ciphertext.kem_ciphertext().to_vec(),
ciphertext.ecdh_ephemeral_pk().to_vec(),
Zeroizing::new(vec![]), );
let opaque = || HybridEncryptionError::DecryptionError("decryption failed".to_string());
let shared_secret = kem_hybrid::decapsulate(hybrid_sk, &encapsulated).map_err(|_e| {
log_crypto_operation_error!(op::HYBRID_DECRYPT, "KEM decapsulation failed");
opaque()
})?;
let encryption_key = derive_encryption_key(&shared_secret, ctx).map_err(|_e| {
log_crypto_operation_error!(op::HYBRID_DECRYPT, "HKDF key derivation failed");
opaque()
})?;
let nonce_bytes: [u8; NONCE_LEN] = ciphertext.nonce().try_into().map_err(|_e| {
log_crypto_operation_error!(op::HYBRID_DECRYPT, "nonce length != 12");
opaque()
})?;
let tag_bytes: [u8; TAG_LEN] = ciphertext.tag().try_into().map_err(|_e| {
log_crypto_operation_error!(op::HYBRID_DECRYPT, "tag length != 16");
opaque()
})?;
let cipher = AesGcm256::new(&*encryption_key).map_err(|_e| {
log_crypto_operation_error!(op::HYBRID_DECRYPT, "AES-256 init failed");
opaque()
})?;
let plaintext = cipher
.decrypt(&nonce_bytes, ciphertext.symmetric_ciphertext(), &tag_bytes, Some(&ctx.aad))
.map_err(|_aead_err| {
log_crypto_operation_error!(op::HYBRID_DECRYPT, "AEAD authentication failed");
opaque()
})?;
Ok(plaintext)
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
#[test]
fn test_key_derivation_properties_are_deterministic_and_unique() {
let shared_secret = vec![1u8; 32];
let context1 =
HybridEncryptionContext { info: b"Context1".to_vec(), aad: b"AAD1".to_vec() };
let context2 =
HybridEncryptionContext { info: b"Context2".to_vec(), aad: b"AAD2".to_vec() };
let key1 = derive_encryption_key(&shared_secret, &context1).unwrap();
let key2 = derive_encryption_key(&shared_secret, &context2).unwrap();
assert_ne!(key1, key2, "Different contexts should produce different keys");
let key1_again = derive_encryption_key(&shared_secret, &context1).unwrap();
assert_eq!(key1, key1_again, "Key derivation should be deterministic");
let invalid_secret = vec![1u8; 31]; let result = derive_encryption_key(&invalid_secret, &context1);
assert!(result.is_err(), "Should reject invalid shared secret length");
let hybrid_secret = vec![1u8; 64];
let result = derive_encryption_key(&hybrid_secret, &context1);
assert!(result.is_ok(), "Should accept 64-byte hybrid shared secret");
}
#[test]
fn test_kem_ecdh_hybrid_encryption_roundtrip() {
let (hybrid_pk, hybrid_sk) = kem_hybrid::generate_keypair().unwrap();
let plaintext = b"Hello, true hybrid encryption!";
let context = HybridEncryptionContext::default();
let ct = encrypt_hybrid(&hybrid_pk, plaintext, Some(&context)).unwrap();
assert_eq!(ct.kem_ciphertext().len(), 1088, "ML-KEM-768 ciphertext should be 1088 bytes");
assert_eq!(ct.ecdh_ephemeral_pk().len(), 32, "X25519 ephemeral PK should be 32 bytes");
assert!(!ct.symmetric_ciphertext().is_empty(), "Symmetric ciphertext should not be empty");
assert_eq!(ct.nonce().len(), 12, "Nonce should be 12 bytes");
assert_eq!(ct.tag().len(), 16, "Tag should be 16 bytes");
let decrypted = decrypt_hybrid(&hybrid_sk, &ct, Some(&context)).unwrap();
assert_eq!(
decrypted.as_slice(),
plaintext.as_slice(),
"Decrypted text should match original"
);
}
#[test]
fn test_kem_ecdh_hybrid_encryption_with_aad_succeeds() {
let (hybrid_pk, hybrid_sk) = kem_hybrid::generate_keypair().unwrap();
let plaintext = b"Secret message with AAD";
let aad = b"Additional authenticated data";
let context = HybridEncryptionContext {
info: crate::types::domains::HYBRID_ENCRYPTION_INFO.to_vec(),
aad: aad.to_vec(),
};
let ct = encrypt_hybrid(&hybrid_pk, plaintext, Some(&context)).unwrap();
let decrypted = decrypt_hybrid(&hybrid_sk, &ct, Some(&context)).unwrap();
assert_eq!(
decrypted.as_slice(),
plaintext.as_slice(),
"Decryption with correct AAD should succeed"
);
let wrong_context = HybridEncryptionContext {
info: crate::types::domains::HYBRID_ENCRYPTION_INFO.to_vec(),
aad: b"Wrong AAD".to_vec(),
};
let result = decrypt_hybrid(&hybrid_sk, &ct, Some(&wrong_context));
assert!(result.is_err(), "Decryption with wrong AAD should fail");
}
#[test]
fn test_kem_ecdh_hybrid_encryption_different_ciphertexts_for_same_plaintext_succeeds() {
let (hybrid_pk, hybrid_sk) = kem_hybrid::generate_keypair().unwrap();
let plaintext = b"Same plaintext, different ciphertexts";
let ct1 = encrypt_hybrid(&hybrid_pk, plaintext, None).unwrap();
let ct2 = encrypt_hybrid(&hybrid_pk, plaintext, None).unwrap();
assert_ne!(ct1.kem_ciphertext(), ct2.kem_ciphertext());
assert_ne!(ct1.ecdh_ephemeral_pk(), ct2.ecdh_ephemeral_pk());
let dec1 = decrypt_hybrid(&hybrid_sk, &ct1, None).unwrap();
let dec2 = decrypt_hybrid(&hybrid_sk, &ct2, None).unwrap();
assert_eq!(dec1.as_slice(), plaintext.as_slice());
assert_eq!(dec2.as_slice(), plaintext.as_slice());
}
#[test]
fn test_error_display_variants_produce_nonempty_strings_fails() {
let kem_err = HybridEncryptionError::KemError("kem fail".to_string());
assert!(kem_err.to_string().contains("kem fail"));
let enc_err = HybridEncryptionError::EncryptionError("enc fail".to_string());
assert!(enc_err.to_string().contains("enc fail"));
let dec_err = HybridEncryptionError::DecryptionError("dec fail".to_string());
assert!(dec_err.to_string().contains("dec fail"));
let kdf_err = HybridEncryptionError::KdfError("kdf fail".to_string());
assert!(kdf_err.to_string().contains("kdf fail"));
let input_err = HybridEncryptionError::InvalidInput("bad input".to_string());
assert!(input_err.to_string().contains("bad input"));
let key_err = HybridEncryptionError::KeyLengthError { expected: 32, actual: 16 };
let msg = key_err.to_string();
assert!(msg.contains("32"));
assert!(msg.contains("16"));
}
#[test]
fn test_error_eq_and_clone_work_correctly_fails() {
let err1 = HybridEncryptionError::KemError("test".to_string());
let err2 = err1.clone();
assert_eq!(err1, err2);
let err3 = HybridEncryptionError::KemError("different".to_string());
assert_ne!(err1, err3);
}
#[test]
fn test_hybrid_ciphertext_clone_debug_work_correctly_succeeds() {
let ct = HybridCiphertext::new(
vec![1, 2, 3],
vec![4, 5],
vec![6, 7, 8],
vec![9; 12],
vec![10; 16],
);
let ct2 = ct.clone();
assert_eq!(ct.kem_ciphertext(), ct2.kem_ciphertext());
assert_eq!(ct.ecdh_ephemeral_pk(), ct2.ecdh_ephemeral_pk());
let debug_str = format!("{:?}", ct);
assert!(debug_str.contains("HybridCiphertext"));
}
#[test]
fn test_encryption_context_default_sets_expected_fields_succeeds() {
let ctx = HybridEncryptionContext::default();
assert_eq!(ctx.info, crate::types::domains::HYBRID_ENCRYPTION_INFO);
assert!(ctx.aad.is_empty());
}
#[test]
fn test_encryption_context_clone_debug_work_correctly_succeeds() {
let ctx =
HybridEncryptionContext { info: b"custom-info".to_vec(), aad: b"custom-aad".to_vec() };
let ctx2 = ctx.clone();
assert_eq!(ctx.info, ctx2.info);
assert_eq!(ctx.aad, ctx2.aad);
let debug_str = format!("{:?}", ctx);
assert!(debug_str.contains("HybridEncryptionContext"));
}
#[test]
fn test_derive_key_invalid_lengths_fail_fails() {
let ctx = HybridEncryptionContext::default();
assert!(derive_encryption_key(&[0u8; 31], &ctx).is_err());
assert!(derive_encryption_key(&[0u8; 65], &ctx).is_err());
assert!(derive_encryption_key(&[0u8; 1], &ctx).is_err());
assert!(derive_encryption_key(&[], &ctx).is_err());
assert!(derive_encryption_key(&[0u8; 33], &ctx).is_err());
}
#[test]
fn test_derive_key_different_secrets_produce_different_keys_succeeds() {
let ctx = HybridEncryptionContext::default();
let secret_a = [1u8; 32];
let secret_b = [2u8; 32];
let key_a = derive_encryption_key(&secret_a, &ctx).unwrap();
let key_b = derive_encryption_key(&secret_b, &ctx).unwrap();
assert_ne!(key_a, key_b);
}
#[test]
fn test_derive_key_64_byte_hybrid_secret_succeeds() {
let ctx = HybridEncryptionContext::default();
let secret = [42u8; 64];
let key = derive_encryption_key(&secret, &ctx).unwrap();
assert_eq!(key.len(), 32);
let key2 = derive_encryption_key(&secret, &ctx).unwrap();
assert_eq!(key, key2);
}
#[test]
fn test_decrypt_hybrid_invalid_kem_ct_length_fails() {
let (_hybrid_pk, hybrid_sk) = kem_hybrid::generate_keypair().unwrap();
let ct = HybridCiphertext::new(
vec![1u8; 500],
vec![2u8; 32],
vec![3u8; 64],
vec![4u8; 12],
vec![5u8; 16],
); let result = decrypt_hybrid(&hybrid_sk, &ct, None);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, HybridEncryptionError::InvalidInput(_)));
}
#[test]
fn test_decrypt_hybrid_invalid_ecdh_pk_length_fails() {
let (_hybrid_pk, hybrid_sk) = kem_hybrid::generate_keypair().unwrap();
let ct = HybridCiphertext::new(
vec![1u8; 1088],
vec![2u8; 16],
vec![3u8; 64],
vec![4u8; 12],
vec![5u8; 16],
); let result = decrypt_hybrid(&hybrid_sk, &ct, None);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, HybridEncryptionError::InvalidInput(_)));
}
#[test]
fn test_decrypt_hybrid_invalid_nonce_length_fails() {
let (_hybrid_pk, hybrid_sk) = kem_hybrid::generate_keypair().unwrap();
let ct = HybridCiphertext::new(
vec![1u8; 1088],
vec![2u8; 32],
vec![3u8; 64],
vec![4u8; 8],
vec![5u8; 16],
); let result = decrypt_hybrid(&hybrid_sk, &ct, None);
assert!(result.is_err());
}
#[test]
fn test_decrypt_hybrid_invalid_tag_length_fails() {
let (_hybrid_pk, hybrid_sk) = kem_hybrid::generate_keypair().unwrap();
let ct = HybridCiphertext::new(
vec![1u8; 1088],
vec![2u8; 32],
vec![3u8; 64],
vec![4u8; 12],
vec![5u8; 10],
); let result = decrypt_hybrid(&hybrid_sk, &ct, None);
assert!(result.is_err());
}
#[test]
fn test_decrypt_hybrid_tampered_ciphertext_fails() {
let (hybrid_pk, hybrid_sk) = kem_hybrid::generate_keypair().unwrap();
let plaintext = b"Test message for tampering";
let ct = encrypt_hybrid(&hybrid_pk, plaintext, None).unwrap();
let mut tampered = ct.clone();
if let Some(byte) = tampered.symmetric_ciphertext_mut().first_mut() {
*byte ^= 0xFF;
}
assert!(decrypt_hybrid(&hybrid_sk, &tampered, None).is_err());
let mut tampered_tag = ct;
if let Some(byte) = tampered_tag.tag.first_mut() {
*byte ^= 0xFF;
}
assert!(decrypt_hybrid(&hybrid_sk, &tampered_tag, None).is_err());
}
#[test]
fn test_encrypt_hybrid_empty_plaintext_succeeds() {
let (hybrid_pk, hybrid_sk) = kem_hybrid::generate_keypair().unwrap();
let plaintext = b"";
let ct = encrypt_hybrid(&hybrid_pk, plaintext, None).unwrap();
let decrypted = decrypt_hybrid(&hybrid_sk, &ct, None).unwrap();
assert!(decrypted.is_empty());
}
#[test]
fn test_encrypt_hybrid_large_plaintext_succeeds() {
let (hybrid_pk, hybrid_sk) = kem_hybrid::generate_keypair().unwrap();
let plaintext = vec![0xABu8; 10_000];
let ct = encrypt_hybrid(&hybrid_pk, &plaintext, None).unwrap();
let decrypted = decrypt_hybrid(&hybrid_sk, &ct, None).unwrap();
assert_eq!(decrypted.as_slice(), plaintext.as_slice());
}
#[test]
fn test_encrypt_hybrid_with_none_context_uses_default_succeeds() {
let (hybrid_pk, hybrid_sk) = kem_hybrid::generate_keypair().unwrap();
let plaintext = b"Default context test";
let ct = encrypt_hybrid(&hybrid_pk, plaintext, None).unwrap();
let decrypted = decrypt_hybrid(&hybrid_sk, &ct, None).unwrap();
assert_eq!(decrypted.as_slice(), plaintext.as_slice());
}
#[test]
fn test_decrypt_hybrid_with_none_context_uses_default_succeeds() {
let (hybrid_pk, hybrid_sk) = kem_hybrid::generate_keypair().unwrap();
let default_ctx = HybridEncryptionContext::default();
let ct = encrypt_hybrid(&hybrid_pk, b"ctx test", Some(&default_ctx)).unwrap();
let decrypted = decrypt_hybrid(&hybrid_sk, &ct, None).unwrap();
assert_eq!(decrypted.as_slice(), b"ctx test");
}
#[test]
fn test_derive_encryption_key_with_64_byte_secret_succeeds() {
let secret = [0xAA; 64]; let ctx = HybridEncryptionContext::default();
let key = derive_encryption_key(&secret, &ctx).unwrap();
assert_eq!(key.len(), 32);
}
#[test]
fn test_derive_encryption_key_invalid_length_fails() {
let ctx = HybridEncryptionContext::default();
let result = derive_encryption_key(&[0u8; 16], &ctx);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("32 bytes"));
}
#[test]
fn test_derive_encryption_key_is_deterministic() {
let secret = [0xBB; 32];
let ctx = HybridEncryptionContext::default();
let k1 = derive_encryption_key(&secret, &ctx).unwrap();
let k2 = derive_encryption_key(&secret, &ctx).unwrap();
assert_eq!(k1, k2, "Same inputs must produce same key");
}
#[test]
fn test_derive_encryption_key_different_contexts_produce_different_keys_succeeds() {
let secret = [0xCC; 32];
let ctx1 = HybridEncryptionContext { info: b"ctx1".to_vec(), aad: vec![] };
let ctx2 = HybridEncryptionContext { info: b"ctx2".to_vec(), aad: vec![] };
let k1 = derive_encryption_key(&secret, &ctx1).unwrap();
let k2 = derive_encryption_key(&secret, &ctx2).unwrap();
assert_ne!(k1, k2, "Different contexts must produce different keys");
}
#[test]
fn test_derive_encryption_key_with_aad_succeeds() {
let secret = [0xDD; 32];
let ctx_no_aad = HybridEncryptionContext::default();
let ctx_with_aad = HybridEncryptionContext {
info: crate::types::domains::HYBRID_ENCRYPTION_INFO.to_vec(),
aad: b"extra-data".to_vec(),
};
let k1 = derive_encryption_key(&secret, &ctx_no_aad).unwrap();
let k2 = derive_encryption_key(&secret, &ctx_with_aad).unwrap();
assert_ne!(k1, k2, "Different AAD must produce different keys");
}
#[test]
fn test_encrypt_hybrid_custom_context_succeeds() {
let (hybrid_pk, hybrid_sk) = kem_hybrid::generate_keypair().unwrap();
let ctx = HybridEncryptionContext {
info: b"custom-app-info".to_vec(),
aad: b"custom-aad".to_vec(),
};
let plaintext = b"Custom context encryption";
let ct = encrypt_hybrid(&hybrid_pk, plaintext, Some(&ctx)).unwrap();
let decrypted = decrypt_hybrid(&hybrid_sk, &ct, Some(&ctx)).unwrap();
assert_eq!(decrypted.as_slice(), plaintext.as_slice());
}
#[test]
fn test_decrypt_hybrid_wrong_context_fails() {
let (hybrid_pk, hybrid_sk) = kem_hybrid::generate_keypair().unwrap();
let ctx1 = HybridEncryptionContext { info: b"context-1".to_vec(), aad: b"aad-1".to_vec() };
let ctx2 = HybridEncryptionContext { info: b"context-2".to_vec(), aad: b"aad-2".to_vec() };
let ct = encrypt_hybrid(&hybrid_pk, b"test", Some(&ctx1)).unwrap();
let result = decrypt_hybrid(&hybrid_sk, &ct, Some(&ctx2));
assert!(result.is_err(), "Wrong context must fail decryption");
}
#[test]
fn test_hybrid_encryption_error_display_all_variants_are_nonempty_fails() {
let errors = vec![
HybridEncryptionError::KemError("kem".into()),
HybridEncryptionError::EncryptionError("enc".into()),
HybridEncryptionError::DecryptionError("dec".into()),
HybridEncryptionError::InvalidInput("inp".into()),
HybridEncryptionError::KdfError("kdf".into()),
HybridEncryptionError::KeyLengthError { expected: 32, actual: 16 },
];
for err in &errors {
let msg = format!("{}", err);
assert!(!msg.is_empty());
}
}
}