use crate::codec::{self, KkAeadPacket, KkPacket};
use crate::entropy::{self, EntropySnapshot};
use crate::error::{KkError, Result};
use crate::kk_mix;
use zeroize::Zeroize;
const DOMAIN_SESSION: &[u8] = b"KK-rope-mix-v1";
const STRAND_ENT_INFO: &[u8] = b"KK-rope-ent-v1";
const STRAND_TMP_INFO: &[u8] = b"KK-rope-tmp-v1";
const STRAND_CHN_INFO: &[u8] = b"KK-rope-chn-v1";
const INIT_ENT_INFO: &[u8] = b"KK-rope-init-ent";
const INIT_TMP_INFO: &[u8] = b"KK-rope-init-tmp";
const INIT_CHN_INFO: &[u8] = b"KK-rope-init-chn";
#[derive(Clone)]
pub struct RopeStep {
pub snapshot: EntropySnapshot,
pub counter: u64,
}
impl RopeStep {
pub const BYTES: usize = 8 + 48;
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(Self::BYTES);
out.extend_from_slice(&self.counter.to_le_bytes());
out.extend_from_slice(&self.snapshot.to_bytes());
out
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
if data.len() < Self::BYTES {
return Err(KkError::InvalidPacket(format!(
"rope step too short: need {}, got {}",
Self::BYTES,
data.len()
)));
}
let counter = u64::from_le_bytes(
data[..8]
.try_into()
.map_err(|_| KkError::InvalidPacket("bad counter bytes".into()))?,
);
let snapshot = EntropySnapshot::from_bytes(&data[8..56])?;
Ok(Self { snapshot, counter })
}
}
pub struct RopeRatchet {
entropy_strand: [u8; 32],
temporal_strand: [u8; 32],
chain_strand: [u8; 32],
counter: u64,
}
impl Drop for RopeRatchet {
fn drop(&mut self) {
self.entropy_strand.zeroize();
self.temporal_strand.zeroize();
self.chain_strand.zeroize();
self.counter = 0;
}
}
impl RopeRatchet {
pub fn new(shared_secret: &[u8], context: &[u8]) -> Result<Self> {
let salt = kk_mix::kk_hash(context);
let mut e = kk_mix::kk_kdf(shared_secret, &salt, INIT_ENT_INFO, 32);
let mut t = kk_mix::kk_kdf(shared_secret, &salt, INIT_TMP_INFO, 32);
let mut c = kk_mix::kk_kdf(shared_secret, &salt, INIT_CHN_INFO, 32);
let mut entropy_strand = [0u8; 32];
let mut temporal_strand = [0u8; 32];
let mut chain_strand = [0u8; 32];
entropy_strand.copy_from_slice(&e);
temporal_strand.copy_from_slice(&t);
chain_strand.copy_from_slice(&c);
e.zeroize();
t.zeroize();
c.zeroize();
Ok(Self {
entropy_strand,
temporal_strand,
chain_strand,
counter: 0,
})
}
pub fn advance(&mut self) -> Result<([u8; 32], RopeStep)> {
let snapshot = entropy::gather()?;
let key = self.step(&snapshot)?;
let step = RopeStep {
snapshot,
counter: self.counter,
};
Ok((key, step))
}
pub fn receive(&mut self, step: &RopeStep) -> Result<[u8; 32]> {
let expected = self.counter + 1;
if step.counter != expected {
return Err(KkError::InvalidPacket(format!(
"counter mismatch: expected {expected}, got {} (strict ordering)",
step.counter
)));
}
self.step(&step.snapshot)
}
pub fn counter(&self) -> u64 {
self.counter
}
#[doc(hidden)]
pub fn advance_with_snapshot(
&mut self,
snapshot: EntropySnapshot,
) -> Result<([u8; 32], RopeStep)> {
let key = self.step(&snapshot)?;
let step = RopeStep {
snapshot,
counter: self.counter,
};
Ok((key, step))
}
fn step(&mut self, snapshot: &EntropySnapshot) -> Result<[u8; 32]> {
let mut e_new = kk_mix::kk_kdf(&self.entropy_strand, &snapshot.bytes, STRAND_ENT_INFO, 32);
self.entropy_strand.copy_from_slice(&e_new);
e_new.zeroize();
let ts_bytes = snapshot.timestamp_nanos.to_le_bytes();
let mut t_new = kk_mix::kk_kdf(&self.temporal_strand, &ts_bytes, STRAND_TMP_INFO, 32);
self.temporal_strand.copy_from_slice(&t_new);
t_new.zeroize();
self.counter += 1;
let ctr_bytes = self.counter.to_le_bytes();
let mut c_new = kk_mix::kk_kdf(&self.chain_strand, &ctr_bytes, STRAND_CHN_INFO, 32);
self.chain_strand.copy_from_slice(&c_new);
c_new.zeroize();
let mut combined = Vec::with_capacity(104);
combined.extend_from_slice(&self.entropy_strand); combined.extend_from_slice(&self.temporal_strand); combined.extend_from_slice(&self.chain_strand); combined.extend_from_slice(&ctr_bytes);
let mut output = kk_mix::kk_kdf(&combined, &snapshot.bytes, DOMAIN_SESSION, 64);
combined.zeroize();
self.chain_strand.copy_from_slice(&output[..32]);
let mut message_key = [0u8; 32];
message_key.copy_from_slice(&output[32..64]);
output.zeroize();
Ok(message_key)
}
}
#[derive(Clone)]
pub struct RopePacket {
pub step: RopeStep,
pub inner: KkPacket,
}
impl RopePacket {
pub fn to_bytes(&self) -> Vec<u8> {
let step_bytes = self.step.to_bytes();
let inner_bytes = self.inner.to_bytes();
let mut out = Vec::with_capacity(step_bytes.len() + inner_bytes.len());
out.extend_from_slice(&step_bytes);
out.extend_from_slice(&inner_bytes);
out
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
if data.len() < RopeStep::BYTES {
return Err(KkError::InvalidPacket(
"rope packet too short for step metadata".into(),
));
}
let step = RopeStep::from_bytes(&data[..RopeStep::BYTES])?;
let inner = KkPacket::from_bytes(&data[RopeStep::BYTES..])?;
Ok(Self { step, inner })
}
}
pub fn encode_session(ratchet: &mut RopeRatchet, plaintext: &[u8]) -> Result<RopePacket> {
let (mut message_key, step) = ratchet.advance()?;
let inner = codec::encode(&message_key, plaintext)?;
message_key.zeroize();
Ok(RopePacket { step, inner })
}
pub fn decode_session(ratchet: &mut RopeRatchet, packet: &RopePacket) -> Result<Vec<u8>> {
let mut message_key = ratchet.receive(&packet.step)?;
let plaintext = codec::decode(&message_key, &packet.inner)?;
message_key.zeroize();
Ok(plaintext)
}
#[derive(Clone)]
pub struct RopeAeadPacket {
pub step: RopeStep,
pub inner: KkAeadPacket,
}
impl RopeAeadPacket {
pub fn to_bytes(&self) -> Vec<u8> {
let step_bytes = self.step.to_bytes();
let inner_bytes = self.inner.to_bytes();
let mut out = Vec::with_capacity(step_bytes.len() + inner_bytes.len());
out.extend_from_slice(&step_bytes);
out.extend_from_slice(&inner_bytes);
out
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
let step = RopeStep::from_bytes(data)?;
let step_len = step.to_bytes().len();
let inner = KkAeadPacket::from_bytes(&data[step_len..])?;
Ok(Self { step, inner })
}
}
pub fn encode_session_aead(
ratchet: &mut RopeRatchet,
plaintext: &[u8],
aad: &[u8],
) -> Result<RopeAeadPacket> {
let (mut message_key, step) = ratchet.advance()?;
let inner = codec::encode_aead(&message_key, plaintext, aad)?;
message_key.zeroize();
Ok(RopeAeadPacket { step, inner })
}
pub fn decode_session_aead(ratchet: &mut RopeRatchet, packet: &RopeAeadPacket) -> Result<Vec<u8>> {
let mut message_key = ratchet.receive(&packet.step)?;
let plaintext = codec::decode_aead(&message_key, &packet.inner)?;
message_key.zeroize();
Ok(plaintext)
}