Skip to main content

rusthound_ce/utils/
b64.rs

1//! Base64 encode/decode helpers, standard RFC 4648 alphabet, no external dependency.
2//!
3//! Used by the ESC8 scanner to encode the NTLM Negotiate token and decode
4//! the server's NTLM Challenge from HTTP response headers.
5
6/// Encode `input` to standard Base64 (RFC 4648, with `=` padding).
7pub fn b64_encode(input: &[u8]) -> String {
8    const TABLE: &[u8] =
9        b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
10
11    let mut out = String::with_capacity((input.len() + 2) / 3 * 4);
12    for chunk in input.chunks(3) {
13        let b0 = chunk[0] as usize;
14        let b1 = if chunk.len() > 1 { chunk[1] as usize } else { 0 };
15        let b2 = if chunk.len() > 2 { chunk[2] as usize } else { 0 };
16
17        out.push(TABLE[(b0 >> 2) & 0x3f] as char);
18        out.push(TABLE[((b0 << 4) | (b1 >> 4)) & 0x3f] as char);
19        out.push(if chunk.len() > 1 {
20            TABLE[((b1 << 2) | (b2 >> 6)) & 0x3f] as char
21        } else {
22            '='
23        });
24        out.push(if chunk.len() > 2 {
25            TABLE[b2 & 0x3f] as char
26        } else {
27            '='
28        });
29    }
30    out
31}
32
33/// Decode standard Base64 (RFC 4648). Returns `None` on invalid input.
34pub fn b64_decode(input: &str) -> Option<Vec<u8>> {
35    fn val(c: u8) -> Option<u8> {
36        match c {
37            b'A'..=b'Z' => Some(c - b'A'),
38            b'a'..=b'z' => Some(c - b'a' + 26),
39            b'0'..=b'9' => Some(c - b'0' + 52),
40            b'+' => Some(62),
41            b'/' => Some(63),
42            _ => None,
43        }
44    }
45
46    // Strip padding and whitespace before processing
47    let bytes: Vec<u8> = input
48        .bytes()
49        .filter(|&b| b != b'=' && b != b'\r' && b != b'\n' && b != b' ')
50        .collect();
51
52    if bytes.len() % 4 == 1 {
53        return None; // impossible valid length
54    }
55
56    let mut out = Vec::with_capacity(bytes.len() * 3 / 4);
57    let mut i = 0;
58    while i + 1 < bytes.len() {
59        let v0 = val(bytes[i])?;
60        let v1 = val(bytes[i + 1])?;
61        out.push((v0 << 2) | (v1 >> 4));
62        if i + 2 < bytes.len() {
63            let v2 = val(bytes[i + 2])?;
64            out.push((v1 << 4) | (v2 >> 2));
65            if i + 3 < bytes.len() {
66                let v3 = val(bytes[i + 3])?;
67                out.push((v2 << 6) | v3);
68            }
69        }
70        i += 4;
71    }
72    Some(out)
73}
74
75// Tests 
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn rfc4648_vectors() {
83        // Official RFC 4648 ยง10 test vectors
84        assert_eq!(b64_encode(b""),       "");
85        assert_eq!(b64_encode(b"f"),      "Zg==");
86        assert_eq!(b64_encode(b"fo"),     "Zm8=");
87        assert_eq!(b64_encode(b"foo"),    "Zm9v");
88        assert_eq!(b64_encode(b"foob"),   "Zm9vYg==");
89        assert_eq!(b64_encode(b"fooba"),  "Zm9vYmE=");
90        assert_eq!(b64_encode(b"foobar"), "Zm9vYmFy");
91    }
92
93    #[test]
94    fn decode_rfc4648_vectors() {
95        assert_eq!(b64_decode(""),         Some(b"".to_vec()));
96        assert_eq!(b64_decode("Zg=="),     Some(b"f".to_vec()));
97        assert_eq!(b64_decode("Zm8="),     Some(b"fo".to_vec()));
98        assert_eq!(b64_decode("Zm9v"),     Some(b"foo".to_vec()));
99        assert_eq!(b64_decode("Zm9vYg=="), Some(b"foob".to_vec()));
100        assert_eq!(b64_decode("Zm9vYmE="), Some(b"fooba".to_vec()));
101        assert_eq!(b64_decode("Zm9vYmFy"), Some(b"foobar".to_vec()));
102    }
103
104    #[test]
105    fn roundtrip_arbitrary_bytes() {
106        let data: Vec<u8> = (0u8..=255).collect();
107        let encoded = b64_encode(&data);
108        let decoded = b64_decode(&encoded).expect("roundtrip decode failed");
109        assert_eq!(data, decoded);
110    }
111
112    #[test]
113    fn decode_without_padding() {
114        // Padding is optional on input
115        assert_eq!(b64_decode("Zg"),   Some(b"f".to_vec()));
116        assert_eq!(b64_decode("Zm8"),  Some(b"fo".to_vec()));
117    }
118
119    #[test]
120    fn decode_invalid_char_returns_none() {
121        assert_eq!(b64_decode("TQ!Q"), None);
122        assert_eq!(b64_decode("TQ@Q"), None);
123    }
124
125    #[test]
126    fn decode_impossible_length_returns_none() {
127        // Length 1 mod 4 is never valid Base64
128        assert_eq!(b64_decode("A"), None);
129    }
130
131    #[test]
132    fn decode_ignores_whitespace_and_padding() {
133        // Common in PEM / HTTP headers
134        assert_eq!(b64_decode("Zm9v\r\nYmFy"), Some(b"foobar".to_vec()));
135        assert_eq!(b64_decode("Zm9vYmFy=="), Some(b"foobar".to_vec()));
136    }
137}