Channels
qhermes-channels provides post-quantum encrypted channels. ML-KEM-768 handles key encapsulation; ChaCha20-Poly1305 handles symmetric encryption. No heap, no I/O, no unsafe code.
Rust only. No Python bindings.
Constants
EK_SIZE: 1184 // ML-KEM-768 encapsulation key (bytes)
DK_SEED_SIZE: 64 // decapsulation key seed (bytes)
CT_SIZE: 1088 // ML-KEM-768 ciphertext (bytes)
KEY_SIZE: 32 // ChaCha20-Poly1305 session key (bytes)
NONCE_SIZE: 12 // ChaCha20-Poly1305 nonce (bytes)
TAG_SIZE: 16 // ChaCha20-Poly1305 authentication tag (bytes) Key derivation
derive_kem_keypair
pub fn derive_kem_keypair(dsa_seed: &[u8; 32]) -> Result<KemKeyPair, ChannelError> Derives a ML-KEM-768 keypair from a 32-byte DSA seed. Passing the same seed used in IdentityIsland::derive binds a KEM identity to a DSA identity under the same root secret, with domain separation via b"QHermes-KEM-768-v1".
Returns a KemKeyPair with encapsulation_key (public, shareable) and decapsulation_key (secret, zeroed on drop).
use qhermes_channels::derive_kem_keypair;
let dsa_seed: [u8; 32] = /* your root seed */;
let kp = derive_kem_keypair(&dsa_seed)?;
let ek_bytes = kp.encapsulation_key.as_bytes(); // send to sender
let dk_seed = kp.decapsulation_key.as_seed(); // keep secret KEM operations
encapsulate
pub fn encapsulate(
ek: &KemEncapsulationKey,
context: &[u8],
rng: &mut impl rand_core::CryptoRng,
) -> Result<(KemCiphertext, SessionKey), ChannelError> Encapsulates a fresh shared secret for ek. Returns the ciphertext to send to the receiver and a SessionKey derived from the shared secret via HKDF-SHA3-512. context is caller-supplied domain separation material.
decapsulate
pub fn decapsulate(
dk: &KemDecapsulationKey,
ct: &KemCiphertext,
context: &[u8],
) -> Result<SessionKey, ChannelError> Decapsulates the shared secret from ct. Returns a SessionKey equivalent to the one returned by encapsulate. context must be identical to the value passed to encapsulate.
use qhermes_channels::{encapsulate, decapsulate, KemEncapsulationKey};
// Sender side
let ek = KemEncapsulationKey::from_bytes(&ek_bytes)?;
let (ct, session_key) = encapsulate(&ek, b"my-context", &mut rng)?;
// send ct.as_bytes() over the wire
// Receiver side
let dk = kp.decapsulation_key;
let ct = KemCiphertext::from_bytes(ct_bytes);
let session_key = decapsulate(&dk, &ct, b"my-context")?; SessionKey
A ChaCha20-Poly1305 session key derived from the KEM shared secret. Zeroed on drop.
seal
pub fn seal(
&self,
nonce: &[u8; NONCE_SIZE],
plaintext: &[u8],
aad: &[u8],
out: &mut [u8],
) -> Result<usize, ChannelError> Encrypts plaintext with ChaCha20-Poly1305. out must have at least plaintext.len() + TAG_SIZE bytes. Returns the number of bytes written. The nonce must not be reused with the same key.
open
pub fn open(
&self,
nonce: &[u8; NONCE_SIZE],
ciphertext: &[u8],
aad: &[u8],
out: &mut [u8],
) -> Result<usize, ChannelError> Decrypts and authenticates a ciphertext produced by seal. Returns AuthenticationFailed if the tag does not verify.
use qhermes_channels::{NONCE_SIZE, TAG_SIZE};
let nonce: [u8; NONCE_SIZE] = /* unique per message */;
let plaintext = b"hello";
let mut out = vec![0u8; plaintext.len() + TAG_SIZE];
let written = session_key.seal(&nonce, plaintext, b"", &mut out)?;
// Decrypt
let mut plain_out = vec![0u8; written - TAG_SIZE];
session_key.open(&nonce, &out[..written], b"", &mut plain_out)?; Errors
pub enum ChannelError {
HkdfExpandFailed, // HKDF produced fewer bytes than requested
KeyDecodingFailed, // invalid ML-KEM-768 encoding
OutputBufferTooSmall, // out buffer too small
InputBufferTooSmall, // ciphertext shorter than TAG_SIZE
AuthenticationFailed, // AEAD tag verification failed
}