Skip to main content

kk_crypto/
session.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 Rope Ratchet: Forward Secrecy
9//!
10//! A 4-strand ratchet that provides ~192-bit forward secrecy using
11//! only KK primitives. Once a message key is derived and the ratchet
12//! advances, the old state is zeroized and irrecoverable.
13//!
14//! ## Strands
15//!
16//! | Strand    | Source            | Purpose                              |
17//! |-----------|-------------------|--------------------------------------|
18//! | Entropy   | `EntropySnapshot` | Environmental randomness per message |
19//! | Temporal  | `ε.timestamp`     | Binds ratchet to real-world time     |
20//! | Chain     | Previous chain    | One-way forward secrecy              |
21//! | Counter   | Monotonic `u64`   | Deterministic ordering               |
22//!
23//! ## The KK Innovation
24//!
25//! All 4 strand outputs are fed into a single KK sponge absorb phase
26//! with entropy-derived rotations. The 32-round permutation (960 MFR +
27//! 480 DDR operations) mixes all strands simultaneously, and the
28//! cipher's mathematical structure changes per message because the
29//! rotation schedule is derived from the entropy snapshot.
30//!
31//! This is fundamentally different from existing ratchet designs
32//! (Signal Double Ratchet, etc.) where the cipher is fixed and only
33//! the keys change. In KK, both the key AND the algebraic structure
34//! of the permutation change with every message.
35//!
36//! ## Security
37//!
38//! - ~192-bit forward secrecy (384-bit sponge capacity)
39//! - Per-message cipher structure rotation via entropy-derived schedules
40//! - 4 independent entropy sources mixed through 32-round permutation
41//! - Stronger than Signal's Double Ratchet (~128-bit DH security)
42//!
43//! ## Protocol
44//!
45//! ```text
46//! Sender:    packet = encode_session(&mut send_ratchet, plaintext)
47//!            transmit(packet.to_bytes())
48//!
49//! Receiver:  packet = RopePacket::from_bytes(&wire_data)
50//!            plaintext = decode_session(&mut recv_ratchet, &packet)
51//! ```
52//!
53//! For bidirectional communication, each party creates two ratchets
54//! with different contexts (one for sending, one for receiving).
55//!
56//! Messages must be processed in order (strict counter synchronization).
57
58use crate::codec::{self, KkAeadPacket, KkPacket};
59use crate::entropy::{self, EntropySnapshot};
60use crate::error::{KkError, Result};
61use crate::kk_mix;
62use zeroize::Zeroize;
63
64/// Domain separation byte for the Rope Ratchet sponge (0x04).
65///
66/// Distinct from DOMAIN_HASH (0x01), DOMAIN_KDF (0x02), DOMAIN_MAC (0x03).
67const DOMAIN_SESSION: &[u8] = b"KK-rope-mix-v1";
68
69// KDF info labels for strand evolution
70const STRAND_ENT_INFO: &[u8] = b"KK-rope-ent-v1";
71const STRAND_TMP_INFO: &[u8] = b"KK-rope-tmp-v1";
72const STRAND_CHN_INFO: &[u8] = b"KK-rope-chn-v1";
73
74// KDF info labels for initial strand derivation from shared secret
75const INIT_ENT_INFO: &[u8] = b"KK-rope-init-ent";
76const INIT_TMP_INFO: &[u8] = b"KK-rope-init-tmp";
77const INIT_CHN_INFO: &[u8] = b"KK-rope-init-chn";
78
79// ─────────────────────────────────────────────────────────────────
80//  RopeStep: metadata for one ratchet advance
81// ─────────────────────────────────────────────────────────────────
82
83/// Metadata from a single ratchet step, embedded in messages so the
84/// receiver can perform the same derivation.
85///
86/// Contains the entropy snapshot (the unrepeatable moment) and the
87/// message counter (for ordering). Both are needed to reproduce
88/// the exact strand evolution on the receiving side.
89#[derive(Clone)]
90pub struct RopeStep {
91    /// The entropy snapshot captured during this step.
92    pub snapshot: EntropySnapshot,
93    /// The message counter at this step.
94    pub counter: u64,
95}
96
97impl RopeStep {
98    /// Serialized size: 8 (counter) + 48 (snapshot) = 56 bytes.
99    pub const BYTES: usize = 8 + 48;
100
101    /// Serialize the step metadata for transmission.
102    pub fn to_bytes(&self) -> Vec<u8> {
103        let mut out = Vec::with_capacity(Self::BYTES);
104        out.extend_from_slice(&self.counter.to_le_bytes());
105        out.extend_from_slice(&self.snapshot.to_bytes());
106        out
107    }
108
109    /// Deserialize step metadata from received bytes.
110    pub fn from_bytes(data: &[u8]) -> Result<Self> {
111        if data.len() < Self::BYTES {
112            return Err(KkError::InvalidPacket(format!(
113                "rope step too short: need {}, got {}",
114                Self::BYTES,
115                data.len()
116            )));
117        }
118        let counter = u64::from_le_bytes(
119            data[..8]
120                .try_into()
121                .map_err(|_| KkError::InvalidPacket("bad counter bytes".into()))?,
122        );
123        let snapshot = EntropySnapshot::from_bytes(&data[8..56])?;
124        Ok(Self { snapshot, counter })
125    }
126}
127
128// ─────────────────────────────────────────────────────────────────
129//  RopeRatchet: the 4-strand forward-secret ratchet
130// ─────────────────────────────────────────────────────────────────
131
132/// A forward-secret session ratchet built on 4 strands mixed through
133/// the KK sponge.
134///
135/// Each call to [`advance`](Self::advance) or [`receive`](Self::receive)
136/// irreversibly evolves the internal state. Old message keys become
137/// unrecoverable, providing forward secrecy at ~192-bit security
138/// (inherited from the KK sponge's 384-bit capacity).
139///
140/// # Two-Way Communication
141///
142/// For bidirectional messaging, create two ratchets with different
143/// contexts:
144///
145/// ```rust
146/// use kk_crypto::session::RopeRatchet;
147///
148/// let secret = b"shared-secret";
149/// let mut send = RopeRatchet::new(secret, b"alice-to-bob").unwrap();
150/// let mut recv = RopeRatchet::new(secret, b"bob-to-alice").unwrap();
151/// ```
152///
153/// Alice uses `send` for outgoing and a second ratchet initialized
154/// with `b"bob-to-alice"` for incoming. Bob mirrors this.
155pub struct RopeRatchet {
156    /// Entropy strand: evolved from environmental randomness.
157    entropy_strand: [u8; 32],
158    /// Temporal strand: evolved from timestamps.
159    temporal_strand: [u8; 32],
160    /// Chain strand: one-way ratchet for forward secrecy.
161    chain_strand: [u8; 32],
162    /// Monotonically increasing message counter.
163    counter: u64,
164}
165
166impl Drop for RopeRatchet {
167    fn drop(&mut self) {
168        self.entropy_strand.zeroize();
169        self.temporal_strand.zeroize();
170        self.chain_strand.zeroize();
171        self.counter = 0;
172    }
173}
174
175impl RopeRatchet {
176    /// Create a new ratchet from a shared secret and direction context.
177    ///
178    /// The `context` parameter provides domain separation between
179    /// directions (e.g., `b"alice-to-bob"` vs `b"bob-to-alice"`).
180    /// Two ratchets with the same `shared_secret` but different
181    /// `context` values produce completely independent key streams.
182    ///
183    /// Both parties must use identical `(shared_secret, context)` pairs
184    /// for the same communication direction.
185    pub fn new(shared_secret: &[u8], context: &[u8]) -> Result<Self> {
186        // Hash the context to produce a fixed-size salt for KDF
187        let salt = kk_mix::kk_hash(context);
188
189        // Derive independent initial seeds for each strand
190        let mut e = kk_mix::kk_kdf(shared_secret, &salt, INIT_ENT_INFO, 32);
191        let mut t = kk_mix::kk_kdf(shared_secret, &salt, INIT_TMP_INFO, 32);
192        let mut c = kk_mix::kk_kdf(shared_secret, &salt, INIT_CHN_INFO, 32);
193
194        let mut entropy_strand = [0u8; 32];
195        let mut temporal_strand = [0u8; 32];
196        let mut chain_strand = [0u8; 32];
197
198        entropy_strand.copy_from_slice(&e);
199        temporal_strand.copy_from_slice(&t);
200        chain_strand.copy_from_slice(&c);
201
202        e.zeroize();
203        t.zeroize();
204        c.zeroize();
205
206        Ok(Self {
207            entropy_strand,
208            temporal_strand,
209            chain_strand,
210            counter: 0,
211        })
212    }
213
214    /// Advance the ratchet and derive a message key (sender side).
215    ///
216    /// Gathers fresh entropy, evolves all 4 strands, and mixes them
217    /// through the KK sponge with entropy-derived rotations.
218    ///
219    /// Returns the 32-byte message key and a [`RopeStep`] that must
220    /// be included in the transmitted message so the receiver can
221    /// derive the same key.
222    ///
223    /// # Security
224    ///
225    /// The returned message key is sensitive material. Zeroize it
226    /// after use. The old chain state is destroyed during this call,
227    /// providing forward secrecy.
228    pub fn advance(&mut self) -> Result<([u8; 32], RopeStep)> {
229        let snapshot = entropy::gather()?;
230        let key = self.step(&snapshot)?;
231        let step = RopeStep {
232            snapshot,
233            counter: self.counter,
234        };
235        Ok((key, step))
236    }
237
238    /// Advance the ratchet using received step metadata (receiver side).
239    ///
240    /// Uses the sender's entropy snapshot and counter from the
241    /// [`RopeStep`] to reproduce the exact same derivation, yielding
242    /// the identical message key.
243    ///
244    /// # Errors
245    ///
246    /// Returns `KkError::InvalidPacket` if the counter is not exactly
247    /// one more than the current counter (strict ordering).
248    pub fn receive(&mut self, step: &RopeStep) -> Result<[u8; 32]> {
249        let expected = self.counter + 1;
250        if step.counter != expected {
251            return Err(KkError::InvalidPacket(format!(
252                "counter mismatch: expected {expected}, got {} (strict ordering)",
253                step.counter
254            )));
255        }
256        self.step(&step.snapshot)
257    }
258
259    /// The current message counter (0 = no messages yet).
260    pub fn counter(&self) -> u64 {
261        self.counter
262    }
263
264    /// Advance with a caller-supplied snapshot (deterministic, for test vectors).
265    #[doc(hidden)]
266    pub fn advance_with_snapshot(
267        &mut self,
268        snapshot: EntropySnapshot,
269    ) -> Result<([u8; 32], RopeStep)> {
270        let key = self.step(&snapshot)?;
271        let step = RopeStep {
272            snapshot,
273            counter: self.counter,
274        };
275        Ok((key, step))
276    }
277
278    /// Core ratchet step: evolve all 4 strands and mix through sponge.
279    ///
280    /// This is where the KK innovation lives. All 4 strand outputs
281    /// are concatenated and fed into `kk_kdf` as the key, with the
282    /// entropy snapshot bytes as salt. Internally, `kk_kdf` creates
283    /// a sponge with entropy-derived rotations and runs the full
284    /// 32-round permutation (960 MFR + 480 DDR), mixing all strands
285    /// simultaneously. The cipher's mathematical structure changes
286    /// per message because the rotation schedule is derived from the
287    /// entropy snapshot.
288    fn step(&mut self, snapshot: &EntropySnapshot) -> Result<[u8; 32]> {
289        // ── Evolve entropy strand ──
290        // New entropy strand = KK-KDF(old_entropy, snapshot.bytes, domain)
291        let mut e_new = kk_mix::kk_kdf(&self.entropy_strand, &snapshot.bytes, STRAND_ENT_INFO, 32);
292        self.entropy_strand.copy_from_slice(&e_new);
293        e_new.zeroize();
294
295        // ── Evolve temporal strand ──
296        // New temporal strand = KK-KDF(old_temporal, timestamp_bytes, domain)
297        let ts_bytes = snapshot.timestamp_nanos.to_le_bytes();
298        let mut t_new = kk_mix::kk_kdf(&self.temporal_strand, &ts_bytes, STRAND_TMP_INFO, 32);
299        self.temporal_strand.copy_from_slice(&t_new);
300        t_new.zeroize();
301
302        // ── Evolve chain strand ──
303        // Increment counter first (counter 0 = no messages, first message = 1)
304        self.counter += 1;
305        let ctr_bytes = self.counter.to_le_bytes();
306        let mut c_new = kk_mix::kk_kdf(&self.chain_strand, &ctr_bytes, STRAND_CHN_INFO, 32);
307        self.chain_strand.copy_from_slice(&c_new);
308        c_new.zeroize();
309
310        // ── THE KK INNOVATION ──
311        // Concatenate all 4 strand outputs into a single block.
312        // This is fed as the key into kk_kdf, with the entropy snapshot
313        // as salt. Internally, kk_kdf creates a sponge with entropy-
314        // derived rotations (with_entropy_rotations(salt)), so the
315        // permutation structure itself, the algebra, changes per message.
316        // The 32-round permutation (960 MFR + 480 DDR) mixes all 4 strands
317        // simultaneously. No separate combine step needed.
318        let mut combined = Vec::with_capacity(104);
319        combined.extend_from_slice(&self.entropy_strand); // 32 bytes
320        combined.extend_from_slice(&self.temporal_strand); // 32 bytes
321        combined.extend_from_slice(&self.chain_strand); // 32 bytes
322        combined.extend_from_slice(&ctr_bytes); // 8 bytes
323
324        // kk_kdf(key=all_strands, salt=entropy, info=domain, len=64)
325        // → sponge with entropy-derived rotations absorbs everything
326        // → 32-round permutation mixes all strand material
327        // → squeeze 64 bytes: 32 for new chain + 32 for message key
328        let mut output = kk_mix::kk_kdf(&combined, &snapshot.bytes, DOMAIN_SESSION, 64);
329        combined.zeroize();
330
331        // First 32 bytes: new chain key (replaces current, forward secrecy).
332        // The old chain_strand value is gone, you cannot go backwards.
333        self.chain_strand.copy_from_slice(&output[..32]);
334
335        // Second 32 bytes: message key (returned to caller).
336        let mut message_key = [0u8; 32];
337        message_key.copy_from_slice(&output[32..64]);
338
339        output.zeroize();
340
341        Ok(message_key)
342    }
343}
344
345// ─────────────────────────────────────────────────────────────────
346//  RopePacket: encrypted message with forward secrecy
347// ─────────────────────────────────────────────────────────────────
348
349/// An encrypted message with forward secrecy.
350///
351/// Contains the ratchet step metadata (so the receiver can derive the
352/// same message key) and the inner [`KkPacket`] (the actual encrypted
353/// payload, which carries its own entropy snapshot and integrity
354/// commitment).
355///
356/// Double entropy: the ratchet step uses one `EntropySnapshot` for
357/// key derivation, and the inner `KkPacket` captures its own independent
358/// snapshot for per-symbol encryption. Two unrepeatable moments per message.
359///
360/// ## Wire Format
361///
362/// ```text
363/// [8-byte counter][48-byte ratchet snapshot][inner KkPacket bytes...]
364/// ```
365#[derive(Clone)]
366pub struct RopePacket {
367    /// Ratchet step metadata (counter + entropy snapshot).
368    pub step: RopeStep,
369    /// The inner encrypted packet, keyed by the ratchet's message key.
370    pub inner: KkPacket,
371}
372
373impl RopePacket {
374    /// Serialize the full forward-secret packet for transmission.
375    pub fn to_bytes(&self) -> Vec<u8> {
376        let step_bytes = self.step.to_bytes();
377        let inner_bytes = self.inner.to_bytes();
378        let mut out = Vec::with_capacity(step_bytes.len() + inner_bytes.len());
379        out.extend_from_slice(&step_bytes);
380        out.extend_from_slice(&inner_bytes);
381        out
382    }
383
384    /// Deserialize a forward-secret packet from received bytes.
385    pub fn from_bytes(data: &[u8]) -> Result<Self> {
386        if data.len() < RopeStep::BYTES {
387            return Err(KkError::InvalidPacket(
388                "rope packet too short for step metadata".into(),
389            ));
390        }
391        let step = RopeStep::from_bytes(&data[..RopeStep::BYTES])?;
392        let inner = KkPacket::from_bytes(&data[RopeStep::BYTES..])?;
393        Ok(Self { step, inner })
394    }
395}
396
397// ─────────────────────────────────────────────────────────────────
398//  High-level API: encode/decode with forward secrecy
399// ─────────────────────────────────────────────────────────────────
400
401/// Encode plaintext with forward secrecy.
402///
403/// Advances the ratchet, derives a per-message key, and encrypts
404/// the plaintext using the standard KK codec. The ratchet step
405/// metadata is embedded in the returned [`RopePacket`].
406///
407/// After this call, the old ratchet state is irreversibly destroyed.
408/// Even if the ratchet is later compromised, past message keys
409/// cannot be recovered.
410///
411/// # Example
412///
413/// ```rust
414/// use kk_crypto::session::{RopeRatchet, encode_session, decode_session};
415///
416/// let secret = b"shared-secret";
417/// let mut sender = RopeRatchet::new(secret, b"a-to-b").unwrap();
418/// let mut receiver = RopeRatchet::new(secret, b"a-to-b").unwrap();
419///
420/// let packet = encode_session(&mut sender, b"hello forward secrecy").unwrap();
421/// let plaintext = decode_session(&mut receiver, &packet).unwrap();
422/// assert_eq!(plaintext, b"hello forward secrecy");
423/// ```
424pub fn encode_session(ratchet: &mut RopeRatchet, plaintext: &[u8]) -> Result<RopePacket> {
425    let (mut message_key, step) = ratchet.advance()?;
426
427    // Use the ratchet's message key as the shared secret for the
428    // standard KK codec. The codec gathers its own independent entropy,
429    // adds its own temporal commitment, and produces a full KkPacket.
430    let inner = codec::encode(&message_key, plaintext)?;
431    message_key.zeroize();
432
433    Ok(RopePacket { step, inner })
434}
435
436/// Decode a forward-secret packet.
437///
438/// Advances the receiver's ratchet using the packet's step metadata,
439/// derives the same per-message key, and decrypts the inner
440/// [`KkPacket`].
441///
442/// # Errors
443///
444/// - `KkError::InvalidPacket` if the counter is out of sequence
445/// - `KkError::CommitmentMismatch` if the inner packet fails integrity
446pub fn decode_session(ratchet: &mut RopeRatchet, packet: &RopePacket) -> Result<Vec<u8>> {
447    let mut message_key = ratchet.receive(&packet.step)?;
448
449    let plaintext = codec::decode(&message_key, &packet.inner)?;
450    message_key.zeroize();
451
452    Ok(plaintext)
453}
454
455// ─────────────────────────────────────────────────────────────────
456//  Session AEAD (forward secrecy + authenticated associated data)
457// ─────────────────────────────────────────────────────────────────
458
459/// A forward-secret AEAD packet: ratchet step + inner AEAD packet.
460///
461/// Combines the Rope Ratchet (forward secrecy, key evolution) with
462/// AEAD (authenticated associated data). The AAD is authenticated
463/// but not encrypted.
464#[derive(Clone)]
465pub struct RopeAeadPacket {
466    /// The ratchet step metadata (strand, counter, direction)
467    pub step: RopeStep,
468    /// The inner KK-AEAD packet
469    pub inner: KkAeadPacket,
470}
471
472impl RopeAeadPacket {
473    /// Serialize for transmission.
474    pub fn to_bytes(&self) -> Vec<u8> {
475        let step_bytes = self.step.to_bytes();
476        let inner_bytes = self.inner.to_bytes();
477        let mut out = Vec::with_capacity(step_bytes.len() + inner_bytes.len());
478        out.extend_from_slice(&step_bytes);
479        out.extend_from_slice(&inner_bytes);
480        out
481    }
482
483    /// Deserialize from received bytes.
484    pub fn from_bytes(data: &[u8]) -> Result<Self> {
485        let step = RopeStep::from_bytes(data)?;
486        let step_len = step.to_bytes().len();
487        let inner = KkAeadPacket::from_bytes(&data[step_len..])?;
488        Ok(Self { step, inner })
489    }
490}
491
492/// Encode a forward-secret AEAD packet.
493///
494/// Advances the ratchet, derives a per-message key, then encrypts
495/// the plaintext with AEAD (AAD is authenticated but not encrypted).
496pub fn encode_session_aead(
497    ratchet: &mut RopeRatchet,
498    plaintext: &[u8],
499    aad: &[u8],
500) -> Result<RopeAeadPacket> {
501    let (mut message_key, step) = ratchet.advance()?;
502    let inner = codec::encode_aead(&message_key, plaintext, aad)?;
503    message_key.zeroize();
504    Ok(RopeAeadPacket { step, inner })
505}
506
507/// Decode a forward-secret AEAD packet.
508///
509/// Advances the receiver's ratchet, derives the same per-message key,
510/// and decrypts the inner AEAD packet (verifying both ciphertext and AAD).
511pub fn decode_session_aead(ratchet: &mut RopeRatchet, packet: &RopeAeadPacket) -> Result<Vec<u8>> {
512    let mut message_key = ratchet.receive(&packet.step)?;
513    let plaintext = codec::decode_aead(&message_key, &packet.inner)?;
514    message_key.zeroize();
515    Ok(plaintext)
516}