Hybrid KEM-DEM
AegisQ implements a Hybrid KEM-DEM architecture. ML-KEM cannot encrypt large payloads directly — it only produces a 32-byte shared secret. AegisQ pairs it with AES-256-GCM as the Data Encapsulation Mechanism (DEM).
The Hybrid Approach
Section titled “The Hybrid Approach”- ML-KEM (KEM): Generates a 32-byte shared secret, quantum-safe
- AES-256-GCM (DEM): Uses that 32-byte secret as the symmetric key to encrypt the actual payload with authenticated encryption (confidentiality + integrity)
AES-256-GCM Properties
Section titled “AES-256-GCM Properties”| Property | Value |
|---|---|
| Key size | 256 bits (32 bytes) — from ML-KEM shared secret |
| Nonce (IV) | 96 bits (12 bytes) — random per operation via OsRng |
| Authentication Tag | 128 bits (16 bytes) |
| Security | IND-CPA + INT-CTXT (authenticated encryption) |
Once ML-KEM generates the 32-byte shared secret K, AegisQ feeds it directly into AES-256-GCM as the symmetric encryption key. No additional KDF is needed — the 32-byte output of ML-KEM is already uniformly random and the correct size for AES-256.
Nonce Management — Critical Rule
Section titled “Nonce Management — Critical Rule”Transit Package Assembly
Section titled “Transit Package Assembly”The hybrid.rs module in aegisq-core is responsible for assembling and parsing the transit package.
Transit Package Structure
Section titled “Transit Package Structure”The final encrypted_package byte array has this fixed structure:
[ ML-KEM Capsule (var) | AES Nonce (12 bytes) | AES Auth Tag (16 bytes) | Ciphertext (var) ]Where ML-KEM Capsule size depends on the security level:
- ML-KEM-512: 768 bytes
- ML-KEM-768: 1088 bytes
- ML-KEM-1024: 1568 bytes
Encrypt Flow
Section titled “Encrypt Flow”- Call
kem::encapsulate(public_key, level)→ result containing capsule and shared secret - Generate random 12-byte nonce via
OsRng - Call
aes_gcm::encrypt(key=shared_secret, nonce, plaintext)→(tag, ciphertext) - Zeroize
shared_secretimmediately - Assemble and return:
capsule || nonce || tag || ciphertext
Decrypt Flow
Section titled “Decrypt Flow”- Split the transit package by known offsets (capsule_size, then 12, 16, rest)
- Call
kem::decapsulate(capsule, secret_key, level)→shared_secret_32B - Call
aes_gcm::decrypt(key=shared_secret, nonce, tag, ciphertext)→plaintextorErr - Zeroize
shared_secretimmediately - If tag verification fails → return
Err(AegisQError::DecryptionFailed)
Error Behavior Contrast
Section titled “Error Behavior Contrast”| Scenario | ML-KEM Decaps | AES-GCM Decrypt |
|---|---|---|
| Invalid contents, correct capsule size | Returns pseudorandom K | Tag verification is expected to fail |
| Wrong key/capsule size or too-short package | Structural error (InvalidParameterError) | Not reached |
| Correct capsule, wrong AES key | N/A (key derived from capsule) | Error: DecryptionError |
| Auth tag mismatch (tampered payload) | N/A | Error: DecryptionError |
| Correct everything | Returns shared secret | Returns plaintext |
Rust Internal API
Section titled “Rust Internal API”use aegisq_core::{hybrid, kem::SecurityLevel};
// Hybrid encrypt: ML-KEM encaps + AES-256-GCMlet encrypted_package: Vec<u8> = hybrid::encrypt( recipient_public_key, plaintext, SecurityLevel::MlKem768,)?;
// Hybrid decrypt: ML-KEM decaps + AES-256-GCM verify + decryptlet plaintext: Vec<u8> = hybrid::decrypt( &encrypted_package, secret_key, SecurityLevel::MlKem768,)?;