use chacha20poly1305::{AeadInOut, KeyInit, XChaCha20Poly1305};
use ctutils::CtEq;
use zeroize::{Zeroize, Zeroizing};
use crate::error::{ConfigError, DecodeError, EncodeError, KeyError, KeyringError};
use crate::limits::Limits;
use crate::message::{Message, Payload, put_payload};
use crate::wire::{Count, Sink};
pub const WIRE_VERSION: u8 = 1;
pub const MAGIC: [u8; 2] = *b"kn";
pub const MAX_LABEL_LEN: usize = 255;
pub const ENCRYPTED_HEADER_LEN: usize = 32;
pub const PLAINTEXT_HEADER_LEN: usize = 12;
pub const TAG_LEN: usize = 16;
pub const NONCE_LEN: usize = 24;
pub const ENCRYPTED_OVERHEAD: usize = ENCRYPTED_HEADER_LEN + TAG_LEN;
const FLAG_ENCRYPTED: u8 = 0b01;
const FLAG_STREAM: u8 = 0b10;
const FLAGS_RESERVED: u8 = !(FLAG_ENCRYPTED | FLAG_STREAM);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PacketKind {
Datagram,
Stream,
}
#[derive(Clone)]
pub struct Key(Box<[u8; 32]>);
impl PartialEq for Key {
fn eq(&self, other: &Self) -> bool {
self.0.ct_eq(&*other.0).to_bool()
}
}
impl Eq for Key {}
impl Key {
pub const LEN: usize = 32;
pub fn from_bytes(mut bytes: [u8; 32]) -> Self {
let key = Self(Box::new(bytes));
bytes.zeroize();
key
}
pub fn from_slice(bytes: &[u8]) -> Option<Self> {
if bytes.len() != Self::LEN {
return None;
}
let mut key = Self(Box::new([0; 32]));
key.0.copy_from_slice(bytes);
Some(key)
}
pub fn from_base64(text: &str) -> Result<Self, KeyError> {
let text = text.trim();
let digits = text.trim_end_matches('=');
if text.len() - digits.len() > 2 {
return Err(KeyError::NotBase64);
}
let mut key = Self(Box::new([0; 32]));
let mut len = 0;
let mut acc = 0u32;
let mut bits = 0;
let mut result = Ok(());
for c in digits.bytes() {
let v = match c {
b'A'..=b'Z' => c - b'A',
b'a'..=b'z' => c - b'a' + 26,
b'0'..=b'9' => c - b'0' + 52,
b'+' => 62,
b'/' => 63,
_ => {
result = Err(KeyError::NotBase64);
break;
}
};
acc = (acc << 6) | u32::from(v);
bits += 6;
if bits >= 8 {
bits -= 8;
if len == Self::LEN {
result = Err(KeyError::WrongLength);
break;
}
key.0[len] = (acc >> bits) as u8;
len += 1;
acc &= (1 << bits) - 1;
}
}
if result.is_ok() && (bits >= 6 || acc != 0) {
result = Err(KeyError::NotBase64);
}
if result.is_ok() && len != Self::LEN {
result = Err(KeyError::WrongLength);
}
acc.zeroize();
result.map(|()| key)
}
pub fn to_base64(&self) -> Zeroizing<String> {
const DIGITS: &[u8; 64] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = Zeroizing::new(String::with_capacity(44));
for chunk in self.0.chunks(3) {
let b = [
chunk[0],
*chunk.get(1).unwrap_or(&0),
*chunk.get(2).unwrap_or(&0),
];
let mut group = u32::from_be_bytes([0, b[0], b[1], b[2]]);
for i in 0..4 {
if i <= chunk.len() {
out.push(DIGITS[(group >> 18) as usize & 63] as char);
} else {
out.push('=');
}
group <<= 6;
}
group.zeroize();
}
out
}
pub fn id(&self) -> u32 {
let hash = blake3::hash(&self.0[..]);
let b = hash.as_bytes();
u32::from_be_bytes([b[0], b[1], b[2], b[3]])
}
pub fn key_id(&self) -> KeyId {
KeyId(self.id())
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct KeyId(u32);
impl KeyId {
pub const fn from_raw(raw: u32) -> Self {
Self(raw)
}
pub const fn to_raw(self) -> u32 {
self.0
}
}
impl core::fmt::Display for KeyId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{:08x}", self.0)
}
}
impl core::fmt::Debug for KeyId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "KeyId({:08x})", self.0)
}
}
impl Drop for Key {
fn drop(&mut self) {
self.0.zeroize();
}
}
impl core::fmt::Debug for Key {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "Key({:08x})", self.id())
}
}
enum Security {
Plaintext,
Encrypted(Keys),
}
const DUMMY_KEY_CONTEXT: &str = "kinship 2026-10-08 key for packets with an unknown key id";
struct Keys {
keys: Vec<Key>,
ids: Vec<u32>,
dummy: Key,
}
impl Keys {
fn new(keys: Vec<Key>) -> Self {
let (ids, dummy) = Self::derive(&keys);
Self { keys, ids, dummy }
}
fn changed(&mut self) {
(self.ids, self.dummy) = Self::derive(&self.keys);
}
fn derive(keys: &[Key]) -> (Vec<u32>, Key) {
let ids = keys.iter().map(Key::id).collect();
let mut dummy = blake3::derive_key(DUMMY_KEY_CONTEXT, &keys[0].0[..]);
let key = Key::from_bytes(dummy);
dummy.zeroize();
(ids, key)
}
}
pub struct Codec {
label: Vec<u8>,
label_hash: [u8; 8],
limits: Limits,
security: Security,
}
impl Codec {
pub fn encrypted(label: &[u8], limits: Limits, keys: Vec<Key>) -> Result<Self, ConfigError> {
if keys.is_empty() {
return Err(ConfigError::NoKeys);
}
Self::new(label, limits, Security::Encrypted(Keys::new(keys)))
}
pub fn insecure_plaintext(label: &[u8], limits: Limits) -> Result<Self, ConfigError> {
Self::new(label, limits, Security::Plaintext)
}
fn new(label: &[u8], limits: Limits, security: Security) -> Result<Self, ConfigError> {
if label.len() > MAX_LABEL_LEN {
return Err(ConfigError::LabelTooLong);
}
let hash = blake3::hash(label);
let mut label_hash = [0u8; 8];
label_hash.copy_from_slice(&hash.as_bytes()[..8]);
let codec = Self {
label: label.to_vec(),
label_hash,
limits,
security,
};
let floor = codec.overhead() + 3;
if limits.udp_max_payload < floor || limits.max_stream_frame < floor {
return Err(ConfigError::LimitTooSmall);
}
Ok(codec)
}
pub fn set_keys(&mut self, keys: Vec<Key>) -> Result<(), ConfigError> {
if keys.is_empty() {
return Err(ConfigError::NoKeys);
}
self.security = Security::Encrypted(Keys::new(keys));
Ok(())
}
pub fn is_encrypted(&self) -> bool {
matches!(self.security, Security::Encrypted(_))
}
pub fn limits(&self) -> &Limits {
&self.limits
}
pub fn key_ids(&self) -> Vec<KeyId> {
match &self.security {
Security::Plaintext => Vec::new(),
Security::Encrypted(keys) => keys.ids.iter().map(|&id| KeyId(id)).collect(),
}
}
pub fn header_check(&self) -> HeaderCheck {
let mut label = [0; 4];
label.copy_from_slice(&self.label_hash[..4]);
HeaderCheck {
key_ids: match &self.security {
Security::Plaintext => None,
Security::Encrypted(keys) => Some(keys.ids.clone()),
},
label,
}
}
pub fn install_key(&mut self, key: Key) -> Result<(), KeyringError> {
let keys = self.keys_mut()?;
if !keys.keys.contains(&key) {
keys.keys.push(key);
keys.changed();
}
Ok(())
}
pub fn use_key(&mut self, key: &Key) -> Result<(), KeyringError> {
let keys = self.keys_mut()?;
let at = keys
.keys
.iter()
.position(|k| k == key)
.ok_or(KeyringError::NotInstalled)?;
keys.keys[..=at].rotate_right(1);
keys.changed();
Ok(())
}
pub fn remove_key(&mut self, key: &Key) -> Result<(), KeyringError> {
let keys = self.keys_mut()?;
match keys.keys.iter().position(|k| k == key) {
None => Ok(()),
Some(_) if keys.keys.len() == 1 => Err(KeyringError::LastKey),
Some(0) => Err(KeyringError::InUse),
Some(at) => {
keys.keys.remove(at);
keys.changed();
Ok(())
}
}
}
fn keys_mut(&mut self) -> Result<&mut Keys, KeyringError> {
match &mut self.security {
Security::Plaintext => Err(KeyringError::Plaintext),
Security::Encrypted(keys) => Ok(keys),
}
}
pub fn overhead(&self) -> usize {
match self.security {
Security::Plaintext => PLAINTEXT_HEADER_LEN,
Security::Encrypted(_) => ENCRYPTED_OVERHEAD,
}
}
pub fn max_payload_len(&self, kind: PacketKind) -> usize {
self.limit(kind) - self.overhead()
}
fn limit(&self, kind: PacketKind) -> usize {
match kind {
PacketKind::Datagram => self.limits.udp_max_payload,
PacketKind::Stream => self.limits.max_stream_frame,
}
}
pub fn seal(
&self,
kind: PacketKind,
msgs: &[Message<'_>],
nonce: &[u8; NONCE_LEN],
out: &mut Vec<u8>,
) -> Result<(), EncodeError> {
if msgs.is_empty() {
return Err(EncodeError::Empty);
}
for m in msgs {
m.validate(&self.limits)?;
}
let mut payload = Count::default();
put_payload(&mut payload, msgs);
self.seal_with(kind, nonce, payload.0, out, |out| put_payload(out, msgs))
}
pub fn seal_stream_frame(
&self,
msgs: &[Message<'_>],
nonce: &[u8; NONCE_LEN],
out: &mut Vec<u8>,
) -> Result<(), EncodeError> {
let start = out.len();
out.put(&[0; 4]);
if let Err(e) = self.seal(PacketKind::Stream, msgs, nonce, out) {
out.truncate(start);
return Err(e);
}
let len = (out.len() - start - 4) as u32;
out[start..start + 4].copy_from_slice(&len.to_be_bytes());
Ok(())
}
#[doc(hidden)]
pub fn seal_raw(
&self,
kind: PacketKind,
inner: &[u8],
nonce: &[u8; NONCE_LEN],
out: &mut Vec<u8>,
) -> Result<(), EncodeError> {
self.seal_with(kind, nonce, inner.len(), out, |out| out.put(inner))
}
fn seal_with(
&self,
kind: PacketKind,
nonce: &[u8; NONCE_LEN],
payload_len: usize,
out: &mut Vec<u8>,
write_payload: impl FnOnce(&mut Vec<u8>),
) -> Result<(), EncodeError> {
if payload_len > self.max_payload_len(kind) {
return Err(EncodeError::TooLarge);
}
let start = out.len();
let mut flags = if kind == PacketKind::Stream {
FLAG_STREAM
} else {
0
};
out.put(&MAGIC);
out.put_u8(WIRE_VERSION);
match &self.security {
Security::Plaintext => {
out.put_u8(flags);
out.put(&self.label_hash);
write_payload(out);
}
Security::Encrypted(keys) => {
let key = &keys.keys[0];
flags |= FLAG_ENCRYPTED;
out.put_u8(flags);
out.put(&keys.ids[0].to_be_bytes());
out.put(nonce);
write_payload(out);
let aad = Aad::new(&out[start..start + ENCRYPTED_HEADER_LEN], &self.label);
let cipher = XChaCha20Poly1305::new((&*key.0).into());
let body = &mut out[start + ENCRYPTED_HEADER_LEN..];
let tag = cipher
.encrypt_inout_detached(nonce.into(), aad.as_slice(), (&mut *body).into())
.map_err(|_| EncodeError::TooLarge);
match tag {
Ok(tag) => out.put(tag.as_slice()),
Err(e) => {
out.truncate(start);
return Err(e);
}
}
}
}
debug_assert_eq!(out.len() - start, payload_len + self.overhead());
Ok(())
}
pub fn open<'a>(
&self,
kind: PacketKind,
buf: &'a mut [u8],
) -> Result<Payload<'a>, DecodeError> {
if buf.len() > self.limit(kind) {
return Err(DecodeError::TooLarge);
}
let encrypted = check_start(kind, buf)?;
let range = match (&self.security, encrypted) {
(Security::Plaintext, false) => {
let hash = buf
.get(4..PLAINTEXT_HEADER_LEN)
.ok_or(DecodeError::Truncated)?;
if hash != self.label_hash {
return Err(DecodeError::WrongCluster);
}
PLAINTEXT_HEADER_LEN..buf.len()
}
(Security::Encrypted(keys), true) => {
if buf.len() < ENCRYPTED_OVERHEAD {
return Err(DecodeError::Truncated);
}
let key_id = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]);
let nonce: [u8; NONCE_LEN] = buf[8..ENCRYPTED_HEADER_LEN]
.try_into()
.map_err(|_| DecodeError::Truncated)?;
let aad = Aad::new(&buf[..ENCRYPTED_HEADER_LEN], &self.label);
let tag_at = buf.len() - TAG_LEN;
let tag: [u8; TAG_LEN] = buf[tag_at..]
.try_into()
.map_err(|_| DecodeError::Truncated)?;
let body = &mut buf[ENCRYPTED_HEADER_LEN..tag_at];
let mut matched = false;
let mut opened = false;
let installed = keys.keys.iter().zip(&keys.ids);
for (key, _) in installed.filter(|&(_, &id)| id == key_id) {
matched = true;
let cipher = XChaCha20Poly1305::new((&*key.0).into());
if cipher
.decrypt_inout_detached(
(&nonce).into(),
aad.as_slice(),
(&mut *body).into(),
(&tag).into(),
)
.is_ok()
{
opened = true;
break;
}
}
if !matched {
let cipher = XChaCha20Poly1305::new((&*keys.dummy.0).into());
let _ = cipher.decrypt_inout_detached(
(&nonce).into(),
aad.as_slice(),
(&mut *body).into(),
(&tag).into(),
);
return Err(DecodeError::UnknownKey(key_id));
}
if !opened {
return Err(DecodeError::AuthFailed);
}
ENCRYPTED_HEADER_LEN..tag_at
}
_ => return Err(DecodeError::EncryptionMismatch),
};
Payload::parse(&buf[range], &self.limits)
}
}
fn check_start(kind: PacketKind, buf: &[u8]) -> Result<bool, DecodeError> {
let [m0, m1, version, flags, ..] = *buf else {
return Err(DecodeError::Truncated);
};
if [m0, m1] != MAGIC {
return Err(DecodeError::BadMagic);
}
if version == 0 || version > WIRE_VERSION {
return Err(DecodeError::UnsupportedVersion(version));
}
if flags & FLAGS_RESERVED != 0 {
return Err(DecodeError::ReservedFlags);
}
if (flags & FLAG_STREAM != 0) != (kind == PacketKind::Stream) {
return Err(DecodeError::WrongPacketKind);
}
Ok(flags & FLAG_ENCRYPTED != 0)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HeaderCheck {
key_ids: Option<Vec<u32>>,
label: [u8; 4],
}
impl HeaderCheck {
pub const LEN: usize = 8;
pub fn check(&self, kind: PacketKind, head: &[u8]) -> Result<(), DecodeError> {
let head = head.get(..Self::LEN).ok_or(DecodeError::Truncated)?;
let encrypted = check_start(kind, head)?;
let id = [head[4], head[5], head[6], head[7]];
match &self.key_ids {
None if encrypted => Err(DecodeError::EncryptionMismatch),
None if id != self.label => Err(DecodeError::WrongCluster),
Some(_) if !encrypted => Err(DecodeError::EncryptionMismatch),
Some(ids) if !ids.contains(&u32::from_be_bytes(id)) => {
Err(DecodeError::UnknownKey(u32::from_be_bytes(id)))
}
_ => Ok(()),
}
}
}
pub fn sealed_nonce(buf: &[u8]) -> Option<[u8; NONCE_LEN]> {
if buf.len() < ENCRYPTED_OVERHEAD || buf[..2] != MAGIC || buf[3] & FLAG_ENCRYPTED == 0 {
return None;
}
buf[8..ENCRYPTED_HEADER_LEN].try_into().ok()
}
struct Aad {
buf: [u8; ENCRYPTED_HEADER_LEN + MAX_LABEL_LEN],
len: usize,
}
impl Aad {
fn new(header: &[u8], label: &[u8]) -> Self {
let mut buf = [0u8; ENCRYPTED_HEADER_LEN + MAX_LABEL_LEN];
let len = header.len() + label.len();
buf[..header.len()].copy_from_slice(header);
buf[header.len()..len].copy_from_slice(label);
Self { buf, len }
}
fn as_slice(&self) -> &[u8] {
&self.buf[..self.len]
}
}
#[cfg(test)]
mod tests {
use super::*;
fn key(n: u8) -> Key {
Key::from_bytes([n; 32])
}
fn keys(c: &Codec) -> &Keys {
match &c.security {
Security::Encrypted(keys) => keys,
Security::Plaintext => unreachable!("an encrypting codec"),
}
}
#[test]
fn keys_are_equal_only_when_every_byte_is() {
let a = key(7);
assert!(a == key(7));
for at in [0, 1, 16, 31] {
let mut bytes = [7; 32];
bytes[at] ^= 0x80;
assert!(a != Key::from_bytes(bytes), "byte {at}");
}
}
#[test]
fn key_bytes_stay_where_they_are_when_keys_move() {
let first = key(1);
let at = first.0.as_ptr();
let mut keys = vec![first];
keys.extend((2..40).map(key));
let moved = keys.swap_remove(0);
assert_eq!(moved.0.as_ptr(), at);
let text: Zeroizing<String> = moved.to_base64();
assert_eq!(
text.capacity(),
text.len(),
"written into its final allocation"
);
}
#[test]
fn ids_and_the_dummy_key_follow_every_change_of_keys() {
let mut c = Codec::encrypted(b"c", Limits::default(), vec![key(1)]).unwrap();
let first = keys(&c).dummy.clone();
assert_eq!(keys(&c).ids, [key(1).id()]);
c.install_key(key(2)).unwrap();
assert_eq!(keys(&c).ids, [key(1).id(), key(2).id()]);
assert!(keys(&c).dummy == first, "the sealing key did not change");
c.use_key(&key(2)).unwrap();
assert_eq!(keys(&c).ids, [key(2).id(), key(1).id()]);
assert!(keys(&c).dummy != first, "derived from the new sealing key");
c.remove_key(&key(1)).unwrap();
assert_eq!(keys(&c).ids, [key(2).id()]);
c.set_keys(vec![key(3)]).unwrap();
assert_eq!(keys(&c).ids, [key(3).id()]);
assert_eq!(c.key_ids(), [key(3).key_id()]);
}
#[test]
fn a_packet_with_an_unknown_key_id_is_refused_even_when_the_dummy_key_opens_it() {
let c = Codec::encrypted(b"c", Limits::default(), vec![key(1)]).unwrap();
let dummy = keys(&c).dummy.clone();
let forger = Codec::encrypted(b"c", Limits::default(), vec![dummy]).unwrap();
let mut pkt = Vec::new();
let ack = Message::Ack { seq: 1 };
forger
.seal(PacketKind::Datagram, &[ack], &[0; 24], &mut pkt)
.unwrap();
let id = keys(&forger).ids[0];
assert!(!keys(&c).ids.contains(&id));
let opened = c.open(PacketKind::Datagram, &mut pkt).map(drop);
assert_eq!(opened, Err(DecodeError::UnknownKey(id)));
}
}