use crate::crypto::kdf::derive_key_32;
use zeroize::Zeroize;
pub const CID_LEN: usize = 8;
const CID_LABEL: &str = "phantom-cid-v1";
const CID_C2S_LABEL: &str = "phantom-cid-c2s-v1";
const CID_S2C_LABEL: &str = "phantom-cid-s2c-v1";
pub const CID_WINDOW_TRAILING: u64 = 2;
pub const CID_WINDOW_LEADING: u64 = 16;
pub struct CidChain {
outbound_secret: [u8; 32],
inbound_secret: [u8; 32],
}
impl CidChain {
pub fn derive(initial_secret: &[u8; 32], is_server: bool) -> Self {
let c2s = derive_key_32(CID_C2S_LABEL, initial_secret);
let s2c = derive_key_32(CID_S2C_LABEL, initial_secret);
let (outbound_secret, inbound_secret) = if is_server { (s2c, c2s) } else { (c2s, s2c) };
Self {
outbound_secret,
inbound_secret,
}
}
#[inline]
pub fn outbound_cid(&self, i: u64) -> [u8; CID_LEN] {
cid_from_secret(&self.outbound_secret, i)
}
#[inline]
pub fn inbound_cid(&self, i: u64) -> [u8; CID_LEN] {
cid_from_secret(&self.inbound_secret, i)
}
pub fn inbound_window(
&self,
highest_seen: u64,
trailing: u64,
leading: u64,
) -> impl Iterator<Item = (u64, [u8; CID_LEN])> + '_ {
let lo = highest_seen.saturating_sub(trailing);
let hi = highest_seen.saturating_add(leading);
(lo..=hi).map(move |i| (i, self.inbound_cid(i)))
}
}
impl Drop for CidChain {
fn drop(&mut self) {
self.outbound_secret.zeroize();
self.inbound_secret.zeroize();
}
}
#[inline]
fn cid_from_secret(secret: &[u8; 32], i: u64) -> [u8; CID_LEN] {
let mut ikm = [0u8; 40];
ikm[..32].copy_from_slice(secret);
ikm[32..].copy_from_slice(&i.to_be_bytes());
let full = derive_key_32(CID_LABEL, &ikm);
let mut cid = [0u8; CID_LEN];
cid.copy_from_slice(&full[..CID_LEN]);
cid
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use std::collections::HashSet;
use super::*;
use crate::crypto::kdf::derive_key_32;
fn reference_cid(secret: &[u8; 32], i: u64) -> [u8; 8] {
let mut ikm = [0u8; 40];
ikm[..32].copy_from_slice(secret);
ikm[32..].copy_from_slice(&i.to_be_bytes());
let full = derive_key_32("phantom-cid-v1", &ikm);
let mut cid = [0u8; 8];
cid.copy_from_slice(&full[..8]);
cid
}
fn direction_secret(initial: &[u8; 32], c2s: bool) -> [u8; 32] {
derive_key_32(
if c2s {
"phantom-cid-c2s-v1"
} else {
"phantom-cid-s2c-v1"
},
initial,
)
}
#[test]
fn outbound_cid_matches_known_construction() {
let initial = [0x42u8; 32];
let client = CidChain::derive(&initial, false);
let server = CidChain::derive(&initial, true);
let c2s = direction_secret(&initial, true);
let s2c = direction_secret(&initial, false);
for i in [0u64, 1, 7, 1000] {
assert_eq!(client.outbound_cid(i), reference_cid(&c2s, i));
assert_eq!(server.outbound_cid(i), reference_cid(&s2c, i));
}
}
#[test]
fn per_direction_swap_links_peers() {
let initial = [0x9cu8; 32];
let client = CidChain::derive(&initial, false);
let server = CidChain::derive(&initial, true);
for i in [0u64, 1, 5, 42, 9999] {
assert_eq!(
client.outbound_cid(i),
server.inbound_cid(i),
"client outbound (c2s) must equal server inbound (c2s)"
);
assert_eq!(
server.outbound_cid(i),
client.inbound_cid(i),
"server outbound (s2c) must equal client inbound (s2c)"
);
}
}
#[test]
fn directions_differ_within_peer() {
let chain = CidChain::derive(&[0x77u8; 32], false);
for i in [0u64, 1, 100] {
assert_ne!(chain.outbound_cid(i), chain.inbound_cid(i));
}
}
#[test]
fn cids_are_distinct_across_index() {
let chain = CidChain::derive(&[0x11u8; 32], false);
let mut seen = HashSet::new();
for i in 0..256u64 {
assert!(seen.insert(chain.outbound_cid(i)), "collision at index {i}");
}
assert_eq!(seen.len(), 256);
}
#[test]
fn derivation_is_deterministic() {
let initial = [0x5au8; 32];
let a = CidChain::derive(&initial, false);
let b = CidChain::derive(&initial, false);
for i in [0u64, 3, 64] {
assert_eq!(a.outbound_cid(i), b.outbound_cid(i));
assert_eq!(a.inbound_cid(i), b.inbound_cid(i));
}
}
#[test]
fn inbound_window_covers_trailing_and_leading() {
let chain = CidChain::derive(&[0x55u8; 32], true);
let win: Vec<(u64, [u8; 8])> = chain.inbound_window(10, 2, 4).collect();
let idxs: Vec<u64> = win.iter().map(|(i, _)| *i).collect();
assert_eq!(idxs, vec![8, 9, 10, 11, 12, 13, 14]);
for (i, cid) in win {
assert_eq!(cid, chain.inbound_cid(i));
}
}
#[test]
fn inbound_window_saturates_at_zero() {
let chain = CidChain::derive(&[0x55u8; 32], true);
let idxs: Vec<u64> = chain.inbound_window(1, 2, 4).map(|(i, _)| i).collect();
assert_eq!(idxs, vec![0, 1, 2, 3, 4, 5]);
}
#[test]
fn canonical_window_size() {
let chain = CidChain::derive(&[0x55u8; 32], false);
let n = chain
.inbound_window(100, CID_WINDOW_TRAILING, CID_WINDOW_LEADING)
.count();
assert_eq!(n, (CID_WINDOW_TRAILING + CID_WINDOW_LEADING + 1) as usize);
assert_eq!(n, 19);
}
#[cfg(not(feature = "fips"))]
#[test]
fn frozen_cid_bytes_default_build() {
let client = CidChain::derive(&[0x42u8; 32], false);
assert_eq!(
client.outbound_cid(0),
[102, 235, 212, 72, 93, 23, 250, 126]
);
assert_eq!(client.outbound_cid(1), [24, 107, 241, 72, 75, 55, 3, 167]);
}
}