Skip to main content

pqc_binary_format/
format.rs

1//! Core binary format implementation with serialization and validation.
2
3use crate::{
4    algorithm::Algorithm, error::CryptoError, metadata::PqcMetadata, Result, PQC_BINARY_VERSION,
5    PQC_MAGIC,
6};
7use serde::{Deserialize, Serialize};
8use sha2::{Digest, Sha256};
9
10/// Fixed header size in bytes: magic(4) + version(1) + algorithm_id(2) +
11/// flags(1) + metadata_len(4) + data_len(8) is split across the metadata
12/// section, so the bytes preceding the metadata section total 12.
13const HEADER_SIZE: usize = 12;
14
15/// SHA-256 checksum size in bytes.
16const CHECKSUM_SIZE: usize = 32;
17
18/// Branch-free, fixed-length equality for two 32-byte checksums.
19///
20/// Avoids leaking checksum-correctness information through timing.
21fn constant_time_eq(a: &[u8; 32], b: &[u8; 32]) -> bool {
22    let mut diff = 0u8;
23    for i in 0..32 {
24        diff |= a[i] ^ b[i];
25    }
26    diff == 0
27}
28
29/// PQC Binary Format v1.0 specification
30///
31/// This structure represents encrypted data in a standardized, self-describing format
32/// compatible across all post-quantum cryptographic algorithms.
33///
34/// # Example
35///
36/// ```
37/// use pqc_binary_format::{PqcBinaryFormat, Algorithm, PqcMetadata, EncParameters};
38/// use std::collections::HashMap;
39///
40/// let metadata = PqcMetadata {
41///     enc_params: EncParameters {
42///         iv: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
43///         tag: vec![1; 16],
44///         params: HashMap::new(),
45///     },
46///     ..Default::default()
47/// };
48///
49/// let format = PqcBinaryFormat::new(
50///     Algorithm::Hybrid,
51///     metadata,
52///     vec![1, 2, 3, 4, 5],
53///);
54///
55/// // Serialize to bytes
56/// let bytes = format.to_bytes().unwrap();
57///
58/// // Deserialize and verify
59/// let recovered = PqcBinaryFormat::from_bytes(&bytes).unwrap();
60/// assert_eq!(format, recovered);
61/// ```
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63pub struct PqcBinaryFormat {
64    /// Magic bytes - always "PQC\x01"
65    pub magic: [u8; 4],
66    /// Format version - currently 0x01
67    pub version: u8,
68    /// Algorithm identifier
69    pub algorithm: Algorithm,
70    /// Feature flags
71    pub flags: u8,
72    /// Algorithm-specific metadata
73    pub metadata: PqcMetadata,
74    /// Encrypted data payload
75    pub data: Vec<u8>,
76    /// SHA-256 checksum of the entire structure (excluding this field)
77    pub checksum: [u8; 32],
78}
79
80/// Feature flags for PQC Binary Format
81///
82/// Indicates optional features used in the encrypted data.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub struct FormatFlags(u8);
85
86impl FormatFlags {
87    /// Create new flags with all features disabled
88    #[must_use]
89    pub const fn new() -> Self {
90        Self(0)
91    }
92
93    /// Enable compression flag (bit 0)
94    #[must_use]
95    pub const fn with_compression(mut self) -> Self {
96        self.0 |= 0x01;
97        self
98    }
99
100    /// Enable streaming flag (bit 1)
101    #[must_use]
102    pub const fn with_streaming(mut self) -> Self {
103        self.0 |= 0x02;
104        self
105    }
106
107    /// Enable additional authentication flag (bit 2)
108    #[must_use]
109    pub const fn with_additional_auth(mut self) -> Self {
110        self.0 |= 0x04;
111        self
112    }
113
114    /// Enable experimental features flag (bit 3)
115    #[must_use]
116    pub const fn with_experimental(mut self) -> Self {
117        self.0 |= 0x08;
118        self
119    }
120
121    /// Check if compression is enabled
122    #[must_use]
123    pub const fn has_compression(self) -> bool {
124        (self.0 & 0x01) != 0
125    }
126
127    /// Check if streaming is enabled
128    #[must_use]
129    pub const fn has_streaming(self) -> bool {
130        (self.0 & 0x02) != 0
131    }
132
133    /// Check if additional authentication is enabled
134    #[must_use]
135    pub const fn has_additional_auth(self) -> bool {
136        (self.0 & 0x04) != 0
137    }
138
139    /// Check if experimental features are enabled
140    #[must_use]
141    pub const fn has_experimental(self) -> bool {
142        (self.0 & 0x08) != 0
143    }
144
145    /// Get raw flags value
146    #[must_use]
147    pub const fn as_u8(self) -> u8 {
148        self.0
149    }
150
151    /// Create flags from u8 value
152    #[must_use]
153    pub const fn from_u8(value: u8) -> Self {
154        Self(value)
155    }
156}
157
158impl Default for FormatFlags {
159    fn default() -> Self {
160        Self::new()
161    }
162}
163
164impl PqcBinaryFormat {
165    /// Create a new PQC binary format structure with default flags
166    ///
167    /// The checksum is automatically calculated and set.
168    ///
169    /// # Example
170    ///
171    /// ```
172    /// use pqc_binary_format::{PqcBinaryFormat, Algorithm, PqcMetadata, EncParameters};
173    /// use std::collections::HashMap;
174    ///
175    /// let metadata = PqcMetadata {
176    ///     enc_params: EncParameters {
177    ///         iv: vec![1; 12],
178    ///         tag: vec![1; 16],
179    ///         params: HashMap::new(),
180    ///     },
181    ///     ..Default::default()
182    /// };
183    ///
184    /// let format = PqcBinaryFormat::new(
185    ///     Algorithm::PostQuantum,
186    ///     metadata,
187    ///     vec![1, 2, 3],
188    /// );
189    /// ```
190    #[must_use]
191    pub fn new(algorithm: Algorithm, metadata: PqcMetadata, data: Vec<u8>) -> Self {
192        let mut format = Self {
193            magic: PQC_MAGIC,
194            version: PQC_BINARY_VERSION,
195            algorithm,
196            flags: FormatFlags::new().as_u8(),
197            metadata,
198            data,
199            checksum: [0u8; 32],
200        };
201
202        // Calculate and set checksum
203        format.checksum = format.calculate_checksum();
204        format
205    }
206
207    /// Create with specific flags
208    ///
209    /// # Example
210    ///
211    /// ```
212    /// use pqc_binary_format::{PqcBinaryFormat, Algorithm, PqcMetadata, FormatFlags, EncParameters};
213    /// use std::collections::HashMap;
214    ///
215    /// let metadata = PqcMetadata {
216    ///     enc_params: EncParameters {
217    ///         iv: vec![1; 12],
218    ///         tag: vec![1; 16],
219    ///         params: HashMap::new(),
220    ///     },
221    ///     ..Default::default()
222    /// };
223    ///
224    /// let flags = FormatFlags::new().with_compression().with_streaming();
225    ///
226    /// let format = PqcBinaryFormat::with_flags(
227    ///     Algorithm::Hybrid,
228    ///     flags,
229    ///     metadata,
230    ///     vec![1, 2, 3],
231    /// );
232    ///
233    /// assert!(format.flags().has_compression());
234    /// assert!(format.flags().has_streaming());
235    /// ```
236    #[must_use]
237    pub fn with_flags(
238        algorithm: Algorithm,
239        flags: FormatFlags,
240        metadata: PqcMetadata,
241        data: Vec<u8>,
242    ) -> Self {
243        let mut format = Self {
244            magic: PQC_MAGIC,
245            version: PQC_BINARY_VERSION,
246            algorithm,
247            flags: flags.as_u8(),
248            metadata,
249            data,
250            checksum: [0u8; 32],
251        };
252
253        format.checksum = format.calculate_checksum();
254        format
255    }
256
257    /// Serialize to binary format
258    ///
259    /// # Errors
260    ///
261    /// Returns [`CryptoError::BinaryFormatError`] if:
262    /// - Format validation fails
263    /// - Binary serialization fails
264    ///
265    /// # Example
266    ///
267    /// ```
268    /// use pqc_binary_format::{PqcBinaryFormat, Algorithm, PqcMetadata, EncParameters};
269    /// use std::collections::HashMap;
270    ///
271    /// # let metadata = PqcMetadata {
272    /// #     enc_params: EncParameters {
273    /// #         iv: vec![1; 12],
274    /// #         tag: vec![1; 16],
275    /// #         params: HashMap::new(),
276    /// #     },
277    /// #     ..Default::default()
278    /// # };
279    /// # let format = PqcBinaryFormat::new(Algorithm::Hybrid, metadata, vec![1, 2, 3]);
280    /// let bytes = format.to_bytes().unwrap();
281    /// // Send bytes over network or save to file
282    /// ```
283    pub fn to_bytes(&self) -> Result<Vec<u8>> {
284        // Validate format before serialization
285        self.validate()?;
286
287        // Build the packed little-endian layout defined by the PQC Binary
288        // Format specification (draft-riddel-pqc-binary-format-00, Section 3):
289        //   magic[4] | version[1] | algorithm_id[2 LE] | flags[1]
290        //   | metadata_len[4 LE] | metadata(JSON) | data_len[8 LE] | data
291        //   | checksum[32]
292        let mut buf = self.serialize_prefix();
293        let checksum: [u8; 32] = Sha256::digest(&buf).into();
294        buf.extend_from_slice(&checksum);
295        Ok(buf)
296    }
297
298    /// Serialize every field preceding the checksum, in spec order. The SHA-256
299    /// of this byte sequence is the integrity checksum.
300    ///
301    /// Lengths are written as the spec's fixed-width little-endian integers. A
302    /// metadata section larger than `u32::MAX` (4 GB) is saturated to `u32::MAX`;
303    /// such inputs are rejected by the practical size limits in any caller.
304    fn serialize_prefix(&self) -> Vec<u8> {
305        let metadata = self.metadata.to_json_bytes();
306        let mut buf = Vec::with_capacity(HEADER_SIZE + metadata.len() + self.data.len());
307        buf.extend_from_slice(&self.magic);
308        buf.push(self.version);
309        buf.extend_from_slice(&self.algorithm.as_id().to_le_bytes());
310        buf.push(self.flags);
311        let metadata_len = u32::try_from(metadata.len()).unwrap_or(u32::MAX);
312        buf.extend_from_slice(&metadata_len.to_le_bytes());
313        buf.extend_from_slice(&metadata);
314        buf.extend_from_slice(&(self.data.len() as u64).to_le_bytes());
315        buf.extend_from_slice(&self.data);
316        buf
317    }
318
319    /// Deserialize from binary format with checksum verification
320    ///
321    /// # Errors
322    ///
323    /// Returns [`CryptoError::BinaryFormatError`] if:
324    /// - Binary deserialization fails
325    /// - Format validation fails after deserialization
326    /// - Checksum verification fails
327    ///
328    /// # Example
329    ///
330    /// ```
331    /// use pqc_binary_format::PqcBinaryFormat;
332    ///
333    /// # use pqc_binary_format::{Algorithm, PqcMetadata, EncParameters};
334    /// # use std::collections::HashMap;
335    /// # let metadata = PqcMetadata {
336    /// #     enc_params: EncParameters {
337    /// #         iv: vec![1; 12],
338    /// #         tag: vec![1; 16],
339    /// #         params: HashMap::new(),
340    /// #     },
341    /// #     ..Default::default()
342    /// # };
343    /// # let format = PqcBinaryFormat::new(Algorithm::Hybrid, metadata, vec![1, 2, 3]);
344    /// # let bytes = format.to_bytes().unwrap();
345    /// let recovered = PqcBinaryFormat::from_bytes(&bytes).unwrap();
346    /// ```
347    pub fn from_bytes(data: &[u8]) -> Result<Self> {
348        // Smallest valid envelope: 12-byte pre-metadata header + 0-byte metadata
349        // + 8-byte data length + 0-byte data + 32-byte checksum = 52 bytes.
350        const MIN_SIZE: usize = HEADER_SIZE + 8 + CHECKSUM_SIZE;
351        if data.len() < MIN_SIZE {
352            return Err(CryptoError::BinaryFormatError(format!(
353                "Buffer too small: {} bytes, minimum {MIN_SIZE}",
354                data.len(),
355            )));
356        }
357
358        // Magic bytes
359        let magic: [u8; 4] = [data[0], data[1], data[2], data[3]];
360        if magic != PQC_MAGIC {
361            return Err(CryptoError::InvalidMagic);
362        }
363
364        // Version
365        let version = data[4];
366        if version != PQC_BINARY_VERSION {
367            return Err(CryptoError::UnsupportedVersion(version));
368        }
369
370        // Algorithm ID (little-endian) + flags
371        let algorithm_id = u16::from_le_bytes([data[5], data[6]]);
372        let algorithm = Algorithm::from_id(algorithm_id).ok_or_else(|| {
373            CryptoError::UnknownAlgorithm(format!("Invalid algorithm ID: {algorithm_id:#x}"))
374        })?;
375        let flags = data[7];
376
377        // Metadata length (little-endian) + metadata section
378        let metadata_len = u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize;
379        let meta_start = HEADER_SIZE;
380        let meta_end = meta_start
381            .checked_add(metadata_len)
382            .ok_or_else(|| CryptoError::BinaryFormatError("Metadata length overflow".into()))?;
383        if meta_end + CHECKSUM_SIZE > data.len() {
384            return Err(CryptoError::BinaryFormatError(
385                "Metadata length exceeds buffer".into(),
386            ));
387        }
388        let metadata = PqcMetadata::from_json_bytes(&data[meta_start..meta_end])?;
389
390        // Data length (little-endian) + data section
391        let len_end = meta_end + 8;
392        if len_end + CHECKSUM_SIZE > data.len() {
393            return Err(CryptoError::BinaryFormatError(
394                "Truncated before data length".into(),
395            ));
396        }
397        let mut data_len_bytes = [0u8; 8];
398        data_len_bytes.copy_from_slice(&data[meta_end..len_end]);
399        let data_len = usize::try_from(u64::from_le_bytes(data_len_bytes))
400            .map_err(|_| CryptoError::BinaryFormatError("Data length exceeds usize".into()))?;
401        let data_end = len_end
402            .checked_add(data_len)
403            .ok_or_else(|| CryptoError::BinaryFormatError("Data length overflow".into()))?;
404        if data_end + CHECKSUM_SIZE != data.len() {
405            return Err(CryptoError::BinaryFormatError(format!(
406                "Length mismatch: expected {} bytes, got {}",
407                data_end + CHECKSUM_SIZE,
408                data.len()
409            )));
410        }
411        let payload = data[len_end..data_end].to_vec();
412
413        // Verify checksum over everything preceding it (constant-time compare).
414        // The exact-length check above guarantees 32 trailing bytes.
415        let mut stored = [0u8; 32];
416        stored.copy_from_slice(&data[data_end..]);
417        let calculated: [u8; 32] = Sha256::digest(&data[..data_end]).into();
418        if !constant_time_eq(&stored, &calculated) {
419            return Err(CryptoError::ChecksumMismatch);
420        }
421
422        let format = Self {
423            magic,
424            version,
425            algorithm,
426            flags,
427            metadata,
428            data: payload,
429            checksum: stored,
430        };
431        format.validate()?;
432        Ok(format)
433    }
434
435    /// Validate the binary format structure
436    ///
437    /// # Errors
438    ///
439    /// Returns [`CryptoError`] if:
440    /// - Magic bytes are invalid
441    /// - Version is unsupported
442    /// - Algorithm ID is invalid
443    /// - Metadata validation fails
444    pub fn validate(&self) -> Result<()> {
445        // Check magic bytes
446        if self.magic != PQC_MAGIC {
447            return Err(CryptoError::InvalidMagic);
448        }
449
450        // Check version
451        if self.version != PQC_BINARY_VERSION {
452            return Err(CryptoError::UnsupportedVersion(self.version));
453        }
454
455        // Validate algorithm
456        if Algorithm::from_id(self.algorithm.as_id()).is_none() {
457            return Err(CryptoError::UnknownAlgorithm(format!(
458                "Invalid algorithm ID: {:#x}",
459                self.algorithm.as_id()
460            )));
461        }
462
463        // Validate metadata
464        self.metadata.validate()?;
465
466        Ok(())
467    }
468
469    /// Update the checksum field with the calculated checksum
470    ///
471    /// Call this after modifying any fields to maintain integrity.
472    pub fn update_checksum(&mut self) {
473        self.checksum = self.calculate_checksum();
474    }
475
476    /// Calculate the SHA-256 integrity checksum over all fields preceding it.
477    ///
478    /// Equivalent to `SHA-256(serialize_prefix())`, where the metadata is the
479    /// canonical (sorted-key) JSON section — matching the deterministic checksum
480    /// definition in Section 3.3 of the specification.
481    fn calculate_checksum(&self) -> [u8; 32] {
482        Sha256::digest(self.serialize_prefix()).into()
483    }
484
485    /// Get format flags
486    #[must_use]
487    pub fn flags(&self) -> FormatFlags {
488        FormatFlags(self.flags)
489    }
490
491    /// Get algorithm
492    #[must_use]
493    pub const fn algorithm(&self) -> Algorithm {
494        self.algorithm
495    }
496
497    /// Get encrypted data
498    #[must_use]
499    pub fn data(&self) -> &[u8] {
500        &self.data
501    }
502
503    /// Get metadata
504    #[must_use]
505    pub const fn metadata(&self) -> &PqcMetadata {
506        &self.metadata
507    }
508
509    /// Get total size of the binary format when serialized
510    #[must_use]
511    pub fn total_size(&self) -> usize {
512        self.to_bytes().map_or(0, |bytes| bytes.len())
513    }
514}
515
516#[cfg(test)]
517mod tests {
518    use super::*;
519    use crate::EncParameters;
520    use std::collections::HashMap;
521
522    #[test]
523    fn test_format_flags() {
524        let flags = FormatFlags::new().with_compression().with_streaming();
525
526        assert!(flags.has_compression());
527        assert!(flags.has_streaming());
528        assert!(!flags.has_additional_auth());
529        assert!(!flags.has_experimental());
530    }
531
532    #[test]
533    fn test_binary_format_roundtrip() {
534        let metadata = PqcMetadata {
535            enc_params: EncParameters {
536                iv: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
537                tag: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
538                params: HashMap::new(),
539            },
540            ..Default::default()
541        };
542
543        let original = PqcBinaryFormat::new(Algorithm::Hybrid, metadata, vec![1, 2, 3, 4, 5]);
544
545        let bytes = original.to_bytes().unwrap();
546        let deserialized = PqcBinaryFormat::from_bytes(&bytes).unwrap();
547
548        assert_eq!(original, deserialized);
549    }
550
551    #[test]
552    fn test_checksum_validation() {
553        let metadata = PqcMetadata {
554            enc_params: EncParameters {
555                iv: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
556                tag: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
557                params: HashMap::new(),
558            },
559            ..Default::default()
560        };
561
562        let format = PqcBinaryFormat::new(Algorithm::PostQuantum, metadata, vec![1, 2, 3, 4, 5]);
563
564        let mut bytes = format.to_bytes().unwrap();
565
566        // Corrupt the data
567        if let Some(byte) = bytes.last_mut() {
568            *byte = byte.wrapping_add(1);
569        }
570
571        // Should fail checksum validation
572        assert!(PqcBinaryFormat::from_bytes(&bytes).is_err());
573    }
574
575    #[test]
576    fn test_flags_roundtrip() {
577        let metadata = PqcMetadata {
578            enc_params: EncParameters {
579                iv: vec![1; 12],
580                tag: vec![1; 16],
581                params: HashMap::new(),
582            },
583            ..Default::default()
584        };
585
586        let flags = FormatFlags::new()
587            .with_compression()
588            .with_streaming()
589            .with_additional_auth();
590
591        let format =
592            PqcBinaryFormat::with_flags(Algorithm::QuadLayer, flags, metadata, vec![1, 2, 3]);
593
594        let bytes = format.to_bytes().unwrap();
595        let recovered = PqcBinaryFormat::from_bytes(&bytes).unwrap();
596
597        assert!(recovered.flags().has_compression());
598        assert!(recovered.flags().has_streaming());
599        assert!(recovered.flags().has_additional_auth());
600        assert!(!recovered.flags().has_experimental());
601    }
602}