Skip to main content

coven_format/
sealed_slot.rs

1//! Clear slot routing and bounded slot envelopes (D9). Writes stream separately.
2
3use crate::admin::StoreChange;
4use crate::error::{bound, exact_length, require, Error, Rule};
5use crate::sealed;
6use crate::sealed_key::{CopyBox, KeyCopyPrefix};
7use crate::sequence::{SlotFrame, MAX_BOUNDED_SLOT};
8use crate::wire::{Decoder, Encoder, Wire};
9use coven_crypto::{
10    CircleKey, Signature, StoreKey, KEY_BOX_HEAD_LEN, KEY_BOX_TAG_LEN,
11    SEALED_OBJECT_CHUNK_OVERHEAD, SIGNATURE_LEN,
12};
13use coven_foundation::id_source::KeyId;
14
15/// Where a key box sits in its slot's clear prefix, which its context binds
16/// (D9, D11).
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum BoxPlace {
19    /// The one box of a key-copy slot.
20    KeyCopy,
21    /// The box at this index of an admission's copies.
22    Admission(u32),
23}
24
25/// Slot kind and the routing which exists only for key-copy slots and
26/// admissions.
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub enum SlotKind {
29    /// One admin operation.
30    Admin,
31    /// One device's queued inner writes.
32    Writes,
33    /// A recipient's anonymous box.
34    KeyCopy(CopyBox),
35    /// An immutable snapshot's announcement.
36    Announcement,
37    /// The entry admitting a member and registering their first device, with
38    /// that member's boxes, all to one recipient and sealing key, in
39    /// increasing `(audience, key)` order (ยง13.2).
40    Admission(Vec<CopyBox>),
41}
42impl SlotKind {
43    /// The D9 kind byte.
44    pub fn tag(&self) -> u8 {
45        match self {
46            Self::Admin => 0,
47            Self::Writes => 1,
48            Self::KeyCopy(_) => 2,
49            Self::Announcement => 3,
50            Self::Admission(_) => 4,
51        }
52    }
53    /// The key boxes the clear prefix carries, each with its place.
54    pub fn boxes(&self) -> impl Iterator<Item = (BoxPlace, &CopyBox)> {
55        let (single, many) = match self {
56            Self::KeyCopy(copy) => (Some(copy), &[][..]),
57            Self::Admission(copies) => (None, &copies[..]),
58            Self::Admin | Self::Writes | Self::Announcement => (None, &[][..]),
59        };
60        single.map(|copy| (BoxPlace::KeyCopy, copy)).into_iter().chain(
61            many.iter().enumerate().map(|(index, copy)| (BoxPlace::Admission(index as u32), copy)),
62        )
63    }
64    /// Check the opened second frame against the clear kind. Only an
65    /// admission holds an admit-member or recover-member change, and it
66    /// holds nothing else (D6).
67    pub fn check_frame(&self, frame: &SlotFrame) -> Result<(), Error> {
68        let admits = matches!(
69            frame,
70            SlotFrame::Admin(StoreChange::AdmitMember { .. } | StoreChange::RecoverMember { .. })
71        );
72        require(
73            match (self, frame) {
74                (Self::Admin, SlotFrame::Admin(_)) => !admits,
75                (Self::Admission(_), SlotFrame::Admin(_)) => admits,
76                (Self::KeyCopy(_), SlotFrame::KeyCopy)
77                | (Self::Announcement, SlotFrame::Announcement(_)) => true,
78                _ => false,
79            },
80            "slot kind",
81            Rule::Kind,
82        )
83    }
84}
85
86/// An admission's boxes: at least one, all to one recipient and sealing key,
87/// in strictly increasing `(audience, key)` order (D9).
88fn check_copies(copies: &[CopyBox]) -> Result<(), Error> {
89    require(!copies.is_empty(), "admission copies", Rule::Required)?;
90    let first = &copies[0];
91    require(
92        copies.iter().all(|copy| {
93            copy.routing.member == first.routing.member
94                && copy.routing.sealing == first.routing.sealing
95        }),
96        "admission copy recipient",
97        Rule::Recipient,
98    )?;
99    require(
100        copies.windows(2).all(|pair| {
101            (&pair[0].routing.audience, pair[0].routing.key)
102                < (&pair[1].routing.audience, pair[1].routing.key)
103        }),
104        "admission copy order",
105        Rule::Order,
106    )
107}
108
109/// The complete clear routing, without any device, claim number or part audience.
110#[derive(Clone, Debug, PartialEq, Eq)]
111pub struct SlotObjectPrefix {
112    /// The recorded format of this claim attempt.
113    pub format: crate::FormatVersion,
114    /// The store key sealing the body.
115    pub key: KeyId,
116    /// Kind and any key boxes.
117    pub kind: SlotKind,
118}
119impl SlotObjectPrefix {
120    /// Longest prefix of any slot but an admission, including a circle key
121    /// copy. An admission's prefix is bounded only by its slot's 4 MiB.
122    pub const MAX_LENGTH: usize = Self::FIXED_LEN
123        + KeyCopyPrefix::CIRCLE_LEN
124        + KEY_BOX_HEAD_LEN
125        + 4
126        + CircleKey::ENCODED_LEN
127        + KEY_BOX_TAG_LEN;
128    /// Kind, version, slot kind, key and the copy flag, before any box.
129    const FIXED_LEN: usize = 1 + 2 + 1 + 16 + 1;
130    /// Encode the exact associated-data prefix, including any anonymous box.
131    pub fn encode(&self) -> Result<Vec<u8>, Error> {
132        let mut out = self.start()?;
133        match &self.kind {
134            SlotKind::KeyCopy(copy) => {
135                1u8.put(&mut out)?;
136                copy.put(&mut out)?;
137            }
138            SlotKind::Admission(copies) => {
139                check_copies(copies)?;
140                copies.put(&mut out)?;
141            }
142            SlotKind::Admin | SlotKind::Writes | SlotKind::Announcement => 0u8.put(&mut out)?,
143        }
144        Ok(out.bytes)
145    }
146    fn start(&self) -> Result<Encoder, Error> {
147        let mut out = Encoder::new();
148        out.bytes(&self.format.prefix(crate::kinds::SEQUENCE_SLOT))?;
149        self.kind.tag().put(&mut out)?;
150        self.key.put(&mut out)?;
151        Ok(out)
152    }
153    /// Box context through the recipient's sealing key, before generating its
154    /// ephemeral key and ciphertext: the slot's fixed fields, the box's place
155    /// and its routing. This context binds the slot path in crypto.
156    pub fn copy_context(
157        format: crate::FormatVersion,
158        key: KeyId,
159        place: BoxPlace,
160        routing: &KeyCopyPrefix,
161    ) -> Result<Vec<u8>, Error> {
162        let mut out = Encoder::new();
163        out.bytes(&format.prefix(crate::kinds::SEQUENCE_SLOT))?;
164        match place {
165            BoxPlace::KeyCopy => {
166                2u8.put(&mut out)?;
167                key.put(&mut out)?;
168                1u8.put(&mut out)?;
169            }
170            BoxPlace::Admission(index) => {
171                4u8.put(&mut out)?;
172                key.put(&mut out)?;
173                index.put(&mut out)?;
174            }
175        }
176        routing.put(&mut out)?;
177        Ok(out.bytes)
178    }
179    /// Whether the prefix `bytes` begins is an admission's, whose length only
180    /// its whole bounded slot bounds. It reads the first four bytes.
181    pub fn admission(bytes: &[u8]) -> Result<bool, Error> {
182        sealed::prefix(bytes, crate::kinds::SEQUENCE_SLOT)?;
183        Ok(*bytes.get(3).ok_or(Error::Truncated)? == 4)
184    }
185    /// Bound the clear prefix before reading its boxes. No length supplied by
186    /// a peer can enlarge the fixed store/circle box sizes.
187    pub fn length(bytes: &[u8]) -> Result<usize, Error> {
188        sealed::prefix(bytes, crate::kinds::SEQUENCE_SLOT)?;
189        let kind = *bytes.get(3).ok_or(Error::Truncated)?;
190        require(kind <= 4, "slot kind", Rule::Kind)?;
191        if kind == 4 {
192            let start = Self::FIXED_LEN - 1;
193            let count = bytes.get(start..start + 4).ok_or(Error::Truncated)?;
194            let count = u32::from_be_bytes(count.try_into().expect("four bytes")) as usize;
195            require(count > 0, "admission copies", Rule::Required)?;
196            bound(count, crate::wire::MAX_ITEMS, "admission copies")?;
197            let mut end = start + 4;
198            for _ in 0..count {
199                end = box_end(bytes, end)?;
200            }
201            return Ok(end);
202        }
203        let copy = *bytes.get(Self::FIXED_LEN - 1).ok_or(Error::Truncated)?;
204        require(copy == u8::from(kind == 2), "slot copy routing", Rule::Kind)?;
205        if kind != 2 {
206            return Ok(Self::FIXED_LEN);
207        }
208        box_end(bytes, Self::FIXED_LEN)
209    }
210    /// Decode exactly the prefix, without authenticating routing.
211    pub fn decode(bytes: &[u8]) -> Result<Self, Error> {
212        exact_length(bytes.len(), Self::length(bytes)?)?;
213        let mut input = Decoder::new(&bytes[4..])?;
214        let key = KeyId::get(&mut input)?;
215        let kind = match bytes[3] {
216            4 => {
217                let copies = Vec::<CopyBox>::get(&mut input)?;
218                check_copies(&copies)?;
219                SlotKind::Admission(copies)
220            }
221            tag => {
222                let copy = Option::<CopyBox>::get(&mut input)?;
223                match tag {
224                    0 => SlotKind::Admin,
225                    1 => SlotKind::Writes,
226                    2 => SlotKind::KeyCopy(copy.expect("copy tag checked")),
227                    3 => SlotKind::Announcement,
228                    _ => unreachable!("kind checked"),
229                }
230            }
231        };
232        input.finish()?;
233        Ok(Self { format: crate::FormatVersion::decode(bytes)?, key, kind })
234    }
235    /// Whether the complete bounded slot with this prefix and `plaintext`
236    /// bytes of frames stays within its 4-MiB limit, signature included (D2).
237    /// An operation checks it before it claims anything.
238    pub fn fits(&self, plaintext: usize) -> Result<bool, Error> {
239        Ok(bound(
240            Self::bounded_size(self.encode()?.len(), plaintext + SEALED_OBJECT_CHUNK_OVERHEAD),
241            MAX_BOUNDED_SLOT,
242            "slot object",
243        )
244        .is_ok())
245    }
246    /// The complete bounded slot's size: its prefix, the chunk's length
247    /// field and bytes, and the signature.
248    fn bounded_size(prefix: usize, chunk: usize) -> usize {
249        prefix + 4 + chunk + SIGNATURE_LEN
250    }
251    /// Frame the sole sealed chunk of an admin, admission, copy or
252    /// announcement slot.
253    /// Checks the complete 4-MiB limit including the eventual signature.
254    pub fn encode_chunk(&self, chunk: &[u8]) -> Result<Vec<u8>, Error> {
255        require(!matches!(self.kind, SlotKind::Writes), "bounded slot kind", Rule::Kind)?;
256        let mut bytes = self.encode()?;
257        let length =
258            chunk.len().checked_sub(SEALED_OBJECT_CHUNK_OVERHEAD).ok_or(Error::Truncated)?;
259        require(length >= 2 * crate::FRAME_PREFIX_LEN, "slot frames", Rule::Chunk)?;
260        bound(Self::bounded_size(bytes.len(), chunk.len()), MAX_BOUNDED_SLOT, "slot object")?;
261        bytes.extend_from_slice(&sealed::encode_chunk(chunk)?);
262        Ok(bytes)
263    }
264}
265
266/// Where the box that starts at `start` ends: its routing, whose audience
267/// fixes the key material's length, the ephemeral key and nonce, the declared
268/// ciphertext length, the ciphertext and the tag.
269fn box_end(bytes: &[u8], start: usize) -> Result<usize, Error> {
270    let (routing, ciphertext) = match *bytes.get(start).ok_or(Error::Truncated)? {
271        0 => (KeyCopyPrefix::STORE_LEN, StoreKey::ENCODED_LEN),
272        1 => (KeyCopyPrefix::CIRCLE_LEN, CircleKey::ENCODED_LEN),
273        tag => return Err(Error::UnknownTag { field: "audience", tag }),
274    };
275    let offset = start + routing + KEY_BOX_HEAD_LEN;
276    let declared = bytes.get(offset..offset + 4).ok_or(Error::Truncated)?;
277    require(
278        u32::from_be_bytes(declared.try_into().expect("four bytes")) as usize == ciphertext,
279        "key box ciphertext",
280        Rule::StreamLength,
281    )?;
282    Ok(offset + 4 + ciphertext + KEY_BOX_TAG_LEN)
283}
284
285/// One complete bounded slot envelope; cryptographic verification is separate.
286#[derive(Debug)]
287pub struct BoundedSlot<'a> {
288    /// Clear routing covered by both chunk authentication and final signature.
289    pub prefix: SlotObjectPrefix,
290    /// Stored nonce, ciphertext and tag.
291    pub chunk: &'a [u8],
292    /// Signature over every preceding byte, bound to the slot path.
293    pub signature: Signature,
294}
295impl<'a> BoundedSlot<'a> {
296    /// Decode without allocating the body.
297    pub fn decode(bytes: &'a [u8]) -> Result<Self, Error> {
298        bound(bytes.len(), MAX_BOUNDED_SLOT, "slot object")?;
299        let prefix_length = SlotObjectPrefix::length(bytes)?;
300        let prefix = SlotObjectPrefix::decode(bytes.get(..prefix_length).ok_or(Error::Truncated)?)?;
301        require(!matches!(prefix.kind, SlotKind::Writes), "bounded slot kind", Rule::Kind)?;
302        let body = &bytes[prefix_length..];
303        let length = sealed::chunk_length(body, MAX_BOUNDED_SLOT)?;
304        let chunk = body.get(4..length).ok_or(Error::Truncated)?;
305        require(
306            chunk.len() >= SEALED_OBJECT_CHUNK_OVERHEAD + 2 * crate::FRAME_PREFIX_LEN,
307            "slot frames",
308            Rule::Chunk,
309        )?;
310        let signature = sealed::signature(body.get(length..).ok_or(Error::Truncated)?)?;
311        Ok(Self { prefix, chunk, signature })
312    }
313    /// Reproduce the original sealed envelope.
314    pub fn encode(&self) -> Result<Vec<u8>, Error> {
315        let mut bytes = self.prefix.encode_chunk(self.chunk)?;
316        bytes.extend_from_slice(self.signature.as_bytes());
317        Ok(bytes)
318    }
319}
320
321#[cfg(test)]
322#[path = "sealed_slot_tests.rs"]
323mod tests;