Skip to main content

rustrails_support/
encryption.rs

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/// Errors returned by [`MessageVerifier`].
24#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
25pub enum VerifierError {
26    /// The message signature is invalid or the message is malformed.
27    #[error("invalid signature")]
28    InvalidSignature,
29    /// The message has expired.
30    #[error("message expired")]
31    Expired,
32    /// The message purpose does not match the expected purpose.
33    #[error("purpose mismatch")]
34    PurposeMismatch,
35    /// The message could not be encoded or decoded.
36    #[error("encoding error: {0}")]
37    Encoding(String),
38}
39
40/// Errors returned by [`MessageEncryptor`].
41#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
42pub enum EncryptorError {
43    /// The message could not be decrypted or authenticated.
44    #[error("decryption failed")]
45    DecryptionFailed,
46    /// The supplied key length is invalid for AES-256-GCM.
47    #[error("invalid key length")]
48    InvalidKeyLength,
49    /// The message has expired.
50    #[error("message expired")]
51    Expired,
52    /// The message purpose does not match the expected purpose.
53    #[error("purpose mismatch")]
54    PurposeMismatch,
55    /// The message could not be encoded or decoded.
56    #[error("encoding error: {0}")]
57    Encoding(String),
58}
59
60/// Signs messages using HMAC-SHA256.
61pub struct MessageVerifier {
62    secret: Vec<u8>,
63}
64
65impl MessageVerifier {
66    /// Creates a new verifier from a secret.
67    pub fn new(secret: &[u8]) -> Self {
68        Self {
69            secret: secret.to_vec(),
70        }
71    }
72
73    /// Generates a signed message in the form `base64(payload)--base64(hmac)`.
74    pub fn generate(&self, data: &[u8]) -> String {
75        self.generate_signed_payload(encode_metadata(data, None, None))
76    }
77
78    /// Generates a signed message with purpose confinement and optional expiration.
79    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    /// Verifies a signed message and returns the original bytes.
90    pub fn verify(&self, signed_message: &str) -> Result<Vec<u8>, VerifierError> {
91        self.verify_internal(signed_message, None)
92    }
93
94    /// Verifies a signed message and enforces its purpose.
95    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    /// Returns `true` when the message has a valid signature.
104    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
137/// Encrypts messages using AES-256-GCM.
138pub struct MessageEncryptor {
139    secret: Vec<u8>,
140    sign_secret: Option<Vec<u8>>,
141}
142
143impl MessageEncryptor {
144    /// Creates a new encryptor from a 32-byte secret.
145    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    /// Encrypts and authenticates raw bytes.
157    pub fn encrypt_and_sign(&self, data: &[u8]) -> Result<String, EncryptorError> {
158        self.encrypt_payload(encode_metadata(data, None, None))
159    }
160
161    /// Encrypts and authenticates raw bytes with purpose confinement and optional expiration.
162    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    /// Decrypts and verifies an encrypted message.
172    pub fn decrypt_and_verify(&self, encrypted_message: &str) -> Result<Vec<u8>, EncryptorError> {
173        self.decrypt_internal(encrypted_message, None)
174    }
175
176    /// Decrypts and verifies an encrypted message for a specific purpose.
177    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
236/// Tries multiple verifiers until one succeeds.
237pub struct RotatingVerifier {
238    verifiers: Vec<MessageVerifier>,
239}
240
241impl RotatingVerifier {
242    /// Creates a rotating verifier with the newest verifier first.
243    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    /// Generates a signed message using the newest verifier.
252    pub fn generate(&self, data: &[u8]) -> String {
253        self.verifiers[0].generate(data)
254    }
255
256    /// Generates a signed message with purpose using the newest verifier.
257    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    /// Verifies a signed message with all configured verifiers.
267    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    /// Verifies a signed message with purpose using all configured verifiers.
272    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    /// Returns `true` when any configured verifier accepts the message.
283    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
310/// Tries multiple encryptors until one succeeds.
311pub struct RotatingEncryptor {
312    encryptors: Vec<MessageEncryptor>,
313}
314
315impl RotatingEncryptor {
316    /// Creates a rotating encryptor with the newest encryptor first.
317    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    /// Encrypts with the newest encryptor.
326    pub fn encrypt_and_sign(&self, data: &[u8]) -> Result<String, EncryptorError> {
327        self.encryptors[0].encrypt_and_sign(data)
328    }
329
330    /// Encrypts with purpose using the newest encryptor.
331    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    /// Decrypts with all configured encryptors.
341    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    /// Decrypts with purpose using all configured encryptors.
348    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}