Skip to content

Commit

Permalink
ssh-encoding: internal PemReader/PemWriter newtypes
Browse files Browse the repository at this point in the history
Ala #220, but for PEM, creates newtypes implementing the `Reader` and
`Writer` traits for greater future flexibility around these types.

Also factors them into internal submodules.
  • Loading branch information
tarcieri committed Apr 29, 2024
1 parent d0c57d4 commit 2ea0513
Show file tree
Hide file tree
Showing 13 changed files with 177 additions and 122 deletions.
8 changes: 4 additions & 4 deletions ssh-encoding/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,19 @@ rust-version = "1.60"
[dependencies]
base64ct = { version = "1.4", optional = true }
bytes = { version = "1", optional = true, default-features = false }
pem = { package = "pem-rfc7468", version = "=1.0.0-pre.0", optional = true }
pem-rfc7468 = { version = "=1.0.0-pre.0", optional = true }
sha2 = { version = "=0.11.0-pre.3", optional = true, default-features = false }

[dev-dependencies]
hex-literal = "0.4.1"

[features]
alloc = ["base64ct?/alloc", "pem?/alloc"]
std = ["alloc", "base64ct?/std", "pem?/std", "sha2?/std"]
alloc = ["base64ct?/alloc", "pem-rfc7468?/alloc"]
std = ["alloc", "base64ct?/std", "pem-rfc7468?/std", "sha2?/std"]

base64 = ["dep:base64ct"]
bytes = ["alloc", "dep:bytes"]
pem = ["base64", "dep:pem"]
pem = ["base64", "dep:pem-rfc7468"]

[package.metadata.docs.rs]
all-features = true
Expand Down
26 changes: 0 additions & 26 deletions ssh-encoding/src/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ use alloc::{string::String, vec::Vec};
#[cfg(feature = "bytes")]
use bytes::Bytes;

#[cfg(feature = "pem")]
use {crate::PEM_LINE_WIDTH, pem::PemLabel};

/// Maximum size of a `usize` this library will accept.
const MAX_SIZE: usize = 0xFFFFF;

Expand All @@ -28,29 +25,6 @@ pub trait Decode: Sized {
fn decode(reader: &mut impl Reader) -> core::result::Result<Self, Self::Error>;
}

/// Decoding trait for PEM documents.
///
/// This is an extension trait which is auto-impl'd for types which impl the
/// [`Decode`], [`PemLabel`], and [`Sized`] traits.
#[cfg(feature = "pem")]
pub trait DecodePem: Decode + PemLabel + Sized {
/// Decode the provided PEM-encoded string, interpreting the Base64-encoded
/// body of the document using the [`Decode`] trait.
fn decode_pem(pem: impl AsRef<[u8]>) -> core::result::Result<Self, Self::Error>;
}

#[cfg(feature = "pem")]
impl<T: Decode + PemLabel + Sized> DecodePem for T {
fn decode_pem(pem: impl AsRef<[u8]>) -> core::result::Result<Self, Self::Error> {
let mut reader =
pem::Decoder::new_wrapped(pem.as_ref(), PEM_LINE_WIDTH).map_err(Error::from)?;

Self::validate_pem_label(reader.type_label()).map_err(Error::from)?;
let ret = Self::decode(&mut reader)?;
Ok(reader.finish(ret)?)
}
}

/// Decode a single `byte` from the input data.
impl Decode for u8 {
type Error = Error;
Expand Down
51 changes: 0 additions & 51 deletions ssh-encoding/src/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,6 @@ use alloc::{string::String, vec::Vec};
#[cfg(feature = "bytes")]
use bytes::Bytes;

#[cfg(feature = "pem")]
use {
crate::PEM_LINE_WIDTH,
pem::{LineEnding, PemLabel},
};

/// Encoding trait.
///
/// This trait describes how to encode a given type.
Expand All @@ -42,51 +36,6 @@ pub trait Encode {
}
}

/// Encoding trait for PEM documents.
///
/// This is an extension trait which is auto-impl'd for types which impl the
/// [`Encode`] and [`PemLabel`] traits.
#[cfg(feature = "pem")]
pub trait EncodePem: Encode + PemLabel {
/// Encode this type using the [`Encode`] trait, writing the resulting PEM
/// document into the provided `out` buffer.
fn encode_pem<'o>(&self, line_ending: LineEnding, out: &'o mut [u8]) -> Result<&'o str, Error>;

/// Encode this type using the [`Encode`] trait, writing the resulting PEM
/// document to a returned [`String`].
#[cfg(feature = "alloc")]
fn encode_pem_string(&self, line_ending: LineEnding) -> Result<String, Error>;
}

