Skip to main content

keyhog_core/
encoding.rs

1//! Standard Base64 (RFC 4648) decode for wire formats and structured data.
2//!
3//! Scan-time variant base64 (URL-safe, unpadded) lives in `keyhog-scanner`.
4
5/// Maximum input length for [`decode_standard_base64`]. Matches the scanner
6/// pipeline cap so credential serde and K8s secret parsing stay consistent.
7pub const MAX_STANDARD_BASE64_INPUT_BYTES: usize = 16 * 1024 * 1024;
8
9/// Decode standard-alphabet base64 (with optional `=` padding).
10pub fn decode_standard_base64(input: &str) -> Result<Vec<u8>, String> {
11    if input.len() > MAX_STANDARD_BASE64_INPUT_BYTES {
12        return Err(format!(
13            "base64 input exceeds {} bytes",
14            MAX_STANDARD_BASE64_INPUT_BYTES
15        ));
16    }
17    fn val(c: u8) -> Result<u8, String> {
18        match c {
19            b'A'..=b'Z' => Ok(c - b'A'),
20            b'a'..=b'z' => Ok(c - b'a' + 26),
21            b'0'..=b'9' => Ok(c - b'0' + 52),
22            b'+' => Ok(62),
23            b'/' => Ok(63),
24            _ => Err(format!("invalid base64 char: {c:#x}")),
25        }
26    }
27    let bytes = input.as_bytes();
28    let stripped: Vec<u8> = bytes.iter().copied().take_while(|&c| c != b'=').collect();
29    let mut out = Vec::with_capacity(stripped.len() * 3 / 4);
30    for chunk in stripped.chunks(4) {
31        let v0 = val(chunk[0])?;
32        let v1 = val(*chunk.get(1).ok_or_else(|| "truncated base64".to_string())?)?;
33        out.push((v0 << 2) | (v1 >> 4));
34        if let Some(&c2) = chunk.get(2) {
35            let v2 = val(c2)?;
36            out.push(((v1 & 0x0F) << 4) | (v2 >> 2));
37            if let Some(&c3) = chunk.get(3) {
38                let v3 = val(c3)?;
39                out.push(((v2 & 0x03) << 6) | v3);
40            }
41        }
42    }
43    Ok(out)
44}