1use base64::{encoded_len, engine::general_purpose::STANDARD, Engine as _};
6use zeroize::Zeroizing;
7
8use crate::{PemEncodeOptions, PemError, PemLabel};
9
10pub fn encode_pem(
12 label: PemLabel,
13 der: &[u8],
14 options: PemEncodeOptions,
15) -> Result<Zeroizing<String>, PemError> {
16 if der.is_empty() || der.len() > options.max_der_len {
17 return Err(PemError::DerTooLarge);
18 }
19 if options.line_width == 0 || options.line_width > 76 {
20 return Err(PemError::InvalidOptions);
21 }
22
23 let encoded_length = encoded_len(der.len(), true).ok_or(PemError::InvalidOptions)?;
24 let mut encoded = Zeroizing::new(vec![0_u8; encoded_length]);
25 let written = STANDARD
26 .encode_slice(der, encoded.as_mut_slice())
27 .map_err(|_| PemError::InvalidOptions)?;
28 if written != encoded_length {
29 return Err(PemError::InvalidOptions);
30 }
31
32 let newline = options.line_ending.as_str();
33 let output_length = encoded_pem_length(
34 label.as_str().len(),
35 encoded_length,
36 options.line_width,
37 newline.len(),
38 )?;
39 let mut output = Zeroizing::new(String::with_capacity(output_length));
43 output.push_str("-----BEGIN ");
44 output.push_str(label.as_str());
45 output.push_str("-----");
46 output.push_str(newline);
47
48 for chunk in encoded.as_slice().chunks(options.line_width) {
49 let line = core::str::from_utf8(chunk).map_err(|_| PemError::InvalidBase64)?;
50 output.push_str(line);
51 output.push_str(newline);
52 }
53
54 output.push_str("-----END ");
55 output.push_str(label.as_str());
56 output.push_str("-----");
57 output.push_str(newline);
58
59 if output.len() != output_length {
60 return Err(PemError::InvalidOptions);
61 }
62
63 Ok(output)
64}
65
66fn encoded_pem_length(
67 label_length: usize,
68 encoded_length: usize,
69 line_width: usize,
70 newline_length: usize,
71) -> Result<usize, PemError> {
72 let boundary_length = "-----BEGIN "
73 .len()
74 .checked_add(label_length)
75 .and_then(|length| length.checked_add("-----".len()))
76 .and_then(|length| length.checked_add(newline_length))
77 .ok_or(PemError::InvalidOptions)?;
78 let footer_length = "-----END "
79 .len()
80 .checked_add(label_length)
81 .and_then(|length| length.checked_add("-----".len()))
82 .and_then(|length| length.checked_add(newline_length))
83 .ok_or(PemError::InvalidOptions)?;
84 let line_count = encoded_length
85 .checked_add(line_width.checked_sub(1).ok_or(PemError::InvalidOptions)?)
86 .ok_or(PemError::InvalidOptions)?
87 / line_width;
88 let body_newlines = line_count
89 .checked_mul(newline_length)
90 .ok_or(PemError::InvalidOptions)?;
91
92 boundary_length
93 .checked_add(encoded_length)
94 .and_then(|length| length.checked_add(body_newlines))
95 .and_then(|length| length.checked_add(footer_length))
96 .ok_or(PemError::InvalidOptions)
97}