1use std::collections::HashMap;
122use std::path::Path;
123use std::sync::Arc;
124
125use aes_gcm::aead::{Aead, KeyInit, Payload};
126use aes_gcm::{Aes256Gcm, Key, Nonce};
127use bytes::Bytes;
128use md5::{Digest as Md5Digest, Md5};
129use rand::RngCore;
130use thiserror::Error;
131
132use crate::kms::{KmsBackend, KmsError, WrappedDek};
133
134pub const SSE_MAGIC_V1: &[u8; 4] = b"S4E1";
135pub const SSE_MAGIC_V2: &[u8; 4] = b"S4E2";
136pub const SSE_MAGIC_V3: &[u8; 4] = b"S4E3";
137pub const SSE_MAGIC_V4: &[u8; 4] = b"S4E4";
138pub const SSE_MAGIC_V5: &[u8; 4] = b"S4E5";
149pub const SSE_MAGIC_V6: &[u8; 4] = b"S4E6";
155pub const SSE_MAGIC: &[u8; 4] = SSE_MAGIC_V1;
157
158pub const SSE_HEADER_BYTES: usize = 4 + 1 + 3 + 12 + 16; pub const SSE_HEADER_BYTES_V3: usize = 4 + 1 + KEY_MD5_LEN + 12 + 16; pub const ALGO_AES_256_GCM: u8 = 1;
169const NONCE_LEN: usize = 12;
170const TAG_LEN: usize = 16;
171const KEY_LEN: usize = 32;
172const KEY_MD5_LEN: usize = 16;
173pub const SSE_C_ALGORITHM: &str = "AES256";
177
178#[derive(Debug, Error)]
179pub enum SseError {
180 #[error("SSE key file {path:?}: {source}")]
181 KeyFileIo {
182 path: std::path::PathBuf,
183 source: std::io::Error,
184 },
185 #[error(
186 "SSE key file must be exactly 32 raw bytes (or 64-char hex / 44-char base64); got {got} bytes after parse"
187 )]
188 BadKeyLength { got: usize },
189 #[error("SSE-encrypted body too short ({got} bytes; need at least {SSE_HEADER_BYTES})")]
190 TooShort { got: usize },
191 #[error("SSE bad magic: expected S4E1/S4E2/S4E3/S4E4/S4E5/S4E6, got {got:?}")]
192 BadMagic { got: [u8; 4] },
193 #[error("SSE unsupported algo tag: {tag} (this build only knows AES-256-GCM = 1)")]
194 UnsupportedAlgo { tag: u8 },
195 #[error(
196 "SSE key_id {id} (S4E2 frame) not present in keyring; rotation history likely incomplete"
197 )]
198 KeyNotInKeyring { id: u16 },
199 #[error("SSE decryption / authentication failed (key mismatch or ciphertext tampered with)")]
200 DecryptFailed,
201 #[error("SSE-C key MD5 fingerprint mismatch — client supplied a different key than PUT")]
209 WrongCustomerKey,
210 #[error("SSE-C customer-key headers invalid: {reason}")]
215 InvalidCustomerKey { reason: &'static str },
216 #[error("SSE-C algorithm {algo:?} unsupported (only {SSE_C_ALGORITHM:?} is allowed)")]
220 CustomerKeyAlgorithmUnsupported { algo: String },
221 #[error("S4E3 frame requires SseSource::CustomerKey; got Keyring")]
226 CustomerKeyRequired,
227 #[error("S4E1/S4E2 frame stored without SSE-C; SseSource::CustomerKey is unexpected")]
232 CustomerKeyUnexpected,
233 #[error(
240 "S4E4 (SSE-KMS) body requires async decrypt — call decrypt_with_kms() instead of decrypt()"
241 )]
242 KmsAsyncRequired,
243 #[error("S4E4 frame too short ({got} bytes; need at least {min})")]
247 KmsFrameTooShort { got: usize, min: usize },
248 #[error("S4E4 frame field length out of bounds: {what}")]
253 KmsFrameFieldOob { what: &'static str },
254 #[error("S4E4 key_id is not valid UTF-8")]
259 KmsKeyIdNotUtf8,
260 #[error(
267 "S4E4 SseSource::Kms wrapped DEK key_id {supplied:?} doesn't match frame key_id {stored:?}"
268 )]
269 KmsWrappedDekMismatch {
270 supplied: String,
271 stored: String,
272 },
273 #[error("S4E4 frame requires SseSource::Kms")]
280 KmsRequired,
281 #[error("KMS unwrap: {0}")]
284 KmsBackend(#[from] KmsError),
285 #[error("S4E5 chunk {chunk_index} auth tag verify failed (key mismatch or chunk tampered with)")]
294 ChunkAuthFailed { chunk_index: u32 },
295 #[error("S4E5 chunk_size must be > 0 (got 0)")]
300 ChunkSizeInvalid,
301 #[error("S4E5 frame truncated: {what}")]
307 ChunkFrameTruncated { what: &'static str },
308 #[error(
318 "S4E6 chunk_count {got} exceeds 24-bit max ({max}) — pick a larger --sse-chunk-size"
319 )]
320 ChunkCountTooLarge { got: u32, max: u32 },
321}
322
323pub struct SseKey {
328 pub bytes: [u8; 32],
329}
330
331impl SseKey {
332 pub fn from_path(path: &Path) -> Result<Self, SseError> {
336 let raw = std::fs::read(path).map_err(|source| SseError::KeyFileIo {
337 path: path.to_path_buf(),
338 source,
339 })?;
340 Self::from_bytes(&raw)
341 }
342
343 pub fn from_bytes(bytes: &[u8]) -> Result<Self, SseError> {
344 if bytes.len() == KEY_LEN {
346 let mut k = [0u8; KEY_LEN];
347 k.copy_from_slice(bytes);
348 return Ok(Self { bytes: k });
349 }
350 let s = std::str::from_utf8(bytes).unwrap_or("").trim();
352 if s.len() == KEY_LEN * 2 && s.chars().all(|c| c.is_ascii_hexdigit()) {
353 let mut k = [0u8; KEY_LEN];
354 for (i, k_byte) in k.iter_mut().enumerate() {
355 *k_byte = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16)
356 .map_err(|_| SseError::BadKeyLength { got: bytes.len() })?;
357 }
358 return Ok(Self { bytes: k });
359 }
360 if let Ok(decoded) =
361 base64::Engine::decode(&base64::engine::general_purpose::STANDARD, s.as_bytes())
362 && decoded.len() == KEY_LEN
363 {
364 let mut k = [0u8; KEY_LEN];
365 k.copy_from_slice(&decoded);
366 return Ok(Self { bytes: k });
367 }
368 Err(SseError::BadKeyLength { got: bytes.len() })
369 }
370
371 fn as_aes_key(&self) -> &Key<Aes256Gcm> {
372 Key::<Aes256Gcm>::from_slice(&self.bytes)
373 }
374}
375
376impl std::fmt::Debug for SseKey {
377 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
378 f.debug_struct("SseKey")
379 .field("len", &KEY_LEN)
380 .field("key", &"<redacted>")
381 .finish()
382 }
383}
384
385#[derive(Clone)]
390pub struct SseKeyring {
391 active: u16,
392 keys: HashMap<u16, Arc<SseKey>>,
393}
394
395impl SseKeyring {
396 pub fn new(active: u16, key: Arc<SseKey>) -> Self {
400 let mut keys = HashMap::new();
401 keys.insert(active, key);
402 Self { active, keys }
403 }
404
405 pub fn add(&mut self, id: u16, key: Arc<SseKey>) {
409 self.keys.insert(id, key);
410 }
411
412 pub fn active(&self) -> (u16, &SseKey) {
415 let id = self.active;
416 let key = self
417 .keys
418 .get(&id)
419 .expect("active key id must be present in keyring (constructor invariant)");
420 (id, key.as_ref())
421 }
422
423 pub fn get(&self, id: u16) -> Option<&SseKey> {
426 self.keys.get(&id).map(Arc::as_ref)
427 }
428}
429
430impl std::fmt::Debug for SseKeyring {
431 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
432 f.debug_struct("SseKeyring")
433 .field("active", &self.active)
434 .field("key_count", &self.keys.len())
435 .field("key_ids", &self.keys.keys().collect::<Vec<_>>())
436 .finish()
437 }
438}
439
440pub type SharedSseKeyring = Arc<SseKeyring>;
441
442pub fn encrypt(key: &SseKey, plaintext: &[u8]) -> Bytes {
449 let cipher = Aes256Gcm::new(key.as_aes_key());
450 let mut nonce_bytes = [0u8; NONCE_LEN];
451 rand::rngs::OsRng.fill_bytes(&mut nonce_bytes);
452 let nonce = Nonce::from_slice(&nonce_bytes);
453 let mut aad = [0u8; 8];
455 aad[..4].copy_from_slice(SSE_MAGIC_V1);
456 aad[4] = ALGO_AES_256_GCM;
457 let ct_with_tag = cipher
458 .encrypt(
459 nonce,
460 Payload {
461 msg: plaintext,
462 aad: &aad,
463 },
464 )
465 .expect("aes-gcm encrypt cannot fail with a 32-byte key");
466 debug_assert!(ct_with_tag.len() >= TAG_LEN);
467 let split = ct_with_tag.len() - TAG_LEN;
468 let (ct, tag) = ct_with_tag.split_at(split);
469
470 let mut out = Vec::with_capacity(SSE_HEADER_BYTES + ct.len());
471 out.extend_from_slice(SSE_MAGIC_V1);
472 out.push(ALGO_AES_256_GCM);
473 out.extend_from_slice(&[0u8; 3]); out.extend_from_slice(&nonce_bytes);
475 out.extend_from_slice(tag);
476 out.extend_from_slice(ct);
477 Bytes::from(out)
478}
479
480pub fn encrypt_v2(plaintext: &[u8], keyring: &SseKeyring) -> Bytes {
485 let (key_id, key) = keyring.active();
486 let cipher = Aes256Gcm::new(key.as_aes_key());
487 let mut nonce_bytes = [0u8; NONCE_LEN];
488 rand::rngs::OsRng.fill_bytes(&mut nonce_bytes);
489 let nonce = Nonce::from_slice(&nonce_bytes);
490 let aad = aad_v2(key_id);
491 let ct_with_tag = cipher
492 .encrypt(
493 nonce,
494 Payload {
495 msg: plaintext,
496 aad: &aad,
497 },
498 )
499 .expect("aes-gcm encrypt cannot fail with a 32-byte key");
500 debug_assert!(ct_with_tag.len() >= TAG_LEN);
501 let split = ct_with_tag.len() - TAG_LEN;
502 let (ct, tag) = ct_with_tag.split_at(split);
503
504 let mut out = Vec::with_capacity(SSE_HEADER_BYTES + ct.len());
505 out.extend_from_slice(SSE_MAGIC_V2);
506 out.push(ALGO_AES_256_GCM);
507 out.extend_from_slice(&key_id.to_be_bytes()); out.push(0u8); out.extend_from_slice(&nonce_bytes);
510 out.extend_from_slice(tag);
511 out.extend_from_slice(ct);
512 Bytes::from(out)
513}
514
515fn aad_v1() -> [u8; 8] {
516 let mut aad = [0u8; 8];
517 aad[..4].copy_from_slice(SSE_MAGIC_V1);
518 aad[4] = ALGO_AES_256_GCM;
519 aad
520}
521
522fn aad_v2(key_id: u16) -> [u8; 8] {
523 let mut aad = [0u8; 8];
524 aad[..4].copy_from_slice(SSE_MAGIC_V2);
525 aad[4] = ALGO_AES_256_GCM;
526 aad[5..7].copy_from_slice(&key_id.to_be_bytes());
527 aad[7] = 0u8;
528 aad
529}
530
531fn aad_v3(key_md5: &[u8; KEY_MD5_LEN]) -> [u8; 4 + 1 + KEY_MD5_LEN] {
537 let mut aad = [0u8; 4 + 1 + KEY_MD5_LEN];
538 aad[..4].copy_from_slice(SSE_MAGIC_V3);
539 aad[4] = ALGO_AES_256_GCM;
540 aad[5..5 + KEY_MD5_LEN].copy_from_slice(key_md5);
541 aad
542}
543
544#[derive(Clone)]
550pub struct CustomerKeyMaterial {
551 pub key: [u8; KEY_LEN],
552 pub key_md5: [u8; KEY_MD5_LEN],
553}
554
555impl std::fmt::Debug for CustomerKeyMaterial {
556 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
557 f.debug_struct("CustomerKeyMaterial")
560 .field("key", &"<redacted>")
561 .field("key_md5_hex", &hex_lower(&self.key_md5))
562 .finish()
563 }
564}
565
566fn hex_lower(bytes: &[u8]) -> String {
567 let mut s = String::with_capacity(bytes.len() * 2);
568 for b in bytes {
569 s.push_str(&format!("{b:02x}"));
570 }
571 s
572}
573
574#[derive(Debug, Clone, Copy)]
582pub enum SseSource<'a> {
583 Keyring(&'a SseKeyring),
586 CustomerKey {
590 key: &'a [u8; KEY_LEN],
591 key_md5: &'a [u8; KEY_MD5_LEN],
592 },
593 Kms {
599 dek: &'a [u8; KEY_LEN],
601 wrapped: &'a WrappedDek,
604 },
605}
606
607impl<'a> From<&'a SseKeyring> for SseSource<'a> {
614 fn from(kr: &'a SseKeyring) -> Self {
615 SseSource::Keyring(kr)
616 }
617}
618
619impl<'a> From<&'a Arc<SseKeyring>> for SseSource<'a> {
623 fn from(kr: &'a Arc<SseKeyring>) -> Self {
624 SseSource::Keyring(kr.as_ref())
625 }
626}
627
628impl<'a> From<&'a CustomerKeyMaterial> for SseSource<'a> {
629 fn from(m: &'a CustomerKeyMaterial) -> Self {
630 SseSource::CustomerKey {
631 key: &m.key,
632 key_md5: &m.key_md5,
633 }
634 }
635}
636
637pub fn parse_customer_key_headers(
649 algorithm: &str,
650 key_base64: &str,
651 key_md5_base64: &str,
652) -> Result<CustomerKeyMaterial, SseError> {
653 use base64::Engine as _;
654 if algorithm != SSE_C_ALGORITHM {
655 return Err(SseError::CustomerKeyAlgorithmUnsupported {
656 algo: algorithm.to_string(),
657 });
658 }
659 let key_bytes = base64::engine::general_purpose::STANDARD
660 .decode(key_base64.trim().as_bytes())
661 .map_err(|_| SseError::InvalidCustomerKey {
662 reason: "base64 decode of key",
663 })?;
664 if key_bytes.len() != KEY_LEN {
665 return Err(SseError::InvalidCustomerKey {
666 reason: "key length (must be 32 bytes after base64 decode)",
667 });
668 }
669 let supplied_md5 = base64::engine::general_purpose::STANDARD
670 .decode(key_md5_base64.trim().as_bytes())
671 .map_err(|_| SseError::InvalidCustomerKey {
672 reason: "base64 decode of key MD5",
673 })?;
674 if supplied_md5.len() != KEY_MD5_LEN {
675 return Err(SseError::InvalidCustomerKey {
676 reason: "key MD5 length (must be 16 bytes after base64 decode)",
677 });
678 }
679 let actual_md5 = compute_key_md5(&key_bytes);
680 if !constant_time_eq(&actual_md5, &supplied_md5) {
683 return Err(SseError::InvalidCustomerKey {
684 reason: "supplied MD5 does not match MD5 of supplied key",
685 });
686 }
687 let mut key = [0u8; KEY_LEN];
688 key.copy_from_slice(&key_bytes);
689 let mut key_md5 = [0u8; KEY_MD5_LEN];
690 key_md5.copy_from_slice(&actual_md5);
691 Ok(CustomerKeyMaterial { key, key_md5 })
692}
693
694pub fn compute_key_md5(key: &[u8]) -> [u8; KEY_MD5_LEN] {
699 let mut h = Md5::new();
700 h.update(key);
701 let out = h.finalize();
702 let mut md5 = [0u8; KEY_MD5_LEN];
703 md5.copy_from_slice(&out);
704 md5
705}
706
707fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
710 if a.len() != b.len() {
711 return false;
712 }
713 let mut acc: u8 = 0;
714 for (x, y) in a.iter().zip(b.iter()) {
715 acc |= x ^ y;
716 }
717 acc == 0
718}
719
720pub fn encrypt_with_source(plaintext: &[u8], source: SseSource<'_>) -> Bytes {
730 match source {
731 SseSource::Keyring(kr) => encrypt_v2(plaintext, kr),
732 SseSource::CustomerKey { key, key_md5 } => encrypt_v3(plaintext, key, key_md5),
733 SseSource::Kms { dek, wrapped } => encrypt_v4(plaintext, dek, wrapped),
734 }
735}
736
737fn encrypt_v3(
738 plaintext: &[u8],
739 key: &[u8; KEY_LEN],
740 key_md5: &[u8; KEY_MD5_LEN],
741) -> Bytes {
742 let aes_key = Key::<Aes256Gcm>::from_slice(key);
743 let cipher = Aes256Gcm::new(aes_key);
744 let mut nonce_bytes = [0u8; NONCE_LEN];
745 rand::rngs::OsRng.fill_bytes(&mut nonce_bytes);
746 let nonce = Nonce::from_slice(&nonce_bytes);
747 let aad = aad_v3(key_md5);
748 let ct_with_tag = cipher
749 .encrypt(
750 nonce,
751 Payload {
752 msg: plaintext,
753 aad: &aad,
754 },
755 )
756 .expect("aes-gcm encrypt cannot fail with a 32-byte key");
757 debug_assert!(ct_with_tag.len() >= TAG_LEN);
758 let split = ct_with_tag.len() - TAG_LEN;
759 let (ct, tag) = ct_with_tag.split_at(split);
760
761 let mut out = Vec::with_capacity(SSE_HEADER_BYTES_V3 + ct.len());
762 out.extend_from_slice(SSE_MAGIC_V3);
763 out.push(ALGO_AES_256_GCM);
764 out.extend_from_slice(key_md5);
765 out.extend_from_slice(&nonce_bytes);
766 out.extend_from_slice(tag);
767 out.extend_from_slice(ct);
768 Bytes::from(out)
769}
770
771pub fn decrypt<'a, S: Into<SseSource<'a>>>(body: &[u8], source: S) -> Result<Bytes, SseError> {
790 let source = source.into();
791 if body.len() < SSE_HEADER_BYTES {
797 return Err(SseError::TooShort { got: body.len() });
798 }
799 let mut magic = [0u8; 4];
800 magic.copy_from_slice(&body[..4]);
801 match &magic {
802 m if m == SSE_MAGIC_V1 || m == SSE_MAGIC_V2 => {
803 let keyring = match source {
804 SseSource::Keyring(kr) => kr,
805 SseSource::CustomerKey { .. } => return Err(SseError::CustomerKeyUnexpected),
806 SseSource::Kms { .. } => return Err(SseError::CustomerKeyUnexpected),
812 };
813 if m == SSE_MAGIC_V1 {
814 decrypt_v1_with_keyring(body, keyring)
815 } else {
816 decrypt_v2_with_keyring(body, keyring)
817 }
818 }
819 m if m == SSE_MAGIC_V3 => {
820 if body.len() < SSE_HEADER_BYTES_V3 {
822 return Err(SseError::TooShort { got: body.len() });
823 }
824 let (key, key_md5) = match source {
825 SseSource::CustomerKey { key, key_md5 } => (key, key_md5),
826 SseSource::Keyring(_) => return Err(SseError::CustomerKeyRequired),
827 SseSource::Kms { .. } => return Err(SseError::CustomerKeyRequired),
828 };
829 decrypt_v3(body, key, key_md5)
830 }
831 m if m == SSE_MAGIC_V4 => {
832 Err(SseError::KmsAsyncRequired)
837 }
838 m if m == SSE_MAGIC_V5 || m == SSE_MAGIC_V6 => {
839 let keyring = match source {
847 SseSource::Keyring(kr) => kr,
848 SseSource::CustomerKey { .. } => {
849 return Err(SseError::CustomerKeyUnexpected);
850 }
851 SseSource::Kms { .. } => return Err(SseError::CustomerKeyUnexpected),
852 };
853 decrypt_chunked_buffered(body, keyring)
854 }
855 _ => Err(SseError::BadMagic { got: magic }),
856 }
857}
858
859fn decrypt_v3(
860 body: &[u8],
861 key: &[u8; KEY_LEN],
862 supplied_md5: &[u8; KEY_MD5_LEN],
863) -> Result<Bytes, SseError> {
864 let algo = body[4];
865 if algo != ALGO_AES_256_GCM {
866 return Err(SseError::UnsupportedAlgo { tag: algo });
867 }
868 let mut stored_md5 = [0u8; KEY_MD5_LEN];
869 stored_md5.copy_from_slice(&body[5..5 + KEY_MD5_LEN]);
870 if !constant_time_eq(supplied_md5, &stored_md5) {
876 return Err(SseError::WrongCustomerKey);
877 }
878 let nonce_off = 5 + KEY_MD5_LEN;
879 let tag_off = nonce_off + NONCE_LEN;
880 let mut nonce_bytes = [0u8; NONCE_LEN];
881 nonce_bytes.copy_from_slice(&body[nonce_off..nonce_off + NONCE_LEN]);
882 let mut tag_bytes = [0u8; TAG_LEN];
883 tag_bytes.copy_from_slice(&body[tag_off..tag_off + TAG_LEN]);
884 let ct = &body[SSE_HEADER_BYTES_V3..];
885
886 let aad = aad_v3(&stored_md5);
887 let nonce = Nonce::from_slice(&nonce_bytes);
888 let mut ct_with_tag = Vec::with_capacity(ct.len() + TAG_LEN);
889 ct_with_tag.extend_from_slice(ct);
890 ct_with_tag.extend_from_slice(&tag_bytes);
891
892 let aes_key = Key::<Aes256Gcm>::from_slice(key);
893 let cipher = Aes256Gcm::new(aes_key);
894 let plain = cipher
895 .decrypt(
896 nonce,
897 Payload {
898 msg: &ct_with_tag,
899 aad: &aad,
900 },
901 )
902 .map_err(|_| SseError::DecryptFailed)?;
903 Ok(Bytes::from(plain))
904}
905
906fn aad_v4(key_id: &[u8], wrapped_dek: &[u8]) -> Vec<u8> {
917 let mut aad = Vec::with_capacity(4 + 1 + 1 + key_id.len() + 4 + wrapped_dek.len());
918 aad.extend_from_slice(SSE_MAGIC_V4);
919 aad.push(ALGO_AES_256_GCM);
920 aad.push(key_id.len() as u8);
921 aad.extend_from_slice(key_id);
922 aad.extend_from_slice(&(wrapped_dek.len() as u32).to_be_bytes());
923 aad.extend_from_slice(wrapped_dek);
924 aad
925}
926
927fn encrypt_v4(plaintext: &[u8], dek: &[u8; KEY_LEN], wrapped: &WrappedDek) -> Bytes {
928 assert!(
936 !wrapped.key_id.is_empty() && wrapped.key_id.len() <= u8::MAX as usize,
937 "S4E4 key_id must be 1..=255 bytes (got {})",
938 wrapped.key_id.len()
939 );
940 assert!(
941 wrapped.ciphertext.len() <= u32::MAX as usize,
942 "S4E4 wrapped_dek longer than u32::MAX",
943 );
944
945 let aes_key = Key::<Aes256Gcm>::from_slice(dek);
946 let cipher = Aes256Gcm::new(aes_key);
947 let mut nonce_bytes = [0u8; NONCE_LEN];
948 rand::rngs::OsRng.fill_bytes(&mut nonce_bytes);
949 let nonce = Nonce::from_slice(&nonce_bytes);
950 let aad = aad_v4(wrapped.key_id.as_bytes(), &wrapped.ciphertext);
951 let ct_with_tag = cipher
952 .encrypt(
953 nonce,
954 Payload {
955 msg: plaintext,
956 aad: &aad,
957 },
958 )
959 .expect("aes-gcm encrypt cannot fail with a 32-byte key");
960 debug_assert!(ct_with_tag.len() >= TAG_LEN);
961 let split = ct_with_tag.len() - TAG_LEN;
962 let (ct, tag) = ct_with_tag.split_at(split);
963
964 let key_id_bytes = wrapped.key_id.as_bytes();
965 let mut out = Vec::with_capacity(
966 4 + 1 + 1 + key_id_bytes.len() + 4 + wrapped.ciphertext.len() + NONCE_LEN + TAG_LEN + ct.len(),
967 );
968 out.extend_from_slice(SSE_MAGIC_V4);
969 out.push(ALGO_AES_256_GCM);
970 out.push(key_id_bytes.len() as u8);
971 out.extend_from_slice(key_id_bytes);
972 out.extend_from_slice(&(wrapped.ciphertext.len() as u32).to_be_bytes());
973 out.extend_from_slice(&wrapped.ciphertext);
974 out.extend_from_slice(&nonce_bytes);
975 out.extend_from_slice(tag);
976 out.extend_from_slice(ct);
977 Bytes::from(out)
978}
979
980#[derive(Debug)]
986pub struct S4E4Header<'a> {
987 pub key_id: &'a str,
988 pub wrapped_dek: &'a [u8],
989 pub nonce: &'a [u8],
990 pub tag: &'a [u8],
991 pub ciphertext: &'a [u8],
992}
993
994pub fn parse_s4e4_header(body: &[u8]) -> Result<S4E4Header<'_>, SseError> {
998 const S4E4_MIN: usize = 4 + 1 + 1 + 4 + NONCE_LEN + TAG_LEN; if body.len() < S4E4_MIN {
1005 return Err(SseError::KmsFrameTooShort {
1006 got: body.len(),
1007 min: S4E4_MIN,
1008 });
1009 }
1010 let magic = &body[..4];
1011 if magic != SSE_MAGIC_V4 {
1012 let mut got = [0u8; 4];
1013 got.copy_from_slice(magic);
1014 return Err(SseError::BadMagic { got });
1015 }
1016 let algo = body[4];
1017 if algo != ALGO_AES_256_GCM {
1018 return Err(SseError::UnsupportedAlgo { tag: algo });
1019 }
1020 let key_id_len = body[5] as usize;
1021 let key_id_off: usize = 6;
1022 let key_id_end = key_id_off
1023 .checked_add(key_id_len)
1024 .ok_or(SseError::KmsFrameFieldOob { what: "key_id_len" })?;
1025 if key_id_end + 4 > body.len() {
1026 return Err(SseError::KmsFrameFieldOob { what: "key_id" });
1027 }
1028 let key_id = std::str::from_utf8(&body[key_id_off..key_id_end])
1029 .map_err(|_| SseError::KmsKeyIdNotUtf8)?;
1030 let wrapped_len_off = key_id_end;
1031 let wrapped_dek_len = u32::from_be_bytes([
1032 body[wrapped_len_off],
1033 body[wrapped_len_off + 1],
1034 body[wrapped_len_off + 2],
1035 body[wrapped_len_off + 3],
1036 ]) as usize;
1037 let wrapped_off = wrapped_len_off + 4;
1038 let wrapped_end = wrapped_off
1039 .checked_add(wrapped_dek_len)
1040 .ok_or(SseError::KmsFrameFieldOob { what: "wrapped_dek_len" })?;
1041 if wrapped_end + NONCE_LEN + TAG_LEN > body.len() {
1042 return Err(SseError::KmsFrameFieldOob { what: "wrapped_dek" });
1043 }
1044 let wrapped_dek = &body[wrapped_off..wrapped_end];
1045 let nonce_off = wrapped_end;
1046 let tag_off = nonce_off + NONCE_LEN;
1047 let ct_off = tag_off + TAG_LEN;
1048 let nonce = &body[nonce_off..nonce_off + NONCE_LEN];
1049 let tag = &body[tag_off..tag_off + TAG_LEN];
1050 let ciphertext = &body[ct_off..];
1051 Ok(S4E4Header {
1052 key_id,
1053 wrapped_dek,
1054 nonce,
1055 tag,
1056 ciphertext,
1057 })
1058}
1059
1060pub async fn decrypt_with_kms(
1076 body: &[u8],
1077 kms: &dyn KmsBackend,
1078) -> Result<Bytes, SseError> {
1079 let hdr = parse_s4e4_header(body)?;
1080 let wrapped = WrappedDek {
1081 key_id: hdr.key_id.to_string(),
1082 ciphertext: hdr.wrapped_dek.to_vec(),
1083 };
1084 let dek_vec = kms.decrypt_dek(&wrapped).await?;
1085 if dek_vec.len() != KEY_LEN {
1086 return Err(SseError::KmsBackend(KmsError::BackendUnavailable {
1091 message: format!(
1092 "KMS returned {} byte DEK; expected {KEY_LEN}",
1093 dek_vec.len()
1094 ),
1095 }));
1096 }
1097 let mut dek = [0u8; KEY_LEN];
1098 dek.copy_from_slice(&dek_vec);
1099
1100 let aad = aad_v4(hdr.key_id.as_bytes(), hdr.wrapped_dek);
1101 let aes_key = Key::<Aes256Gcm>::from_slice(&dek);
1102 let cipher = Aes256Gcm::new(aes_key);
1103 let nonce = Nonce::from_slice(hdr.nonce);
1104 let mut ct_with_tag = Vec::with_capacity(hdr.ciphertext.len() + TAG_LEN);
1105 ct_with_tag.extend_from_slice(hdr.ciphertext);
1106 ct_with_tag.extend_from_slice(hdr.tag);
1107 let plain = cipher
1108 .decrypt(
1109 nonce,
1110 Payload {
1111 msg: &ct_with_tag,
1112 aad: &aad,
1113 },
1114 )
1115 .map_err(|_| SseError::DecryptFailed)?;
1116 Ok(Bytes::from(plain))
1117}
1118
1119fn decrypt_v1_with_keyring(body: &[u8], keyring: &SseKeyring) -> Result<Bytes, SseError> {
1120 let algo = body[4];
1121 if algo != ALGO_AES_256_GCM {
1122 return Err(SseError::UnsupportedAlgo { tag: algo });
1123 }
1124 let mut nonce_bytes = [0u8; NONCE_LEN];
1127 nonce_bytes.copy_from_slice(&body[8..8 + NONCE_LEN]);
1128 let mut tag_bytes = [0u8; TAG_LEN];
1129 tag_bytes.copy_from_slice(&body[8 + NONCE_LEN..SSE_HEADER_BYTES]);
1130 let ct = &body[SSE_HEADER_BYTES..];
1131
1132 let aad = aad_v1();
1133 let nonce = Nonce::from_slice(&nonce_bytes);
1134 let mut ct_with_tag = Vec::with_capacity(ct.len() + TAG_LEN);
1135 ct_with_tag.extend_from_slice(ct);
1136 ct_with_tag.extend_from_slice(&tag_bytes);
1137
1138 let (active_id, _active_key) = keyring.active();
1142 let mut ids: Vec<u16> = keyring.keys.keys().copied().collect();
1143 ids.sort_by_key(|id| if *id == active_id { 0 } else { 1 });
1144 for id in ids {
1145 let key = keyring.get(id).expect("id came from keyring iteration");
1146 let cipher = Aes256Gcm::new(key.as_aes_key());
1147 if let Ok(plain) = cipher.decrypt(
1148 nonce,
1149 Payload {
1150 msg: &ct_with_tag,
1151 aad: &aad,
1152 },
1153 ) {
1154 return Ok(Bytes::from(plain));
1155 }
1156 }
1157 Err(SseError::DecryptFailed)
1158}
1159
1160fn decrypt_v2_with_keyring(body: &[u8], keyring: &SseKeyring) -> Result<Bytes, SseError> {
1161 let algo = body[4];
1162 if algo != ALGO_AES_256_GCM {
1163 return Err(SseError::UnsupportedAlgo { tag: algo });
1164 }
1165 let key_id = u16::from_be_bytes([body[5], body[6]]);
1166 let key = keyring
1168 .get(key_id)
1169 .ok_or(SseError::KeyNotInKeyring { id: key_id })?;
1170 let mut nonce_bytes = [0u8; NONCE_LEN];
1171 nonce_bytes.copy_from_slice(&body[8..8 + NONCE_LEN]);
1172 let mut tag_bytes = [0u8; TAG_LEN];
1173 tag_bytes.copy_from_slice(&body[8 + NONCE_LEN..SSE_HEADER_BYTES]);
1174 let ct = &body[SSE_HEADER_BYTES..];
1175
1176 let aad = aad_v2(key_id);
1177 let nonce = Nonce::from_slice(&nonce_bytes);
1178 let mut ct_with_tag = Vec::with_capacity(ct.len() + TAG_LEN);
1179 ct_with_tag.extend_from_slice(ct);
1180 ct_with_tag.extend_from_slice(&tag_bytes);
1181 let cipher = Aes256Gcm::new(key.as_aes_key());
1182 let plain = cipher
1183 .decrypt(
1184 nonce,
1185 Payload {
1186 msg: &ct_with_tag,
1187 aad: &aad,
1188 },
1189 )
1190 .map_err(|_| SseError::DecryptFailed)?;
1191 Ok(Bytes::from(plain))
1192}
1193
1194pub fn looks_encrypted(body: &[u8]) -> bool {
1205 if body.len() < SSE_HEADER_BYTES {
1206 return false;
1207 }
1208 let m = &body[..4];
1209 m == SSE_MAGIC_V1
1210 || m == SSE_MAGIC_V2
1211 || m == SSE_MAGIC_V3
1212 || m == SSE_MAGIC_V4
1213 || m == SSE_MAGIC_V5
1214 || m == SSE_MAGIC_V6
1215}
1216
1217pub fn peek_magic(body: &[u8]) -> Option<&'static str> {
1228 if body.len() < SSE_HEADER_BYTES {
1229 return None;
1230 }
1231 match &body[..4] {
1232 m if m == SSE_MAGIC_V1 => Some("S4E1"),
1233 m if m == SSE_MAGIC_V2 => Some("S4E2"),
1234 m if m == SSE_MAGIC_V3 => Some("S4E3"),
1235 m if m == SSE_MAGIC_V4 => Some("S4E4"),
1236 m if m == SSE_MAGIC_V5 => Some("S4E5"),
1241 m if m == SSE_MAGIC_V6 => Some("S4E6"),
1243 _ => None,
1244 }
1245}
1246
1247pub type SharedSseKey = Arc<SseKey>;
1248
1249pub const S4E5_HEADER_BYTES: usize = 4 + 1 + 2 + 1 + 4 + 4 + 4; pub const S4E5_PER_CHUNK_OVERHEAD: usize = TAG_LEN; pub const S4E6_HEADER_BYTES: usize = 4 + 1 + 2 + 1 + 4 + 4 + 8; pub const S4E6_PER_CHUNK_OVERHEAD: usize = TAG_LEN; pub const S4E6_MAX_CHUNK_COUNT: u32 = (1u32 << 24) - 1; const S4E5_NONCE_TAG: [u8; 4] = [b'E', b'5', 0, 0];
1371
1372const S4E6_NONCE_PREFIX: u8 = b'E';
1377
1378#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1383enum ChunkedVariant {
1384 V5,
1385 V6,
1386}
1387
1388impl ChunkedVariant {
1389 fn header_bytes(self) -> usize {
1390 match self {
1391 ChunkedVariant::V5 => S4E5_HEADER_BYTES,
1392 ChunkedVariant::V6 => S4E6_HEADER_BYTES,
1393 }
1394 }
1395}
1396
1397fn aad_v5(
1402 chunk_index: u32,
1403 total_chunks: u32,
1404 key_id: u16,
1405 salt: &[u8; 4],
1406) -> [u8; 4 + 1 + 4 + 4 + 2 + 4] {
1407 let mut aad = [0u8; 4 + 1 + 4 + 4 + 2 + 4]; aad[..4].copy_from_slice(SSE_MAGIC_V5);
1409 aad[4] = ALGO_AES_256_GCM;
1410 aad[5..9].copy_from_slice(&chunk_index.to_be_bytes());
1411 aad[9..13].copy_from_slice(&total_chunks.to_be_bytes());
1412 aad[13..15].copy_from_slice(&key_id.to_be_bytes());
1413 aad[15..19].copy_from_slice(salt);
1414 aad
1415}
1416
1417fn aad_v6(
1423 chunk_index: u32,
1424 total_chunks: u32,
1425 key_id: u16,
1426 salt: &[u8; 8],
1427) -> [u8; 4 + 1 + 4 + 4 + 2 + 8] {
1428 let mut aad = [0u8; 4 + 1 + 4 + 4 + 2 + 8]; aad[..4].copy_from_slice(SSE_MAGIC_V6);
1430 aad[4] = ALGO_AES_256_GCM;
1431 aad[5..9].copy_from_slice(&chunk_index.to_be_bytes());
1432 aad[9..13].copy_from_slice(&total_chunks.to_be_bytes());
1433 aad[13..15].copy_from_slice(&key_id.to_be_bytes());
1434 aad[15..23].copy_from_slice(salt);
1435 aad
1436}
1437
1438fn nonce_v5(salt: &[u8; 4], chunk_index: u32) -> [u8; NONCE_LEN] {
1444 let mut n = [0u8; NONCE_LEN];
1445 n[..4].copy_from_slice(&S4E5_NONCE_TAG);
1446 n[4..8].copy_from_slice(salt);
1447 n[8..12].copy_from_slice(&chunk_index.to_be_bytes());
1448 n
1449}
1450
1451fn nonce_v6(salt: &[u8; 8], chunk_index: u32) -> [u8; NONCE_LEN] {
1459 debug_assert!(
1460 chunk_index <= S4E6_MAX_CHUNK_COUNT,
1461 "S4E6 chunk_index {chunk_index} exceeds 24-bit cap (caller MUST validate)",
1462 );
1463 let mut n = [0u8; NONCE_LEN];
1464 n[0] = S4E6_NONCE_PREFIX;
1465 n[1..9].copy_from_slice(salt);
1466 let be = chunk_index.to_be_bytes(); n[9..12].copy_from_slice(&be[1..4]);
1470 n
1471}
1472
1473pub fn encrypt_v2_chunked(
1496 plaintext: &[u8],
1497 keyring: &SseKeyring,
1498 chunk_size: usize,
1499) -> Result<Bytes, SseError> {
1500 if chunk_size == 0 {
1501 return Err(SseError::ChunkSizeInvalid);
1502 }
1503 let (key_id, key) = keyring.active();
1504 let cipher = Aes256Gcm::new(key.as_aes_key());
1505 let mut salt = [0u8; 8];
1506 rand::rngs::OsRng.fill_bytes(&mut salt);
1507
1508 let chunk_count_usize = if plaintext.is_empty() {
1511 1
1512 } else {
1513 plaintext.len().div_ceil(chunk_size)
1514 };
1515 let chunk_count: u32 = u32::try_from(chunk_count_usize).unwrap_or(u32::MAX);
1519 if chunk_count > S4E6_MAX_CHUNK_COUNT {
1520 return Err(SseError::ChunkCountTooLarge {
1521 got: chunk_count,
1522 max: S4E6_MAX_CHUNK_COUNT,
1523 });
1524 }
1525
1526 let mut out = Vec::with_capacity(
1527 S4E6_HEADER_BYTES + plaintext.len() + (chunk_count as usize * S4E6_PER_CHUNK_OVERHEAD),
1528 );
1529 out.extend_from_slice(SSE_MAGIC_V6);
1530 out.push(ALGO_AES_256_GCM);
1531 out.extend_from_slice(&key_id.to_be_bytes());
1532 out.push(0u8); out.extend_from_slice(&(chunk_size as u32).to_be_bytes());
1534 out.extend_from_slice(&chunk_count.to_be_bytes());
1535 out.extend_from_slice(&salt);
1536
1537 for i in 0..chunk_count {
1538 let off = (i as usize).saturating_mul(chunk_size);
1539 let end = off.saturating_add(chunk_size).min(plaintext.len());
1540 let chunk_pt: &[u8] = if off >= plaintext.len() {
1541 &[]
1544 } else {
1545 &plaintext[off..end]
1546 };
1547 let nonce_bytes = nonce_v6(&salt, i);
1548 let nonce = Nonce::from_slice(&nonce_bytes);
1549 let aad = aad_v6(i, chunk_count, key_id, &salt);
1550 let ct_with_tag = cipher
1551 .encrypt(
1552 nonce,
1553 Payload {
1554 msg: chunk_pt,
1555 aad: &aad,
1556 },
1557 )
1558 .expect("aes-gcm encrypt cannot fail with a 32-byte key");
1559 debug_assert!(ct_with_tag.len() >= TAG_LEN);
1560 let split = ct_with_tag.len() - TAG_LEN;
1561 let (ct, tag) = ct_with_tag.split_at(split);
1562 out.extend_from_slice(tag);
1563 out.extend_from_slice(ct);
1564 crate::metrics::record_sse_streaming_chunk("encrypt");
1565 }
1566 Ok(Bytes::from(out))
1567}
1568
1569#[derive(Debug, Clone, Copy)]
1573enum ChunkedSalt {
1574 V5([u8; 4]),
1575 V6([u8; 8]),
1576}
1577
1578#[derive(Debug, Clone, Copy)]
1583struct ChunkedHeader {
1584 #[allow(dead_code)]
1590 variant: ChunkedVariant,
1591 key_id: u16,
1592 chunk_size: u32,
1593 chunk_count: u32,
1594 salt: ChunkedSalt,
1595 chunks_offset: usize,
1599}
1600
1601#[derive(Debug, Clone, Copy)]
1608pub struct S4E6Header<'a> {
1609 pub key_id: u16,
1610 pub chunk_size: u32,
1611 pub chunk_count: u32,
1612 pub salt: &'a [u8; 8],
1613}
1614
1615pub fn parse_s4e6_header(blob: &[u8]) -> Result<S4E6Header<'_>, SseError> {
1619 if blob.len() < S4E6_HEADER_BYTES {
1620 return Err(SseError::ChunkFrameTruncated { what: "header" });
1621 }
1622 if &blob[..4] != SSE_MAGIC_V6 {
1623 let mut got = [0u8; 4];
1624 got.copy_from_slice(&blob[..4]);
1625 return Err(SseError::BadMagic { got });
1626 }
1627 let algo = blob[4];
1628 if algo != ALGO_AES_256_GCM {
1629 return Err(SseError::UnsupportedAlgo { tag: algo });
1630 }
1631 let key_id = u16::from_be_bytes([blob[5], blob[6]]);
1632 let chunk_size = u32::from_be_bytes([blob[8], blob[9], blob[10], blob[11]]);
1634 let chunk_count = u32::from_be_bytes([blob[12], blob[13], blob[14], blob[15]]);
1635 if chunk_size == 0 {
1636 return Err(SseError::ChunkSizeInvalid);
1637 }
1638 if chunk_count == 0 {
1639 return Err(SseError::ChunkFrameTruncated {
1640 what: "chunk_count == 0",
1641 });
1642 }
1643 if chunk_count > S4E6_MAX_CHUNK_COUNT {
1644 return Err(SseError::ChunkCountTooLarge {
1645 got: chunk_count,
1646 max: S4E6_MAX_CHUNK_COUNT,
1647 });
1648 }
1649 let salt: &[u8; 8] = (&blob[16..24]).try_into().expect("8B salt slice");
1650 Ok(S4E6Header {
1651 key_id,
1652 chunk_size,
1653 chunk_count,
1654 salt,
1655 })
1656}
1657
1658fn parse_chunked_header(body: &[u8]) -> Result<ChunkedHeader, SseError> {
1659 if body.len() < 4 {
1660 return Err(SseError::ChunkFrameTruncated { what: "magic" });
1661 }
1662 let magic = &body[..4];
1663 let variant = if magic == SSE_MAGIC_V5 {
1664 ChunkedVariant::V5
1665 } else if magic == SSE_MAGIC_V6 {
1666 ChunkedVariant::V6
1667 } else {
1668 let mut got = [0u8; 4];
1669 got.copy_from_slice(magic);
1670 return Err(SseError::BadMagic { got });
1671 };
1672 let header_bytes = variant.header_bytes();
1673 if body.len() < header_bytes {
1674 return Err(SseError::ChunkFrameTruncated { what: "header" });
1675 }
1676 let algo = body[4];
1677 if algo != ALGO_AES_256_GCM {
1678 return Err(SseError::UnsupportedAlgo { tag: algo });
1679 }
1680 let key_id = u16::from_be_bytes([body[5], body[6]]);
1681 let chunk_size = u32::from_be_bytes([body[8], body[9], body[10], body[11]]);
1683 let chunk_count = u32::from_be_bytes([body[12], body[13], body[14], body[15]]);
1684 if chunk_size == 0 {
1685 return Err(SseError::ChunkSizeInvalid);
1686 }
1687 if chunk_count == 0 {
1688 return Err(SseError::ChunkFrameTruncated {
1689 what: "chunk_count == 0",
1690 });
1691 }
1692 let salt = match variant {
1693 ChunkedVariant::V5 => {
1694 let mut s = [0u8; 4];
1695 s.copy_from_slice(&body[16..20]);
1696 ChunkedSalt::V5(s)
1697 }
1698 ChunkedVariant::V6 => {
1699 if chunk_count > S4E6_MAX_CHUNK_COUNT {
1704 return Err(SseError::ChunkCountTooLarge {
1705 got: chunk_count,
1706 max: S4E6_MAX_CHUNK_COUNT,
1707 });
1708 }
1709 let mut s = [0u8; 8];
1710 s.copy_from_slice(&body[16..24]);
1711 ChunkedSalt::V6(s)
1712 }
1713 };
1714 Ok(ChunkedHeader {
1715 variant,
1716 key_id,
1717 chunk_size,
1718 chunk_count,
1719 salt,
1720 chunks_offset: header_bytes,
1721 })
1722}
1723
1724fn decrypt_chunked_chunk(
1728 cipher: &Aes256Gcm,
1729 chunk_index: u32,
1730 chunk_count: u32,
1731 key_id: u16,
1732 salt: &ChunkedSalt,
1733 tag: &[u8; TAG_LEN],
1734 ct: &[u8],
1735) -> Result<Bytes, SseError> {
1736 let nonce_bytes = match salt {
1737 ChunkedSalt::V5(s) => nonce_v5(s, chunk_index),
1738 ChunkedSalt::V6(s) => nonce_v6(s, chunk_index),
1739 };
1740 let nonce = Nonce::from_slice(&nonce_bytes);
1741 let mut ct_with_tag = Vec::with_capacity(ct.len() + TAG_LEN);
1742 ct_with_tag.extend_from_slice(ct);
1743 ct_with_tag.extend_from_slice(tag);
1744 let result = match salt {
1745 ChunkedSalt::V5(s) => {
1746 let aad = aad_v5(chunk_index, chunk_count, key_id, s);
1747 cipher.decrypt(
1748 nonce,
1749 Payload {
1750 msg: &ct_with_tag,
1751 aad: &aad,
1752 },
1753 )
1754 }
1755 ChunkedSalt::V6(s) => {
1756 let aad = aad_v6(chunk_index, chunk_count, key_id, s);
1757 cipher.decrypt(
1758 nonce,
1759 Payload {
1760 msg: &ct_with_tag,
1761 aad: &aad,
1762 },
1763 )
1764 }
1765 };
1766 result
1767 .map(Bytes::from)
1768 .map_err(|_| SseError::ChunkAuthFailed { chunk_index })
1769}
1770
1771fn walk_chunked<F: FnMut(Bytes) -> Result<(), SseError>>(
1777 body: &[u8],
1778 keyring: &SseKeyring,
1779 mut emit: F,
1780) -> Result<(), SseError> {
1781 let hdr = parse_chunked_header(body)?;
1782 let key = keyring
1783 .get(hdr.key_id)
1784 .ok_or(SseError::KeyNotInKeyring { id: hdr.key_id })?;
1785 let cipher = Aes256Gcm::new(key.as_aes_key());
1786
1787 let mut cursor = hdr.chunks_offset;
1788 let chunk_size = hdr.chunk_size as usize;
1789 for i in 0..hdr.chunk_count {
1790 if cursor + TAG_LEN > body.len() {
1791 return Err(SseError::ChunkFrameTruncated { what: "chunk tag" });
1792 }
1793 let tag_off = cursor;
1794 let ct_off = tag_off + TAG_LEN;
1795 let is_last = i + 1 == hdr.chunk_count;
1796 let ct_len = if is_last {
1797 if ct_off > body.len() {
1798 return Err(SseError::ChunkFrameTruncated {
1799 what: "final chunk ciphertext",
1800 });
1801 }
1802 let remaining = body.len() - ct_off;
1803 if remaining > chunk_size {
1804 return Err(SseError::ChunkFrameTruncated {
1805 what: "trailing bytes after final chunk",
1806 });
1807 }
1808 remaining
1809 } else {
1810 chunk_size
1811 };
1812 let ct_end = ct_off + ct_len;
1813 if ct_end > body.len() {
1814 return Err(SseError::ChunkFrameTruncated {
1815 what: "chunk ciphertext",
1816 });
1817 }
1818 let mut tag = [0u8; TAG_LEN];
1819 tag.copy_from_slice(&body[tag_off..ct_off]);
1820 let ct = &body[ct_off..ct_end];
1821 let plain = decrypt_chunked_chunk(
1822 &cipher,
1823 i,
1824 hdr.chunk_count,
1825 hdr.key_id,
1826 &hdr.salt,
1827 &tag,
1828 ct,
1829 )?;
1830 crate::metrics::record_sse_streaming_chunk("decrypt");
1831 emit(plain)?;
1832 cursor = ct_end;
1833 }
1834 if cursor != body.len() {
1835 return Err(SseError::ChunkFrameTruncated {
1836 what: "trailing bytes after declared chunk_count",
1837 });
1838 }
1839 Ok(())
1840}
1841
1842fn decrypt_chunked_buffered(body: &[u8], keyring: &SseKeyring) -> Result<Bytes, SseError> {
1848 let hdr = parse_chunked_header(body)?;
1849 let mut out = Vec::with_capacity(hdr.chunk_size as usize * hdr.chunk_count as usize);
1850 walk_chunked(body, keyring, |chunk| {
1851 out.extend_from_slice(&chunk);
1852 Ok(())
1853 })?;
1854 Ok(Bytes::from(out))
1855}
1856
1857pub fn decrypt_chunked_stream(
1882 body: bytes::Bytes,
1883 keyring: &SseKeyring,
1884) -> impl futures::Stream<Item = Result<Bytes, SseError>> + 'static {
1885 use futures::stream::{self, StreamExt};
1886
1887 let prelude = (|| {
1894 let hdr = parse_chunked_header(&body)?;
1895 let key = keyring
1896 .get(hdr.key_id)
1897 .ok_or(SseError::KeyNotInKeyring { id: hdr.key_id })?;
1898 let cipher = Aes256Gcm::new(key.as_aes_key());
1899 Ok::<_, SseError>((hdr, cipher))
1900 })();
1901
1902 match prelude {
1903 Err(e) => stream::iter(std::iter::once(Err(e))).left_stream(),
1904 Ok((hdr, cipher)) => {
1905 let chunks_offset = hdr.chunks_offset;
1906 let state = ChunkedDecryptState {
1907 body,
1908 cipher,
1909 hdr,
1910 cursor: chunks_offset,
1911 next_index: 0,
1912 };
1913 stream::try_unfold(state, decrypt_next_chunk).right_stream()
1914 }
1915 }
1916}
1917
1918struct ChunkedDecryptState {
1922 body: bytes::Bytes,
1923 cipher: Aes256Gcm,
1924 hdr: ChunkedHeader,
1925 cursor: usize,
1926 next_index: u32,
1927}
1928
1929async fn decrypt_next_chunk(
1930 mut state: ChunkedDecryptState,
1931) -> Result<Option<(Bytes, ChunkedDecryptState)>, SseError> {
1932 if state.next_index >= state.hdr.chunk_count {
1933 if state.cursor != state.body.len() {
1936 return Err(SseError::ChunkFrameTruncated {
1937 what: "trailing bytes after declared chunk_count",
1938 });
1939 }
1940 return Ok(None);
1941 }
1942 let i = state.next_index;
1943 let chunk_size = state.hdr.chunk_size as usize;
1944 if state.cursor + TAG_LEN > state.body.len() {
1945 return Err(SseError::ChunkFrameTruncated { what: "chunk tag" });
1946 }
1947 let tag_off = state.cursor;
1948 let ct_off = tag_off + TAG_LEN;
1949 let is_last = i + 1 == state.hdr.chunk_count;
1950 let ct_len = if is_last {
1951 if ct_off > state.body.len() {
1952 return Err(SseError::ChunkFrameTruncated {
1953 what: "final chunk ciphertext",
1954 });
1955 }
1956 let remaining = state.body.len() - ct_off;
1957 if remaining > chunk_size {
1958 return Err(SseError::ChunkFrameTruncated {
1959 what: "trailing bytes after final chunk",
1960 });
1961 }
1962 remaining
1963 } else {
1964 chunk_size
1965 };
1966 let ct_end = ct_off + ct_len;
1967 if ct_end > state.body.len() {
1968 return Err(SseError::ChunkFrameTruncated {
1969 what: "chunk ciphertext",
1970 });
1971 }
1972 let mut tag = [0u8; TAG_LEN];
1973 tag.copy_from_slice(&state.body[tag_off..ct_off]);
1974 let ct = &state.body[ct_off..ct_end];
1975 let plain = decrypt_chunked_chunk(
1976 &state.cipher,
1977 i,
1978 state.hdr.chunk_count,
1979 state.hdr.key_id,
1980 &state.hdr.salt,
1981 &tag,
1982 ct,
1983 )?;
1984 crate::metrics::record_sse_streaming_chunk("decrypt");
1985 state.cursor = ct_end;
1986 state.next_index += 1;
1987 Ok(Some((plain, state)))
1988}
1989
1990#[cfg(test)]
1996fn encrypt_v2_chunked_s4e5_for_test(
1997 plaintext: &[u8],
1998 keyring: &SseKeyring,
1999 chunk_size: usize,
2000) -> Result<Bytes, SseError> {
2001 if chunk_size == 0 {
2002 return Err(SseError::ChunkSizeInvalid);
2003 }
2004 let (key_id, key) = keyring.active();
2005 let cipher = Aes256Gcm::new(key.as_aes_key());
2006 let mut salt = [0u8; 4];
2007 rand::rngs::OsRng.fill_bytes(&mut salt);
2008
2009 let chunk_count: u32 = if plaintext.is_empty() {
2010 1
2011 } else {
2012 plaintext
2013 .len()
2014 .div_ceil(chunk_size)
2015 .try_into()
2016 .expect("chunk_count overflows u32")
2017 };
2018
2019 let mut out = Vec::with_capacity(
2020 S4E5_HEADER_BYTES + plaintext.len() + (chunk_count as usize * S4E5_PER_CHUNK_OVERHEAD),
2021 );
2022 out.extend_from_slice(SSE_MAGIC_V5);
2023 out.push(ALGO_AES_256_GCM);
2024 out.extend_from_slice(&key_id.to_be_bytes());
2025 out.push(0u8);
2026 out.extend_from_slice(&(chunk_size as u32).to_be_bytes());
2027 out.extend_from_slice(&chunk_count.to_be_bytes());
2028 out.extend_from_slice(&salt);
2029
2030 for i in 0..chunk_count {
2031 let off = (i as usize).saturating_mul(chunk_size);
2032 let end = off.saturating_add(chunk_size).min(plaintext.len());
2033 let chunk_pt: &[u8] = if off >= plaintext.len() {
2034 &[]
2035 } else {
2036 &plaintext[off..end]
2037 };
2038 let nonce_bytes = nonce_v5(&salt, i);
2039 let nonce = Nonce::from_slice(&nonce_bytes);
2040 let aad = aad_v5(i, chunk_count, key_id, &salt);
2041 let ct_with_tag = cipher
2042 .encrypt(
2043 nonce,
2044 Payload {
2045 msg: chunk_pt,
2046 aad: &aad,
2047 },
2048 )
2049 .expect("aes-gcm encrypt cannot fail with a 32-byte key");
2050 let split = ct_with_tag.len() - TAG_LEN;
2051 let (ct, tag) = ct_with_tag.split_at(split);
2052 out.extend_from_slice(tag);
2053 out.extend_from_slice(ct);
2054 }
2055 Ok(Bytes::from(out))
2056}
2057
2058#[cfg(test)]
2059mod tests {
2060 use super::*;
2061
2062 fn key32(seed: u8) -> Arc<SseKey> {
2063 Arc::new(SseKey::from_bytes(&[seed; 32]).unwrap())
2064 }
2065
2066 fn keyring_single(seed: u8) -> SseKeyring {
2067 SseKeyring::new(1, key32(seed))
2068 }
2069
2070 #[test]
2071 fn roundtrip_basic_v1() {
2072 let k = SseKey::from_bytes(&[7u8; 32]).unwrap();
2074 let pt = b"the quick brown fox jumps over the lazy dog";
2075 let ct = encrypt(&k, pt);
2076 assert!(looks_encrypted(&ct));
2077 assert_eq!(&ct[..4], SSE_MAGIC_V1);
2078 assert_eq!(ct[4], ALGO_AES_256_GCM);
2079 assert_eq!(ct.len(), SSE_HEADER_BYTES + pt.len());
2080 let kr = SseKeyring::new(1, Arc::new(k));
2082 let pt2 = decrypt(&ct, &kr).unwrap();
2083 assert_eq!(pt2.as_ref(), pt);
2084 }
2085
2086 #[test]
2087 fn s4e2_roundtrip_active_key() {
2088 let kr = keyring_single(7);
2089 let pt = b"S4E2 active-key roundtrip";
2090 let ct = encrypt_v2(pt, &kr);
2091 assert_eq!(&ct[..4], SSE_MAGIC_V2);
2092 assert_eq!(ct[4], ALGO_AES_256_GCM);
2093 assert_eq!(u16::from_be_bytes([ct[5], ct[6]]), 1, "key_id BE");
2094 assert_eq!(ct[7], 0, "reserved byte");
2095 assert_eq!(ct.len(), SSE_HEADER_BYTES + pt.len());
2096 assert!(looks_encrypted(&ct));
2097 let pt2 = decrypt(&ct, &kr).unwrap();
2098 assert_eq!(pt2.as_ref(), pt);
2099 }
2100
2101 #[test]
2102 fn decrypt_s4e1_via_active_only_keyring() {
2103 let k_arc = key32(11);
2106 let legacy_ct = encrypt(&k_arc, b"v0.4 vintage object");
2107 assert_eq!(&legacy_ct[..4], SSE_MAGIC_V1);
2108 let kr = SseKeyring::new(1, Arc::clone(&k_arc));
2109 let plain = decrypt(&legacy_ct, &kr).unwrap();
2110 assert_eq!(plain.as_ref(), b"v0.4 vintage object");
2111 }
2112
2113 #[test]
2114 fn decrypt_s4e2_under_old_key_after_rotation() {
2115 let k1 = key32(1);
2119 let k2 = key32(2);
2120 let mut kr_old = SseKeyring::new(1, Arc::clone(&k1));
2121 let ct = encrypt_v2(b"old-rotation object", &kr_old);
2122 assert_eq!(u16::from_be_bytes([ct[5], ct[6]]), 1);
2123
2124 kr_old.add(2, Arc::clone(&k2));
2126 let mut kr_new = SseKeyring::new(2, Arc::clone(&k2));
2127 kr_new.add(1, Arc::clone(&k1));
2128
2129 let plain = decrypt(&ct, &kr_new).unwrap();
2130 assert_eq!(plain.as_ref(), b"old-rotation object");
2131
2132 let new_ct = encrypt_v2(b"new-rotation object", &kr_new);
2134 assert_eq!(u16::from_be_bytes([new_ct[5], new_ct[6]]), 2);
2135 let plain_new = decrypt(&new_ct, &kr_new).unwrap();
2136 assert_eq!(plain_new.as_ref(), b"new-rotation object");
2137 }
2138
2139 #[test]
2140 fn s4e2_unknown_key_id_errors() {
2141 let kr = keyring_single(3); let kr_other = SseKeyring::new(99, key32(3));
2143 let ct = encrypt_v2(b"x", &kr_other); let err = decrypt(&ct, &kr).unwrap_err();
2145 assert!(
2146 matches!(err, SseError::KeyNotInKeyring { id: 99 }),
2147 "got {err:?}"
2148 );
2149 }
2150
2151 #[test]
2152 fn s4e2_tampered_key_id_fails_auth() {
2153 let kr = SseKeyring::new(1, key32(4));
2154 let mut kr_with_2 = kr.clone();
2155 kr_with_2.add(2, key32(5)); let mut ct = encrypt_v2(b"do not flip my key id", &kr).to_vec();
2157 assert_eq!(u16::from_be_bytes([ct[5], ct[6]]), 1);
2161 ct[5] = 0;
2162 ct[6] = 2;
2163 let err = decrypt(&ct, &kr_with_2).unwrap_err();
2164 assert!(matches!(err, SseError::DecryptFailed), "got {err:?}");
2165 }
2166
2167 #[test]
2168 fn s4e2_tampered_ciphertext_fails() {
2169 let kr = SseKeyring::new(7, key32(9));
2170 let mut ct = encrypt_v2(b"secret message v2", &kr).to_vec();
2171 let last = ct.len() - 1;
2172 ct[last] ^= 0x01;
2173 let err = decrypt(&ct, &kr).unwrap_err();
2174 assert!(matches!(err, SseError::DecryptFailed));
2175 }
2176
2177 #[test]
2178 fn s4e2_tampered_algo_byte_fails() {
2179 let kr = SseKeyring::new(1, key32(2));
2180 let mut ct = encrypt_v2(b"hi", &kr).to_vec();
2181 ct[4] = 99;
2182 let err = decrypt(&ct, &kr).unwrap_err();
2183 assert!(matches!(err, SseError::UnsupportedAlgo { tag: 99 }));
2184 }
2185
2186 #[test]
2187 fn wrong_key_fails_v1_via_keyring() {
2188 let k1 = SseKey::from_bytes(&[1u8; 32]).unwrap();
2190 let ct = encrypt(&k1, b"secret");
2191 let kr_wrong = SseKeyring::new(1, Arc::new(SseKey::from_bytes(&[2u8; 32]).unwrap()));
2192 let err = decrypt(&ct, &kr_wrong).unwrap_err();
2193 assert!(matches!(err, SseError::DecryptFailed));
2194 }
2195
2196 #[test]
2197 fn rejects_short_body() {
2198 let kr = SseKeyring::new(1, key32(1));
2199 let err = decrypt(b"short", &kr).unwrap_err();
2200 assert!(matches!(err, SseError::TooShort { got: 5 }));
2201 }
2202
2203 #[test]
2204 fn looks_encrypted_passthrough_returns_false() {
2205 let f2 = b"S4F2\x01\x00\x00\x00........................................";
2207 assert!(!looks_encrypted(f2));
2208 assert!(!looks_encrypted(b""));
2209 }
2210
2211 #[test]
2212 fn looks_encrypted_detects_both_v1_and_v2() {
2213 let kr = SseKeyring::new(1, key32(8));
2214 let v1 = encrypt(&SseKey::from_bytes(&[8u8; 32]).unwrap(), b"x");
2215 let v2 = encrypt_v2(b"x", &kr);
2216 assert!(looks_encrypted(&v1));
2217 assert!(looks_encrypted(&v2));
2218 }
2219
2220 #[test]
2221 fn key_from_hex_string() {
2222 let bad =
2223 SseKey::from_bytes(b"0102030405060708090a0b0c0d0e0f10111213141516171819202122232425")
2224 .unwrap_err();
2225 assert!(matches!(bad, SseError::BadKeyLength { .. }));
2226 let good = b"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
2227 let _ = SseKey::from_bytes(good).expect("64-char hex should parse");
2228 }
2229
2230 #[test]
2231 fn encrypt_v2_uses_random_nonce() {
2232 let kr = SseKeyring::new(1, key32(3));
2233 let pt = b"deterministic input";
2234 let a = encrypt_v2(pt, &kr);
2235 let b = encrypt_v2(pt, &kr);
2236 assert_ne!(a, b, "nonce must be random per-call");
2237 }
2238
2239 #[test]
2240 fn keyring_active_and_get() {
2241 let k1 = key32(1);
2242 let k2 = key32(2);
2243 let mut kr = SseKeyring::new(1, Arc::clone(&k1));
2244 kr.add(2, Arc::clone(&k2));
2245 let (id, active) = kr.active();
2246 assert_eq!(id, 1);
2247 assert_eq!(active.bytes, [1u8; 32]);
2248 assert!(kr.get(2).is_some());
2249 assert!(kr.get(3).is_none());
2250 }
2251
2252 use base64::Engine as _;
2257
2258 fn cust_key(seed: u8) -> CustomerKeyMaterial {
2259 let key = [seed; KEY_LEN];
2260 let key_md5 = compute_key_md5(&key);
2261 CustomerKeyMaterial { key, key_md5 }
2262 }
2263
2264 #[test]
2265 fn s4e3_roundtrip_happy_path() {
2266 let m = cust_key(42);
2267 let pt = b"top-secret SSE-C payload";
2268 let ct = encrypt_with_source(
2269 pt,
2270 SseSource::CustomerKey {
2271 key: &m.key,
2272 key_md5: &m.key_md5,
2273 },
2274 );
2275 assert_eq!(&ct[..4], SSE_MAGIC_V3);
2277 assert_eq!(ct[4], ALGO_AES_256_GCM);
2278 assert_eq!(&ct[5..5 + KEY_MD5_LEN], &m.key_md5);
2279 assert_eq!(ct.len(), SSE_HEADER_BYTES_V3 + pt.len());
2280 assert!(looks_encrypted(&ct));
2281 let plain = decrypt(
2283 &ct,
2284 SseSource::CustomerKey {
2285 key: &m.key,
2286 key_md5: &m.key_md5,
2287 },
2288 )
2289 .unwrap();
2290 assert_eq!(plain.as_ref(), pt);
2291 let plain2 = decrypt(&ct, &m).unwrap();
2293 assert_eq!(plain2.as_ref(), pt);
2294 }
2295
2296 #[test]
2297 fn s4e3_wrong_key_yields_wrong_customer_key_error() {
2298 let m = cust_key(1);
2299 let other = cust_key(2);
2300 let ct = encrypt_with_source(b"payload", (&m).into());
2301 let err = decrypt(
2302 &ct,
2303 SseSource::CustomerKey {
2304 key: &other.key,
2305 key_md5: &other.key_md5,
2306 },
2307 )
2308 .unwrap_err();
2309 assert!(matches!(err, SseError::WrongCustomerKey), "got {err:?}");
2310 }
2311
2312 #[test]
2313 fn s4e3_tampered_stored_md5_is_caught() {
2314 let m = cust_key(7);
2321 let mut ct = encrypt_with_source(b"victim payload", (&m).into()).to_vec();
2322 ct[5] ^= 0x55;
2324 let err = decrypt(
2326 &ct,
2327 SseSource::CustomerKey {
2328 key: &m.key,
2329 key_md5: &m.key_md5,
2330 },
2331 )
2332 .unwrap_err();
2333 assert!(matches!(err, SseError::WrongCustomerKey), "got {err:?}");
2334 }
2335
2336 #[test]
2337 fn s4e3_tampered_md5_with_matching_supplied_md5_fails_aead() {
2338 let m = cust_key(3);
2342 let mut ct = encrypt_with_source(b"x", (&m).into()).to_vec();
2343 ct[5] ^= 0xFF;
2344 let mut bogus_md5 = m.key_md5;
2345 bogus_md5[0] ^= 0xFF;
2346 let err = decrypt(
2347 &ct,
2348 SseSource::CustomerKey {
2349 key: &m.key,
2350 key_md5: &bogus_md5,
2351 },
2352 )
2353 .unwrap_err();
2354 assert!(matches!(err, SseError::DecryptFailed), "got {err:?}");
2355 }
2356
2357 #[test]
2358 fn s4e3_tampered_ciphertext_fails_aead() {
2359 let m = cust_key(8);
2360 let mut ct = encrypt_with_source(b"sealed message", (&m).into()).to_vec();
2361 let last = ct.len() - 1;
2362 ct[last] ^= 0x01;
2363 let err = decrypt(&ct, &m).unwrap_err();
2364 assert!(matches!(err, SseError::DecryptFailed), "got {err:?}");
2365 }
2366
2367 #[test]
2368 fn s4e3_tampered_algo_byte_rejected() {
2369 let m = cust_key(9);
2370 let mut ct = encrypt_with_source(b"x", (&m).into()).to_vec();
2371 ct[4] = 99;
2372 let err = decrypt(&ct, &m).unwrap_err();
2373 assert!(matches!(err, SseError::UnsupportedAlgo { tag: 99 }));
2374 }
2375
2376 #[test]
2377 fn s4e3_uses_random_nonce() {
2378 let m = cust_key(10);
2379 let a = encrypt_with_source(b"deterministic input", (&m).into());
2380 let b = encrypt_with_source(b"deterministic input", (&m).into());
2381 assert_ne!(a, b, "nonce must be random per-call");
2382 }
2383
2384 #[test]
2385 fn parse_customer_key_headers_happy_path() {
2386 let key = [11u8; KEY_LEN];
2387 let md5 = compute_key_md5(&key);
2388 let key_b64 = base64::engine::general_purpose::STANDARD.encode(key);
2389 let md5_b64 = base64::engine::general_purpose::STANDARD.encode(md5);
2390 let m = parse_customer_key_headers("AES256", &key_b64, &md5_b64).unwrap();
2391 assert_eq!(m.key, key);
2392 assert_eq!(m.key_md5, md5);
2393 }
2394
2395 #[test]
2396 fn parse_customer_key_headers_rejects_wrong_algorithm() {
2397 let key = [1u8; KEY_LEN];
2398 let md5 = compute_key_md5(&key);
2399 let kb = base64::engine::general_purpose::STANDARD.encode(key);
2400 let mb = base64::engine::general_purpose::STANDARD.encode(md5);
2401 let err = parse_customer_key_headers("AES128", &kb, &mb).unwrap_err();
2402 assert!(
2403 matches!(err, SseError::CustomerKeyAlgorithmUnsupported { ref algo } if algo == "AES128"),
2404 "got {err:?}"
2405 );
2406 let err2 = parse_customer_key_headers("aes256", &kb, &mb).unwrap_err();
2408 assert!(
2409 matches!(err2, SseError::CustomerKeyAlgorithmUnsupported { .. }),
2410 "got {err2:?}"
2411 );
2412 }
2413
2414 #[test]
2415 fn parse_customer_key_headers_rejects_wrong_key_length() {
2416 let short_key = vec![5u8; 16]; let md5 = compute_key_md5(&short_key);
2418 let kb = base64::engine::general_purpose::STANDARD.encode(&short_key);
2419 let mb = base64::engine::general_purpose::STANDARD.encode(md5);
2420 let err = parse_customer_key_headers("AES256", &kb, &mb).unwrap_err();
2421 assert!(
2422 matches!(err, SseError::InvalidCustomerKey { reason } if reason.contains("key length")),
2423 "got {err:?}"
2424 );
2425 }
2426
2427 #[test]
2428 fn parse_customer_key_headers_rejects_wrong_md5_length() {
2429 let key = [3u8; KEY_LEN];
2430 let kb = base64::engine::general_purpose::STANDARD.encode(key);
2431 let bad_md5 = vec![0u8; 15];
2433 let mb = base64::engine::general_purpose::STANDARD.encode(bad_md5);
2434 let err = parse_customer_key_headers("AES256", &kb, &mb).unwrap_err();
2435 assert!(
2436 matches!(err, SseError::InvalidCustomerKey { reason } if reason.contains("MD5 length")),
2437 "got {err:?}"
2438 );
2439 }
2440
2441 #[test]
2442 fn parse_customer_key_headers_rejects_md5_mismatch() {
2443 let key = [4u8; KEY_LEN];
2444 let other = [5u8; KEY_LEN];
2445 let kb = base64::engine::general_purpose::STANDARD.encode(key);
2446 let wrong_md5 = compute_key_md5(&other);
2447 let mb = base64::engine::general_purpose::STANDARD.encode(wrong_md5);
2448 let err = parse_customer_key_headers("AES256", &kb, &mb).unwrap_err();
2449 assert!(
2450 matches!(err, SseError::InvalidCustomerKey { reason } if reason.contains("MD5 does not match")),
2451 "got {err:?}"
2452 );
2453 }
2454
2455 #[test]
2456 fn parse_customer_key_headers_rejects_bad_base64() {
2457 let valid_key = [0u8; KEY_LEN];
2458 let md5 = compute_key_md5(&valid_key);
2459 let mb = base64::engine::general_purpose::STANDARD.encode(md5);
2460 let err = parse_customer_key_headers("AES256", "!!!not-base64!!!", &mb).unwrap_err();
2461 assert!(
2462 matches!(err, SseError::InvalidCustomerKey { reason } if reason.contains("base64")),
2463 "got {err:?}"
2464 );
2465 let kb = base64::engine::general_purpose::STANDARD.encode(valid_key);
2467 let err2 = parse_customer_key_headers("AES256", &kb, "??not-base64??").unwrap_err();
2468 assert!(
2469 matches!(err2, SseError::InvalidCustomerKey { reason } if reason.contains("base64")),
2470 "got {err2:?}"
2471 );
2472 }
2473
2474 #[test]
2475 fn parse_customer_key_headers_trims_whitespace() {
2476 let key = [12u8; KEY_LEN];
2478 let md5 = compute_key_md5(&key);
2479 let kb = format!(
2480 " {}\n",
2481 base64::engine::general_purpose::STANDARD.encode(key)
2482 );
2483 let mb = format!(
2484 "\t{} ",
2485 base64::engine::general_purpose::STANDARD.encode(md5)
2486 );
2487 let m = parse_customer_key_headers("AES256", &kb, &mb).unwrap();
2488 assert_eq!(m.key, key);
2489 }
2490
2491 #[test]
2496 fn back_compat_decrypt_s4e1_with_keyring_source() {
2497 let k = key32(33);
2498 let legacy_ct = encrypt(&k, b"v0.4 vintage object");
2499 let kr = SseKeyring::new(1, Arc::clone(&k));
2500 let plain = decrypt(&legacy_ct, &kr).unwrap();
2503 assert_eq!(plain.as_ref(), b"v0.4 vintage object");
2504 let plain2 = decrypt(&legacy_ct, SseSource::Keyring(&kr)).unwrap();
2505 assert_eq!(plain2.as_ref(), b"v0.4 vintage object");
2506 }
2507
2508 #[test]
2509 fn back_compat_decrypt_s4e2_with_keyring_source() {
2510 let kr = keyring_single(34);
2511 let ct = encrypt_v2(b"v0.5 #29 object", &kr);
2512 let plain = decrypt(&ct, &kr).unwrap();
2513 assert_eq!(plain.as_ref(), b"v0.5 #29 object");
2514 let ct2 = encrypt_with_source(b"v0.5 #29 object", SseSource::Keyring(&kr));
2517 assert_eq!(&ct2[..4], SSE_MAGIC_V2);
2518 let plain2 = decrypt(&ct2, &kr).unwrap();
2519 assert_eq!(plain2.as_ref(), b"v0.5 #29 object");
2520 }
2521
2522 #[test]
2523 fn s4e2_blob_with_customer_key_source_is_rejected() {
2524 let kr = keyring_single(50);
2528 let ct = encrypt_v2(b"server-managed object", &kr);
2529 let m = cust_key(99);
2530 let err = decrypt(
2531 &ct,
2532 SseSource::CustomerKey {
2533 key: &m.key,
2534 key_md5: &m.key_md5,
2535 },
2536 )
2537 .unwrap_err();
2538 assert!(matches!(err, SseError::CustomerKeyUnexpected), "got {err:?}");
2539 }
2540
2541 #[test]
2542 fn s4e3_blob_with_keyring_source_is_rejected() {
2543 let m = cust_key(60);
2546 let ct = encrypt_with_source(b"customer-key object", (&m).into());
2547 let kr = keyring_single(60);
2548 let err = decrypt(&ct, &kr).unwrap_err();
2549 assert!(matches!(err, SseError::CustomerKeyRequired), "got {err:?}");
2550 }
2551
2552 #[test]
2553 fn looks_encrypted_detects_s4e3() {
2554 let m = cust_key(13);
2555 let ct = encrypt_with_source(b"x", (&m).into());
2556 assert!(looks_encrypted(&ct));
2557 }
2558
2559 #[test]
2560 fn s4e3_rejects_short_body() {
2561 let mut short = Vec::new();
2564 short.extend_from_slice(SSE_MAGIC_V3);
2565 short.push(ALGO_AES_256_GCM);
2566 short.extend_from_slice(&[0u8; SSE_HEADER_BYTES - 5]);
2569 assert_eq!(short.len(), SSE_HEADER_BYTES);
2570 let m = cust_key(1);
2571 let err = decrypt(
2572 &short,
2573 SseSource::CustomerKey {
2574 key: &m.key,
2575 key_md5: &m.key_md5,
2576 },
2577 )
2578 .unwrap_err();
2579 assert!(matches!(err, SseError::TooShort { .. }), "got {err:?}");
2580 }
2581
2582 #[test]
2583 fn customer_key_material_debug_redacts_key() {
2584 let m = cust_key(99);
2585 let s = format!("{m:?}");
2586 assert!(s.contains("redacted"));
2587 assert!(!s.contains(&format!("{:?}", m.key.as_slice())));
2588 }
2589
2590 #[test]
2591 fn constant_time_eq_basic() {
2592 assert!(constant_time_eq(b"abc", b"abc"));
2593 assert!(!constant_time_eq(b"abc", b"abd"));
2594 assert!(!constant_time_eq(b"abc", b"abcd"));
2595 assert!(constant_time_eq(b"", b""));
2596 }
2597
2598 #[test]
2599 fn compute_key_md5_known_vector() {
2600 let got = compute_key_md5(b"");
2602 let expected_hex = "d41d8cd98f00b204e9800998ecf8427e";
2603 assert_eq!(hex_lower(&got), expected_hex);
2604 }
2605
2606 use crate::kms::{KmsBackend, LocalKms};
2611 use std::collections::HashMap;
2612 use std::path::PathBuf;
2613
2614 fn local_kms_with(key_ids: &[(&str, [u8; 32])]) -> LocalKms {
2615 let mut keks: HashMap<String, [u8; 32]> = HashMap::new();
2616 for (id, k) in key_ids {
2617 keks.insert((*id).to_string(), *k);
2618 }
2619 LocalKms::from_keks(PathBuf::from("/tmp/none"), keks)
2620 }
2621
2622 #[tokio::test]
2623 async fn s4e4_roundtrip_via_local_kms() {
2624 let kms = local_kms_with(&[("alpha", [42u8; 32])]);
2625 let (dek_vec, wrapped) = kms.generate_dek("alpha").await.unwrap();
2626 let mut dek = [0u8; 32];
2627 dek.copy_from_slice(&dek_vec);
2628 let pt = b"SSE-KMS envelope payload across the S4E4 frame";
2629 let ct = encrypt_with_source(
2630 pt,
2631 SseSource::Kms {
2632 dek: &dek,
2633 wrapped: &wrapped,
2634 },
2635 );
2636 assert_eq!(&ct[..4], SSE_MAGIC_V4);
2638 assert_eq!(ct[4], ALGO_AES_256_GCM);
2639 let key_id_len = ct[5] as usize;
2640 assert_eq!(key_id_len, "alpha".len());
2641 assert_eq!(&ct[6..6 + key_id_len], b"alpha");
2642 assert!(looks_encrypted(&ct));
2644 assert_eq!(peek_magic(&ct), Some("S4E4"));
2645 let plain = decrypt_with_kms(&ct, &kms).await.unwrap();
2647 assert_eq!(plain.as_ref(), pt);
2648 }
2649
2650 #[tokio::test]
2651 async fn s4e4_tampered_key_id_fails_aead() {
2652 let kms = local_kms_with(&[("alpha", [1u8; 32]), ("beta", [2u8; 32])]);
2653 let (dek_vec, wrapped) = kms.generate_dek("alpha").await.unwrap();
2654 let mut dek = [0u8; 32];
2655 dek.copy_from_slice(&dek_vec);
2656 let mut ct = encrypt_with_source(
2657 b"do not redirect",
2658 SseSource::Kms {
2659 dek: &dek,
2660 wrapped: &wrapped,
2661 },
2662 )
2663 .to_vec();
2664 let key_id_off = 6;
2669 ct[key_id_off] = b'b';
2670 let err = decrypt_with_kms(&ct, &kms).await.unwrap_err();
2671 assert!(
2672 matches!(
2673 err,
2674 SseError::KmsBackend(crate::kms::KmsError::UnwrapFailed { .. })
2675 | SseError::KmsBackend(crate::kms::KmsError::KeyNotFound { .. })
2676 ),
2677 "got {err:?}"
2678 );
2679 }
2680
2681 #[tokio::test]
2682 async fn s4e4_tampered_key_id_to_real_other_id_still_fails() {
2683 let kms = local_kms_with(&[("alpha", [1u8; 32]), ("beta", [2u8; 32])]);
2689 let (dek_vec, wrapped) = kms.generate_dek("alpha").await.unwrap();
2690 let mut dek = [0u8; 32];
2691 dek.copy_from_slice(&dek_vec);
2692 let mut ct = encrypt_with_source(
2693 b"redirect attempt",
2694 SseSource::Kms {
2695 dek: &dek,
2696 wrapped: &wrapped,
2697 },
2698 )
2699 .to_vec();
2700 let key_id_off = 6;
2703 ct[key_id_off..key_id_off + 5].copy_from_slice(b"beta_");
2704 let err = decrypt_with_kms(&ct, &kms).await.unwrap_err();
2711 assert!(
2712 matches!(
2713 err,
2714 SseError::KmsBackend(crate::kms::KmsError::KeyNotFound { .. })
2715 ),
2716 "got {err:?}"
2717 );
2718 }
2719
2720 #[tokio::test]
2721 async fn s4e4_tampered_wrapped_dek_fails_unwrap() {
2722 let kms = local_kms_with(&[("k", [3u8; 32])]);
2723 let (dek_vec, wrapped) = kms.generate_dek("k").await.unwrap();
2724 let mut dek = [0u8; 32];
2725 dek.copy_from_slice(&dek_vec);
2726 let mut ct = encrypt_with_source(
2727 b"target body",
2728 SseSource::Kms {
2729 dek: &dek,
2730 wrapped: &wrapped,
2731 },
2732 )
2733 .to_vec();
2734 let key_id_len = ct[5] as usize;
2738 let wrapped_len_off = 6 + key_id_len;
2739 let wrapped_off = wrapped_len_off + 4;
2740 let mid = wrapped_off + (wrapped.ciphertext.len() / 2);
2741 ct[mid] ^= 0xFF;
2742 let err = decrypt_with_kms(&ct, &kms).await.unwrap_err();
2743 assert!(
2744 matches!(
2745 err,
2746 SseError::KmsBackend(crate::kms::KmsError::UnwrapFailed { .. })
2747 ),
2748 "got {err:?}"
2749 );
2750 }
2751
2752 #[tokio::test]
2753 async fn s4e4_tampered_ciphertext_fails_aead() {
2754 let kms = local_kms_with(&[("k", [4u8; 32])]);
2755 let (dek_vec, wrapped) = kms.generate_dek("k").await.unwrap();
2756 let mut dek = [0u8; 32];
2757 dek.copy_from_slice(&dek_vec);
2758 let mut ct = encrypt_with_source(
2759 b"sealed body",
2760 SseSource::Kms {
2761 dek: &dek,
2762 wrapped: &wrapped,
2763 },
2764 )
2765 .to_vec();
2766 let last = ct.len() - 1;
2767 ct[last] ^= 0x01;
2768 let err = decrypt_with_kms(&ct, &kms).await.unwrap_err();
2769 assert!(matches!(err, SseError::DecryptFailed), "got {err:?}");
2770 }
2771
2772 #[tokio::test]
2773 async fn s4e4_uses_random_nonce_and_dek_per_put() {
2774 let kms = local_kms_with(&[("k", [5u8; 32])]);
2775 let (dek1_vec, wrapped1) = kms.generate_dek("k").await.unwrap();
2778 let (dek2_vec, wrapped2) = kms.generate_dek("k").await.unwrap();
2779 let mut dek1 = [0u8; 32];
2780 dek1.copy_from_slice(&dek1_vec);
2781 let mut dek2 = [0u8; 32];
2782 dek2.copy_from_slice(&dek2_vec);
2783 let pt = b"deterministic input";
2784 let a = encrypt_with_source(
2785 pt,
2786 SseSource::Kms {
2787 dek: &dek1,
2788 wrapped: &wrapped1,
2789 },
2790 );
2791 let b = encrypt_with_source(
2792 pt,
2793 SseSource::Kms {
2794 dek: &dek2,
2795 wrapped: &wrapped2,
2796 },
2797 );
2798 assert_ne!(a, b);
2799 let plain_a = decrypt_with_kms(&a, &kms).await.unwrap();
2801 let plain_b = decrypt_with_kms(&b, &kms).await.unwrap();
2802 assert_eq!(plain_a.as_ref(), pt);
2803 assert_eq!(plain_b.as_ref(), pt);
2804 }
2805
2806 #[tokio::test]
2807 async fn s4e4_sync_decrypt_returns_kms_async_required() {
2808 let kms = local_kms_with(&[("k", [6u8; 32])]);
2813 let (dek_vec, wrapped) = kms.generate_dek("k").await.unwrap();
2814 let mut dek = [0u8; 32];
2815 dek.copy_from_slice(&dek_vec);
2816 let ct = encrypt_with_source(
2817 b"async only",
2818 SseSource::Kms {
2819 dek: &dek,
2820 wrapped: &wrapped,
2821 },
2822 );
2823 let kr = SseKeyring::new(1, key32(0));
2825 let err = decrypt(&ct, &kr).unwrap_err();
2826 assert!(matches!(err, SseError::KmsAsyncRequired), "got {err:?}");
2827 }
2828
2829 #[test]
2830 fn back_compat_s4e1_e2_e3_still_decrypt_via_sync() {
2831 let k = key32(7);
2834 let v1 = encrypt(&k, b"v0.4 vintage");
2835 let kr = SseKeyring::new(1, Arc::clone(&k));
2836 assert_eq!(decrypt(&v1, &kr).unwrap().as_ref(), b"v0.4 vintage");
2837
2838 let v2 = encrypt_v2(b"v0.5 #29 vintage", &kr);
2839 assert_eq!(
2840 decrypt(&v2, &kr).unwrap().as_ref(),
2841 b"v0.5 #29 vintage"
2842 );
2843
2844 let m = cust_key(7);
2845 let v3 = encrypt_with_source(b"v0.5 #27 vintage", (&m).into());
2846 assert_eq!(
2847 decrypt(&v3, &m).unwrap().as_ref(),
2848 b"v0.5 #27 vintage"
2849 );
2850 }
2851
2852 #[test]
2853 fn peek_magic_distinguishes_all_variants() {
2854 let k = key32(9);
2857 let v1 = encrypt(&k, b"x");
2858 assert_eq!(peek_magic(&v1), Some("S4E1"));
2859 let kr = SseKeyring::new(1, Arc::clone(&k));
2860 let v2 = encrypt_v2(b"x", &kr);
2861 assert_eq!(peek_magic(&v2), Some("S4E2"));
2862 let m = cust_key(9);
2863 let v3 = encrypt_with_source(b"x", (&m).into());
2864 assert_eq!(peek_magic(&v3), Some("S4E3"));
2865 let mut v4 = Vec::new();
2870 v4.extend_from_slice(SSE_MAGIC_V4);
2871 v4.extend_from_slice(&[0u8; 40]);
2872 assert_eq!(peek_magic(&v4), Some("S4E4"));
2873 assert!(peek_magic(b"NOPE").is_none());
2875 assert!(peek_magic(b"short").is_none());
2876 assert!(peek_magic(&[0u8; 100]).is_none());
2877 }
2878
2879 #[tokio::test]
2880 async fn s4e4_truncated_frame_errors_cleanly() {
2881 let truncated = b"S4E4\x01\x05hi";
2884 let kms = local_kms_with(&[("k", [1u8; 32])]);
2885 let err = decrypt_with_kms(truncated, &kms).await.unwrap_err();
2886 assert!(
2887 matches!(err, SseError::KmsFrameTooShort { .. }),
2888 "got {err:?}"
2889 );
2890 }
2891
2892 #[tokio::test]
2893 async fn s4e4_oob_key_id_len_errors() {
2894 let mut body = Vec::new();
2898 body.extend_from_slice(SSE_MAGIC_V4);
2899 body.push(ALGO_AES_256_GCM);
2900 body.push(200u8); body.extend_from_slice(&[0u8; 50]);
2905 let kms = local_kms_with(&[("k", [1u8; 32])]);
2906 let err = decrypt_with_kms(&body, &kms).await.unwrap_err();
2907 assert!(
2908 matches!(err, SseError::KmsFrameFieldOob { .. }),
2909 "got {err:?}"
2910 );
2911 }
2912
2913 #[tokio::test]
2914 async fn s4e4_via_keyring_source_into_sync_decrypt_is_kms_async_required() {
2915 let kms = local_kms_with(&[("k", [9u8; 32])]);
2921 let (dek_vec, wrapped) = kms.generate_dek("k").await.unwrap();
2922 let mut dek = [0u8; 32];
2923 dek.copy_from_slice(&dek_vec);
2924 let ct = encrypt_with_source(
2925 b"x",
2926 SseSource::Kms {
2927 dek: &dek,
2928 wrapped: &wrapped,
2929 },
2930 );
2931 let m = cust_key(1);
2932 let err = decrypt(&ct, &m).unwrap_err();
2933 assert!(matches!(err, SseError::KmsAsyncRequired), "got {err:?}");
2934 }
2935
2936 #[tokio::test]
2937 async fn s4e4_looks_encrypted_passthrough_returns_false_for_synthetic() {
2938 let mut not_s4e4 = Vec::new();
2940 not_s4e4.extend_from_slice(b"S4F4");
2941 not_s4e4.extend_from_slice(&[0u8; 60]);
2942 assert!(!looks_encrypted(¬_s4e4));
2943 assert_eq!(peek_magic(¬_s4e4), None);
2944 }
2945
2946 #[tokio::test]
2947 async fn s4e4_aad_length_prefix_prevents_byte_shifting() {
2948 let kms = local_kms_with(&[("kk", [11u8; 32])]);
2955 let (dek_vec, wrapped) = kms.generate_dek("kk").await.unwrap();
2956 let mut dek = [0u8; 32];
2957 dek.copy_from_slice(&dek_vec);
2958 let mut ct = encrypt_with_source(
2959 b"length-shift defense",
2960 SseSource::Kms {
2961 dek: &dek,
2962 wrapped: &wrapped,
2963 },
2964 )
2965 .to_vec();
2966 let key_id_len = ct[5] as usize;
2967 let wrapped_len_off = 6 + key_id_len;
2968 let original_len = u32::from_be_bytes([
2974 ct[wrapped_len_off],
2975 ct[wrapped_len_off + 1],
2976 ct[wrapped_len_off + 2],
2977 ct[wrapped_len_off + 3],
2978 ]);
2979 let new_len = (original_len - 1).to_be_bytes();
2980 ct[wrapped_len_off..wrapped_len_off + 4].copy_from_slice(&new_len);
2981 let err = decrypt_with_kms(&ct, &kms).await.unwrap_err();
2982 assert!(
2985 matches!(
2986 err,
2987 SseError::KmsBackend(_)
2988 | SseError::DecryptFailed
2989 | SseError::KmsFrameFieldOob { .. }
2990 | SseError::KmsFrameTooShort { .. }
2991 ),
2992 "got {err:?}"
2993 );
2994 }
2995
2996 use futures::StreamExt;
3001
3002 async fn collect_chunks(
3005 s: impl futures::Stream<Item = Result<Bytes, SseError>>,
3006 ) -> Result<Vec<Bytes>, SseError> {
3007 let mut out = Vec::new();
3008 let mut s = std::pin::pin!(s);
3009 while let Some(item) = s.next().await {
3010 out.push(item?);
3011 }
3012 Ok(out)
3013 }
3014
3015 #[test]
3016 fn s4e6_encrypt_layout_10mb_at_1mib() {
3017 let kr = keyring_single(0x42);
3022 let chunk_size = 1024 * 1024;
3023 let pt_len = 10 * 1024 * 1024;
3024 let pt = vec![0xAB_u8; pt_len];
3025 let ct = encrypt_v2_chunked(&pt, &kr, chunk_size).expect("encrypt ok");
3026 assert_eq!(&ct[..4], SSE_MAGIC_V6, "new PUTs emit S4E6 (v0.8.1 #57)");
3027 assert_eq!(ct[4], ALGO_AES_256_GCM);
3028 assert_eq!(u16::from_be_bytes([ct[5], ct[6]]), 1, "key_id BE = active id");
3029 assert_eq!(ct[7], 0, "reserved must be 0");
3030 assert_eq!(
3031 u32::from_be_bytes([ct[8], ct[9], ct[10], ct[11]]),
3032 chunk_size as u32,
3033 "chunk_size BE",
3034 );
3035 assert_eq!(
3036 u32::from_be_bytes([ct[12], ct[13], ct[14], ct[15]]),
3037 10,
3038 "chunk_count BE — 10 MiB / 1 MiB = 10 (no remainder)",
3039 );
3040 assert_eq!(&ct[16..24].len(), &8, "S4E6 salt slot is 8 bytes");
3044 assert_ne!(&ct[16..24], &[0u8; 8], "S4E6 salt must be random, not zeros");
3045 assert_eq!(
3046 ct.len(),
3047 S4E6_HEADER_BYTES + 10 * S4E6_PER_CHUNK_OVERHEAD + pt_len,
3048 "total = header (24) + 10 tags + plaintext",
3049 );
3050 assert!(looks_encrypted(&ct), "looks_encrypted must accept S4E6");
3051 assert_eq!(peek_magic(&ct), Some("S4E6"));
3052 }
3053
3054 #[tokio::test]
3055 async fn s4e6_decrypt_chunked_stream_byte_equal() {
3056 let kr = keyring_single(0x55);
3059 let pt: Vec<u8> = (0..(10 * 1024 * 1024_u32)).map(|i| (i & 0xFF) as u8).collect();
3060 let ct = encrypt_v2_chunked(&pt, &kr, 1024 * 1024).unwrap();
3061 assert_eq!(&ct[..4], SSE_MAGIC_V6, "new emit is S4E6");
3063 let stream = decrypt_chunked_stream(ct, &kr);
3064 let chunks = collect_chunks(stream).await.expect("stream ok");
3065 assert_eq!(chunks.len(), 10, "10 chunks expected for 10 MiB / 1 MiB");
3066 let mut joined = Vec::with_capacity(pt.len());
3067 for c in chunks {
3068 joined.extend_from_slice(&c);
3069 }
3070 assert_eq!(joined.len(), pt.len(), "byte length matches");
3071 assert_eq!(joined, pt, "byte-equal round-trip");
3072 }
3073
3074 #[tokio::test]
3075 async fn s4e6_single_chunk_for_small_object() {
3076 let kr = keyring_single(0x77);
3080 let pt = b"tiny payload, smaller than chunk_size";
3081 let ct = encrypt_v2_chunked(pt, &kr, 1024 * 1024).unwrap();
3082 assert_eq!(
3083 u32::from_be_bytes([ct[12], ct[13], ct[14], ct[15]]),
3084 1,
3085 "small plaintext = single chunk",
3086 );
3087 let stream = decrypt_chunked_stream(ct, &kr);
3088 let chunks = collect_chunks(stream).await.expect("stream ok");
3089 assert_eq!(chunks.len(), 1);
3090 assert_eq!(chunks[0].as_ref(), pt);
3091 }
3092
3093 #[tokio::test]
3094 async fn s4e6_tampered_chunk_n_reports_chunk_index() {
3095 let kr = keyring_single(0x91);
3100 let chunk_size = 1024;
3101 let pt = vec![0xCD_u8; chunk_size * 8]; let mut ct = encrypt_v2_chunked(&pt, &kr, chunk_size).unwrap().to_vec();
3103 let target = S4E6_HEADER_BYTES + 3 * (TAG_LEN + chunk_size) + TAG_LEN;
3106 ct[target] ^= 0x42;
3107 let stream = decrypt_chunked_stream(bytes::Bytes::from(ct), &kr);
3108 let mut s = std::pin::pin!(stream);
3109 for expected_i in 0..3_u32 {
3111 let item = s.next().await.expect("yield");
3112 item.unwrap_or_else(|e| panic!("chunk {expected_i}: {e:?}"));
3113 }
3114 let err = s.next().await.expect("yield error").unwrap_err();
3116 assert!(
3117 matches!(err, SseError::ChunkAuthFailed { chunk_index: 3 }),
3118 "got {err:?}",
3119 );
3120 }
3121
3122 #[tokio::test]
3123 async fn s4e5_back_compat_s4e2_blob_rejected_with_clear_error() {
3124 let kr = keyring_single(0x12);
3128 let s4e2 = encrypt_v2(b"a v2 blob, not chunked", &kr);
3129 let stream = decrypt_chunked_stream(s4e2, &kr);
3130 let result = collect_chunks(stream).await;
3131 let err = result.unwrap_err();
3132 assert!(matches!(err, SseError::BadMagic { .. }), "got {err:?}");
3133 }
3134
3135 #[test]
3136 fn s4e6_salt_randomness_smoke() {
3137 let kr = keyring_single(0x33);
3144 let mut salts = std::collections::HashSet::new();
3145 let n = 1024;
3146 for _ in 0..n {
3147 let ct = encrypt_v2_chunked(b"x", &kr, 64).unwrap();
3148 let mut salt = [0u8; 8];
3149 salt.copy_from_slice(&ct[16..24]);
3150 salts.insert(salt);
3151 }
3152 assert!(
3153 salts.len() > n / 2,
3154 "expected most of the {n} salts to be unique (got {} unique)",
3155 salts.len(),
3156 );
3157 }
3158
3159 #[test]
3160 fn s4e6_chunk_size_zero_invalid() {
3161 let kr = keyring_single(0x66);
3162 let err = encrypt_v2_chunked(b"hi", &kr, 0).unwrap_err();
3163 assert!(matches!(err, SseError::ChunkSizeInvalid));
3164 }
3165
3166 #[tokio::test]
3167 async fn s4e6_truncated_body_reports_frame_truncated() {
3168 let kr = keyring_single(0xA1);
3171 let chunk_size = 256;
3172 let pt = vec![0u8; chunk_size * 4];
3173 let ct = encrypt_v2_chunked(&pt, &kr, chunk_size).unwrap();
3174 let trunc = S4E6_HEADER_BYTES + 2 * (TAG_LEN + chunk_size) + 8;
3177 let truncated = bytes::Bytes::copy_from_slice(&ct[..trunc]);
3178 let stream = decrypt_chunked_stream(truncated, &kr);
3179 let result = collect_chunks(stream).await;
3180 let err = result.unwrap_err();
3181 assert!(
3182 matches!(err, SseError::ChunkFrameTruncated { .. }),
3183 "got {err:?}",
3184 );
3185 }
3186
3187 #[test]
3188 fn s4e6_decrypt_buffered_round_trip_via_top_level_decrypt() {
3189 let kr = keyring_single(0xDE);
3193 let pt = b"buffered sync decrypt path".repeat(32);
3194 let ct = encrypt_v2_chunked(&pt, &kr, 13).unwrap();
3195 let plain = decrypt(&ct, &kr).expect("buffered S4E6 decrypt ok");
3196 assert_eq!(plain.as_ref(), pt.as_slice());
3197 }
3198
3199 #[tokio::test]
3200 async fn s4e6_unknown_key_id_in_frame_errors() {
3201 let kr_put = SseKeyring::new(7, key32(0xCC));
3203 let kr_get = keyring_single(0xCC); let ct = encrypt_v2_chunked(b"orphan key", &kr_put, 64).unwrap();
3205 let err = decrypt(&ct, &kr_get).unwrap_err();
3207 assert!(matches!(err, SseError::KeyNotInKeyring { id: 7 }), "got {err:?}");
3208 let stream = decrypt_chunked_stream(ct, &kr_get);
3210 let result = collect_chunks(stream).await;
3211 assert!(
3212 matches!(result, Err(SseError::KeyNotInKeyring { id: 7 })),
3213 "got {result:?}",
3214 );
3215 }
3216
3217 #[tokio::test]
3218 async fn s4e6_final_chunk_smaller_than_chunk_size() {
3219 let kr = keyring_single(0xEF);
3222 let chunk_size = 100;
3223 let pt: Vec<u8> = (0..250_u32).map(|i| i as u8).collect();
3224 let ct = encrypt_v2_chunked(&pt, &kr, chunk_size).unwrap();
3225 assert_eq!(
3226 u32::from_be_bytes([ct[12], ct[13], ct[14], ct[15]]),
3227 3,
3228 "ceil(250/100) = 3 chunks",
3229 );
3230 assert_eq!(ct.len(), S4E6_HEADER_BYTES + 48 + 250);
3232 let stream = decrypt_chunked_stream(ct, &kr);
3233 let chunks = collect_chunks(stream).await.expect("stream ok");
3234 assert_eq!(chunks.len(), 3);
3235 assert_eq!(chunks[0].len(), 100);
3236 assert_eq!(chunks[1].len(), 100);
3237 assert_eq!(chunks[2].len(), 50, "final chunk is the remainder");
3238 let joined: Vec<u8> = chunks.iter().flat_map(|c| c.iter().copied()).collect();
3239 assert_eq!(joined, pt);
3240 }
3241
3242 #[test]
3251 fn s4e6_back_compat_read_s4e5_blob() {
3252 let kr = keyring_single(0x57);
3258 let pt = b"v0.8.0 vintage chunked SSE-S4 object".repeat(64);
3259 let s4e5 = encrypt_v2_chunked_s4e5_for_test(&pt, &kr, 91).unwrap();
3260 assert_eq!(&s4e5[..4], SSE_MAGIC_V5, "fixture must be S4E5");
3262 assert_eq!(peek_magic(&s4e5), Some("S4E5"));
3263 let plain_sync = decrypt(&s4e5, &kr).expect("sync S4E5 decrypt ok");
3265 assert_eq!(plain_sync.as_ref(), pt.as_slice());
3266 let collected = futures::executor::block_on(async {
3268 let stream = decrypt_chunked_stream(s4e5.clone(), &kr);
3269 collect_chunks(stream).await
3270 })
3271 .expect("stream S4E5 decrypt ok");
3272 let mut joined = Vec::with_capacity(pt.len());
3273 for c in collected {
3274 joined.extend_from_slice(&c);
3275 }
3276 assert_eq!(joined, pt, "S4E5 streaming round-trip byte-equal");
3277 }
3278
3279 #[test]
3280 fn s4e6_layout_24_bytes_header() {
3281 assert_eq!(S4E6_HEADER_BYTES, 24);
3285 assert_eq!(S4E6_PER_CHUNK_OVERHEAD, TAG_LEN);
3286 assert_eq!(S4E6_HEADER_BYTES, S4E5_HEADER_BYTES + 4);
3287 }
3288
3289 #[test]
3290 fn s4e6_parse_header_round_trip() {
3291 let kr = keyring_single(0xAB);
3295 let chunk_size = 256;
3296 let pt = vec![1u8; 7 * chunk_size];
3297 let ct = encrypt_v2_chunked(&pt, &kr, chunk_size).unwrap();
3298 let hdr = parse_s4e6_header(&ct).expect("parse ok");
3299 assert_eq!(hdr.key_id, 1);
3300 assert_eq!(hdr.chunk_size, chunk_size as u32);
3301 assert_eq!(hdr.chunk_count, 7);
3302 assert_eq!(hdr.salt.len(), 8);
3303 let bogus = b"S4E2\x01\x00\x00\x00........................";
3305 let err = parse_s4e6_header(bogus).unwrap_err();
3306 assert!(matches!(err, SseError::BadMagic { .. }), "got {err:?}");
3307 let err2 = parse_s4e6_header(&ct[..10]).unwrap_err();
3309 assert!(matches!(err2, SseError::ChunkFrameTruncated { .. }), "got {err2:?}");
3310 }
3311
3312 #[test]
3313 fn s4e6_salt_uniqueness_smoke_16m() {
3314 let kr = keyring_single(0xA6);
3331 let mut salts = std::collections::HashSet::with_capacity(16384);
3332 let n = 16384_usize;
3333 let mut collisions_top4 = 0usize;
3334 let mut top4_seen = std::collections::HashSet::with_capacity(16384);
3335 for _ in 0..n {
3336 let ct = encrypt_v2_chunked(b"x", &kr, 64).unwrap();
3337 let mut salt = [0u8; 8];
3338 salt.copy_from_slice(&ct[16..24]);
3339 salts.insert(salt);
3340 let mut top4 = [0u8; 4];
3350 top4.copy_from_slice(&salt[..4]);
3351 if !top4_seen.insert(top4) {
3352 collisions_top4 += 1;
3353 }
3354 }
3355 assert_eq!(
3356 salts.len(),
3357 n,
3358 "all 8-byte salts must be unique across {n} PUTs (got {} unique)",
3359 salts.len(),
3360 );
3361 eprintln!(
3368 "s4e6_salt_uniqueness_smoke_16m: 16k PUTs, full 8B salts \
3369 all unique ({}/{}), simulated 4B-truncated salt yielded \
3370 {} collisions (this is what S4E5 would have shipped)",
3371 salts.len(),
3372 n,
3373 collisions_top4,
3374 );
3375 }
3379
3380 #[test]
3381 fn s4e6_max_chunks_24bit() {
3382 assert_eq!(S4E6_MAX_CHUNK_COUNT, (1u32 << 24) - 1);
3391 assert_eq!(S4E6_MAX_CHUNK_COUNT, 16_777_215);
3392
3393 let kr = keyring_single(0xC4);
3397 let pt = vec![0u8; (S4E6_MAX_CHUNK_COUNT as usize) + 1]; let err = encrypt_v2_chunked(&pt, &kr, 1).unwrap_err();
3399 assert!(
3400 matches!(
3401 err,
3402 SseError::ChunkCountTooLarge {
3403 got: 16_777_216,
3404 max: 16_777_215
3405 }
3406 ),
3407 "got {err:?}",
3408 );
3409
3410 let pt_ok = vec![0u8; 1023];
3419 let ct = encrypt_v2_chunked(&pt_ok, &kr, 1).expect("under-cap PUT must succeed");
3420 let hdr = parse_s4e6_header(&ct).unwrap();
3421 assert_eq!(hdr.chunk_count, 1023);
3422
3423 let mut tampered = ct.to_vec();
3427 let bad = (S4E6_MAX_CHUNK_COUNT + 1).to_be_bytes();
3429 tampered[12..16].copy_from_slice(&bad);
3430 let err2 = parse_s4e6_header(&tampered).unwrap_err();
3431 assert!(
3432 matches!(
3433 err2,
3434 SseError::ChunkCountTooLarge { got: 16_777_216, max: 16_777_215 }
3435 ),
3436 "got {err2:?}",
3437 );
3438 }
3439
3440 #[test]
3441 fn s4e6_nonce_v6_layout() {
3442 let salt = [0xAA_u8; 8];
3446 let n0 = nonce_v6(&salt, 0);
3447 assert_eq!(n0[0], b'E');
3448 assert_eq!(&n0[1..9], &salt);
3449 assert_eq!(&n0[9..12], &[0, 0, 0]);
3450 let n1 = nonce_v6(&salt, 1);
3451 assert_eq!(&n1[9..12], &[0, 0, 1]);
3452 let n_mid = nonce_v6(&salt, 0x123456);
3453 assert_eq!(&n_mid[9..12], &[0x12, 0x34, 0x56]);
3454 let n_max = nonce_v6(&salt, S4E6_MAX_CHUNK_COUNT);
3455 assert_eq!(&n_max[9..12], &[0xFF, 0xFF, 0xFF]);
3456 }
3457
3458 #[tokio::test]
3459 async fn s4e6_tampered_salt_byte_fails_aead() {
3460 let kr = keyring_single(0xB6);
3465 let pt = b"salt-in-aad coverage".repeat(64);
3466 let mut ct = encrypt_v2_chunked(&pt, &kr, 128).unwrap().to_vec();
3467 ct[20] ^= 0x01;
3469 let err = decrypt(&ct, &kr).unwrap_err();
3470 assert!(
3471 matches!(err, SseError::ChunkAuthFailed { chunk_index: 0 }),
3472 "got {err:?}",
3473 );
3474 }
3475}