use rand::RngCore;
use zeroize::Zeroize;
use crate::error::{KkError, Result};
use crate::kk_mix;
pub const ENTROPY_SNAPSHOT_SIZE: usize = 32;
#[derive(Clone)]
pub struct EntropySnapshot {
pub bytes: [u8; ENTROPY_SNAPSHOT_SIZE],
pub timestamp_nanos: u128,
}
impl Drop for EntropySnapshot {
fn drop(&mut self) {
self.bytes.zeroize();
}
}
impl EntropySnapshot {
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(ENTROPY_SNAPSHOT_SIZE + 16);
out.extend_from_slice(&self.bytes);
out.extend_from_slice(&self.timestamp_nanos.to_le_bytes());
out
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
if data.len() < ENTROPY_SNAPSHOT_SIZE + 16 {
return Err(KkError::InvalidPacket("entropy snapshot too short".into()));
}
let mut bytes = [0u8; ENTROPY_SNAPSHOT_SIZE];
bytes.copy_from_slice(&data[..ENTROPY_SNAPSHOT_SIZE]);
let timestamp_nanos = u128::from_le_bytes(
data[ENTROPY_SNAPSHOT_SIZE..ENTROPY_SNAPSHOT_SIZE + 16]
.try_into()
.map_err(|_| KkError::InvalidPacket("bad timestamp bytes".into()))?,
);
Ok(Self {
bytes,
timestamp_nanos,
})
}
}
fn source_csprng() -> Result<[u8; 32]> {
let mut buf = [0u8; 32];
rand::rngs::OsRng
.try_fill_bytes(&mut buf)
.map_err(|e| KkError::EntropyFailure(format!("CSPRNG: {e}")))?;
Ok(buf)
}
fn source_timestamp() -> (u128, [u8; 16]) {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
(nanos, nanos.to_le_bytes())
}
#[cfg(target_arch = "x86_64")]
fn source_cpu_counter() -> [u8; 8] {
let raw: u64 = unsafe { core::arch::x86_64::_rdtsc() };
let stack_addr = &raw as *const u64 as u64;
(raw ^ stack_addr).to_le_bytes()
}
#[cfg(not(target_arch = "x86_64"))]
fn source_cpu_counter() -> [u8; 8] {
let now = std::time::Instant::now();
let raw = now.elapsed().as_nanos() as u64;
let stack_addr = &raw as *const u64 as u64;
(raw ^ stack_addr).to_le_bytes()
}
fn source_thread_jitter() -> [u8; 32] {
let mut raw_timings = Vec::with_capacity(64 * 8);
for _ in 0..64 {
let start = std::time::Instant::now();
std::thread::yield_now();
std::hint::black_box(0u64.wrapping_add(1));
let elapsed = start.elapsed().as_nanos() as u64;
raw_timings.extend_from_slice(&elapsed.to_le_bytes());
}
kk_mix::kk_hash(&raw_timings)
}
pub fn gather() -> Result<EntropySnapshot> {
let csprng_bytes = source_csprng()?;
let (timestamp_nanos, timestamp_bytes) = source_timestamp();
let cpu_bytes = source_cpu_counter();
let jitter_bytes = source_thread_jitter();
let sources: Vec<&[u8]> = vec![&csprng_bytes, ×tamp_bytes, &cpu_bytes, &jitter_bytes];
let mixed = kk_mix::kk_entropy_mix(&sources, ENTROPY_SNAPSHOT_SIZE);
let mut output = [0u8; ENTROPY_SNAPSHOT_SIZE];
output.copy_from_slice(&mixed);
Ok(EntropySnapshot {
bytes: output,
timestamp_nanos,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn entropy_snapshots_are_unique() {
let s1 = gather().unwrap();
let s2 = gather().unwrap();
assert_ne!(s1.bytes, s2.bytes, "KK(S) at T₁ ≠ KK(S) at T₂");
}
#[test]
fn snapshot_roundtrip() {
let snap = gather().unwrap();
let bytes = snap.to_bytes();
let restored = EntropySnapshot::from_bytes(&bytes).unwrap();
assert_eq!(snap.bytes, restored.bytes);
assert_eq!(snap.timestamp_nanos, restored.timestamp_nanos);
}
}