Skip to main content

kk_crypto/
codec.rs

1// Copyright (c) 2026 John A Keeney, Entrouter. All rights reserved.
2// Licensed under the Apache License, Version 2.0 with Additional Terms.
3// NO COMMERCIAL USE without prior written authorization from Entrouter.
4// Unauthorized commercial use will be prosecuted to the fullest extent of the law.
5// See the LICENSE file in the project root for full license information.
6// NOTICE: Removal of this header is a violation of the license.
7
8//! KK Codec, The core encoding/decoding primitive.
9//!
10//! This is where the fundamental KK operation happens:
11//!
12//!   KK(S) = S ^ ε
13//!
14//! For each symbol (byte) in the plaintext, we derive a unique key stream
15//! from the shared secret and the entropy snapshot, then XOR to encode.
16//!
17//! The same symbol encoded at two different moments produces two
18//! cryptographically unrelated values, because the entropy snapshot ε
19//! is different, that moment is gone, unrepeatable, unrecoverable.
20//!
21//! ## Encoding Flow
22//!
23//! ```text
24//! plaintext bytes → for each byte[i]:
25//!   key_i = KK-KDF(shared_secret, salt=ε, info=i||timestamp)
26//!   cipher_i = byte[i] ⊕ key_i
27//! → ciphertext
28//! ```
29//!
30//! ## Decoding Flow
31//!
32//! ```text
33//! ciphertext + ε → for each byte[i]:
34//!   key_i = KK-KDF(shared_secret, salt=ε, info=i||timestamp)  // SAME derivation
35//!   plain_i = cipher_i ⊕ key_i                                 // XOR is its own inverse
36//! → plaintext
37//! ```
38//!
39//! All key derivation uses the novel KK-Sponge-KDF, no HKDF, no SHA-256.
40
41use rayon::prelude::*;
42use zeroize::Zeroize;
43
44use std::time::Duration;
45
46use crate::entropy::{self, EntropySnapshot};
47use crate::error::{KkError, Result};
48use crate::kdf;
49use crate::kk_mix::kk_hash;
50use crate::temporal::{self, TemporalCommitment, TemporalProof};
51
52/// The number of plaintext bytes processed per KDF derivation.
53/// Larger chunks = fewer KDF calls = better throughput.
54/// Each chunk still gets a unique key derived from its position.
55const CHUNK_SIZE: usize = 4096;
56
57/// A KK-encoded packet: everything the receiver needs to decode.
58///
59/// Contains:
60///   - The ciphertext (XOR of plaintext with per-symbol key stream)
61///   - The entropy snapshot ε (the unrepeatable moment)
62///   - Temporal commitment (proves integrity of ε + ciphertext binding)
63#[derive(Clone)]
64pub struct KkPacket {
65    /// The encoded bytes, symbol values transmuted by entropy
66    pub ciphertext: Vec<u8>,
67    /// The entropy snapshot, the captured moment
68    pub entropy_snapshot: EntropySnapshot,
69    /// Temporal commitment, binds ciphertext to its entropic moment
70    pub commitment: TemporalCommitment,
71}
72
73impl KkPacket {
74    /// Serialize the full packet for transmission.
75    ///
76    /// Format: `[4-byte ciphertext length][ciphertext][48-byte snapshot][32-byte commitment]`
77    pub fn to_bytes(&self) -> Vec<u8> {
78        let ct_len = self.ciphertext.len() as u32;
79        let snap_bytes = self.entropy_snapshot.to_bytes();
80        let commit_bytes = self.commitment.to_bytes();
81
82        let mut out =
83            Vec::with_capacity(4 + self.ciphertext.len() + snap_bytes.len() + commit_bytes.len());
84        out.extend_from_slice(&ct_len.to_le_bytes());
85        out.extend_from_slice(&self.ciphertext);
86        out.extend_from_slice(&snap_bytes);
87        out.extend_from_slice(&commit_bytes);
88        out
89    }
90
91    /// Deserialize a packet from received bytes.
92    pub fn from_bytes(data: &[u8]) -> Result<Self> {
93        if data.len() < 4 {
94            return Err(KkError::InvalidPacket("packet too short".into()));
95        }
96
97        let ct_len = u32::from_le_bytes(
98            data[..4]
99                .try_into()
100                .map_err(|_| KkError::InvalidPacket("bad length".into()))?,
101        ) as usize;
102
103        let expected_min = 4 + ct_len + 48 + 32; // 48 = snapshot, 32 = commitment
104        if data.len() < expected_min {
105            return Err(KkError::InvalidPacket(format!(
106                "packet too short: expected at least {expected_min}, got {}",
107                data.len()
108            )));
109        }
110
111        let ciphertext = data[4..4 + ct_len].to_vec();
112        let snapshot = EntropySnapshot::from_bytes(&data[4 + ct_len..4 + ct_len + 48])?;
113        let commitment = TemporalCommitment::from_bytes(&data[4 + ct_len + 48..])?;
114
115        Ok(Self {
116            ciphertext,
117            entropy_snapshot: snapshot,
118            commitment,
119        })
120    }
121}
122
123// ─────────────────────────────────────────────────────────────────
124//  Split-channel types, ε travels separately from ciphertext
125// ─────────────────────────────────────────────────────────────────
126
127/// A sealed message: ciphertext + integrity commitment, but NO entropy.
128///
129/// This is what travels on the public channel. Without the corresponding
130/// `EntropySnapshot` (which must arrive on a separate, private channel),
131/// the attacker cannot even begin brute-forcing, ε is the HKDF salt,
132/// and without it every passphrase guess is meaningless.
133///
134/// ```text
135/// Channel 1 (public):  KkSealedMessage  →  ciphertext + HMAC
136/// Channel 2 (private): EntropySnapshot  →  ε (the moment)
137/// ```
138#[derive(Clone)]
139pub struct KkSealedMessage {
140    /// The encoded bytes, symbol values transmuted by entropy
141    pub ciphertext: Vec<u8>,
142    /// Temporal commitment, binds ciphertext to its entropic moment
143    pub commitment: TemporalCommitment,
144}
145
146impl KkSealedMessage {
147    /// Serialize for Channel 1 transmission.
148    ///
149    /// Format: `[4-byte ciphertext length][ciphertext][32-byte commitment]`
150    pub fn to_bytes(&self) -> Vec<u8> {
151        let ct_len = self.ciphertext.len() as u32;
152        let commit_bytes = self.commitment.to_bytes();
153
154        let mut out = Vec::with_capacity(4 + self.ciphertext.len() + commit_bytes.len());
155        out.extend_from_slice(&ct_len.to_le_bytes());
156        out.extend_from_slice(&self.ciphertext);
157        out.extend_from_slice(&commit_bytes);
158        out
159    }
160
161    /// Deserialize from Channel 1 bytes.
162    pub fn from_bytes(data: &[u8]) -> Result<Self> {
163        if data.len() < 4 {
164            return Err(KkError::InvalidPacket("sealed message too short".into()));
165        }
166
167        let ct_len = u32::from_le_bytes(
168            data[..4]
169                .try_into()
170                .map_err(|_| KkError::InvalidPacket("bad length".into()))?,
171        ) as usize;
172
173        let expected_min = 4 + ct_len + 32;
174        if data.len() < expected_min {
175            return Err(KkError::InvalidPacket(format!(
176                "sealed message too short: expected at least {expected_min}, got {}",
177                data.len()
178            )));
179        }
180
181        let ciphertext = data[4..4 + ct_len].to_vec();
182        let commitment = TemporalCommitment::from_bytes(&data[4 + ct_len..])?;
183
184        Ok(Self {
185            ciphertext,
186            commitment,
187        })
188    }
189}
190
191/// Encode plaintext using the KK primitive.
192///
193/// This is the fundamental KK operation:
194///   1. Capture entropy from the universe at this exact moment
195///   2. For each symbol, derive a unique key from (secret, ε, position)
196///   3. XOR the symbol with its key, the symbol's value is now
197///      a function of the universe at the instant it was born
198///   4. Create a temporal commitment binding everything together
199///
200/// The returned KkPacket contains everything the receiver needs.
201pub fn encode(shared_secret: &[u8], plaintext: &[u8]) -> Result<KkPacket> {
202    if plaintext.is_empty() {
203        return Err(KkError::EmptyInput);
204    }
205
206    // Step 1: Capture the entropic moment, this instant will never exist again
207    let snapshot = entropy::gather()?;
208
209    // Step 2-3: Derive per-symbol keys and encode
210    let ciphertext = xor_with_keystream(shared_secret, &snapshot, plaintext)?;
211
212    // Step 4: Create temporal commitment
213    let commitment = temporal::commit(shared_secret, &snapshot, &ciphertext)?;
214
215    Ok(KkPacket {
216        ciphertext,
217        entropy_snapshot: snapshot,
218        commitment,
219    })
220}
221
222/// Decode a KK packet back to plaintext.
223///
224/// The receiver uses:
225///   - The shared secret (what both parties know)
226///   - The entropy snapshot ε (transmitted with the packet)
227///   - Deterministic derivation (same HKDF, same inputs = same keys)
228///
229/// Same universe, same moment reference, same symbol values.
230pub fn decode(shared_secret: &[u8], packet: &KkPacket) -> Result<Vec<u8>> {
231    // Step 1: Verify temporal commitment, is this packet intact?
232    temporal::verify(
233        shared_secret,
234        &packet.entropy_snapshot,
235        &packet.ciphertext,
236        &packet.commitment,
237    )?;
238
239    // Step 2: Derive same keystream and XOR to recover plaintext
240    // XOR is its own inverse: (P ⊕ K) ⊕ K = P
241    xor_with_keystream(shared_secret, &packet.entropy_snapshot, &packet.ciphertext)
242}
243
244// ─────────────────────────────────────────────────────────────────
245//  Split-channel API, ε never touches the ciphertext wire
246// ─────────────────────────────────────────────────────────────────
247
248/// Encode plaintext and split the result across two channels.
249///
250/// Returns `(KkSealedMessage, EntropySnapshot)`:
251///   - **Channel 1 (public):** `KkSealedMessage`, ciphertext + HMAC
252///   - **Channel 2 (private):** `EntropySnapshot`, the ε key
253///
254/// An attacker intercepting only Channel 1 sees ciphertext + HMAC but
255/// has no ε. Without ε they cannot derive any key material, every
256/// passphrase guess is meaningless because the HKDF salt is missing.
257///
258/// The ε is physically non-reconstructible (proved in examples/proof.rs).
259/// If it never reaches the attacker, the ciphertext is information-
260/// theoretically unbreakable regardless of compute power.
261pub fn encode_split(
262    shared_secret: &[u8],
263    plaintext: &[u8],
264) -> Result<(KkSealedMessage, EntropySnapshot)> {
265    if plaintext.is_empty() {
266        return Err(KkError::EmptyInput);
267    }
268
269    // Step 1: Capture the entropic moment
270    let snapshot = entropy::gather()?;
271
272    // Step 2-3: Derive per-symbol keys and encode
273    let ciphertext = xor_with_keystream(shared_secret, &snapshot, plaintext)?;
274
275    // Step 4: Create temporal commitment
276    let commitment = temporal::commit(shared_secret, &snapshot, &ciphertext)?;
277
278    let sealed = KkSealedMessage {
279        ciphertext,
280        commitment,
281    };
282
283    // The two halves go on separate channels
284    Ok((sealed, snapshot))
285}
286
287/// Decode a split-channel message by reuniting ciphertext with ε.
288///
289/// The receiver needs:
290///   - The shared secret (what both parties know)
291///   - The `KkSealedMessage` (from Channel 1, the public wire)
292///   - The `EntropySnapshot` (from Channel 2, the private channel)
293///
294/// All three factors must be present. Missing any one = no decryption.
295pub fn decode_split(
296    shared_secret: &[u8],
297    sealed: &KkSealedMessage,
298    epsilon: &EntropySnapshot,
299) -> Result<Vec<u8>> {
300    // Step 1: Verify temporal commitment
301    temporal::verify(
302        shared_secret,
303        epsilon,
304        &sealed.ciphertext,
305        &sealed.commitment,
306    )?;
307
308    // Step 2: Derive keystream and XOR to recover plaintext
309    xor_with_keystream(shared_secret, epsilon, &sealed.ciphertext)
310}
311
312// ─────────────────────────────────────────────────────────────────
313//  Bound-commitment API, challenge-response temporal proof
314// ─────────────────────────────────────────────────────────────────
315
316/// A KK packet with a full temporal proof (challenge-response).
317///
318/// Unlike [`KkPacket`], which carries a basic integrity MAC, this packet
319/// carries a [`TemporalProof`] providing:
320///
321///   - **Freshness**: verifier-supplied nonce proves creation was after the challenge
322///   - **Recency**: epoch check bounds when the encoding actually happened
323///   - **Ordering**: `prev_mac` chains packets into a total order
324///   - **Temporal MAC**: the permutation structure itself varies with entropy
325///
326/// The receiver must supply the nonce they originally issued and the
327/// maximum acceptable clock drift.
328#[derive(Clone)]
329pub struct KkBoundPacket {
330    /// The encoded bytes
331    pub ciphertext: Vec<u8>,
332    /// The entropy snapshot, the captured moment
333    pub entropy_snapshot: EntropySnapshot,
334    /// Temporal proof, freshness + recency + integrity + ordering
335    pub proof: TemporalProof,
336}
337
338impl KkBoundPacket {
339    /// Serialize for transmission.
340    ///
341    /// Format: `[4-byte ct_len][ciphertext][48-byte snapshot][96-byte proof]`
342    pub fn to_bytes(&self) -> Vec<u8> {
343        let ct_len = self.ciphertext.len() as u32;
344        let snap_bytes = self.entropy_snapshot.to_bytes();
345        let proof_bytes = self.proof.to_bytes();
346
347        let mut out =
348            Vec::with_capacity(4 + self.ciphertext.len() + snap_bytes.len() + proof_bytes.len());
349        out.extend_from_slice(&ct_len.to_le_bytes());
350        out.extend_from_slice(&self.ciphertext);
351        out.extend_from_slice(&snap_bytes);
352        out.extend_from_slice(&proof_bytes);
353        out
354    }
355
356    /// Deserialize from received bytes.
357    pub fn from_bytes(data: &[u8]) -> Result<Self> {
358        if data.len() < 4 {
359            return Err(KkError::InvalidPacket("bound packet too short".into()));
360        }
361
362        let ct_len = u32::from_le_bytes(
363            data[..4]
364                .try_into()
365                .map_err(|_| KkError::InvalidPacket("bad length".into()))?,
366        ) as usize;
367
368        let expected_min = 4 + ct_len + 48 + TemporalProof::BYTES;
369        if data.len() < expected_min {
370            return Err(KkError::InvalidPacket(format!(
371                "bound packet too short: expected at least {expected_min}, got {}",
372                data.len()
373            )));
374        }
375
376        let ciphertext = data[4..4 + ct_len].to_vec();
377        let snapshot = EntropySnapshot::from_bytes(&data[4 + ct_len..4 + ct_len + 48])?;
378        let proof = TemporalProof::from_bytes(&data[4 + ct_len + 48..])?;
379
380        Ok(Self {
381            ciphertext,
382            entropy_snapshot: snapshot,
383            proof,
384        })
385    }
386}
387
388/// Encode plaintext with a full temporal proof (challenge-response).
389///
390/// # Protocol
391///
392/// ```text
393/// Verifier ── generate_challenge() ──→ nonce ──→ Prover
394/// Prover   ── encode_bound(secret, plain, nonce, prev_mac) ──→ KkBoundPacket
395/// ```
396///
397/// # Arguments
398/// - `shared_secret`, the pre-shared key
399/// - `plaintext`, data to encode
400/// - `verifier_nonce`, challenge nonce from the verifier
401/// - `prev_mac`, MAC of the previous proof in the chain, or
402///   [`temporal::GENESIS_MAC`] for the first message
403pub fn encode_bound(
404    shared_secret: &[u8],
405    plaintext: &[u8],
406    verifier_nonce: &[u8; 32],
407    prev_mac: &[u8; 32],
408) -> Result<KkBoundPacket> {
409    if plaintext.is_empty() {
410        return Err(KkError::EmptyInput);
411    }
412
413    let snapshot = entropy::gather()?;
414    let ciphertext = xor_with_keystream(shared_secret, &snapshot, plaintext)?;
415    let proof = temporal::commit_bound(
416        shared_secret,
417        &snapshot,
418        &ciphertext,
419        verifier_nonce,
420        prev_mac,
421    )?;
422
423    Ok(KkBoundPacket {
424        ciphertext,
425        entropy_snapshot: snapshot,
426        proof,
427    })
428}
429
430/// Decode a bound packet, verifying freshness + recency + integrity.
431///
432/// # Protocol
433///
434/// ```text
435/// Verifier receives KkBoundPacket, then:
436///   decode_bound(secret, packet, nonce_I_issued, max_drift)
437/// ```
438///
439/// Verification checks (in order):
440/// 1. **Nonce**, proof contains the nonce the verifier issued
441/// 2. **Epoch**, `|now - ε.timestamp| ≤ max_drift`
442/// 3. **MAC**, entropy-derived rotations, constant-time compare
443///
444/// The caller is responsible for:
445/// - Tracking nonces and rejecting reuse
446/// - Verifying `packet.proof.prev_mac` matches the expected chain link
447pub fn decode_bound(
448    shared_secret: &[u8],
449    packet: &KkBoundPacket,
450    expected_nonce: &[u8; 32],
451    max_drift: Duration,
452) -> Result<Vec<u8>> {
453    temporal::verify_bound(
454        shared_secret,
455        &packet.entropy_snapshot,
456        &packet.ciphertext,
457        &packet.proof,
458        expected_nonce,
459        max_drift,
460    )?;
461
462    xor_with_keystream(shared_secret, &packet.entropy_snapshot, &packet.ciphertext)
463}
464
465// ─────────────────────────────────────────────────────────────────
466//  AEAD API, authenticated encryption with associated data
467// ─────────────────────────────────────────────────────────────────
468
469/// A KK-AEAD packet: ciphertext + authenticated associated data.
470///
471/// Contains:
472///   - Associated data (AAD) - transmitted in the clear, authenticated
473///   - The ciphertext (XOR of plaintext with per-symbol key stream)
474///   - The entropy snapshot ε (the unrepeatable moment)
475///   - Temporal commitment (binds ciphertext + AAD to the entropic moment)
476///
477/// The AAD is NOT encrypted but IS integrity-protected by the commitment.
478/// Any modification to the AAD or ciphertext will be detected on decode.
479#[derive(Clone)]
480pub struct KkAeadPacket {
481    /// Associated data, authenticated but not encrypted
482    pub aad: Vec<u8>,
483    /// The encoded bytes
484    pub ciphertext: Vec<u8>,
485    /// The entropy snapshot
486    pub entropy_snapshot: EntropySnapshot,
487    /// Temporal commitment binding ciphertext + AAD to the entropic moment
488    pub commitment: TemporalCommitment,
489}
490
491impl KkAeadPacket {
492    /// Serialize for transmission.
493    ///
494    /// Format: `[4-byte aad_len][aad][4-byte ct_len][ciphertext][48-byte snapshot][32-byte commitment]`
495    pub fn to_bytes(&self) -> Vec<u8> {
496        let aad_len = self.aad.len() as u32;
497        let ct_len = self.ciphertext.len() as u32;
498        let snap_bytes = self.entropy_snapshot.to_bytes();
499        let commit_bytes = self.commitment.to_bytes();
500
501        let mut out = Vec::with_capacity(
502            4 + self.aad.len() + 4 + self.ciphertext.len() + snap_bytes.len() + commit_bytes.len(),
503        );
504        out.extend_from_slice(&aad_len.to_le_bytes());
505        out.extend_from_slice(&self.aad);
506        out.extend_from_slice(&ct_len.to_le_bytes());
507        out.extend_from_slice(&self.ciphertext);
508        out.extend_from_slice(&snap_bytes);
509        out.extend_from_slice(&commit_bytes);
510        out
511    }
512
513    /// Deserialize from received bytes.
514    pub fn from_bytes(data: &[u8]) -> Result<Self> {
515        if data.len() < 8 {
516            return Err(KkError::InvalidPacket("AEAD packet too short".into()));
517        }
518
519        let aad_len = u32::from_le_bytes(
520            data[..4]
521                .try_into()
522                .map_err(|_| KkError::InvalidPacket("bad aad length".into()))?,
523        ) as usize;
524
525        if data.len() < 4 + aad_len + 4 {
526            return Err(KkError::InvalidPacket(
527                "AEAD packet truncated at ct_len".into(),
528            ));
529        }
530
531        let aad = data[4..4 + aad_len].to_vec();
532        let ct_offset = 4 + aad_len;
533
534        let ct_len = u32::from_le_bytes(
535            data[ct_offset..ct_offset + 4]
536                .try_into()
537                .map_err(|_| KkError::InvalidPacket("bad ct length".into()))?,
538        ) as usize;
539
540        let expected_min = ct_offset + 4 + ct_len + 48 + 32;
541        if data.len() < expected_min {
542            return Err(KkError::InvalidPacket(format!(
543                "AEAD packet too short: expected at least {expected_min}, got {}",
544                data.len()
545            )));
546        }
547
548        let ct_start = ct_offset + 4;
549        let ciphertext = data[ct_start..ct_start + ct_len].to_vec();
550        let snap_start = ct_start + ct_len;
551        let snapshot = EntropySnapshot::from_bytes(&data[snap_start..snap_start + 48])?;
552        let commitment =
553            TemporalCommitment::from_bytes(&data[snap_start + 48..snap_start + 48 + 32])?;
554
555        Ok(Self {
556            aad,
557            ciphertext,
558            entropy_snapshot: snapshot,
559            commitment,
560        })
561    }
562}
563
564/// Encode plaintext with authenticated associated data (AEAD).
565///
566/// The AAD is included in the commitment MAC but is NOT encrypted.
567/// This is useful for metadata (headers, routing info, version tags)
568/// that must be readable in the clear but tamper-proof.
569///
570/// # Arguments
571/// - `shared_secret` - the pre-shared key
572/// - `plaintext` - data to encrypt
573/// - `aad` - associated data to authenticate (not encrypted)
574pub fn encode_aead(shared_secret: &[u8], plaintext: &[u8], aad: &[u8]) -> Result<KkAeadPacket> {
575    if plaintext.is_empty() {
576        return Err(KkError::EmptyInput);
577    }
578
579    let snapshot = entropy::gather()?;
580    let ciphertext = xor_with_keystream(shared_secret, &snapshot, plaintext)?;
581    let commitment = temporal::commit_aead(shared_secret, &snapshot, &ciphertext, aad)?;
582
583    Ok(KkAeadPacket {
584        aad: aad.to_vec(),
585        ciphertext,
586        entropy_snapshot: snapshot,
587        commitment,
588    })
589}
590
591/// Decode a KK-AEAD packet, verifying integrity of both ciphertext and AAD.
592///
593/// # Errors
594/// - `KkError::CommitmentMismatch` if the ciphertext or AAD was tampered with
595pub fn decode_aead(shared_secret: &[u8], packet: &KkAeadPacket) -> Result<Vec<u8>> {
596    temporal::verify_aead(
597        shared_secret,
598        &packet.entropy_snapshot,
599        &packet.ciphertext,
600        &packet.aad,
601        &packet.commitment,
602    )?;
603
604    xor_with_keystream(shared_secret, &packet.entropy_snapshot, &packet.ciphertext)
605}
606
607// ─────────────────────────────────────────────────────────────────
608//  Deterministic helpers (fixed snapshot, for test-vector generation)
609// ─────────────────────────────────────────────────────────────────
610
611/// Encode plaintext with a caller-supplied [`EntropySnapshot`].
612///
613/// Identical to [`encode`] but skips `entropy::gather()` so the output
614/// is fully deterministic for a given (secret, plaintext, snapshot).
615///
616/// # Visibility
617/// Exposed for integration tests and the `generate_vectors` example.
618/// **Not part of the public API contract** - may change without notice.
619#[doc(hidden)]
620pub fn encode_with_snapshot(
621    shared_secret: &[u8],
622    plaintext: &[u8],
623    snapshot: EntropySnapshot,
624) -> Result<KkPacket> {
625    if plaintext.is_empty() {
626        return Err(KkError::EmptyInput);
627    }
628    let ciphertext = xor_with_keystream(shared_secret, &snapshot, plaintext)?;
629    let commitment = temporal::commit(shared_secret, &snapshot, &ciphertext)?;
630    Ok(KkPacket {
631        ciphertext,
632        entropy_snapshot: snapshot,
633        commitment,
634    })
635}
636
637/// AEAD encode with a caller-supplied [`EntropySnapshot`].
638///
639/// Identical to [`encode_aead`] but deterministic when the snapshot is fixed.
640#[doc(hidden)]
641pub fn encode_aead_with_snapshot(
642    shared_secret: &[u8],
643    plaintext: &[u8],
644    aad: &[u8],
645    snapshot: EntropySnapshot,
646) -> Result<KkAeadPacket> {
647    if plaintext.is_empty() {
648        return Err(KkError::EmptyInput);
649    }
650    let ciphertext = xor_with_keystream(shared_secret, &snapshot, plaintext)?;
651    let commitment = temporal::commit_aead(shared_secret, &snapshot, &ciphertext, aad)?;
652    Ok(KkAeadPacket {
653        aad: aad.to_vec(),
654        ciphertext,
655        entropy_snapshot: snapshot,
656        commitment,
657    })
658}
659
660// ─────────────────────────────────────────────────────────────────
661//  Pooled encode - pre-generated entropy for high-throughput paths
662// ─────────────────────────────────────────────────────────────────
663
664/// Encode plaintext using a pre-warmed [`EntropyPool`](crate::EntropyPool) instead of
665/// calling `entropy::gather()` on every invocation.
666///
667/// Identical semantics to [`encode`], but the entropy snapshot is drawn
668/// from the pool (near-zero latency) rather than generated on the spot.
669pub fn encode_pooled(
670    shared_secret: &[u8],
671    plaintext: &[u8],
672    pool: &crate::entropy_pool::EntropyPool,
673) -> Result<KkPacket> {
674    if plaintext.is_empty() {
675        return Err(KkError::EmptyInput);
676    }
677    let snapshot = pool.draw()?;
678    let ciphertext = xor_with_keystream(shared_secret, &snapshot, plaintext)?;
679    let commitment = temporal::commit(shared_secret, &snapshot, &ciphertext)?;
680    Ok(KkPacket {
681        ciphertext,
682        entropy_snapshot: snapshot,
683        commitment,
684    })
685}
686
687/// AEAD encode using a pre-warmed [`EntropyPool`](crate::EntropyPool).
688///
689/// Identical semantics to [`encode_aead`], but draws entropy from the pool.
690pub fn encode_aead_pooled(
691    shared_secret: &[u8],
692    plaintext: &[u8],
693    aad: &[u8],
694    pool: &crate::entropy_pool::EntropyPool,
695) -> Result<KkAeadPacket> {
696    if plaintext.is_empty() {
697        return Err(KkError::EmptyInput);
698    }
699    let snapshot = pool.draw()?;
700    let ciphertext = xor_with_keystream(shared_secret, &snapshot, plaintext)?;
701    let commitment = temporal::commit_aead(shared_secret, &snapshot, &ciphertext, aad)?;
702    Ok(KkAeadPacket {
703        aad: aad.to_vec(),
704        ciphertext,
705        entropy_snapshot: snapshot,
706        commitment,
707    })
708}
709
710// ─────────────────────────────────────────────────────────────────
711//  Batched AEAD - parallel encrypt/decrypt of N independent messages
712// ─────────────────────────────────────────────────────────────────
713
714/// Encrypt N independent messages in parallel using Rayon.
715///
716/// Each message gets its own entropy snapshot (drawn from `pool` when
717/// provided, otherwise gathered synchronously). Results are returned
718/// in the same order as the input slice.
719///
720/// This is the real server-workload API: thousands of concurrent messages
721/// processed across all CPU cores.
722pub fn encode_aead_batch(
723    shared_secret: &[u8],
724    messages: &[(&[u8], &[u8])], // (plaintext, aad) pairs
725    pool: Option<&crate::entropy_pool::EntropyPool>,
726) -> Result<Vec<KkAeadPacket>> {
727    // Process in chunks of 8 for batch MAC, scalar fallback for tail
728    let results: Vec<KkAeadPacket> = messages
729        .par_chunks(8)
730        .flat_map_iter(|chunk| {
731            if chunk.len() == 8 {
732                // Full batch of 8 - use vectorized MAC
733                encode_aead_batch_8_inner(shared_secret, chunk, pool).expect("batch encode failed")
734            } else {
735                // Tail < 8 - scalar fallback
736                chunk
737                    .iter()
738                    .map(|(pt, aad)| match pool {
739                        Some(p) => encode_aead_pooled(shared_secret, pt, aad, p),
740                        None => encode_aead(shared_secret, pt, aad),
741                    })
742                    .collect::<Result<Vec<_>>>()
743                    .expect("scalar encode failed")
744            }
745        })
746        .collect();
747
748    Ok(results)
749}
750
751/// Inner function: encrypt 8 messages and commit with batch MAC.
752fn encode_aead_batch_8_inner(
753    shared_secret: &[u8],
754    chunk: &[(&[u8], &[u8])],
755    pool: Option<&crate::entropy_pool::EntropyPool>,
756) -> Result<Vec<KkAeadPacket>> {
757    debug_assert_eq!(chunk.len(), 8);
758
759    // Draw 8 snapshots
760    let snapshots: [EntropySnapshot; 8] = core::array::from_fn(|i| {
761        let _ = i;
762        match pool {
763            Some(p) => p.draw().expect("pool draw failed"),
764            None => entropy::gather().expect("entropy gather failed"),
765        }
766    });
767
768    // XOR-encrypt 8 ciphertexts (sequential to avoid nested Rayon contention -
769    // outer par_chunks(8) already provides parallelism)
770    let ciphertexts: [Vec<u8>; 8] = core::array::from_fn(|i| {
771        xor_with_keystream_seq(shared_secret, &snapshots[i], chunk[i].0)
772            .expect("xor_with_keystream failed")
773    });
774
775    // Batch MAC - the hot path
776    let snap_refs: [&EntropySnapshot; 8] = core::array::from_fn(|i| &snapshots[i]);
777    let ct_refs: [&[u8]; 8] = core::array::from_fn(|i| ciphertexts[i].as_slice());
778    let aad_refs: [&[u8]; 8] = core::array::from_fn(|i| chunk[i].1);
779
780    let commitments = temporal::commit_aead_batch_8(shared_secret, snap_refs, ct_refs, aad_refs)?;
781
782    // Assemble packets
783    let mut ct_arr = ciphertexts;
784    let packets: Vec<KkAeadPacket> = (0..8)
785        .map(|i| KkAeadPacket {
786            aad: chunk[i].1.to_vec(),
787            ciphertext: std::mem::take(&mut ct_arr[i]),
788            entropy_snapshot: snapshots[i].clone(),
789            commitment: commitments[i].clone(),
790        })
791        .collect();
792
793    Ok(packets)
794}
795
796/// Decrypt N independent AEAD packets in parallel using Rayon.
797///
798/// Each packet is verified and decrypted independently. Results are
799/// returned in the same order as the input slice.
800pub fn decode_aead_batch(shared_secret: &[u8], packets: &[KkAeadPacket]) -> Result<Vec<Vec<u8>>> {
801    packets
802        .par_iter()
803        .map(|pkt| {
804            temporal::verify_aead(
805                shared_secret,
806                &pkt.entropy_snapshot,
807                &pkt.ciphertext,
808                &pkt.aad,
809                &pkt.commitment,
810            )?;
811            // Sequential XOR to avoid nested Rayon contention
812            xor_with_keystream_seq(shared_secret, &pkt.entropy_snapshot, &pkt.ciphertext)
813        })
814        .collect()
815}
816
817// ─────────────────────────────────────────────────────────────────
818//  Parallel Encode/Decode - single large payload, chunked + Merkle
819// ─────────────────────────────────────────────────────────────────
820
821/// Default chunk size for parallel encode: 1 MiB.
822pub const PARALLEL_CHUNK_SIZE: usize = 1 << 20;
823
824/// A parallel-encoded packet: large payload split into independently
825/// encrypted chunks, bound together by a Merkle commitment root.
826///
827/// Each chunk is a full [`KkAeadPacket`] with its own entropy snapshot
828/// and temporal commitment. The Merkle root binds all chunk commitments
829/// together so that no chunk can be reordered, removed, or replaced
830/// without detection.
831#[derive(Clone)]
832pub struct KkParallelPacket {
833    /// The independently encrypted chunks, in order.
834    pub chunks: Vec<KkAeadPacket>,
835    /// The chunk size used during encoding (needed to verify padding on last chunk).
836    pub chunk_size: usize,
837    /// Merkle root: `kk_hash(chunk_0.commitment || chunk_1.commitment || …)`
838    pub merkle_root: [u8; 32],
839}
840
841/// Compute the Merkle root over chunk commitments.
842///
843/// root = kk_hash( c_0.mac || c_1.mac || … || c_n.mac )
844fn compute_merkle_root(chunks: &[KkAeadPacket]) -> [u8; 32] {
845    let mut preimage = Vec::with_capacity(chunks.len() * 32);
846    for chunk in chunks {
847        preimage.extend_from_slice(&chunk.commitment.mac);
848    }
849    kk_hash(&preimage)
850}
851
852/// Encode a large payload in parallel by splitting it into chunks.
853///
854/// Each chunk is encrypted independently via [`encode_aead`] (or the pooled
855/// variant when a pool is provided). All chunks are processed in parallel
856/// using Rayon. A Merkle root over the chunk commitments binds the entire
857/// payload together.
858///
859/// # Arguments
860/// - `shared_secret` - the pre-shared key
861/// - `plaintext` - the full payload to encrypt
862/// - `aad` - associated data, authenticated on every chunk
863/// - `chunk_size` - bytes per chunk (use [`PARALLEL_CHUNK_SIZE`] for default 1 MiB)
864/// - `pool` - optional [`EntropyPool`](crate::EntropyPool) for high-throughput paths
865pub fn encode_parallel(
866    shared_secret: &[u8],
867    plaintext: &[u8],
868    aad: &[u8],
869    chunk_size: usize,
870    pool: Option<&crate::entropy_pool::EntropyPool>,
871) -> Result<KkParallelPacket> {
872    if plaintext.is_empty() {
873        return Err(KkError::EmptyInput);
874    }
875    if chunk_size == 0 {
876        return Err(KkError::InvalidPacket("chunk_size must be > 0".into()));
877    }
878
879    // Build (index, chunk_data) pairs so par_iter preserves ordering
880    let chunk_pairs: Vec<(usize, &[u8])> = plaintext.chunks(chunk_size).enumerate().collect();
881
882    let chunks: Vec<KkAeadPacket> = chunk_pairs
883        .par_iter()
884        .map(|(_idx, chunk_data)| {
885            let snapshot = match pool {
886                Some(p) => p.draw()?,
887                None => entropy::gather()?,
888            };
889            encode_aead_par_inner(shared_secret, chunk_data, aad, snapshot)
890        })
891        .collect::<Result<Vec<_>>>()?;
892
893    let merkle_root = compute_merkle_root(&chunks);
894
895    Ok(KkParallelPacket {
896        chunks,
897        chunk_size,
898        merkle_root,
899    })
900}
901
902/// Decode a parallel-encoded packet, verifying the Merkle root and each chunk.
903///
904/// Steps:
905/// 1. Recompute the Merkle root from chunk commitments
906/// 2. Verify it matches the packet's stored root (detects reorder/tamper)
907/// 3. Decrypt all chunks in parallel
908/// 4. Concatenate plaintext in order
909pub fn decode_parallel(shared_secret: &[u8], packet: &KkParallelPacket) -> Result<Vec<u8>> {
910    if packet.chunks.is_empty() {
911        return Err(KkError::InvalidPacket(
912            "parallel packet has no chunks".into(),
913        ));
914    }
915
916    // Verify Merkle root
917    let computed_root = compute_merkle_root(&packet.chunks);
918    if computed_root != packet.merkle_root {
919        return Err(KkError::CommitmentMismatch);
920    }
921
922    // Decrypt all chunks in parallel (sequential XOR per chunk avoids nested Rayon)
923    let plaintexts: Vec<Vec<u8>> = packet
924        .chunks
925        .par_iter()
926        .map(|chunk| decode_aead_seq(shared_secret, chunk))
927        .collect::<Result<Vec<_>>>()?;
928
929    // Concatenate in order
930    let total_len: usize = plaintexts.iter().map(|p| p.len()).sum();
931    let mut result = Vec::with_capacity(total_len);
932    for pt in plaintexts {
933        result.extend_from_slice(&pt);
934    }
935    Ok(result)
936}
937
938impl KkParallelPacket {
939    /// Serialize the parallel packet for transmission.
940    ///
941    /// Format:
942    /// ```text
943    /// [4-byte num_chunks][4-byte chunk_size][32-byte merkle_root]
944    /// for each chunk:
945    ///   [4-byte chunk_bytes_len][chunk_bytes…]
946    /// ```
947    pub fn to_bytes(&self) -> Vec<u8> {
948        let num_chunks = self.chunks.len() as u32;
949        // Pre-serialize chunks to compute total size
950        let chunk_bytes: Vec<Vec<u8>> = self.chunks.iter().map(|c| c.to_bytes()).collect();
951        let payload_size: usize = chunk_bytes.iter().map(|cb| 4 + cb.len()).sum();
952        let header_size = 4 + 4 + 32; // num_chunks + chunk_size + merkle_root
953
954        let mut out = Vec::with_capacity(header_size + payload_size);
955        out.extend_from_slice(&num_chunks.to_le_bytes());
956        out.extend_from_slice(&(self.chunk_size as u32).to_le_bytes());
957        out.extend_from_slice(&self.merkle_root);
958
959        for cb in &chunk_bytes {
960            out.extend_from_slice(&(cb.len() as u32).to_le_bytes());
961            out.extend_from_slice(cb);
962        }
963        out
964    }
965
966    /// Deserialize a parallel packet from received bytes.
967    pub fn from_bytes(data: &[u8]) -> Result<Self> {
968        const HEADER: usize = 4 + 4 + 32;
969        if data.len() < HEADER {
970            return Err(KkError::InvalidPacket("parallel packet too short".into()));
971        }
972
973        let num_chunks = u32::from_le_bytes(
974            data[..4]
975                .try_into()
976                .map_err(|_| KkError::InvalidPacket("bad chunk count".into()))?,
977        ) as usize;
978        let chunk_size = u32::from_le_bytes(
979            data[4..8]
980                .try_into()
981                .map_err(|_| KkError::InvalidPacket("bad chunk size".into()))?,
982        ) as usize;
983
984        let mut merkle_root = [0u8; 32];
985        merkle_root.copy_from_slice(&data[8..40]);
986
987        let mut offset = HEADER;
988        let mut chunks = Vec::with_capacity(num_chunks);
989        for _ in 0..num_chunks {
990            if data.len() < offset + 4 {
991                return Err(KkError::InvalidPacket(
992                    "parallel packet truncated at chunk length".into(),
993                ));
994            }
995            let cb_len = u32::from_le_bytes(
996                data[offset..offset + 4]
997                    .try_into()
998                    .map_err(|_| KkError::InvalidPacket("bad chunk byte length".into()))?,
999            ) as usize;
1000            offset += 4;
1001
1002            if data.len() < offset + cb_len {
1003                return Err(KkError::InvalidPacket(
1004                    "parallel packet truncated at chunk data".into(),
1005                ));
1006            }
1007            let chunk = KkAeadPacket::from_bytes(&data[offset..offset + cb_len])?;
1008            chunks.push(chunk);
1009            offset += cb_len;
1010        }
1011
1012        Ok(Self {
1013            chunks,
1014            chunk_size,
1015            merkle_root,
1016        })
1017    }
1018}
1019
1020/// Internal: XOR input with the KK-derived keystream.
1021///
1022/// Processes chunks in batches of 8 using AVX-512 vectorized KDF when
1023/// available, falling back to per-chunk scalar derivation otherwise.
1024/// Within each batch, rayon parallelises across CPU cores.
1025fn xor_with_keystream(
1026    shared_secret: &[u8],
1027    snapshot: &EntropySnapshot,
1028    input: &[u8],
1029) -> Result<Vec<u8>> {
1030    let mut output = vec![0u8; input.len()];
1031    let batch_bytes = CHUNK_SIZE * 8;
1032
1033    let result = output.par_chunks_mut(batch_bytes).enumerate().try_for_each(
1034        |(batch_idx, out_batch)| -> Result<()> {
1035            let base_chunk = batch_idx * 8;
1036            let in_base = base_chunk * CHUNK_SIZE;
1037
1038            if out_batch.len() == batch_bytes {
1039                // Full batch of 8 chunks, use vectorized KDF
1040                let mut keys = kdf::derive_symbol_key_batch(
1041                    shared_secret,
1042                    snapshot,
1043                    base_chunk as u64,
1044                    CHUNK_SIZE,
1045                )?;
1046
1047                for (c, key) in keys.iter_mut().enumerate() {
1048                    let out_off = c * CHUNK_SIZE;
1049                    let in_off = in_base + c * CHUNK_SIZE;
1050                    for i in 0..CHUNK_SIZE {
1051                        out_batch[out_off + i] = input[in_off + i] ^ key[i];
1052                    }
1053                    key.zeroize();
1054                }
1055            } else {
1056                // Partial tail batch, scalar per-chunk
1057                let chunks_in_batch = out_batch.len().div_ceil(CHUNK_SIZE);
1058
1059                for c in 0..chunks_in_batch {
1060                    let chunk_idx = base_chunk + c;
1061                    let out_off = c * CHUNK_SIZE;
1062                    let chunk_len = (out_batch.len() - out_off).min(CHUNK_SIZE);
1063                    let in_off = in_base + c * CHUNK_SIZE;
1064
1065                    let mut key_bytes = kdf::derive_symbol_key(
1066                        shared_secret,
1067                        snapshot,
1068                        chunk_idx as u64,
1069                        chunk_len,
1070                    )?;
1071
1072                    for i in 0..chunk_len {
1073                        out_batch[out_off + i] = input[in_off + i] ^ key_bytes[i];
1074                    }
1075                    key_bytes.zeroize();
1076                }
1077            }
1078
1079            Ok(())
1080        },
1081    );
1082
1083    match result {
1084        Ok(()) => Ok(output),
1085        Err(e) => {
1086            output.zeroize();
1087            Err(e)
1088        }
1089    }
1090}
1091
1092/// Sequential variant of [`xor_with_keystream`] for use inside an outer
1093/// `par_iter` (e.g. `encode_parallel`). Avoids nested Rayon parallelism
1094/// which causes thread-pool contention. Produces byte-identical output.
1095fn xor_with_keystream_seq(
1096    shared_secret: &[u8],
1097    snapshot: &EntropySnapshot,
1098    input: &[u8],
1099) -> Result<Vec<u8>> {
1100    let mut output = vec![0u8; input.len()];
1101    let batch_bytes = CHUNK_SIZE * 8;
1102
1103    for (batch_idx, out_batch) in output.chunks_mut(batch_bytes).enumerate() {
1104        let base_chunk = batch_idx * 8;
1105        let in_base = base_chunk * CHUNK_SIZE;
1106
1107        if out_batch.len() == batch_bytes {
1108            let mut keys = kdf::derive_symbol_key_batch(
1109                shared_secret,
1110                snapshot,
1111                base_chunk as u64,
1112                CHUNK_SIZE,
1113            )?;
1114
1115            for (c, key) in keys.iter_mut().enumerate() {
1116                let out_off = c * CHUNK_SIZE;
1117                let in_off = in_base + c * CHUNK_SIZE;
1118                for i in 0..CHUNK_SIZE {
1119                    out_batch[out_off + i] = input[in_off + i] ^ key[i];
1120                }
1121                key.zeroize();
1122            }
1123        } else {
1124            let chunks_in_batch = out_batch.len().div_ceil(CHUNK_SIZE);
1125
1126            for c in 0..chunks_in_batch {
1127                let chunk_idx = base_chunk + c;
1128                let out_off = c * CHUNK_SIZE;
1129                let chunk_len = (out_batch.len() - out_off).min(CHUNK_SIZE);
1130                let in_off = in_base + c * CHUNK_SIZE;
1131
1132                let mut key_bytes =
1133                    kdf::derive_symbol_key(shared_secret, snapshot, chunk_idx as u64, chunk_len)?;
1134
1135                for i in 0..chunk_len {
1136                    out_batch[out_off + i] = input[in_off + i] ^ key_bytes[i];
1137                }
1138                key_bytes.zeroize();
1139            }
1140        }
1141    }
1142
1143    Ok(output)
1144}
1145
1146/// AEAD encode a single chunk using sequential keystream XOR.
1147/// Used inside `encode_parallel` to avoid nested Rayon parallelism.
1148fn encode_aead_par_inner(
1149    shared_secret: &[u8],
1150    plaintext: &[u8],
1151    aad: &[u8],
1152    snapshot: EntropySnapshot,
1153) -> Result<KkAeadPacket> {
1154    let ciphertext = xor_with_keystream_seq(shared_secret, &snapshot, plaintext)?;
1155    let commitment = temporal::commit_aead(shared_secret, &snapshot, &ciphertext, aad)?;
1156    Ok(KkAeadPacket {
1157        aad: aad.to_vec(),
1158        ciphertext,
1159        entropy_snapshot: snapshot,
1160        commitment,
1161    })
1162}
1163
1164/// Decode a single AEAD chunk using sequential keystream XOR.
1165/// Used inside `decode_parallel` to avoid nested Rayon parallelism.
1166fn decode_aead_seq(shared_secret: &[u8], packet: &KkAeadPacket) -> Result<Vec<u8>> {
1167    temporal::verify_aead(
1168        shared_secret,
1169        &packet.entropy_snapshot,
1170        &packet.ciphertext,
1171        &packet.aad,
1172        &packet.commitment,
1173    )?;
1174    xor_with_keystream_seq(shared_secret, &packet.entropy_snapshot, &packet.ciphertext)
1175}
1176
1177// ─────────────────────────────────────────────────────────────────
1178//  Streaming API - incremental encode / decode
1179// ─────────────────────────────────────────────────────────────────
1180
1181/// Incremental encoder that accumulates plaintext via [`update`](StreamEncoder::update)
1182/// and produces a single [`KkPacket`] on [`finalize`](StreamEncoder::finalize).
1183///
1184/// The entropy snapshot is captured once at construction and reused for the
1185/// entire stream. This avoids capturing a new snapshot per chunk while still
1186/// binding every byte to the same unrepeatable moment.
1187///
1188/// # Example
1189///
1190/// ```rust
1191/// use kk_crypto::StreamEncoder;
1192///
1193/// let secret = b"our-shared-secret";
1194/// let mut enc = StreamEncoder::new(secret).unwrap();
1195/// enc.update(b"Hello ");
1196/// enc.update(b"KK!");
1197/// let packet = enc.finalize().unwrap();
1198/// ```
1199pub struct StreamEncoder {
1200    shared_secret: Vec<u8>,
1201    buffer: Vec<u8>,
1202    snapshot: EntropySnapshot,
1203}
1204
1205impl StreamEncoder {
1206    /// Create a new streaming encoder.
1207    ///
1208    /// Captures the entropy snapshot immediately so the caller can feed
1209    /// data at their own pace without worrying about timing skew.
1210    pub fn new(shared_secret: &[u8]) -> Result<Self> {
1211        let snapshot = entropy::gather()?;
1212        Ok(Self {
1213            shared_secret: shared_secret.to_vec(),
1214            buffer: Vec::new(),
1215            snapshot,
1216        })
1217    }
1218
1219    /// Append plaintext bytes to the internal buffer.
1220    pub fn update(&mut self, data: &[u8]) {
1221        self.buffer.extend_from_slice(data);
1222    }
1223
1224    /// Consume the encoder and produce the final [`KkPacket`].
1225    ///
1226    /// Returns [`KkError::EmptyInput`] if no bytes were fed via [`update`](Self::update).
1227    pub fn finalize(mut self) -> Result<KkPacket> {
1228        if self.buffer.is_empty() {
1229            return Err(KkError::EmptyInput);
1230        }
1231
1232        let ciphertext = xor_with_keystream(&self.shared_secret, &self.snapshot, &self.buffer)?;
1233        let commitment = temporal::commit(&self.shared_secret, &self.snapshot, &ciphertext)?;
1234
1235        self.shared_secret.zeroize();
1236        self.buffer.zeroize();
1237
1238        Ok(KkPacket {
1239            ciphertext,
1240            entropy_snapshot: self.snapshot.clone(),
1241            commitment,
1242        })
1243    }
1244}
1245
1246impl Drop for StreamEncoder {
1247    fn drop(&mut self) {
1248        self.shared_secret.zeroize();
1249        self.buffer.zeroize();
1250    }
1251}
1252
1253/// Incremental decoder that accumulates ciphertext via [`update`](StreamDecoder::update)
1254/// and decodes at [`finalize`](StreamDecoder::finalize) using a pre-received
1255/// entropy snapshot and commitment.
1256///
1257/// # Example
1258///
1259/// ```rust,no_run
1260/// use kk_crypto::{StreamDecoder, StreamEncoder};
1261///
1262/// let secret = b"our-shared-secret";
1263///
1264/// // Sender side (streaming encode)
1265/// let mut enc = StreamEncoder::new(secret).unwrap();
1266/// enc.update(b"Hello ");
1267/// enc.update(b"KK!");
1268/// let packet = enc.finalize().unwrap();
1269///
1270/// // Receiver side (streaming decode)
1271/// let mut dec = StreamDecoder::new(
1272///     secret,
1273///     packet.entropy_snapshot.clone(),
1274///     packet.commitment.clone(),
1275/// );
1276/// dec.update(&packet.ciphertext);
1277/// let plaintext = dec.finalize().unwrap();
1278/// assert_eq!(plaintext, b"Hello KK!");
1279/// ```
1280pub struct StreamDecoder {
1281    shared_secret: Vec<u8>,
1282    buffer: Vec<u8>,
1283    snapshot: EntropySnapshot,
1284    commitment: TemporalCommitment,
1285}
1286
1287impl StreamDecoder {
1288    /// Create a new streaming decoder.
1289    ///
1290    /// The caller must supply the entropy snapshot and commitment from
1291    /// the packet header (typically received before the ciphertext body).
1292    pub fn new(
1293        shared_secret: &[u8],
1294        snapshot: EntropySnapshot,
1295        commitment: TemporalCommitment,
1296    ) -> Self {
1297        Self {
1298            shared_secret: shared_secret.to_vec(),
1299            buffer: Vec::new(),
1300            snapshot,
1301            commitment,
1302        }
1303    }
1304
1305    /// Append ciphertext bytes to the internal buffer.
1306    pub fn update(&mut self, data: &[u8]) {
1307        self.buffer.extend_from_slice(data);
1308    }
1309
1310    /// Consume the decoder, verify integrity, and return plaintext.
1311    ///
1312    /// Returns [`KkError::EmptyInput`] if no bytes were fed, or a
1313    /// temporal verification error if the commitment does not match.
1314    pub fn finalize(mut self) -> Result<Vec<u8>> {
1315        if self.buffer.is_empty() {
1316            return Err(KkError::EmptyInput);
1317        }
1318
1319        // Verify temporal commitment before decoding
1320        temporal::verify(
1321            &self.shared_secret,
1322            &self.snapshot,
1323            &self.buffer,
1324            &self.commitment,
1325        )?;
1326
1327        let plaintext = xor_with_keystream(&self.shared_secret, &self.snapshot, &self.buffer)?;
1328
1329        self.shared_secret.zeroize();
1330        self.buffer.zeroize();
1331
1332        Ok(plaintext)
1333    }
1334}
1335
1336impl Drop for StreamDecoder {
1337    fn drop(&mut self) {
1338        self.shared_secret.zeroize();
1339        self.buffer.zeroize();
1340    }
1341}
1342
1343#[cfg(test)]
1344mod tests {
1345    use super::*;
1346
1347    #[test]
1348    fn encode_decode_roundtrip() {
1349        let secret = b"test-shared-secret-2026";
1350        let plaintext = b"Hello from KK! The language only existed for one cosmic instant.";
1351
1352        let packet = encode(secret, plaintext).unwrap();
1353        let decoded = decode(secret, &packet).unwrap();
1354
1355        assert_eq!(plaintext.as_slice(), decoded.as_slice());
1356    }
1357
1358    #[test]
1359    fn same_plaintext_different_ciphertext() {
1360        let secret = b"test-key";
1361        let plaintext = b"A"; // Same symbol
1362
1363        let p1 = encode(secret, plaintext).unwrap();
1364        let p2 = encode(secret, plaintext).unwrap();
1365
1366        // KK(S) at T₁ ≠ KK(S) at T₂
1367        // The same symbol encoded twice produces cryptographically unrelated values
1368        assert_ne!(
1369            p1.ciphertext, p2.ciphertext,
1370            "Same symbol at different moments MUST produce different ciphertext"
1371        );
1372    }
1373
1374    #[test]
1375    fn wrong_key_fails_decode() {
1376        let plaintext = b"secret message";
1377        let packet = encode(b"correct-key", plaintext).unwrap();
1378
1379        let result = decode(b"wrong-key", &packet);
1380        assert!(
1381            result.is_err(),
1382            "Decoding with wrong shared secret must fail commitment verification"
1383        );
1384    }
1385
1386    #[test]
1387    fn empty_input_rejected() {
1388        let result = encode(b"key", b"");
1389        assert!(result.is_err());
1390    }
1391
1392    #[test]
1393    fn packet_serialization_roundtrip() {
1394        let secret = b"serialize-test";
1395        let plaintext = b"test packet roundtrip";
1396
1397        let packet = encode(secret, plaintext).unwrap();
1398        let bytes = packet.to_bytes();
1399        let restored = KkPacket::from_bytes(&bytes).unwrap();
1400
1401        let decoded = decode(secret, &restored).unwrap();
1402        assert_eq!(plaintext.as_slice(), decoded.as_slice());
1403    }
1404
1405    #[test]
1406    fn tampered_ciphertext_detected() {
1407        let secret = b"tamper-test";
1408        let packet = encode(secret, b"important data").unwrap();
1409
1410        let mut tampered = packet.clone();
1411        tampered.ciphertext[0] ^= 0xFF; // Flip bits
1412
1413        let result = decode(secret, &tampered);
1414        assert!(
1415            result.is_err(),
1416            "Tampered ciphertext must fail commitment verification"
1417        );
1418    }
1419
1420    #[test]
1421    fn large_message_works() {
1422        let secret = b"large-msg-test";
1423        let plaintext: Vec<u8> = (0..10_000).map(|i| (i % 256) as u8).collect();
1424
1425        let packet = encode(secret, &plaintext).unwrap();
1426        let decoded = decode(secret, &packet).unwrap();
1427
1428        assert_eq!(plaintext, decoded);
1429    }
1430
1431    // ── Split-channel tests ─────────────────────────────────────
1432
1433    #[test]
1434    fn split_encode_decode_roundtrip() {
1435        let secret = b"split-test-secret";
1436        let plaintext = b"Split-channel KK: ciphertext and epsilon travel separately.";
1437
1438        let (sealed, epsilon) = encode_split(secret, plaintext).unwrap();
1439        let decoded = decode_split(secret, &sealed, &epsilon).unwrap();
1440
1441        assert_eq!(plaintext.as_slice(), decoded.as_slice());
1442    }
1443
1444    #[test]
1445    fn split_wrong_key_fails() {
1446        let plaintext = b"split secret";
1447        let (sealed, epsilon) = encode_split(b"right-key", plaintext).unwrap();
1448
1449        let result = decode_split(b"wrong-key", &sealed, &epsilon);
1450        assert!(result.is_err(), "Wrong passphrase must fail");
1451    }
1452
1453    #[test]
1454    fn split_wrong_epsilon_fails() {
1455        let secret = b"epsilon-test";
1456        let plaintext = b"the moment matters";
1457
1458        let (sealed, _real_epsilon) = encode_split(secret, plaintext).unwrap();
1459
1460        // An attacker fabricates a different ε
1461        let fake_epsilon = entropy::gather().unwrap();
1462
1463        let result = decode_split(secret, &sealed, &fake_epsilon);
1464        assert!(
1465            result.is_err(),
1466            "Wrong epsilon must fail commitment verification"
1467        );
1468    }
1469
1470    #[test]
1471    fn split_sealed_message_serialization() {
1472        let secret = b"serde-split";
1473        let plaintext = b"roundtrip the sealed half";
1474
1475        let (sealed, epsilon) = encode_split(secret, plaintext).unwrap();
1476
1477        // Serialize / deserialize the sealed message (Channel 1)
1478        let wire = sealed.to_bytes();
1479        let restored = KkSealedMessage::from_bytes(&wire).unwrap();
1480
1481        // Serialize / deserialize epsilon (Channel 2)
1482        let eps_wire = epsilon.to_bytes();
1483        let restored_eps = EntropySnapshot::from_bytes(&eps_wire).unwrap();
1484
1485        let decoded = decode_split(secret, &restored, &restored_eps).unwrap();
1486        assert_eq!(plaintext.as_slice(), decoded.as_slice());
1487    }
1488
1489    #[test]
1490    fn split_empty_input_rejected() {
1491        let result = encode_split(b"key", b"");
1492        assert!(result.is_err());
1493    }
1494
1495    // ── Bound-commitment tests ──────────────────────────────────
1496
1497    #[test]
1498    fn bound_encode_decode_roundtrip() {
1499        let secret = b"bound-test-secret";
1500        let plaintext = b"Temporal proof: challenge-response freshness.";
1501
1502        let nonce = temporal::generate_challenge().unwrap();
1503        let packet = encode_bound(secret, plaintext, &nonce, &temporal::GENESIS_MAC).unwrap();
1504        let decoded = decode_bound(secret, &packet, &nonce, Duration::from_secs(30)).unwrap();
1505
1506        assert_eq!(plaintext.as_slice(), decoded.as_slice());
1507    }
1508
1509    #[test]
1510    fn bound_wrong_nonce_rejected() {
1511        let secret = b"nonce-reject";
1512        let nonce = temporal::generate_challenge().unwrap();
1513        let wrong_nonce = temporal::generate_challenge().unwrap();
1514
1515        let packet = encode_bound(secret, b"test data", &nonce, &temporal::GENESIS_MAC).unwrap();
1516        let result = decode_bound(secret, &packet, &wrong_nonce, Duration::from_secs(30));
1517        assert!(result.is_err(), "Wrong nonce must be rejected");
1518    }
1519
1520    #[test]
1521    fn bound_wrong_key_rejected() {
1522        let nonce = temporal::generate_challenge().unwrap();
1523        let packet = encode_bound(b"right-key", b"secret", &nonce, &temporal::GENESIS_MAC).unwrap();
1524
1525        let result = decode_bound(b"wrong-key", &packet, &nonce, Duration::from_secs(30));
1526        assert!(result.is_err(), "Wrong key must fail");
1527    }
1528
1529    #[test]
1530    fn bound_packet_serialization_roundtrip() {
1531        let secret = b"bound-serde";
1532        let plaintext = b"serialize a bound packet";
1533
1534        let nonce = temporal::generate_challenge().unwrap();
1535        let packet = encode_bound(secret, plaintext, &nonce, &temporal::GENESIS_MAC).unwrap();
1536
1537        let bytes = packet.to_bytes();
1538        let restored = KkBoundPacket::from_bytes(&bytes).unwrap();
1539
1540        let decoded = decode_bound(secret, &restored, &nonce, Duration::from_secs(30)).unwrap();
1541        assert_eq!(plaintext.as_slice(), decoded.as_slice());
1542    }
1543
1544    #[test]
1545    fn bound_tampered_ciphertext_detected() {
1546        let secret = b"tamper-bound";
1547        let nonce = temporal::generate_challenge().unwrap();
1548        let mut packet =
1549            encode_bound(secret, b"important", &nonce, &temporal::GENESIS_MAC).unwrap();
1550        packet.ciphertext[0] ^= 0xFF;
1551
1552        let result = decode_bound(secret, &packet, &nonce, Duration::from_secs(30));
1553        assert!(result.is_err(), "Tampered ciphertext must fail");
1554    }
1555
1556    #[test]
1557    fn bound_chain_ordering() {
1558        let secret = b"chain-test";
1559        let nonce1 = temporal::generate_challenge().unwrap();
1560        let nonce2 = temporal::generate_challenge().unwrap();
1561
1562        let p1 = encode_bound(secret, b"first", &nonce1, &temporal::GENESIS_MAC).unwrap();
1563        let p2 = encode_bound(secret, b"second", &nonce2, &p1.proof.mac).unwrap();
1564
1565        // Both decode successfully with correct nonces
1566        let d1 = decode_bound(secret, &p1, &nonce1, Duration::from_secs(30)).unwrap();
1567        let d2 = decode_bound(secret, &p2, &nonce2, Duration::from_secs(30)).unwrap();
1568        assert_eq!(d1, b"first");
1569        assert_eq!(d2, b"second");
1570
1571        // Chain is verifiable: p2 references p1
1572        assert_eq!(p2.proof.prev_mac, p1.proof.mac);
1573    }
1574
1575    #[test]
1576    fn bound_empty_input_rejected() {
1577        let nonce = temporal::generate_challenge().unwrap();
1578        let result = encode_bound(b"key", b"", &nonce, &temporal::GENESIS_MAC);
1579        assert!(result.is_err());
1580    }
1581
1582    // ── Streaming API tests ──────────────────────────────────────
1583
1584    #[test]
1585    fn stream_encode_decode_roundtrip() {
1586        let secret = b"stream-secret";
1587        let mut enc = StreamEncoder::new(secret).unwrap();
1588        enc.update(b"Hello ");
1589        enc.update(b"KK ");
1590        enc.update(b"Stream!");
1591        let packet = enc.finalize().unwrap();
1592
1593        let plaintext = decode(secret, &packet).unwrap();
1594        assert_eq!(plaintext, b"Hello KK Stream!");
1595    }
1596
1597    #[test]
1598    fn stream_decoder_roundtrip() {
1599        let secret = b"stream-dec-secret";
1600        let mut enc = StreamEncoder::new(secret).unwrap();
1601        enc.update(b"chunk1");
1602        enc.update(b"chunk2");
1603        let packet = enc.finalize().unwrap();
1604
1605        let mut dec = StreamDecoder::new(
1606            secret,
1607            packet.entropy_snapshot.clone(),
1608            packet.commitment.clone(),
1609        );
1610        dec.update(&packet.ciphertext);
1611        let plaintext = dec.finalize().unwrap();
1612        assert_eq!(plaintext, b"chunk1chunk2");
1613    }
1614
1615    #[test]
1616    fn stream_decoder_incremental_ciphertext() {
1617        let secret = b"stream-incr-secret";
1618        let mut enc = StreamEncoder::new(secret).unwrap();
1619        enc.update(b"ABCDEFGHIJ");
1620        let packet = enc.finalize().unwrap();
1621
1622        // Feed ciphertext in two parts
1623        let mid = packet.ciphertext.len() / 2;
1624        let mut dec = StreamDecoder::new(
1625            secret,
1626            packet.entropy_snapshot.clone(),
1627            packet.commitment.clone(),
1628        );
1629        dec.update(&packet.ciphertext[..mid]);
1630        dec.update(&packet.ciphertext[mid..]);
1631        let plaintext = dec.finalize().unwrap();
1632        assert_eq!(plaintext, b"ABCDEFGHIJ");
1633    }
1634
1635    #[test]
1636    fn stream_encoder_empty_rejected() {
1637        let enc = StreamEncoder::new(b"key").unwrap();
1638        assert!(enc.finalize().is_err());
1639    }
1640
1641    #[test]
1642    fn stream_decoder_empty_rejected() {
1643        let snapshot = crate::entropy::gather().unwrap();
1644        let commitment = crate::temporal::commit(b"key", &snapshot, b"dummy").unwrap();
1645        let dec = StreamDecoder::new(b"key", snapshot, commitment);
1646        assert!(dec.finalize().is_err());
1647    }
1648
1649    #[test]
1650    fn stream_matches_oneshot() {
1651        let secret = b"stream-vs-oneshot";
1652        let data = b"The quick brown fox jumps over the lazy dog";
1653
1654        // One-shot encode with a specific snapshot
1655        let snapshot = crate::entropy::gather().unwrap();
1656        let oneshot = encode_with_snapshot(secret, data, snapshot.clone()).unwrap();
1657
1658        // Streaming encode would use its own snapshot, so we just verify
1659        // the streaming roundtrip produces the same plaintext
1660        let mut enc = StreamEncoder::new(secret).unwrap();
1661        enc.update(data);
1662        let stream_pkt = enc.finalize().unwrap();
1663
1664        let oneshot_pt = decode(secret, &oneshot).unwrap();
1665        let stream_pt = decode(secret, &stream_pkt).unwrap();
1666        assert_eq!(oneshot_pt, stream_pt);
1667        assert_eq!(&stream_pt[..], &data[..]);
1668    }
1669
1670    // ─── Parallel encode/decode tests ───────────────────────────
1671
1672    #[test]
1673    fn parallel_roundtrip_small() {
1674        let secret = b"parallel-test-secret";
1675        let plaintext = b"Hello parallel world!";
1676        let aad = b"test-aad";
1677
1678        let packet = encode_parallel(secret, plaintext, aad, 8, None).unwrap();
1679        assert!(packet.chunks.len() >= 2); // 21 bytes / 8 = 3 chunks
1680        let decoded = decode_parallel(secret, &packet).unwrap();
1681        assert_eq!(&decoded[..], &plaintext[..]);
1682    }
1683
1684    #[test]
1685    fn parallel_roundtrip_exact_chunk() {
1686        let secret = b"exact-chunk-secret";
1687        let plaintext = vec![0xABu8; 1024];
1688        let aad = b"exact";
1689
1690        let packet = encode_parallel(secret, &plaintext, aad, 1024, None).unwrap();
1691        assert_eq!(packet.chunks.len(), 1);
1692        let decoded = decode_parallel(secret, &packet).unwrap();
1693        assert_eq!(decoded, plaintext);
1694    }
1695
1696    #[test]
1697    fn parallel_roundtrip_large() {
1698        let secret = b"large-parallel-secret";
1699        let plaintext = vec![42u8; 1_000_000]; // 1 MB
1700        let aad = b"large-aad";
1701        let chunk_size = PARALLEL_CHUNK_SIZE; // 1 MiB
1702
1703        let packet = encode_parallel(secret, &plaintext, aad, chunk_size, None).unwrap();
1704        assert_eq!(packet.chunks.len(), 1);
1705        let decoded = decode_parallel(secret, &packet).unwrap();
1706        assert_eq!(decoded, plaintext);
1707    }
1708
1709    #[test]
1710    fn parallel_merkle_detects_reorder() {
1711        let secret = b"merkle-reorder-test";
1712        let plaintext = vec![0u8; 2048];
1713        let aad = b"reorder";
1714
1715        let mut packet = encode_parallel(secret, &plaintext, aad, 512, None).unwrap();
1716        assert!(packet.chunks.len() >= 2);
1717
1718        // Swap first two chunks - Merkle root should no longer match
1719        packet.chunks.swap(0, 1);
1720        let result = decode_parallel(secret, &packet);
1721        assert!(result.is_err());
1722    }
1723
1724    #[test]
1725    fn parallel_merkle_detects_removal() {
1726        let secret = b"merkle-removal-test";
1727        let plaintext = vec![0u8; 2048];
1728        let aad = b"removal";
1729
1730        let mut packet = encode_parallel(secret, &plaintext, aad, 512, None).unwrap();
1731        assert!(packet.chunks.len() >= 2);
1732
1733        // Remove a chunk - Merkle root should no longer match
1734        packet.chunks.pop();
1735        let result = decode_parallel(secret, &packet);
1736        assert!(result.is_err());
1737    }
1738
1739    #[test]
1740    fn parallel_serde_roundtrip() {
1741        let secret = b"serde-parallel-secret";
1742        let plaintext = b"serialize me in parallel chunks";
1743        let aad = b"serde-aad";
1744
1745        let packet = encode_parallel(secret, plaintext, aad, 10, None).unwrap();
1746        let bytes = packet.to_bytes();
1747        let restored = KkParallelPacket::from_bytes(&bytes).unwrap();
1748
1749        assert_eq!(restored.chunks.len(), packet.chunks.len());
1750        assert_eq!(restored.chunk_size, packet.chunk_size);
1751        assert_eq!(restored.merkle_root, packet.merkle_root);
1752
1753        let decoded = decode_parallel(secret, &restored).unwrap();
1754        assert_eq!(&decoded[..], &plaintext[..]);
1755    }
1756
1757    #[test]
1758    fn parallel_empty_input_rejected() {
1759        let secret = b"empty-test";
1760        let result = encode_parallel(secret, b"", b"aad", 1024, None);
1761        assert!(result.is_err());
1762    }
1763
1764    #[test]
1765    fn parallel_zero_chunk_size_rejected() {
1766        let secret = b"zero-chunk";
1767        let result = encode_parallel(secret, b"data", b"aad", 0, None);
1768        assert!(result.is_err());
1769    }
1770}