Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions pem-rfc7468/src/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ pub fn decode_label(pem: &[u8]) -> Result<&str> {
Ok(Encapsulation::try_from(pem)?.label())
}

/// Attempt to detect the Base64 line width for the given PEM document.
///
/// NOTE: not constant time with respect to the input.
pub fn detect_base64_line_width(pem: &[u8]) -> Result<usize> {
Ok(Encapsulation::try_from(pem)?.encapsulated_text_line_width())
}

/// Buffered PEM decoder.
///
/// Stateful buffered decoder type which decodes an input PEM document according
Expand All @@ -88,9 +95,19 @@ impl<'i> Decoder<'i> {
let encapsulation = Encapsulation::try_from(pem)?;
let type_label = encapsulation.label();
let base64 = Base64Decoder::new_wrapped(encapsulation.encapsulated_text, line_width)?;

Ok(Self { type_label, base64 })
}

/// Create a new PEM [`Decoder`] which automatically detects the line width the input is wrapped
/// at and flexibly handles widths other than the default 64-characters.
///
/// Note: unlike `new` and `new_wrapped`, this method is not constant-time.
pub fn new_detect_wrap(pem: &'i [u8]) -> Result<Self> {
let line_width = detect_base64_line_width(pem)?;
Self::new_wrapped(pem, line_width)
}

/// Get the PEM type label for the input document.
pub fn type_label(&self) -> &'i str {
self.type_label
Expand Down Expand Up @@ -224,6 +241,16 @@ impl<'a> Encapsulation<'a> {
pub fn label(self) -> &'a str {
self.label
}

/// Detect the line width of the encapsulated text by looking for the position of the first EOL.
pub fn encapsulated_text_line_width(self) -> usize {
// TODO(tarcieri): handle empty space between the pre-encapsulation boundary and Base64
self.encapsulated_text
.iter()
.copied()
.position(|c| matches!(c, grammar::CHAR_CR | grammar::CHAR_LF))
.unwrap_or(self.encapsulated_text.len())
}
}

impl<'a> TryFrom<&'a [u8]> for Encapsulation<'a> {
Expand Down
2 changes: 1 addition & 1 deletion pem-rfc7468/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ mod error;
mod grammar;

pub use crate::{
decoder::{decode, decode_label, Decoder},
decoder::{decode, decode_label, detect_base64_line_width, Decoder},
encoder::{encapsulated_len, encapsulated_len_wrapped, encode, encoded_len, Encoder},
error::{Error, Result},
};
Expand Down
15 changes: 15 additions & 0 deletions pem-rfc7468/tests/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,18 @@ fn ed25519_example() {
let label = pem_rfc7468::decode_label(pem).unwrap();
assert_eq!(label, "ED25519 CERT");
}

#[test]
fn line_width_detection() {
let pem_64cols = include_bytes!("examples/pkcs1.pem");
assert_eq!(
pem_rfc7468::detect_base64_line_width(pem_64cols).unwrap(),
64
);

let pem_70cols = include_bytes!("examples/ssh-id_ed25519.pem");
assert_eq!(
pem_rfc7468::detect_base64_line_width(pem_70cols).unwrap(),
70
);
}
7 changes: 7 additions & 0 deletions pem-rfc7468/tests/examples/ssh-id_ed25519.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACCzPq7zfqLffKoBDe/eo04kH2XxtSmk9D7RQyf1xUqrYgAAAJgAIAxdACAM
XQAAAAtzc2gtZWQyNTUxOQAAACCzPq7zfqLffKoBDe/eo04kH2XxtSmk9D7RQyf1xUqrYg
AAAEC2BsIi0QwW2uFscKTUUXNHLsYX4FxlaSDSblbAj7WR7bM+rvN+ot98qgEN796jTiQf
ZfG1KaT0PtFDJ/XFSqtiAAAAEHVzZXJAZXhhbXBsZS5jb20BAgMEBQ==
-----END OPENSSH PRIVATE KEY-----