From 644da09a60a2fe261c6d6900a5ceb10e2a2cd2b9 Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Fri, 4 Mar 2022 00:17:42 -0700 Subject: [PATCH] pem-rfc7468: add buffered `Encoder` type --- base64ct/src/encoder.rs | 36 ++++++- pem-rfc7468/src/decoder.rs | 4 +- pem-rfc7468/src/encoder.rs | 203 ++++++++++++++++++++++--------------- pem-rfc7468/src/error.rs | 4 +- pem-rfc7468/src/lib.rs | 35 +++---- 5 files changed, 174 insertions(+), 108 deletions(-) diff --git a/base64ct/src/encoder.rs b/base64ct/src/encoder.rs index f1fd506c9..8df424586 100644 --- a/base64ct/src/encoder.rs +++ b/base64ct/src/encoder.rs @@ -114,15 +114,28 @@ impl<'o, E: Variant> Encoder<'o, E> { Ok(()) } + /// Get the position inside of the output buffer where the write cursor + /// is currently located. + pub fn position(&self) -> usize { + self.position + } + /// Finish encoding data, returning the resulting Base64 as a `str`. - pub fn finish(mut self) -> Result<&'o str, Error> { + pub fn finish(self) -> Result<&'o str, Error> { + self.finish_with_remaining().map(|(base64, _)| base64) + } + + /// Finish encoding data, returning the resulting Base64 as a `str` + /// along with the remaining space in the output buffer. + pub fn finish_with_remaining(mut self) -> Result<(&'o str, &'o mut [u8]), Error> { if !self.block_buffer.is_empty() { let buffer_len = self.block_buffer.position; let block = self.block_buffer.bytes; self.perform_encode(&block[..buffer_len])?; } - Ok(str::from_utf8(&self.output[..self.position])?) + let (base64, remaining) = self.output.split_at_mut(self.position); + Ok((str::from_utf8(base64)?, remaining)) } /// Borrow the remaining data in the buffer. @@ -253,7 +266,7 @@ impl LineWrapper { fn insert_newlines(&mut self, mut buffer: &mut [u8], len: &mut usize) -> Result<(), Error> { let mut buffer_len = *len; - if buffer_len < self.remaining { + if buffer_len <= self.remaining { self.remaining = self .remaining .checked_sub(buffer_len) @@ -267,8 +280,8 @@ impl LineWrapper { .checked_sub(self.remaining) .ok_or(InvalidLength)?; - // The `wrap_blocks` function should ensure the buffer is smaller than a Base64 block - debug_assert!(buffer_len < 4, "buffer exceeds 4-bytes"); + // The `wrap_blocks` function should ensure the buffer is no larger than a Base64 block + debug_assert!(buffer_len <= 4, "buffer too long: {}", buffer_len); if buffer_len + self.ending.len() >= buffer.len() { // Not enough space in buffer to add newlines @@ -312,6 +325,19 @@ mod tests { encode_test::(MULTILINE_UNPADDED_BIN, MULTILINE_UNPADDED_BASE64, Some(70)); } + #[test] + fn no_trailing_newline_when_aligned() { + let mut buffer = [0u8; 64]; + let mut encoder = Encoder::::new_wrapped(&mut buffer, 64, LineEnding::LF).unwrap(); + encoder.encode(&[0u8; 48]).unwrap(); + + // Ensure no newline character is present in this case + assert_eq!( + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + encoder.finish().unwrap() + ); + } + /// Core functionality of an encoding test. fn encode_test(input: &[u8], expected: &str, wrapped: Option) { let mut buffer = [0u8; 1024]; diff --git a/pem-rfc7468/src/decoder.rs b/pem-rfc7468/src/decoder.rs index 366c7749d..5ce6da312 100644 --- a/pem-rfc7468/src/decoder.rs +++ b/pem-rfc7468/src/decoder.rs @@ -74,7 +74,7 @@ fn check_for_headers(pem: &[u8], err: Error) -> Error { } } -/// PEM decoder. +/// Buffered PEM decoder. /// /// Stateful buffered decoder type which decodes an input PEM document according /// to RFC 7468's "Strict" grammar. @@ -133,7 +133,7 @@ impl<'i> Decoder<'i> { self.base64.is_finished() } - /// Convert into the inner [`base64::Decoder`]. + /// Convert into the inner [`Base64Decoder`]. pub fn into_base64_decoder(self) -> Base64Decoder<'i> { self.base64 } diff --git a/pem-rfc7468/src/encoder.rs b/pem-rfc7468/src/encoder.rs index 768ea02a2..43a8a1f19 100644 --- a/pem-rfc7468/src/encoder.rs +++ b/pem-rfc7468/src/encoder.rs @@ -1,8 +1,8 @@ //! PEM encoder. use crate::{ - grammar, Error, LineEnding, Result, BASE64_WRAP_WIDTH, ENCAPSULATION_BOUNDARY_DELIMITER, - POST_ENCAPSULATION_BOUNDARY, PRE_ENCAPSULATION_BOUNDARY, + grammar, Base64Encoder, Error, LineEnding, Result, BASE64_WRAP_WIDTH, + ENCAPSULATION_BOUNDARY_DELIMITER, POST_ENCAPSULATION_BOUNDARY, PRE_ENCAPSULATION_BOUNDARY, }; use base64ct::{Base64, Encoding}; @@ -10,45 +10,28 @@ use base64ct::{Base64, Encoding}; use alloc::string::String; /// Encode a PEM document according to RFC 7468's "Strict" grammar. -pub fn encode<'a>( - label: &str, +pub fn encode<'o>( + type_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() + buf: &'o mut [u8], +) -> Result<&'o [u8]> { + let mut encoder = Encoder::new(type_label, line_ending, buf)?; + encoder.encode(input)?; + let encoded_len = encoder.finish()?; + Ok(&buf[..encoded_len]) } /// 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() + let base64_len = input + .chunks((BASE64_WRAP_WIDTH * 3) / 4) + .fold(0, |acc, chunk| { + acc + Base64::encoded_len(chunk) + line_ending.len() + }); + + encoded_len_inner(label, line_ending, base64_len) } /// Encode a PEM document according to RFC 7468's "Strict" grammar, returning @@ -61,66 +44,128 @@ pub fn encode_string(label: &str, line_ending: LineEnding, input: &[u8]) -> Resu String::from_utf8(buf).map_err(|_| Error::CharacterEncoding) } -/// Output buffer for writing encoded PEM output. -struct Buffer<'a> { - /// Backing byte slice where PEM output is being written. - bytes: &'a mut [u8], +/// Buffered PEM encoder. +/// +/// Stateful buffered encoder type which encodes an input PEM document according +/// to RFC 7468's "Strict" grammar. +pub struct Encoder<'l, 'o> { + /// PEM type label. + type_label: &'l str, - /// Total number of bytes written into the buffer so far. - position: usize, - - /// Line ending to use + /// Line ending used to wrap Base64. line_ending: LineEnding, + + /// Buffered Base64 encoder. + base64: Base64Encoder<'o>, } -impl<'a> Buffer<'a> { - /// Initialize buffer. - pub fn new(bytes: &'a mut [u8], line_ending: LineEnding) -> Self { - Self { - bytes, - position: 0, - line_ending, - } +impl<'l, 'o> Encoder<'l, 'o> { + /// Create a new PEM [`Encoder`] with the default options which + /// writes output into the provided buffer. + /// + /// Uses the default 64-character line wrapping. + pub fn new(type_label: &'l str, line_ending: LineEnding, out: &'o mut [u8]) -> Result { + Self::new_wrapped(type_label, BASE64_WRAP_WIDTH, line_ending, out) } - /// 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(()) + /// Create a new PEM [`Encoder`] which wraps at the given line width. + pub fn new_wrapped( + type_label: &'l str, + line_width: usize, + line_ending: LineEnding, + mut out: &'o mut [u8], + ) -> Result { + grammar::validate_label(type_label.as_bytes())?; + + for boundary_part in [ + PRE_ENCAPSULATION_BOUNDARY, + type_label.as_bytes(), + ENCAPSULATION_BOUNDARY_DELIMITER, + line_ending.as_bytes(), + ] { + if out.len() < boundary_part.len() { + return Err(Error::Length); + } + + let (part, rest) = out.split_at_mut(boundary_part.len()); + out = rest; + + part.copy_from_slice(boundary_part); + } + + let base64 = Base64Encoder::new_wrapped(out, line_width, line_ending)?; + + Ok(Self { + type_label, + line_ending, + base64, + }) } - /// 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()) + /// Get the PEM type label used for this document. + pub fn type_label(&self) -> &'l str { + self.type_label } - /// Write Base64-encoded data to the buffer. + /// Encode the provided input data. /// - /// 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()) + /// This method can be called as many times as needed with any sized input + /// to write data encoded data into the output buffer, so long as there is + /// sufficient space in the buffer to handle the resulting Base64 encoded + /// data. + pub fn encode(&mut self, input: &[u8]) -> Result<()> { + self.base64.encode(input)?; + Ok(()) } - /// 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) + /// Borrow the inner [`Base64Encoder`]. + pub fn base64_encoder(&mut self) -> &mut Base64Encoder<'o> { + &mut self.base64 } - /// 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)?; + /// Finish encoding PEM, writing the post-encapsulation boundary. + /// + /// On success, returns the total number of bytes written to the output + /// buffer. + pub fn finish(self) -> Result { + let (base64, mut out) = self.base64.finish_with_remaining()?; + + for boundary_part in [ + self.line_ending.as_bytes(), + POST_ENCAPSULATION_BOUNDARY, + self.type_label.as_bytes(), + ENCAPSULATION_BOUNDARY_DELIMITER, + self.line_ending.as_bytes(), + ] { + if out.len() < boundary_part.len() { + return Err(Error::Length); + } + + let (part, rest) = out.split_at_mut(boundary_part.len()); + out = rest; + + part.copy_from_slice(boundary_part); + } - self.position = new_position; - Ok(reserved) + Ok(encoded_len_inner( + self.type_label, + self.line_ending, + base64.len() + self.line_ending.len(), + )) } } + +/// Compute the length of a PEM encoded document with a Base64-encoded body of +/// the given length. +fn encoded_len_inner(label: &str, line_ending: LineEnding, base64_len: usize) -> usize { + // TODO(tarcieri): use checked arithmetic + PRE_ENCAPSULATION_BOUNDARY.len() + + label.as_bytes().len() + + ENCAPSULATION_BOUNDARY_DELIMITER.len() + + line_ending.len() + + base64_len + + POST_ENCAPSULATION_BOUNDARY.len() + + label.as_bytes().len() + + ENCAPSULATION_BOUNDARY_DELIMITER.len() + + line_ending.len() +} diff --git a/pem-rfc7468/src/error.rs b/pem-rfc7468/src/error.rs index 643479a44..d0172fcff 100644 --- a/pem-rfc7468/src/error.rs +++ b/pem-rfc7468/src/error.rs @@ -2,10 +2,10 @@ use core::fmt; -/// Result type. +/// Result type with the `pem-rfc7468` crate's [`Error`] type. pub type Result = core::result::Result; -/// Error type. +/// PEM errors. #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[non_exhaustive] pub enum Error { diff --git a/pem-rfc7468/src/lib.rs b/pem-rfc7468/src/lib.rs index bf8d8a54f..a7ee2fa77 100644 --- a/pem-rfc7468/src/lib.rs +++ b/pem-rfc7468/src/lib.rs @@ -1,11 +1,19 @@ +#![no_std] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/RustCrypto/meta/master/logo.svg", + html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/meta/master/logo.svg", + html_root_url = "https://docs.rs/pem-rfc7468/0.4.0-pre.0" +)] +#![forbid(unsafe_code, clippy::unwrap_used)] +#![warn(missing_docs, rust_2018_idioms, unused_qualifications)] #![doc = include_str!("../README.md")] //! # Usage //! -//! ``` +#![cfg_attr(feature = "std", doc = " ```")] +#![cfg_attr(not(feature = "std"), doc = " ```ignore")] //! # fn main() -> Result<(), Box> { -//! # #[cfg(feature = "std")] -//! # { //! /// Example PEM document //! /// NOTE: do not actually put private key literals into your source code!!! //! let example_pem = "\ @@ -30,25 +38,9 @@ //! use pem_rfc7468::LineEnding; //! let encoded_pem = pem_rfc7468::encode_string(type_label, LineEnding::default(), &data)?; //! assert_eq!(&encoded_pem, example_pem); -//! # } //! # Ok(()) //! # } //! ``` -//! -//! [RFC 1421]: https://datatracker.ietf.org/doc/html/rfc1421 -//! [RFC 7468]: https://datatracker.ietf.org/doc/html/rfc7468 -//! [RFC 7468 p6]: https://datatracker.ietf.org/doc/html/rfc7468#page-6 -//! [Util::Lookup]: https://arxiv.org/pdf/2108.04600.pdf - -#![no_std] -#![cfg_attr(docsrs, feature(doc_cfg))] -#![doc( - html_logo_url = "https://raw.githubusercontent.com/RustCrypto/meta/master/logo.svg", - html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/meta/master/logo.svg", - html_root_url = "https://docs.rs/pem-rfc7468/0.4.0-pre.0" -)] -#![forbid(unsafe_code, clippy::unwrap_used)] -#![warn(missing_docs, rust_2018_idioms, unused_qualifications)] #[cfg(feature = "alloc")] #[macro_use] @@ -63,7 +55,7 @@ mod grammar; pub use crate::{ decoder::{decode, decode_label, Decoder}, - encoder::{encode, encoded_len}, + encoder::{encode, encoded_len, Encoder}, error::{Error, Result}, }; pub use base64ct::LineEnding; @@ -98,6 +90,9 @@ const BASE64_WRAP_WIDTH: usize = 64; /// Buffered Base64 decoder type. pub type Base64Decoder<'i> = base64ct::Decoder<'i, base64ct::Base64>; +/// Buffered Base64 encoder type. +pub type Base64Encoder<'o> = base64ct::Encoder<'o, base64ct::Base64>; + /// Marker trait for types with an associated PEM type label. pub trait PemLabel { /// Expected PEM type label for a given document, e.g. `"PRIVATE KEY"`