pem_rfc7468/
encoder.rs

1//! PEM encoder.
2
3use crate::{
4    grammar, Base64Encoder, Error, LineEnding, Result, BASE64_WRAP_WIDTH,
5    ENCAPSULATION_BOUNDARY_DELIMITER, POST_ENCAPSULATION_BOUNDARY, PRE_ENCAPSULATION_BOUNDARY,
6};
7use base64ct::{Base64, Encoding};
8use core::str;
9
10#[cfg(feature = "alloc")]
11use alloc::string::String;
12
13#[cfg(feature = "std")]
14use std::io;
15
16/// Compute the length of a PEM encoded document which encapsulates a
17/// Base64-encoded body including line endings every 64 characters.
18///
19/// The `input_len` parameter specifies the length of the raw input
20/// bytes prior to Base64 encoding.
21///
22/// Note that the current implementation of this function computes an upper
23/// bound of the length and the actual encoded document may be slightly shorter
24/// (typically 1-byte). Downstream consumers of this function should check the
25/// actual encoded length and potentially truncate buffers allocated using this
26/// function to estimate the encapsulated size.
27///
28/// Use [`encoded_len`] (when possible) to obtain a precise length.
29///
30/// ## Returns
31/// - `Ok(len)` on success
32/// - `Err(Error::Length)` on length overflow
33pub fn encapsulated_len(label: &str, line_ending: LineEnding, input_len: usize) -> Result<usize> {
34    encapsulated_len_wrapped(label, BASE64_WRAP_WIDTH, line_ending, input_len)
35}
36
37/// Compute the length of a PEM encoded document with the Base64 body
38/// line wrapped at the specified `width`.
39///
40/// This is the same as [`encapsulated_len`], which defaults to a width of 64.
41///
42/// Note that per [RFC7468 § 2] encoding PEM with any other wrap width besides
43/// 64 is technically non-compliant:
44///
45/// > Generators MUST wrap the base64-encoded lines so that each line
46/// > consists of exactly 64 characters except for the final line, which
47/// > will encode the remainder of the data (within the 64-character line
48/// > boundary)
49///
50/// [RFC7468 § 2]: https://datatracker.ietf.org/doc/html/rfc7468#section-2
51pub fn encapsulated_len_wrapped(
52    label: &str,
53    line_width: usize,
54    line_ending: LineEnding,
55    input_len: usize,
56) -> Result<usize> {
57    if line_width < 4 {
58        return Err(Error::Length);
59    }
60
61    let base64_len = input_len
62        .checked_mul(4)
63        .and_then(|n| n.checked_div(3))
64        .and_then(|n| n.checked_add(3))
65        .ok_or(Error::Length)?
66        & !3;
67
68    let base64_len_wrapped = base64_len_wrapped(base64_len, line_width, line_ending)?;
69    encapsulated_len_inner(label, line_ending, base64_len_wrapped)
70}
71
72/// Get the length of a PEM encoded document with the given bytes and label.
73///
74/// This function computes a precise length of the PEM encoding of the given
75/// `input` data.
76///
77/// ## Returns
78/// - `Ok(len)` on success
79/// - `Err(Error::Length)` on length overflow
80pub fn encoded_len(label: &str, line_ending: LineEnding, input: &[u8]) -> Result<usize> {
81    let base64_len = Base64::encoded_len(input);
82    let base64_len_wrapped = base64_len_wrapped(base64_len, BASE64_WRAP_WIDTH, line_ending)?;
83    encapsulated_len_inner(label, line_ending, base64_len_wrapped)
84}
85
86/// Encode a PEM document according to RFC 7468's "Strict" grammar.
87pub fn encode<'o>(
88    type_label: &str,
89    line_ending: LineEnding,
90    input: &[u8],
91    buf: &'o mut [u8],
92) -> Result<&'o str> {
93    let mut encoder = Encoder::new(type_label, line_ending, buf)?;
94    encoder.encode(input)?;
95    let encoded_len = encoder.finish()?;
96    let output = &buf[..encoded_len];
97
98    // Sanity check
99    debug_assert!(str::from_utf8(output).is_ok());
100
101    // Ensure `output` contains characters from the lower 7-bit ASCII set
102    if output.iter().fold(0u8, |acc, &byte| acc | (byte & 0x80)) == 0 {
103        // Use unchecked conversion to avoid applying UTF-8 checks to potentially
104        // secret PEM documents (and therefore introducing a potential timing
105        // sidechannel)
106        //
107        // SAFETY: contents of this buffer are controlled entirely by the encoder,
108        // which ensures the contents are always a valid (ASCII) subset of UTF-8.
109        // It's also additionally sanity checked by two assertions above to ensure
110        // the validity (with the always-on runtime check implemented in a
111        // constant time-ish manner.
112        #[allow(unsafe_code)]
113        Ok(unsafe { str::from_utf8_unchecked(output) })
114    } else {
115        Err(Error::CharacterEncoding)
116    }
117}
118
119/// Encode a PEM document according to RFC 7468's "Strict" grammar, returning
120/// the result as a [`String`].
121#[cfg(feature = "alloc")]
122#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
123pub fn encode_string(label: &str, line_ending: LineEnding, input: &[u8]) -> Result<String> {
124    let expected_len = encoded_len(label, line_ending, input)?;
125    let mut buf = vec![0u8; expected_len];
126    let actual_len = encode(label, line_ending, input, &mut buf)?.len();
127    debug_assert_eq!(expected_len, actual_len);
128    String::from_utf8(buf).map_err(|_| Error::CharacterEncoding)
129}
130
131/// Compute the encapsulated length of Base64 data of the given length.
132fn encapsulated_len_inner(
133    label: &str,
134    line_ending: LineEnding,
135    base64_len: usize,
136) -> Result<usize> {
137    [
138        PRE_ENCAPSULATION_BOUNDARY.len(),
139        label.as_bytes().len(),
140        ENCAPSULATION_BOUNDARY_DELIMITER.len(),
141        line_ending.len(),
142        base64_len,
143        line_ending.len(),
144        POST_ENCAPSULATION_BOUNDARY.len(),
145        label.as_bytes().len(),
146        ENCAPSULATION_BOUNDARY_DELIMITER.len(),
147        line_ending.len(),
148    ]
149    .into_iter()
150    .try_fold(0usize, |acc, len| acc.checked_add(len))
151    .ok_or(Error::Length)
152}
153
154/// Compute Base64 length line-wrapped at the specified width with the given
155/// line ending.
156fn base64_len_wrapped(
157    base64_len: usize,
158    line_width: usize,
159    line_ending: LineEnding,
160) -> Result<usize> {
161    base64_len
162        .saturating_sub(1)
163        .checked_div(line_width)
164        .and_then(|lines| lines.checked_mul(line_ending.len()))
165        .and_then(|len| len.checked_add(base64_len))
166        .ok_or(Error::Length)
167}
168
169/// Buffered PEM encoder.
170///
171/// Stateful buffered encoder type which encodes an input PEM document according
172/// to RFC 7468's "Strict" grammar.
173pub struct Encoder<'l, 'o> {
174    /// PEM type label.
175    type_label: &'l str,
176
177    /// Line ending used to wrap Base64.
178    line_ending: LineEnding,
179
180    /// Buffered Base64 encoder.
181    base64: Base64Encoder<'o>,
182}
183
184impl<'l, 'o> Encoder<'l, 'o> {
185    /// Create a new PEM [`Encoder`] with the default options which
186    /// writes output into the provided buffer.
187    ///
188    /// Uses the default 64-character line wrapping.
189    pub fn new(type_label: &'l str, line_ending: LineEnding, out: &'o mut [u8]) -> Result<Self> {
190        Self::new_wrapped(type_label, BASE64_WRAP_WIDTH, line_ending, out)
191    }
192
193    /// Create a new PEM [`Encoder`] which wraps at the given line width.
194    ///
195    /// Note that per [RFC7468 § 2] encoding PEM with any other wrap width besides
196    /// 64 is technically non-compliant:
197    ///
198    /// > Generators MUST wrap the base64-encoded lines so that each line
199    /// > consists of exactly 64 characters except for the final line, which
200    /// > will encode the remainder of the data (within the 64-character line
201    /// > boundary)
202    ///
203    /// This method is provided with the intended purpose of implementing the
204    /// OpenSSH private key format, which uses a non-standard wrap width of 70.
205    ///
206    /// [RFC7468 § 2]: https://datatracker.ietf.org/doc/html/rfc7468#section-2
207    pub fn new_wrapped(
208        type_label: &'l str,
209        line_width: usize,
210        line_ending: LineEnding,
211        mut out: &'o mut [u8],
212    ) -> Result<Self> {
213        grammar::validate_label(type_label.as_bytes())?;
214
215        for boundary_part in [
216            PRE_ENCAPSULATION_BOUNDARY,
217            type_label.as_bytes(),
218            ENCAPSULATION_BOUNDARY_DELIMITER,
219            line_ending.as_bytes(),
220        ] {
221            if out.len() < boundary_part.len() {
222                return Err(Error::Length);
223            }
224
225            let (part, rest) = out.split_at_mut(boundary_part.len());
226            out = rest;
227
228            part.copy_from_slice(boundary_part);
229        }
230
231        let base64 = Base64Encoder::new_wrapped(out, line_width, line_ending)?;
232
233        Ok(Self {
234            type_label,
235            line_ending,
236            base64,
237        })
238    }
239
240    /// Get the PEM type label used for this document.
241    pub fn type_label(&self) -> &'l str {
242        self.type_label
243    }
244
245    /// Encode the provided input data.
246    ///
247    /// This method can be called as many times as needed with any sized input
248    /// to write data encoded data into the output buffer, so long as there is
249    /// sufficient space in the buffer to handle the resulting Base64 encoded
250    /// data.
251    pub fn encode(&mut self, input: &[u8]) -> Result<()> {
252        self.base64.encode(input)?;
253        Ok(())
254    }
255
256    /// Borrow the inner [`Base64Encoder`].
257    pub fn base64_encoder(&mut self) -> &mut Base64Encoder<'o> {
258        &mut self.base64
259    }
260
261    /// Finish encoding PEM, writing the post-encapsulation boundary.
262    ///
263    /// On success, returns the total number of bytes written to the output
264    /// buffer.
265    pub fn finish(self) -> Result<usize> {
266        let (base64, mut out) = self.base64.finish_with_remaining()?;
267
268        for boundary_part in [
269            self.line_ending.as_bytes(),
270            POST_ENCAPSULATION_BOUNDARY,
271            self.type_label.as_bytes(),
272            ENCAPSULATION_BOUNDARY_DELIMITER,
273            self.line_ending.as_bytes(),
274        ] {
275            if out.len() < boundary_part.len() {
276                return Err(Error::Length);
277            }
278
279            let (part, rest) = out.split_at_mut(boundary_part.len());
280            out = rest;
281
282            part.copy_from_slice(boundary_part);
283        }
284
285        encapsulated_len_inner(self.type_label, self.line_ending, base64.len())
286    }
287}
288
289#[cfg(feature = "std")]
290#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
291impl<'l, 'o> io::Write for Encoder<'l, 'o> {
292    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
293        self.encode(buf)?;
294        Ok(buf.len())
295    }
296
297    fn flush(&mut self) -> io::Result<()> {
298        // TODO(tarcieri): return an error if there's still data remaining in the buffer?
299        Ok(())
300    }
301}