Skip to main content

hide_format/
codec.rs

1use minicbor::{Decoder, Encoder};
2
3use crate::{
4    FormatError, MAX_HEADER_LEN, MAX_METADATA_LEN, MAX_RECIPIENTS, MAX_SIGNATURES, SUITE, TAG_LEN,
5};
6
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct RecipientStanza {
9    pub encapsulation: Vec<u8>,
10    pub wrapped_cek: Vec<u8>,
11}
12
13/// A public signature over the container, readable by anyone holding the file.
14/// Confidential signatures live inside the encrypted metadata instead.
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct SignatureStanza {
17    pub verifying_key: Vec<u8>,
18    pub signature: Vec<u8>,
19}
20
21pub const VERIFYING_KEY_LEN: usize = 1984;
22pub const SIGNATURE_LEN: usize = 3373;
23
24#[derive(Clone, Debug, PartialEq, Eq)]
25pub struct ProtectedHeader {
26    pub object_id: [u8; 32],
27    pub recipients: Vec<RecipientStanza>,
28    pub encrypted_metadata: Vec<u8>,
29    /// Occupies the slot reserved as an empty array in v0.1. Empty here encodes
30    /// byte-for-byte as a v0.1 header, which is what keeps old files readable.
31    pub signatures: Vec<SignatureStanza>,
32}
33
34impl ProtectedHeader {
35    pub fn encode(&self) -> Result<Vec<u8>, FormatError> {
36        self.encode_with_signatures(true)
37    }
38
39    /// The exact bytes a signature covers: the header with the signature slot
40    /// empty. Signing the slot that holds the signature would be circular.
41    pub fn signing_base(&self) -> Result<Vec<u8>, FormatError> {
42        self.encode_with_signatures(false)
43    }
44
45    fn encode_with_signatures(&self, include: bool) -> Result<Vec<u8>, FormatError> {
46        if self.recipients.is_empty() || self.recipients.len() > MAX_RECIPIENTS {
47            return Err(FormatError::MalformedHeader);
48        }
49        if !(TAG_LEN..=MAX_METADATA_LEN + TAG_LEN).contains(&self.encrypted_metadata.len()) {
50            return Err(FormatError::InvalidMetadata);
51        }
52        if self.signatures.len() > MAX_SIGNATURES {
53            return Err(FormatError::MalformedHeader);
54        }
55        let mut encoder = Encoder::new(Vec::new());
56        encoder
57            .map(5)?
58            .u8(1)?
59            .u16(SUITE)?
60            .u8(2)?
61            .bytes(&self.object_id)?;
62        encoder.u8(3)?.array(self.recipients.len() as u64)?;
63        for stanza in &self.recipients {
64            if stanza.encapsulation.len() != 1120 || stanza.wrapped_cek.len() != 48 {
65                return Err(FormatError::MalformedHeader);
66            }
67            encoder
68                .array(3)?
69                .u8(1)?
70                .bytes(&stanza.encapsulation)?
71                .bytes(&stanza.wrapped_cek)?;
72        }
73        encoder.u8(4)?.bytes(&self.encrypted_metadata)?.u8(5)?;
74        let signatures: &[SignatureStanza] = if include { &self.signatures } else { &[] };
75        encoder.array(signatures.len() as u64)?;
76        for stanza in signatures {
77            if stanza.verifying_key.len() != VERIFYING_KEY_LEN
78                || stanza.signature.len() != SIGNATURE_LEN
79            {
80                return Err(FormatError::MalformedHeader);
81            }
82            encoder
83                .array(3)?
84                .u8(1)?
85                .bytes(&stanza.verifying_key)?
86                .bytes(&stanza.signature)?;
87        }
88        let bytes = encoder.into_writer();
89        check_header_len(bytes.len())?;
90        Ok(bytes)
91    }
92
93    pub fn decode(bytes: &[u8]) -> Result<Self, FormatError> {
94        check_header_len(bytes.len())?;
95        let mut decoder = Decoder::new(bytes);
96        if decoder.map()? != Some(5) {
97            return Err(FormatError::MalformedHeader);
98        }
99        key(&mut decoder, 1)?;
100        if decoder.u16()? != SUITE {
101            return Err(FormatError::UnsupportedSuite);
102        }
103        key(&mut decoder, 2)?;
104        let object_id = decoder
105            .bytes()?
106            .try_into()
107            .map_err(|_| FormatError::MalformedHeader)?;
108        key(&mut decoder, 3)?;
109        let count = decoder.array()?.ok_or(FormatError::MalformedHeader)?;
110        if count == 0 || count > MAX_RECIPIENTS as u64 {
111            return Err(FormatError::MalformedHeader);
112        }
113        let mut recipients = Vec::with_capacity(count as usize);
114        for _ in 0..count {
115            recipients.push(decode_stanza(&mut decoder)?);
116        }
117        key(&mut decoder, 4)?;
118        let metadata = decoder.bytes()?;
119        if !(TAG_LEN..=MAX_METADATA_LEN + TAG_LEN).contains(&metadata.len()) {
120            return Err(FormatError::InvalidMetadata);
121        }
122        key(&mut decoder, 5)?;
123        let signature_count = decoder.array()?.ok_or(FormatError::MalformedHeader)?;
124        if signature_count > MAX_SIGNATURES as u64 {
125            return Err(FormatError::MalformedHeader);
126        }
127        let mut signatures = Vec::with_capacity(signature_count as usize);
128        for _ in 0..signature_count {
129            signatures.push(decode_signature(&mut decoder)?);
130        }
131        let header = Self {
132            object_id,
133            recipients,
134            encrypted_metadata: metadata.to_vec(),
135            signatures,
136        };
137        canonical(bytes, decoder.position(), &header.encode()?)?;
138        Ok(header)
139    }
140}
141
142fn decode_stanza(decoder: &mut Decoder<'_>) -> Result<RecipientStanza, FormatError> {
143    if decoder.array()? != Some(3) || decoder.u8()? != 1 {
144        return Err(FormatError::UnsupportedFeature);
145    }
146    let encapsulation = decoder.bytes()?;
147    let wrapped_cek = decoder.bytes()?;
148    if encapsulation.len() != 1120 || wrapped_cek.len() != 48 {
149        return Err(FormatError::MalformedHeader);
150    }
151    Ok(RecipientStanza {
152        encapsulation: encapsulation.to_vec(),
153        wrapped_cek: wrapped_cek.to_vec(),
154    })
155}
156
157fn decode_signature(decoder: &mut Decoder<'_>) -> Result<SignatureStanza, FormatError> {
158    if decoder.array()? != Some(3) || decoder.u8()? != 1 {
159        return Err(FormatError::UnsupportedFeature);
160    }
161    let verifying_key = decoder.bytes()?;
162    let signature = decoder.bytes()?;
163    if verifying_key.len() != VERIFYING_KEY_LEN || signature.len() != SIGNATURE_LEN {
164        return Err(FormatError::MalformedHeader);
165    }
166    Ok(SignatureStanza {
167        verifying_key: verifying_key.to_vec(),
168        signature: signature.to_vec(),
169    })
170}
171
172pub fn encode_header(protected: &[u8], mac: &[u8; 32]) -> Result<Vec<u8>, FormatError> {
173    check_header_len(protected.len())?;
174    let mut encoder = Encoder::new(Vec::new());
175    encoder.array(2)?.bytes(protected)?.bytes(mac)?;
176    let bytes = encoder.into_writer();
177    check_header_len(bytes.len())?;
178    Ok(bytes)
179}
180
181pub fn decode_header(bytes: &[u8]) -> Result<(Vec<u8>, [u8; 32]), FormatError> {
182    check_header_len(bytes.len())?;
183    let mut decoder = Decoder::new(bytes);
184    if decoder.array()? != Some(2) {
185        return Err(FormatError::MalformedHeader);
186    }
187    let protected = decoder.bytes()?;
188    let mac = decoder
189        .bytes()?
190        .try_into()
191        .map_err(|_| FormatError::MalformedHeader)?;
192    canonical(bytes, decoder.position(), &encode_header(protected, &mac)?)?;
193    Ok((protected.to_vec(), mac))
194}
195
196#[derive(Default, Clone, Debug, PartialEq, Eq)]
197pub struct Metadata {
198    pub filename: Option<String>,
199    pub media_type: Option<String>,
200    /// A signature visible only to recipients. Excluded from the signing base,
201    /// because it cannot cover itself.
202    pub signature: Option<SignatureStanza>,
203}
204
205impl Metadata {
206    pub fn encode(&self) -> Result<Vec<u8>, FormatError> {
207        self.encode_with_signature(true)
208    }
209
210    /// The metadata as it looked before a signature was attached.
211    pub fn signing_base(&self) -> Result<Vec<u8>, FormatError> {
212        self.encode_with_signature(false)
213    }
214
215    fn encode_with_signature(&self, include: bool) -> Result<Vec<u8>, FormatError> {
216        let signature = if include {
217            self.signature.as_ref()
218        } else {
219            None
220        };
221        let mut encoder = Encoder::new(Vec::new());
222        encoder.map(
223            u64::from(self.filename.is_some())
224                + u64::from(self.media_type.is_some())
225                + u64::from(signature.is_some()),
226        )?;
227        if let Some(filename) = &self.filename {
228            validate_filename(filename)?;
229            encoder.u8(1)?.str(filename)?;
230        }
231        if let Some(media_type) = &self.media_type {
232            if media_type.is_empty()
233                || media_type.len() > 255
234                || !media_type.bytes().all(|byte| (32..=126).contains(&byte))
235            {
236                return Err(FormatError::InvalidMetadata);
237            }
238            encoder.u8(2)?.str(media_type)?;
239        }
240        if let Some(stanza) = signature {
241            if stanza.verifying_key.len() != VERIFYING_KEY_LEN
242                || stanza.signature.len() != SIGNATURE_LEN
243            {
244                return Err(FormatError::InvalidMetadata);
245            }
246            encoder
247                .u8(3)?
248                .array(3)?
249                .u8(1)?
250                .bytes(&stanza.verifying_key)?
251                .bytes(&stanza.signature)?;
252        }
253        Ok(encoder.into_writer())
254    }
255
256    pub fn decode(bytes: &[u8]) -> Result<Self, FormatError> {
257        if bytes.len() > MAX_METADATA_LEN {
258            return Err(FormatError::InvalidMetadata);
259        }
260        let mut decoder = Decoder::new(bytes);
261        let count = decoder.map()?.ok_or(FormatError::InvalidMetadata)?;
262        if count > 3 {
263            return Err(FormatError::InvalidMetadata);
264        }
265        let mut metadata = Self::default();
266        let mut previous = 0;
267        for _ in 0..count {
268            let field = decoder.u8()?;
269            if field <= previous || field > 3 {
270                return Err(FormatError::InvalidMetadata);
271            }
272            previous = field;
273            if field == 3 {
274                metadata.signature =
275                    Some(decode_signature(&mut decoder).map_err(|_| FormatError::InvalidMetadata)?);
276                continue;
277            }
278            let text = decoder.str()?;
279            if text.len() > 255 {
280                return Err(FormatError::InvalidMetadata);
281            }
282            match field {
283                1 => metadata.filename = Some(text.to_owned()),
284                2 => metadata.media_type = Some(text.to_owned()),
285                _ => return Err(FormatError::InvalidMetadata),
286            }
287        }
288        canonical(bytes, decoder.position(), &metadata.encode()?)?;
289        Ok(metadata)
290    }
291}
292
293fn validate_filename(filename: &str) -> Result<(), FormatError> {
294    let stem = filename
295        .split('.')
296        .next()
297        .unwrap_or("")
298        .to_ascii_uppercase();
299    let reserved = matches!(stem.as_str(), "CON" | "PRN" | "AUX" | "NUL")
300        || (stem.len() == 4
301            && (stem.starts_with("COM") || stem.starts_with("LPT"))
302            && (b'1'..=b'9').contains(&stem.as_bytes()[3]));
303    if filename.is_empty()
304        || filename.len() > 255
305        || matches!(filename, "." | "..")
306        || filename.ends_with(['.', ' '])
307        || reserved
308        || filename
309            .chars()
310            .any(|character| character.is_control() || "<>:\"/\\|?*".contains(character))
311    {
312        return Err(FormatError::InvalidMetadata);
313    }
314    Ok(())
315}
316
317fn key(decoder: &mut Decoder<'_>, expected: u8) -> Result<(), FormatError> {
318    if decoder.u8()? != expected {
319        return Err(FormatError::MalformedHeader);
320    }
321    Ok(())
322}
323
324fn check_header_len(length: usize) -> Result<(), FormatError> {
325    if length == 0 || length > MAX_HEADER_LEN {
326        return Err(FormatError::HeaderTooLarge);
327    }
328    Ok(())
329}
330
331fn canonical(original: &[u8], consumed: usize, encoded: &[u8]) -> Result<(), FormatError> {
332    if consumed != original.len() || original != encoded {
333        return Err(FormatError::NonCanonical);
334    }
335    Ok(())
336}
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341
342    fn header() -> ProtectedHeader {
343        ProtectedHeader {
344            object_id: [1; 32],
345            recipients: vec![RecipientStanza {
346                encapsulation: vec![2; 1120],
347                wrapped_cek: vec![3; 48],
348            }],
349            encrypted_metadata: vec![4; 17],
350            signatures: Vec::new(),
351        }
352    }
353
354    #[test]
355    fn header_roundtrip_preserves_authenticated_bytes() -> Result<(), FormatError> {
356        let protected = header().encode()?;
357        assert_eq!(ProtectedHeader::decode(&protected)?, header());
358        assert_eq!(
359            decode_header(&encode_header(&protected, &[5; 32])?)?,
360            (protected, [5; 32])
361        );
362        Ok(())
363    }
364
365    fn signed_header() -> ProtectedHeader {
366        ProtectedHeader {
367            signatures: vec![SignatureStanza {
368                verifying_key: vec![6; VERIFYING_KEY_LEN],
369                signature: vec![7; SIGNATURE_LEN],
370            }],
371            ..header()
372        }
373    }
374
375    /// The compatibility invariant: an unsigned header must be byte-identical to
376    /// what v0.1 wrote, or every existing container stops opening.
377    #[test]
378    fn an_unsigned_header_still_ends_with_the_v0_1_empty_slot() -> Result<(), FormatError> {
379        let encoded = header().encode()?;
380        // 0x05 = key 5, 0x80 = CBOR array of length 0.
381        assert_eq!(&encoded[encoded.len() - 2..], &[0x05, 0x80]);
382        assert_eq!(header().signing_base()?, encoded);
383        Ok(())
384    }
385
386    #[test]
387    fn a_signed_header_survives_encoding() -> Result<(), FormatError> {
388        let encoded = signed_header().encode()?;
389        assert_eq!(ProtectedHeader::decode(&encoded)?, signed_header());
390        Ok(())
391    }
392
393    /// The signature covers the header with the slot empty, so adding a
394    /// signature must not disturb what earlier signers committed to.
395    #[test]
396    fn the_signing_base_excludes_the_signatures() -> Result<(), FormatError> {
397        assert_eq!(signed_header().signing_base()?, header().encode()?);
398        Ok(())
399    }
400
401    #[test]
402    fn a_malformed_signature_stanza_is_refused() {
403        for (key_len, sig_len) in [
404            (VERIFYING_KEY_LEN - 1, SIGNATURE_LEN),
405            (VERIFYING_KEY_LEN, SIGNATURE_LEN + 1),
406        ] {
407            let header = ProtectedHeader {
408                signatures: vec![SignatureStanza {
409                    verifying_key: vec![6; key_len],
410                    signature: vec![7; sig_len],
411                }],
412                ..header()
413            };
414            assert_eq!(header.encode(), Err(FormatError::MalformedHeader));
415        }
416    }
417
418    #[test]
419    fn too_many_signatures_are_refused() {
420        let header = ProtectedHeader {
421            signatures: vec![
422                SignatureStanza {
423                    verifying_key: vec![6; VERIFYING_KEY_LEN],
424                    signature: vec![7; SIGNATURE_LEN],
425                };
426                MAX_SIGNATURES + 1
427            ],
428            ..header()
429        };
430        assert_eq!(header.encode(), Err(FormatError::MalformedHeader));
431    }
432
433    #[test]
434    fn metadata_has_exact_encoding() -> Result<(), FormatError> {
435        let metadata = Metadata {
436            filename: Some("hello.txt".into()),
437            media_type: None,
438            signature: None,
439        };
440        assert_eq!(metadata.encode()?, b"\xa1\x01\x69hello.txt");
441        assert_eq!(Metadata::decode(b"\xa1\x01\x69hello.txt")?, metadata);
442        assert_eq!(Metadata::default().encode()?, [0xa0]);
443        Ok(())
444    }
445
446    #[test]
447    fn rejects_noncanonical_duplicate_unknown_and_indefinite() -> Result<(), FormatError> {
448        for bytes in [
449            b"\xa1\x18\x01\x61a".as_slice(),
450            b"\xbf\xff",
451            b"\xa2\x01\x61a\x01\x61b",
452            b"\xa1\x03\x61a",
453            b"\xa0\x00",
454            b"\xa1\x01\x7f\xff",
455            b"\xa1\x01\xc0\x61a",
456        ] {
457            assert!(Metadata::decode(bytes).is_err(), "{bytes:?}");
458        }
459        let original = header().encode()?;
460        let mut nonminimal = original.clone();
461        nonminimal.splice(2..3, [0x18, 1]);
462        assert_eq!(
463            ProtectedHeader::decode(&nonminimal),
464            Err(FormatError::NonCanonical)
465        );
466        let mut duplicate = original;
467        duplicate[3] = 1;
468        assert!(ProtectedHeader::decode(&duplicate).is_err());
469        Ok(())
470    }
471
472    #[test]
473    fn rejects_hostile_counts_and_every_truncated_header() -> Result<(), FormatError> {
474        let bytes = header().encode()?;
475        for length in 0..bytes.len() {
476            assert!(ProtectedHeader::decode(&bytes[..length]).is_err());
477        }
478        let mut oversized = header();
479        oversized.recipients = vec![oversized.recipients[0].clone(); MAX_RECIPIENTS + 1];
480        assert!(oversized.encode().is_err());
481        assert!(decode_header(&vec![0; MAX_HEADER_LEN + 1]).is_err());
482        assert!(ProtectedHeader::decode(b"\xa5\x01\x01\x02\x58\xff").is_err());
483        Ok(())
484    }
485
486    #[test]
487    fn rejects_nonportable_filenames() {
488        for filename in [
489            "",
490            ".",
491            "..",
492            "../a",
493            "a/b",
494            "a\\b",
495            "C:a",
496            "NUL.txt",
497            "com1",
498            "LPT9.pdf",
499            "file.",
500            "file ",
501            "line\nname",
502            "a*b",
503        ] {
504            assert!(
505                Metadata {
506                    filename: Some(filename.into()),
507                    media_type: None,
508                    signature: None
509                }
510                .encode()
511                .is_err(),
512                "{filename:?}"
513            );
514        }
515    }
516}