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 let output_length = preflight_pem_encoded_length(label, der.len(), options)?;
17 let encoded_length = encoded_len(der.len(), true).ok_or(PemError::InvalidOptions)?;
18 let mut encoded = Zeroizing::new(vec![0_u8; encoded_length]);
19 let written = STANDARD
20 .encode_slice(der, encoded.as_mut_slice())
21 .map_err(|_| PemError::InvalidOptions)?;
22 if written != encoded_length {
23 return Err(PemError::InvalidOptions);
24 }
25
26 let newline = options.line_ending.as_str();
27 let mut output = Zeroizing::new(String::with_capacity(output_length));
31 output.push_str("-----BEGIN ");
32 output.push_str(label.as_str());
33 output.push_str("-----");
34 output.push_str(newline);
35
36 for chunk in encoded.as_slice().chunks(options.line_width) {
37 let line = core::str::from_utf8(chunk).map_err(|_| PemError::InvalidBase64)?;
38 output.push_str(line);
39 output.push_str(newline);
40 }
41
42 output.push_str("-----END ");
43 output.push_str(label.as_str());
44 output.push_str("-----");
45 output.push_str(newline);
46
47 if output.len() != output_length {
48 return Err(PemError::InvalidOptions);
49 }
50
51 Ok(output)
52}
53
54pub fn preflight_pem_encoded_length(
61 label: PemLabel,
62 der_length: usize,
63 options: PemEncodeOptions,
64) -> Result<usize, PemError> {
65 if der_length == 0 {
66 return Err(PemError::EmptyDer);
67 }
68 if der_length > options.max_der_len {
69 return Err(PemError::DerTooLarge);
70 }
71 if options.line_width == 0 || options.line_width > 76 {
72 return Err(PemError::InvalidOptions);
73 }
74 let encoded_length = encoded_len(der_length, true).ok_or(PemError::InvalidOptions)?;
75 encoded_pem_length(
76 label.as_str().len(),
77 encoded_length,
78 options.line_width,
79 options.line_ending.as_str().len(),
80 )
81}
82
83fn encoded_pem_length(
84 label_length: usize,
85 encoded_length: usize,
86 line_width: usize,
87 newline_length: usize,
88) -> Result<usize, PemError> {
89 let boundary_length = "-----BEGIN "
90 .len()
91 .checked_add(label_length)
92 .and_then(|length| length.checked_add("-----".len()))
93 .and_then(|length| length.checked_add(newline_length))
94 .ok_or(PemError::InvalidOptions)?;
95 let footer_length = "-----END "
96 .len()
97 .checked_add(label_length)
98 .and_then(|length| length.checked_add("-----".len()))
99 .and_then(|length| length.checked_add(newline_length))
100 .ok_or(PemError::InvalidOptions)?;
101 let line_count = encoded_length
102 .checked_add(line_width.checked_sub(1).ok_or(PemError::InvalidOptions)?)
103 .ok_or(PemError::InvalidOptions)?
104 / line_width;
105 let body_newlines = line_count
106 .checked_mul(newline_length)
107 .ok_or(PemError::InvalidOptions)?;
108
109 boundary_length
110 .checked_add(encoded_length)
111 .and_then(|length| length.checked_add(body_newlines))
112 .and_then(|length| length.checked_add(footer_length))
113 .ok_or(PemError::InvalidOptions)
114}