1const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
4
5pub fn base64_encode(data: &[u8]) -> String {
6 let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
7 for chunk in data.chunks(3) {
8 let b = [chunk[0], *chunk.get(1).unwrap_or(&0), *chunk.get(2).unwrap_or(&0)];
9 let n = (b[0] as u32) << 16 | (b[1] as u32) << 8 | b[2] as u32;
10 out.push(B64[(n >> 18) as usize & 63] as char);
11 out.push(B64[(n >> 12) as usize & 63] as char);
12 out.push(if chunk.len() > 1 { B64[(n >> 6) as usize & 63] as char } else { '=' });
13 out.push(if chunk.len() > 2 { B64[n as usize & 63] as char } else { '=' });
14 }
15 out
16}
17
18pub fn base64_decode(s: &str) -> Option<Vec<u8>> {
20 let mut out = Vec::with_capacity(s.len() * 3 / 4);
21 let mut acc = 0u32;
22 let mut bits = 0;
23 let mut pad = 0;
24 for c in s.bytes() {
25 let v = match c {
26 b'A'..=b'Z' => c - b'A',
27 b'a'..=b'z' => c - b'a' + 26,
28 b'0'..=b'9' => c - b'0' + 52,
29 b'+' => 62,
30 b'/' => 63,
31 b'=' => {
32 pad += 1;
33 continue;
34 }
35 b' ' | b'\t' | b'\r' | b'\n' => continue,
36 _ => return None,
37 };
38 if pad > 0 {
39 return None;
40 }
41 acc = (acc << 6) | v as u32;
42 bits += 6;
43 if bits >= 8 {
44 bits -= 8;
45 out.push((acc >> bits) as u8);
46 }
47 }
48 if pad > 2 {
49 return None;
50 }
51 Some(out)
52}
53
54#[derive(Clone, Debug, PartialEq, Eq)]
56pub struct PemBlock {
57 pub label: String,
58 pub der: Vec<u8>,
59}
60
61pub fn parse_pem(input: &[u8]) -> Result<Vec<PemBlock>, String> {
64 let text = String::from_utf8_lossy(input);
65 let mut blocks = Vec::new();
66 let mut current: Option<(String, String)> = None;
67 for line in text.lines() {
68 let line = line.trim();
69 if let Some(rest) = line.strip_prefix("-----BEGIN ") {
70 if let Some(label) = rest.strip_suffix("-----") {
71 current = Some((label.to_string(), String::new()));
72 continue;
73 }
74 }
75 if let Some(rest) = line.strip_prefix("-----END ") {
76 if let Some(label) = rest.strip_suffix("-----") {
77 match current.take() {
78 Some((l, body)) if l == label => {
79 let der = base64_decode(&body).ok_or_else(|| format!("invalid base64 in PEM block {l}"))?;
80 blocks.push(PemBlock { label: l, der });
81 }
82 _ => return Err(format!("mismatched PEM END {label}")),
83 }
84 continue;
85 }
86 }
87 if let Some((label, body)) = current.as_mut() {
88 if line.contains(':') {
89 return Err(format!("PEM block {label} has headers (encrypted keys are not supported)"));
90 }
91 body.push_str(line);
92 }
93 }
94 if let Some((l, _)) = current {
95 return Err(format!("unterminated PEM block {l}"));
96 }
97 Ok(blocks)
98}
99
100pub fn encode_pem(label: &str, der: &[u8]) -> String {
101 let b = base64_encode(der);
102 let mut out = format!("-----BEGIN {label}-----\n");
103 for chunk in b.as_bytes().chunks(64) {
104 out.push_str(std::str::from_utf8(chunk).unwrap());
105 out.push('\n');
106 }
107 out.push_str(&format!("-----END {label}-----\n"));
108 out
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn base64_rfc4648() {
117 let cases = [("", ""), ("f", "Zg=="), ("fo", "Zm8="), ("foo", "Zm9v"), ("foob", "Zm9vYg=="), ("fooba", "Zm9vYmE="), ("foobar", "Zm9vYmFy")];
118 for (plain, enc) in cases {
119 assert_eq!(base64_encode(plain.as_bytes()), enc);
120 assert_eq!(base64_decode(enc).unwrap(), plain.as_bytes());
121 }
122 assert!(base64_decode("Zm9v!").is_none());
123 }
124
125 #[test]
126 fn pem_roundtrip() {
127 let data: Vec<u8> = (0..200u8).collect();
128 let p = encode_pem("CERTIFICATE", &data) + "junk\n" + &encode_pem("PRIVATE KEY", &[1, 2, 3]);
129 let blocks = parse_pem(p.as_bytes()).unwrap();
130 assert_eq!(blocks.len(), 2);
131 assert_eq!(blocks[0].der, data);
132 assert_eq!(blocks[1].label, "PRIVATE KEY");
133 }
134}