use rayon::prelude::*;
use zeroize::Zeroize;
use std::time::Duration;
use crate::entropy::{self, EntropySnapshot};
use crate::error::{KkError, Result};
use crate::kdf;
use crate::kk_mix::kk_hash;
use crate::temporal::{self, TemporalCommitment, TemporalProof};
const CHUNK_SIZE: usize = 4096;
#[derive(Clone)]
pub struct KkPacket {
pub ciphertext: Vec<u8>,
pub entropy_snapshot: EntropySnapshot,
pub commitment: TemporalCommitment,
}
impl KkPacket {
pub fn to_bytes(&self) -> Vec<u8> {
let ct_len = self.ciphertext.len() as u32;
let snap_bytes = self.entropy_snapshot.to_bytes();
let commit_bytes = self.commitment.to_bytes();
let mut out =
Vec::with_capacity(4 + self.ciphertext.len() + snap_bytes.len() + commit_bytes.len());
out.extend_from_slice(&ct_len.to_le_bytes());
out.extend_from_slice(&self.ciphertext);
out.extend_from_slice(&snap_bytes);
out.extend_from_slice(&commit_bytes);
out
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
if data.len() < 4 {
return Err(KkError::InvalidPacket("packet too short".into()));
}
let ct_len = u32::from_le_bytes(
data[..4]
.try_into()
.map_err(|_| KkError::InvalidPacket("bad length".into()))?,
) as usize;
let expected_min = 4 + ct_len + 48 + 32; if data.len() < expected_min {
return Err(KkError::InvalidPacket(format!(
"packet too short: expected at least {expected_min}, got {}",
data.len()
)));
}
let ciphertext = data[4..4 + ct_len].to_vec();
let snapshot = EntropySnapshot::from_bytes(&data[4 + ct_len..4 + ct_len + 48])?;
let commitment = TemporalCommitment::from_bytes(&data[4 + ct_len + 48..])?;
Ok(Self {
ciphertext,
entropy_snapshot: snapshot,
commitment,
})
}
}
#[derive(Clone)]
pub struct KkSealedMessage {
pub ciphertext: Vec<u8>,
pub commitment: TemporalCommitment,
}
impl KkSealedMessage {
pub fn to_bytes(&self) -> Vec<u8> {
let ct_len = self.ciphertext.len() as u32;
let commit_bytes = self.commitment.to_bytes();
let mut out = Vec::with_capacity(4 + self.ciphertext.len() + commit_bytes.len());
out.extend_from_slice(&ct_len.to_le_bytes());
out.extend_from_slice(&self.ciphertext);
out.extend_from_slice(&commit_bytes);
out
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
if data.len() < 4 {
return Err(KkError::InvalidPacket("sealed message too short".into()));
}
let ct_len = u32::from_le_bytes(
data[..4]
.try_into()
.map_err(|_| KkError::InvalidPacket("bad length".into()))?,
) as usize;
let expected_min = 4 + ct_len + 32;
if data.len() < expected_min {
return Err(KkError::InvalidPacket(format!(
"sealed message too short: expected at least {expected_min}, got {}",
data.len()
)));
}
let ciphertext = data[4..4 + ct_len].to_vec();
let commitment = TemporalCommitment::from_bytes(&data[4 + ct_len..])?;
Ok(Self {
ciphertext,
commitment,
})
}
}
pub fn encode(shared_secret: &[u8], plaintext: &[u8]) -> Result<KkPacket> {
if plaintext.is_empty() {
return Err(KkError::EmptyInput);
}
let snapshot = entropy::gather()?;
let ciphertext = xor_with_keystream(shared_secret, &snapshot, plaintext)?;
let commitment = temporal::commit(shared_secret, &snapshot, &ciphertext)?;
Ok(KkPacket {
ciphertext,
entropy_snapshot: snapshot,
commitment,
})
}
pub fn decode(shared_secret: &[u8], packet: &KkPacket) -> Result<Vec<u8>> {
temporal::verify(
shared_secret,
&packet.entropy_snapshot,
&packet.ciphertext,
&packet.commitment,
)?;
xor_with_keystream(shared_secret, &packet.entropy_snapshot, &packet.ciphertext)
}
pub fn encode_split(
shared_secret: &[u8],
plaintext: &[u8],
) -> Result<(KkSealedMessage, EntropySnapshot)> {
if plaintext.is_empty() {
return Err(KkError::EmptyInput);
}
let snapshot = entropy::gather()?;
let ciphertext = xor_with_keystream(shared_secret, &snapshot, plaintext)?;
let commitment = temporal::commit(shared_secret, &snapshot, &ciphertext)?;
let sealed = KkSealedMessage {
ciphertext,
commitment,
};
Ok((sealed, snapshot))
}
pub fn decode_split(
shared_secret: &[u8],
sealed: &KkSealedMessage,
epsilon: &EntropySnapshot,
) -> Result<Vec<u8>> {
temporal::verify(
shared_secret,
epsilon,
&sealed.ciphertext,
&sealed.commitment,
)?;
xor_with_keystream(shared_secret, epsilon, &sealed.ciphertext)
}
#[derive(Clone)]
pub struct KkBoundPacket {
pub ciphertext: Vec<u8>,
pub entropy_snapshot: EntropySnapshot,
pub proof: TemporalProof,
}
impl KkBoundPacket {
pub fn to_bytes(&self) -> Vec<u8> {
let ct_len = self.ciphertext.len() as u32;
let snap_bytes = self.entropy_snapshot.to_bytes();
let proof_bytes = self.proof.to_bytes();
let mut out =
Vec::with_capacity(4 + self.ciphertext.len() + snap_bytes.len() + proof_bytes.len());
out.extend_from_slice(&ct_len.to_le_bytes());
out.extend_from_slice(&self.ciphertext);
out.extend_from_slice(&snap_bytes);
out.extend_from_slice(&proof_bytes);
out
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
if data.len() < 4 {
return Err(KkError::InvalidPacket("bound packet too short".into()));
}
let ct_len = u32::from_le_bytes(
data[..4]
.try_into()
.map_err(|_| KkError::InvalidPacket("bad length".into()))?,
) as usize;
let expected_min = 4 + ct_len + 48 + TemporalProof::BYTES;
if data.len() < expected_min {
return Err(KkError::InvalidPacket(format!(
"bound packet too short: expected at least {expected_min}, got {}",
data.len()
)));
}
let ciphertext = data[4..4 + ct_len].to_vec();
let snapshot = EntropySnapshot::from_bytes(&data[4 + ct_len..4 + ct_len + 48])?;
let proof = TemporalProof::from_bytes(&data[4 + ct_len + 48..])?;
Ok(Self {
ciphertext,
entropy_snapshot: snapshot,
proof,
})
}
}
pub fn encode_bound(
shared_secret: &[u8],
plaintext: &[u8],
verifier_nonce: &[u8; 32],
prev_mac: &[u8; 32],
) -> Result<KkBoundPacket> {
if plaintext.is_empty() {
return Err(KkError::EmptyInput);
}
let snapshot = entropy::gather()?;
let ciphertext = xor_with_keystream(shared_secret, &snapshot, plaintext)?;
let proof = temporal::commit_bound(
shared_secret,
&snapshot,
&ciphertext,
verifier_nonce,
prev_mac,
)?;
Ok(KkBoundPacket {
ciphertext,
entropy_snapshot: snapshot,
proof,
})
}
pub fn decode_bound(
shared_secret: &[u8],
packet: &KkBoundPacket,
expected_nonce: &[u8; 32],
max_drift: Duration,
) -> Result<Vec<u8>> {
temporal::verify_bound(
shared_secret,
&packet.entropy_snapshot,
&packet.ciphertext,
&packet.proof,
expected_nonce,
max_drift,
)?;
xor_with_keystream(shared_secret, &packet.entropy_snapshot, &packet.ciphertext)
}
#[derive(Clone)]
pub struct KkAeadPacket {
pub aad: Vec<u8>,
pub ciphertext: Vec<u8>,
pub entropy_snapshot: EntropySnapshot,
pub commitment: TemporalCommitment,
}
impl KkAeadPacket {
pub fn to_bytes(&self) -> Vec<u8> {
let aad_len = self.aad.len() as u32;
let ct_len = self.ciphertext.len() as u32;
let snap_bytes = self.entropy_snapshot.to_bytes();
let commit_bytes = self.commitment.to_bytes();
let mut out = Vec::with_capacity(
4 + self.aad.len() + 4 + self.ciphertext.len() + snap_bytes.len() + commit_bytes.len(),
);
out.extend_from_slice(&aad_len.to_le_bytes());
out.extend_from_slice(&self.aad);
out.extend_from_slice(&ct_len.to_le_bytes());
out.extend_from_slice(&self.ciphertext);
out.extend_from_slice(&snap_bytes);
out.extend_from_slice(&commit_bytes);
out
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
if data.len() < 8 {
return Err(KkError::InvalidPacket("AEAD packet too short".into()));
}
let aad_len = u32::from_le_bytes(
data[..4]
.try_into()
.map_err(|_| KkError::InvalidPacket("bad aad length".into()))?,
) as usize;
if data.len() < 4 + aad_len + 4 {
return Err(KkError::InvalidPacket(
"AEAD packet truncated at ct_len".into(),
));
}
let aad = data[4..4 + aad_len].to_vec();
let ct_offset = 4 + aad_len;
let ct_len = u32::from_le_bytes(
data[ct_offset..ct_offset + 4]
.try_into()
.map_err(|_| KkError::InvalidPacket("bad ct length".into()))?,
) as usize;
let expected_min = ct_offset + 4 + ct_len + 48 + 32;
if data.len() < expected_min {
return Err(KkError::InvalidPacket(format!(
"AEAD packet too short: expected at least {expected_min}, got {}",
data.len()
)));
}
let ct_start = ct_offset + 4;
let ciphertext = data[ct_start..ct_start + ct_len].to_vec();
let snap_start = ct_start + ct_len;
let snapshot = EntropySnapshot::from_bytes(&data[snap_start..snap_start + 48])?;
let commitment =
TemporalCommitment::from_bytes(&data[snap_start + 48..snap_start + 48 + 32])?;
Ok(Self {
aad,
ciphertext,
entropy_snapshot: snapshot,
commitment,
})
}
}
pub fn encode_aead(shared_secret: &[u8], plaintext: &[u8], aad: &[u8]) -> Result<KkAeadPacket> {
if plaintext.is_empty() {
return Err(KkError::EmptyInput);
}
let snapshot = entropy::gather()?;
let ciphertext = xor_with_keystream(shared_secret, &snapshot, plaintext)?;
let commitment = temporal::commit_aead(shared_secret, &snapshot, &ciphertext, aad)?;
Ok(KkAeadPacket {
aad: aad.to_vec(),
ciphertext,
entropy_snapshot: snapshot,
commitment,
})
}
pub fn decode_aead(shared_secret: &[u8], packet: &KkAeadPacket) -> Result<Vec<u8>> {
temporal::verify_aead(
shared_secret,
&packet.entropy_snapshot,
&packet.ciphertext,
&packet.aad,
&packet.commitment,
)?;
xor_with_keystream(shared_secret, &packet.entropy_snapshot, &packet.ciphertext)
}
#[doc(hidden)]
pub fn encode_with_snapshot(
shared_secret: &[u8],
plaintext: &[u8],
snapshot: EntropySnapshot,
) -> Result<KkPacket> {
if plaintext.is_empty() {
return Err(KkError::EmptyInput);
}
let ciphertext = xor_with_keystream(shared_secret, &snapshot, plaintext)?;
let commitment = temporal::commit(shared_secret, &snapshot, &ciphertext)?;
Ok(KkPacket {
ciphertext,
entropy_snapshot: snapshot,
commitment,
})
}
#[doc(hidden)]
pub fn encode_aead_with_snapshot(
shared_secret: &[u8],
plaintext: &[u8],
aad: &[u8],
snapshot: EntropySnapshot,
) -> Result<KkAeadPacket> {
if plaintext.is_empty() {
return Err(KkError::EmptyInput);
}
let ciphertext = xor_with_keystream(shared_secret, &snapshot, plaintext)?;
let commitment = temporal::commit_aead(shared_secret, &snapshot, &ciphertext, aad)?;
Ok(KkAeadPacket {
aad: aad.to_vec(),
ciphertext,
entropy_snapshot: snapshot,
commitment,
})
}
pub fn encode_pooled(
shared_secret: &[u8],
plaintext: &[u8],
pool: &crate::entropy_pool::EntropyPool,
) -> Result<KkPacket> {
if plaintext.is_empty() {
return Err(KkError::EmptyInput);
}
let snapshot = pool.draw()?;
let ciphertext = xor_with_keystream(shared_secret, &snapshot, plaintext)?;
let commitment = temporal::commit(shared_secret, &snapshot, &ciphertext)?;
Ok(KkPacket {
ciphertext,
entropy_snapshot: snapshot,
commitment,
})
}
pub fn encode_aead_pooled(
shared_secret: &[u8],
plaintext: &[u8],
aad: &[u8],
pool: &crate::entropy_pool::EntropyPool,
) -> Result<KkAeadPacket> {
if plaintext.is_empty() {
return Err(KkError::EmptyInput);
}
let snapshot = pool.draw()?;
let ciphertext = xor_with_keystream(shared_secret, &snapshot, plaintext)?;
let commitment = temporal::commit_aead(shared_secret, &snapshot, &ciphertext, aad)?;
Ok(KkAeadPacket {
aad: aad.to_vec(),
ciphertext,
entropy_snapshot: snapshot,
commitment,
})
}
pub fn encode_aead_batch(
shared_secret: &[u8],
messages: &[(&[u8], &[u8])], pool: Option<&crate::entropy_pool::EntropyPool>,
) -> Result<Vec<KkAeadPacket>> {
let results: Vec<KkAeadPacket> = messages
.par_chunks(8)
.flat_map_iter(|chunk| {
if chunk.len() == 8 {
encode_aead_batch_8_inner(shared_secret, chunk, pool).expect("batch encode failed")
} else {
chunk
.iter()
.map(|(pt, aad)| match pool {
Some(p) => encode_aead_pooled(shared_secret, pt, aad, p),
None => encode_aead(shared_secret, pt, aad),
})
.collect::<Result<Vec<_>>>()
.expect("scalar encode failed")
}
})
.collect();
Ok(results)
}
fn encode_aead_batch_8_inner(
shared_secret: &[u8],
chunk: &[(&[u8], &[u8])],
pool: Option<&crate::entropy_pool::EntropyPool>,
) -> Result<Vec<KkAeadPacket>> {
debug_assert_eq!(chunk.len(), 8);
let snapshots: [EntropySnapshot; 8] = core::array::from_fn(|i| {
let _ = i;
match pool {
Some(p) => p.draw().expect("pool draw failed"),
None => entropy::gather().expect("entropy gather failed"),
}
});
let ciphertexts: [Vec<u8>; 8] = core::array::from_fn(|i| {
xor_with_keystream_seq(shared_secret, &snapshots[i], chunk[i].0)
.expect("xor_with_keystream failed")
});
let snap_refs: [&EntropySnapshot; 8] = core::array::from_fn(|i| &snapshots[i]);
let ct_refs: [&[u8]; 8] = core::array::from_fn(|i| ciphertexts[i].as_slice());
let aad_refs: [&[u8]; 8] = core::array::from_fn(|i| chunk[i].1);
let commitments = temporal::commit_aead_batch_8(shared_secret, snap_refs, ct_refs, aad_refs)?;
let mut ct_arr = ciphertexts;
let packets: Vec<KkAeadPacket> = (0..8)
.map(|i| KkAeadPacket {
aad: chunk[i].1.to_vec(),
ciphertext: std::mem::take(&mut ct_arr[i]),
entropy_snapshot: snapshots[i].clone(),
commitment: commitments[i].clone(),
})
.collect();
Ok(packets)
}
pub fn decode_aead_batch(shared_secret: &[u8], packets: &[KkAeadPacket]) -> Result<Vec<Vec<u8>>> {
packets
.par_iter()
.map(|pkt| {
temporal::verify_aead(
shared_secret,
&pkt.entropy_snapshot,
&pkt.ciphertext,
&pkt.aad,
&pkt.commitment,
)?;
xor_with_keystream_seq(shared_secret, &pkt.entropy_snapshot, &pkt.ciphertext)
})
.collect()
}
pub const PARALLEL_CHUNK_SIZE: usize = 1 << 20;
#[derive(Clone)]
pub struct KkParallelPacket {
pub chunks: Vec<KkAeadPacket>,
pub chunk_size: usize,
pub merkle_root: [u8; 32],
}
fn compute_merkle_root(chunks: &[KkAeadPacket]) -> [u8; 32] {
let mut preimage = Vec::with_capacity(chunks.len() * 32);
for chunk in chunks {
preimage.extend_from_slice(&chunk.commitment.mac);
}
kk_hash(&preimage)
}
pub fn encode_parallel(
shared_secret: &[u8],
plaintext: &[u8],
aad: &[u8],
chunk_size: usize,
pool: Option<&crate::entropy_pool::EntropyPool>,
) -> Result<KkParallelPacket> {
if plaintext.is_empty() {
return Err(KkError::EmptyInput);
}
if chunk_size == 0 {
return Err(KkError::InvalidPacket("chunk_size must be > 0".into()));
}
let chunk_pairs: Vec<(usize, &[u8])> = plaintext.chunks(chunk_size).enumerate().collect();
let chunks: Vec<KkAeadPacket> = chunk_pairs
.par_iter()
.map(|(_idx, chunk_data)| {
let snapshot = match pool {
Some(p) => p.draw()?,
None => entropy::gather()?,
};
encode_aead_par_inner(shared_secret, chunk_data, aad, snapshot)
})
.collect::<Result<Vec<_>>>()?;
let merkle_root = compute_merkle_root(&chunks);
Ok(KkParallelPacket {
chunks,
chunk_size,
merkle_root,
})
}
pub fn decode_parallel(shared_secret: &[u8], packet: &KkParallelPacket) -> Result<Vec<u8>> {
if packet.chunks.is_empty() {
return Err(KkError::InvalidPacket(
"parallel packet has no chunks".into(),
));
}
let computed_root = compute_merkle_root(&packet.chunks);
if computed_root != packet.merkle_root {
return Err(KkError::CommitmentMismatch);
}
let plaintexts: Vec<Vec<u8>> = packet
.chunks
.par_iter()
.map(|chunk| decode_aead_seq(shared_secret, chunk))
.collect::<Result<Vec<_>>>()?;
let total_len: usize = plaintexts.iter().map(|p| p.len()).sum();
let mut result = Vec::with_capacity(total_len);
for pt in plaintexts {
result.extend_from_slice(&pt);
}
Ok(result)
}
impl KkParallelPacket {
pub fn to_bytes(&self) -> Vec<u8> {
let num_chunks = self.chunks.len() as u32;
let chunk_bytes: Vec<Vec<u8>> = self.chunks.iter().map(|c| c.to_bytes()).collect();
let payload_size: usize = chunk_bytes.iter().map(|cb| 4 + cb.len()).sum();
let header_size = 4 + 4 + 32;
let mut out = Vec::with_capacity(header_size + payload_size);
out.extend_from_slice(&num_chunks.to_le_bytes());
out.extend_from_slice(&(self.chunk_size as u32).to_le_bytes());
out.extend_from_slice(&self.merkle_root);
for cb in &chunk_bytes {
out.extend_from_slice(&(cb.len() as u32).to_le_bytes());
out.extend_from_slice(cb);
}
out
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
const HEADER: usize = 4 + 4 + 32;
if data.len() < HEADER {
return Err(KkError::InvalidPacket("parallel packet too short".into()));
}
let num_chunks = u32::from_le_bytes(
data[..4]
.try_into()
.map_err(|_| KkError::InvalidPacket("bad chunk count".into()))?,
) as usize;
let chunk_size = u32::from_le_bytes(
data[4..8]
.try_into()
.map_err(|_| KkError::InvalidPacket("bad chunk size".into()))?,
) as usize;
let mut merkle_root = [0u8; 32];
merkle_root.copy_from_slice(&data[8..40]);
let mut offset = HEADER;
let mut chunks = Vec::with_capacity(num_chunks);
for _ in 0..num_chunks {
if data.len() < offset + 4 {
return Err(KkError::InvalidPacket(
"parallel packet truncated at chunk length".into(),
));
}
let cb_len = u32::from_le_bytes(
data[offset..offset + 4]
.try_into()
.map_err(|_| KkError::InvalidPacket("bad chunk byte length".into()))?,
) as usize;
offset += 4;
if data.len() < offset + cb_len {
return Err(KkError::InvalidPacket(
"parallel packet truncated at chunk data".into(),
));
}
let chunk = KkAeadPacket::from_bytes(&data[offset..offset + cb_len])?;
chunks.push(chunk);
offset += cb_len;
}
Ok(Self {
chunks,
chunk_size,
merkle_root,
})
}
}
fn xor_with_keystream(
shared_secret: &[u8],
snapshot: &EntropySnapshot,
input: &[u8],
) -> Result<Vec<u8>> {
let mut output = vec![0u8; input.len()];
let batch_bytes = CHUNK_SIZE * 8;
let result = output.par_chunks_mut(batch_bytes).enumerate().try_for_each(
|(batch_idx, out_batch)| -> Result<()> {
let base_chunk = batch_idx * 8;
let in_base = base_chunk * CHUNK_SIZE;
if out_batch.len() == batch_bytes {
let mut keys = kdf::derive_symbol_key_batch(
shared_secret,
snapshot,
base_chunk as u64,
CHUNK_SIZE,
)?;
for (c, key) in keys.iter_mut().enumerate() {
let out_off = c * CHUNK_SIZE;
let in_off = in_base + c * CHUNK_SIZE;
for i in 0..CHUNK_SIZE {
out_batch[out_off + i] = input[in_off + i] ^ key[i];
}
key.zeroize();
}
} else {
let chunks_in_batch = out_batch.len().div_ceil(CHUNK_SIZE);
for c in 0..chunks_in_batch {
let chunk_idx = base_chunk + c;
let out_off = c * CHUNK_SIZE;
let chunk_len = (out_batch.len() - out_off).min(CHUNK_SIZE);
let in_off = in_base + c * CHUNK_SIZE;
let mut key_bytes = kdf::derive_symbol_key(
shared_secret,
snapshot,
chunk_idx as u64,
chunk_len,
)?;
for i in 0..chunk_len {
out_batch[out_off + i] = input[in_off + i] ^ key_bytes[i];
}
key_bytes.zeroize();
}
}
Ok(())
},
);
match result {
Ok(()) => Ok(output),
Err(e) => {
output.zeroize();
Err(e)
}
}
}
fn xor_with_keystream_seq(
shared_secret: &[u8],
snapshot: &EntropySnapshot,
input: &[u8],
) -> Result<Vec<u8>> {
let mut output = vec![0u8; input.len()];
let batch_bytes = CHUNK_SIZE * 8;
for (batch_idx, out_batch) in output.chunks_mut(batch_bytes).enumerate() {
let base_chunk = batch_idx * 8;
let in_base = base_chunk * CHUNK_SIZE;
if out_batch.len() == batch_bytes {
let mut keys = kdf::derive_symbol_key_batch(
shared_secret,
snapshot,
base_chunk as u64,
CHUNK_SIZE,
)?;
for (c, key) in keys.iter_mut().enumerate() {
let out_off = c * CHUNK_SIZE;
let in_off = in_base + c * CHUNK_SIZE;
for i in 0..CHUNK_SIZE {
out_batch[out_off + i] = input[in_off + i] ^ key[i];
}
key.zeroize();
}
} else {
let chunks_in_batch = out_batch.len().div_ceil(CHUNK_SIZE);
for c in 0..chunks_in_batch {
let chunk_idx = base_chunk + c;
let out_off = c * CHUNK_SIZE;
let chunk_len = (out_batch.len() - out_off).min(CHUNK_SIZE);
let in_off = in_base + c * CHUNK_SIZE;
let mut key_bytes =
kdf::derive_symbol_key(shared_secret, snapshot, chunk_idx as u64, chunk_len)?;
for i in 0..chunk_len {
out_batch[out_off + i] = input[in_off + i] ^ key_bytes[i];
}
key_bytes.zeroize();
}
}
}
Ok(output)
}
fn encode_aead_par_inner(
shared_secret: &[u8],
plaintext: &[u8],
aad: &[u8],
snapshot: EntropySnapshot,
) -> Result<KkAeadPacket> {
let ciphertext = xor_with_keystream_seq(shared_secret, &snapshot, plaintext)?;
let commitment = temporal::commit_aead(shared_secret, &snapshot, &ciphertext, aad)?;
Ok(KkAeadPacket {
aad: aad.to_vec(),
ciphertext,
entropy_snapshot: snapshot,
commitment,
})
}
fn decode_aead_seq(shared_secret: &[u8], packet: &KkAeadPacket) -> Result<Vec<u8>> {
temporal::verify_aead(
shared_secret,
&packet.entropy_snapshot,
&packet.ciphertext,
&packet.aad,
&packet.commitment,
)?;
xor_with_keystream_seq(shared_secret, &packet.entropy_snapshot, &packet.ciphertext)
}
pub struct StreamEncoder {
shared_secret: Vec<u8>,
buffer: Vec<u8>,
snapshot: EntropySnapshot,
}
impl StreamEncoder {
pub fn new(shared_secret: &[u8]) -> Result<Self> {
let snapshot = entropy::gather()?;
Ok(Self {
shared_secret: shared_secret.to_vec(),
buffer: Vec::new(),
snapshot,
})
}
pub fn update(&mut self, data: &[u8]) {
self.buffer.extend_from_slice(data);
}
pub fn finalize(mut self) -> Result<KkPacket> {
if self.buffer.is_empty() {
return Err(KkError::EmptyInput);
}
let ciphertext = xor_with_keystream(&self.shared_secret, &self.snapshot, &self.buffer)?;
let commitment = temporal::commit(&self.shared_secret, &self.snapshot, &ciphertext)?;
self.shared_secret.zeroize();
self.buffer.zeroize();
Ok(KkPacket {
ciphertext,
entropy_snapshot: self.snapshot.clone(),
commitment,
})
}
}
impl Drop for StreamEncoder {
fn drop(&mut self) {
self.shared_secret.zeroize();
self.buffer.zeroize();
}
}
pub struct StreamDecoder {
shared_secret: Vec<u8>,
buffer: Vec<u8>,
snapshot: EntropySnapshot,
commitment: TemporalCommitment,
}
impl StreamDecoder {
pub fn new(
shared_secret: &[u8],
snapshot: EntropySnapshot,
commitment: TemporalCommitment,
) -> Self {
Self {
shared_secret: shared_secret.to_vec(),
buffer: Vec::new(),
snapshot,
commitment,
}
}
pub fn update(&mut self, data: &[u8]) {
self.buffer.extend_from_slice(data);
}
pub fn finalize(mut self) -> Result<Vec<u8>> {
if self.buffer.is_empty() {
return Err(KkError::EmptyInput);
}
temporal::verify(
&self.shared_secret,
&self.snapshot,
&self.buffer,
&self.commitment,
)?;
let plaintext = xor_with_keystream(&self.shared_secret, &self.snapshot, &self.buffer)?;
self.shared_secret.zeroize();
self.buffer.zeroize();
Ok(plaintext)
}
}
impl Drop for StreamDecoder {
fn drop(&mut self) {
self.shared_secret.zeroize();
self.buffer.zeroize();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_decode_roundtrip() {
let secret = b"test-shared-secret-2026";
let plaintext = b"Hello from KK! The language only existed for one cosmic instant.";
let packet = encode(secret, plaintext).unwrap();
let decoded = decode(secret, &packet).unwrap();
assert_eq!(plaintext.as_slice(), decoded.as_slice());
}
#[test]
fn same_plaintext_different_ciphertext() {
let secret = b"test-key";
let plaintext = b"A";
let p1 = encode(secret, plaintext).unwrap();
let p2 = encode(secret, plaintext).unwrap();
assert_ne!(
p1.ciphertext, p2.ciphertext,
"Same symbol at different moments MUST produce different ciphertext"
);
}
#[test]
fn wrong_key_fails_decode() {
let plaintext = b"secret message";
let packet = encode(b"correct-key", plaintext).unwrap();
let result = decode(b"wrong-key", &packet);
assert!(
result.is_err(),
"Decoding with wrong shared secret must fail commitment verification"
);
}
#[test]
fn empty_input_rejected() {
let result = encode(b"key", b"");
assert!(result.is_err());
}
#[test]
fn packet_serialization_roundtrip() {
let secret = b"serialize-test";
let plaintext = b"test packet roundtrip";
let packet = encode(secret, plaintext).unwrap();
let bytes = packet.to_bytes();
let restored = KkPacket::from_bytes(&bytes).unwrap();
let decoded = decode(secret, &restored).unwrap();
assert_eq!(plaintext.as_slice(), decoded.as_slice());
}
#[test]
fn tampered_ciphertext_detected() {
let secret = b"tamper-test";
let packet = encode(secret, b"important data").unwrap();
let mut tampered = packet.clone();
tampered.ciphertext[0] ^= 0xFF;
let result = decode(secret, &tampered);
assert!(
result.is_err(),
"Tampered ciphertext must fail commitment verification"
);
}
#[test]
fn large_message_works() {
let secret = b"large-msg-test";
let plaintext: Vec<u8> = (0..10_000).map(|i| (i % 256) as u8).collect();
let packet = encode(secret, &plaintext).unwrap();
let decoded = decode(secret, &packet).unwrap();
assert_eq!(plaintext, decoded);
}
#[test]
fn split_encode_decode_roundtrip() {
let secret = b"split-test-secret";
let plaintext = b"Split-channel KK: ciphertext and epsilon travel separately.";
let (sealed, epsilon) = encode_split(secret, plaintext).unwrap();
let decoded = decode_split(secret, &sealed, &epsilon).unwrap();
assert_eq!(plaintext.as_slice(), decoded.as_slice());
}
#[test]
fn split_wrong_key_fails() {
let plaintext = b"split secret";
let (sealed, epsilon) = encode_split(b"right-key", plaintext).unwrap();
let result = decode_split(b"wrong-key", &sealed, &epsilon);
assert!(result.is_err(), "Wrong passphrase must fail");
}
#[test]
fn split_wrong_epsilon_fails() {
let secret = b"epsilon-test";
let plaintext = b"the moment matters";
let (sealed, _real_epsilon) = encode_split(secret, plaintext).unwrap();
let fake_epsilon = entropy::gather().unwrap();
let result = decode_split(secret, &sealed, &fake_epsilon);
assert!(
result.is_err(),
"Wrong epsilon must fail commitment verification"
);
}
#[test]
fn split_sealed_message_serialization() {
let secret = b"serde-split";
let plaintext = b"roundtrip the sealed half";
let (sealed, epsilon) = encode_split(secret, plaintext).unwrap();
let wire = sealed.to_bytes();
let restored = KkSealedMessage::from_bytes(&wire).unwrap();
let eps_wire = epsilon.to_bytes();
let restored_eps = EntropySnapshot::from_bytes(&eps_wire).unwrap();
let decoded = decode_split(secret, &restored, &restored_eps).unwrap();
assert_eq!(plaintext.as_slice(), decoded.as_slice());
}
#[test]
fn split_empty_input_rejected() {
let result = encode_split(b"key", b"");
assert!(result.is_err());
}
#[test]
fn bound_encode_decode_roundtrip() {
let secret = b"bound-test-secret";
let plaintext = b"Temporal proof: challenge-response freshness.";
let nonce = temporal::generate_challenge().unwrap();
let packet = encode_bound(secret, plaintext, &nonce, &temporal::GENESIS_MAC).unwrap();
let decoded = decode_bound(secret, &packet, &nonce, Duration::from_secs(30)).unwrap();
assert_eq!(plaintext.as_slice(), decoded.as_slice());
}
#[test]
fn bound_wrong_nonce_rejected() {
let secret = b"nonce-reject";
let nonce = temporal::generate_challenge().unwrap();
let wrong_nonce = temporal::generate_challenge().unwrap();
let packet = encode_bound(secret, b"test data", &nonce, &temporal::GENESIS_MAC).unwrap();
let result = decode_bound(secret, &packet, &wrong_nonce, Duration::from_secs(30));
assert!(result.is_err(), "Wrong nonce must be rejected");
}
#[test]
fn bound_wrong_key_rejected() {
let nonce = temporal::generate_challenge().unwrap();
let packet = encode_bound(b"right-key", b"secret", &nonce, &temporal::GENESIS_MAC).unwrap();
let result = decode_bound(b"wrong-key", &packet, &nonce, Duration::from_secs(30));
assert!(result.is_err(), "Wrong key must fail");
}
#[test]
fn bound_packet_serialization_roundtrip() {
let secret = b"bound-serde";
let plaintext = b"serialize a bound packet";
let nonce = temporal::generate_challenge().unwrap();
let packet = encode_bound(secret, plaintext, &nonce, &temporal::GENESIS_MAC).unwrap();
let bytes = packet.to_bytes();
let restored = KkBoundPacket::from_bytes(&bytes).unwrap();
let decoded = decode_bound(secret, &restored, &nonce, Duration::from_secs(30)).unwrap();
assert_eq!(plaintext.as_slice(), decoded.as_slice());
}
#[test]
fn bound_tampered_ciphertext_detected() {
let secret = b"tamper-bound";
let nonce = temporal::generate_challenge().unwrap();
let mut packet =
encode_bound(secret, b"important", &nonce, &temporal::GENESIS_MAC).unwrap();
packet.ciphertext[0] ^= 0xFF;
let result = decode_bound(secret, &packet, &nonce, Duration::from_secs(30));
assert!(result.is_err(), "Tampered ciphertext must fail");
}
#[test]
fn bound_chain_ordering() {
let secret = b"chain-test";
let nonce1 = temporal::generate_challenge().unwrap();
let nonce2 = temporal::generate_challenge().unwrap();
let p1 = encode_bound(secret, b"first", &nonce1, &temporal::GENESIS_MAC).unwrap();
let p2 = encode_bound(secret, b"second", &nonce2, &p1.proof.mac).unwrap();
let d1 = decode_bound(secret, &p1, &nonce1, Duration::from_secs(30)).unwrap();
let d2 = decode_bound(secret, &p2, &nonce2, Duration::from_secs(30)).unwrap();
assert_eq!(d1, b"first");
assert_eq!(d2, b"second");
assert_eq!(p2.proof.prev_mac, p1.proof.mac);
}
#[test]
fn bound_empty_input_rejected() {
let nonce = temporal::generate_challenge().unwrap();
let result = encode_bound(b"key", b"", &nonce, &temporal::GENESIS_MAC);
assert!(result.is_err());
}
#[test]
fn stream_encode_decode_roundtrip() {
let secret = b"stream-secret";
let mut enc = StreamEncoder::new(secret).unwrap();
enc.update(b"Hello ");
enc.update(b"KK ");
enc.update(b"Stream!");
let packet = enc.finalize().unwrap();
let plaintext = decode(secret, &packet).unwrap();
assert_eq!(plaintext, b"Hello KK Stream!");
}
#[test]
fn stream_decoder_roundtrip() {
let secret = b"stream-dec-secret";
let mut enc = StreamEncoder::new(secret).unwrap();
enc.update(b"chunk1");
enc.update(b"chunk2");
let packet = enc.finalize().unwrap();
let mut dec = StreamDecoder::new(
secret,
packet.entropy_snapshot.clone(),
packet.commitment.clone(),
);
dec.update(&packet.ciphertext);
let plaintext = dec.finalize().unwrap();
assert_eq!(plaintext, b"chunk1chunk2");
}
#[test]
fn stream_decoder_incremental_ciphertext() {
let secret = b"stream-incr-secret";
let mut enc = StreamEncoder::new(secret).unwrap();
enc.update(b"ABCDEFGHIJ");
let packet = enc.finalize().unwrap();
let mid = packet.ciphertext.len() / 2;
let mut dec = StreamDecoder::new(
secret,
packet.entropy_snapshot.clone(),
packet.commitment.clone(),
);
dec.update(&packet.ciphertext[..mid]);
dec.update(&packet.ciphertext[mid..]);
let plaintext = dec.finalize().unwrap();
assert_eq!(plaintext, b"ABCDEFGHIJ");
}
#[test]
fn stream_encoder_empty_rejected() {
let enc = StreamEncoder::new(b"key").unwrap();
assert!(enc.finalize().is_err());
}
#[test]
fn stream_decoder_empty_rejected() {
let snapshot = crate::entropy::gather().unwrap();
let commitment = crate::temporal::commit(b"key", &snapshot, b"dummy").unwrap();
let dec = StreamDecoder::new(b"key", snapshot, commitment);
assert!(dec.finalize().is_err());
}
#[test]
fn stream_matches_oneshot() {
let secret = b"stream-vs-oneshot";
let data = b"The quick brown fox jumps over the lazy dog";
let snapshot = crate::entropy::gather().unwrap();
let oneshot = encode_with_snapshot(secret, data, snapshot.clone()).unwrap();
let mut enc = StreamEncoder::new(secret).unwrap();
enc.update(data);
let stream_pkt = enc.finalize().unwrap();
let oneshot_pt = decode(secret, &oneshot).unwrap();
let stream_pt = decode(secret, &stream_pkt).unwrap();
assert_eq!(oneshot_pt, stream_pt);
assert_eq!(&stream_pt[..], &data[..]);
}
#[test]
fn parallel_roundtrip_small() {
let secret = b"parallel-test-secret";
let plaintext = b"Hello parallel world!";
let aad = b"test-aad";
let packet = encode_parallel(secret, plaintext, aad, 8, None).unwrap();
assert!(packet.chunks.len() >= 2); let decoded = decode_parallel(secret, &packet).unwrap();
assert_eq!(&decoded[..], &plaintext[..]);
}
#[test]
fn parallel_roundtrip_exact_chunk() {
let secret = b"exact-chunk-secret";
let plaintext = vec![0xABu8; 1024];
let aad = b"exact";
let packet = encode_parallel(secret, &plaintext, aad, 1024, None).unwrap();
assert_eq!(packet.chunks.len(), 1);
let decoded = decode_parallel(secret, &packet).unwrap();
assert_eq!(decoded, plaintext);
}
#[test]
fn parallel_roundtrip_large() {
let secret = b"large-parallel-secret";
let plaintext = vec![42u8; 1_000_000]; let aad = b"large-aad";
let chunk_size = PARALLEL_CHUNK_SIZE;
let packet = encode_parallel(secret, &plaintext, aad, chunk_size, None).unwrap();
assert_eq!(packet.chunks.len(), 1);
let decoded = decode_parallel(secret, &packet).unwrap();
assert_eq!(decoded, plaintext);
}
#[test]
fn parallel_merkle_detects_reorder() {
let secret = b"merkle-reorder-test";
let plaintext = vec![0u8; 2048];
let aad = b"reorder";
let mut packet = encode_parallel(secret, &plaintext, aad, 512, None).unwrap();
assert!(packet.chunks.len() >= 2);
packet.chunks.swap(0, 1);
let result = decode_parallel(secret, &packet);
assert!(result.is_err());
}
#[test]
fn parallel_merkle_detects_removal() {
let secret = b"merkle-removal-test";
let plaintext = vec![0u8; 2048];
let aad = b"removal";
let mut packet = encode_parallel(secret, &plaintext, aad, 512, None).unwrap();
assert!(packet.chunks.len() >= 2);
packet.chunks.pop();
let result = decode_parallel(secret, &packet);
assert!(result.is_err());
}
#[test]
fn parallel_serde_roundtrip() {
let secret = b"serde-parallel-secret";
let plaintext = b"serialize me in parallel chunks";
let aad = b"serde-aad";
let packet = encode_parallel(secret, plaintext, aad, 10, None).unwrap();
let bytes = packet.to_bytes();
let restored = KkParallelPacket::from_bytes(&bytes).unwrap();
assert_eq!(restored.chunks.len(), packet.chunks.len());
assert_eq!(restored.chunk_size, packet.chunk_size);
assert_eq!(restored.merkle_root, packet.merkle_root);
let decoded = decode_parallel(secret, &restored).unwrap();
assert_eq!(&decoded[..], &plaintext[..]);
}
#[test]
fn parallel_empty_input_rejected() {
let secret = b"empty-test";
let result = encode_parallel(secret, b"", b"aad", 1024, None);
assert!(result.is_err());
}
#[test]
fn parallel_zero_chunk_size_rejected() {
let secret = b"zero-chunk";
let result = encode_parallel(secret, b"data", b"aad", 0, None);
assert!(result.is_err());
}
}