Skip to content

Commit a2f59e9

Browse files
authored
pem-rfc7468: add detect_base64_line_width and new_detect_wrap (#1464)
Adds the following new functions: - `detect_base64_line_width`: a toplevel free function similar to `decode_label` which outputs an autodetected line width for a given input PEM byte slice. - `Decoder::new_detect_wrap`: an alternative to `Decoder::new_wrapped` which attempts to autodetect the Base64 line width in order to flexibly handle inputs wrapped at any size. For an initial implementation, empty space between the pre-encapsulation boundary and the start of the Base64 is not handled.
1 parent 0d294bb commit a2f59e9

4 files changed

Lines changed: 50 additions & 1 deletion

File tree

pem-rfc7468/src/decoder.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ pub fn decode_label(pem: &[u8]) -> Result<&str> {
6262
Ok(Encapsulation::try_from(pem)?.label())
6363
}
6464

65+
/// Attempt to detect the Base64 line width for the given PEM document.
66+
///
67+
/// NOTE: not constant time with respect to the input.
68+
pub fn detect_base64_line_width(pem: &[u8]) -> Result<usize> {
69+
Ok(Encapsulation::try_from(pem)?.encapsulated_text_line_width())
70+
}
71+
6572
/// Buffered PEM decoder.
6673
///
6774
/// Stateful buffered decoder type which decodes an input PEM document according
@@ -88,9 +95,19 @@ impl<'i> Decoder<'i> {
8895
let encapsulation = Encapsulation::try_from(pem)?;
8996
let type_label = encapsulation.label();
9097
let base64 = Base64Decoder::new_wrapped(encapsulation.encapsulated_text, line_width)?;
98+
9199
Ok(Self { type_label, base64 })
92100
}
93101

102+
/// Create a new PEM [`Decoder`] which automatically detects the line width the input is wrapped
103+
/// at and flexibly handles widths other than the default 64-characters.
104+
///
105+
/// Note: unlike `new` and `new_wrapped`, this method is not constant-time.
106+
pub fn new_detect_wrap(pem: &'i [u8]) -> Result<Self> {
107+
let line_width = detect_base64_line_width(pem)?;
108+
Self::new_wrapped(pem, line_width)
109+
}
110+
94111
/// Get the PEM type label for the input document.
95112
pub fn type_label(&self) -> &'i str {
96113
self.type_label
@@ -224,6 +241,16 @@ impl<'a> Encapsulation<'a> {
224241
pub fn label(self) -> &'a str {
225242
self.label
226243
}
244+
245+
/// Detect the line width of the encapsulated text by looking for the position of the first EOL.
246+
pub fn encapsulated_text_line_width(self) -> usize {
247+
// TODO(tarcieri): handle empty space between the pre-encapsulation boundary and Base64
248+
self.encapsulated_text
249+
.iter()
250+
.copied()
251+
.position(|c| matches!(c, grammar::CHAR_CR | grammar::CHAR_LF))
252+
.unwrap_or(self.encapsulated_text.len())
253+
}
227254
}
228255

229256
impl<'a> TryFrom<&'a [u8]> for Encapsulation<'a> {

pem-rfc7468/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ mod error;
6363
mod grammar;
6464

6565
pub use crate::{
66-
decoder::{decode, decode_label, Decoder},
66+
decoder::{decode, decode_label, detect_base64_line_width, Decoder},
6767
encoder::{encapsulated_len, encapsulated_len_wrapped, encode, encoded_len, Encoder},
6868
error::{Error, Result},
6969
};

pem-rfc7468/tests/decode.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,18 @@ fn ed25519_example() {
110110
let label = pem_rfc7468::decode_label(pem).unwrap();
111111
assert_eq!(label, "ED25519 CERT");
112112
}
113+
114+
#[test]
115+
fn line_width_detection() {
116+
let pem_64cols = include_bytes!("examples/pkcs1.pem");
117+
assert_eq!(
118+
pem_rfc7468::detect_base64_line_width(pem_64cols).unwrap(),
119+
64
120+
);
121+
122+
let pem_70cols = include_bytes!("examples/ssh-id_ed25519.pem");
123+
assert_eq!(
124+
pem_rfc7468::detect_base64_line_width(pem_70cols).unwrap(),
125+
70
126+
);
127+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-----BEGIN OPENSSH PRIVATE KEY-----
2+
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
3+
QyNTUxOQAAACCzPq7zfqLffKoBDe/eo04kH2XxtSmk9D7RQyf1xUqrYgAAAJgAIAxdACAM
4+
XQAAAAtzc2gtZWQyNTUxOQAAACCzPq7zfqLffKoBDe/eo04kH2XxtSmk9D7RQyf1xUqrYg
5+
AAAEC2BsIi0QwW2uFscKTUUXNHLsYX4FxlaSDSblbAj7WR7bM+rvN+ot98qgEN796jTiQf
6+
ZfG1KaT0PtFDJ/XFSqtiAAAAEHVzZXJAZXhhbXBsZS5jb20BAgMEBQ==
7+
-----END OPENSSH PRIVATE KEY-----

0 commit comments

Comments
 (0)