#[cfg(feature = "pem")]
impl<T: Encode + PemLabel> EncodePem for T {
fn encode_pem<'o>(&self, line_ending: LineEnding, out: &'o mut [u8]) -> Result<&'o str, Error> {
let mut writer =
pem::Encoder::new_wrapped(Self::PEM_LABEL, PEM_LINE_WIDTH, line_ending, out)
.map_err(Error::from)?;

self.encode(&mut writer)?;
let encoded_len = writer.finish().map_err(Error::from)?;
str::from_utf8(&out[..encoded_len]).map_err(Error::from)
}

#[cfg(feature = "alloc")]
fn encode_pem_string(&self, line_ending: LineEnding) -> Result<String, Error> {
let encoded_len = pem::encapsulated_len_wrapped(
Self::PEM_LABEL,
PEM_LINE_WIDTH,
line_ending,
self.encoded_len()?,
)
.map_err(Error::from)?;

let mut buf = vec![0u8; encoded_len];
let actual_len = self.encode_pem(line_ending, &mut buf)?.len();
buf.truncate(actual_len);
String::from_utf8(buf).map_err(Error::from)
}
}

/// Encode a single `byte` to the writer.
impl Encode for u8 {
fn encoded_len(&self) -> Result<usize, Error> {
Expand Down
6 changes: 3 additions & 3 deletions ssh-encoding/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pub enum Error {

/// PEM encoding errors.
#[cfg(feature = "pem")]
Pem(pem::Error),
Pem(pem_rfc7468::Error),

/// Unexpected trailing data at end of message.
TrailingData {
Expand Down Expand Up @@ -96,8 +96,8 @@ impl From<base64ct::InvalidLengthError> for Error {
}

#[cfg(feature = "pem")]
impl From<pem::Error> for Error {
fn from(err: pem::Error) -> Error {
impl From<pem_rfc7468::Error> for Error {
fn from(err: pem_rfc7468::Error) -> Error {
Error::Pem(err)
}
}
Expand Down
11 changes: 3 additions & 8 deletions ssh-encoding/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ mod writer;

#[cfg(feature = "base64")]
pub mod base64;
#[cfg(feature = "pem")]
pub mod pem;

pub use crate::{
checked::CheckedSum,
Expand All @@ -48,11 +50,4 @@ pub use crate::{
pub use crate::{base64::Base64Reader, base64::Base64Writer};

#[cfg(feature = "pem")]
pub use {
crate::{decode::DecodePem, encode::EncodePem},
pem::{self, LineEnding},
};

/// Line width used by the PEM encoding of OpenSSH documents.
#[cfg(feature = "pem")]
const PEM_LINE_WIDTH: usize = 70;
pub use crate::pem::{DecodePem, EncodePem};
12 changes: 12 additions & 0 deletions ssh-encoding/src/pem.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//! PEM encoding support.

mod decode;
mod encode;
mod reader;
mod writer;

pub use self::{decode::DecodePem, encode::EncodePem};
pub use pem_rfc7468::{Error, LineEnding, PemLabel};

/// Line width used by the PEM encoding of OpenSSH documents.
const LINE_WIDTH: usize = 70;
22 changes: 22 additions & 0 deletions ssh-encoding/src/pem/decode.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use super::{reader::PemReader, PemLabel};
use crate::{Decode, Reader};

/// Decoding trait for PEM documents.
///
/// This is an extension trait which is auto-impl'd for types which impl the
/// [`Decode`], [`PemLabel`], and [`Sized`] traits.
pub trait DecodePem: Decode + PemLabel + Sized {
/// Decode the provided PEM-encoded string, interpreting the Base64-encoded
/// body of the document using the [`Decode`] trait.
fn decode_pem(pem: impl AsRef<[u8]>) -> Result<Self, Self::Error>;
}

impl<T: Decode + PemLabel + Sized> DecodePem for T {
fn decode_pem(pem: impl AsRef<[u8]>) -> Result<Self, Self::Error> {
let mut reader = PemReader::new(pem.as_ref()).map_err(crate::Error::from)?;
Self::validate_pem_label(reader.type_label()).map_err(crate::Error::from)?;

let ret = Self::decode(&mut reader)?;
Ok(reader.finish(ret)?)
}
}
47 changes: 47 additions & 0 deletions ssh-encoding/src/pem/encode.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use super::{writer::PemWriter, LineEnding, PemLabel};
use crate::{Encode, Error};
use core::str;

#[cfg(feature = "alloc")]
use {super::LINE_WIDTH, alloc::string::String};

/// Encoding trait for PEM documents.
///
/// This is an extension trait which is auto-impl'd for types which impl the
/// [`Encode`] and [`PemLabel`] traits.
pub trait EncodePem: Encode + PemLabel {
/// Encode this type using the [`Encode`] trait, writing the resulting PEM
/// document into the provided `out` buffer.
fn encode_pem<'o>(&self, line_ending: LineEnding, out: &'o mut [u8]) -> Result<&'o str, Error>;

/// Encode this type using the [`Encode`] trait, writing the resulting PEM
/// document to a returned [`String`].
#[cfg(feature = "alloc")]
fn encode_pem_string(&self, line_ending: LineEnding) -> Result<String, Error>;
}

impl<T: Encode + PemLabel> EncodePem for T {
fn encode_pem<'o>(&self, line_ending: LineEnding, out: &'o mut [u8]) -> Result<&'o str, Error> {
let mut writer = PemWriter::new(Self::PEM_LABEL, line_ending, out).map_err(Error::from)?;
self.encode(&mut writer)?;

let encoded_len = writer.finish().map_err(Error::from)?;
str::from_utf8(&out[..encoded_len]).map_err(Error::from)
}

#[cfg(feature = "alloc")]
fn encode_pem_string(&self, line_ending: LineEnding) -> Result<String, Error> {
let encoded_len = pem_rfc7468::encapsulated_len_wrapped(
Self::PEM_LABEL,
LINE_WIDTH,
line_ending,
self.encoded_len()?,
)
.map_err(Error::from)?;

let mut buf = vec![0u8; encoded_len];
let actual_len = self.encode_pem(line_ending, &mut buf)?.len();
buf.truncate(actual_len);
String::from_utf8(buf).map_err(Error::from)
}
}
34 changes: 34 additions & 0 deletions ssh-encoding/src/pem/reader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use super::LINE_WIDTH;
use crate::{Reader, Result};

/// Inner PEM decoder.
type Inner<'i> = pem_rfc7468::Decoder<'i>;

/// Constant-time PEM reader.
pub struct PemReader<'i> {
inner: Inner<'i>,
}

impl<'i> PemReader<'i> {
/// TODO
pub fn new(pem: &'i [u8]) -> Result<Self> {
Ok(Self {
inner: Inner::new_wrapped(pem, LINE_WIDTH)?,
})
}

/// Get the PEM type label for the input document.
pub fn type_label(&self) -> &'i str {
self.inner.type_label()
}
}

impl Reader for PemReader<'_> {
fn read<'o>(&mut self, out: &'o mut [u8]) -> Result<&'o [u8]> {
Ok(self.inner.decode(out)?)
}

fn remaining_len(&self) -> usize {
self.inner.remaining_len()
}
}
40 changes: 40 additions & 0 deletions ssh-encoding/src/pem/writer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use super::{LineEnding, LINE_WIDTH};
use crate::{Result, Writer};

