Skip to main content

fast_floe/
parameters.rs

1use core::iter::FusedIterator;
2use core::ops::Range;
3
4use crate::{Error, LengthRequirement, Result};
5
6pub(crate) const AEAD_IV_LENGTH: usize = 12;
7pub(crate) const AEAD_TAG_LENGTH: usize = 16;
8pub(crate) const AEAD_MAX_SEGMENTS: u64 = 1 << 40;
9
10pub(crate) const FLOE_IV_LENGTH: usize = 32;
11pub(crate) const ENCODED_PARAMETERS_LENGTH: usize = 10;
12pub(crate) const HEADER_TAG_LENGTH: usize = 32;
13pub(crate) const HEADER_LENGTH: usize =
14    ENCODED_PARAMETERS_LENGTH + FLOE_IV_LENGTH + HEADER_TAG_LENGTH;
15
16const _: () = assert!(HEADER_LENGTH == 74, "unexpected size of HEADER");
17
18const _: () = assert!(
19    usize::BITS == 32 || usize::BITS == 64,
20    "fast-floe supports only 32-bit and 64-bit targets"
21);
22
23/// Segment length prefix size in bytes.
24pub const SEGMENT_PREFIX_LENGTH: usize = 4;
25
26/// Offset at which plaintext or ciphertext payload bytes begin in a segment.
27///
28/// Safe APIs encapsulate this detail in [`crate::SegmentBuffer`].
29pub const SEGMENT_PAYLOAD_OFFSET: usize = SEGMENT_PREFIX_LENGTH + AEAD_IV_LENGTH;
30
31pub(crate) const SEGMENT_OVERHEAD: usize = SEGMENT_PAYLOAD_OFFSET + AEAD_TAG_LENGTH;
32
33const ROTATION_BITS: u8 = 20;
34const ROTATION_MASK: u64 = !((1_u64 << ROTATION_BITS) - 1);
35const FLOE_IV_LENGTH_U32: u32 = 32;
36const _: () = assert!(length_u32_to_usize(FLOE_IV_LENGTH_U32) == FLOE_IV_LENGTH);
37
38pub(crate) const SEGMENT_OVERHEAD_U32: u32 = 32;
39const _: () = assert!(length_u32_to_usize(SEGMENT_OVERHEAD_U32) == SEGMENT_OVERHEAD);
40
41/// Converts a wire-format length to the crate's in-memory length type.
42#[inline]
43pub(crate) const fn length_u32_to_usize(value: u32) -> usize {
44    value as usize
45}
46
47/// Converts an in-memory length to the crate's message-arithmetic type.
48/// `std` has no `From<usize> for u64`, so this documents the assumption once.
49#[inline]
50pub(crate) const fn length_usize_to_u64(value: usize) -> u64 {
51    value as u64
52}
53
54/// Clamps a message-arithmetic length into `usize` for error reporting.
55#[inline]
56pub(crate) fn length_u64_to_usize_saturating(value: u64) -> usize {
57    usize::try_from(value).unwrap_or(usize::MAX)
58}
59
60pub(crate) const HEADER_LENGTH_U64: u64 = length_usize_to_u64(HEADER_LENGTH);
61pub(crate) const SEGMENT_OVERHEAD_U64: u64 = length_usize_to_u64(SEGMENT_OVERHEAD);
62
63/// The segment length is the only varying parameter in the current
64/// specification; AES-256-GCM, HKDF-Expand-SHA-384, and the 32-byte FLOE
65/// IV are fixed.
66///
67/// Use [`Parameters::with_segment_length`] to construct a [`Parameters`] instance with
68/// your desired segment length, or use one of the pre-made [`Parameters`] constants
69/// like [`Parameters::SEGMENT_4_KIB`] or [`Parameters::SEGMENT_1_MIB`] if convenient.
70///
71#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub struct Parameters {
73    ciphertext_segment_length: u32,
74    #[cfg(test)]
75    rotation_mask: u64,
76}
77
78/// Whether a segment is an internal or final segment.
79#[derive(Clone, Copy, Debug, Eq, PartialEq)]
80pub enum SegmentKind {
81    /// A full-sized segment followed by another segment.
82    NonFinal,
83    /// The authenticated final segment of a message.
84    Final,
85}
86
87impl SegmentKind {
88    /// Returns whether this identifies the message's final segment.
89    #[must_use]
90    pub const fn is_final(self) -> bool {
91        matches!(self, Self::Final)
92    }
93
94    pub(crate) const fn indicator(self) -> u8 {
95        match self {
96            Self::NonFinal => 0,
97            Self::Final => 1,
98        }
99    }
100}
101
102/// Complete length and segment layout for one FLOE message.
103///
104/// Construct this with [`Parameters::plaintext_layout`] when the plaintext
105/// length is known, or [`Parameters::ciphertext_layout`] when the complete
106/// ciphertext length is known.
107///
108/// Each [`SegmentLayout`] supplies the offsets, lengths, position,
109/// and [`SegmentKind`] needed by the random-access encryption and
110/// decryption APIs.
111#[derive(Clone, Copy, Debug, Eq, PartialEq)]
112pub struct MessageLayout {
113    parameters: Parameters,
114    plaintext_length: u64,
115    ciphertext_length: u64,
116    segment_count: u64,
117}
118
119impl MessageLayout {
120    /// Returns the layout of this message's final segment.
121    #[must_use]
122    #[allow(clippy::missing_panics_doc)] // a valid layout always contains a final segment
123    pub fn final_segment(self) -> SegmentLayout {
124        self.segment_for_position(self.segment_count - 1)
125            .expect("every FLOE message layout contains one final segment")
126    }
127
128    /// Returns the parameter set used by this layout.
129    #[must_use]
130    pub const fn parameters(self) -> Parameters {
131        self.parameters
132    }
133
134    /// Returns the complete plaintext length.
135    #[must_use]
136    pub const fn plaintext_length(self) -> u64 {
137        self.plaintext_length
138    }
139
140    /// Returns the complete ciphertext length, including the FLOE header.
141    #[must_use]
142    pub const fn ciphertext_length(self) -> u64 {
143        self.ciphertext_length
144    }
145
146    /// Returns the number of segments, including exactly one final segment.
147    #[must_use]
148    pub const fn segment_count(self) -> u64 {
149        self.segment_count
150    }
151
152    /// Iterates over every segment in position order.
153    ///
154    /// See [`Self::segment_for_position`] when accessing an individual
155    /// segment by position.
156    #[must_use]
157    pub fn segments(self) -> Segments {
158        Segments {
159            layout: self,
160            positions: 0..self.segment_count,
161        }
162    }
163
164    /// Returns the [`SegmentLayout`] of `position`, or `None` if it is outside this message.
165    ///
166    /// The returned values can be passed directly to the corresponding
167    /// random-access segment operation.
168    #[must_use]
169    pub fn segment_for_position(self, position: u64) -> Option<SegmentLayout> {
170        if position >= self.segment_count {
171            return None;
172        }
173
174        let plaintext_segment_length = self.parameters.plaintext_segment_length();
175        let plaintext_segment_length_u64 =
176            u64::from(self.parameters.plaintext_segment_length_u32());
177        let ciphertext_segment_length = self.parameters.ciphertext_segment_length();
178        let ciphertext_segment_length_u64 =
179            u64::from(self.parameters.ciphertext_segment_length_u32());
180        let plaintext_offset = position * plaintext_segment_length_u64;
181        let kind = if position + 1 == self.segment_count {
182            SegmentKind::Final
183        } else {
184            SegmentKind::NonFinal
185        };
186
187        let (plaintext_length, ciphertext_length) = match kind {
188            SegmentKind::NonFinal => (plaintext_segment_length, ciphertext_segment_length),
189            SegmentKind::Final => {
190                let plaintext_length =
191                    usize::try_from(self.plaintext_length - plaintext_offset).ok()?;
192                (plaintext_length, SEGMENT_OVERHEAD + plaintext_length)
193            }
194        };
195
196        let ciphertext_offset = HEADER_LENGTH_U64 + position * ciphertext_segment_length_u64;
197
198        Some(SegmentLayout {
199            parameters: self.parameters,
200            position,
201            plaintext_offset,
202            plaintext_length,
203            ciphertext_offset,
204            ciphertext_length,
205            kind,
206        })
207    }
208
209    /// Returns the position of the segment containing `offset` in the
210    /// complete plaintext. Offsets at or beyond the plaintext length map to
211    /// positions outside this layout.
212    pub(crate) fn position_for_plaintext_offset(self, offset: u64) -> u64 {
213        offset / u64::from(self.parameters.plaintext_segment_length_u32())
214    }
215}
216
217impl IntoIterator for MessageLayout {
218    type Item = SegmentLayout;
219    type IntoIter = Segments;
220
221    fn into_iter(self) -> Self::IntoIter {
222        self.segments()
223    }
224}
225
226/// Iterator over the segments in a [`MessageLayout`].
227///
228/// Obtain this with [`MessageLayout::segments`] or by iterating over a
229/// [`MessageLayout`] directly.
230#[derive(Clone, Debug)]
231pub struct Segments {
232    layout: MessageLayout,
233    positions: Range<u64>,
234}
235
236impl Segments {
237    fn segment_at(&self, position: u64) -> SegmentLayout {
238        self.layout
239            .segment_for_position(position)
240            .expect("a layout iterator only produces valid segment positions")
241    }
242}
243
244impl Iterator for Segments {
245    type Item = SegmentLayout;
246
247    fn next(&mut self) -> Option<Self::Item> {
248        self.positions
249            .next()
250            .map(|position| self.segment_at(position))
251    }
252
253    fn size_hint(&self) -> (usize, Option<usize>) {
254        self.positions.size_hint()
255    }
256}
257
258impl DoubleEndedIterator for Segments {
259    fn next_back(&mut self) -> Option<Self::Item> {
260        self.positions
261            .next_back()
262            .map(|position| self.segment_at(position))
263    }
264}
265
266impl FusedIterator for Segments {}
267
268/// Offsets and lengths for one segment in a [`MessageLayout`].
269#[derive(Clone, Copy, Debug, Eq, PartialEq)]
270pub struct SegmentLayout {
271    parameters: Parameters,
272    position: u64,
273    plaintext_offset: u64,
274    plaintext_length: usize,
275    ciphertext_offset: u64,
276    ciphertext_length: usize,
277    kind: SegmentKind,
278}
279
280impl SegmentLayout {
281    /// Returns this segment's zero-based position.
282    #[must_use]
283    pub const fn position(self) -> u64 {
284        self.position
285    }
286
287    /// Returns this segment's byte offset in the complete plaintext.
288    #[must_use]
289    pub const fn plaintext_offset(self) -> u64 {
290        self.plaintext_offset
291    }
292
293    /// Returns this segment's plaintext length.
294    #[must_use]
295    pub const fn plaintext_length(self) -> usize {
296        self.plaintext_length
297    }
298
299    /// Returns this segment's byte offset in the complete ciphertext,
300    /// including the FLOE header.
301    #[must_use]
302    pub const fn ciphertext_offset(self) -> u64 {
303        self.ciphertext_offset
304    }
305
306    /// Returns this segment's ciphertext length.
307    #[must_use]
308    pub const fn ciphertext_length(self) -> usize {
309        self.ciphertext_length
310    }
311
312    /// Returns whether this is the message's final segment.
313    #[must_use]
314    pub const fn is_final(self) -> bool {
315        self.kind.is_final()
316    }
317
318    /// Returns whether this is an internal or final segment.
319    #[must_use]
320    pub const fn kind(self) -> SegmentKind {
321        self.kind
322    }
323
324    pub(crate) const fn parameters(self) -> Parameters {
325        self.parameters
326    }
327}
328
329/// Segment framing information decoded from a FLOE segment prefix.
330///
331/// Construct this with [`Self::decode`]. A streaming
332/// decryptor can use [`Self::ciphertext_length`] to read the remainder of the
333/// segment and [`Self::plaintext_length`] to size an output buffer.
334#[derive(Clone, Copy, Debug, Eq, PartialEq)]
335pub struct SegmentFraming {
336    ciphertext_length: usize,
337    plaintext_length: usize,
338    kind: SegmentKind,
339}
340
341impl SegmentFraming {
342    /// Decodes the **unauthenticated** framing declared by a segment prefix.
343    ///
344    /// This **does not authenticate** the prefix or the rest of the segment.
345    /// You must successfully decrypt the segment before trusting it.
346    ///
347    /// # Errors
348    ///
349    /// Returns [`Error::InvalidCiphertextLength`] when a final prefix encodes
350    /// a length outside the supported range.
351    pub fn decode(parameters: Parameters, prefix: [u8; SEGMENT_PREFIX_LENGTH]) -> Result<Self> {
352        let encoded = u32::from_be_bytes(prefix);
353
354        let (ciphertext_length, kind) = if encoded == u32::MAX {
355            (
356                parameters.ciphertext_segment_length(),
357                SegmentKind::NonFinal,
358            )
359        } else {
360            // Validated in u32 space: the range check must not depend on the
361            // width of usize, because `encoded` is attacker-controlled and
362            // unauthenticated.
363            let maximum = parameters.ciphertext_segment_length_u32();
364            if !(SEGMENT_OVERHEAD_U32..=maximum).contains(&encoded) {
365                return Err(Error::InvalidCiphertextLength {
366                    actual: length_u32_to_usize(encoded),
367                    required: LengthRequirement::Between {
368                        minimum: SEGMENT_OVERHEAD,
369                        maximum: parameters.ciphertext_segment_length(),
370                    },
371                });
372            }
373            (length_u32_to_usize(encoded), SegmentKind::Final)
374        };
375
376        Ok(Self {
377            ciphertext_length,
378            plaintext_length: ciphertext_length - SEGMENT_OVERHEAD,
379            kind,
380        })
381    }
382
383    /// Returns the complete ciphertext segment length.
384    #[must_use]
385    pub const fn ciphertext_length(self) -> usize {
386        self.ciphertext_length
387    }
388
389    /// Returns the segment's plaintext payload length.
390    #[must_use]
391    pub const fn plaintext_length(self) -> usize {
392        self.plaintext_length
393    }
394
395    /// Returns whether the prefix identifies a final segment.
396    #[must_use]
397    pub const fn is_final(self) -> bool {
398        self.kind.is_final()
399    }
400
401    /// Returns whether the prefix identifies an internal or final segment.
402    #[must_use]
403    pub const fn kind(self) -> SegmentKind {
404        self.kind
405    }
406}
407
408impl Parameters {
409    /// Range of valid FLOE segment sizes in bytes. FLOE accepts _any_ segment size
410    /// in this range and is not restricted to powers of 2.
411    pub const VALID_SEGMENT_LENGTHS: Range<u32> = 64..u32::MAX;
412
413    /// FLOE with 64-byte encrypted segments.
414    pub const SEGMENT_64_B: Self = Self::with_segment_length_unchecked(64);
415
416    /// FLOE with 4 KiB encrypted segments.
417    pub const SEGMENT_4_KIB: Self = Self::with_segment_length_unchecked(4 * 1024);
418
419    /// FLOE with 1 MiB encrypted segments.
420    pub const SEGMENT_1_MIB: Self = Self::with_segment_length_unchecked(1024 * 1024);
421
422    /// FLOE with 4 MiB encrypted segments.
423    pub const SEGMENT_4_MIB: Self = Self::with_segment_length_unchecked(4 * 1024 * 1024);
424
425    /// FLOE with 5 MiB encrypted segments.
426    pub const SEGMENT_5_MIB: Self = Self::with_segment_length_unchecked(5 * 1024 * 1024);
427
428    /// FLOE with 8 MiB encrypted segments.
429    pub const SEGMENT_8_MIB: Self = Self::with_segment_length_unchecked(8 * 1024 * 1024);
430
431    /// FLOE with 16 MiB encrypted segments.
432    pub const SEGMENT_16_MIB: Self = Self::with_segment_length_unchecked(16 * 1024 * 1024);
433
434    /// Construct a [`Parameters`] instance with the provided segment length in bytes.
435    /// `segment_len` can be any value in the range [`Parameters::VALID_SEGMENT_LENGTHS`].
436    ///
437    /// # Errors
438    ///
439    /// Returns [`Error::InvalidSegmentLength`] when `segment_len` is outside
440    /// the supported range.
441    pub fn with_segment_length(segment_len: u32) -> Result<Self> {
442        if !Self::VALID_SEGMENT_LENGTHS.contains(&segment_len) {
443            return Err(Error::InvalidSegmentLength {
444                actual: segment_len,
445            });
446        }
447
448        Ok(Self::with_segment_length_unchecked(segment_len))
449    }
450
451    const fn with_segment_length_unchecked(segment_len: u32) -> Self {
452        Self {
453            ciphertext_segment_length: segment_len,
454            #[cfg(test)]
455            rotation_mask: ROTATION_MASK,
456        }
457    }
458
459    /// Skips segment-length validation: the specification's key-rotation KATs
460    /// use 40-byte segments, below [`Self::VALID_SEGMENT_LENGTHS`].
461    #[cfg(test)]
462    pub(crate) fn with_rotation_mask_for_test(segment_len: u32, rotation_mask: u64) -> Self {
463        let mut parameters = Self::with_segment_length_unchecked(segment_len);
464        parameters.rotation_mask = rotation_mask;
465        parameters
466    }
467
468    /// Returns the exact length of every non-final ciphertext segment.
469    #[must_use]
470    #[inline]
471    pub const fn ciphertext_segment_length(self) -> usize {
472        length_u32_to_usize(self.ciphertext_segment_length)
473    }
474
475    pub(crate) const fn ciphertext_segment_length_u32(self) -> u32 {
476        self.ciphertext_segment_length
477    }
478
479    /// Returns the plaintext length of every non-final segment and the maximum
480    /// plaintext length of a final segment.
481    #[must_use]
482    #[inline]
483    pub const fn plaintext_segment_length(self) -> usize {
484        length_u32_to_usize(self.plaintext_segment_length_u32())
485    }
486
487    pub(crate) const fn plaintext_segment_length_u32(self) -> u32 {
488        self.ciphertext_segment_length - SEGMENT_OVERHEAD_U32
489    }
490
491    /// Checks that `actual` can be the length of a ciphertext segment under
492    /// this parameter set: at least the framing overhead and at most one full
493    /// segment.
494    pub(crate) fn validate_ciphertext_segment_length(self, actual: usize) -> Result<()> {
495        let maximum = self.ciphertext_segment_length();
496        if (SEGMENT_OVERHEAD..=maximum).contains(&actual) {
497            Ok(())
498        } else {
499            Err(Error::InvalidCiphertextLength {
500                actual,
501                required: LengthRequirement::Between {
502                    minimum: SEGMENT_OVERHEAD,
503                    maximum,
504                },
505            })
506        }
507    }
508
509    /// Calculates the complete FLOE layout for `plaintext_length`.
510    ///
511    /// # Errors
512    ///
513    /// Returns [`Error::SegmentLimit`] when the message would exceed the
514    /// specification's segment limit, or [`Error::LengthOverflow`] when the
515    /// resulting ciphertext length cannot be represented as a `u64`.
516    pub fn plaintext_layout(self, plaintext_length: u64) -> Result<MessageLayout> {
517        let plaintext_segment_length = u64::from(self.plaintext_segment_length_u32());
518
519        // An empty message still occupies one (final) segment.
520        let segment_count = plaintext_length.div_ceil(plaintext_segment_length).max(1);
521
522        if segment_count > AEAD_MAX_SEGMENTS {
523            return Err(Error::SegmentLimit);
524        }
525
526        let framing_length = segment_count
527            .checked_mul(SEGMENT_OVERHEAD_U64)
528            .ok_or(Error::LengthOverflow)?;
529
530        let ciphertext_length = HEADER_LENGTH_U64
531            .checked_add(plaintext_length)
532            .and_then(|length| length.checked_add(framing_length))
533            .ok_or(Error::LengthOverflow)?;
534
535        Ok(MessageLayout {
536            parameters: self,
537            plaintext_length,
538            ciphertext_length,
539            segment_count,
540        })
541    }
542
543    /// Calculates the complete FLOE layout for `ciphertext_length`.
544    ///
545    /// `ciphertext_length` includes the FLOE header. This validates only the
546    /// lengths implied by the file size and assumes every preceding segment is
547    /// a full non-final segment. Each prefix and authentication tag must still
548    /// be validated while decrypting. For streaming input whose complete
549    /// length is unavailable, use [`SegmentFraming::decode`] instead.
550    ///
551    /// # Errors
552    ///
553    /// Returns an error when the ciphertext is too short, implies an invalid
554    /// final segment length, or exceeds the specification's segment limit.
555    pub fn ciphertext_layout(self, ciphertext_length: u64) -> Result<MessageLayout> {
556        let body_length = ciphertext_length
557            .checked_sub(HEADER_LENGTH_U64)
558            .ok_or_else(|| Error::InvalidHeaderLength {
559                actual: length_u64_to_usize_saturating(ciphertext_length),
560            })?;
561
562        if body_length == 0 {
563            return Err(Error::Truncated);
564        }
565
566        let ciphertext_segment_length = u64::from(self.ciphertext_segment_length_u32());
567
568        let segment_count = body_length.div_ceil(ciphertext_segment_length);
569
570        if segment_count > AEAD_MAX_SEGMENTS {
571            return Err(Error::SegmentLimit);
572        }
573
574        let preceding_length = (segment_count - 1) * ciphertext_segment_length;
575        let final_length = body_length - preceding_length;
576
577        if final_length < SEGMENT_OVERHEAD_U64 {
578            return Err(Error::InvalidCiphertextLength {
579                actual: length_u64_to_usize_saturating(final_length),
580                required: LengthRequirement::Between {
581                    minimum: SEGMENT_OVERHEAD,
582                    maximum: self.ciphertext_segment_length(),
583                },
584            });
585        }
586
587        let framing_length = segment_count
588            .checked_mul(SEGMENT_OVERHEAD_U64)
589            .ok_or(Error::LengthOverflow)?;
590
591        let plaintext_length = body_length
592            .checked_sub(framing_length)
593            .ok_or(Error::LengthOverflow)?;
594
595        Ok(MessageLayout {
596            parameters: self,
597            plaintext_length,
598            ciphertext_length,
599            segment_count,
600        })
601    }
602
603    /// Encodes the parameters as `AEAD_ID || KDF_ID || ENC_SEG_LEN || FLOE_IV_LEN`.
604    #[must_use]
605    #[inline]
606    pub(crate) const fn encode(self) -> [u8; ENCODED_PARAMETERS_LENGTH] {
607        let segment_length = self.ciphertext_segment_length.to_be_bytes();
608        let iv_length = FLOE_IV_LENGTH_U32.to_be_bytes();
609        [
610            0,
611            0,
612            segment_length[0],
613            segment_length[1],
614            segment_length[2],
615            segment_length[3],
616            iv_length[0],
617            iv_length[1],
618            iv_length[2],
619            iv_length[3],
620        ]
621    }
622
623    pub(crate) fn decode(encoded: [u8; ENCODED_PARAMETERS_LENGTH]) -> Result<Self> {
624        let mut seg_len_bytes = [0u8; 4];
625        seg_len_bytes.copy_from_slice(&encoded[2..6]);
626
627        let segment_length = u32::from_be_bytes(seg_len_bytes);
628        let parameters = Self::with_segment_length(segment_length)
629            .map_err(|_| Error::InvalidHeaderParameters)?;
630
631        if parameters.encode() == encoded {
632            Ok(parameters)
633        } else {
634            Err(Error::InvalidHeaderParameters)
635        }
636    }
637
638    #[inline]
639    #[cfg(not(test))]
640    pub(crate) const fn masked_position(self, position: u64) -> u64 {
641        let _ = self;
642        position & ROTATION_MASK
643    }
644
645    #[cfg(test)]
646    pub(crate) const fn masked_position(self, position: u64) -> u64 {
647        position & self.rotation_mask
648    }
649}
650
651#[cfg(test)]
652mod tests {
653    use super::*;
654
655    #[test]
656    fn parameter_encoding_matches_specification() {
657        // Given the specification's encodings of the fixed parameter sets
658
659        // Then each constant encodes to the specified bytes and reports the
660        // specified segment length
661        assert_eq!(
662            Parameters::SEGMENT_4_KIB.encode(),
663            hex::decode("00000000100000000020").unwrap().as_slice()
664        );
665        assert_eq!(
666            Parameters::SEGMENT_1_MIB.encode(),
667            hex::decode("00000010000000000020").unwrap().as_slice()
668        );
669        assert_eq!(
670            Parameters::SEGMENT_4_KIB.ciphertext_segment_length(),
671            4 * 1024
672        );
673        assert_eq!(
674            Parameters::SEGMENT_1_MIB.ciphertext_segment_length(),
675            1024 * 1024
676        );
677    }
678
679    #[test]
680    fn parameters_accept_every_valid_segment_length() {
681        // Given segment lengths across the supported range, including both
682        // endpoints
683        let valid_range = Parameters::VALID_SEGMENT_LENGTHS;
684        let first_valid = valid_range.start;
685        let last_valid = valid_range.end - 1;
686
687        for segment_length in [
688            first_valid,
689            first_valid + 1,
690            4 * 1024,
691            64 * 1024,
692            1_000_000,
693            1024 * 1024,
694            last_valid,
695        ] {
696            assert!(valid_range.contains(&segment_length));
697
698            // When parameters are constructed from the segment length
699            let parameters = Parameters::with_segment_length(segment_length).unwrap();
700
701            // Then they report that length and survive an encode/decode
702            // round trip
703            assert_eq!(
704                parameters.ciphertext_segment_length(),
705                usize::try_from(segment_length).unwrap()
706            );
707            assert_eq!(Parameters::decode(parameters.encode()), Ok(parameters));
708        }
709    }
710
711    #[test]
712    fn parameters_reject_segment_lengths_outside_valid_range() {
713        // Given segment lengths just outside the supported range and at the
714        // u32 extremes
715        let valid_range = Parameters::VALID_SEGMENT_LENGTHS;
716        let first_valid = valid_range.start;
717
718        // valid_range.end is u32::MAX, the non-final segment marker
719        for segment_length in [0, first_valid - 1, valid_range.end] {
720            assert!(!valid_range.contains(&segment_length));
721
722            // When parameters are constructed from the segment length
723            // Then construction is rejected with the offending value
724            assert_eq!(
725                Parameters::with_segment_length(segment_length),
726                Err(Error::InvalidSegmentLength {
727                    actual: segment_length
728                })
729            );
730
731            // When the length is spliced into an otherwise valid encoding
732            // Then decoding is rejected as a header-parameter problem
733            let mut encoded = Parameters::SEGMENT_4_KIB.encode();
734            encoded[2..6].copy_from_slice(&segment_length.to_be_bytes());
735            assert_eq!(
736                Parameters::decode(encoded),
737                Err(Error::InvalidHeaderParameters)
738            );
739        }
740    }
741
742    #[test]
743    fn invalid_segment_length_error_names_the_value_and_bounds() {
744        // Given a segment length below the supported minimum
745        let error = Parameters::with_segment_length(63).unwrap_err();
746
747        // Then the error carries the value and its message states the value
748        // and the supported bounds, not a parameter-set mismatch
749        assert_eq!(error, Error::InvalidSegmentLength { actual: 63 });
750        let message = error.to_string();
751        assert!(message.contains("63"), "missing value: {message}");
752        assert!(message.contains("64"), "missing minimum: {message}");
753        assert!(
754            message.contains((u32::MAX - 1).to_string().as_str()),
755            "missing maximum: {message}"
756        );
757        assert!(
758            !message.contains("do not match"),
759            "reads as a mismatch: {message}"
760        );
761    }
762
763    #[test]
764    fn message_layouts_cover_plaintext_boundaries() {
765        // Given plaintext lengths at and around every segment boundary
766        let parameters = Parameters::SEGMENT_4_KIB;
767        let plaintext_segment_length =
768            u64::try_from(parameters.plaintext_segment_length()).unwrap();
769        let ciphertext_segment_length =
770            u64::try_from(parameters.ciphertext_segment_length()).unwrap();
771        let header_length = u64::try_from(HEADER_LENGTH).unwrap();
772        let overhead = u64::try_from(SEGMENT_OVERHEAD).unwrap();
773
774        for plaintext_length in [
775            0,
776            1,
777            plaintext_segment_length - 1,
778            plaintext_segment_length,
779            plaintext_segment_length + 1,
780            2 * plaintext_segment_length,
781            2 * plaintext_segment_length + 7,
782        ] {
783            // When the message layout is calculated from the plaintext length
784            let layout = parameters.plaintext_layout(plaintext_length).unwrap();
785            let expected_count = if plaintext_length == 0 {
786                1
787            } else {
788                (plaintext_length - 1) / plaintext_segment_length + 1
789            };
790
791            // Then the layout reports the expected lengths and segment count,
792            // and the ciphertext length maps back to the same layout
793            assert_eq!(layout.parameters(), parameters);
794            assert_eq!(layout.plaintext_length(), plaintext_length);
795            assert_eq!(layout.segment_count(), expected_count);
796            assert_eq!(
797                layout.ciphertext_length(),
798                header_length + plaintext_length + expected_count * overhead
799            );
800            assert_eq!(
801                parameters
802                    .ciphertext_layout(layout.ciphertext_length())
803                    .unwrap(),
804                layout
805            );
806
807            // Then iteration, indexed access, and reverse iteration agree
808            let segments: Vec<_> = layout.segments().collect();
809            assert_eq!(u64::try_from(segments.len()).unwrap(), expected_count);
810            assert_eq!(layout.into_iter().collect::<Vec<_>>(), segments);
811            assert_eq!(
812                layout.segments().next_back(),
813                layout.segment_for_position(expected_count - 1)
814            );
815
816            // Then every segment carries consistent offsets, lengths, and
817            // exactly the last position is final
818            for segment in segments {
819                let position = segment.position();
820                assert_eq!(Some(segment), layout.segment_for_position(position));
821                assert_eq!(segment.position(), position);
822                assert_eq!(
823                    segment.plaintext_offset(),
824                    position * plaintext_segment_length
825                );
826                assert_eq!(
827                    segment.ciphertext_offset(),
828                    header_length + position * ciphertext_segment_length
829                );
830                assert_eq!(
831                    u64::try_from(segment.ciphertext_length()).unwrap(),
832                    u64::try_from(segment.plaintext_length()).unwrap() + overhead
833                );
834                assert_eq!(segment.is_final(), position + 1 == expected_count);
835                assert_eq!(
836                    segment.kind(),
837                    if segment.is_final() {
838                        SegmentKind::Final
839                    } else {
840                        SegmentKind::NonFinal
841                    }
842                );
843            }
844
845            // Then positions beyond the message resolve to no segment
846            assert_eq!(layout.segment_for_position(layout.segment_count()), None);
847        }
848    }
849
850    #[test]
851    fn message_layouts_enforce_segment_limit() {
852        // Given the largest plaintext the specification's segment limit allows
853        let parameters = Parameters::SEGMENT_4_KIB;
854        let plaintext_segment_length =
855            u64::try_from(parameters.plaintext_segment_length()).unwrap();
856        let maximum_plaintext_length = AEAD_MAX_SEGMENTS * plaintext_segment_length;
857
858        // When its layout is calculated
859        let maximum = parameters
860            .plaintext_layout(maximum_plaintext_length)
861            .unwrap();
862
863        // Then the layout fills the limit exactly and ends with a final segment
864        assert_eq!(maximum.segment_count(), AEAD_MAX_SEGMENTS);
865        assert!(
866            maximum
867                .segment_for_position(AEAD_MAX_SEGMENTS - 1)
868                .unwrap()
869                .is_final()
870        );
871
872        // When one more byte is added on either the plaintext or ciphertext
873        // side, then the segment limit rejects the layout
874        assert_eq!(
875            parameters.plaintext_layout(maximum_plaintext_length + 1),
876            Err(Error::SegmentLimit)
877        );
878        assert_eq!(
879            parameters.ciphertext_layout(maximum.ciphertext_length() + 1),
880            Err(Error::SegmentLimit)
881        );
882    }
883
884    #[test]
885    fn ciphertext_layouts_classify_short_lengths() {
886        // Given ciphertext lengths around the header and minimum-segment
887        // boundaries
888        let parameters = Parameters::SEGMENT_4_KIB;
889        let header_length = u64::try_from(HEADER_LENGTH).unwrap();
890        let overhead = u64::try_from(SEGMENT_OVERHEAD).unwrap();
891
892        // When a length cannot hold a complete header,
893        // then it is classified as an invalid header length
894        assert!(matches!(
895            parameters.ciphertext_layout(header_length - 1),
896            Err(Error::InvalidHeaderLength { .. })
897        ));
898
899        // When a length holds the header but no body,
900        // then it is classified as truncated
901        assert_eq!(
902            parameters.ciphertext_layout(header_length),
903            Err(Error::Truncated)
904        );
905
906        // When the body cannot hold a minimum final segment,
907        // then the segment length is rejected
908        assert!(matches!(
909            parameters.ciphertext_layout(header_length + overhead - 1),
910            Err(Error::InvalidCiphertextLength { .. })
911        ));
912
913        // When the body holds exactly an empty final segment,
914        // then the layout matches the empty message
915        assert_eq!(
916            parameters
917                .ciphertext_layout(header_length + overhead)
918                .unwrap(),
919            parameters.plaintext_layout(0).unwrap()
920        );
921    }
922
923    #[test]
924    fn ciphertext_layout_accepts_length_valid_empty_final_segment() {
925        // Given a ciphertext length implying one full segment plus an empty
926        // final segment, a framing the canonical encoder never produces
927        let parameters = Parameters::SEGMENT_4_KIB;
928        let header_length = u64::try_from(HEADER_LENGTH).unwrap();
929        let ciphertext_segment_length =
930            u64::try_from(parameters.ciphertext_segment_length()).unwrap();
931        let overhead = u64::try_from(SEGMENT_OVERHEAD).unwrap();
932        let ciphertext_length = header_length + ciphertext_segment_length + overhead;
933
934        // When the layout is calculated from that ciphertext length
935        let layout = parameters.ciphertext_layout(ciphertext_length).unwrap();
936
937        // Then it describes a full non-final segment and an empty final one
938        assert_eq!(layout.segment_count(), 2);
939        assert_eq!(
940            layout.plaintext_length(),
941            u64::try_from(parameters.plaintext_segment_length()).unwrap()
942        );
943
944        let first = layout.segment_for_position(0).unwrap();
945        assert!(!first.is_final());
946        assert_eq!(
947            first.ciphertext_length(),
948            parameters.ciphertext_segment_length()
949        );
950        assert_eq!(
951            first.plaintext_length(),
952            parameters.plaintext_segment_length()
953        );
954
955        let final_segment = layout.segment_for_position(1).unwrap();
956        assert!(final_segment.is_final());
957        assert_eq!(final_segment.plaintext_length(), 0);
958        assert_eq!(final_segment.ciphertext_length(), SEGMENT_OVERHEAD);
959
960        // Then the canonical layout for the same plaintext differs, proving
961        // this framing is an accepted alternative rather than the default
962        let canonical = parameters
963            .plaintext_layout(layout.plaintext_length())
964            .unwrap();
965        assert_eq!(canonical.segment_count(), 1);
966        assert_ne!(canonical.ciphertext_length(), ciphertext_length);
967    }
968
969    #[test]
970    fn segment_prefixes_classify_final_and_non_final_framing() {
971        // Given the all-ones non-final prefix
972        let parameters = Parameters::SEGMENT_4_KIB;
973
974        // When it is decoded
975        let non_final = SegmentFraming::decode(parameters, u32::MAX.to_be_bytes()).unwrap();
976
977        // Then it classifies as a full non-final segment
978        assert_eq!(non_final.kind(), SegmentKind::NonFinal);
979        assert!(!non_final.is_final());
980        assert_eq!(
981            non_final.ciphertext_length(),
982            parameters.ciphertext_segment_length()
983        );
984        assert_eq!(
985            non_final.plaintext_length(),
986            parameters.plaintext_segment_length()
987        );
988
989        // Given final-segment lengths across the permitted range
990        for encrypted_length in [
991            SEGMENT_OVERHEAD,
992            SEGMENT_OVERHEAD + 7,
993            parameters.ciphertext_segment_length(),
994        ] {
995            // When the length prefix is decoded
996            let prefix = u32::try_from(encrypted_length).unwrap().to_be_bytes();
997            let final_segment = SegmentFraming::decode(parameters, prefix).unwrap();
998
999            // Then it classifies as a final segment of exactly that length
1000            assert_eq!(final_segment.kind(), SegmentKind::Final);
1001            assert!(final_segment.is_final());
1002            assert_eq!(final_segment.ciphertext_length(), encrypted_length);
1003            assert_eq!(
1004                final_segment.plaintext_length(),
1005                encrypted_length - SEGMENT_OVERHEAD
1006            );
1007        }
1008    }
1009
1010    #[test]
1011    fn segment_framing_rejects_lengths_outside_final_range() {
1012        // Given prefixes just below the minimum and just above the maximum
1013        // final-segment length
1014        let parameters = Parameters::SEGMENT_4_KIB;
1015        for invalid in [
1016            SEGMENT_OVERHEAD - 1,
1017            parameters.ciphertext_segment_length() + 1,
1018        ] {
1019            // When the prefix is decoded
1020            // Then the declared length is rejected
1021            let prefix = u32::try_from(invalid).unwrap().to_be_bytes();
1022            assert!(matches!(
1023                SegmentFraming::decode(parameters, prefix),
1024                Err(Error::InvalidCiphertextLength { .. })
1025            ));
1026        }
1027    }
1028
1029    #[test]
1030    fn segment_framing_rejects_prefix_whose_low_bits_look_valid() {
1031        // Given forged prefixes whose low bytes alone would decode to a
1032        // valid final-segment length
1033        let parameters = Parameters::SEGMENT_4_KIB;
1034        for forged in [69_632_u32, 1_048_576 + 4_096] {
1035            // When the full 32-bit prefix is decoded
1036            // Then the forgery is rejected rather than truncated to its
1037            // low bits
1038            assert!(matches!(
1039                SegmentFraming::decode(parameters, forged.to_be_bytes()),
1040                Err(Error::InvalidCiphertextLength { .. })
1041            ));
1042        }
1043    }
1044
1045    #[test]
1046    fn segment_payload_offset_follows_prefix_and_nonce() {
1047        // Given the specification's segment framing
1048        // Then the payload begins directly after the length prefix and nonce
1049        assert_eq!(
1050            SEGMENT_PAYLOAD_OFFSET,
1051            SEGMENT_PREFIX_LENGTH + AEAD_IV_LENGTH
1052        );
1053    }
1054
1055    #[test]
1056    fn masked_positions_rotate_at_specification_interval() {
1057        const ROTATION_INTERVAL: u64 = 1 << 20;
1058
1059        // Given the specification's key-rotation interval
1060        let parameters = Parameters::SEGMENT_4_KIB;
1061
1062        // Then positions mask to the interval boundary below them, up to the
1063        // segment limit
1064        assert_eq!(parameters.masked_position(ROTATION_INTERVAL - 1), 0);
1065        assert_eq!(
1066            parameters.masked_position(ROTATION_INTERVAL),
1067            ROTATION_INTERVAL
1068        );
1069        assert_eq!(
1070            parameters.masked_position(AEAD_MAX_SEGMENTS - 1),
1071            AEAD_MAX_SEGMENTS - ROTATION_INTERVAL
1072        );
1073    }
1074}