Skip to main content

rustlavel_core/
base64.rs

1//! Base64, in both alphabets this crate needs.
2//!
3//! `APP_KEY` is written the way an operator expects to see it — standard
4//! alphabet, padded, exactly what `openssl rand -base64 32` prints. Encrypted
5//! payloads and signatures travel in cookies and query strings, so they use the
6//! URL-safe alphabet with no padding and need no percent-encoding at all.
7//!
8//! Fifty lines is cheaper than a dependency, and getting base64 wrong is
9//! visible immediately, unlike getting a cipher wrong.
10
11const STANDARD: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
12const URL_SAFE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
13
14/// Standard alphabet, padded with `=`.
15pub fn encode(input: &[u8]) -> String {
16    encode_with(input, STANDARD, true)
17}
18
19/// URL-safe alphabet, unpadded — safe in a cookie value or a query string.
20pub fn encode_url(input: &[u8]) -> String {
21    encode_with(input, URL_SAFE, false)
22}
23
24fn encode_with(input: &[u8], alphabet: &[u8; 64], pad: bool) -> String {
25    let mut out = String::with_capacity(input.len().div_ceil(3) * 4);
26
27    for chunk in input.chunks(3) {
28        let b = [chunk[0], *chunk.get(1).unwrap_or(&0), *chunk.get(2).unwrap_or(&0)];
29        let bits = (u32::from(b[0]) << 16) | (u32::from(b[1]) << 8) | u32::from(b[2]);
30
31        out.push(alphabet[(bits >> 18 & 0x3f) as usize] as char);
32        out.push(alphabet[(bits >> 12 & 0x3f) as usize] as char);
33
34        match (chunk.len() > 1, pad) {
35            (true, _) => out.push(alphabet[(bits >> 6 & 0x3f) as usize] as char),
36            (false, true) => out.push('='),
37            (false, false) => {}
38        }
39        match (chunk.len() > 2, pad) {
40            (true, _) => out.push(alphabet[(bits & 0x3f) as usize] as char),
41            (false, true) => out.push('='),
42            (false, false) => {}
43        }
44    }
45
46    out
47}
48
49/// Decode either alphabet, with or without padding.
50///
51/// Deliberately permissive about which alphabet it is handed: a key pasted
52/// from a password manager and a signature copied out of a URL should both
53/// just work. It is not permissive about *characters* — anything outside both
54/// alphabets is rejected, so a truncated or mangled payload fails here rather
55/// than silently decoding to different bytes.
56pub fn decode(input: &str) -> Option<Vec<u8>> {
57    let mut out = Vec::with_capacity(input.len() / 4 * 3);
58    let mut buffer = 0u32;
59    let mut bits = 0u32;
60
61    for byte in input.bytes() {
62        let value = match byte {
63            b'A'..=b'Z' => byte - b'A',
64            b'a'..=b'z' => byte - b'a' + 26,
65            b'0'..=b'9' => byte - b'0' + 52,
66            b'+' | b'-' => 62,
67            b'/' | b'_' => 63,
68            b'=' | b'\n' | b'\r' => continue,
69            _ => return None,
70        };
71
72        buffer = (buffer << 6) | u32::from(value);
73        bits += 6;
74        if bits >= 8 {
75            bits -= 8;
76            out.push((buffer >> bits) as u8);
77        }
78    }
79
80    Some(out)
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn standard_encoding_matches_the_rfc_4648_vectors() {
89        for (plain, encoded) in [
90            ("", ""),
91            ("f", "Zg=="),
92            ("fo", "Zm8="),
93            ("foo", "Zm9v"),
94            ("foob", "Zm9vYg=="),
95            ("fooba", "Zm9vYmE="),
96            ("foobar", "Zm9vYmFy"),
97        ] {
98            assert_eq!(encode(plain.as_bytes()), encoded, "encoding {plain:?}");
99            assert_eq!(decode(encoded).unwrap(), plain.as_bytes(), "decoding {encoded:?}");
100        }
101    }
102
103    #[test]
104    fn url_safe_encoding_avoids_the_characters_a_url_would_escape() {
105        // 0xfb 0xff encodes to `+/` in the standard alphabet.
106        let bytes = [0xfb, 0xff, 0xbf];
107        assert_eq!(encode(&bytes), "+/+/");
108        assert_eq!(encode_url(&bytes), "-_-_");
109
110        let unpadded = encode_url(b"f");
111        assert_eq!(unpadded, "Zg");
112        assert_eq!(decode(&unpadded).unwrap(), b"f");
113    }
114
115    #[test]
116    fn both_alphabets_round_trip_arbitrary_bytes() {
117        let bytes: Vec<u8> = (0..=255).collect();
118
119        assert_eq!(decode(&encode(&bytes)).unwrap(), bytes);
120        assert_eq!(decode(&encode_url(&bytes)).unwrap(), bytes);
121    }
122
123    #[test]
124    fn rejects_characters_outside_the_alphabets() {
125        assert!(decode("not base64!").is_none());
126        assert!(decode("abc$def").is_none());
127    }
128}