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