1use 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 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}