Skip to main content

kinship_proto/
packet.rs

1//! The outer packet: header, AEAD envelope and size limits.
2//!
3//! Encrypted layout (all integers big-endian):
4//!
5//! ```text
6//!  0..2   magic "kn"
7//!  2      version
8//!  3      flags: bit 0 encrypted, bit 1 stream frame, rest reserved (zero)
9//!  4..8   key_id, first 4 bytes of BLAKE3(key)
10//!  8..32  nonce, 24 bytes from the caller: kinship-core puts its cluster time first
11//!  32..n  ciphertext of the inner payload
12//!  n..+16 Poly1305 tag
13//! ```
14//!
15//! The AEAD associated data is the 32 header bytes followed by the cluster label. Plaintext
16//! mode, for tests only, replaces `key_id` and `nonce` with the first 8 bytes of
17//! BLAKE3(label), giving a 12-byte header and no tag.
18
19use chacha20poly1305::{AeadInOut, KeyInit, XChaCha20Poly1305};
20use ctutils::CtEq;
21use zeroize::{Zeroize, Zeroizing};
22
23use crate::error::{ConfigError, DecodeError, EncodeError, KeyError, KeyringError};
24use crate::limits::Limits;
25use crate::message::{Message, Payload, put_payload};
26use crate::wire::{Count, Sink};
27
28/// Wire format version spoken by this build.
29pub const WIRE_VERSION: u8 = 1;
30
31/// First two bytes of every packet: "kn".
32pub const MAGIC: [u8; 2] = *b"kn";
33
34/// Longest cluster label in bytes. The label is part of the AEAD associated data, which is
35/// assembled on the stack.
36pub const MAX_LABEL_LEN: usize = 255;
37
38/// Header size of an encrypted packet.
39pub const ENCRYPTED_HEADER_LEN: usize = 32;
40/// Header size of a plaintext packet.
41pub const PLAINTEXT_HEADER_LEN: usize = 12;
42/// Size of the Poly1305 tag.
43pub const TAG_LEN: usize = 16;
44/// Size of the XChaCha20 nonce.
45pub const NONCE_LEN: usize = 24;
46/// Per-packet cost of encryption: header plus tag.
47pub const ENCRYPTED_OVERHEAD: usize = ENCRYPTED_HEADER_LEN + TAG_LEN;
48
49const FLAG_ENCRYPTED: u8 = 0b01;
50const FLAG_STREAM: u8 = 0b10;
51const FLAGS_RESERVED: u8 = !(FLAG_ENCRYPTED | FLAG_STREAM);
52
53/// Whether a packet travels as a UDP datagram or inside a TCP stream frame.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
55pub enum PacketKind {
56    Datagram,
57    Stream,
58}
59
60/// A 32-byte XChaCha20-Poly1305 key. Zeroized on drop and never printed.
61///
62/// The bytes live on the heap, so moving a key, or growing a list of keys, moves a pointer and
63/// leaves no copy of the bytes behind. Keys compare in constant time.
64#[derive(Clone)]
65pub struct Key(Box<[u8; 32]>);
66
67impl PartialEq for Key {
68    /// Looks at every byte whatever it finds, so the time taken does not tell where two keys
69    /// differ.
70    fn eq(&self, other: &Self) -> bool {
71        self.0.ct_eq(&*other.0).to_bool()
72    }
73}
74
75impl Eq for Key {}
76
77impl Key {
78    pub const LEN: usize = 32;
79
80    /// A key holding `bytes`. Copies the caller still holds are the caller's to zeroize.
81    pub fn from_bytes(mut bytes: [u8; 32]) -> Self {
82        let key = Self(Box::new(bytes));
83        bytes.zeroize();
84        key
85    }
86
87    /// Returns `None` unless `bytes` is exactly 32 bytes long.
88    pub fn from_slice(bytes: &[u8]) -> Option<Self> {
89        if bytes.len() != Self::LEN {
90            return None;
91        }
92        let mut key = Self(Box::new([0; 32]));
93        key.0.copy_from_slice(bytes);
94        Some(key)
95    }
96
97    /// A key written as standard base64, with or without `=` padding, as `kinship keygen`
98    /// prints it. Surrounding whitespace is ignored.
99    pub fn from_base64(text: &str) -> Result<Self, KeyError> {
100        let text = text.trim();
101        let digits = text.trim_end_matches('=');
102        if text.len() - digits.len() > 2 {
103            return Err(KeyError::NotBase64);
104        }
105        // Decoded in place, and zeroized on drop if it is not a key after all.
106        let mut key = Self(Box::new([0; 32]));
107        let mut len = 0;
108        let mut acc = 0u32;
109        let mut bits = 0;
110        let mut result = Ok(());
111        for c in digits.bytes() {
112            let v = match c {
113                b'A'..=b'Z' => c - b'A',
114                b'a'..=b'z' => c - b'a' + 26,
115                b'0'..=b'9' => c - b'0' + 52,
116                b'+' => 62,
117                b'/' => 63,
118                _ => {
119                    result = Err(KeyError::NotBase64);
120                    break;
121                }
122            };
123            acc = (acc << 6) | u32::from(v);
124            bits += 6;
125            if bits >= 8 {
126                bits -= 8;
127                if len == Self::LEN {
128                    result = Err(KeyError::WrongLength);
129                    break;
130                }
131                key.0[len] = (acc >> bits) as u8;
132                len += 1;
133                acc &= (1 << bits) - 1;
134            }
135        }
136        // Leftover bits must be the zero padding of the last group.
137        if result.is_ok() && (bits >= 6 || acc != 0) {
138            result = Err(KeyError::NotBase64);
139        }
140        if result.is_ok() && len != Self::LEN {
141            result = Err(KeyError::WrongLength);
142        }
143        acc.zeroize();
144        result.map(|()| key)
145    }
146
147    /// The key as padded standard base64, the form [`from_base64`](Self::from_base64) reads and
148    /// `kinship keygen` prints. The text is the secret itself: never log it. It is zeroized when
149    /// dropped, and written into its final allocation, so no copy is left behind.
150    pub fn to_base64(&self) -> Zeroizing<String> {
151        const DIGITS: &[u8; 64] =
152            b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
153        let mut out = Zeroizing::new(String::with_capacity(44));
154        for chunk in self.0.chunks(3) {
155            let b = [
156                chunk[0],
157                *chunk.get(1).unwrap_or(&0),
158                *chunk.get(2).unwrap_or(&0),
159            ];
160            let mut group = u32::from_be_bytes([0, b[0], b[1], b[2]]);
161            for i in 0..4 {
162                if i <= chunk.len() {
163                    out.push(DIGITS[(group >> 18) as usize & 63] as char);
164                } else {
165                    out.push('=');
166                }
167                group <<= 6;
168            }
169            group.zeroize();
170        }
171        out
172    }
173
174    /// First 4 bytes of BLAKE3(key), big-endian. Sent in the clear to pick the right key.
175    pub fn id(&self) -> u32 {
176        let hash = blake3::hash(&self.0[..]);
177        let b = hash.as_bytes();
178        u32::from_be_bytes([b[0], b[1], b[2], b[3]])
179    }
180
181    /// [`id`](Self::id) as a [`KeyId`], which prints as 8 hex characters.
182    pub fn key_id(&self) -> KeyId {
183        KeyId(self.id())
184    }
185}
186
187/// The public id of a [`Key`]: safe to log, and printed as 8 hex characters.
188#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
189pub struct KeyId(u32);
190
191impl KeyId {
192    pub const fn from_raw(raw: u32) -> Self {
193        Self(raw)
194    }
195
196    pub const fn to_raw(self) -> u32 {
197        self.0
198    }
199}
200
201impl core::fmt::Display for KeyId {
202    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
203        write!(f, "{:08x}", self.0)
204    }
205}
206
207impl core::fmt::Debug for KeyId {
208    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
209        write!(f, "KeyId({:08x})", self.0)
210    }
211}
212
213impl Drop for Key {
214    fn drop(&mut self) {
215        self.0.zeroize();
216    }
217}
218
219impl core::fmt::Debug for Key {
220    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
221        write!(f, "Key({:08x})", self.id())
222    }
223}
224
225enum Security {
226    Plaintext,
227    Encrypted(Keys),
228}
229
230/// The context BLAKE3 derives the dummy key in; see [`Keys::dummy`].
231const DUMMY_KEY_CONTEXT: &str = "kinship 2026-10-08 key for packets with an unknown key id";
232
233/// The installed keys, with what is worked out from them once whenever they change rather than
234/// for every packet.
235struct Keys {
236    /// The first seals; every key opens.
237    keys: Vec<Key>,
238    /// The id of each key in `keys`.
239    ids: Vec<u32>,
240    /// Checks the tag of a packet whose key id no installed key has, so that refusing it costs
241    /// the one tag verification that a packet with an installed key's id and a bad tag costs.
242    /// Derived from the sealing key, so only a holder of that key could seal a packet it opens.
243    dummy: Key,
244}
245
246impl Keys {
247    /// `keys` must not be empty.
248    fn new(keys: Vec<Key>) -> Self {
249        let (ids, dummy) = Self::derive(&keys);
250        Self { keys, ids, dummy }
251    }
252
253    /// Works out the ids and the dummy key again; call after every change to `keys`.
254    fn changed(&mut self) {
255        (self.ids, self.dummy) = Self::derive(&self.keys);
256    }
257
258    /// The ids of `keys`, and the dummy key derived from the first of them.
259    fn derive(keys: &[Key]) -> (Vec<u32>, Key) {
260        let ids = keys.iter().map(Key::id).collect();
261        let mut dummy = blake3::derive_key(DUMMY_KEY_CONTEXT, &keys[0].0[..]);
262        let key = Key::from_bytes(dummy);
263        dummy.zeroize();
264        (ids, key)
265    }
266}
267
268/// Encodes and decodes packets for one cluster.
269///
270/// The codec owns the cluster label, the size limits and the keys. It performs no I/O, reads no
271/// clock and draws no randomness: the caller supplies the nonce for each sealed packet.
272pub struct Codec {
273    label: Vec<u8>,
274    label_hash: [u8; 8],
275    limits: Limits,
276    security: Security,
277}
278
279impl Codec {
280    /// A codec that encrypts with `keys[0]` and accepts any key in `keys`.
281    pub fn encrypted(label: &[u8], limits: Limits, keys: Vec<Key>) -> Result<Self, ConfigError> {
282        if keys.is_empty() {
283            return Err(ConfigError::NoKeys);
284        }
285        Self::new(label, limits, Security::Encrypted(Keys::new(keys)))
286    }
287
288    /// A codec that sends and accepts unauthenticated, unencrypted packets. This is the
289    /// `insecure_plaintext` mode and exists for tests, the simulator and loopback use.
290    pub fn insecure_plaintext(label: &[u8], limits: Limits) -> Result<Self, ConfigError> {
291        Self::new(label, limits, Security::Plaintext)
292    }
293
294    fn new(label: &[u8], limits: Limits, security: Security) -> Result<Self, ConfigError> {
295        if label.len() > MAX_LABEL_LEN {
296            return Err(ConfigError::LabelTooLong);
297        }
298        let hash = blake3::hash(label);
299        let mut label_hash = [0u8; 8];
300        label_hash.copy_from_slice(&hash.as_bytes()[..8]);
301        let codec = Self {
302            label: label.to_vec(),
303            label_hash,
304            limits,
305            security,
306        };
307        // The smallest packet holds a count, one message type and one length byte.
308        let floor = codec.overhead() + 3;
309        if limits.udp_max_payload < floor || limits.max_stream_frame < floor {
310            return Err(ConfigError::LimitTooSmall);
311        }
312        Ok(codec)
313    }
314
315    /// Replaces the installed keys; `keys[0]` becomes the sealing key.
316    pub fn set_keys(&mut self, keys: Vec<Key>) -> Result<(), ConfigError> {
317        if keys.is_empty() {
318            return Err(ConfigError::NoKeys);
319        }
320        self.security = Security::Encrypted(Keys::new(keys));
321        Ok(())
322    }
323
324    pub fn is_encrypted(&self) -> bool {
325        matches!(self.security, Security::Encrypted(_))
326    }
327
328    pub fn limits(&self) -> &Limits {
329        &self.limits
330    }
331
332    /// Ids of the installed keys, sealing key first. Empty in plaintext mode.
333    pub fn key_ids(&self) -> Vec<KeyId> {
334        match &self.security {
335            Security::Plaintext => Vec::new(),
336            Security::Encrypted(keys) => keys.ids.iter().map(|&id| KeyId(id)).collect(),
337        }
338    }
339
340    /// What the first [`HeaderCheck::LEN`] bytes of a packet must hold for this codec, with
341    /// the keys installed now.
342    pub fn header_check(&self) -> HeaderCheck {
343        let mut label = [0; 4];
344        label.copy_from_slice(&self.label_hash[..4]);
345        HeaderCheck {
346            key_ids: match &self.security {
347                Security::Plaintext => None,
348                Security::Encrypted(keys) => Some(keys.ids.clone()),
349            },
350            label,
351        }
352    }
353
354    /// Adds `key` to the keys that open packets, after the existing ones. Does nothing if it is
355    /// already installed.
356    pub fn install_key(&mut self, key: Key) -> Result<(), KeyringError> {
357        let keys = self.keys_mut()?;
358        if !keys.keys.contains(&key) {
359            keys.keys.push(key);
360            keys.changed();
361        }
362        Ok(())
363    }
364
365    /// Makes the installed `key` the one that seals; the others keep their order and still open.
366    pub fn use_key(&mut self, key: &Key) -> Result<(), KeyringError> {
367        let keys = self.keys_mut()?;
368        let at = keys
369            .keys
370            .iter()
371            .position(|k| k == key)
372            .ok_or(KeyringError::NotInstalled)?;
373        keys.keys[..=at].rotate_right(1);
374        keys.changed();
375        Ok(())
376    }
377
378    /// Drops `key`. Refuses the only key and the sealing key; a key that is not installed is
379    /// already gone, so that succeeds.
380    pub fn remove_key(&mut self, key: &Key) -> Result<(), KeyringError> {
381        let keys = self.keys_mut()?;
382        match keys.keys.iter().position(|k| k == key) {
383            None => Ok(()),
384            Some(_) if keys.keys.len() == 1 => Err(KeyringError::LastKey),
385            Some(0) => Err(KeyringError::InUse),
386            Some(at) => {
387                keys.keys.remove(at);
388                keys.changed();
389                Ok(())
390            }
391        }
392    }
393
394    fn keys_mut(&mut self) -> Result<&mut Keys, KeyringError> {
395        match &mut self.security {
396            Security::Plaintext => Err(KeyringError::Plaintext),
397            Security::Encrypted(keys) => Ok(keys),
398        }
399    }
400
401    /// Bytes a packet adds around its inner payload.
402    pub fn overhead(&self) -> usize {
403        match self.security {
404            Security::Plaintext => PLAINTEXT_HEADER_LEN,
405            Security::Encrypted(_) => ENCRYPTED_OVERHEAD,
406        }
407    }
408
409    /// Most inner-payload bytes (count included) that fit one packet of this kind.
410    pub fn max_payload_len(&self, kind: PacketKind) -> usize {
411        self.limit(kind) - self.overhead()
412    }
413
414    fn limit(&self, kind: PacketKind) -> usize {
415        match kind {
416            PacketKind::Datagram => self.limits.udp_max_payload,
417            PacketKind::Stream => self.limits.max_stream_frame,
418        }
419    }
420
421    /// Seals `msgs` into one packet appended to `out`.
422    ///
423    /// `nonce` must be 24 fresh random bytes for every call; it is ignored in plaintext mode.
424    /// On error `out` is left as it was. For [`PacketKind::Stream`] the result has no length
425    /// prefix; see [`seal_stream_frame`](Self::seal_stream_frame).
426    pub fn seal(
427        &self,
428        kind: PacketKind,
429        msgs: &[Message<'_>],
430        nonce: &[u8; NONCE_LEN],
431        out: &mut Vec<u8>,
432    ) -> Result<(), EncodeError> {
433        if msgs.is_empty() {
434            return Err(EncodeError::Empty);
435        }
436        for m in msgs {
437            m.validate(&self.limits)?;
438        }
439        let mut payload = Count::default();
440        put_payload(&mut payload, msgs);
441        self.seal_with(kind, nonce, payload.0, out, |out| put_payload(out, msgs))
442    }
443
444    /// Like [`seal`](Self::seal) for a TCP stream: prefixes the packet with its `u32`
445    /// big-endian length.
446    pub fn seal_stream_frame(
447        &self,
448        msgs: &[Message<'_>],
449        nonce: &[u8; NONCE_LEN],
450        out: &mut Vec<u8>,
451    ) -> Result<(), EncodeError> {
452        let start = out.len();
453        out.put(&[0; 4]);
454        if let Err(e) = self.seal(PacketKind::Stream, msgs, nonce, out) {
455            out.truncate(start);
456            return Err(e);
457        }
458        let len = (out.len() - start - 4) as u32;
459        out[start..start + 4].copy_from_slice(&len.to_be_bytes());
460        Ok(())
461    }
462
463    /// Seals arbitrary bytes as the inner payload, without checking that they parse. For tests
464    /// and fuzzing of the receive path only.
465    #[doc(hidden)]
466    pub fn seal_raw(
467        &self,
468        kind: PacketKind,
469        inner: &[u8],
470        nonce: &[u8; NONCE_LEN],
471        out: &mut Vec<u8>,
472    ) -> Result<(), EncodeError> {
473        self.seal_with(kind, nonce, inner.len(), out, |out| out.put(inner))
474    }
475
476    fn seal_with(
477        &self,
478        kind: PacketKind,
479        nonce: &[u8; NONCE_LEN],
480        payload_len: usize,
481        out: &mut Vec<u8>,
482        write_payload: impl FnOnce(&mut Vec<u8>),
483    ) -> Result<(), EncodeError> {
484        if payload_len > self.max_payload_len(kind) {
485            return Err(EncodeError::TooLarge);
486        }
487        let start = out.len();
488        let mut flags = if kind == PacketKind::Stream {
489            FLAG_STREAM
490        } else {
491            0
492        };
493        out.put(&MAGIC);
494        out.put_u8(WIRE_VERSION);
495        match &self.security {
496            Security::Plaintext => {
497                out.put_u8(flags);
498                out.put(&self.label_hash);
499                write_payload(out);
500            }
501            Security::Encrypted(keys) => {
502                let key = &keys.keys[0];
503                flags |= FLAG_ENCRYPTED;
504                out.put_u8(flags);
505                out.put(&keys.ids[0].to_be_bytes());
506                out.put(nonce);
507                write_payload(out);
508                let aad = Aad::new(&out[start..start + ENCRYPTED_HEADER_LEN], &self.label);
509                let cipher = XChaCha20Poly1305::new((&*key.0).into());
510                let body = &mut out[start + ENCRYPTED_HEADER_LEN..];
511                let tag = cipher
512                    .encrypt_inout_detached(nonce.into(), aad.as_slice(), (&mut *body).into())
513                    // The only failure is a body past the cipher's 256 GiB limit.
514                    .map_err(|_| EncodeError::TooLarge);
515                match tag {
516                    Ok(tag) => out.put(tag.as_slice()),
517                    Err(e) => {
518                        out.truncate(start);
519                        return Err(e);
520                    }
521                }
522            }
523        }
524        debug_assert_eq!(out.len() - start, payload_len + self.overhead());
525        Ok(())
526    }
527
528    /// Authenticates, decrypts in place and parses one packet.
529    ///
530    /// The size limit, magic, version, flags, packet kind and key id are all checked before any
531    /// cryptography runs, so unauthenticated traffic costs one tag verification per installed
532    /// key with the packet's key id, or, when no key has it, one with a dummy key, so that the
533    /// time taken does not tell which ids are installed. The returned payload borrows the decrypted bytes of `buf`; on success the
534    /// ciphertext has been overwritten with plaintext, on failure `buf` is unchanged.
535    pub fn open<'a>(
536        &self,
537        kind: PacketKind,
538        buf: &'a mut [u8],
539    ) -> Result<Payload<'a>, DecodeError> {
540        if buf.len() > self.limit(kind) {
541            return Err(DecodeError::TooLarge);
542        }
543        let encrypted = check_start(kind, buf)?;
544        let range = match (&self.security, encrypted) {
545            (Security::Plaintext, false) => {
546                let hash = buf
547                    .get(4..PLAINTEXT_HEADER_LEN)
548                    .ok_or(DecodeError::Truncated)?;
549                if hash != self.label_hash {
550                    return Err(DecodeError::WrongCluster);
551                }
552                PLAINTEXT_HEADER_LEN..buf.len()
553            }
554            (Security::Encrypted(keys), true) => {
555                if buf.len() < ENCRYPTED_OVERHEAD {
556                    return Err(DecodeError::Truncated);
557                }
558                let key_id = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]);
559                let nonce: [u8; NONCE_LEN] = buf[8..ENCRYPTED_HEADER_LEN]
560                    .try_into()
561                    .map_err(|_| DecodeError::Truncated)?;
562                let aad = Aad::new(&buf[..ENCRYPTED_HEADER_LEN], &self.label);
563                let tag_at = buf.len() - TAG_LEN;
564                let tag: [u8; TAG_LEN] = buf[tag_at..]
565                    .try_into()
566                    .map_err(|_| DecodeError::Truncated)?;
567                let body = &mut buf[ENCRYPTED_HEADER_LEN..tag_at];
568                let mut matched = false;
569                let mut opened = false;
570                let installed = keys.keys.iter().zip(&keys.ids);
571                for (key, _) in installed.filter(|&(_, &id)| id == key_id) {
572                    matched = true;
573                    let cipher = XChaCha20Poly1305::new((&*key.0).into());
574                    if cipher
575                        .decrypt_inout_detached(
576                            (&nonce).into(),
577                            aad.as_slice(),
578                            (&mut *body).into(),
579                            (&tag).into(),
580                        )
581                        .is_ok()
582                    {
583                        opened = true;
584                        break;
585                    }
586                }
587                if !matched {
588                    // One tag verification all the same, so that refusing a packet whose key
589                    // id no key has takes as long as refusing one with an installed key's id and
590                    // a bad tag, and the time does not tell which ids are installed. Should the
591                    // tag pass, which only a holder of the sealing key could arrange, the packet
592                    // is refused anyway.
593                    let cipher = XChaCha20Poly1305::new((&*keys.dummy.0).into());
594                    let _ = cipher.decrypt_inout_detached(
595                        (&nonce).into(),
596                        aad.as_slice(),
597                        (&mut *body).into(),
598                        (&tag).into(),
599                    );
600                    return Err(DecodeError::UnknownKey(key_id));
601                }
602                if !opened {
603                    return Err(DecodeError::AuthFailed);
604                }
605                ENCRYPTED_HEADER_LEN..tag_at
606            }
607            _ => return Err(DecodeError::EncryptionMismatch),
608        };
609        Payload::parse(&buf[range], &self.limits)
610    }
611}
612
613/// Checks the magic, version and flags that start every packet, and that the packet is of
614/// `kind`. Returns whether it is encrypted.
615fn check_start(kind: PacketKind, buf: &[u8]) -> Result<bool, DecodeError> {
616    let [m0, m1, version, flags, ..] = *buf else {
617        return Err(DecodeError::Truncated);
618    };
619    if [m0, m1] != MAGIC {
620        return Err(DecodeError::BadMagic);
621    }
622    if version == 0 || version > WIRE_VERSION {
623        return Err(DecodeError::UnsupportedVersion(version));
624    }
625    if flags & FLAGS_RESERVED != 0 {
626        return Err(DecodeError::ReservedFlags);
627    }
628    if (flags & FLAG_STREAM != 0) != (kind == PacketKind::Stream) {
629        return Err(DecodeError::WrongPacketKind);
630    }
631    Ok(flags & FLAG_ENCRYPTED != 0)
632}
633
634/// What the first [`LEN`](Self::LEN) bytes of a packet must hold for one [`Codec`]: magic,
635/// version, flags, and an installed key's id, or in plaintext mode the start of the cluster
636/// label's hash. These are the checks [`Codec::open`] makes before any cryptography that need
637/// nothing past the key id.
638///
639/// A driver uses it to drop a TCP connection that does not start like a frame for this node
640/// before the rest of the frame arrives. It is a copy: take a new one from
641/// [`Codec::header_check`] whenever the keys change. Like the header itself, passing it proves
642/// nothing about the sender: key ids are sent in the clear.
643#[derive(Debug, Clone, PartialEq, Eq)]
644pub struct HeaderCheck {
645    /// Ids of the installed keys; `None` in plaintext mode.
646    key_ids: Option<Vec<u32>>,
647    /// The first bytes of BLAKE3(label), which a plaintext header carries after its flags.
648    label: [u8; 4],
649}
650
651impl HeaderCheck {
652    /// Bytes checked: magic, version, flags and key id.
653    pub const LEN: usize = 8;
654
655    /// Checks the first [`LEN`](Self::LEN) bytes of a packet of `kind`. Fewer bytes fail as
656    /// [`DecodeError::Truncated`]; bytes past them are ignored.
657    pub fn check(&self, kind: PacketKind, head: &[u8]) -> Result<(), DecodeError> {
658        let head = head.get(..Self::LEN).ok_or(DecodeError::Truncated)?;
659        let encrypted = check_start(kind, head)?;
660        let id = [head[4], head[5], head[6], head[7]];
661        match &self.key_ids {
662            None if encrypted => Err(DecodeError::EncryptionMismatch),
663            None if id != self.label => Err(DecodeError::WrongCluster),
664            Some(_) if !encrypted => Err(DecodeError::EncryptionMismatch),
665            Some(ids) if !ids.contains(&u32::from_be_bytes(id)) => {
666                Err(DecodeError::UnknownKey(u32::from_be_bytes(id)))
667            }
668            _ => Ok(()),
669        }
670    }
671}
672
673/// The nonce in the header of a sealed packet, read without authenticating anything.
674///
675/// `None` unless `buf` starts like an encrypted packet of either kind and is long enough to
676/// hold a header and a tag. The receiver may use it to drop a packet cheaply before opening it,
677/// but nothing read here is authentic until [`Codec::open`] succeeds.
678pub fn sealed_nonce(buf: &[u8]) -> Option<[u8; NONCE_LEN]> {
679    if buf.len() < ENCRYPTED_OVERHEAD || buf[..2] != MAGIC || buf[3] & FLAG_ENCRYPTED == 0 {
680        return None;
681    }
682    buf[8..ENCRYPTED_HEADER_LEN].try_into().ok()
683}
684
685/// The AEAD associated data: header bytes then label, on the stack.
686struct Aad {
687    buf: [u8; ENCRYPTED_HEADER_LEN + MAX_LABEL_LEN],
688    len: usize,
689}
690
691impl Aad {
692    fn new(header: &[u8], label: &[u8]) -> Self {
693        let mut buf = [0u8; ENCRYPTED_HEADER_LEN + MAX_LABEL_LEN];
694        let len = header.len() + label.len();
695        buf[..header.len()].copy_from_slice(header);
696        buf[header.len()..len].copy_from_slice(label);
697        Self { buf, len }
698    }
699
700    fn as_slice(&self) -> &[u8] {
701        &self.buf[..self.len]
702    }
703}
704
705#[cfg(test)]
706mod tests {
707    use super::*;
708
709    fn key(n: u8) -> Key {
710        Key::from_bytes([n; 32])
711    }
712
713    fn keys(c: &Codec) -> &Keys {
714        match &c.security {
715            Security::Encrypted(keys) => keys,
716            Security::Plaintext => unreachable!("an encrypting codec"),
717        }
718    }
719
720    #[test]
721    fn keys_are_equal_only_when_every_byte_is() {
722        let a = key(7);
723        assert!(a == key(7));
724        for at in [0, 1, 16, 31] {
725            let mut bytes = [7; 32];
726            bytes[at] ^= 0x80;
727            assert!(a != Key::from_bytes(bytes), "byte {at}");
728        }
729    }
730
731    #[test]
732    fn key_bytes_stay_where_they_are_when_keys_move() {
733        let first = key(1);
734        let at = first.0.as_ptr();
735        let mut keys = vec![first];
736        // Growing the list moves its keys more than once.
737        keys.extend((2..40).map(key));
738        let moved = keys.swap_remove(0);
739        assert_eq!(moved.0.as_ptr(), at);
740        let text: Zeroizing<String> = moved.to_base64();
741        assert_eq!(
742            text.capacity(),
743            text.len(),
744            "written into its final allocation"
745        );
746    }
747
748    #[test]
749    fn ids_and_the_dummy_key_follow_every_change_of_keys() {
750        let mut c = Codec::encrypted(b"c", Limits::default(), vec![key(1)]).unwrap();
751        let first = keys(&c).dummy.clone();
752        assert_eq!(keys(&c).ids, [key(1).id()]);
753        c.install_key(key(2)).unwrap();
754        assert_eq!(keys(&c).ids, [key(1).id(), key(2).id()]);
755        assert!(keys(&c).dummy == first, "the sealing key did not change");
756        c.use_key(&key(2)).unwrap();
757        assert_eq!(keys(&c).ids, [key(2).id(), key(1).id()]);
758        assert!(keys(&c).dummy != first, "derived from the new sealing key");
759        c.remove_key(&key(1)).unwrap();
760        assert_eq!(keys(&c).ids, [key(2).id()]);
761        c.set_keys(vec![key(3)]).unwrap();
762        assert_eq!(keys(&c).ids, [key(3).id()]);
763        assert_eq!(c.key_ids(), [key(3).key_id()]);
764    }
765
766    #[test]
767    fn a_packet_with_an_unknown_key_id_is_refused_even_when_the_dummy_key_opens_it() {
768        let c = Codec::encrypted(b"c", Limits::default(), vec![key(1)]).unwrap();
769        let dummy = keys(&c).dummy.clone();
770        let forger = Codec::encrypted(b"c", Limits::default(), vec![dummy]).unwrap();
771        let mut pkt = Vec::new();
772        let ack = Message::Ack { seq: 1 };
773        forger
774            .seal(PacketKind::Datagram, &[ack], &[0; 24], &mut pkt)
775            .unwrap();
776        let id = keys(&forger).ids[0];
777        assert!(!keys(&c).ids.contains(&id));
778        let opened = c.open(PacketKind::Datagram, &mut pkt).map(drop);
779        assert_eq!(opened, Err(DecodeError::UnknownKey(id)));
780    }
781}