use base64::Engine;
use base64::engine::general_purpose::STANDARD;
const PEM_HEADER: &[u8] = b"-----BEGIN ";
const PEM_FOOTER: &[u8] = b"-----END ";
const PEM_ENDING: &[u8] = b"-----";
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum Error {
#[error("missing PEM header")]
MissingHeader,
#[error("malformed PEM header: {0}")]
MalformedHeader(String),
#[error("empty PEM block type")]
EmptyBlockType,
#[error("malformed PEM block type")]
MalformedBlockType,
#[error("missing PEM footer")]
MissingFooter,
#[error("trailing data after PEM block")]
TrailingData,
#[error("malformed PEM body: {0}")]
MalformedBody(String),
#[error("malformed base64 payload: {0}")]
MalformedPayload(String),
}
pub fn decode(data: &[u8]) -> Result<(String, Vec<u8>), Error> {
if !data.starts_with(PEM_HEADER) {
return Err(Error::MissingHeader);
}
let header_end = data
.iter()
.position(|&b| b == b'\n')
.ok_or_else(|| Error::MalformedHeader("incomplete header line".into()))?;
let line_ending: &[u8] = if header_end > 0 && data[header_end - 1] == b'\r' {
b"\r\n"
} else {
b"\n"
};
let header = if line_ending.len() == 2 {
&data[..header_end - 1]
} else {
&data[..header_end]
};
if !header.starts_with(PEM_HEADER) || !header.ends_with(PEM_ENDING) {
return Err(Error::MalformedHeader("unterminated block type".into()));
}
let block_type = &header[PEM_HEADER.len()..header.len() - PEM_ENDING.len()];
if block_type.is_empty() {
return Err(Error::EmptyBlockType);
}
let kind = String::from_utf8(block_type.to_vec()).map_err(|_| Error::MalformedBlockType)?;
let mut footer = Vec::with_capacity(PEM_FOOTER.len() + block_type.len() + PEM_ENDING.len());
footer.extend_from_slice(PEM_FOOTER);
footer.extend_from_slice(block_type);
footer.extend_from_slice(PEM_ENDING);
let search_area = &data[header_end + 1..];
let footer_idx = search_area
.windows(footer.len())
.position(|w| w == footer.as_slice())
.ok_or(Error::MissingFooter)?;
let footer_start = header_end + 1 + footer_idx;
let footer_end = footer_start + footer.len();
let rest = &data[footer_end..];
if !rest.is_empty() && rest != line_ending {
return Err(Error::TrailingData);
}
let body = &data[header_end + 1..footer_start];
if body.is_empty() {
return Err(Error::MalformedBody("empty body".into()));
}
if !body.ends_with(line_ending) {
return Err(Error::MalformedBody("missing newline before footer".into()));
}
let body = &body[..body.len() - line_ending.len()];
let b64: Vec<u8> = body
.split(|&b| b == b'\n')
.flat_map(|line| {
if line.ends_with(b"\r") {
&line[..line.len() - 1]
} else {
line
}
})
.copied()
.collect();
let decoded = STANDARD
.decode(&b64)
.map_err(|err| Error::MalformedPayload(err.to_string()))?;
Ok((kind, decoded))
}
pub fn encode(kind: &str, data: &[u8]) -> String {
let b64 = STANDARD.encode(data);
let mut buf = String::new();
buf.push_str("-----BEGIN ");
buf.push_str(kind);
buf.push_str("-----\n");
for chunk in b64.as_bytes().chunks(64) {
buf.push_str(std::str::from_utf8(chunk).unwrap());
buf.push('\n');
}
buf.push_str("-----END ");
buf.push_str(kind);
buf.push_str("-----\n");
buf
}