diff --git a/der/src/encode.rs b/der/src/encode.rs index 1de1cb371..078026a8d 100644 --- a/der/src/encode.rs +++ b/der/src/encode.rs @@ -3,15 +3,18 @@ use crate::{Encoder, Header, Length, Result, Tagged, Writer}; #[cfg(feature = "alloc")] -use {crate::ErrorKind, alloc::vec::Vec, core::iter}; +use {alloc::vec::Vec, core::iter}; #[cfg(feature = "pem")] use { + crate::PemWriter, alloc::string::String, pem_rfc7468::{self as pem, LineEnding, PemLabel}, - zeroize::Zeroizing, }; +#[cfg(any(feature = "alloc", feature = "pem"))] +use crate::ErrorKind; + #[cfg(doc)] use crate::Tag; @@ -95,10 +98,30 @@ pub trait EncodePem: Encode + PemLabel { #[cfg(feature = "pem")] #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] impl EncodePem for T { + #[allow(clippy::integer_arithmetic)] fn to_pem(&self, line_ending: LineEnding) -> Result { - // TODO(tarcieri): support for encoding directly from PEM (instead of two-pass) - let der = Zeroizing::new(self.to_vec()?); - Ok(pem::encode_string(Self::PEM_LABEL, line_ending, &der)?) + // TODO(tarcieri): checked arithmetic, maybe extract this into `base64ct::base64_len`? + let der_len = usize::try_from(self.encoded_len()?)?; + let mut base64_len = (((der_len * 4) / 3) + 3) & !3; + + // Add the length of the line endings which will be inserted when + // encoded Base64 is line wrapped + // TODO(tarcieri): factor this logic into `pem-rfc7468` + base64_len += base64_len + .saturating_sub(1) + .checked_div(64) + .and_then(|len| len.checked_add(line_ending.len())) + .ok_or(ErrorKind::Overflow)?; + + let pem_len = pem::encapsulated_len(Self::PEM_LABEL, line_ending, base64_len)?; + + let mut buf = vec![0u8; pem_len]; + let mut writer = PemWriter::new(Self::PEM_LABEL, line_ending, &mut buf)?; + self.encode(&mut writer)?; + + let actual_len = writer.finish()?; + buf.truncate(actual_len); + Ok(String::from_utf8(buf)?) } } diff --git a/der/src/error.rs b/der/src/error.rs index 69d2485cb..5e492a48d 100644 --- a/der/src/error.rs +++ b/der/src/error.rs @@ -118,6 +118,13 @@ impl From for Error { } } +#[cfg(feature = "alloc")] +impl From for Error { + fn from(err: alloc::string::FromUtf8Error) -> Error { + ErrorKind::Utf8(err.utf8_error()).into() + } +} + #[cfg(feature = "oid")] impl From for Error { fn from(_: const_oid::Error) -> Error { @@ -150,6 +157,7 @@ impl From for Error { ErrorKind::DateTime.into() } } + /// Error type. #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[non_exhaustive] diff --git a/der/src/lib.rs b/der/src/lib.rs index 2ab2a117e..c7410f0bb 100644 --- a/der/src/lib.rs +++ b/der/src/lib.rs @@ -338,7 +338,8 @@ //! [`Utf8String`]: asn1::Utf8String #[cfg(feature = "alloc")] -#[cfg_attr(test, macro_use)] +#[allow(unused_imports)] +#[macro_use] extern crate alloc; #[cfg(feature = "std")] extern crate std; @@ -396,7 +397,7 @@ pub use der_derive::{Choice, Enumerated, Newtype, Sequence, ValueOrd}; #[cfg(feature = "pem")] #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] pub use { - crate::{decode::DecodePem, encode::EncodePem}, + crate::{decode::DecodePem, encode::EncodePem, writer::PemWriter}, pem_rfc7468 as pem, }; diff --git a/der/src/writer.rs b/der/src/writer.rs index 1d03134e7..79cbc0662 100644 --- a/der/src/writer.rs +++ b/der/src/writer.rs @@ -2,6 +2,9 @@ use crate::Result; +#[cfg(feature = "pem")] +use crate::pem; + #[cfg(feature = "std")] use std::io; @@ -16,6 +19,48 @@ pub trait Writer { } } +/// `Writer` type which outputs PEM-encoded data. +#[cfg(feature = "pem")] +#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] +pub struct PemWriter<'o>(pem::Encoder<'static, 'o>); + +#[cfg(feature = "pem")] +#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] +impl<'o> PemWriter<'o> { + /// Create a new PEM writer which outputs into the provided buffer. + /// + /// Uses the default 64-character line wrapping. + pub fn new( + type_label: &'static str, + line_ending: pem::LineEnding, + out: &'o mut [u8], + ) -> Result { + Ok(Self(pem::Encoder::new(type_label, line_ending, out)?)) + } + + /// Get the PEM label which will be used in the encapsulation boundaries + /// for this document. + pub fn type_label(&self) -> &'static str { + self.0.type_label() + } + + /// 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 { + Ok(self.0.finish()?) + } +} + +#[cfg(feature = "pem")] +#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] +impl Writer for PemWriter<'_> { + fn write(&mut self, slice: &[u8]) -> Result<()> { + self.0.encode(slice)?; + Ok(()) + } +} + #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] impl Writer for W {