1use aes_gcm::aead::{Aead, KeyInit};
2use aes_gcm::{Aes256Gcm, Nonce};
3use base64::prelude::*;
4use chrono::{DateTime, TimeZone, Utc};
5use hmac::{Hmac, Mac};
6use rand::random;
7use sha2::Sha256;
8
9const SEPARATOR: &str = "--";
10const META_PREFIX: &[u8] = b"RRS1";
11const FLAG_PURPOSE: u8 = 0b0000_0001;
12const FLAG_EXPIRES_AT: u8 = 0b0000_0010;
13const NONCE_LENGTH: usize = 12;
14
15type HmacSha256 = Hmac<Sha256>;
16
17struct DecodedPayload {
18 data: Vec<u8>,
19 purpose: Option<String>,
20 expires_at: Option<DateTime<Utc>>,
21}
22
23#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
25pub enum VerifierError {
26 #[error("invalid signature")]
28 InvalidSignature,
29 #[error("message expired")]
31 Expired,
32 #[error("purpose mismatch")]
34 PurposeMismatch,
35 #[error("encoding error: {0}")]
37 Encoding(String),
38}
39
40#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
42pub enum EncryptorError {
43 #[error("decryption failed")]
45 DecryptionFailed,
46 #[error("invalid key length")]
48 InvalidKeyLength,
49 #[error("message expired")]
51 Expired,
52 #[error("purpose mismatch")]
54 PurposeMismatch,
55 #[error("encoding error: {0}")]
57 Encoding(String),
58}
59
60pub struct MessageVerifier {
62 secret: Vec<u8>,
63}
64
65impl MessageVerifier {
66 pub fn new(secret: &[u8]) -> Self {
68 Self {
69 secret: secret.to_vec(),
70 }
71 }
72
73 pub fn generate(&self, data: &[u8]) -> String {
75 self.generate_signed_payload(encode_metadata(data, None, None))
76 }
77
78 pub fn generate_with_purpose(
80 &self,
81 data: &[u8],
82 purpose: &str,
83 expires_at: Option<DateTime<Utc>>,
84 ) -> String {
85 let payload = encode_metadata(data, Some(purpose), expires_at);
86 self.generate_signed_payload(payload)
87 }
88
89 pub fn verify(&self, signed_message: &str) -> Result<Vec<u8>, VerifierError> {
91 self.verify_internal(signed_message, None)
92 }
93
94 pub fn verify_with_purpose(
96 &self,
97 signed_message: &str,
98 purpose: &str,
99 ) -> Result<Vec<u8>, VerifierError> {
100 self.verify_internal(signed_message, Some(purpose))
101 }
102
103 pub fn valid_message(&self, signed_message: &str) -> bool {
105 let Some((encoded_payload, encoded_signature)) = split_signed_message(signed_message)
106 else {
107 return false;
108 };
109
110 verify_hmac(&self.secret, encoded_payload, encoded_signature).is_ok()
111 && BASE64_STANDARD.decode(encoded_payload).is_ok()
112 }
113
114 fn generate_signed_payload(&self, payload: Vec<u8>) -> String {
115 let encoded_payload = BASE64_STANDARD.encode(payload);
116 let signature = sign_hmac(&self.secret, encoded_payload.as_bytes());
117 format!("{encoded_payload}{SEPARATOR}{signature}")
118 }
119
120 fn verify_internal(
121 &self,
122 signed_message: &str,
123 expected_purpose: Option<&str>,
124 ) -> Result<Vec<u8>, VerifierError> {
125 let (encoded_payload, encoded_signature) =
126 split_signed_message(signed_message).ok_or(VerifierError::InvalidSignature)?;
127 verify_hmac(&self.secret, encoded_payload, encoded_signature)?;
128
129 let payload = BASE64_STANDARD
130 .decode(encoded_payload)
131 .map_err(|_| VerifierError::InvalidSignature)?;
132 let decoded = parse_payload(&payload).map_err(VerifierError::Encoding)?;
133 validate_payload(decoded, expected_purpose)
134 }
135}
136
137pub struct MessageEncryptor {
139 secret: Vec<u8>,
140 sign_secret: Option<Vec<u8>>,
141}
142
143impl MessageEncryptor {
144 pub fn new(secret: &[u8]) -> Result<Self, EncryptorError> {
146 if secret.len() != 32 {
147 return Err(EncryptorError::InvalidKeyLength);
148 }
149
150 Ok(Self {
151 secret: secret.to_vec(),
152 sign_secret: None,
153 })
154 }
155
156 pub fn encrypt_and_sign(&self, data: &[u8]) -> Result<String, EncryptorError> {
158 self.encrypt_payload(encode_metadata(data, None, None))
159 }
160
161 pub fn encrypt_and_sign_with_purpose(
163 &self,
164 data: &[u8],
165 purpose: &str,
166 expires_at: Option<DateTime<Utc>>,
167 ) -> Result<String, EncryptorError> {
168 self.encrypt_payload(encode_metadata(data, Some(purpose), expires_at))
169 }
170
171 pub fn decrypt_and_verify(&self, encrypted_message: &str) -> Result<Vec<u8>, EncryptorError> {
173 self.decrypt_internal(encrypted_message, None)
174 }
175
176 pub fn decrypt_and_verify_with_purpose(
178 &self,
179 encrypted_message: &str,
180 purpose: &str,
181 ) -> Result<Vec<u8>, EncryptorError> {
182 self.decrypt_internal(encrypted_message, Some(purpose))
183 }
184
185 fn encrypt_payload(&self, payload: Vec<u8>) -> Result<String, EncryptorError> {
186 let cipher = Aes256Gcm::new_from_slice(&self.secret)
187 .map_err(|_| EncryptorError::InvalidKeyLength)?;
188 let nonce: [u8; NONCE_LENGTH] = random();
189
190 let ciphertext = cipher
191 .encrypt(Nonce::from_slice(&nonce), payload.as_ref())
192 .map_err(|_| EncryptorError::Encoding("encryption failed".to_owned()))?;
193
194 let encoded_nonce = BASE64_STANDARD.encode(nonce);
195 let encoded_ciphertext = BASE64_STANDARD.encode(ciphertext);
196 Ok(format!("{encoded_nonce}{SEPARATOR}{encoded_ciphertext}"))
197 }
198
199 fn decrypt_internal(
200 &self,
201 encrypted_message: &str,
202 expected_purpose: Option<&str>,
203 ) -> Result<Vec<u8>, EncryptorError> {
204 let (encoded_nonce, encoded_ciphertext) = split_signed_message(encrypted_message)
205 .ok_or_else(|| {
206 EncryptorError::Encoding("invalid encrypted message format".to_owned())
207 })?;
208
209 let nonce = BASE64_STANDARD
210 .decode(encoded_nonce)
211 .map_err(|_| EncryptorError::Encoding("invalid nonce encoding".to_owned()))?;
212 if nonce.len() != NONCE_LENGTH {
213 return Err(EncryptorError::Encoding("invalid nonce length".to_owned()));
214 }
215
216 let ciphertext = BASE64_STANDARD
217 .decode(encoded_ciphertext)
218 .map_err(|_| EncryptorError::Encoding("invalid ciphertext encoding".to_owned()))?;
219
220 let cipher = Aes256Gcm::new_from_slice(&self.secret)
221 .map_err(|_| EncryptorError::InvalidKeyLength)?;
222 let plaintext = cipher
223 .decrypt(Nonce::from_slice(&nonce), ciphertext.as_ref())
224 .map_err(|_| EncryptorError::DecryptionFailed)?;
225
226 let decoded = parse_payload(&plaintext).map_err(EncryptorError::Encoding)?;
227 validate_payload_encryptor(decoded, expected_purpose)
228 }
229
230 #[cfg(test)]
231 fn sign_secret(&self) -> Option<&[u8]> {
232 self.sign_secret.as_deref()
233 }
234}
235
236pub struct RotatingVerifier {
238 verifiers: Vec<MessageVerifier>,
239}
240
241impl RotatingVerifier {
242 pub fn new(verifiers: Vec<MessageVerifier>) -> Self {
244 assert!(
245 !verifiers.is_empty(),
246 "rotating verifier requires at least one verifier"
247 );
248 Self { verifiers }
249 }
250
251 pub fn generate(&self, data: &[u8]) -> String {
253 self.verifiers[0].generate(data)
254 }
255
256 pub fn generate_with_purpose(
258 &self,
259 data: &[u8],
260 purpose: &str,
261 expires_at: Option<DateTime<Utc>>,
262 ) -> String {
263 self.verifiers[0].generate_with_purpose(data, purpose, expires_at)
264 }
265
266 pub fn verify(&self, signed_message: &str) -> Result<Vec<u8>, VerifierError> {
268 self.verify_with(signed_message, |verifier| verifier.verify(signed_message))
269 }
270
271 pub fn verify_with_purpose(
273 &self,
274 signed_message: &str,
275 purpose: &str,
276 ) -> Result<Vec<u8>, VerifierError> {
277 self.verify_with(signed_message, |verifier| {
278 verifier.verify_with_purpose(signed_message, purpose)
279 })
280 }
281
282 pub fn valid_message(&self, signed_message: &str) -> bool {
284 self.verifiers
285 .iter()
286 .any(|verifier| verifier.valid_message(signed_message))
287 }
288
289 fn verify_with<F>(
290 &self,
291 _signed_message: &str,
292 mut operation: F,
293 ) -> Result<Vec<u8>, VerifierError>
294 where
295 F: FnMut(&MessageVerifier) -> Result<Vec<u8>, VerifierError>,
296 {
297 let mut best_error = None;
298
299 for verifier in &self.verifiers {
300 match operation(verifier) {
301 Ok(value) => return Ok(value),
302 Err(error) => best_error = Some(prefer_verifier_error(best_error, error)),
303 }
304 }
305
306 Err(best_error.unwrap_or(VerifierError::InvalidSignature))
307 }
308}
309
310pub struct RotatingEncryptor {
312 encryptors: Vec<MessageEncryptor>,
313}
314
315impl RotatingEncryptor {
316 pub fn new(encryptors: Vec<MessageEncryptor>) -> Self {
318 assert!(
319 !encryptors.is_empty(),
320 "rotating encryptor requires at least one encryptor"
321 );
322 Self { encryptors }
323 }
324
325 pub fn encrypt_and_sign(&self, data: &[u8]) -> Result<String, EncryptorError> {
327 self.encryptors[0].encrypt_and_sign(data)
328 }
329
330 pub fn encrypt_and_sign_with_purpose(
332 &self,
333 data: &[u8],
334 purpose: &str,
335 expires_at: Option<DateTime<Utc>>,
336 ) -> Result<String, EncryptorError> {
337 self.encryptors[0].encrypt_and_sign_with_purpose(data, purpose, expires_at)
338 }
339
340 pub fn decrypt_and_verify(&self, encrypted_message: &str) -> Result<Vec<u8>, EncryptorError> {
342 self.decrypt_with(encrypted_message, |encryptor| {
343 encryptor.decrypt_and_verify(encrypted_message)
344 })
345 }
346
347 pub fn decrypt_and_verify_with_purpose(
349 &self,
350 encrypted_message: &str,
351 purpose: &str,
352 ) -> Result<Vec<u8>, EncryptorError> {
353 self.decrypt_with(encrypted_message, |encryptor| {
354 encryptor.decrypt_and_verify_with_purpose(encrypted_message, purpose)
355 })
356 }
357
358 fn decrypt_with<F>(
359 &self,
360 _encrypted_message: &str,
361 mut operation: F,
362 ) -> Result<Vec<u8>, EncryptorError>
363 where
364 F: FnMut(&MessageEncryptor) -> Result<Vec<u8>, EncryptorError>,
365 {
366 let mut best_error = None;
367
368 for encryptor in &self.encryptors {
369 match operation(encryptor) {
370 Ok(value) => return Ok(value),
371 Err(error) => best_error = Some(prefer_encryptor_error(best_error, error)),
372 }
373 }
374
375 Err(best_error.unwrap_or(EncryptorError::DecryptionFailed))
376 }
377}
378
379fn split_signed_message(message: &str) -> Option<(&str, &str)> {
380 message.split_once(SEPARATOR)
381}
382
383fn sign_hmac(secret: &[u8], data: &[u8]) -> String {
384 let mut mac = new_hmac(secret);
385 mac.update(data);
386 BASE64_STANDARD.encode(mac.finalize().into_bytes())
387}
388
389fn verify_hmac(
390 secret: &[u8],
391 encoded_payload: &str,
392 encoded_signature: &str,
393) -> Result<(), VerifierError> {
394 let signature = BASE64_STANDARD
395 .decode(encoded_signature)
396 .map_err(|_| VerifierError::InvalidSignature)?;
397 let mut mac = new_hmac(secret);
398 mac.update(encoded_payload.as_bytes());
399 mac.verify_slice(&signature)
400 .map_err(|_| VerifierError::InvalidSignature)
401}
402
403fn new_hmac(secret: &[u8]) -> HmacSha256 {
404 match <HmacSha256 as Mac>::new_from_slice(secret) {
405 Ok(mac) => mac,
406 Err(_) => unreachable!("HMAC accepts secrets of any size"),
407 }
408}
409
410fn encode_metadata(
411 data: &[u8],
412 purpose: Option<&str>,
413 expires_at: Option<DateTime<Utc>>,
414) -> Vec<u8> {
415 let purpose_bytes = purpose.unwrap_or_default().as_bytes();
416 let mut flags = 0_u8;
417 let mut payload =
418 Vec::with_capacity(META_PREFIX.len() + 1 + 4 + purpose_bytes.len() + 8 + data.len());
419
420 if purpose.is_some() {
421 flags |= FLAG_PURPOSE;
422 }
423 if expires_at.is_some() {
424 flags |= FLAG_EXPIRES_AT;
425 }
426
427 payload.extend_from_slice(META_PREFIX);
428 payload.push(flags);
429
430 if purpose.is_some() {
431 let length = purpose_bytes.len() as u32;
432 payload.extend_from_slice(&length.to_be_bytes());
433 payload.extend_from_slice(purpose_bytes);
434 }
435
436 if let Some(expires_at) = expires_at {
437 payload.extend_from_slice(&expires_at.timestamp_millis().to_be_bytes());
438 }
439
440 payload.extend_from_slice(data);
441 payload
442}
443
444fn parse_payload(payload: &[u8]) -> Result<DecodedPayload, String> {
445 if !payload.starts_with(META_PREFIX) {
446 return Ok(DecodedPayload {
447 data: payload.to_vec(),
448 purpose: None,
449 expires_at: None,
450 });
451 }
452
453 let mut cursor = META_PREFIX.len();
454 let flags = *payload
455 .get(cursor)
456 .ok_or_else(|| "missing metadata flags".to_owned())?;
457 cursor += 1;
458
459 let purpose = if flags & FLAG_PURPOSE != 0 {
460 let length_bytes: [u8; 4] = payload
461 .get(cursor..cursor + 4)
462 .ok_or_else(|| "missing purpose length".to_owned())?
463 .try_into()
464 .map_err(|_| "invalid purpose length".to_owned())?;
465 cursor += 4;
466
467 let length = u32::from_be_bytes(length_bytes) as usize;
468 let purpose_bytes = payload
469 .get(cursor..cursor + length)
470 .ok_or_else(|| "truncated purpose".to_owned())?;
471 cursor += length;
472
473 Some(
474 std::str::from_utf8(purpose_bytes)
475 .map_err(|_| "invalid purpose encoding".to_owned())?
476 .to_owned(),
477 )
478 } else {
479 None
480 };
481
482 let expires_at = if flags & FLAG_EXPIRES_AT != 0 {
483 let timestamp_bytes: [u8; 8] = payload
484 .get(cursor..cursor + 8)
485 .ok_or_else(|| "missing expiration timestamp".to_owned())?
486 .try_into()
487 .map_err(|_| "invalid expiration timestamp".to_owned())?;
488 cursor += 8;
489
490 Some(
491 Utc.timestamp_millis_opt(i64::from_be_bytes(timestamp_bytes))
492 .single()
493 .ok_or_else(|| "invalid expiration timestamp".to_owned())?,
494 )
495 } else {
496 None
497 };
498
499 Ok(DecodedPayload {
500 data: payload[cursor..].to_vec(),
501 purpose,
502 expires_at,
503 })
504}
505
506fn validate_payload(
507 payload: DecodedPayload,
508 expected_purpose: Option<&str>,
509) -> Result<Vec<u8>, VerifierError> {
510 validate_common_payload(payload, expected_purpose).map_err(|error| match error {
511 ValidationError::Expired => VerifierError::Expired,
512 ValidationError::PurposeMismatch => VerifierError::PurposeMismatch,
513 })
514}
515
516fn validate_payload_encryptor(
517 payload: DecodedPayload,
518 expected_purpose: Option<&str>,
519) -> Result<Vec<u8>, EncryptorError> {
520 validate_common_payload(payload, expected_purpose).map_err(|error| match error {
521 ValidationError::Expired => EncryptorError::Expired,
522 ValidationError::PurposeMismatch => EncryptorError::PurposeMismatch,
523 })
524}
525
526fn validate_common_payload(
527 payload: DecodedPayload,
528 expected_purpose: Option<&str>,
529) -> Result<Vec<u8>, ValidationError> {
530 if payload.purpose.as_deref() != expected_purpose
531 && (payload.purpose.is_some() || expected_purpose.is_some())
532 {
533 return Err(ValidationError::PurposeMismatch);
534 }
535
536 if let Some(expires_at) = payload.expires_at
537 && expires_at <= Utc::now()
538 {
539 return Err(ValidationError::Expired);
540 }
541
542 Ok(payload.data)
543}
544
545fn prefer_verifier_error(
546 current: Option<VerifierError>,
547 candidate: VerifierError,
548) -> VerifierError {
549 match current {
550 None => candidate,
551 Some(existing) => {
552 if verifier_error_priority(&candidate) > verifier_error_priority(&existing) {
553 candidate
554 } else {
555 existing
556 }
557 }
558 }
559}
560
561fn prefer_encryptor_error(
562 current: Option<EncryptorError>,
563 candidate: EncryptorError,
564) -> EncryptorError {
565 match current {
566 None => candidate,
567 Some(existing) => {
568 if encryptor_error_priority(&candidate) > encryptor_error_priority(&existing) {
569 candidate
570 } else {
571 existing
572 }
573 }
574 }
575}
576
577fn verifier_error_priority(error: &VerifierError) -> u8 {
578 match error {
579 VerifierError::Expired => 3,
580 VerifierError::PurposeMismatch => 2,
581 VerifierError::Encoding(_) => 1,
582 VerifierError::InvalidSignature => 0,
583 }
584}
585
586fn encryptor_error_priority(error: &EncryptorError) -> u8 {
587 match error {
588 EncryptorError::Expired => 3,
589 EncryptorError::PurposeMismatch => 2,
590 EncryptorError::Encoding(_) => 1,
591 EncryptorError::DecryptionFailed | EncryptorError::InvalidKeyLength => 0,
592 }
593}
594
595enum ValidationError {
596 Expired,
597 PurposeMismatch,
598}
599
600#[cfg(test)]
601mod tests {
602 use super::*;
603 use chrono::Duration;
604
605 fn verifier() -> MessageVerifier {
606 MessageVerifier::new(b"verifier-secret")
607 }
608
609 fn encryptor() -> MessageEncryptor {
610 MessageEncryptor::new(&[7_u8; 32]).unwrap()
611 }
612
613 #[test]
614 fn verifier_round_trip() {
615 let verifier = verifier();
616 let message = verifier.generate(b"hello");
617
618 assert_eq!(verifier.verify(&message).unwrap(), b"hello");
619 }
620
621 #[test]
622 fn verifier_rejects_tampering() {
623 let verifier = verifier();
624 let mut message = verifier.generate(b"hello");
625 message.push('x');
626
627 assert_eq!(
628 verifier.verify(&message),
629 Err(VerifierError::InvalidSignature)
630 );
631 }
632
633 #[test]
634 fn verifier_rejects_wrong_key() {
635 let message = verifier().generate(b"hello");
636 let wrong = MessageVerifier::new(b"wrong-secret");
637
638 assert_eq!(wrong.verify(&message), Err(VerifierError::InvalidSignature));
639 }
640
641 #[test]
642 fn verifier_checks_purpose() {
643 let verifier = verifier();
644 let message = verifier.generate_with_purpose(b"hello", "login", None);
645
646 assert_eq!(
647 verifier.verify_with_purpose(&message, "login").unwrap(),
648 b"hello"
649 );
650 assert_eq!(
651 verifier.verify_with_purpose(&message, "reset"),
652 Err(VerifierError::PurposeMismatch)
653 );
654 assert_eq!(
655 verifier.verify(&message),
656 Err(VerifierError::PurposeMismatch)
657 );
658 }
659
660 #[test]
661 fn verifier_checks_expiry() {
662 let verifier = verifier();
663 let message = verifier.generate_with_purpose(
664 b"hello",
665 "login",
666 Some(Utc::now() - Duration::seconds(1)),
667 );
668
669 assert_eq!(
670 verifier.verify_with_purpose(&message, "login"),
671 Err(VerifierError::Expired)
672 );
673 }
674
675 #[test]
676 fn verifier_valid_message_only_checks_signature() {
677 let verifier = verifier();
678 let valid = verifier.generate(b"hello");
679 let expired = verifier.generate_with_purpose(
680 b"hello",
681 "login",
682 Some(Utc::now() - Duration::seconds(1)),
683 );
684
685 assert!(verifier.valid_message(&valid));
686 assert!(verifier.valid_message(&expired));
687 assert!(!verifier.valid_message("not-a-message"));
688 }
689
690 #[test]
691 fn verifier_handles_empty_long_and_unicode_payloads() {
692 let verifier = verifier();
693 let payloads = [
694 Vec::new(),
695 "héllø 🌍".as_bytes().to_vec(),
696 vec![b'x'; 8 * 1024],
697 ];
698
699 for payload in payloads {
700 let message = verifier.generate(&payload);
701 assert_eq!(verifier.verify(&message).unwrap(), payload);
702 }
703 }
704
705 #[test]
706 fn encryptor_round_trip() {
707 let encryptor = encryptor();
708 let message = encryptor.encrypt_and_sign(b"secret").unwrap();
709
710 assert_eq!(encryptor.decrypt_and_verify(&message).unwrap(), b"secret");
711 }
712
713 #[test]
714 fn encryptor_uses_random_nonces() {
715 let encryptor = encryptor();
716 let first = encryptor.encrypt_and_sign(b"secret").unwrap();
717 let second = encryptor.encrypt_and_sign(b"secret").unwrap();
718
719 assert_ne!(first, second);
720 }
721
722 #[test]
723 fn encryptor_rejects_wrong_key() {
724 let message = encryptor().encrypt_and_sign(b"secret").unwrap();
725 let wrong = MessageEncryptor::new(&[8_u8; 32]).unwrap();
726
727 assert_eq!(
728 wrong.decrypt_and_verify(&message),
729 Err(EncryptorError::DecryptionFailed)
730 );
731 }
732
733 #[test]
734 fn encryptor_checks_purpose() {
735 let encryptor = encryptor();
736 let message = encryptor
737 .encrypt_and_sign_with_purpose(b"secret", "login", None)
738 .unwrap();
739
740 assert_eq!(
741 encryptor
742 .decrypt_and_verify_with_purpose(&message, "login")
743 .unwrap(),
744 b"secret"
745 );
746 assert_eq!(
747 encryptor.decrypt_and_verify_with_purpose(&message, "reset"),
748 Err(EncryptorError::PurposeMismatch)
749 );
750 assert_eq!(
751 encryptor.decrypt_and_verify(&message),
752 Err(EncryptorError::PurposeMismatch)
753 );
754 }
755
756 #[test]
757 fn encryptor_checks_expiry() {
758 let encryptor = encryptor();
759 let message = encryptor
760 .encrypt_and_sign_with_purpose(
761 b"secret",
762 "login",
763 Some(Utc::now() - Duration::seconds(1)),
764 )
765 .unwrap();
766
767 assert_eq!(
768 encryptor.decrypt_and_verify_with_purpose(&message, "login"),
769 Err(EncryptorError::Expired)
770 );
771 }
772
773 #[test]
774 fn encryptor_requires_a_32_byte_key() {
775 assert!(matches!(
776 MessageEncryptor::new(&[1_u8; 16]),
777 Err(EncryptorError::InvalidKeyLength)
778 ));
779 }
780
781 #[test]
782 fn rotating_verifier_accepts_old_keys_and_uses_newest_for_generation() {
783 let old = MessageVerifier::new(b"old-secret");
784 let new = MessageVerifier::new(b"new-secret");
785 let rotating = RotatingVerifier::new(vec![new, old]);
786
787 let old_message = MessageVerifier::new(b"old-secret").generate(b"legacy");
788 assert_eq!(rotating.verify(&old_message).unwrap(), b"legacy");
789
790 let new_message = rotating.generate(b"current");
791 assert_eq!(
792 MessageVerifier::new(b"new-secret")
793 .verify(&new_message)
794 .unwrap(),
795 b"current"
796 );
797 assert_eq!(
798 MessageVerifier::new(b"old-secret").verify(&new_message),
799 Err(VerifierError::InvalidSignature)
800 );
801 }
802
803 #[test]
804 fn rotating_encryptor_accepts_old_keys_and_uses_newest_for_generation() {
805 let old = MessageEncryptor::new(&[1_u8; 32]).unwrap();
806 let new = MessageEncryptor::new(&[2_u8; 32]).unwrap();
807 let rotating = RotatingEncryptor::new(vec![new, old]);
808
809 let old_message = MessageEncryptor::new(&[1_u8; 32])
810 .unwrap()
811 .encrypt_and_sign(b"legacy")
812 .unwrap();
813 assert_eq!(
814 rotating.decrypt_and_verify(&old_message).unwrap(),
815 b"legacy"
816 );
817
818 let new_message = rotating.encrypt_and_sign(b"current").unwrap();
819 assert_eq!(
820 MessageEncryptor::new(&[2_u8; 32])
821 .unwrap()
822 .decrypt_and_verify(&new_message)
823 .unwrap(),
824 b"current"
825 );
826 assert_eq!(
827 MessageEncryptor::new(&[1_u8; 32])
828 .unwrap()
829 .decrypt_and_verify(&new_message),
830 Err(EncryptorError::DecryptionFailed)
831 );
832 }
833
834 #[test]
835 fn encryptor_handles_empty_long_and_unicode_payloads() {
836 let encryptor = encryptor();
837 let payloads = [
838 Vec::new(),
839 "héllø 🌍".as_bytes().to_vec(),
840 vec![b'x'; 8 * 1024],
841 ];
842
843 for payload in payloads {
844 let message = encryptor.encrypt_and_sign(&payload).unwrap();
845 assert_eq!(encryptor.decrypt_and_verify(&message).unwrap(), payload);
846 }
847 }
848
849 #[test]
850 fn encryptor_does_not_set_sign_secret_in_aead_mode() {
851 let encryptor = encryptor();
852 assert_eq!(encryptor.sign_secret(), None);
853 }
854
855 #[test]
856 fn verifier_accepts_future_expiry_for_multiple_binary_sizes() {
857 let verifier = verifier();
858 let payloads = [
859 Vec::new(),
860 vec![0_u8],
861 (0_u8..=32).collect::<Vec<_>>(),
862 vec![0xAB; 4 * 1024],
863 ];
864
865 for payload in payloads {
866 let message = verifier.generate_with_purpose(
867 &payload,
868 "download",
869 Some(Utc::now() + Duration::minutes(5)),
870 );
871 assert_eq!(
872 verifier.verify_with_purpose(&message, "download").unwrap(),
873 payload
874 );
875 }
876 }
877
878 #[test]
879 fn verifier_rejects_payload_and_signature_mutation() {
880 let verifier = verifier();
881 let message = verifier.generate(b"hello");
882 let (payload, signature) = message.split_once(SEPARATOR).unwrap();
883
884 let mut payload_bytes = base64::Engine::decode(&BASE64_STANDARD, payload).unwrap();
885 payload_bytes[0] ^= 0x01;
886 let tampered_payload = format!(
887 "{}{}{}",
888 base64::Engine::encode(&BASE64_STANDARD, payload_bytes),
889 SEPARATOR,
890 signature
891 );
892 assert_eq!(
893 verifier.verify(&tampered_payload),
894 Err(VerifierError::InvalidSignature)
895 );
896
897 let mut signature_bytes = base64::Engine::decode(&BASE64_STANDARD, signature).unwrap();
898 signature_bytes[0] ^= 0x01;
899 let tampered_signature = format!(
900 "{}{}{}",
901 payload,
902 SEPARATOR,
903 base64::Engine::encode(&BASE64_STANDARD, signature_bytes),
904 );
905 assert_eq!(
906 verifier.verify(&tampered_signature),
907 Err(VerifierError::InvalidSignature)
908 );
909 }
910
911 #[test]
912 fn encryptor_accepts_future_expiry_for_multiple_binary_sizes() {
913 let encryptor = encryptor();
914 let payloads = [
915 Vec::new(),
916 vec![0_u8],
917 (0_u8..=32).collect::<Vec<_>>(),
918 vec![0xCD; 4 * 1024],
919 ];
920
921 for payload in payloads {
922 let message = encryptor
923 .encrypt_and_sign_with_purpose(
924 &payload,
925 "download",
926 Some(Utc::now() + Duration::minutes(5)),
927 )
928 .unwrap();
929 assert_eq!(
930 encryptor
931 .decrypt_and_verify_with_purpose(&message, "download")
932 .unwrap(),
933 payload
934 );
935 }
936 }
937
938 #[test]
939 fn encryptor_rejects_nonce_and_ciphertext_mutation() {
940 let encryptor = encryptor();
941 let message = encryptor.encrypt_and_sign(b"secret").unwrap();
942 let (nonce, ciphertext) = message.split_once(SEPARATOR).unwrap();
943
944 let mut nonce_bytes = base64::Engine::decode(&BASE64_STANDARD, nonce).unwrap();
945 nonce_bytes[0] ^= 0x01;
946 let tampered_nonce = format!(
947 "{}{}{}",
948 base64::Engine::encode(&BASE64_STANDARD, nonce_bytes),
949 SEPARATOR,
950 ciphertext
951 );
952 assert_eq!(
953 encryptor.decrypt_and_verify(&tampered_nonce),
954 Err(EncryptorError::DecryptionFailed)
955 );
956
957 let mut ciphertext_bytes = base64::Engine::decode(&BASE64_STANDARD, ciphertext).unwrap();
958 ciphertext_bytes[0] ^= 0x01;
959 let tampered_ciphertext = format!(
960 "{}{}{}",
961 nonce,
962 SEPARATOR,
963 base64::Engine::encode(&BASE64_STANDARD, ciphertext_bytes),
964 );
965 assert_eq!(
966 encryptor.decrypt_and_verify(&tampered_ciphertext),
967 Err(EncryptorError::DecryptionFailed)
968 );
969 }
970
971 #[test]
972 fn rotating_verifier_preserves_purpose_and_expiry_errors_for_old_keys() {
973 let old = MessageVerifier::new(b"old-secret");
974 let new = MessageVerifier::new(b"new-secret");
975 let rotating = RotatingVerifier::new(vec![new, old]);
976
977 let old_message = MessageVerifier::new(b"old-secret").generate_with_purpose(
978 b"legacy",
979 "login",
980 Some(Utc::now() + Duration::minutes(5)),
981 );
982 assert_eq!(
983 rotating.verify_with_purpose(&old_message, "login").unwrap(),
984 b"legacy"
985 );
986 assert_eq!(
987 rotating.verify_with_purpose(&old_message, "reset"),
988 Err(VerifierError::PurposeMismatch)
989 );
990
991 let expired_message = MessageVerifier::new(b"old-secret").generate_with_purpose(
992 b"legacy",
993 "login",
994 Some(Utc::now() - Duration::seconds(1)),
995 );
996 assert_eq!(
997 rotating.verify_with_purpose(&expired_message, "login"),
998 Err(VerifierError::Expired)
999 );
1000 }
1001
1002 #[test]
1003 fn rotating_encryptor_preserves_purpose_and_expiry_errors_for_old_keys() {
1004 let old = MessageEncryptor::new(&[1_u8; 32]).unwrap();
1005 let new = MessageEncryptor::new(&[2_u8; 32]).unwrap();
1006 let rotating = RotatingEncryptor::new(vec![new, old]);
1007
1008 let old_message = MessageEncryptor::new(&[1_u8; 32])
1009 .unwrap()
1010 .encrypt_and_sign_with_purpose(
1011 b"legacy",
1012 "login",
1013 Some(Utc::now() + Duration::minutes(5)),
1014 )
1015 .unwrap();
1016 assert_eq!(
1017 rotating
1018 .decrypt_and_verify_with_purpose(&old_message, "login")
1019 .unwrap(),
1020 b"legacy"
1021 );
1022 assert_eq!(
1023 rotating.decrypt_and_verify_with_purpose(&old_message, "reset"),
1024 Err(EncryptorError::PurposeMismatch)
1025 );
1026
1027 let expired_message = MessageEncryptor::new(&[1_u8; 32])
1028 .unwrap()
1029 .encrypt_and_sign_with_purpose(
1030 b"legacy",
1031 "login",
1032 Some(Utc::now() - Duration::seconds(1)),
1033 )
1034 .unwrap();
1035 assert_eq!(
1036 rotating.decrypt_and_verify_with_purpose(&expired_message, "login"),
1037 Err(EncryptorError::Expired)
1038 );
1039 }
1040
1041 fn verifier_signed_message(payload: Vec<u8>) -> String {
1042 let encoded_payload = BASE64_STANDARD.encode(payload);
1043 let signature = sign_hmac(b"verifier-secret", encoded_payload.as_bytes());
1044 format!("{encoded_payload}{SEPARATOR}{signature}")
1045 }
1046
1047 fn encryptor_message(payload: Vec<u8>) -> String {
1048 encryptor().encrypt_payload(payload).unwrap()
1049 }
1050
1051 fn bytes(len: usize) -> Vec<u8> {
1052 (0..len).map(|index| (index % 251) as u8).collect()
1053 }
1054
1055 fn metadata_prefixed_payload() -> Vec<u8> {
1056 let mut payload = META_PREFIX.to_vec();
1057 payload.extend_from_slice(&[0, b'h', b'e', b'l', b'l', b'o', 0xFF]);
1058 payload
1059 }
1060
1061 macro_rules! verifier_round_trip_size_case {
1062 ($name:ident, $len:expr) => {
1063 #[test]
1064 fn $name() {
1065 let verifier = verifier();
1066 let payload = bytes($len);
1067 let message = verifier.generate(&payload);
1068
1069 assert_eq!(verifier.verify(&message).unwrap(), payload);
1070 }
1071 };
1072 }
1073
1074 macro_rules! encryptor_round_trip_size_case {
1075 ($name:ident, $len:expr) => {
1076 #[test]
1077 fn $name() {
1078 let encryptor = encryptor();
1079 let payload = bytes($len);
1080 let message = encryptor.encrypt_and_sign(&payload).unwrap();
1081
1082 assert_eq!(encryptor.decrypt_and_verify(&message).unwrap(), payload);
1083 }
1084 };
1085 }
1086
1087 macro_rules! verifier_future_expiry_case {
1088 ($name:ident, $len:expr) => {
1089 #[test]
1090 fn $name() {
1091 let verifier = verifier();
1092 let payload = bytes($len);
1093 let message = verifier.generate_with_purpose(
1094 &payload,
1095 "download",
1096 Some(Utc::now() + Duration::minutes(5)),
1097 );
1098
1099 assert_eq!(
1100 verifier.verify_with_purpose(&message, "download").unwrap(),
1101 payload
1102 );
1103 }
1104 };
1105 }
1106
1107 macro_rules! encryptor_future_expiry_case {
1108 ($name:ident, $len:expr) => {
1109 #[test]
1110 fn $name() {
1111 let encryptor = encryptor();
1112 let payload = bytes($len);
1113 let message = encryptor
1114 .encrypt_and_sign_with_purpose(
1115 &payload,
1116 "download",
1117 Some(Utc::now() + Duration::minutes(5)),
1118 )
1119 .unwrap();
1120
1121 assert_eq!(
1122 encryptor
1123 .decrypt_and_verify_with_purpose(&message, "download")
1124 .unwrap(),
1125 payload
1126 );
1127 }
1128 };
1129 }
1130
1131 verifier_round_trip_size_case!(verifier_round_trip_size_0, 0);
1132 verifier_round_trip_size_case!(verifier_round_trip_size_1, 1);
1133 verifier_round_trip_size_case!(verifier_round_trip_size_2, 2);
1134 verifier_round_trip_size_case!(verifier_round_trip_size_31, 31);
1135 verifier_round_trip_size_case!(verifier_round_trip_size_32, 32);
1136 verifier_round_trip_size_case!(verifier_round_trip_size_33, 33);
1137 verifier_round_trip_size_case!(verifier_round_trip_size_255, 255);
1138 verifier_round_trip_size_case!(verifier_round_trip_size_1024, 1024);
1139
1140 encryptor_round_trip_size_case!(encryptor_round_trip_size_0, 0);
1141 encryptor_round_trip_size_case!(encryptor_round_trip_size_1, 1);
1142 encryptor_round_trip_size_case!(encryptor_round_trip_size_2, 2);
1143 encryptor_round_trip_size_case!(encryptor_round_trip_size_31, 31);
1144 encryptor_round_trip_size_case!(encryptor_round_trip_size_32, 32);
1145 encryptor_round_trip_size_case!(encryptor_round_trip_size_33, 33);
1146 encryptor_round_trip_size_case!(encryptor_round_trip_size_255, 255);
1147 encryptor_round_trip_size_case!(encryptor_round_trip_size_1024, 1024);
1148
1149 verifier_future_expiry_case!(verifier_future_expiry_size_0, 0);
1150 verifier_future_expiry_case!(verifier_future_expiry_size_7, 7);
1151 verifier_future_expiry_case!(verifier_future_expiry_size_64, 64);
1152 verifier_future_expiry_case!(verifier_future_expiry_size_2048, 2048);
1153
1154 encryptor_future_expiry_case!(encryptor_future_expiry_size_0, 0);
1155 encryptor_future_expiry_case!(encryptor_future_expiry_size_7, 7);
1156 encryptor_future_expiry_case!(encryptor_future_expiry_size_64, 64);
1157 encryptor_future_expiry_case!(encryptor_future_expiry_size_2048, 2048);
1158
1159 #[test]
1160 fn verifier_raw_message_requires_no_purpose() {
1161 let verifier = verifier();
1162 let message = verifier.generate(b"hello");
1163
1164 assert_eq!(
1165 verifier.verify_with_purpose(&message, "login"),
1166 Err(VerifierError::PurposeMismatch)
1167 );
1168 }
1169
1170 #[test]
1171 fn verifier_empty_purpose_round_trip() {
1172 let verifier = verifier();
1173 let message = verifier.generate_with_purpose(b"hello", "", None);
1174
1175 assert_eq!(
1176 verifier.verify_with_purpose(&message, "").unwrap(),
1177 b"hello"
1178 );
1179 assert_eq!(
1180 verifier.verify(&message),
1181 Err(VerifierError::PurposeMismatch)
1182 );
1183 }
1184
1185 #[test]
1186 fn verifier_valid_message_rejects_missing_separator() {
1187 assert!(!verifier().valid_message("not-a-message"));
1188 }
1189
1190 #[test]
1191 fn verifier_valid_message_rejects_invalid_payload_base64() {
1192 let encoded_payload = "***";
1193 let signature = sign_hmac(b"verifier-secret", encoded_payload.as_bytes());
1194 let message = format!("{encoded_payload}{SEPARATOR}{signature}");
1195
1196 assert!(!verifier().valid_message(&message));
1197 }
1198
1199 #[test]
1200 fn verifier_valid_message_rejects_invalid_signature_base64() {
1201 let message = format!("{}{}***", BASE64_STANDARD.encode(b"hello"), SEPARATOR);
1202
1203 assert!(!verifier().valid_message(&message));
1204 }
1205
1206 #[test]
1207 fn verifier_signed_prefix_without_flags_returns_encoding_error() {
1208 let message = verifier_signed_message(META_PREFIX.to_vec());
1209
1210 assert_eq!(
1211 verifier().verify(&message),
1212 Err(VerifierError::Encoding("missing metadata flags".to_owned()))
1213 );
1214 }
1215
1216 #[test]
1217 fn verifier_truncated_purpose_returns_encoding_error() {
1218 let mut payload = META_PREFIX.to_vec();
1219 payload.push(FLAG_PURPOSE);
1220 payload.extend_from_slice(&4_u32.to_be_bytes());
1221 payload.extend_from_slice(b"ab");
1222 let message = verifier_signed_message(payload);
1223
1224 assert_eq!(
1225 verifier().verify(&message),
1226 Err(VerifierError::Encoding("truncated purpose".to_owned()))
1227 );
1228 }
1229
1230 #[test]
1231 fn verifier_invalid_utf8_purpose_returns_encoding_error() {
1232 let mut payload = META_PREFIX.to_vec();
1233 payload.push(FLAG_PURPOSE);
1234 payload.extend_from_slice(&2_u32.to_be_bytes());
1235 payload.extend_from_slice(&[0xFF, 0xFE]);
1236 let message = verifier_signed_message(payload);
1237
1238 assert_eq!(
1239 verifier().verify(&message),
1240 Err(VerifierError::Encoding(
1241 "invalid purpose encoding".to_owned(),
1242 ))
1243 );
1244 }
1245
1246 #[test]
1247 fn verifier_missing_expiration_timestamp_returns_encoding_error() {
1248 let mut payload = META_PREFIX.to_vec();
1249 payload.push(FLAG_EXPIRES_AT);
1250 let message = verifier_signed_message(payload);
1251
1252 assert_eq!(
1253 verifier().verify(&message),
1254 Err(VerifierError::Encoding(
1255 "missing expiration timestamp".to_owned(),
1256 ))
1257 );
1258 }
1259
1260 #[test]
1261 fn verifier_invalid_expiration_timestamp_returns_encoding_error() {
1262 let mut payload = META_PREFIX.to_vec();
1263 payload.push(FLAG_EXPIRES_AT);
1264 payload.extend_from_slice(&i64::MAX.to_be_bytes());
1265 let message = verifier_signed_message(payload);
1266
1267 assert_eq!(
1268 verifier().verify(&message),
1269 Err(VerifierError::Encoding(
1270 "invalid expiration timestamp".to_owned(),
1271 ))
1272 );
1273 }
1274
1275 #[test]
1276 fn verifier_missing_purpose_length_returns_encoding_error() {
1277 let mut payload = META_PREFIX.to_vec();
1278 payload.push(FLAG_PURPOSE);
1279 let message = verifier_signed_message(payload);
1280
1281 assert_eq!(
1282 verifier().verify(&message),
1283 Err(VerifierError::Encoding("missing purpose length".to_owned()))
1284 );
1285 }
1286
1287 #[test]
1288 fn verifier_rejects_empty_messages() {
1289 assert_eq!(verifier().verify(""), Err(VerifierError::InvalidSignature));
1290 }
1291
1292 #[test]
1293 fn verifier_rejects_extra_separator_in_signature() {
1294 let verifier = verifier();
1295 let message = format!("{}--extra", verifier.generate(b"hello"));
1296
1297 assert_eq!(
1298 verifier.verify(&message),
1299 Err(VerifierError::InvalidSignature)
1300 );
1301 }
1302
1303 #[test]
1304 fn verifier_purpose_mismatch_precedes_expiry() {
1305 let verifier = verifier();
1306 let message = verifier.generate_with_purpose(
1307 b"hello",
1308 "login",
1309 Some(Utc::now() - Duration::seconds(1)),
1310 );
1311
1312 assert_eq!(
1313 verifier.verify_with_purpose(&message, "reset"),
1314 Err(VerifierError::PurposeMismatch)
1315 );
1316 }
1317
1318 #[test]
1319 fn verifier_preserves_payloads_starting_with_metadata_prefix() {
1320 let verifier = verifier();
1321 let payload = metadata_prefixed_payload();
1322 let message = verifier.generate(&payload);
1323
1324 assert_eq!(verifier.verify(&message).unwrap(), payload);
1325 }
1326
1327 #[test]
1328 fn rotating_verifier_valid_message_accepts_old_key() {
1329 let rotating = RotatingVerifier::new(vec![
1330 MessageVerifier::new(b"new-secret"),
1331 MessageVerifier::new(b"old-secret"),
1332 ]);
1333 let old_message = MessageVerifier::new(b"old-secret").generate(b"legacy");
1334
1335 assert!(rotating.valid_message(&old_message));
1336 }
1337
1338 #[test]
1339 fn rotating_verifier_prefers_purpose_mismatch_over_invalid_signature() {
1340 let rotating = RotatingVerifier::new(vec![
1341 MessageVerifier::new(b"new-secret"),
1342 MessageVerifier::new(b"old-secret"),
1343 ]);
1344 let old_message = MessageVerifier::new(b"old-secret").generate_with_purpose(
1345 b"legacy",
1346 "login",
1347 Some(Utc::now() + Duration::minutes(5)),
1348 );
1349
1350 assert_eq!(
1351 rotating.verify_with_purpose(&old_message, "reset"),
1352 Err(VerifierError::PurposeMismatch)
1353 );
1354 }
1355
1356 #[test]
1357 fn rotating_verifier_prefers_expired_over_invalid_signature() {
1358 let rotating = RotatingVerifier::new(vec![
1359 MessageVerifier::new(b"new-secret"),
1360 MessageVerifier::new(b"old-secret"),
1361 ]);
1362 let old_message = MessageVerifier::new(b"old-secret").generate_with_purpose(
1363 b"legacy",
1364 "login",
1365 Some(Utc::now() - Duration::seconds(1)),
1366 );
1367
1368 assert_eq!(
1369 rotating.verify_with_purpose(&old_message, "login"),
1370 Err(VerifierError::Expired)
1371 );
1372 }
1373
1374 #[test]
1375 fn rotating_verifier_generate_with_purpose_uses_newest_key() {
1376 let rotating = RotatingVerifier::new(vec![
1377 MessageVerifier::new(b"new-secret"),
1378 MessageVerifier::new(b"old-secret"),
1379 ]);
1380 let message = rotating.generate_with_purpose(
1381 b"fresh",
1382 "login",
1383 Some(Utc::now() + Duration::minutes(5)),
1384 );
1385
1386 assert_eq!(
1387 MessageVerifier::new(b"new-secret")
1388 .verify_with_purpose(&message, "login")
1389 .unwrap(),
1390 b"fresh"
1391 );
1392 }
1393
1394 #[test]
1395 fn rotating_verifier_prefers_encoding_over_invalid_signature() {
1396 let rotating = RotatingVerifier::new(vec![
1397 MessageVerifier::new(b"new-secret"),
1398 MessageVerifier::new(b"old-secret"),
1399 ]);
1400 let old_message = {
1401 let encoded_payload = BASE64_STANDARD.encode(META_PREFIX);
1402 let signature = sign_hmac(b"old-secret", encoded_payload.as_bytes());
1403 format!("{encoded_payload}{SEPARATOR}{signature}")
1404 };
1405
1406 assert_eq!(
1407 rotating.verify(&old_message),
1408 Err(VerifierError::Encoding("missing metadata flags".to_owned()))
1409 );
1410 }
1411
1412 #[test]
1413 fn encryptor_raw_message_requires_no_purpose() {
1414 let encryptor = encryptor();
1415 let message = encryptor.encrypt_and_sign(b"hello").unwrap();
1416
1417 assert_eq!(
1418 encryptor.decrypt_and_verify_with_purpose(&message, "login"),
1419 Err(EncryptorError::PurposeMismatch)
1420 );
1421 }
1422
1423 #[test]
1424 fn encryptor_empty_purpose_round_trip() {
1425 let encryptor = encryptor();
1426 let message = encryptor
1427 .encrypt_and_sign_with_purpose(b"hello", "", None)
1428 .unwrap();
1429
1430 assert_eq!(
1431 encryptor
1432 .decrypt_and_verify_with_purpose(&message, "")
1433 .unwrap(),
1434 b"hello"
1435 );
1436 assert_eq!(
1437 encryptor.decrypt_and_verify(&message),
1438 Err(EncryptorError::PurposeMismatch)
1439 );
1440 }
1441
1442 #[test]
1443 fn encryptor_missing_separator_returns_encoding_error() {
1444 assert_eq!(
1445 encryptor().decrypt_and_verify("not-a-message"),
1446 Err(EncryptorError::Encoding(
1447 "invalid encrypted message format".to_owned(),
1448 ))
1449 );
1450 }
1451
1452 #[test]
1453 fn encryptor_invalid_nonce_base64_returns_encoding_error() {
1454 let message = format!("***{SEPARATOR}{}", BASE64_STANDARD.encode(b"ciphertext"));
1455
1456 assert_eq!(
1457 encryptor().decrypt_and_verify(&message),
1458 Err(EncryptorError::Encoding(
1459 "invalid nonce encoding".to_owned()
1460 ))
1461 );
1462 }
1463
1464 #[test]
1465 fn encryptor_invalid_nonce_length_returns_encoding_error() {
1466 let message = format!(
1467 "{}{SEPARATOR}{}",
1468 BASE64_STANDARD.encode([1_u8, 2_u8]),
1469 BASE64_STANDARD.encode(b"ciphertext")
1470 );
1471
1472 assert_eq!(
1473 encryptor().decrypt_and_verify(&message),
1474 Err(EncryptorError::Encoding("invalid nonce length".to_owned()))
1475 );
1476 }
1477
1478 #[test]
1479 fn encryptor_invalid_ciphertext_base64_returns_encoding_error() {
1480 let message = format!("{}{SEPARATOR}***", BASE64_STANDARD.encode([0_u8; 12]));
1481
1482 assert_eq!(
1483 encryptor().decrypt_and_verify(&message),
1484 Err(EncryptorError::Encoding(
1485 "invalid ciphertext encoding".to_owned(),
1486 ))
1487 );
1488 }
1489
1490 #[test]
1491 fn encryptor_signed_prefix_without_flags_returns_encoding_error() {
1492 let message = encryptor_message(META_PREFIX.to_vec());
1493
1494 assert_eq!(
1495 encryptor().decrypt_and_verify(&message),
1496 Err(EncryptorError::Encoding(
1497 "missing metadata flags".to_owned()
1498 ))
1499 );
1500 }
1501
1502 #[test]
1503 fn encryptor_truncated_purpose_returns_encoding_error() {
1504 let mut payload = META_PREFIX.to_vec();
1505 payload.push(FLAG_PURPOSE);
1506 payload.extend_from_slice(&4_u32.to_be_bytes());
1507 payload.extend_from_slice(b"ab");
1508 let message = encryptor_message(payload);
1509
1510 assert_eq!(
1511 encryptor().decrypt_and_verify(&message),
1512 Err(EncryptorError::Encoding("truncated purpose".to_owned()))
1513 );
1514 }
1515
1516 #[test]
1517 fn encryptor_invalid_utf8_purpose_returns_encoding_error() {
1518 let mut payload = META_PREFIX.to_vec();
1519 payload.push(FLAG_PURPOSE);
1520 payload.extend_from_slice(&2_u32.to_be_bytes());
1521 payload.extend_from_slice(&[0xFF, 0xFE]);
1522 let message = encryptor_message(payload);
1523
1524 assert_eq!(
1525 encryptor().decrypt_and_verify(&message),
1526 Err(EncryptorError::Encoding(
1527 "invalid purpose encoding".to_owned(),
1528 ))
1529 );
1530 }
1531
1532 #[test]
1533 fn encryptor_missing_expiration_timestamp_returns_encoding_error() {
1534 let mut payload = META_PREFIX.to_vec();
1535 payload.push(FLAG_EXPIRES_AT);
1536 let message = encryptor_message(payload);
1537
1538 assert_eq!(
1539 encryptor().decrypt_and_verify(&message),
1540 Err(EncryptorError::Encoding(
1541 "missing expiration timestamp".to_owned(),
1542 ))
1543 );
1544 }
1545
1546 #[test]
1547 fn encryptor_invalid_expiration_timestamp_returns_encoding_error() {
1548 let mut payload = META_PREFIX.to_vec();
1549 payload.push(FLAG_EXPIRES_AT);
1550 payload.extend_from_slice(&i64::MAX.to_be_bytes());
1551 let message = encryptor_message(payload);
1552
1553 assert_eq!(
1554 encryptor().decrypt_and_verify(&message),
1555 Err(EncryptorError::Encoding(
1556 "invalid expiration timestamp".to_owned(),
1557 ))
1558 );
1559 }
1560
1561 #[test]
1562 fn encryptor_missing_purpose_length_returns_encoding_error() {
1563 let mut payload = META_PREFIX.to_vec();
1564 payload.push(FLAG_PURPOSE);
1565 let message = encryptor_message(payload);
1566
1567 assert_eq!(
1568 encryptor().decrypt_and_verify(&message),
1569 Err(EncryptorError::Encoding(
1570 "missing purpose length".to_owned()
1571 ))
1572 );
1573 }
1574
1575 #[test]
1576 fn encryptor_purpose_mismatch_precedes_expiry() {
1577 let encryptor = encryptor();
1578 let message = encryptor
1579 .encrypt_and_sign_with_purpose(
1580 b"hello",
1581 "login",
1582 Some(Utc::now() - Duration::seconds(1)),
1583 )
1584 .unwrap();
1585
1586 assert_eq!(
1587 encryptor.decrypt_and_verify_with_purpose(&message, "reset"),
1588 Err(EncryptorError::PurposeMismatch)
1589 );
1590 }
1591
1592 #[test]
1593 fn encryptor_preserves_payloads_starting_with_metadata_prefix() {
1594 let encryptor = encryptor();
1595 let payload = metadata_prefixed_payload();
1596 let message = encryptor.encrypt_and_sign(&payload).unwrap();
1597
1598 assert_eq!(encryptor.decrypt_and_verify(&message).unwrap(), payload);
1599 }
1600
1601 #[test]
1602 fn rotating_encryptor_prefers_encoding_over_decryption_failed() {
1603 let rotating = RotatingEncryptor::new(vec![
1604 MessageEncryptor::new(&[2_u8; 32]).unwrap(),
1605 MessageEncryptor::new(&[1_u8; 32]).unwrap(),
1606 ]);
1607 let old_message = MessageEncryptor::new(&[1_u8; 32])
1608 .unwrap()
1609 .encrypt_payload(META_PREFIX.to_vec())
1610 .unwrap();
1611
1612 assert_eq!(
1613 rotating.decrypt_and_verify(&old_message),
1614 Err(EncryptorError::Encoding(
1615 "missing metadata flags".to_owned()
1616 ))
1617 );
1618 }
1619
1620 #[test]
1621 fn rotating_encryptor_generate_with_purpose_uses_newest_key() {
1622 let rotating = RotatingEncryptor::new(vec![
1623 MessageEncryptor::new(&[2_u8; 32]).unwrap(),
1624 MessageEncryptor::new(&[1_u8; 32]).unwrap(),
1625 ]);
1626 let message = rotating
1627 .encrypt_and_sign_with_purpose(
1628 b"fresh",
1629 "login",
1630 Some(Utc::now() + Duration::minutes(5)),
1631 )
1632 .unwrap();
1633
1634 assert_eq!(
1635 MessageEncryptor::new(&[2_u8; 32])
1636 .unwrap()
1637 .decrypt_and_verify_with_purpose(&message, "login")
1638 .unwrap(),
1639 b"fresh"
1640 );
1641 }
1642}