use crate::{Error, Result, SecretBytes};
const ALPHABET: &[u8; 32] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
const BITS_PER_CHAR: usize = 5;
const fn decode_char(byte: u8) -> Option<u8> {
match byte {
b'A'..=b'Z' => Some(byte - b'A'),
b'a'..=b'z' => Some(byte - b'a'),
b'2'..=b'7' => Some(byte - b'2' + 26),
_ => None,
}
}
const fn output_len_for(count: usize) -> Option<usize> {
match count % 8 {
0 => Some(count / 8 * 5),
2 => Some(count / 8 * 5 + 1),
4 => Some(count / 8 * 5 + 2),
5 => Some(count / 8 * 5 + 3),
7 => Some(count / 8 * 5 + 4),
_ => None,
}
}
pub fn decode(input: &str) -> Result<SecretBytes> {
decode_inner(input, false)
}
pub fn decode_strict(input: &str) -> Result<SecretBytes> {
decode_inner(input, true)
}
fn decode_inner(input: &str, require_canonical: bool) -> Result<SecretBytes> {
let mut valid = 0_usize;
let mut seen_padding = false;
for (index, &byte) in input.as_bytes().iter().enumerate() {
if byte.is_ascii_whitespace() {
continue;
}
if byte == b'=' {
seen_padding = true;
continue;
}
if seen_padding {
return Err(Error::Base32InvalidCharacter { index });
}
if decode_char(byte).is_none() {
return Err(Error::Base32InvalidCharacter { index });
}
valid += 1;
}
let Some(byte_len) = output_len_for(valid) else {
return Err(Error::Base32InvalidLength { length: valid });
};
if byte_len == 0 {
return Err(Error::SecretTooShort {
required: 1,
actual: 0,
});
}
let mut out = Vec::with_capacity(byte_len);
let mut buffer: u64 = 0;
let mut buffered_bits = 0_u32;
for &byte in input.as_bytes() {
if byte.is_ascii_whitespace() || byte == b'=' {
continue;
}
let Some(value) = decode_char(byte) else {
return Err(Error::Base32InvalidCharacter { index: 0 });
};
buffer = (buffer << BITS_PER_CHAR) | u64::from(value);
buffered_bits += BITS_PER_CHAR as u32;
if buffered_bits >= 8 {
buffered_bits -= 8;
out.push((buffer >> buffered_bits) as u8);
}
}
if require_canonical && buffered_bits > 0 {
let residual_mask = (1_u64 << buffered_bits) - 1;
if buffer & residual_mask != 0 {
return Err(Error::Base32InvalidPadding);
}
}
debug_assert_eq!(out.len(), byte_len, "容量计算与实际写入不一致");
Ok(out.into())
}
#[must_use]
pub fn encode(data: &[u8]) -> String {
let mut out = String::with_capacity(data.len().div_ceil(5) * 8);
let mut buffer: u64 = 0;
let mut buffered_bits = 0_u32;
for &byte in data {
buffer = (buffer << 8) | u64::from(byte);
buffered_bits += 8;
while buffered_bits >= BITS_PER_CHAR as u32 {
buffered_bits -= BITS_PER_CHAR as u32;
let index = ((buffer >> buffered_bits) & 0x1f) as usize;
out.push(char::from(ALPHABET[index]));
}
}
if buffered_bits > 0 {
let index = ((buffer << (BITS_PER_CHAR as u32 - buffered_bits)) & 0x1f) as usize;
out.push(char::from(ALPHABET[index]));
}
while !out.len().is_multiple_of(8) {
out.push('=');
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ExposeSecret;
#[test]
fn rfc4648_test_vectors() {
let vectors: &[(&str, &[u8])] = &[
("", b""),
("MY======", b"f"),
("MZXQ====", b"fo"),
("MZXW6===", b"foo"),
("MZXW6YQ=", b"foob"),
("MZXW6YTB", b"fooba"),
("MZXW6YTBOI======", b"foobar"),
];
for (encoded, expected) in vectors {
if expected.is_empty() {
assert!(
matches!(decode(encoded), Err(Error::SecretTooShort { .. })),
"空输入应被拒绝"
);
continue;
}
let decoded = decode(encoded).unwrap_or_else(|e| panic!("{encoded:?} 解码失败: {e}"));
assert_eq!(
decoded.expose_secret(),
*expected,
"{encoded:?} 解码结果不符"
);
}
}
#[test]
fn encode_matches_rfc4648_vectors() {
assert_eq!(encode(b"f"), "MY======");
assert_eq!(encode(b"fo"), "MZXQ====");
assert_eq!(encode(b"foo"), "MZXW6===");
assert_eq!(encode(b"foob"), "MZXW6YQ=");
assert_eq!(encode(b"fooba"), "MZXW6YTB");
assert_eq!(encode(b"foobar"), "MZXW6YTBOI======");
}
#[test]
fn accepts_lowercase() {
let upper = decode("MZXW6YTBOI======").expect("大写应可解码");
let lower = decode("mzxw6ytboi======").expect("小写应可解码");
assert_eq!(upper.expose_secret(), lower.expose_secret());
}
#[test]
fn tolerates_missing_padding() {
let padded = decode("MZXW6YTBOI======").expect("带填充应可解码");
let unpadded = decode("MZXW6YTBOI").expect("无填充应可解码");
assert_eq!(padded.expose_secret(), unpadded.expose_secret());
}
#[test]
fn tolerates_ascii_whitespace() {
let expected = decode("MZXW6YTBOI").expect("基准应可解码");
for variant in [
" MZXW6YTBOI ",
"MZXW 6YTBOI",
"MZXW6YTB\nOI",
"MZ\tXW6YTBOI",
"MZ XW 6Y TB OI ",
] {
let got = decode(variant).unwrap_or_else(|e| panic!("{variant:?} 应可解码: {e}"));
assert_eq!(got.expose_secret(), expected.expose_secret(), "{variant:?}");
}
}
#[test]
fn rejects_characters_outside_alphabet() {
for bad in [
"MZXW6YTBO0======",
"MZXW6YTBO1======",
"MZXW6YTBO8======",
"MZXW6YTBO9======",
"MZXW6YTB-=",
"MZXW6YTB!",
"MZXW6YTBO\u{4e2d}",
] {
let result = decode(bad);
assert!(
matches!(result, Err(Error::Base32InvalidCharacter { .. })),
"{bad:?} 应因非法字符被拒,实际: {result:?}"
);
}
}
#[test]
fn rejects_invalid_lengths() {
for bad in ["M", "MZX", "MZXW6Y"] {
let result = decode(bad);
assert!(
matches!(result, Err(Error::Base32InvalidLength { .. })),
"{bad:?} 应因长度非法被拒,实际: {result:?}"
);
}
}
#[test]
fn rejects_data_after_padding() {
let result = decode("MZXW6YTB=OI");
assert!(
matches!(result, Err(Error::Base32InvalidCharacter { .. })),
"填充后出现数据应被拒,实际: {result:?}"
);
}
#[test]
fn padding_bits_are_accepted_leniently_but_rejected_strictly() {
let lenient = decode("MZ").expect("宽容模式应接受非规范填充比特");
assert_eq!(lenient.expose_secret(), &[0x66]);
let strict = decode_strict("MZ");
assert!(
matches!(strict, Err(Error::Base32InvalidPadding)),
"严格模式应拒绝非零填充比特,实际: {strict:?}"
);
}
#[test]
fn zero_padding_bits_accepted_in_both_modes() {
for input in ["MA", "ME", "MI", "MM", "MQ", "MU", "MY", "M4"] {
assert!(decode(input).is_ok(), "{input:?} 宽容模式应可解码");
assert!(
decode_strict(input).is_ok(),
"{input:?} 严格模式应可解码(残留比特为零)"
);
}
assert!(decode("ML").is_ok(), "宽容模式仍应接受");
assert!(
matches!(decode_strict("ML"), Err(Error::Base32InvalidPadding)),
"严格模式应拒绝 ML"
);
}
#[test]
fn our_own_encoding_is_canonical() {
for len in 1_usize..=70 {
let data: Vec<u8> = (0..len).map(|i| (i as u8).wrapping_mul(31)).collect();
let encoded = encode(&data);
let decoded = decode_strict(&encoded)
.unwrap_or_else(|e| panic!("长度 {len} 的自产编码不满足规范形式: {e}"));
assert_eq!(decoded.expose_secret(), data.as_slice(), "长度 {len}");
}
}
#[test]
fn rejects_empty_and_padding_only() {
for bad in ["", " ", "========", "="] {
let result = decode(bad);
assert!(
matches!(result, Err(Error::SecretTooShort { .. })),
"{bad:?} 应被拒为空秘密,实际: {result:?}"
);
}
}
#[test]
fn roundtrip_realistic_seed_lengths() {
for len in [10_usize, 16, 20, 32, 64] {
let original: Vec<u8> = (0..len).map(|i| (i as u8).wrapping_mul(7)).collect();
let encoded = encode(&original);
let decoded = decode(&encoded).unwrap_or_else(|e| panic!("长度 {len} 往返失败: {e}"));
assert_eq!(decoded.expose_secret(), original.as_slice(), "长度 {len}");
}
}
#[test]
fn errors_do_not_embed_input() {
let secret_like = "JBSWY3DPEHPK3PXP0";
let Err(error) = decode(secret_like) else {
panic!("应解码失败");
};
let text = format!("{error}");
assert!(
!text.contains("JBSWY3DPEHPK3PXP"),
"错误信息泄露了种子内容:{text}"
);
}
}