#![deny(unsafe_code)]
#![deny(missing_docs)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::panic)]
use crate::hybrid::kem_hybrid::{self, 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, 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
}
#[cfg(feature = "test-utils")]
pub fn kem_ciphertext_mut(&mut self) -> &mut Vec<u8> {
&mut self.kem_ciphertext
}
#[cfg(feature = "test-utils")]
pub fn ecdh_ephemeral_pk_mut(&mut self) -> &mut Vec<u8> {
&mut self.ecdh_ephemeral_pk
}
#[cfg(feature = "test-utils")]
pub fn symmetric_ciphertext_mut(&mut self) -> &mut Vec<u8> {
&mut self.symmetric_ciphertext
}
#[cfg(feature = "test-utils")]
pub fn nonce_mut(&mut self) -> &mut Vec<u8> {
&mut self.nonce
}
#[cfg(feature = "test-utils")]
pub fn tag_mut(&mut self) -> &mut Vec<u8> {
&mut self.tag
}
}
#[derive(Clone)]
pub struct HybridEncryptionContext {
info: Vec<u8>,
pub aad: Vec<u8>,
}
impl std::fmt::Debug for HybridEncryptionContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HybridEncryptionContext")
.field("info_len", &self.info.len())
.field("aad_len", &self.aad.len())
.finish()
}
}
impl Default for HybridEncryptionContext {
fn default() -> Self {
Self { info: crate::types::domains::HYBRID_ENCRYPTION_INFO.to_vec(), aad: vec![] }
}
}
impl HybridEncryptionContext {
pub const MAX_AAD_LEN: usize = 64 * 1024;
#[must_use]
pub fn with_aad(aad: Vec<u8>) -> Self {
Self { info: crate::types::domains::HYBRID_ENCRYPTION_INFO.to_vec(), aad }
}
#[must_use]
pub fn with_explicit_info(info: &'static [u8], aad: Vec<u8>) -> Self {
Self { info: info.to_vec(), aad }
}
#[must_use]
pub fn info(&self) -> &[u8] {
&self.info
}
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct DerivationBinding<'a> {
pub recipient_static_pk: &'a [u8],
pub recipient_ml_kem_pk: &'a [u8],
pub ephemeral_pk: &'a [u8],
pub kem_ciphertext: &'a [u8],
}
impl<'a> DerivationBinding<'a> {
#[must_use]
pub const fn empty() -> Self {
Self {
recipient_static_pk: &[],
recipient_ml_kem_pk: &[],
ephemeral_pk: &[],
kem_ciphertext: &[],
}
}
}
pub fn derive_encryption_key(
shared_secret: &[u8],
context: &HybridEncryptionContext,
binding: &DerivationBinding<'_>,
) -> 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(),
));
}
let oversize = context.info.len() > u32::MAX as usize
|| context.aad.len() > u32::MAX as usize
|| binding.recipient_static_pk.len() > u32::MAX as usize
|| binding.recipient_ml_kem_pk.len() > u32::MAX as usize
|| binding.ephemeral_pk.len() > u32::MAX as usize
|| binding.kem_ciphertext.len() > u32::MAX as usize;
if oversize {
return Err(HybridEncryptionError::KdfError(
"HKDF info / aad / binding field exceeds 2^32 bytes".to_string(),
));
}
let segments: [&[u8]; 6] = [
&context.info,
&context.aad,
binding.recipient_static_pk,
binding.recipient_ml_kem_pk,
binding.ephemeral_pk,
binding.kem_ciphertext,
];
let total: usize = segments
.iter()
.try_fold(0usize, |acc, s| acc.checked_add(4)?.checked_add(s.len()))
.ok_or_else(|| {
HybridEncryptionError::KdfError("HKDF info payload size overflow".to_string())
})?;
let mut info = Vec::with_capacity(total);
for segment in segments {
let len_u32 = u32::try_from(segment.len()).map_err(|_e| {
HybridEncryptionError::KdfError("HKDF info segment exceeds 2^32 bytes".to_string())
})?;
info.extend_from_slice(&len_u32.to_be_bytes());
info.extend_from_slice(segment);
}
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.expose_secret());
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());
if ctx.aad.len() > HybridEncryptionContext::MAX_AAD_LEN {
log_crypto_operation_error!(op::HYBRID_ENCRYPT, "AAD exceeds MAX_AAD_LEN");
return Err(opaque_enc());
}
let encapsulated = kem_hybrid::encapsulate(hybrid_pk).map_err(|_e| {
log_crypto_operation_error!(op::HYBRID_ENCRYPT, "KEM encapsulation failed");
opaque_kem()
})?;
let binding = DerivationBinding {
recipient_static_pk: hybrid_pk.ecdh_pk(),
recipient_ml_kem_pk: hybrid_pk.ml_kem_pk(),
ephemeral_pk: encapsulated.ecdh_pk(),
kem_ciphertext: encapsulated.ml_kem_ct(),
};
let encryption_key = derive_encryption_key(encapsulated.expose_secret(), ctx, &binding)?;
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 opaque = || HybridEncryptionError::DecryptionError("decryption failed".to_string());
if ctx.aad.len() > HybridEncryptionContext::MAX_AAD_LEN {
log_crypto_operation_error!(op::HYBRID_DECRYPT, "AAD exceeds MAX_AAD_LEN");
return Err(opaque());
}
let expected_ct_size = hybrid_sk.security_level().ciphertext_size();
if ciphertext.kem_ciphertext().len() != expected_ct_size {
log_crypto_operation_error!(op::HYBRID_DECRYPT, "KEM ciphertext length mismatch");
return Err(opaque());
}
if ciphertext.ecdh_ephemeral_pk().len() != 32 {
log_crypto_operation_error!(op::HYBRID_DECRYPT, "ECDH ephemeral PK length invalid");
return Err(opaque());
}
if ciphertext.nonce().len() != 12 {
log_crypto_operation_error!(op::HYBRID_DECRYPT, "AES-GCM nonce length invalid");
return Err(opaque());
}
if ciphertext.tag().len() != 16 {
log_crypto_operation_error!(op::HYBRID_DECRYPT, "AES-GCM tag length invalid");
return Err(opaque());
}
let shared_secret = kem_hybrid::decapsulate_from_parts(
hybrid_sk,
ciphertext.kem_ciphertext(),
ciphertext.ecdh_ephemeral_pk(),
)
.map_err(|_e| {
log_crypto_operation_error!(op::HYBRID_DECRYPT, "KEM decapsulation failed");
opaque()
})?;
let recipient_static_pk = hybrid_sk.ecdh_public_key_bytes();
let recipient_ml_kem_pk = hybrid_sk.ml_kem_pk_bytes();
let binding = DerivationBinding {
recipient_static_pk: &recipient_static_pk,
recipient_ml_kem_pk: &recipient_ml_kem_pk,
ephemeral_pk: ciphertext.ecdh_ephemeral_pk(),
kem_ciphertext: ciphertext.kem_ciphertext(),
};
let encryption_key = derive_encryption_key(shared_secret.expose_secret(), ctx, &binding)
.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)]
#[expect(clippy::unwrap_used, reason = "Tests use unwrap for simplicity")]
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, &DerivationBinding::empty()).unwrap();
let key2 =
derive_encryption_key(&shared_secret, &context2, &DerivationBinding::empty()).unwrap();
assert_ne!(key1, key2, "Different contexts should produce different keys");
let key1_again =
derive_encryption_key(&shared_secret, &context1, &DerivationBinding::empty()).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, &DerivationBinding::empty());
assert!(result.is_err(), "Should reject invalid shared secret length");
let hybrid_secret = vec![1u8; 64];
let result = derive_encryption_key(&hybrid_secret, &context1, &DerivationBinding::empty());
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_clone_round_trips() {
let err1 = HybridEncryptionError::KemError("test".to_string());
let err2 = err1.clone();
assert_eq!(err1.to_string(), err2.to_string());
assert!(matches!(err2, HybridEncryptionError::KemError(_)));
let err3 = HybridEncryptionError::KemError("different".to_string());
assert_ne!(err1.to_string(), err3.to_string());
}
#[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, &DerivationBinding::empty()).is_err());
assert!(derive_encryption_key(&[0u8; 65], &ctx, &DerivationBinding::empty()).is_err());
assert!(derive_encryption_key(&[0u8; 1], &ctx, &DerivationBinding::empty()).is_err());
assert!(derive_encryption_key(&[], &ctx, &DerivationBinding::empty()).is_err());
assert!(derive_encryption_key(&[0u8; 33], &ctx, &DerivationBinding::empty()).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, &DerivationBinding::empty()).unwrap();
let key_b = derive_encryption_key(&secret_b, &ctx, &DerivationBinding::empty()).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, &DerivationBinding::empty()).unwrap();
assert_eq!(key.len(), 32);
let key2 = derive_encryption_key(&secret, &ctx, &DerivationBinding::empty()).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::DecryptionError(_)));
}
#[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::DecryptionError(_)));
}
#[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());
}
#[cfg(feature = "test-utils")]
#[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, &DerivationBinding::empty()).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, &DerivationBinding::empty());
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, &DerivationBinding::empty()).unwrap();
let k2 = derive_encryption_key(&secret, &ctx, &DerivationBinding::empty()).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, &DerivationBinding::empty()).unwrap();
let k2 = derive_encryption_key(&secret, &ctx2, &DerivationBinding::empty()).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, &DerivationBinding::empty()).unwrap();
let k2 =
derive_encryption_key(&secret, &ctx_with_aad, &DerivationBinding::empty()).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());
}
}
}