/// Inner PEM encoder.
type Inner<'o> = pem_rfc7468::Encoder<'static, 'o>;

/// Constant-time PEM writer.
pub struct PemWriter<'o> {
inner: Inner<'o>,
}

impl<'o> PemWriter<'o> {
/// Create a new PEM writer with the default options which writes output into the provided
/// buffer.
///
/// Uses 70-character line wrapping to be equivalent to OpenSSH.
pub fn new(
type_label: &'static str,
line_ending: LineEnding,
out: &'o mut [u8],
) -> Result<Self> {
Ok(Self {
inner: Inner::new_wrapped(type_label, LINE_WIDTH, line_ending, out)?,
})
}

/// 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<usize> {
Ok(self.inner.finish()?)
}
}

impl<'o> Writer for PemWriter<'o> {
fn write(&mut self, bytes: &[u8]) -> Result<()> {
Ok(self.inner.encode(bytes)?)
}
}
11 changes: 0 additions & 11 deletions ssh-encoding/src/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,17 +141,6 @@ impl Reader for &[u8] {
}
}

#[cfg(feature = "pem")]
impl Reader for pem::Decoder<'_> {
fn read<'o>(&mut self, out: &'o mut [u8]) -> Result<&'o [u8]> {
Ok(self.decode(out)?)
}

fn remaining_len(&self) -> usize {
self.remaining_len()
}
}

/// Reader type used by [`Reader::read_prefixed`].
pub struct NestedReader<'r, R: Reader> {
/// Inner reader type.
Expand Down
7 changes: 0 additions & 7 deletions ssh-encoding/src/writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,6 @@ impl Writer for Vec<u8> {
}
}

#[cfg(feature = "pem")]
impl Writer for pem::Encoder<'_, '_> {
fn write(&mut self, bytes: &[u8]) -> Result<()> {
Ok(self.encode(bytes)?)
}
}

#[cfg(feature = "sha2")]
impl Writer for Sha256 {
fn write(&mut self, bytes: &[u8]) -> Result<()> {
Expand Down
Loading

0 comments on commit 2ea0513

Please sign in to comment.