Skip to main content

coven_format/
sealed_write.rs

1//! Piecewise sealed write layout. Crypto supplies and authenticates the bytes.
2
3use crate::chunks::{chunk_count, chunk_length};
4use crate::error::{bound, exact_length, require, Error, Rule};
5use crate::sealed;
6use crate::wire::{Decoder, Encoder, Wire, MAX_ITEMS, MAX_OBJECT};
7use coven_crypto::{Signature, SEALED_OBJECT_CHUNK_OVERHEAD, SIGNATURE_LEN};
8use coven_foundation::id_source::KeyId;
9
10/// The exact stored size, independent of key identities, nonces and signature
11/// contents. Header and part lengths come from the validated plaintext codec.
12pub fn sealed_length(header_length: usize, part_lengths: &[u64]) -> Result<u64, Error> {
13    validate_lengths(header_length, part_lengths)?;
14    let overhead = crate::STORED_CHUNK_OVERHEAD as u64;
15    let mut length = WriteObjectPrefix::encoded_length(part_lengths.len())? as u64
16        + header_length as u64
17        + overhead
18        + WriteObjectPrefix::tail_length(part_lengths.len()) as u64
19        + SIGNATURE_LEN as u64;
20    for part in part_lengths {
21        length = chunk_count(*part)
22            .checked_mul(overhead)
23            .and_then(|n| n.checked_add(*part))
24            .and_then(|n| n.checked_add(length))
25            .ok_or(Error::Invalid { field: "sealed write length", rule: Rule::StreamLength })?;
26    }
27    Ok(length)
28}
29
30fn validate_lengths(header_length: usize, part_lengths: &[u64]) -> Result<(), Error> {
31    bound(header_length, MAX_OBJECT, "write header")?;
32    require(header_length >= crate::FRAME_PREFIX_LEN, "write header", Rule::StreamLength)?;
33    bound(part_lengths.len(), MAX_ITEMS, "write parts")?;
34    for length in part_lengths {
35        require(
36            *length >= crate::FRAME_PREFIX_LEN as u64,
37            "write part length",
38            Rule::StreamLength,
39        )?;
40    }
41    Ok(())
42}
43
44/// The part roots and header signature after a write's sealed header (D9).
45#[derive(Clone, Debug, PartialEq, Eq)]
46pub struct HeaderTail {
47    /// Each part's keyed authorship root, in header order.
48    pub roots: Vec<[u8; 32]>,
49    /// The author's signature over the prefix, sealed header and roots.
50    pub signature: Signature,
51}
52
53/// Cleartext key identities, authenticated by the object's final signature.
54#[derive(Clone, Debug, PartialEq, Eq)]
55pub struct WriteObjectPrefix {
56    /// The format fixed with these keys before the first upload attempt.
57    pub format: crate::FormatVersion,
58    /// The store key sealing section 0, the header frame.
59    pub store_key: KeyId,
60    /// Keys of the audience parts in header order; section i + 1 uses entry i.
61    pub part_keys: Vec<KeyId>,
62}
63impl WriteObjectPrefix {
64    /// Greatest permitted end of the clear prefix, sealed header, part roots
65    /// and header signature (D1/D2, D9).
66    pub const MAX_HEADER_END: usize = 23
67        + 16 * MAX_ITEMS
68        + crate::STORED_CHUNK_OVERHEAD
69        + MAX_OBJECT
70        + 32 * MAX_ITEMS
71        + SIGNATURE_LEN;
72
73    /// Exact end of the header and its tail once its length field is
74    /// available. A truncated prefix requires the caller's second bounded
75    /// read to use MAX_HEADER_END.
76    pub fn header_end(bytes: &[u8]) -> Result<usize, Error> {
77        let prefix = Self::length(bytes)?;
78        let parts = (prefix - 23) / 16;
79        let header = bytes.get(prefix..).ok_or(Error::Truncated)?;
80        Ok(prefix + Self::header_chunk_length(header)? + Self::tail_length(parts))
81    }
82
83    /// Bytes of the part roots and header signature for this many parts.
84    pub fn tail_length(parts: usize) -> usize {
85        32 * parts + SIGNATURE_LEN
86    }
87
88    /// Read exactly the part roots and header signature that follow the
89    /// sealed header chunk.
90    pub fn decode_tail(&self, bytes: &[u8]) -> Result<HeaderTail, Error> {
91        exact_length(bytes.len(), Self::tail_length(self.part_keys.len()))?;
92        let (roots, signature) = bytes.split_at(32 * self.part_keys.len());
93        Ok(HeaderTail {
94            roots: roots.chunks_exact(32).map(|root| root.try_into().expect("32 bytes")).collect(),
95            signature: sealed::signature(signature)?,
96        })
97    }
98
99    /// Exact prefix size for this many parts, before their key ids are selected.
100    pub fn encoded_length(parts: usize) -> Result<usize, Error> {
101        bound(parts, MAX_ITEMS, "write section keys")?;
102        Ok(23 + 16 * parts)
103    }
104
105    /// Encode kind, version, header key and the counted list of part keys.
106    pub fn encode(&self) -> Result<Vec<u8>, Error> {
107        let crate::FormatVersion::V1 = self.format;
108        let mut out = Encoder::new();
109        out.bytes(&self.format.prefix(crate::kinds::INNER_WRITE))?;
110        self.store_key.put(&mut out)?;
111        self.part_keys.put(&mut out)?;
112        Ok(out.bytes)
113    }
114
115    /// Determine the complete prefix size from its first 23 bytes, before
116    /// allocating the counted key list. This is not a plaintext frame prefix.
117    pub fn length(bytes: &[u8]) -> Result<usize, Error> {
118        sealed::prefix(bytes, crate::kinds::INNER_WRITE)?;
119        let count = bytes.get(19..23).ok_or(Error::Truncated)?;
120        let count = u32::from_be_bytes(count.try_into().expect("four bytes")) as usize;
121        Self::encoded_length(count)
122    }
123
124    /// Decode exactly the complete prefix, refusing trailing bytes.
125    pub fn decode(bytes: &[u8]) -> Result<Self, Error> {
126        exact_length(bytes.len(), Self::length(bytes)?)?;
127        let mut input = Decoder::new(&bytes[3..])?;
128        let result = Self {
129            format: crate::FormatVersion::decode(bytes)?,
130            store_key: KeyId::get(&mut input)?,
131            part_keys: Vec::get(&mut input)?,
132        };
133        input.finish()?;
134        Ok(result)
135    }
136
137    /// Bound the following header's length prefix before fetching or allocating.
138    pub fn header_chunk_length(prefix: &[u8]) -> Result<usize, Error> {
139        let length = sealed::chunk_length(prefix, MAX_OBJECT)?;
140        require(
141            length >= crate::FRAME_PREFIX_LEN + crate::STORED_CHUNK_OVERHEAD,
142            "sealed header frame",
143            Rule::Chunk,
144        )?;
145        Ok(length)
146    }
147
148    /// Read the length-prefixed sealed header. Open it with `store_key`, the
149    /// object's path, section 0 and index 0, then call [`Self::opened_header`].
150    pub fn header_chunk(piece: &[u8]) -> Result<&[u8], Error> {
151        Self::header_chunk_length(piece)?;
152        sealed::chunk(piece, MAX_OBJECT)
153    }
154
155    /// Begin part chunks after the caller authenticates and decodes the header
156    /// and reads its tail. `part_lengths` must come from that header, in part
157    /// order. This layer treats frame bytes as opaque; the header signature and
158    /// the final signature still must be checked.
159    pub fn opened_header(
160        self,
161        bytes: &[u8],
162        part_lengths: Vec<u64>,
163    ) -> Result<WriteObjectLayout, Error> {
164        let mut layout = WriteObjectLayout::new(self, bytes, part_lengths)?;
165        layout.advance();
166        layout.position = layout.after_tail();
167        Ok(layout)
168    }
169}
170
171/// Coordinates, selected key and exact plaintext length of the next chunk.
172#[derive(Clone, Copy, Debug, PartialEq, Eq)]
173pub struct ObjectChunk {
174    /// The audience key to select by id.
175    pub key: KeyId,
176    /// Header 0, or part i + 1.
177    pub section: u64,
178    /// Zero-based position within that section.
179    pub index: u64,
180    /// Exact plaintext size before adding the stored nonce and tag.
181    pub plaintext_length: usize,
182}
183
184enum Position {
185    Chunk { section: usize, index: u64 },
186    Tail,
187    Signature,
188    End,
189}
190
191/// Sealed layout cursor, retaining header metadata rather than object bytes.
192/// Writers start at the header. Readers start after opening the header with
193/// [`WriteObjectPrefix::opened_header`]. Hash each encoded piece, including the
194/// prefix and length fields, before the final signature.
195pub struct WriteObjectLayout {
196    prefix: WriteObjectPrefix,
197    part_lengths: Vec<u64>,
198    header_length: usize,
199    position: Position,
200}
201impl WriteObjectLayout {
202    /// Begin producing a write from its header bytes and declared part lengths.
203    /// Exactly one key is required for every part; lengths come from the header's
204    /// plaintext codec, which remains responsible for validating its fields.
205    pub fn new(
206        prefix: WriteObjectPrefix,
207        header: &[u8],
208        part_lengths: Vec<u64>,
209    ) -> Result<Self, Error> {
210        let header_length = header.len();
211        validate_lengths(header_length, &part_lengths)?;
212        require(
213            prefix.part_keys.len() == part_lengths.len(),
214            "write section key count",
215            Rule::StreamLength,
216        )?;
217        Ok(Self {
218            prefix,
219            part_lengths,
220            header_length,
221            position: Position::Chunk { section: 0, index: 0 },
222        })
223    }
224
225    /// Encode the cleartext prefix before supplying sealed chunks.
226    pub fn prefix(&self) -> Result<Vec<u8>, Error> {
227        self.prefix.encode()
228    }
229
230    /// Coordinates for the next sealed chunk, or `None` when only the signature remains.
231    pub fn next_chunk(&self) -> Option<ObjectChunk> {
232        let Position::Chunk { section, index } = self.position else {
233            return None;
234        };
235        if section == 0 {
236            Some(ObjectChunk {
237                key: self.prefix.store_key,
238                section: 0,
239                index: 0,
240                plaintext_length: self.header_length,
241            })
242        } else {
243            Some(ObjectChunk {
244                key: self.prefix.part_keys[section - 1],
245                section: section as u64,
246                index,
247                plaintext_length: chunk_length(self.part_lengths[section - 1], index),
248            })
249        }
250    }
251
252    /// Validate a chunk length prefix against the next exact boundary before allocating.
253    pub fn chunk_length(&self, prefix: &[u8]) -> Result<usize, Error> {
254        let chunk = self.next_chunk().ok_or(Error::TrailingBytes)?;
255        let length = sealed::chunk_length(prefix, chunk.plaintext_length)?;
256        require(
257            length == crate::STORED_CHUNK_OVERHEAD + chunk.plaintext_length,
258            "sealed write chunk length",
259            Rule::Chunk,
260        )?;
261        Ok(length)
262    }
263
264    /// Write the next length-prefixed sealed chunk, advancing only on success.
265    pub fn encode_chunk(&mut self, bytes: &[u8]) -> Result<Vec<u8>, Error> {
266        let chunk = self.next_chunk().ok_or(Error::TrailingBytes)?;
267        require(
268            bytes.len() == SEALED_OBJECT_CHUNK_OVERHEAD + chunk.plaintext_length,
269            "sealed write chunk length",
270            Rule::Chunk,
271        )?;
272        let encoded = sealed::encode_chunk(bytes)?;
273        self.advance();
274        Ok(encoded)
275    }
276
277    /// Read the next complete piece without opening or copying it. A reader
278    /// lacking this part's key can skip the returned bytes while hashing them.
279    pub fn decode_chunk<'a>(&mut self, piece: &'a [u8]) -> Result<(ObjectChunk, &'a [u8]), Error> {
280        let chunk = self.next_chunk().ok_or(Error::TrailingBytes)?;
281        exact_length(piece.len(), self.chunk_length(piece)?)?;
282        self.advance();
283        Ok((chunk, &piece[4..]))
284    }
285
286    /// Write the part roots and header signature, only right after the header.
287    pub fn tail(&mut self, tail: &HeaderTail) -> Result<Vec<u8>, Error> {
288        require(
289            matches!(self.position, Position::Tail) && tail.roots.len() == self.part_lengths.len(),
290            "write header tail",
291            Rule::StreamLength,
292        )?;
293        let mut bytes = Vec::with_capacity(WriteObjectPrefix::tail_length(tail.roots.len()));
294        for root in &tail.roots {
295            bytes.extend_from_slice(root);
296        }
297        bytes.extend_from_slice(tail.signature.as_bytes());
298        self.position = self.after_tail();
299        Ok(bytes)
300    }
301
302    fn after_tail(&self) -> Position {
303        if self.part_lengths.is_empty() {
304            Position::Signature
305        } else {
306            Position::Chunk { section: 1, index: 0 }
307        }
308    }
309
310    /// Write a signature only after every declared chunk.
311    pub fn signature(&mut self, signature: &Signature) -> Result<[u8; SIGNATURE_LEN], Error> {
312        require(
313            matches!(self.position, Position::Signature),
314            "write signature position",
315            Rule::StreamLength,
316        )?;
317        self.position = Position::End;
318        Ok(*signature.as_bytes())
319    }
320
321    /// Read the final 64-byte signature. Crypto verifies it against the author,
322    /// path and accumulated digest; this method only checks layout.
323    pub fn read_signature(&mut self, bytes: &[u8]) -> Result<Signature, Error> {
324        let signature = sealed::signature(bytes)?;
325        self.signature(&signature)?;
326        Ok(signature)
327    }
328
329    /// Check EOF after the signature. No additional bytes may follow it.
330    pub fn finish(&self, trailing: &[u8]) -> Result<(), Error> {
331        if !trailing.is_empty() {
332            return Err(Error::TrailingBytes);
333        }
334        require(matches!(self.position, Position::End), "write signature", Rule::StreamLength)
335    }
336
337    fn advance(&mut self) {
338        let Position::Chunk { section, index } = self.position else {
339            unreachable!("a chunk was checked");
340        };
341        if section == 0 {
342            self.position = Position::Tail;
343        } else if index + 1 < chunk_count(self.part_lengths[section - 1]) {
344            self.position = Position::Chunk { section, index: index + 1 };
345        } else if section < self.part_lengths.len() {
346            self.position = Position::Chunk { section: section + 1, index: 0 };
347        } else {
348            self.position = Position::Signature;
349        }
350    }
351}
352
353#[cfg(test)]
354#[path = "sealed_write_tests.rs"]
355mod tests;