pem-rfc7468 0.3.0

PEM Encoding (RFC 7468) for PKIX, PKCS, and CMS Structures, implementing a strict subset of the original Privacy-Enhanced Mail encoding intended specifically for use with cryptographic keys, certificates, and other messages. Provides a no_std-friendly, constant-time implementation suitable for use with cryptographic private keys.
Documentation
//! PEM encoder.

use crate::{
    grammar::{self, CHAR_CR, CHAR_LF},
    Error, Result, BASE64_WRAP_WIDTH, ENCAPSULATION_BOUNDARY_DELIMITER,
    POST_ENCAPSULATION_BOUNDARY, PRE_ENCAPSULATION_BOUNDARY,
};
use base64ct::{Base64, Encoding};

#[cfg(feature = "alloc")]
use alloc::string::String;

/// Encode a PEM document according to RFC 7468's "Strict" grammar.
pub fn encode<'a>(
    label: &str,
    line_ending: LineEnding,
    input: &[u8],
    buf: &'a mut [u8],
) -> Result<&'a [u8]> {
    grammar::validate_label(label.as_bytes())?;

    let mut buf = Buffer::new(buf, line_ending);
    buf.write(PRE_ENCAPSULATION_BOUNDARY)?;
    buf.write(label.as_bytes())?;
    buf.writeln(ENCAPSULATION_BOUNDARY_DELIMITER)?;

    for chunk in input.chunks((BASE64_WRAP_WIDTH * 3) / 4) {
        buf.write_base64ln(chunk)?;
    }

    buf.write(POST_ENCAPSULATION_BOUNDARY)?;
    buf.write(label.as_bytes())?;
    buf.writeln(ENCAPSULATION_BOUNDARY_DELIMITER)?;
    buf.finish()
}

/// Get the length of a PEM encoded document with the given bytes and label.
pub fn encoded_len(label: &str, line_ending: LineEnding, input: &[u8]) -> usize {
    // TODO(tarcieri): use checked arithmetic
    PRE_ENCAPSULATION_BOUNDARY.len()
        + label.as_bytes().len()
        + ENCAPSULATION_BOUNDARY_DELIMITER.len()
        + line_ending.len()
        + input
            .chunks((BASE64_WRAP_WIDTH * 3) / 4)
            .fold(0, |acc, chunk| {
                acc + Base64::encoded_len(chunk) + line_ending.len()
            })
        + POST_ENCAPSULATION_BOUNDARY.len()
        + label.as_bytes().len()
        + ENCAPSULATION_BOUNDARY_DELIMITER.len()
        + line_ending.len()
}

/// Encode a PEM document according to RFC 7468's "Strict" grammar, returning
/// the result as a [`String`].
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub fn encode_string(label: &str, line_ending: LineEnding, input: &[u8]) -> Result<String> {
    let mut buf = vec![0u8; encoded_len(label, line_ending, input)];
    encode(label, line_ending, input, &mut buf)?;
    String::from_utf8(buf).map_err(|_| Error::CharacterEncoding)
}

/// Line endings.
///
/// Use [`LineEnding::default`] to get an appropriate line ending for the
/// current operating system.
#[allow(clippy::upper_case_acronyms)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
pub enum LineEnding {
    /// Carriage return: `\r` (Pre-OS X Macintosh)
    CR,

    /// Line feed: `\n` (Unix OSes)
    LF,

    /// Carriage return + line feed: `\r\n` (Windows)
    CRLF,
}

impl Default for LineEnding {
    /// Use the line ending for the current OS
    #[cfg(windows)]
    fn default() -> LineEnding {
        LineEnding::CRLF
    }
    #[cfg(not(windows))]
    fn default() -> LineEnding {
        LineEnding::LF
    }
}

#[allow(clippy::len_without_is_empty)]
impl LineEnding {
    /// Get the byte serialization of this [`LineEnding`].
    pub fn as_bytes(self) -> &'static [u8] {
        match self {
            LineEnding::CR => &[CHAR_CR],
            LineEnding::LF => &[CHAR_LF],
            LineEnding::CRLF => &[CHAR_CR, CHAR_LF],
        }
    }

    /// Get the encoded length of this [`LineEnding`].
    pub fn len(self) -> usize {
        self.as_bytes().len()
    }
}

/// Output buffer for writing encoded PEM output.
struct Buffer<'a> {
    /// Backing byte slice where PEM output is being written.
    bytes: &'a mut [u8],

    /// Total number of bytes written into the buffer so far.
    position: usize,

    /// Line ending to use
    line_ending: LineEnding,
}

impl<'a> Buffer<'a> {
    /// Initialize buffer.
    pub fn new(bytes: &'a mut [u8], line_ending: LineEnding) -> Self {
        Self {
            bytes,
            position: 0,
            line_ending,
        }
    }

    /// Write a byte slice to the buffer.
    pub fn write(&mut self, slice: &[u8]) -> Result<()> {
        let reserved = self.reserve(slice.len())?;
        reserved.copy_from_slice(slice);
        Ok(())
    }

    /// Write a byte slice to the buffer with a newline at the end.
    pub fn writeln(&mut self, slice: &[u8]) -> Result<()> {
        self.write(slice)?;
        self.write(self.line_ending.as_bytes())
    }

    /// Write Base64-encoded data to the buffer.
    ///
    /// Automatically adds a newline at the end.
    pub fn write_base64ln(&mut self, bytes: &[u8]) -> Result<()> {
        let reserved = self.reserve(Base64::encoded_len(bytes))?;
        Base64::encode(bytes, reserved)?;
        self.write(self.line_ending.as_bytes())
    }

    /// Finish writing to the buffer, returning the portion that has been
    /// written to.
    pub fn finish(self) -> Result<&'a [u8]> {
        self.bytes.get(..self.position).ok_or(Error::Length)
    }

    /// Reserve space in the encoding buffer, returning a mutable slice.
    fn reserve(&mut self, nbytes: usize) -> Result<&mut [u8]> {
        let new_position = self.position.checked_add(nbytes).ok_or(Error::Length)?;

        let reserved = self
            .bytes
            .get_mut(self.position..new_position)
            .ok_or(Error::Length)?;

        self.position = new_position;
        Ok(reserved)
    }
}