use zeroize::Zeroize;
use crate::entropy::{self, EntropySnapshot};
use crate::error::{KkError, Result};
use crate::kk_mix;
const EKA_SESSION_INFO: &[u8] = b"KK-EKA-session";
#[derive(Clone)]
pub struct EkaMsg1 {
pub commit: [u8; 32],
}
impl EkaMsg1 {
pub const BYTES: usize = 32;
pub fn to_bytes(&self) -> Vec<u8> {
self.commit.to_vec()
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
if data.len() < Self::BYTES {
return Err(KkError::InvalidPacket("EkaMsg1 too short".into()));
}
let mut commit = [0u8; 32];
commit.copy_from_slice(&data[..32]);
Ok(Self { commit })
}
}
#[derive(Clone)]
pub struct EkaMsg2 {
pub entropy_b_bytes: [u8; 48],
pub auth_b: [u8; 32],
}
impl EkaMsg2 {
pub const BYTES: usize = 80;
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(Self::BYTES);
out.extend_from_slice(&self.entropy_b_bytes);
out.extend_from_slice(&self.auth_b);
out
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
if data.len() < Self::BYTES {
return Err(KkError::InvalidPacket("EkaMsg2 too short".into()));
}
let mut entropy_b_bytes = [0u8; 48];
entropy_b_bytes.copy_from_slice(&data[..48]);
let mut auth_b = [0u8; 32];
auth_b.copy_from_slice(&data[48..80]);
Ok(Self {
entropy_b_bytes,
auth_b,
})
}
}
#[derive(Clone)]
pub struct EkaMsg3 {
pub entropy_a_bytes: [u8; 48],
pub auth_a: [u8; 32],
}
impl EkaMsg3 {
pub const BYTES: usize = 80;
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(Self::BYTES);
out.extend_from_slice(&self.entropy_a_bytes);
out.extend_from_slice(&self.auth_a);
out
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
if data.len() < Self::BYTES {
return Err(KkError::InvalidPacket("EkaMsg3 too short".into()));
}
let mut entropy_a_bytes = [0u8; 48];
entropy_a_bytes.copy_from_slice(&data[..48]);
let mut auth_a = [0u8; 32];
auth_a.copy_from_slice(&data[48..80]);
Ok(Self {
entropy_a_bytes,
auth_a,
})
}
}
pub struct EkaInitiator {
psk: Vec<u8>,
#[allow(dead_code)] entropy_a: EntropySnapshot,
entropy_a_serialized: Vec<u8>,
commit_a: [u8; 32],
}
impl Drop for EkaInitiator {
fn drop(&mut self) {
self.psk.zeroize();
self.entropy_a_serialized.zeroize();
self.commit_a.zeroize();
}
}
impl EkaInitiator {
pub fn new(psk: &[u8]) -> Result<(Self, EkaMsg1)> {
let entropy_a = entropy::gather()?;
Self::new_with_entropy(psk, entropy_a)
}
#[doc(hidden)]
pub fn new_with_entropy(psk: &[u8], entropy_a: EntropySnapshot) -> Result<(Self, EkaMsg1)> {
let entropy_a_serialized = entropy_a.to_bytes();
let commit_a = kk_mix::kk_hash(&entropy_a_serialized);
let msg1 = EkaMsg1 { commit: commit_a };
let state = Self {
psk: psk.to_vec(),
entropy_a,
entropy_a_serialized,
commit_a,
};
Ok((state, msg1))
}
pub fn process_msg2(self, msg2: &EkaMsg2) -> Result<(EkaMsg3, [u8; 32])> {
let mut auth_b_message = Vec::with_capacity(48 + 32);
auth_b_message.extend_from_slice(&msg2.entropy_b_bytes);
auth_b_message.extend_from_slice(&self.commit_a);
if !kk_mix::kk_mac_verify(&self.psk, &auth_b_message, &msg2.auth_b) {
return Err(KkError::CommitmentMismatch);
}
let mut auth_a_message = Vec::with_capacity(48 + 48);
auth_a_message.extend_from_slice(&self.entropy_a_serialized);
auth_a_message.extend_from_slice(&msg2.entropy_b_bytes);
let auth_a = kk_mix::kk_mac(&self.psk, &auth_a_message);
let mut entropy_a_bytes = [0u8; 48];
entropy_a_bytes.copy_from_slice(&self.entropy_a_serialized);
let msg3 = EkaMsg3 {
entropy_a_bytes,
auth_a,
};
let mut salt = Vec::with_capacity(48 + 48);
salt.extend_from_slice(&self.entropy_a_serialized);
salt.extend_from_slice(&msg2.entropy_b_bytes);
let session_key_vec = kk_mix::kk_kdf(&self.psk, &salt, EKA_SESSION_INFO, 32);
let mut session_key = [0u8; 32];
session_key.copy_from_slice(&session_key_vec);
Ok((msg3, session_key))
}
}
pub struct EkaResponder {
psk: Vec<u8>,
entropy_b_serialized: Vec<u8>,
commit_a: [u8; 32],
}
impl Drop for EkaResponder {
fn drop(&mut self) {
self.psk.zeroize();
self.entropy_b_serialized.zeroize();
self.commit_a.zeroize();
}
}
impl EkaResponder {
pub fn new(psk: &[u8], msg1: &EkaMsg1) -> Result<(Self, EkaMsg2)> {
let entropy_b = entropy::gather()?;
Self::new_with_entropy(psk, msg1, entropy_b)
}
#[doc(hidden)]
pub fn new_with_entropy(
psk: &[u8],
msg1: &EkaMsg1,
entropy_b: EntropySnapshot,
) -> Result<(Self, EkaMsg2)> {
let entropy_b_serialized = entropy_b.to_bytes();
let mut auth_b_message = Vec::with_capacity(48 + 32);
auth_b_message.extend_from_slice(&entropy_b_serialized);
auth_b_message.extend_from_slice(&msg1.commit);
let auth_b = kk_mix::kk_mac(psk, &auth_b_message);
let mut entropy_b_bytes = [0u8; 48];
entropy_b_bytes.copy_from_slice(&entropy_b_serialized);
let msg2 = EkaMsg2 {
entropy_b_bytes,
auth_b,
};
let state = Self {
psk: psk.to_vec(),
entropy_b_serialized,
commit_a: msg1.commit,
};
Ok((state, msg2))
}
pub fn process_msg3(self, msg3: &EkaMsg3) -> Result<[u8; 32]> {
let recomputed_commit = kk_mix::kk_hash(&msg3.entropy_a_bytes);
if recomputed_commit != self.commit_a {
return Err(KkError::CommitmentMismatch);
}
let mut auth_a_message = Vec::with_capacity(48 + 48);
auth_a_message.extend_from_slice(&msg3.entropy_a_bytes);
auth_a_message.extend_from_slice(&self.entropy_b_serialized);
if !kk_mix::kk_mac_verify(&self.psk, &auth_a_message, &msg3.auth_a) {
return Err(KkError::CommitmentMismatch);
}
let mut salt = Vec::with_capacity(48 + 48);
salt.extend_from_slice(&msg3.entropy_a_bytes);
salt.extend_from_slice(&self.entropy_b_serialized);
let session_key_vec = kk_mix::kk_kdf(&self.psk, &salt, EKA_SESSION_INFO, 32);
let mut session_key = [0u8; 32];
session_key.copy_from_slice(&session_key_vec);
Ok(session_key)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn eka_happy_path_live_entropy() {
let psk = b"test-psk-for-eka";
let (alice, msg1) = EkaInitiator::new(psk).unwrap();
let (bob, msg2) = EkaResponder::new(psk, &msg1).unwrap();
let (msg3, alice_key) = alice.process_msg2(&msg2).unwrap();
let bob_key = bob.process_msg3(&msg3).unwrap();
assert_eq!(alice_key, bob_key);
assert_ne!(alice_key, [0u8; 32]);
}
#[test]
fn eka_wire_format_sizes() {
assert_eq!(EkaMsg1::BYTES, 32);
assert_eq!(EkaMsg2::BYTES, 80);
assert_eq!(EkaMsg3::BYTES, 80);
let psk = b"size-test";
let (_, msg1) = EkaInitiator::new(psk).unwrap();
assert_eq!(msg1.to_bytes().len(), 32);
let (_, msg2) = EkaResponder::new(psk, &msg1).unwrap();
assert_eq!(msg2.to_bytes().len(), 80);
}
#[test]
fn eka_msg_roundtrip() {
let psk = b"roundtrip-test";
let (alice, msg1) = EkaInitiator::new(psk).unwrap();
let msg1_bytes = msg1.to_bytes();
let msg1_restored = EkaMsg1::from_bytes(&msg1_bytes).unwrap();
assert_eq!(msg1.commit, msg1_restored.commit);
let (bob, msg2) = EkaResponder::new(psk, &msg1).unwrap();
let msg2_bytes = msg2.to_bytes();
let msg2_restored = EkaMsg2::from_bytes(&msg2_bytes).unwrap();
assert_eq!(msg2.entropy_b_bytes, msg2_restored.entropy_b_bytes);
assert_eq!(msg2.auth_b, msg2_restored.auth_b);
let (msg3, _) = alice.process_msg2(&msg2).unwrap();
let msg3_bytes = msg3.to_bytes();
let msg3_restored = EkaMsg3::from_bytes(&msg3_bytes).unwrap();
assert_eq!(msg3.entropy_a_bytes, msg3_restored.entropy_a_bytes);
assert_eq!(msg3.auth_a, msg3_restored.auth_a);
let key = bob.process_msg3(&msg3_restored).unwrap();
assert_ne!(key, [0u8; 32]);
}
#[test]
fn eka_different_sessions_different_keys() {
let psk = b"same-psk";
let (alice1, msg1_a) = EkaInitiator::new(psk).unwrap();
let (bob1, msg2_a) = EkaResponder::new(psk, &msg1_a).unwrap();
let (msg3_a, key_a) = alice1.process_msg2(&msg2_a).unwrap();
let _ = bob1.process_msg3(&msg3_a).unwrap();
let (alice2, msg1_b) = EkaInitiator::new(psk).unwrap();
let (bob2, msg2_b) = EkaResponder::new(psk, &msg1_b).unwrap();
let (msg3_b, key_b) = alice2.process_msg2(&msg2_b).unwrap();
let _ = bob2.process_msg3(&msg3_b).unwrap();
assert_ne!(
key_a, key_b,
"different sessions must derive different keys"
);
}
}