Skip to main content

signed_crypto/
lib.rs

1// Copyright 2026, Kehan Pan, All rights reserved.
2//
3// Cryptographic scheme inspired by:
4// https://github.com/google/openrtb-doubleclick/blob/master/doubleclick-core/src/main/java/com/google/doubleclick/crypto/DoubleClickCrypto.java
5
6//! # signed-crypto
7//!
8//! A Rust library for encrypted payloads with built-in integrity verification.
9//!
10//! ## Package Format
11//!
12//! Encrypted payloads follow this structure:
13//!
14//! ```text
15//! initVector:16 || E(payload:?) || I(signature:4)
16//! ```
17//!
18//! where:
19//! - `initVector` = `timestamp:8 || serverId:8`
20//! - `E(payload)` = AES-256/CTR64 encryption with encryption key
21//! - `I(signature)` = First 4 bytes of HMAC-SHA256(integrityKey, payload || initVector)
22//!
23//! ## Example
24//!
25//! ```rust
26//! use signed_crypto::{Crypto, Keys};
27//!
28//! // WARNING: Never use all-zero keys in production!
29//! // Generate secure random keys using a cryptographic RNG.
30//! let keys = Keys::new(&[0u8; 32], &[0u8; 32]).unwrap();
31//! let crypto = Crypto::new(keys);
32//!
33//! // Encrypt → URL-safe Base64 string
34//! let encoded = crypto.package(b"Hello, world!", None).unwrap();
35//!
36//! // Decrypt → original payload
37//! let payload = crypto.unpackage(&encoded).unwrap();
38//! assert_eq!(payload, b"Hello, world!");
39//! ```
40
41use aes::cipher::{KeyIvInit, StreamCipher};
42use base64::{engine::general_purpose::URL_SAFE, Engine as _};
43use std::io::Write;
44use byteorder::{BigEndian, ByteOrder};
45use hmac::{Hmac, Mac};
46use sha2::Sha256;
47use thiserror::Error;
48use time::{Duration, OffsetDateTime};
49
50type HmacSha256 = Hmac<Sha256>;
51type Aes256Ctr64BE = ctr::Ctr64BE<aes::Aes256>;
52
53const UNIX_EPOCH: OffsetDateTime = time::OffsetDateTime::UNIX_EPOCH;
54
55/// Holds the encryption and integrity keys.
56///
57/// Both keys must be exactly 32 bytes (256 bits).
58///
59/// # Fields
60///
61/// * `encryption_key` - AES-256 encryption key
62/// * `integrity_key` - HMAC-SHA256 integrity key
63#[derive(Clone, Debug)]
64pub struct Keys {
65    /// AES-256 encryption key (32 bytes)
66    pub encryption_key: [u8; 32],
67    /// HMAC-SHA256 integrity key (32 bytes)
68    pub integrity_key: [u8; 32],
69}
70
71impl Keys {
72    /// Creates a new `Keys` instance from raw byte slices.
73    ///
74    /// Both keys must be exactly 32 bytes.
75    ///
76    /// # Errors
77    ///
78    /// Returns [`CryptoError::InvalidKey`] if either key is not 32 bytes.
79    ///
80    /// # Example
81    ///
82    /// ```rust
83    /// use signed_crypto::Keys;
84    ///
85    /// let enc_key = [0u8; 32];
86    /// let int_key = [0u8; 32];
87    /// let keys = Keys::new(&enc_key, &int_key).unwrap();
88    /// ```
89    pub fn new(encryption_key: &[u8], integrity_key: &[u8]) -> Result<Self, CryptoError> {
90        let encryption_key: [u8; 32] = encryption_key
91            .try_into()
92            .map_err(|_| CryptoError::InvalidKey)?;
93        let integrity_key: [u8; 32] = integrity_key
94            .try_into()
95            .map_err(|_| CryptoError::InvalidKey)?;
96
97        Ok(Self {
98            encryption_key,
99            integrity_key,
100        })
101    }
102}
103
104/// Errors that can occur during cryptographic operations.
105#[derive(Error, Debug)]
106pub enum CryptoError {
107    /// Key is not exactly 32 bytes.
108    #[error("invalid key")]
109    InvalidKey,
110    /// HMAC signature verification failed.
111    #[error("invalid signature")]
112    InvalidSign,
113    /// Initialization vector is invalid.
114    #[error("invalid init vector")]
115    InvalidInitVector,
116    /// Data is too short to be a valid package.
117    #[error("data too short")]
118    DataTooShort,
119    /// Payload size does not match expected size.
120    #[error("payload size mismatch")]
121    PayloadSizeMismatch,
122    /// Base64 decoding failed.
123    #[error("decode error: {0}")]
124    DecodeError(#[from] base64::DecodeError),
125    /// Writing to the output stream failed.
126    #[error("io error: {0}")]
127    IoError(#[from] std::io::Error),
128}
129
130/// Main cryptographic operations instance.
131///
132/// Holds the keys and provides methods for encryption, decryption,
133/// and metadata extraction.
134///
135/// # Example
136///
137/// ```rust
138/// use signed_crypto::{Crypto, Keys};
139///
140/// let keys = Keys::new(&[0u8; 32], &[0u8; 32]).unwrap();
141/// let crypto = Crypto::new(keys);
142/// ```
143pub struct Crypto {
144    /// The encryption and integrity keys.
145    pub keys: Keys,
146}
147
148impl Crypto {
149    /// Creates a new `Crypto` instance.
150    ///
151    /// # Example
152    ///
153    /// ```rust
154    /// use signed_crypto::{Crypto, Keys};
155    ///
156    /// let keys = Keys::new(&[0u8; 32], &[0u8; 32]).unwrap();
157    /// let crypto = Crypto::new(keys);
158    /// ```
159    pub fn new(keys: Keys) -> Self {
160        Self { keys }
161    }
162
163    /// Offset of the initialization vector in a package.
164    pub const IV_BASE: usize = 0;
165    /// Size of the initialization vector in bytes.
166    pub const IV_SIZE: usize = 16;
167    /// Offset of the timestamp within the IV.
168    pub const IV_TIME_OFFSET: usize = 0;
169    /// Size of the timestamp in bytes.
170    pub const IV_TIME_SIZE: usize = 8;
171    /// Offset of the server ID within the IV.
172    pub const IV_SERVER_ID_OFFSET: usize = 8;
173    /// Size of the server ID in bytes.
174    pub const IV_SERVER_ID_SIZE: usize = 8;
175    /// Size of the HMAC signature in bytes.
176    pub const SIGNATURE_SIZE: usize = 4;
177    /// Offset where the payload begins.
178    pub const PAYLOAD_BASE: usize = Crypto::IV_BASE + Crypto::IV_SIZE;
179    /// Total overhead size (IV + signature) in bytes.
180    pub const OVERHEAD_SIZE: usize = Crypto::IV_SIZE + Crypto::SIGNATURE_SIZE;
181
182    /// Decodes a URL-safe Base64 encoded string.
183    ///
184    /// # Example
185    ///
186    /// ```rust
187    /// use signed_crypto::{Crypto, Keys};
188    ///
189    /// let crypto = Crypto::new(Keys::new(&[0u8; 32], &[0u8; 32]).unwrap());
190    /// let encoded = "SGVsbG8=";
191    /// let decoded = crypto.decode(encoded).unwrap();
192    /// ```
193    #[inline]
194    pub fn decode<T>(&self, data: T) -> Result<Vec<u8>, CryptoError>
195    where
196        T: AsRef<[u8]>,
197    {
198        URL_SAFE
199            .decode(data)
200            .map(|v| v.to_vec())
201            .map_err(|e| e.into())
202    }
203
204    /// Encodes data as a URL-safe Base64 string.
205    ///
206    /// # Example
207    ///
208    /// ```rust
209    /// use signed_crypto::{Crypto, Keys};
210    ///
211    /// let crypto = Crypto::new(Keys::new(&[0u8; 32], &[0u8; 32]).unwrap());
212    /// let data = b"Hello";
213    /// let encoded = crypto.encode(data);
214    /// ```
215    #[inline]
216    pub fn encode<T>(&self, data: T) -> String
217    where
218        T: AsRef<[u8]>,
219    {
220        URL_SAFE.encode(data)
221    }
222
223    /// Decrypts a package and verifies the HMAC signature.
224    ///
225    /// # Errors
226    ///
227    /// Returns [`CryptoError::InvalidSign`] if signature verification fails.
228    ///
229    /// # Example
230    ///
231    /// ```rust
232    /// use signed_crypto::{Crypto, Keys};
233    ///
234    /// let crypto = Crypto::new(Keys::new(&[0u8; 32], &[0u8; 32]).unwrap());
235    /// let mut pkg = crypto.init_plain_data(5, None).unwrap();
236    /// crypto.set_payload(&mut pkg, b"Hello").unwrap();
237    /// let encrypted = crypto.encrypt(&pkg).unwrap();
238    /// let decrypted = crypto.decrypt(&encrypted).unwrap();
239    /// ```
240    #[inline]
241    pub fn decrypt(&self, cipher_data: &[u8]) -> Result<Vec<u8>, CryptoError> {
242        if cipher_data.len() < Self::OVERHEAD_SIZE {
243            return Err(CryptoError::DataTooShort);
244        }
245
246        let mut data = cipher_data.to_vec();
247        let data_size = data.len();
248
249        self.xor_payload(&mut data)?;
250
251        let confirmation_signature = self.hmac_signature(&data)?;
252        let integrity_signature = self.read_i32(&data, data_size - Self::SIGNATURE_SIZE);
253        self.write_i32(
254            &mut data,
255            data_size - Self::SIGNATURE_SIZE,
256            confirmation_signature,
257        );
258
259        if confirmation_signature != integrity_signature {
260            return Err(CryptoError::InvalidSign);
261        }
262
263        Ok(data)
264    }
265
266    /// Encrypts a package in-place.
267    ///
268    /// # Example
269    ///
270    /// ```rust
271    /// use signed_crypto::{Crypto, Keys};
272    ///
273    /// let crypto = Crypto::new(Keys::new(&[0u8; 32], &[0u8; 32]).unwrap());
274    /// let mut pkg = crypto.init_plain_data(5, None).unwrap();
275    /// crypto.set_payload(&mut pkg, b"Hello").unwrap();
276    /// let encrypted = crypto.encrypt(&pkg).unwrap();
277    /// ```
278    #[inline]
279    pub fn encrypt(&self, plain_data: &[u8]) -> Result<Vec<u8>, CryptoError> {
280        if plain_data.len() < Self::OVERHEAD_SIZE {
281            return Err(CryptoError::DataTooShort);
282        }
283
284        let mut data = plain_data.to_vec();
285        let data_size = data.len();
286        let signature = self.hmac_signature(&data)?;
287        self.write_i32(&mut data, data_size - Self::SIGNATURE_SIZE, signature);
288
289        self.xor_payload(&mut data)?;
290
291        Ok(data)
292    }
293
294    /// Packages a payload into a URL-safe Base64 encoded encrypted string.
295    ///
296    /// # Arguments
297    ///
298    /// * `payload` - The data to encrypt
299    /// * `iv` - Optional custom initialization vector; a random IV with the
300    ///   current timestamp is generated when `None`
301    ///
302    /// # Example
303    ///
304    /// ```rust
305    /// use signed_crypto::{Crypto, Keys};
306    ///
307    /// let crypto = Crypto::new(Keys::new(&[0u8; 32], &[0u8; 32]).unwrap());
308    /// let encoded = crypto.package(b"Hello, world!", None).unwrap();
309    /// ```
310    #[inline]
311    pub fn package<T>(
312        &self,
313        payload: T,
314        iv: Option<&[u8]>,
315    ) -> Result<String, CryptoError>
316    where
317        T: AsRef<[u8]>,
318    {
319        let mut out = Vec::new();
320        self.package_to(payload, iv, &mut out)?;
321        // `package_to` writes Base64 output, which is always valid ASCII/UTF-8.
322        Ok(String::from_utf8(out).expect("base64 output is valid UTF-8"))
323    }
324
325    /// Unpackages and decrypts a URL-safe Base64 encoded encrypted string.
326    ///
327    /// # Errors
328    ///
329    /// Returns [`CryptoError::InvalidSign`] if signature verification fails.
330    ///
331    /// # Example
332    ///
333    /// ```rust
334    /// use signed_crypto::{Crypto, Keys};
335    ///
336    /// let crypto = Crypto::new(Keys::new(&[0u8; 32], &[0u8; 32]).unwrap());
337    /// let encoded = crypto.package(b"Hello, world!", None).unwrap();
338    /// let payload = crypto.unpackage(&encoded).unwrap();
339    /// assert_eq!(payload, b"Hello, world!");
340    /// ```
341    #[inline]
342    pub fn unpackage<T>(&self, data: T) -> Result<Vec<u8>, CryptoError>
343    where
344        T: AsRef<[u8]>,
345    {
346        let mut out = Vec::new();
347        self.unpackage_to(data, &mut out)?;
348        Ok(out)
349    }
350
351    /// Packages a payload and writes the URL-safe Base64 encoded encrypted
352    /// result into the provided writer.
353    ///
354    /// # Arguments
355    ///
356    /// * `payload` - The data to encrypt
357    /// * `iv` - Optional custom initialization vector; a random IV with the
358    ///   current timestamp is generated when `None`
359    /// * `out` - Any writer that receives the Base64-encoded encrypted package
360    ///
361    /// # Example
362    ///
363    /// ```rust
364    /// use signed_crypto::{Crypto, Keys};
365    ///
366    /// let crypto = Crypto::new(Keys::new(&[0u8; 32], &[0u8; 32]).unwrap());
367    /// let mut buf = Vec::new();
368    /// crypto.package_to(b"Hello, world!", None, &mut buf).unwrap();
369    /// ```
370    #[inline]
371    pub fn package_to<T, W>(
372        &self,
373        payload: T,
374        iv: Option<&[u8]>,
375        out: &mut W,
376    ) -> Result<(), CryptoError>
377    where
378        T: AsRef<[u8]>,
379        W: Write,
380    {
381        let payload = payload.as_ref();
382        let mut pkg = self.init_plain_data(payload.len(), iv)?;
383        self.set_payload(&mut pkg, payload)?;
384        let encrypted = self.encrypt(&pkg)?;
385        out.write_all(URL_SAFE.encode(&encrypted).as_bytes())?;
386        Ok(())
387    }
388
389    /// Unpackages and decrypts a URL-safe Base64 encoded string, writing the
390    /// decrypted payload into the provided writer.
391    ///
392    /// # Errors
393    ///
394    /// Returns [`CryptoError::InvalidSign`] if signature verification fails.
395    ///
396    /// # Example
397    ///
398    /// ```rust
399    /// use signed_crypto::{Crypto, Keys};
400    ///
401    /// let crypto = Crypto::new(Keys::new(&[0u8; 32], &[0u8; 32]).unwrap());
402    /// let encoded = crypto.package(b"Hello, world!", None).unwrap();
403    /// let mut buf = Vec::new();
404    /// crypto.unpackage_to(&encoded, &mut buf).unwrap();
405    /// assert_eq!(buf, b"Hello, world!");
406    /// ```
407    #[inline]
408    pub fn unpackage_to<T, W>(
409        &self,
410        data: T,
411        out: &mut W,
412    ) -> Result<(), CryptoError>
413    where
414        T: AsRef<[u8]>,
415        W: Write,
416    {
417        let decoded = self.decode(data)?;
418        let decrypted = self.decrypt(&decoded)?;
419        let payload = self.payload(&decrypted).ok_or(CryptoError::DataTooShort)?;
420        out.write_all(payload)?;
421        Ok(())
422    }
423
424    /// Creates a custom initialization vector.
425    ///
426    /// # Arguments
427    ///
428    /// * `timestamp` - The timestamp to embed
429    /// * `server_id` - The server ID to embed
430    ///
431    /// # Example
432    ///
433    /// ```rust
434    /// use signed_crypto::{Crypto, Keys};
435    /// use time::OffsetDateTime;
436    ///
437    /// let crypto = Crypto::new(Keys::new(&[0u8; 32], &[0u8; 32]).unwrap());
438    /// let iv = crypto.create_init_vector(OffsetDateTime::now_utc(), 12345);
439    /// ```
440    #[inline]
441    pub fn create_init_vector(&self, timestamp: OffsetDateTime, server_id: i64) -> Vec<u8> {
442        let timestamp = (timestamp.unix_timestamp_nanos() / 1_000) as i64; // microseconds
443        let mut iv = vec![0; Self::IV_SIZE];
444        self.write_i64(&mut iv, Self::IV_TIME_OFFSET, timestamp);
445        self.write_i64(&mut iv, Self::IV_SERVER_ID_OFFSET, server_id);
446        iv
447    }
448
449    /// Extracts the timestamp from a package's initialization vector.
450    ///
451    /// Returns `None` if the data is too short.
452    ///
453    /// # Example
454    ///
455    /// ```rust
456    /// use signed_crypto::{Crypto, Keys};
457    /// use time::OffsetDateTime;
458    ///
459    /// let crypto = Crypto::new(Keys::new(&[0u8; 32], &[0u8; 32]).unwrap());
460    /// let mut pkg = crypto.init_plain_data(5, None).unwrap();
461    /// crypto.set_payload(&mut pkg, b"Hello").unwrap();
462    /// let encrypted = crypto.encrypt(&pkg).unwrap();
463    /// let ts = crypto.timestamp(&encrypted).unwrap();
464    /// ```
465    #[inline]
466    pub fn timestamp(&self, data: &[u8]) -> Option<OffsetDateTime> {
467        if data.len() < Self::IV_SIZE {
468            return None;
469        }
470        let ts = self.read_i64(data, Self::IV_BASE + Self::IV_TIME_OFFSET);
471        Some(
472            UNIX_EPOCH
473                .checked_add(Duration::microseconds(ts))
474                .unwrap_or(UNIX_EPOCH),
475        )
476    }
477
478    /// Extracts the server ID from a package's initialization vector.
479    ///
480    /// Returns `None` if the data is too short.
481    ///
482    /// # Example
483    ///
484    /// ```rust
485    /// use signed_crypto::{Crypto, Keys};
486    ///
487    /// let crypto = Crypto::new(Keys::new(&[0u8; 32], &[0u8; 32]).unwrap());
488    /// let mut pkg = crypto.init_plain_data(5, None).unwrap();
489    /// crypto.set_payload(&mut pkg, b"Hello").unwrap();
490    /// let encrypted = crypto.encrypt(&pkg).unwrap();
491    /// let server_id = crypto.server_id(&encrypted).unwrap();
492    /// ```
493    #[inline]
494    pub fn server_id(&self, data: &[u8]) -> Option<i64> {
495        if data.len() < Self::IV_SIZE {
496            return None;
497        }
498        Some(self.read_i64(data, Self::IV_BASE + Self::IV_SERVER_ID_OFFSET))
499    }
500
501    /// Extracts the payload from a package without decryption.
502    ///
503    /// Returns `None` if the data is too short.
504    ///
505    /// # Example
506    ///
507    /// ```rust
508    /// use signed_crypto::{Crypto, Keys};
509    ///
510    /// let crypto = Crypto::new(Keys::new(&[0u8; 32], &[0u8; 32]).unwrap());
511    /// let mut pkg = crypto.init_plain_data(5, None).unwrap();
512    /// crypto.set_payload(&mut pkg, b"Hello").unwrap();
513    /// let payload = crypto.payload(&pkg).unwrap();
514    /// assert_eq!(payload, b"Hello");
515    /// ```
516    #[inline]
517    pub fn payload<'a>(&self, data: &'a [u8]) -> Option<&'a [u8]> {
518        if data.len() < Self::OVERHEAD_SIZE {
519            return None;
520        }
521        Some(&data[Self::PAYLOAD_BASE..data.len() - Self::SIGNATURE_SIZE])
522    }
523
524    /// Initializes a plain data package buffer.
525    ///
526    /// If `iv` is `None`, generates a random IV with current timestamp.
527    ///
528    /// # Arguments
529    ///
530    /// * `payload_size` - Size of the payload in bytes
531    /// * `iv` - Optional custom initialization vector
532    ///
533    /// # Example
534    ///
535    /// ```rust
536    /// use signed_crypto::{Crypto, Keys};
537    ///
538    /// let crypto = Crypto::new(Keys::new(&[0u8; 32], &[0u8; 32]).unwrap());
539    /// let pkg = crypto.init_plain_data(10, None).unwrap();
540    /// ```
541    #[inline]
542    pub fn init_plain_data(
543        &self,
544        payload_size: usize,
545        iv: Option<&[u8]>,
546    ) -> Result<Vec<u8>, CryptoError> {
547        let mut plain_data = vec![0; Self::OVERHEAD_SIZE + payload_size];
548        if let Some(iv) = iv {
549            plain_data[Self::IV_BASE..Self::IV_BASE + Self::IV_SIZE].copy_from_slice(iv);
550        } else {
551            let now = (OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000) as i64;
552            self.write_i64(&mut plain_data, Self::IV_TIME_OFFSET, now);
553            self.write_i64(
554                &mut plain_data,
555                Self::IV_SERVER_ID_OFFSET,
556                rand::random::<i64>(),
557            );
558        }
559
560        Ok(plain_data)
561    }
562
563    /// Sets the payload in a plain data package buffer.
564    ///
565    /// # Errors
566    ///
567    /// Returns [`CryptoError::PayloadSizeMismatch`] if the payload size
568    /// does not match the expected size.
569    ///
570    /// # Example
571    ///
572    /// ```rust
573    /// use signed_crypto::{Crypto, Keys};
574    ///
575    /// let crypto = Crypto::new(Keys::new(&[0u8; 32], &[0u8; 32]).unwrap());
576    /// let mut pkg = crypto.init_plain_data(5, None).unwrap();
577    /// crypto.set_payload(&mut pkg, b"Hello").unwrap();
578    /// ```
579    #[inline]
580    pub fn set_payload(&self, plain_data: &mut [u8], payload: &[u8]) -> Result<(), CryptoError> {
581        if payload.len() != plain_data.len() - Self::OVERHEAD_SIZE {
582            return Err(CryptoError::PayloadSizeMismatch);
583        }
584        plain_data[Self::PAYLOAD_BASE..Self::PAYLOAD_BASE + payload.len()].copy_from_slice(payload);
585        Ok(())
586    }
587
588    #[inline]
589    fn read_i32(&self, data: &[u8], offset: usize) -> i32 {
590        BigEndian::read_i32(&data[offset..offset + 4])
591    }
592
593    #[inline]
594    fn read_i64(&self, data: &[u8], offset: usize) -> i64 {
595        BigEndian::read_i64(&data[offset..offset + 8])
596    }
597
598    #[inline]
599    fn write_i32(&self, data: &mut [u8], offset: usize, value: i32) {
600        BigEndian::write_i32(&mut data[offset..offset + 4], value);
601    }
602
603    #[inline]
604    fn write_i64(&self, data: &mut [u8], offset: usize, value: i64) {
605        BigEndian::write_i64(&mut data[offset..offset + 8], value);
606    }
607
608    #[inline]
609    fn xor_payload(&self, data: &mut [u8]) -> Result<(), CryptoError> {
610        let iv: &[u8; 16] = &data[Self::IV_BASE..Self::IV_BASE + Self::IV_SIZE]
611            .try_into()
612            .map_err(|_| CryptoError::InvalidInitVector)?;
613
614        let mut cipher = Aes256Ctr64BE::new(&self.keys.encryption_key.into(), iv.into());
615        let data_size = data.len();
616        cipher.apply_keystream(&mut data[Self::PAYLOAD_BASE..data_size - Self::SIGNATURE_SIZE]);
617
618        Ok(())
619    }
620
621    #[inline]
622    fn hmac_signature(&self, data: &[u8]) -> Result<i32, CryptoError> {
623        let mut mac = HmacSha256::new_from_slice(&self.keys.integrity_key)
624            .map_err(|_| CryptoError::InvalidKey)?;
625
626        mac.update(&data[Self::PAYLOAD_BASE..data.len() - Self::SIGNATURE_SIZE]);
627        mac.update(&data[Self::IV_BASE..Self::IV_BASE + Self::IV_SIZE]);
628
629        let b = mac.finalize().into_bytes();
630
631        Ok(self.read_i32(&b, 0))
632    }
633}
634
635#[cfg(test)]
636mod tests {
637    use super::*;
638    use base64::prelude::*;
639
640    static TEST_ENCRYPTION_KEY: &str = "sIxwz7yw62yrfoLGt12lIHKuYrK/S5kLuApI2BQe7Ac=";
641    static TEST_INTEGRITY_KEY: &str = "v3fsVcMBMMHYzRhi7SpM0sdqwzvAxM6KPTu9OtVod5I=";
642
643    fn create_keys() -> Keys {
644        Keys::new(
645            &BASE64_STANDARD.decode(TEST_ENCRYPTION_KEY).unwrap(),
646            &BASE64_STANDARD.decode(TEST_INTEGRITY_KEY).unwrap(),
647        )
648        .unwrap()
649    }
650
651    #[test]
652    fn test_decode() {
653        let crypto = Crypto::new(create_keys());
654        let encoded = "aGVsbG8sIHdvcmxk";
655        let decoded = crypto.decode(encoded).unwrap();
656        assert_eq!(decoded, b"hello, world");
657    }
658
659    #[test]
660    fn test_encode() {
661        let crypto = Crypto::new(create_keys());
662        let data = b"hello, world";
663        let encoded = crypto.encode(data);
664        assert_eq!(encoded, "aGVsbG8sIHdvcmxk");
665    }
666
667    #[test]
668    fn test_decrypt() {
669        let crypto = Crypto::new(create_keys());
670        let timestamp = OffsetDateTime::UNIX_EPOCH + Duration::seconds(1);
671        let iv = crypto.create_init_vector(timestamp, 123456789);
672        let payload = "https://example.com".as_bytes();
673
674        let mut plain_data = crypto.init_plain_data(payload.len(), Some(&iv)).unwrap();
675        crypto.set_payload(&mut plain_data, payload).unwrap();
676        let encrypted_data = crypto.encrypt(&plain_data).unwrap();
677
678        assert_eq!(crypto.timestamp(&iv), Some(timestamp));
679        assert_eq!(crypto.server_id(&iv), Some(123456789));
680        assert_eq!(
681            crypto.payload(&encrypted_data).unwrap().len(),
682            payload.len()
683        );
684        assert_ne!(crypto.payload(&encrypted_data), Some(payload));
685
686        let decrypted_data = crypto.decrypt(&encrypted_data).unwrap();
687        assert_eq!(crypto.timestamp(&decrypted_data), Some(timestamp));
688        assert_eq!(crypto.server_id(&decrypted_data), Some(123456789));
689        assert_eq!(crypto.payload(&decrypted_data), Some(payload));
690
691        let mut encrypted_data_invalid_sign = encrypted_data.clone();
692        crypto.write_i32(
693            &mut encrypted_data_invalid_sign,
694            encrypted_data.len() - Crypto::SIGNATURE_SIZE,
695            123456789,
696        );
697        assert!(matches!(
698            crypto.decrypt(&encrypted_data_invalid_sign),
699            Err(CryptoError::InvalidSign)
700        ));
701        assert_ne!(crypto.payload(&encrypted_data_invalid_sign), Some(payload))
702    }
703
704    #[test]
705    fn test_create_init_vector() {
706        let crypto = Crypto::new(create_keys());
707        let timestamp = OffsetDateTime::UNIX_EPOCH + Duration::seconds(1);
708        let iv = crypto.create_init_vector(timestamp, 123456789);
709        assert_eq!(iv.len(), Crypto::IV_SIZE);
710        assert_eq!(crypto.read_i64(&iv, Crypto::IV_TIME_OFFSET), 1_000_000);
711        assert_eq!(crypto.read_i64(&iv, Crypto::IV_SERVER_ID_OFFSET), 123456789);
712        assert_eq!(crypto.timestamp(&iv), Some(timestamp));
713        assert_eq!(crypto.server_id(&iv), Some(123456789));
714    }
715
716    #[test]
717    fn test_init_plain_data() {
718        let crypto = Crypto::new(create_keys());
719        let payload = "https://example.com".as_bytes();
720
721        let mut plain_data = crypto.init_plain_data(payload.len(), None).unwrap();
722        crypto.set_payload(&mut plain_data, payload).unwrap();
723
724        assert_eq!(plain_data.len(), Crypto::OVERHEAD_SIZE + payload.len());
725        assert_eq!(crypto.payload(&plain_data), Some(payload));
726    }
727
728    #[test]
729    fn test_init_plain_data_empty_payload() {
730        let crypto = Crypto::new(create_keys());
731        let payload = "".as_bytes();
732
733        let mut plain_data = crypto.init_plain_data(0, None).unwrap();
734        crypto.set_payload(&mut plain_data, payload).unwrap();
735        assert_eq!(crypto.payload(&plain_data), Some(payload));
736    }
737
738    #[test]
739    fn test_package_unpackage() {
740        let crypto = Crypto::new(create_keys());
741        let payload = b"Hello, world!".as_slice();
742
743        let encoded = crypto.package(payload, None).unwrap();
744        assert_ne!(encoded, "");
745
746        let decoded = crypto.unpackage(&encoded).unwrap();
747        assert_eq!(decoded, payload);
748    }
749
750    #[test]
751    fn test_package_unpackage_with_iv() {
752        let crypto = Crypto::new(create_keys());
753        let timestamp = OffsetDateTime::UNIX_EPOCH + Duration::seconds(1);
754        let iv = crypto.create_init_vector(timestamp, 123456789);
755        let payload = b"https://example.com".as_slice();
756
757        let encoded = crypto.package(payload, Some(&iv)).unwrap();
758
759        // Metadata is still readable from the base64-encoded package.
760        let decoded = crypto.decode(encoded.as_bytes()).unwrap();
761        assert_eq!(crypto.timestamp(&decoded), Some(timestamp));
762        assert_eq!(crypto.server_id(&decoded), Some(123456789));
763
764        let recovered = crypto.unpackage(&encoded).unwrap();
765        assert_eq!(recovered, payload);
766    }
767
768    #[test]
769    fn test_package_unpackage_empty_payload() {
770        let crypto = Crypto::new(create_keys());
771        let payload = b"".as_slice();
772
773        let encoded = crypto.package(payload, None).unwrap();
774        let recovered = crypto.unpackage(&encoded).unwrap();
775        assert_eq!(recovered, payload);
776    }
777
778    #[test]
779    fn test_unpackage_tampered_signature() {
780        let crypto = Crypto::new(create_keys());
781        let encoded = crypto.package(b"Hello, world!", None).unwrap();
782
783        let mut bytes = crypto.decode(encoded.as_bytes()).unwrap();
784        let last = bytes.len() - Crypto::SIGNATURE_SIZE;
785        crypto.write_i32(&mut bytes, last, 123456789);
786
787        let tampered = crypto.encode(&bytes);
788        assert!(matches!(
789            crypto.unpackage(&tampered),
790            Err(CryptoError::InvalidSign)
791        ));
792    }
793
794    #[test]
795    fn test_package_to_matches_package() {
796        let crypto = Crypto::new(create_keys());
797        let timestamp = OffsetDateTime::UNIX_EPOCH + Duration::seconds(1);
798        let iv = crypto.create_init_vector(timestamp, 123456789);
799        let payload = b"https://example.com".as_slice();
800
801        // Same IV + payload must yield identical output.
802        let encoded_alloc = crypto.package(payload, Some(&iv)).unwrap();
803
804        let mut buf = Vec::new();
805        crypto.package_to(payload, Some(&iv), &mut buf).unwrap();
806        assert_eq!(buf, encoded_alloc.as_bytes());
807    }
808
809    #[test]
810    fn test_package_to_unpackage_to_roundtrip() {
811        let crypto = Crypto::new(create_keys());
812        let payload = b"Hello, world!".as_slice();
813
814        let mut enc_buf = Vec::new();
815        crypto.package_to(payload, None, &mut enc_buf).unwrap();
816
817        let mut dec_buf = Vec::new();
818        crypto.unpackage_to(&enc_buf, &mut dec_buf).unwrap();
819        assert_eq!(dec_buf, payload);
820    }
821
822    #[test]
823    fn test_package_to_appends_and_preserves_existing() {
824        let crypto = Crypto::new(create_keys());
825        let payload = b"Hello".as_slice();
826
827        let mut buf = b"prefix".to_vec();
828        let prefix_len = buf.len();
829        crypto.package_to(payload, None, &mut buf).unwrap();
830
831        // Existing bytes are preserved; encoded output is appended.
832        assert_eq!(&buf[..prefix_len], b"prefix");
833        assert!(buf.len() > prefix_len);
834
835        let mut dec_buf = Vec::new();
836        crypto.unpackage_to(&buf[prefix_len..], &mut dec_buf).unwrap();
837        assert_eq!(dec_buf, payload);
838    }
839
840    #[test]
841    fn test_package_to_empty_payload() {
842        let crypto = Crypto::new(create_keys());
843
844        let mut enc_buf = Vec::new();
845        crypto.package_to(b"", None, &mut enc_buf).unwrap();
846
847        let mut dec_buf = Vec::new();
848        crypto.unpackage_to(&enc_buf, &mut dec_buf).unwrap();
849        assert_eq!(dec_buf, b"");
850    }
851
852    #[test]
853    fn test_unpackage_to_tampered_signature() {
854        let crypto = Crypto::new(create_keys());
855
856        let mut enc_buf = Vec::new();
857        crypto.package_to(b"Hello", None, &mut enc_buf).unwrap();
858
859        // Tamper via the allocating decode/encode helpers, then feed the
860        // streaming unpackage path.
861        let mut raw = crypto.decode(&enc_buf).unwrap();
862        let last = raw.len() - Crypto::SIGNATURE_SIZE;
863        crypto.write_i32(&mut raw, last, 123456789);
864        let tampered = crypto.encode(&raw);
865
866        let mut dec_buf = Vec::new();
867        assert!(matches!(
868            crypto.unpackage_to(tampered.as_bytes(), &mut dec_buf),
869            Err(CryptoError::InvalidSign)
870        ));
871    }
872
873    #[test]
874    fn test_package_to_with_non_vec_writer() {
875        // Exercises the generic `io::Write` path through a writer whose
876        // `Write` impl is not `Vec<u8>`'s (BufWriter buffers, then flushes).
877        let crypto = Crypto::new(create_keys());
878        let payload = b"Hello, world!".as_slice();
879
880        let mut writer = std::io::BufWriter::new(Vec::<u8>::new());
881        crypto.package_to(payload, None, &mut writer).unwrap();
882        let encoded = writer.into_inner().unwrap();
883
884        let mut dec_buf = Vec::new();
885        crypto.unpackage_to(&encoded, &mut dec_buf).unwrap();
886        assert_eq!(dec_buf, payload);
887    }
888
889    #[test]
890    fn test_unpackage_to_appends_to_existing_buffer() {
891        // Symmetric to `test_package_to_appends_and_preserves_existing`:
892        // unpackage_to must append, not overwrite.
893        let crypto = Crypto::new(create_keys());
894        let payload = b"Hello".as_slice();
895        let encoded = crypto.package(payload, None).unwrap();
896
897        let mut buf = b"prefix".to_vec();
898        let prefix_len = buf.len();
899        crypto.unpackage_to(&encoded, &mut buf).unwrap();
900
901        assert_eq!(&buf[..prefix_len], b"prefix");
902        assert_eq!(&buf[prefix_len..], payload);
903    }
904}