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//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
29//! // WARNING: Never use all-zero keys in production!
30//! // Generate secure random keys using a cryptographic RNG.
31//! let keys = Keys::new(&[0u8; 32], &[0u8; 32])?;
32//! let crypto = Crypto::new(keys);
33//!
34//! // Encrypt
35//! let payload = b"Hello, world!";
36//! let mut pkg = crypto.init_plain_data(payload.len(), None)?;
37//! crypto.set_payload(&mut pkg, payload)?;
38//! let encrypted = crypto.encrypt(&pkg)?;
39//!
40//! // Decrypt
41//! let decrypted = crypto.decrypt(&encrypted)?;
42//! assert_eq!(crypto.payload(&decrypted), Some(payload.as_slice()));
43//! # Ok(())
44//! # }
45//! ```
46
47use aes::cipher::{KeyIvInit, StreamCipher};
48use base64::{engine::general_purpose::URL_SAFE, Engine as _};
49use byteorder::{BigEndian, ByteOrder};
50use hmac::{Hmac, Mac};
51use sha2::Sha256;
52use thiserror::Error;
53use time::{Duration, OffsetDateTime};
54
55type HmacSha256 = Hmac<Sha256>;
56type Aes256Ctr64BE = ctr::Ctr64BE<aes::Aes256>;
57
58const UNIX_EPOCH: OffsetDateTime = time::OffsetDateTime::UNIX_EPOCH;
59
60/// Holds the encryption and integrity keys.
61///
62/// Both keys must be exactly 32 bytes (256 bits).
63///
64/// # Fields
65///
66/// * `encryption_key` - AES-256 encryption key
67/// * `integrity_key` - HMAC-SHA256 integrity key
68#[derive(Clone, Debug)]
69pub struct Keys {
70    /// AES-256 encryption key (32 bytes)
71    pub encryption_key: [u8; 32],
72    /// HMAC-SHA256 integrity key (32 bytes)
73    pub integrity_key: [u8; 32],
74}
75
76impl Keys {
77    /// Creates a new `Keys` instance from raw byte slices.
78    ///
79    /// Both keys must be exactly 32 bytes.
80    ///
81    /// # Errors
82    ///
83    /// Returns [`CryptoError::InvalidKey`] if either key is not 32 bytes.
84    ///
85    /// # Example
86    ///
87    /// ```rust
88    /// use signed_crypto::Keys;
89    ///
90    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
91    /// // WARNING: Never use all-zero keys in production!
92    /// // Generate secure random keys using a cryptographic RNG.
93    /// let enc_key = [0u8; 32];
94    /// let int_key = [0u8; 32];
95    /// let keys = Keys::new(&enc_key, &int_key)?;
96    /// # Ok(())
97    /// # }
98    /// ```
99    pub fn new(encryption_key: &[u8], integrity_key: &[u8]) -> Result<Self, CryptoError> {
100        let encryption_key: [u8; 32] = encryption_key
101            .try_into()
102            .map_err(|_| CryptoError::InvalidKey)?;
103        let integrity_key: [u8; 32] = integrity_key
104            .try_into()
105            .map_err(|_| CryptoError::InvalidKey)?;
106
107        Ok(Self {
108            encryption_key,
109            integrity_key,
110        })
111    }
112}
113
114/// Errors that can occur during cryptographic operations.
115#[derive(Error, Debug)]
116pub enum CryptoError {
117    /// Key is not exactly 32 bytes.
118    #[error("invalid key")]
119    InvalidKey,
120    /// HMAC signature verification failed.
121    #[error("invalid signature")]
122    InvalidSign,
123    /// Initialization vector is invalid.
124    #[error("invalid init vector")]
125    InvalidInitVector,
126    /// Data is too short to be a valid package.
127    #[error("data too short")]
128    DataTooShort,
129    /// Payload size does not match expected size.
130    #[error("payload size mismatch")]
131    PayloadSizeMismatch,
132    /// Base64 decoding failed.
133    #[error("decode error: {0}")]
134    DecodeError(#[from] base64::DecodeError),
135}
136
137/// Main cryptographic operations instance.
138///
139/// Holds the keys and provides methods for encryption, decryption,
140/// and metadata extraction.
141///
142/// # Example
143///
144/// ```rust
145/// use signed_crypto::{Crypto, Keys};
146///
147/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
148/// // WARNING: Never use all-zero keys in production!
149/// let keys = Keys::new(&[0u8; 32], &[0u8; 32])?;
150/// let crypto = Crypto::new(keys);
151/// # Ok(())
152/// # }
153/// ```
154pub struct Crypto {
155    /// The encryption and integrity keys.
156    pub keys: Keys,
157}
158
159impl Crypto {
160    /// Creates a new `Crypto` instance.
161    ///
162    /// # Example
163    ///
164    /// ```rust
165    /// use signed_crypto::{Crypto, Keys};
166    ///
167    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
168    /// // WARNING: Never use all-zero keys in production!
169    /// let keys = Keys::new(&[0u8; 32], &[0u8; 32])?;
170    /// let crypto = Crypto::new(keys);
171    /// # Ok(())
172    /// # }
173    /// ```
174    pub fn new(keys: Keys) -> Self {
175        Self { keys }
176    }
177
178    /// Offset of the initialization vector in a package.
179    pub const IV_BASE: usize = 0;
180    /// Size of the initialization vector in bytes.
181    pub const IV_SIZE: usize = 16;
182    /// Offset of the timestamp within the IV.
183    pub const IV_TIME_OFFSET: usize = 0;
184    /// Size of the timestamp in bytes.
185    pub const IV_TIME_SIZE: usize = 8;
186    /// Offset of the server ID within the IV.
187    pub const IV_SERVER_ID_OFFSET: usize = 8;
188    /// Size of the server ID in bytes.
189    pub const IV_SERVER_ID_SIZE: usize = 8;
190    /// Size of the HMAC signature in bytes.
191    pub const SIGNATURE_SIZE: usize = 4;
192    /// Offset where the payload begins.
193    pub const PAYLOAD_BASE: usize = Crypto::IV_BASE + Crypto::IV_SIZE;
194    /// Total overhead size (IV + signature) in bytes.
195    pub const OVERHEAD_SIZE: usize = Crypto::IV_SIZE + Crypto::SIGNATURE_SIZE;
196
197    /// Decodes a URL-safe Base64 encoded string.
198    ///
199    /// # Example
200    ///
201    /// ```rust
202    /// use signed_crypto::{Crypto, Keys};
203    ///
204    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
205    /// // WARNING: Never use all-zero keys in production!
206    /// let crypto = Crypto::new(Keys::new(&[0u8; 32], &[0u8; 32])?);
207    /// let encoded = "SGVsbG8=";
208    /// let decoded = crypto.decode(encoded)?;
209    /// # Ok(())
210    /// # }
211    /// ```
212    #[inline]
213    pub fn decode<T>(&self, data: T) -> Result<Vec<u8>, CryptoError>
214    where
215        T: AsRef<[u8]>,
216    {
217        URL_SAFE
218            .decode(data)
219            .map(|v| v.to_vec())
220            .map_err(|e| e.into())
221    }
222
223    /// Encodes data as a URL-safe Base64 string.
224    ///
225    /// # Example
226    ///
227    /// ```rust
228    /// use signed_crypto::{Crypto, Keys};
229    ///
230    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
231    /// // WARNING: Never use all-zero keys in production!
232    /// let crypto = Crypto::new(Keys::new(&[0u8; 32], &[0u8; 32])?);
233    /// let data = b"Hello";
234    /// let encoded = crypto.encode(data);
235    /// # Ok(())
236    /// # }
237    /// ```
238    #[inline]
239    pub fn encode<T>(&self, data: T) -> String
240    where
241        T: AsRef<[u8]>,
242    {
243        URL_SAFE.encode(data)
244    }
245
246    /// Decrypts a package and verifies the HMAC signature.
247    ///
248    /// # Errors
249    ///
250    /// Returns [`CryptoError::InvalidSign`] if signature verification fails.
251    ///
252    /// # Example
253    ///
254    /// ```rust
255    /// use signed_crypto::{Crypto, Keys};
256    ///
257    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
258    /// // WARNING: Never use all-zero keys in production!
259    /// let crypto = Crypto::new(Keys::new(&[0u8; 32], &[0u8; 32])?);
260    /// let mut pkg = crypto.init_plain_data(5, None)?;
261    /// crypto.set_payload(&mut pkg, b"Hello")?;
262    /// let encrypted = crypto.encrypt(&pkg)?;
263    /// let decrypted = crypto.decrypt(&encrypted)?;
264    /// # Ok(())
265    /// # }
266    /// ```
267    #[inline]
268    pub fn decrypt(&self, cipher_data: &[u8]) -> Result<Vec<u8>, CryptoError> {
269        if cipher_data.len() < Self::OVERHEAD_SIZE {
270            return Err(CryptoError::DataTooShort);
271        }
272
273        let mut data = cipher_data.to_vec();
274        let data_size = data.len();
275
276        self.xor_payload(&mut data)?;
277
278        let confirmation_signature = self.hmac_signature(&data)?;
279        let integrity_signature = self.read_i32(&data, data_size - Self::SIGNATURE_SIZE);
280        self.write_i32(
281            &mut data,
282            data_size - Self::SIGNATURE_SIZE,
283            confirmation_signature,
284        );
285
286        if confirmation_signature != integrity_signature {
287            return Err(CryptoError::InvalidSign);
288        }
289
290        Ok(data)
291    }
292
293    /// Encrypts a package in-place.
294    ///
295    /// # Example
296    ///
297    /// ```rust
298    /// use signed_crypto::{Crypto, Keys};
299    ///
300    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
301    /// // WARNING: Never use all-zero keys in production!
302    /// let crypto = Crypto::new(Keys::new(&[0u8; 32], &[0u8; 32])?);
303    /// let mut pkg = crypto.init_plain_data(5, None)?;
304    /// crypto.set_payload(&mut pkg, b"Hello")?;
305    /// let encrypted = crypto.encrypt(&pkg)?;
306    /// # Ok(())
307    /// # }
308    /// ```
309    #[inline]
310    pub fn encrypt(&self, plain_data: &[u8]) -> Result<Vec<u8>, CryptoError> {
311        if plain_data.len() < Self::OVERHEAD_SIZE {
312            return Err(CryptoError::DataTooShort);
313        }
314
315        let mut data = plain_data.to_vec();
316        let data_size = data.len();
317        let signature = self.hmac_signature(&data)?;
318        self.write_i32(&mut data, data_size - Self::SIGNATURE_SIZE, signature);
319
320        self.xor_payload(&mut data)?;
321
322        Ok(data)
323    }
324
325    /// Creates a custom initialization vector.
326    ///
327    /// # Arguments
328    ///
329    /// * `timestamp` - The timestamp to embed
330    /// * `server_id` - The server ID to embed
331    ///
332    /// # Example
333    ///
334    /// ```rust
335    /// use signed_crypto::{Crypto, Keys};
336    /// use time::OffsetDateTime;
337    ///
338    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
339    /// let crypto = Crypto::new(Keys::new(&[0u8; 32], &[0u8; 32])?);
340    /// let iv = crypto.create_init_vector(OffsetDateTime::now_utc(), 12345);
341    /// # Ok(())
342    /// # }
343    /// ```
344    #[inline]
345    pub fn create_init_vector(&self, timestamp: OffsetDateTime, server_id: i64) -> Vec<u8> {
346        let timestamp = (timestamp.unix_timestamp_nanos() / 1_000) as i64; // microseconds
347        let mut iv = vec![0; Self::IV_SIZE];
348        self.write_i64(&mut iv, Self::IV_TIME_OFFSET, timestamp);
349        self.write_i64(&mut iv, Self::IV_SERVER_ID_OFFSET, server_id);
350        iv
351    }
352
353    /// Extracts the timestamp from a package's initialization vector.
354    ///
355    /// Returns `None` if the data is too short.
356    ///
357    /// # Example
358    ///
359    /// ```rust
360    /// use signed_crypto::{Crypto, Keys};
361    /// use time::OffsetDateTime;
362    ///
363    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
364    /// let crypto = Crypto::new(Keys::new(&[0u8; 32], &[0u8; 32])?);
365    /// let mut pkg = crypto.init_plain_data(5, None)?;
366    /// crypto.set_payload(&mut pkg, b"Hello")?;
367    /// let encrypted = crypto.encrypt(&pkg)?;
368    /// let ts = crypto.timestamp(&encrypted).unwrap();
369    /// # Ok(())
370    /// # }
371    /// ```
372    #[inline]
373    pub fn timestamp(&self, data: &[u8]) -> Option<OffsetDateTime> {
374        if data.len() < Self::IV_SIZE {
375            return None;
376        }
377        let ts = self.read_i64(data, Self::IV_BASE + Self::IV_TIME_OFFSET);
378        Some(
379            UNIX_EPOCH
380                .checked_add(Duration::microseconds(ts))
381                .unwrap_or(UNIX_EPOCH),
382        )
383    }
384
385    /// Extracts the server ID from a package's initialization vector.
386    ///
387    /// Returns `None` if the data is too short.
388    ///
389    /// # Example
390    ///
391    /// ```rust
392    /// use signed_crypto::{Crypto, Keys};
393    ///
394    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
395    /// // WARNING: Never use all-zero keys in production!
396    /// let crypto = Crypto::new(Keys::new(&[0u8; 32], &[0u8; 32])?);
397    /// let mut pkg = crypto.init_plain_data(5, None)?;
398    /// crypto.set_payload(&mut pkg, b"Hello")?;
399    /// let encrypted = crypto.encrypt(&pkg)?;
400    /// let server_id = crypto.server_id(&encrypted).unwrap();
401    /// # Ok(())
402    /// # }
403    /// ```
404    #[inline]
405    pub fn server_id(&self, data: &[u8]) -> Option<i64> {
406        if data.len() < Self::IV_SIZE {
407            return None;
408        }
409        Some(self.read_i64(data, Self::IV_BASE + Self::IV_SERVER_ID_OFFSET))
410    }
411
412    /// Extracts the payload from a package without decryption.
413    ///
414    /// Returns `None` if the data is too short.
415    ///
416    /// # Example
417    ///
418    /// ```rust
419    /// use signed_crypto::{Crypto, Keys};
420    ///
421    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
422    /// // WARNING: Never use all-zero keys in production!
423    /// let crypto = Crypto::new(Keys::new(&[0u8; 32], &[0u8; 32])?);
424    /// let mut pkg = crypto.init_plain_data(5, None)?;
425    /// crypto.set_payload(&mut pkg, b"Hello")?;
426    /// let payload = crypto.payload(&pkg).unwrap();
427    /// assert_eq!(payload, b"Hello");
428    /// # Ok(())
429    /// # }
430    /// ```
431    #[inline]
432    pub fn payload<'a>(&self, data: &'a [u8]) -> Option<&'a [u8]> {
433        if data.len() < Self::OVERHEAD_SIZE {
434            return None;
435        }
436        Some(&data[Self::PAYLOAD_BASE..data.len() - Self::SIGNATURE_SIZE])
437    }
438
439    /// Initializes a plain data package buffer.
440    ///
441    /// If `iv` is `None`, generates a random IV with current timestamp.
442    ///
443    /// # Arguments
444    ///
445    /// * `payload_size` - Size of the payload in bytes
446    /// * `iv` - Optional custom initialization vector
447    ///
448    /// # Example
449    ///
450    /// ```rust
451    /// use signed_crypto::{Crypto, Keys};
452    ///
453    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
454    /// // WARNING: Never use all-zero keys in production!
455    /// let crypto = Crypto::new(Keys::new(&[0u8; 32], &[0u8; 32])?);
456    /// let pkg = crypto.init_plain_data(10, None)?;
457    /// # Ok(())
458    /// # }
459    /// ```
460    #[inline]
461    pub fn init_plain_data(
462        &self,
463        payload_size: usize,
464        iv: Option<&[u8]>,
465    ) -> Result<Vec<u8>, CryptoError> {
466        let mut plain_data = vec![0; Self::OVERHEAD_SIZE + payload_size];
467        if let Some(iv) = iv {
468            plain_data[Self::IV_BASE..Self::IV_BASE + Self::IV_SIZE].copy_from_slice(iv);
469        } else {
470            let now = (OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000) as i64;
471            self.write_i64(&mut plain_data, Self::IV_TIME_OFFSET, now);
472            self.write_i64(
473                &mut plain_data,
474                Self::IV_SERVER_ID_OFFSET,
475                rand::random::<i64>(),
476            );
477        }
478
479        Ok(plain_data)
480    }
481
482    /// Sets the payload in a plain data package buffer.
483    ///
484    /// # Errors
485    ///
486    /// Returns [`CryptoError::PayloadSizeMismatch`] if the payload size
487    /// does not match the expected size.
488    ///
489    /// # Example
490    ///
491    /// ```rust
492    /// use signed_crypto::{Crypto, Keys};
493    ///
494    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
495    /// // WARNING: Never use all-zero keys in production!
496    /// let crypto = Crypto::new(Keys::new(&[0u8; 32], &[0u8; 32])?);
497    /// let mut pkg = crypto.init_plain_data(5, None)?;
498    /// crypto.set_payload(&mut pkg, b"Hello")?;
499    /// # Ok(())
500    /// # }
501    /// ```
502    #[inline]
503    pub fn set_payload(&self, plain_data: &mut [u8], payload: &[u8]) -> Result<(), CryptoError> {
504        if payload.len() != plain_data.len() - Self::OVERHEAD_SIZE {
505            return Err(CryptoError::PayloadSizeMismatch);
506        }
507        plain_data[Self::PAYLOAD_BASE..Self::PAYLOAD_BASE + payload.len()].copy_from_slice(payload);
508        Ok(())
509    }
510
511    #[inline]
512    fn read_i32(&self, data: &[u8], offset: usize) -> i32 {
513        BigEndian::read_i32(&data[offset..offset + 4])
514    }
515
516    #[inline]
517    fn read_i64(&self, data: &[u8], offset: usize) -> i64 {
518        BigEndian::read_i64(&data[offset..offset + 8])
519    }
520
521    #[inline]
522    fn write_i32(&self, data: &mut [u8], offset: usize, value: i32) {
523        BigEndian::write_i32(&mut data[offset..offset + 4], value);
524    }
525
526    #[inline]
527    fn write_i64(&self, data: &mut [u8], offset: usize, value: i64) {
528        BigEndian::write_i64(&mut data[offset..offset + 8], value);
529    }
530
531    #[inline]
532    fn xor_payload(&self, data: &mut [u8]) -> Result<(), CryptoError> {
533        let iv: &[u8; 16] = &data[Self::IV_BASE..Self::IV_BASE + Self::IV_SIZE]
534            .try_into()
535            .map_err(|_| CryptoError::InvalidInitVector)?;
536
537        let mut cipher = Aes256Ctr64BE::new(&self.keys.encryption_key.into(), iv.into());
538        let data_size = data.len();
539        cipher.apply_keystream(&mut data[Self::PAYLOAD_BASE..data_size - Self::SIGNATURE_SIZE]);
540
541        Ok(())
542    }
543
544    #[inline]
545    fn hmac_signature(&self, data: &[u8]) -> Result<i32, CryptoError> {
546        let mut mac = HmacSha256::new_from_slice(&self.keys.integrity_key)
547            .map_err(|_| CryptoError::InvalidKey)?;
548
549        mac.update(&data[Self::PAYLOAD_BASE..data.len() - Self::SIGNATURE_SIZE]);
550        mac.update(&data[Self::IV_BASE..Self::IV_BASE + Self::IV_SIZE]);
551
552        let b = mac.finalize().into_bytes();
553
554        Ok(self.read_i32(&b, 0))
555    }
556}
557
558#[cfg(test)]
559mod tests {
560    use super::*;
561    use base64::prelude::*;
562
563    static TEST_ENCRYPTION_KEY: &str = "sIxwz7yw62yrfoLGt12lIHKuYrK/S5kLuApI2BQe7Ac=";
564    static TEST_INTEGRITY_KEY: &str = "v3fsVcMBMMHYzRhi7SpM0sdqwzvAxM6KPTu9OtVod5I=";
565
566    fn create_keys() -> Keys {
567        Keys::new(
568            &BASE64_STANDARD.decode(TEST_ENCRYPTION_KEY).unwrap(),
569            &BASE64_STANDARD.decode(TEST_INTEGRITY_KEY).unwrap(),
570        )
571        .unwrap()
572    }
573
574    #[test]
575    fn test_decode() {
576        let crypto = Crypto::new(create_keys());
577        let encoded = "aGVsbG8sIHdvcmxk";
578        let decoded = crypto.decode(encoded).unwrap();
579        assert_eq!(decoded, b"hello, world");
580    }
581
582    #[test]
583    fn test_encode() {
584        let crypto = Crypto::new(create_keys());
585        let data = b"hello, world";
586        let encoded = crypto.encode(data);
587        assert_eq!(encoded, "aGVsbG8sIHdvcmxk");
588    }
589
590    #[test]
591    fn test_decrypt() {
592        let crypto = Crypto::new(create_keys());
593        let timestamp = OffsetDateTime::UNIX_EPOCH + Duration::seconds(1);
594        let iv = crypto.create_init_vector(timestamp, 123456789);
595        let payload = "https://example.com".as_bytes();
596
597        let mut plain_data = crypto.init_plain_data(payload.len(), Some(&iv)).unwrap();
598        crypto.set_payload(&mut plain_data, payload).unwrap();
599        let encrypted_data = crypto.encrypt(&plain_data).unwrap();
600
601        assert_eq!(crypto.timestamp(&iv), Some(timestamp));
602        assert_eq!(crypto.server_id(&iv), Some(123456789));
603        assert_eq!(
604            crypto.payload(&encrypted_data).unwrap().len(),
605            payload.len()
606        );
607        assert_ne!(crypto.payload(&encrypted_data), Some(payload));
608
609        let decrypted_data = crypto.decrypt(&encrypted_data).unwrap();
610        assert_eq!(crypto.timestamp(&decrypted_data), Some(timestamp));
611        assert_eq!(crypto.server_id(&decrypted_data), Some(123456789));
612        assert_eq!(crypto.payload(&decrypted_data), Some(payload));
613
614        let mut encrypted_data_invalid_sign = encrypted_data.clone();
615        crypto.write_i32(
616            &mut encrypted_data_invalid_sign,
617            encrypted_data.len() - Crypto::SIGNATURE_SIZE,
618            123456789,
619        );
620        assert!(matches!(
621            crypto.decrypt(&encrypted_data_invalid_sign),
622            Err(CryptoError::InvalidSign)
623        ));
624        assert_ne!(crypto.payload(&encrypted_data_invalid_sign), Some(payload))
625    }
626
627    #[test]
628    fn test_create_init_vector() {
629        let crypto = Crypto::new(create_keys());
630        let timestamp = OffsetDateTime::UNIX_EPOCH + Duration::seconds(1);
631        let iv = crypto.create_init_vector(timestamp, 123456789);
632        assert_eq!(iv.len(), Crypto::IV_SIZE);
633        assert_eq!(crypto.read_i64(&iv, Crypto::IV_TIME_OFFSET), 1_000_000);
634        assert_eq!(crypto.read_i64(&iv, Crypto::IV_SERVER_ID_OFFSET), 123456789);
635        assert_eq!(crypto.timestamp(&iv), Some(timestamp));
636        assert_eq!(crypto.server_id(&iv), Some(123456789));
637    }
638
639    #[test]
640    fn test_init_plain_data() {
641        let crypto = Crypto::new(create_keys());
642        let payload = "https://example.com".as_bytes();
643
644        let mut plain_data = crypto.init_plain_data(payload.len(), None).unwrap();
645        crypto.set_payload(&mut plain_data, payload).unwrap();
646
647        assert_eq!(plain_data.len(), Crypto::OVERHEAD_SIZE + payload.len());
648        assert_eq!(crypto.payload(&plain_data), Some(payload));
649    }
650
651    #[test]
652    fn test_init_plain_data_empty_payload() {
653        let crypto = Crypto::new(create_keys());
654        let payload = "".as_bytes();
655
656        let mut plain_data = crypto.init_plain_data(0, None).unwrap();
657        crypto.set_payload(&mut plain_data, payload).unwrap();
658        assert_eq!(crypto.payload(&plain_data), Some(payload));
659    }
660}