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#[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 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 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 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 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 #[test]
378 fn an_unsigned_header_still_ends_with_the_v0_1_empty_slot() -> Result<(), FormatError> {
379 let encoded = header().encode()?;
380 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 #[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}