1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
//! Standard Base64 (RFC 4648) decode for wire formats and structured data.
//!
//! Scan-time variant base64 (URL-safe, unpadded) lives in `keyhog-scanner`.
/// Maximum input length for [`decode_standard_base64`]. Matches the scanner's
/// byte limit so credential serde and K8s secret parsing stay consistent.
pub(crate) const MAX_STANDARD_BASE64_INPUT_BYTES: usize = 16 * 1024 * 1024;
/// Encode bytes with the standard RFC 4648 alphabet and canonical `=` padding.
pub(crate) fn encode_standard_base64(input: &[u8]) -> String {
const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(input.len().div_ceil(3) * 4);
for chunk in input.chunks(3) {
let b0 = chunk[0];
let b1 = match chunk.get(1) {
Some(byte) => *byte,
None => 0,
};
let b2 = match chunk.get(2) {
Some(byte) => *byte,
None => 0,
};
out.push(TABLE[(b0 >> 2) as usize] as char);
out.push(TABLE[(((b0 & 0x03) << 4) | (b1 >> 4)) as usize] as char);
if chunk.len() > 1 {
out.push(TABLE[(((b1 & 0x0F) << 2) | (b2 >> 6)) as usize] as char);
} else {
out.push('=');
}
if chunk.len() > 2 {
out.push(TABLE[(b2 & 0x3F) as usize] as char);
} else {
out.push('=');
}
}
out
}
/// Decode standard-alphabet base64 (with optional `=` padding).
pub fn decode_standard_base64(input: &str) -> Result<Vec<u8>, String> {
if input.len() > MAX_STANDARD_BASE64_INPUT_BYTES {
return Err(format!(
"base64 input exceeds {} bytes",
MAX_STANDARD_BASE64_INPUT_BYTES
));
}
fn val(c: u8) -> Result<u8, String> {
match c {
b'A'..=b'Z' => Ok(c - b'A'),
b'a'..=b'z' => Ok(c - b'a' + 26),
b'0'..=b'9' => Ok(c - b'0' + 52),
b'+' => Ok(62),
b'/' => Ok(63),
_ => Err(format!("invalid base64 char: {c:#x}")),
}
}
let bytes = input.as_bytes();
// `=` is only legal as TRAILING padding in standard base64. The previous
// `take_while(|c| c != b'=')` silently TRUNCATED at the first `=`, so
// `"AB=CD"` decoded as `"AB"` and dropped `"CD"` with no error, a
// silent-accept that corrupts a credential round-trip. Split at the first
// `=`: everything before it is data, and everything FROM it onward must be
// padding-only (`=`). A non-`=` byte after an `=` is malformed; reject it
// loudly instead of swallowing the tail.
let first_pad = bytes.iter().position(|&c| c == b'=');
let stripped: &[u8] = match first_pad {
Some(idx) => {
if idx == 0 {
// Pad-only input (`"="`, `"=="`) carries zero data bytes: it is
// malformed base64, not an empty-string encoding. Reject loudly
// instead of returning Ok(vec![]) (silent-accept, Law 10).
return Err("invalid base64: padding '=' with no preceding data".to_string());
}
if bytes[idx..].iter().any(|&c| c != b'=') {
return Err(
"invalid base64: data after padding '=' (padding may only appear at the end)"
.to_string(),
);
}
// At most 2 padding chars are well-formed; more than 2, or padding
// that does not align the data to a 4-char-quad boundary, is
// malformed. `idx % 4` is the data length within the final quad:
// 0 => no quad in progress (only valid with zero padding),
// 1 => impossible to encode (1 base64 char carries <6 bits of a byte),
// 2 => one data byte, needs `==`,
// 3 => two data bytes, needs `=`.
let pad_len = bytes.len() - idx;
let rem = idx % 4;
let pad_ok = match rem {
2 => pad_len == 2 || pad_len == 1, // tolerate `QQ=`/`QQ==`
3 => pad_len == 1,
0 => pad_len <= 2, // trailing `=`/`==` after a whole quad ("QUJD==")
_ => false, // rem == 1: no valid encoding produces a lone char
};
if !pad_ok {
return Err(format!(
"invalid base64: {pad_len} padding char(s) do not align the {idx} data char(s) to a quad"
));
}
&bytes[..idx]
}
None => bytes,
};
let mut out = Vec::with_capacity(stripped.len() * 3 / 4);
for chunk in stripped.chunks(4) {
let v0 = val(chunk[0])?;
let v1 = val(*chunk.get(1).ok_or_else(|| "truncated base64".to_string())?)?;
out.push((v0 << 2) | (v1 >> 4));
if let Some(&c2) = chunk.get(2) {
let v2 = val(c2)?;
out.push(((v1 & 0x0F) << 4) | (v2 >> 2));
if let Some(&c3) = chunk.get(3) {
let v3 = val(c3)?;
out.push(((v2 & 0x03) << 6) | v3);
}
}
}
Ok(out)
}