Skip to main content

s2_common/record/
encryption.rs

1//! Encrypted record storage, wire format, and raw cryptography.
2//!
3//! ```text
4//! [format_id: 1 byte] [nonce] [ciphertext] [tag]
5//! ```
6//!
7//! | format_id | Format         | Nonce  | Tag  |
8//! |-----------|----------------|--------|------|
9//! | 0x01      | AEGIS-256 v1   | 32 B   | 16 B |
10//! | 0x02      | AES-256-GCM v1 | 12 B   | 16 B |
11//!
12//! The leading format byte identifies the full encrypted record framing,
13//! including the framing version and encryption algorithm. This leaves room for
14//! future layout changes without a separate version byte.
15//!
16//! AAD is caller-supplied associated data and is not stored in the encoded
17//! record.
18//!
19//! Plaintext records are stored as `StoredRecord::Plaintext(Record)` and use
20//! the same command/envelope framing as the logical record layer.
21//!
22//! Encrypted envelope records are stored as `StoredRecord::Encrypted`. Their
23//! outer record type is `RecordType::EncryptedEnvelope`, and the encoded body is
24//! an [`EncryptedRecord`] containing encrypted bytes for the byte-for-byte
25//! plaintext [`EnvelopeRecord`](super::EnvelopeRecord) encoding.
26//!
27//! The stored `metered_size` remains the logical plaintext metered size rather
28//! than the encoded encrypted record size, so protection does not change
29//! append/read metering, limits, or accounting.
30
31use aegis::aegis256::Aegis256;
32use aes_gcm::{Aes256Gcm, KeyInit, aead::AeadInPlace};
33use bytes::{BufMut, Bytes, BytesMut};
34use rand::random;
35
36use super::{Encodable, Metered, MeteredSize, Record, RecordDecodeError, StoredRecord};
37use crate::{
38    deep_size::DeepSize,
39    encryption::{EncryptionAlgorithm, EncryptionMode, EncryptionSpec},
40    record::MeteredExt as _,
41};
42
43const FORMAT_ID_LEN: usize = 1;
44
45const FORMAT_ID_AEGIS256_V1: u8 = 0x01;
46const FORMAT_ID_AES256GCM_V1: u8 = 0x02;
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub(crate) enum EncryptedRecordFormat {
50    Aegis256V1,
51    Aes256GcmV1,
52}
53
54impl EncryptedRecordFormat {
55    /// Current write format for newly encrypted records with this algorithm.
56    const fn current_for_algorithm(algorithm: EncryptionAlgorithm) -> Self {
57        match algorithm {
58            EncryptionAlgorithm::Aegis256 => Self::Aegis256V1,
59            EncryptionAlgorithm::Aes256Gcm => Self::Aes256GcmV1,
60        }
61    }
62
63    const fn try_from_format_id(format_id: u8) -> Result<Self, RecordDecodeError> {
64        match format_id {
65            FORMAT_ID_AEGIS256_V1 => Ok(Self::Aegis256V1),
66            FORMAT_ID_AES256GCM_V1 => Ok(Self::Aes256GcmV1),
67            _ => Err(RecordDecodeError::InvalidValue(
68                "EncryptedRecord",
69                "invalid encrypted record format id",
70            )),
71        }
72    }
73
74    const fn format_id(self) -> u8 {
75        match self {
76            Self::Aegis256V1 => FORMAT_ID_AEGIS256_V1,
77            Self::Aes256GcmV1 => FORMAT_ID_AES256GCM_V1,
78        }
79    }
80
81    const fn algorithm(self) -> EncryptionAlgorithm {
82        match self {
83            Self::Aegis256V1 => EncryptionAlgorithm::Aegis256,
84            Self::Aes256GcmV1 => EncryptionAlgorithm::Aes256Gcm,
85        }
86    }
87
88    const fn nonce_len(self) -> usize {
89        match self {
90            Self::Aegis256V1 => 32,
91            Self::Aes256GcmV1 => 12,
92        }
93    }
94
95    const fn tag_len(self) -> usize {
96        match self {
97            Self::Aegis256V1 => 16,
98            Self::Aes256GcmV1 => 16,
99        }
100    }
101
102    fn put_random_nonce(self, buf: &mut impl BufMut) {
103        match self {
104            Self::Aegis256V1 => buf.put_slice(&random::<[u8; 32]>()),
105            Self::Aes256GcmV1 => buf.put_slice(&random::<[u8; 12]>()),
106        }
107    }
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
111pub enum RecordDecryptionError {
112    #[error("record encryption mode mismatch")]
113    ModeMismatch {
114        expected: EncryptionMode,
115        actual: EncryptionMode,
116    },
117    #[error("record decryption failed")]
118    AuthenticationFailed,
119    #[error("malformed encrypted record")]
120    MalformedEncryptedRecord,
121    #[error("decrypted record metered size mismatch: stored {stored}, actual {actual}")]
122    MeteredSizeMismatch { stored: usize, actual: usize },
123    #[error("malformed decrypted record: {0}")]
124    MalformedDecryptedRecord(#[from] RecordDecodeError),
125}
126
127#[derive(PartialEq, Eq, Clone)]
128pub struct EncryptedRecord {
129    encoded: Bytes,
130    format: EncryptedRecordFormat,
131}
132
133impl EncryptedRecord {
134    fn new(encoded: Bytes, format: EncryptedRecordFormat) -> Self {
135        debug_assert!(!encoded.is_empty());
136        debug_assert_eq!(encoded[0], format.format_id());
137        debug_assert!(encoded.len() >= FORMAT_ID_LEN + format.nonce_len() + format.tag_len());
138        Self { encoded, format }
139    }
140
141    pub fn algorithm(&self) -> EncryptionAlgorithm {
142        self.format.algorithm()
143    }
144
145    pub(crate) fn nonce(&self) -> &[u8] {
146        let start = FORMAT_ID_LEN;
147        let end = start + self.format.nonce_len();
148        &self.encoded[start..end]
149    }
150
151    pub(crate) fn ciphertext(&self) -> &[u8] {
152        let start = FORMAT_ID_LEN + self.format.nonce_len();
153        let end = self.encoded.len() - self.format.tag_len();
154        &self.encoded[start..end]
155    }
156
157    pub(crate) fn tag(&self) -> &[u8] {
158        let start = self.encoded.len() - self.format.tag_len();
159        let end = self.encoded.len();
160        &self.encoded[start..end]
161    }
162
163    fn into_mut_encoded(self) -> BytesMut {
164        self.encoded
165            .try_into_mut()
166            .unwrap_or_else(|encoded| BytesMut::from(encoded.as_ref()))
167    }
168}
169
170impl std::fmt::Debug for EncryptedRecord {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        f.debug_struct("EncryptedRecord")
173            .field("format_id", &self.encoded[0])
174            .field("format", &self.format)
175            .field("algorithm", &self.format.algorithm())
176            .field("nonce.len", &self.nonce().len())
177            .field("ciphertext.len", &self.ciphertext().len())
178            .field("tag.len", &self.tag().len())
179            .finish()
180    }
181}
182
183impl DeepSize for EncryptedRecord {
184    fn deep_size(&self) -> usize {
185        self.encoded.len()
186    }
187}
188
189impl Encodable for EncryptedRecord {
190    fn encoded_size(&self) -> usize {
191        self.encoded.len()
192    }
193
194    fn encode_into(&self, buf: &mut impl BufMut) {
195        buf.put_slice(self.encoded.as_ref());
196    }
197}
198
199pub fn encrypt_record(
200    record: Metered<Record>,
201    encryption: &EncryptionSpec,
202    aad: &[u8],
203) -> Metered<StoredRecord> {
204    let metered_size = record.metered_size();
205    let record = match (record.into_inner(), encryption) {
206        (record @ Record::Command(_), _) => StoredRecord::Plaintext(record),
207        (record @ Record::Envelope(_), EncryptionSpec::Plain) => StoredRecord::Plaintext(record),
208        (Record::Envelope(envelope), EncryptionSpec::Aegis256(key)) => {
209            let encrypted =
210                encrypt_payload(&envelope, EncryptionAlgorithm::Aegis256, key.secret(), aad);
211            StoredRecord::encrypted(encrypted, metered_size)
212        }
213        (Record::Envelope(envelope), EncryptionSpec::Aes256Gcm(key)) => {
214            let encrypted =
215                encrypt_payload(&envelope, EncryptionAlgorithm::Aes256Gcm, key.secret(), aad);
216            StoredRecord::encrypted(encrypted, metered_size)
217        }
218    };
219    Metered::with_size(metered_size, record)
220}
221
222fn encrypt_payload(
223    plaintext: &(impl Encodable + ?Sized),
224    alg: EncryptionAlgorithm,
225    key: &[u8; 32],
226    aad: &[u8],
227) -> EncryptedRecord {
228    let format = EncryptedRecordFormat::current_for_algorithm(alg);
229    let payload_start = FORMAT_ID_LEN + format.nonce_len();
230    let mut encoded =
231        BytesMut::with_capacity(payload_start + plaintext.encoded_size() + format.tag_len());
232    encoded.put_u8(format.format_id());
233    format.put_random_nonce(&mut encoded);
234    plaintext.encode_into(&mut encoded);
235
236    let (prefix, payload) = encoded.split_at_mut(payload_start);
237    let nonce = &prefix[FORMAT_ID_LEN..];
238
239    match format {
240        EncryptedRecordFormat::Aegis256V1 => {
241            let nonce: &[u8; 32] = nonce
242                .try_into()
243                .expect("AEGIS-256 nonce should match the encoded record framing");
244            let tag = Aegis256::<16>::new(key, nonce).encrypt_in_place(payload, aad);
245            encoded.put_slice(tag.as_ref());
246        }
247        EncryptedRecordFormat::Aes256GcmV1 => {
248            let nonce = aes_gcm::Nonce::from_slice(nonce);
249            let tag = Aes256Gcm::new(aes_gcm::Key::<Aes256Gcm>::from_slice(key))
250                .encrypt_in_place_detached(nonce, aad, payload)
251                .expect("AES-256-GCM encryption should not fail on size validation");
252            encoded.put_slice(tag.as_ref());
253        }
254    }
255
256    EncryptedRecord::new(encoded.freeze(), format)
257}
258
259impl TryFrom<Bytes> for EncryptedRecord {
260    type Error = RecordDecodeError;
261
262    fn try_from(encoded: Bytes) -> Result<Self, Self::Error> {
263        if encoded.len() < FORMAT_ID_LEN {
264            return Err(RecordDecodeError::Truncated("EncryptedRecordFormatId"));
265        }
266
267        let format = EncryptedRecordFormat::try_from_format_id(encoded[0])?;
268        let nonce_len = format.nonce_len();
269        let tag_len = format.tag_len();
270        if encoded.len() < FORMAT_ID_LEN + nonce_len + tag_len {
271            return Err(RecordDecodeError::Truncated("EncryptedRecordFrame"));
272        }
273
274        Ok(Self::new(encoded, format))
275    }
276}
277
278pub fn decrypt_stored_record(
279    record: StoredRecord,
280    encryption: &EncryptionSpec,
281    aad: &[u8],
282) -> Result<Metered<Record>, RecordDecryptionError> {
283    match record {
284        StoredRecord::Plaintext(record) => Ok(record.metered()),
285        StoredRecord::Encrypted {
286            metered_size,
287            record: encrypted,
288        } => {
289            let plaintext = decrypt_payload(encrypted, encryption, aad)?;
290            let record = Record::Envelope(plaintext.try_into()?);
291            let actual_metered_size = record.metered_size();
292            if metered_size != actual_metered_size {
293                return Err(RecordDecryptionError::MeteredSizeMismatch {
294                    stored: metered_size,
295                    actual: actual_metered_size,
296                });
297            }
298            Ok(Metered::with_size(metered_size, record))
299        }
300    }
301}
302
303fn decrypt_payload(
304    record: EncryptedRecord,
305    encryption: &EncryptionSpec,
306    aad: &[u8],
307) -> Result<Bytes, RecordDecryptionError> {
308    let format = record.format;
309    let expected = encryption.mode();
310    let (mut encoded, payload_start, payload_end) = decryption_layout(record, format)?;
311    let plaintext_len = payload_end - payload_start;
312
313    match format {
314        EncryptedRecordFormat::Aegis256V1 => {
315            let key = match encryption {
316                EncryptionSpec::Aegis256(key) => key,
317                _ => {
318                    return Err(RecordDecryptionError::ModeMismatch {
319                        expected,
320                        actual: EncryptionMode::Aegis256,
321                    });
322                }
323            };
324            {
325                let (prefix, payload_and_tag) = encoded.split_at_mut(payload_start);
326                let nonce: &[u8; 32] = prefix
327                    .get(FORMAT_ID_LEN..)
328                    .ok_or(RecordDecryptionError::MalformedEncryptedRecord)?
329                    .try_into()
330                    .map_err(|_| RecordDecryptionError::MalformedEncryptedRecord)?;
331                let (ciphertext, tag) = payload_and_tag.split_at_mut(plaintext_len);
332                let tag: &[u8; 16] = tag
333                    .as_ref()
334                    .try_into()
335                    .map_err(|_| RecordDecryptionError::MalformedEncryptedRecord)?;
336
337                Aegis256::<16>::new(key.secret(), nonce)
338                    .decrypt_in_place(ciphertext, tag, aad)
339                    .map_err(|_| RecordDecryptionError::AuthenticationFailed)?;
340            }
341            Ok(decryption_finish(encoded, payload_start, plaintext_len))
342        }
343        EncryptedRecordFormat::Aes256GcmV1 => {
344            let key = match encryption {
345                EncryptionSpec::Aes256Gcm(key) => key,
346                _ => {
347                    return Err(RecordDecryptionError::ModeMismatch {
348                        expected,
349                        actual: EncryptionMode::Aes256Gcm,
350                    });
351                }
352            };
353            let cipher = Aes256Gcm::new(aes_gcm::Key::<Aes256Gcm>::from_slice(key.secret()));
354            {
355                let (prefix, payload_and_tag) = encoded.split_at_mut(payload_start);
356                let nonce: &[u8; 12] = prefix
357                    .get(FORMAT_ID_LEN..)
358                    .ok_or(RecordDecryptionError::MalformedEncryptedRecord)?
359                    .try_into()
360                    .map_err(|_| RecordDecryptionError::MalformedEncryptedRecord)?;
361                let nonce = aes_gcm::Nonce::from_slice(nonce);
362                let (ciphertext, tag) = payload_and_tag.split_at_mut(plaintext_len);
363                let tag: &[u8; 16] = tag
364                    .as_ref()
365                    .try_into()
366                    .map_err(|_| RecordDecryptionError::MalformedEncryptedRecord)?;
367                let tag = aes_gcm::Tag::from_slice(tag);
368                cipher
369                    .decrypt_in_place_detached(nonce, aad, ciphertext, tag)
370                    .map_err(|_| RecordDecryptionError::AuthenticationFailed)?;
371            }
372            Ok(decryption_finish(encoded, payload_start, plaintext_len))
373        }
374    }
375}
376
377fn decryption_layout(
378    record: EncryptedRecord,
379    format: EncryptedRecordFormat,
380) -> Result<(BytesMut, usize, usize), RecordDecryptionError> {
381    let payload_start = FORMAT_ID_LEN + format.nonce_len();
382    let payload_end = record
383        .encoded
384        .len()
385        .checked_sub(format.tag_len())
386        .ok_or(RecordDecryptionError::MalformedEncryptedRecord)?;
387    if payload_start > payload_end {
388        return Err(RecordDecryptionError::MalformedEncryptedRecord);
389    }
390    Ok((record.into_mut_encoded(), payload_start, payload_end))
391}
392
393fn decryption_finish(mut encoded: BytesMut, payload_start: usize, plaintext_len: usize) -> Bytes {
394    let _ = encoded.split_to(payload_start);
395    encoded.truncate(plaintext_len);
396    encoded.freeze()
397}
398
399#[cfg(test)]
400mod tests {
401    use bytes::Bytes;
402    use rstest::rstest;
403
404    use super::*;
405    use crate::record::{EnvelopeRecord, Header, MeteredExt};
406
407    const TEST_KEY: [u8; 32] = [0x42; 32];
408    const OTHER_TEST_KEY: [u8; 32] = [0x99; 32];
409
410    fn test_encryption(alg: EncryptionAlgorithm) -> EncryptionSpec {
411        match alg {
412            EncryptionAlgorithm::Aegis256 => EncryptionSpec::aegis256(TEST_KEY),
413            EncryptionAlgorithm::Aes256Gcm => EncryptionSpec::aes256_gcm(TEST_KEY),
414        }
415    }
416
417    fn other_test_encryption(alg: EncryptionAlgorithm) -> EncryptionSpec {
418        match alg {
419            EncryptionAlgorithm::Aegis256 => EncryptionSpec::aegis256(OTHER_TEST_KEY),
420            EncryptionAlgorithm::Aes256Gcm => EncryptionSpec::aes256_gcm(OTHER_TEST_KEY),
421        }
422    }
423
424    fn encrypt_test_record(
425        plaintext: &(impl Encodable + ?Sized),
426        alg: EncryptionAlgorithm,
427        aad: &[u8],
428    ) -> EncryptedRecord {
429        encrypt_payload(plaintext, alg, &TEST_KEY, aad)
430    }
431
432    fn make_encrypted_record(
433        format: EncryptedRecordFormat,
434        nonce: impl AsRef<[u8]>,
435        ciphertext: impl AsRef<[u8]>,
436        tag: impl AsRef<[u8]>,
437    ) -> EncryptedRecord {
438        let nonce = nonce.as_ref();
439        let ciphertext = ciphertext.as_ref();
440        let tag = tag.as_ref();
441
442        assert_eq!(nonce.len(), format.nonce_len());
443        assert_eq!(tag.len(), format.tag_len());
444
445        let mut encoded =
446            BytesMut::with_capacity(FORMAT_ID_LEN + nonce.len() + ciphertext.len() + tag.len());
447        encoded.put_u8(format.format_id());
448        encoded.put_slice(nonce);
449        encoded.put_slice(ciphertext);
450        encoded.put_slice(tag);
451
452        EncryptedRecord::new(encoded.freeze(), format)
453    }
454
455    fn aad() -> [u8; 32] {
456        [0xA5; 32]
457    }
458
459    fn make_envelope(headers: Vec<Header>, body: Bytes) -> EnvelopeRecord {
460        EnvelopeRecord::try_from_parts(headers, body).unwrap()
461    }
462
463    fn make_plaintext_envelope(headers: Vec<Header>, body: Bytes) -> Record {
464        Record::Envelope(make_envelope(headers, body))
465    }
466
467    fn make_encrypted_stored_record(
468        encryption: &EncryptionSpec,
469        headers: Vec<Header>,
470        body: Bytes,
471        aad: &[u8],
472    ) -> StoredRecord {
473        let metered_size = make_plaintext_envelope(headers.clone(), body.clone()).metered_size();
474        let plaintext = make_envelope(headers, body);
475        let encrypted = match encryption {
476            EncryptionSpec::Plain => {
477                unreachable!("plain mode should not produce an encrypted record")
478            }
479            EncryptionSpec::Aegis256(key) => {
480                encrypt_payload(&plaintext, EncryptionAlgorithm::Aegis256, key.secret(), aad)
481            }
482            EncryptionSpec::Aes256Gcm(key) => encrypt_payload(
483                &plaintext,
484                EncryptionAlgorithm::Aes256Gcm,
485                key.secret(),
486                aad,
487            ),
488        };
489        StoredRecord::encrypted(encrypted, metered_size)
490    }
491
492    #[rstest]
493    #[case::aegis_unique(EncryptionAlgorithm::Aegis256, false)]
494    #[case::aegis_shared(EncryptionAlgorithm::Aegis256, true)]
495    #[case::aes_unique(EncryptionAlgorithm::Aes256Gcm, false)]
496    #[case::aes_shared(EncryptionAlgorithm::Aes256Gcm, true)]
497    fn encrypted_payload_roundtrips(
498        #[case] algorithm: EncryptionAlgorithm,
499        #[case] shared_encoded_record_buffer: bool,
500    ) {
501        let headers = vec![Header {
502            name: Bytes::from_static(b"x-test"),
503            value: Bytes::from_static(b"hello"),
504        }];
505        let body = Bytes::from_static(b"secret payload");
506
507        let aad = aad();
508        let plaintext = make_envelope(headers.clone(), body.clone());
509        let encryption = test_encryption(algorithm);
510        let encrypted_record = encrypt_test_record(&plaintext, algorithm, &aad);
511        let encrypted_record = if shared_encoded_record_buffer {
512            let shared = encrypted_record.encoded.clone();
513            EncryptedRecord::try_from(shared).unwrap()
514        } else {
515            encrypted_record
516        };
517        let decrypted = decrypt_payload(encrypted_record, &encryption, &aad).unwrap();
518        let (out_headers, out_body) = EnvelopeRecord::try_from(decrypted).unwrap().into_parts();
519
520        assert_eq!(out_headers, headers);
521        assert_eq!(out_body, body);
522    }
523
524    #[rstest]
525    #[case(EncryptionAlgorithm::Aegis256)]
526    #[case(EncryptionAlgorithm::Aes256Gcm)]
527    fn wrong_key_fails(#[case] algorithm: EncryptionAlgorithm) {
528        let aad = aad();
529        let plaintext = make_envelope(vec![], Bytes::from_static(b"data"));
530        let encrypted_record = encrypt_test_record(&plaintext, algorithm, &aad);
531        let result = decrypt_payload(encrypted_record, &other_test_encryption(algorithm), &aad);
532        assert!(matches!(
533            result,
534            Err(RecordDecryptionError::AuthenticationFailed)
535        ));
536    }
537
538    #[test]
539    fn empty_body_fails() {
540        let result = EncryptedRecord::try_from(Bytes::new());
541        assert!(matches!(
542            result,
543            Err(RecordDecodeError::Truncated("EncryptedRecordFormatId"))
544        ));
545    }
546
547    #[test]
548    fn format_id_byte_present() {
549        let aad = aad();
550        let plaintext = make_envelope(vec![], Bytes::from_static(b"data"));
551        let encrypted_record = encrypt_test_record(&plaintext, EncryptionAlgorithm::Aegis256, &aad);
552        let encoded = encrypted_record.to_bytes();
553        assert_eq!(encrypted_record.format, EncryptedRecordFormat::Aegis256V1);
554        assert_eq!(encrypted_record.algorithm(), EncryptionAlgorithm::Aegis256);
555        assert_eq!(encoded[0], 0x01);
556    }
557
558    #[test]
559    fn format_id_flip_detected() {
560        let aad = aad();
561        let plaintext = make_envelope(vec![], Bytes::from_static(b"data"));
562        let mut encoded_record =
563            encrypt_test_record(&plaintext, EncryptionAlgorithm::Aegis256, &aad)
564                .to_bytes()
565                .to_vec();
566        assert_eq!(encoded_record[0], 0x01);
567        encoded_record[0] = 0x02;
568        let encrypted_record = EncryptedRecord::try_from(Bytes::from(encoded_record)).unwrap();
569        let result = decrypt_payload(
570            encrypted_record,
571            &test_encryption(EncryptionAlgorithm::Aegis256),
572            &aad,
573        );
574        assert!(matches!(
575            result,
576            Err(RecordDecryptionError::ModeMismatch {
577                expected: EncryptionMode::Aegis256,
578                actual: EncryptionMode::Aes256Gcm,
579            })
580        ));
581    }
582
583    #[test]
584    fn wrong_aad_fails() {
585        let aad = aad();
586        let other_aad = [0x5A; 32];
587        let plaintext = make_envelope(vec![], Bytes::from_static(b"data"));
588        let encrypted_record = encrypt_test_record(&plaintext, EncryptionAlgorithm::Aegis256, &aad);
589        let result = decrypt_payload(
590            encrypted_record,
591            &test_encryption(EncryptionAlgorithm::Aegis256),
592            &other_aad,
593        );
594        assert!(matches!(
595            result,
596            Err(RecordDecryptionError::AuthenticationFailed)
597        ));
598    }
599
600    #[test]
601    fn malformed_encrypted_record_layout_returns_error_instead_of_panicking() {
602        let aad = aad();
603        let record = EncryptedRecord {
604            encoded: Bytes::from_static(b"\x01short"),
605            format: EncryptedRecordFormat::Aegis256V1,
606        };
607
608        let result = decrypt_payload(
609            record,
610            &test_encryption(EncryptionAlgorithm::Aegis256),
611            &aad,
612        );
613
614        assert!(matches!(
615            result,
616            Err(RecordDecryptionError::MalformedEncryptedRecord)
617        ));
618    }
619
620    #[test]
621    fn encrypted_record_roundtrips_aes256gcm() {
622        let record = make_encrypted_record(
623            EncryptedRecordFormat::Aes256GcmV1,
624            Bytes::from_static(b"0123456789ab"),
625            Bytes::from_static(b"ciphertext"),
626            Bytes::from_static(b"0123456789abcdef"),
627        );
628
629        let bytes = record.to_bytes();
630        let decoded = EncryptedRecord::try_from(bytes).unwrap();
631
632        assert_eq!(decoded, record);
633        assert_eq!(decoded.format, EncryptedRecordFormat::Aes256GcmV1);
634        assert_eq!(decoded.encoded[0], FORMAT_ID_AES256GCM_V1);
635        assert_eq!(decoded.nonce(), b"0123456789ab");
636        assert_eq!(decoded.ciphertext(), b"ciphertext");
637        assert_eq!(decoded.tag(), b"0123456789abcdef");
638    }
639
640    #[test]
641    fn rejects_invalid_format_id() {
642        let err = EncryptedRecord::try_from(Bytes::from_static(b"\xFFpayload")).unwrap_err();
643        assert_eq!(
644            err,
645            RecordDecodeError::InvalidValue(
646                "EncryptedRecord",
647                "invalid encrypted record format id"
648            )
649        );
650    }
651
652    #[test]
653    fn rejects_truncated_layout() {
654        let err = EncryptedRecord::try_from(Bytes::from_static(b"\x01tiny")).unwrap_err();
655        assert_eq!(err, RecordDecodeError::Truncated("EncryptedRecordFrame"));
656    }
657
658    #[test]
659    fn encrypt_record_encrypts_envelope_records() {
660        let aad = aad();
661        let encryption = test_encryption(EncryptionAlgorithm::Aegis256);
662        let headers = vec![Header {
663            name: Bytes::from_static(b"x-test"),
664            value: Bytes::from_static(b"hello"),
665        }];
666        let body = Bytes::from_static(b"secret payload");
667        let record = make_plaintext_envelope(headers.clone(), body.clone()).metered();
668
669        let stored = encrypt_record(record, &encryption, &aad).into_inner();
670        let StoredRecord::Encrypted {
671            record: envelope, ..
672        } = &stored
673        else {
674            panic!("expected encrypted envelope record");
675        };
676        assert_eq!(envelope.format, EncryptedRecordFormat::Aegis256V1);
677        assert_eq!(envelope.algorithm(), EncryptionAlgorithm::Aegis256);
678
679        let decrypted = decrypt_stored_record(stored, &encryption, &aad).unwrap();
680        let Record::Envelope(record) = decrypted.into_inner() else {
681            panic!("expected envelope record");
682        };
683        assert_eq!(record.headers(), headers.as_slice());
684        assert_eq!(record.body().as_ref(), body.as_ref());
685    }
686
687    #[test]
688    fn decrypt_stored_record_preserves_plaintext() {
689        let record = StoredRecord::Plaintext(Record::Envelope(
690            EnvelopeRecord::try_from_parts(vec![], Bytes::from_static(b"legacy-plaintext"))
691                .unwrap(),
692        ));
693
694        let decrypted = decrypt_stored_record(
695            record,
696            &test_encryption(EncryptionAlgorithm::Aegis256),
697            &aad(),
698        )
699        .unwrap();
700
701        let Record::Envelope(record) = decrypted.into_inner() else {
702            panic!("expected envelope record");
703        };
704        assert_eq!(record.body().as_ref(), b"legacy-plaintext");
705    }
706
707    #[test]
708    fn decrypt_stored_record_decrypts_encrypted_records() {
709        let aad = aad();
710        let record = make_encrypted_stored_record(
711            &test_encryption(EncryptionAlgorithm::Aegis256),
712            vec![Header {
713                name: Bytes::from_static(b"x-test"),
714                value: Bytes::from_static(b"hello"),
715            }],
716            Bytes::from_static(b"secret payload"),
717            &aad,
718        );
719
720        let decrypted = decrypt_stored_record(
721            record,
722            &test_encryption(EncryptionAlgorithm::Aegis256),
723            &aad,
724        )
725        .unwrap();
726
727        let Record::Envelope(record) = decrypted.into_inner() else {
728            panic!("expected envelope record");
729        };
730        assert_eq!(record.headers().len(), 1);
731        assert_eq!(record.headers()[0].name.as_ref(), b"x-test");
732        assert_eq!(record.headers()[0].value.as_ref(), b"hello");
733        assert_eq!(record.body().as_ref(), b"secret payload");
734    }
735
736    #[test]
737    fn decrypt_stored_record_plain_rejects_encrypted_records() {
738        let aad = aad();
739        let record = make_encrypted_stored_record(
740            &test_encryption(EncryptionAlgorithm::Aegis256),
741            vec![],
742            Bytes::from_static(b"secret payload"),
743            &aad,
744        );
745
746        let result = decrypt_stored_record(record, &EncryptionSpec::Plain, &aad);
747
748        assert!(matches!(
749            result,
750            Err(RecordDecryptionError::ModeMismatch {
751                expected: EncryptionMode::Plain,
752                actual: EncryptionMode::Aegis256,
753            })
754        ));
755    }
756
757    #[test]
758    fn decode_stored_record_rejects_encrypted_metered_size_mismatch() {
759        let aad = aad();
760        let stored = make_encrypted_stored_record(
761            &test_encryption(EncryptionAlgorithm::Aegis256),
762            vec![Header {
763                name: Bytes::from_static(b"x-test"),
764                value: Bytes::from_static(b"hello"),
765            }],
766            Bytes::from_static(b"secret payload"),
767            &aad,
768        );
769        let StoredRecord::Encrypted {
770            metered_size,
771            record,
772        } = stored
773        else {
774            panic!("expected encrypted stored record");
775        };
776
777        let result = decrypt_stored_record(
778            StoredRecord::encrypted(record, metered_size + 1),
779            &test_encryption(EncryptionAlgorithm::Aegis256),
780            &aad,
781        );
782
783        assert!(matches!(
784            result,
785            Err(RecordDecryptionError::MeteredSizeMismatch {
786                stored,
787                actual
788            }) if stored == metered_size + 1 && actual == metered_size
789        ));
790    }
791}