Skip to main content

arcana/encoding/
pem.rs

1//! PEM armor: base64 encoding with `-----BEGIN/END ...-----` headers.
2
3// ====================================================================
4// Base64 (RFC 4648)
5// ====================================================================
6
7const B64_CHARS: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
8
9/// Encode `data` as base64.
10pub fn base64_encode(data: &[u8]) -> String {
11    let mut out = String::with_capacity((data.len() + 2) / 3 * 4);
12    for chunk in data.chunks(3) {
13        let b0 = chunk[0] as u32;
14        let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
15        let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
16        let triple = (b0 << 16) | (b1 << 8) | b2;
17
18        out.push(B64_CHARS[((triple >> 18) & 0x3F) as usize] as char);
19        out.push(B64_CHARS[((triple >> 12) & 0x3F) as usize] as char);
20        if chunk.len() > 1 {
21            out.push(B64_CHARS[((triple >> 6) & 0x3F) as usize] as char);
22        } else {
23            out.push('=');
24        }
25        if chunk.len() > 2 {
26            out.push(B64_CHARS[(triple & 0x3F) as usize] as char);
27        } else {
28            out.push('=');
29        }
30    }
31    out
32}
33
34/// Decode a base64 string. Returns `None` on invalid input.
35pub fn base64_decode(s: &str) -> Option<Vec<u8>> {
36    let mut out = Vec::with_capacity(s.len() * 3 / 4);
37    let mut buf = 0u32;
38    let mut count = 0u32;
39
40    for c in s.chars() {
41        if c.is_whitespace() {
42            continue;
43        }
44        if c == '=' {
45            break;
46        }
47        let val = b64_char_value(c)? as u32;
48        buf = (buf << 6) | val;
49        count += 1;
50        if count == 4 {
51            out.push((buf >> 16) as u8);
52            out.push((buf >> 8) as u8);
53            out.push(buf as u8);
54            buf = 0;
55            count = 0;
56        }
57    }
58    match count {
59        2 => {
60            buf <<= 12;
61            out.push((buf >> 16) as u8);
62        }
63        3 => {
64            buf <<= 6;
65            out.push((buf >> 16) as u8);
66            out.push((buf >> 8) as u8);
67        }
68        0 => {}
69        _ => return None, // 1 leftover char is invalid
70    }
71    Some(out)
72}
73
74fn b64_char_value(c: char) -> Option<u8> {
75    match c {
76        'A'..='Z' => Some(c as u8 - b'A'),
77        'a'..='z' => Some(c as u8 - b'a' + 26),
78        '0'..='9' => Some(c as u8 - b'0' + 52),
79        '+' => Some(62),
80        '/' => Some(63),
81        _ => None,
82    }
83}
84
85// ====================================================================
86// PEM
87// ====================================================================
88
89/// Encode DER bytes as PEM with the given label (e.g. "RSA PUBLIC KEY").
90pub fn pem_encode(label: &str, der: &[u8]) -> String {
91    let b64 = base64_encode(der);
92    let mut out = String::new();
93    out.push_str("-----BEGIN ");
94    out.push_str(label);
95    out.push_str("-----\n");
96    // Wrap at 64 characters per line (PEM standard).
97    for line in b64.as_bytes().chunks(64) {
98        out.push_str(std::str::from_utf8(line).unwrap());
99        out.push('\n');
100    }
101    out.push_str("-----END ");
102    out.push_str(label);
103    out.push_str("-----\n");
104    out
105}
106
107/// Decode a PEM block with the expected label. Returns the DER bytes.
108/// Returns `None` if the label doesn't match or base64 is invalid.
109pub fn pem_decode(label: &str, pem: &str) -> Option<Vec<u8>> {
110    let begin = format!("-----BEGIN {}-----", label);
111    let end = format!("-----END {}-----", label);
112
113    let start = pem.find(&begin)? + begin.len();
114    let stop = pem.find(&end)?;
115    let b64_block = &pem[start..stop];
116    base64_decode(b64_block)
117}
118
119// ====================================================================
120// Tests
121// ====================================================================
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn base64_roundtrip() {
129        let cases: &[(&[u8], &str)] = &[
130            (b"", ""),
131            (b"f", "Zg=="),
132            (b"fo", "Zm8="),
133            (b"foo", "Zm9v"),
134            (b"foob", "Zm9vYg=="),
135            (b"fooba", "Zm9vYmE="),
136            (b"foobar", "Zm9vYmFy"),
137        ];
138        for (data, expected) in cases {
139            let encoded = base64_encode(data);
140            assert_eq!(encoded, *expected, "encode {:?}", data);
141            let decoded = base64_decode(&encoded).unwrap();
142            assert_eq!(decoded, *data, "decode {:?}", expected);
143        }
144    }
145
146    #[test]
147    fn base64_binary() {
148        let data: Vec<u8> = (0..256).map(|i| i as u8).collect();
149        let enc = base64_encode(&data);
150        let dec = base64_decode(&enc).unwrap();
151        assert_eq!(dec, data);
152    }
153
154    #[test]
155    fn pem_roundtrip() {
156        let der = vec![0x30, 0x03, 0x02, 0x01, 0x42]; // tiny DER
157        let pem = pem_encode("TEST DATA", &der);
158        assert!(pem.contains("-----BEGIN TEST DATA-----"));
159        assert!(pem.contains("-----END TEST DATA-----"));
160        let decoded = pem_decode("TEST DATA", &pem).unwrap();
161        assert_eq!(decoded, der);
162    }
163
164    #[test]
165    fn pem_wrong_label_returns_none() {
166        let pem = pem_encode("FOO", &[0x01]);
167        assert!(pem_decode("BAR", &pem).is_none());
168    }
169}