pub mod base64url {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Base64UrlDecodeError {
InvalidLength,
InvalidByte {
index: usize,
byte: u8,
},
NonCanonicalTrailingBits,
}
impl std::fmt::Display for Base64UrlDecodeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidLength => write!(f, "invalid base64url length (len % 4 == 1)"),
Self::InvalidByte { index, byte } => {
write!(f, "invalid base64url byte {byte:#04x} at index {index}")
}
Self::NonCanonicalTrailingBits => {
write!(f, "non-canonical trailing bits in base64url input")
}
}
}
}
impl std::error::Error for Base64UrlDecodeError {}
#[must_use]
pub fn encode(bytes: &[u8]) -> String {
const TABLE: &[u8; 64] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
for chunk in bytes.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = chunk.get(1).map_or(0, |&b| u32::from(b));
let b2 = chunk.get(2).map_or(0, |&b| u32::from(b));
let n = (b0 << 16) | (b1 << 8) | b2;
out.push(TABLE[(n >> 18 & 63) as usize] as char);
out.push(TABLE[(n >> 12 & 63) as usize] as char);
if chunk.len() > 1 {
out.push(TABLE[(n >> 6 & 63) as usize] as char);
}
if chunk.len() > 2 {
out.push(TABLE[(n & 63) as usize] as char);
}
}
out
}
pub fn decode(s: &str) -> Result<Vec<u8>, Base64UrlDecodeError> {
fn val(c: u8) -> Option<u32> {
match c {
b'A'..=b'Z' => Some(u32::from(c - b'A')),
b'a'..=b'z' => Some(u32::from(c - b'a') + 26),
b'0'..=b'9' => Some(u32::from(c - b'0') + 52),
b'-' => Some(62),
b'_' => Some(63),
_ => None,
}
}
if s.len() % 4 == 1 {
return Err(Base64UrlDecodeError::InvalidLength);
}
let s = s.trim_end_matches('=');
let mut out = Vec::with_capacity(s.len() * 3 / 4 + 3);
let mut acc: u32 = 0;
let mut bits: u32 = 0;
for (i, &c) in s.as_bytes().iter().enumerate() {
let v = val(c).ok_or(Base64UrlDecodeError::InvalidByte { index: i, byte: c })?;
acc = (acc << 6) | v;
bits += 6;
if bits >= 8 {
bits -= 8;
out.push(((acc >> bits) & 0xff) as u8);
}
if i == s.len() - 1 && bits > 0 && (acc & ((1 << bits) - 1)) != 0 {
return Err(Base64UrlDecodeError::NonCanonicalTrailingBits);
}
}
Ok(out)
}
}
#[must_use]
pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
let mut diff = u64::from(a.len() != b.len());
let max_len = std::cmp::max(a.len(), b.len());
for i in 0..max_len {
let x = a.get(i).copied().unwrap_or(0);
let y = b.get(i).copied().unwrap_or(0);
diff |= u64::from(x ^ y);
}
diff == 0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_constant_time_eq_equal() {
assert!(constant_time_eq(b"hello", b"hello"));
}
#[test]
fn test_constant_time_eq_not_equal() {
assert!(!constant_time_eq(b"hello", b"world"));
}
#[test]
fn test_constant_time_eq_different_lengths() {
assert!(!constant_time_eq(b"ab", b"abc"));
}
#[test]
fn test_constant_time_eq_empty() {
assert!(constant_time_eq(b"", b""));
}
#[test]
fn base64url_encode_rfc4648_vectors() {
assert_eq!(base64url::encode(b""), "");
assert_eq!(base64url::encode(b"f"), "Zg");
assert_eq!(base64url::encode(b"fo"), "Zm8");
assert_eq!(base64url::encode(b"foo"), "Zm9v");
assert_eq!(base64url::encode(b"foob"), "Zm9vYg");
assert_eq!(base64url::encode(b"fooba"), "Zm9vYmE");
assert_eq!(base64url::encode(b"foobar"), "Zm9vYmFy");
assert_eq!(base64url::encode(&[0xfb, 0xff]), "-_8");
assert_eq!(base64url::encode(&[0xfb, 0xff, 0xfe, 0xfd]), "-__-_Q");
}
#[test]
fn base64url_decode_rfc4648_vectors() {
assert_eq!(base64url::decode("").unwrap(), b"");
assert_eq!(base64url::decode("Zg").unwrap(), b"f");
assert_eq!(base64url::decode("Zm8").unwrap(), b"fo");
assert_eq!(base64url::decode("Zm9v").unwrap(), b"foo");
assert_eq!(base64url::decode("Zm9vYg").unwrap(), b"foob");
assert_eq!(base64url::decode("Zm9vYmE").unwrap(), b"fooba");
assert_eq!(base64url::decode("Zm9vYmFy").unwrap(), b"foobar");
assert_eq!(base64url::decode("Zg==").unwrap(), b"f");
assert_eq!(base64url::decode("Zm8=").unwrap(), b"fo");
assert_eq!(base64url::decode("Zm9v").unwrap(), b"foo");
}
#[test]
fn base64url_decode_rejects_malformed() {
assert_eq!(
base64url::decode("A").unwrap_err(),
base64url::Base64UrlDecodeError::InvalidLength
);
assert_eq!(
base64url::decode("Zm9=").unwrap_err(),
base64url::Base64UrlDecodeError::NonCanonicalTrailingBits
);
assert_eq!(
base64url::decode("Zm$v").unwrap_err(),
base64url::Base64UrlDecodeError::InvalidByte {
index: 2,
byte: b'$'
}
);
assert!(base64url::decode("+/8A").is_err());
assert!(base64url::decode("Zm9 vYg").is_err());
}
#[test]
fn base64url_roundtrip_all_lengths() {
for len in 0..=64 {
let data: Vec<u8> = (0..len).map(|i| (i * 7 + 13) as u8).collect();
let encoded = base64url::encode(&data);
assert!(!encoded.contains('='), "unpadded for len={len}");
assert_eq!(
base64url::decode(&encoded).unwrap(),
data,
"roundtrip failed for len={len}"
);
}
}
}