use subtle::{Choice, ConstantTimeEq};
use zeroize::Zeroizing;
pub fn b64url_encode(data: &[u8]) -> String {
let mut out = String::with_capacity(encoded_len(data.len()));
encode_groups(data, URL_SAFE, &mut out);
out
}
fn encode_groups(data: &[u8], alphabet: Alphabet, out: &mut String) {
for group in data.chunks(3) {
let mut octets = [0u8; 3];
octets[..group.len()].copy_from_slice(group);
let bits = u32::from(octets[0]) << 16 | u32::from(octets[1]) << 8 | u32::from(octets[2]);
for i in 0..=group.len() {
let value = (bits >> (18 - 6 * i)) as u8;
out.push(char::from(encode_symbol_in(value, alphabet)));
}
}
}
fn encoded_len(len: usize) -> usize {
len / 3 * 4 + (len % 3 * 4).div_ceil(3)
}
#[derive(Clone, Copy)]
struct Alphabet {
c62: u8,
c63: u8,
}
const URL_SAFE: Alphabet = Alphabet {
c62: b'-',
c63: b'_',
};
#[inline]
fn ge_mask(x: i16, k: i16) -> i16 {
(k - 1 - x) >> 15
}
#[cfg(test)]
fn encode_symbol(value: u8) -> u8 {
encode_symbol_in(value, URL_SAFE)
}
#[inline]
fn encode_symbol_in(value: u8, alphabet: Alphabet) -> u8 {
let v = i16::from(value & 0x3f);
let (c62, c63) = (i16::from(alphabet.c62), i16::from(alphabet.c63));
let mut offset = i16::from(b'A');
offset += ge_mask(v, 26) & ((i16::from(b'a') - 26) - i16::from(b'A'));
offset += ge_mask(v, 52) & ((i16::from(b'0') - 52) - (i16::from(b'a') - 26));
offset += ge_mask(v, 62) & ((c62 - 62) - (i16::from(b'0') - 52));
offset += ge_mask(v, 63) & ((c63 - 63) - (c62 - 62));
(v + offset) as u8
}
pub fn b64url_decode(s: &str) -> crate::Result<Vec<u8>> {
if s.len() % 4 == 1 {
return Err(crate::Error::InvalidBase64);
}
let mut buf = Zeroizing::new(vec![0u8; decoded_len(s.len())]);
let valid = decode_into(s.as_bytes(), &mut buf);
if bool::from(valid) {
Ok(core::mem::take(&mut *buf))
} else {
Err(crate::Error::InvalidBase64)
}
}
fn decode_into(input: &[u8], out: &mut [u8]) -> Choice {
decode_into_in(input, out, URL_SAFE)
}
fn decode_into_in(input: &[u8], out: &mut [u8], alphabet: Alphabet) -> Choice {
let mut valid = Choice::from(1);
for (quantum, octets) in input.chunks(4).zip(out.chunks_mut(3)) {
let mut bits = 0u32;
for (i, &byte) in quantum.iter().enumerate() {
let (value, symbol_valid) = decode_symbol_in(byte, alphabet);
valid &= symbol_valid;
bits |= u32::from(value) << (18 - 6 * i);
}
for (j, octet) in octets.iter_mut().enumerate() {
*octet = (bits >> (16 - 8 * j)) as u8;
}
let pad_bits = bits & (0x00ff_ffff >> (8 * octets.len()));
valid &= pad_bits.ct_eq(&0);
}
valid
}
#[inline]
fn in_range_mask(x: i16, lo: i16, hi: i16) -> i16 {
((lo - 1 - x) & (x - hi - 1)) >> 15
}
#[cfg(test)]
fn decode_symbol(byte: u8) -> (u8, Choice) {
decode_symbol_in(byte, URL_SAFE)
}
#[inline]
fn decode_symbol_in(byte: u8, alphabet: Alphabet) -> (u8, Choice) {
let c = i16::from(byte);
let mut value = 0i16;
let mut found = 0i16;
for (lo, hi, first_value) in [
(b'A', b'Z', 0),
(b'a', b'z', 26),
(b'0', b'9', 52),
(alphabet.c62, alphabet.c62, 62),
(alphabet.c63, alphabet.c63, 63),
] {
let (lo, hi) = (i16::from(lo), i16::from(hi));
let mask = in_range_mask(c, lo, hi);
value |= mask & (c - lo + first_value);
found |= mask;
}
(value as u8, Choice::from((found & 1) as u8))
}
fn decoded_len(encoded_len: usize) -> usize {
encoded_len / 4 * 3 + (encoded_len % 4) * 3 / 4
}
const STANDARD: Alphabet = Alphabet {
c62: b'+',
c63: b'/',
};
const PAD: u8 = b'=';
pub(crate) fn b64_encoded_len(len: usize) -> usize {
len / 3 * 4 + if len.is_multiple_of(3) { 0 } else { 4 }
}
pub(crate) fn b64_encode_append(data: &[u8], out: &mut String) {
encode_groups(data, STANDARD, out);
for _ in 0..(3 - data.len() % 3) % 3 {
out.push(char::from(PAD));
}
}
pub(crate) fn b64_decode_into(input: &[u8], out: &mut Vec<u8>) -> crate::Result<()> {
use zeroize::Zeroize;
if !input.len().is_multiple_of(4) {
return Err(crate::Error::InvalidBase64);
}
let pad = match input {
[.., a, b] if *a == PAD && *b == PAD => 2,
[.., b] if *b == PAD => 1,
_ => 0,
};
let body = &input[..input.len() - pad];
let len = decoded_len(body.len());
out.zeroize();
out.reserve_exact(len);
out.resize(len, 0);
let valid = decode_into_in(body, out, STANDARD);
if bool::from(valid) {
Ok(())
} else {
out.as_mut_slice().zeroize();
Err(crate::Error::InvalidBase64)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip() {
let s = b64url_encode(b"{\"alg\":\"HS256\"}");
assert_eq!(s, "eyJhbGciOiJIUzI1NiJ9");
assert_eq!(b64url_decode(&s).unwrap(), b"{\"alg\":\"HS256\"}");
}
const RFC4648_SECTION_10: [(&[u8], &str); 7] = [
(b"", ""), (b"f", "Zg"), (b"fo", "Zm8"), (b"foo", "Zm9v"), (b"foob", "Zm9vYg"), (b"fooba", "Zm9vYmE"), (b"foobar", "Zm9vYmFy"),
];
#[test]
fn rfc4648_section_10_vectors_encode() {
for (bytes, encoded) in RFC4648_SECTION_10 {
assert_eq!(b64url_encode(bytes), encoded, "encode {bytes:?}");
}
}
#[test]
fn rfc4648_section_10_vectors_decode() {
for (bytes, encoded) in RFC4648_SECTION_10 {
assert_eq!(b64url_decode(encoded).unwrap(), bytes, "decode {encoded:?}");
}
}
#[test]
fn rfc7515_appendix_c_vector_uses_url_alphabet() {
let bytes = [3u8, 236, 255, 224, 193];
assert_eq!(b64url_encode(&bytes), "A-z_4ME");
assert_eq!(b64url_decode("A-z_4ME").unwrap(), bytes);
}
#[test]
fn url_alphabet_covers_values_62_and_63() {
assert_eq!(b64url_encode(&[0xfb, 0xff]), "-_8");
assert_eq!(b64url_decode("-_8").unwrap(), [0xfb, 0xff]);
}
fn assert_all_rejected(inputs: &[&str]) {
for input in inputs {
assert_eq!(
b64url_decode(input),
Err(crate::Error::InvalidBase64),
"{input:?} must be rejected"
);
}
}
#[test]
fn rejects_padding() {
assert_all_rejected(&[
"Zg==", "Zg=", "Zm8=", "Zm9vYg==", "Zm9vYmE=", "=", "==", "Zm9v====",
]);
}
#[test]
fn rejects_standard_alphabet_characters() {
assert_all_rejected(&["+/8", "+_8", "-/8", "Zm9v+mFy", "Zm9v/mFy"]);
}
#[test]
fn rejects_whitespace_and_line_breaks() {
assert_all_rejected(&[
" Zm9v",
"Zm9v ",
"Zm 9v",
"Zm9v\nYmFy",
"Zm9v\r\nYmFy",
"Zm9v\tYmFy",
"Zm9v\n",
"\u{a0}Zm9v",
" ",
]);
}
#[test]
fn rejects_non_alphabet_bytes() {
assert_all_rejected(&[
"Zm9v*",
"Zm9v.",
"Zm9v%3D",
"Zm9v\0",
"Zm9vé",
"Zm9v~",
"Zm9v,YmFy",
]);
}
#[test]
fn rejects_impossible_lengths() {
assert_all_rejected(&["Z", "A", "Zm9vY", "AAAAA", "Zm9vYmFyZ"]);
}
#[test]
fn rejects_non_canonical_trailing_bits() {
assert_eq!(b64url_decode("QQ").unwrap(), [0x41]);
assert_eq!(b64url_decode("QUE").unwrap(), [0x41, 0x41]);
assert_all_rejected(&[
"QR", "QS", "QT", "Q_", "QUF", "QUG", "QUH", "Zh", "Zm9vYh", "Zm9vYmF",
]);
}
#[test]
fn decoded_len_matches_every_canonical_length() {
for n in 0..=64usize {
let encoded = b64url_encode(&vec![0xa5; n]);
assert_eq!(decoded_len(encoded.len()), n, "{n} octets");
}
assert_eq!(decoded_len(usize::MAX), usize::MAX / 4 * 3 + 2);
}
#[test]
fn empty_input_decodes_to_empty_bytes() {
assert_eq!(b64url_decode("").unwrap(), Vec::<u8>::new());
assert_eq!(b64url_encode(&[]), "");
}
const TABLE_2: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
#[test]
fn encode_maps_every_value_per_table_2() {
for (v, &symbol) in (0u8..).zip(TABLE_2.iter()) {
assert_eq!(
b64url_encode(&[v << 2]).as_bytes(),
[symbol, b'A'],
"value {v} first"
);
assert_eq!(
b64url_encode(&[0, 0, v]).as_bytes(),
[b'A', b'A', b'A', symbol],
"value {v} last"
);
}
}
#[test]
fn decode_maps_every_byte_per_table_2() {
for c in (0u8..=0xff).map(char::from) {
let value = u8::try_from(c)
.ok()
.and_then(|b| TABLE_2.iter().position(|&s| s == b))
.and_then(|v| u8::try_from(v).ok());
let first = b64url_decode(&format!("{c}A"));
let last = b64url_decode(&format!("AAA{c}"));
match value {
Some(v) => {
assert_eq!(first, Ok(vec![v << 2]), "{c:?} first");
assert_eq!(last, Ok(vec![0, 0, v]), "{c:?} last");
}
None => {
assert_eq!(first, Err(crate::Error::InvalidBase64), "{c:?} first");
assert_eq!(last, Err(crate::Error::InvalidBase64), "{c:?} last");
}
}
}
}
#[test]
fn encode_symbol_matches_table_2() {
for (v, &symbol) in (0u8..).zip(TABLE_2.iter()) {
assert_eq!(encode_symbol(v), symbol, "value {v}");
assert_eq!(
encode_symbol(v | 0xc0),
symbol,
"value {v} with high bits set"
);
}
}
#[test]
fn decode_symbol_matches_table_2() {
for b in 0u8..=0xff {
let (value, valid) = decode_symbol(b);
match TABLE_2.iter().position(|&s| s == b) {
Some(v) => {
assert_eq!(usize::from(value), v, "byte {b:#04x}");
assert!(bool::from(valid), "byte {b:#04x} must be valid");
}
None => {
assert_eq!(value, 0, "byte {b:#04x} must contribute no bits");
assert!(!bool::from(valid), "byte {b:#04x} must be invalid");
}
}
}
}
#[test]
fn decode_into_processes_the_whole_input_before_the_verdict() {
let mut out = [0u8; 6];
assert!(!bool::from(decode_into(b"*AAAZm9v", &mut out)));
assert_eq!(&out[3..], b"foo", "quantum after the bad byte decoded");
let mut out = [0u8; 4];
assert!(!bool::from(decode_into(b"Zm9vQR", &mut out)));
assert_eq!(out, *b"fooA", "non-canonical tail decoded, then rejected");
let mut out = [0u8; 5];
assert!(bool::from(decode_into(b"Zm9vYmE", &mut out)));
assert_eq!(out, *b"fooba");
}
mod properties {
use super::super::*;
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(256))]
#[test]
fn encode_then_decode_round_trips(bytes in proptest::collection::vec(any::<u8>(), 0..512)) {
let encoded = b64url_encode(&bytes);
prop_assert!(encoded.bytes().all(|c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_'));
prop_assert_eq!(encoded.len(), (bytes.len() * 4).div_ceil(3));
prop_assert_eq!(b64url_decode(&encoded)?, bytes);
}
#[test]
fn decode_accepts_only_canonical_alphabet_strings(s in "[A-Za-z0-9_-]{0,96}") {
if let Ok(bytes) = b64url_decode(&s) {
prop_assert_eq!(b64url_encode(&bytes), s);
}
}
#[test]
fn decode_of_arbitrary_text_is_canonical_or_rejected(
s in prop_oneof![
"[A-Za-z0-9_=+/ \\t\\r\\n-]{0,64}",
any::<String>(),
]
) {
if let Ok(bytes) = b64url_decode(&s) {
prop_assert_eq!(b64url_encode(&bytes), s);
}
}
}
}
}
#[cfg(test)]
mod standard_tests {
use super::*;
fn encode(data: &[u8]) -> String {
let mut out = String::with_capacity(b64_encoded_len(data.len()));
b64_encode_append(data, &mut out);
out
}
fn decode(s: &str) -> crate::Result<Vec<u8>> {
let mut out = Vec::new();
b64_decode_into(s.as_bytes(), &mut out).map(|()| out)
}
const RFC4648_SECTION_10: [(&[u8], &str); 7] = [
(b"", ""),
(b"f", "Zg=="),
(b"fo", "Zm8="),
(b"foo", "Zm9v"),
(b"foob", "Zm9vYg=="),
(b"fooba", "Zm9vYmE="),
(b"foobar", "Zm9vYmFy"),
];
#[test]
fn rfc4648_section_10_vectors_encode() {
for (bytes, encoded) in RFC4648_SECTION_10 {
assert_eq!(encode(bytes), encoded, "encode {bytes:?}");
assert_eq!(b64_encoded_len(bytes.len()), encoded.len());
}
}
#[test]
fn rfc4648_section_10_vectors_decode() {
for (bytes, encoded) in RFC4648_SECTION_10 {
assert_eq!(decode(encoded), Ok(bytes.to_vec()), "decode {encoded:?}");
}
}
const TABLE_1: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
#[test]
fn encode_maps_every_value_per_table_1() {
for (v, &symbol) in (0u8..).zip(TABLE_1.iter()) {
assert_eq!(encode_symbol_in(v, STANDARD), symbol, "value {v}");
assert_eq!(encode_symbol_in(v | 0xc0, STANDARD), symbol, "value {v}");
assert_eq!(encode(&[0, 0, v]).as_bytes(), [b'A', b'A', b'A', symbol]);
}
assert_eq!(encode(&[0xfb, 0xff]), "+/8=");
}
#[test]
fn decode_maps_every_byte_per_table_1() {
for b in 0u8..=0xff {
let (value, valid) = decode_symbol_in(b, STANDARD);
let mut out = Vec::new();
let result = b64_decode_into(&[b, b'A', b'A', b'A'], &mut out);
match TABLE_1.iter().position(|&s| s == b) {
Some(v) => {
assert_eq!(usize::from(value), v, "byte {b:#04x}");
assert!(bool::from(valid));
assert_eq!(result, Ok(()));
assert_eq!(usize::from(out[0] >> 2), v);
}
None => {
assert_eq!(value, 0, "byte {b:#04x} must contribute no bits");
assert!(!bool::from(valid), "byte {b:#04x} must be invalid");
assert_eq!(result, Err(crate::Error::InvalidBase64), "{b:#04x}");
}
}
}
}
fn assert_all_rejected(inputs: &[&str]) {
for input in inputs {
assert_eq!(
decode(input),
Err(crate::Error::InvalidBase64),
"{input:?} must be rejected"
);
}
}
#[test]
fn rejects_missing_or_misplaced_padding() {
assert_all_rejected(&[
"Zg", "Zg=", "Zm8", "Zm9vYg", "Zm9vYg=", "Zm9vYmE", "Z", "Zm9vY", "=", "==", "===",
"====", "Z===", "Zg==Zg==", "Zm=v", "=m9v", "Zg==\n", "Zm9v====",
]);
}
#[test]
fn rejects_url_alphabet_and_whitespace() {
assert_all_rejected(&["-_8=", "+_8=", "-/8=", "Zm9v YmFy", "Zm9v\nYmFy", " Zg=="]);
}
#[test]
fn rejects_non_canonical_pad_bits() {
assert_eq!(decode("QQ=="), Ok(vec![0x41]));
assert_eq!(decode("QUE="), Ok(vec![0x41, 0x41]));
assert_all_rejected(&["QR==", "QS==", "Q/==", "QUF=", "QUH=", "Zh==", "Zm9vYmF="]);
}
#[test]
fn rejected_decode_wipes_the_output_buffer() {
for input in ["Zm9vYmFy*AAA", "Zm9vYmFyQR==", "Zm9vYmFyZm9v-A=="] {
let mut out = Vec::new();
assert_eq!(
b64_decode_into(input.as_bytes(), &mut out),
Err(crate::Error::InvalidBase64)
);
assert!(!out.is_empty(), "{input:?}: the buffer was written");
assert!(out.iter().all(|&b| b == 0), "{input:?}: {out:?}");
}
}
#[test]
fn buffers_are_sized_exactly() {
for n in 0..=50usize {
let data = vec![0xa5; n];
let encoded = encode(&data);
assert_eq!(encoded.len(), b64_encoded_len(n));
let mut out = Vec::new();
assert_eq!(b64_decode_into(encoded.as_bytes(), &mut out), Ok(()));
assert_eq!(out, data);
assert_eq!(out.capacity(), n, "{n} octets");
}
}
fn planned_len(input: &[u8]) -> usize {
let pad = input
.iter()
.rev()
.take(2)
.take_while(|&&c| c == PAD)
.count();
input.len() / 4 * 3 - pad
}
fn check_planned_capacity(input: &[u8], out: &mut Vec<u8>) -> crate::Result<()> {
assert!(input.len().is_multiple_of(4));
let before = out.capacity();
let result = b64_decode_into(input, out);
let planned = planned_len(input);
assert_eq!(out.len(), planned);
assert_eq!(out.capacity(), before.max(planned), "{} octets", planned);
result
}
#[test]
fn output_keeps_its_planned_capacity_across_the_key_corpus() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/data/keys");
let mut checked = 0;
for dir in ["openssl", "rfc", "negative"] {
for entry in std::fs::read_dir(root.join(dir)).unwrap() {
let path = entry.unwrap().path();
let der = match path.extension().and_then(|e| e.to_str()) {
Some("der") => std::fs::read(&path).unwrap(),
Some("pem") => match pem_rfc7468::decode_vec(&std::fs::read(&path).unwrap()) {
Ok((_, der)) => der,
Err(_) => continue,
},
_ => continue,
};
let encoded = encode(&der);
let mut fresh = Vec::new();
assert_eq!(
check_planned_capacity(encoded.as_bytes(), &mut fresh),
Ok(())
);
assert_eq!(fresh, der, "{}", path.display());
assert_eq!(fresh.capacity(), der.len(), "{}", path.display());
let mut roomy = Vec::with_capacity(der.len() + 100);
assert_eq!(
check_planned_capacity(encoded.as_bytes(), &mut roomy),
Ok(())
);
let mut used = vec![0xa5; der.len() / 2];
assert_eq!(
check_planned_capacity(encoded.as_bytes(), &mut used),
Ok(())
);
assert_eq!(used, der, "{}", path.display());
checked += 1;
}
}
assert!(checked >= 100, "only {checked} fixtures");
}
#[test]
fn exhaustive_short_strings_agree_with_base64ct() {
use base64ct::{Base64, Encoding};
let full: Vec<char> = "ABCDQRSTwxyz0189+/=-_ ".chars().collect();
let mut strings = vec![String::new()];
for &a in &full {
strings.push(format!("{a}"));
for &b in &full {
strings.push(format!("{a}{b}"));
for &c in &full {
strings.push(format!("{a}{b}{c}"));
for &d in &full {
strings.push(format!("{a}{b}{c}{d}"));
}
}
}
}
for s in strings {
assert_eq!(decode(&s).ok(), Base64::decode_vec(&s).ok(), "{s:?}");
}
}
mod properties {
use super::{check_planned_capacity, decode, encode};
use base64ct::{Base64, Encoding};
use proptest::prelude::*;
fn mutated_encoding() -> impl Strategy<Value = String> {
(
proptest::collection::vec(any::<u8>(), 1..64),
any::<prop::sample::Index>(),
prop::sample::select("AQgw+/RSTUVf09=-_ \n.".chars().collect::<Vec<char>>()),
)
.prop_map(|(bytes, at, with)| {
let encoded = Base64::encode_string(&bytes);
let i = at.index(encoded.len());
format!("{}{with}{}", &encoded[..i], &encoded[i + 1..])
})
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(1000))]
#[test]
fn encode_matches_base64ct(bytes in proptest::collection::vec(any::<u8>(), 0..300)) {
let ours = encode(&bytes);
prop_assert_eq!(&ours, &Base64::encode_string(&bytes));
prop_assert_eq!(decode(&ours)?, bytes);
}
#[test]
fn decode_matches_base64ct(
s in prop_oneof![
"[A-Za-z0-9+/=_ \\n-]{0,40}",
any::<String>(),
mutated_encoding(),
]
) {
prop_assert_eq!(decode(&s).ok(), Base64::decode_vec(&s).ok(), "{:?}", s);
}
#[test]
fn output_keeps_its_planned_capacity(
bytes in proptest::collection::vec(any::<u8>(), 0..600),
prior in proptest::collection::vec(any::<u8>(), 0..700),
spare in 0usize..64,
mutation in proptest::option::of((
any::<prop::sample::Index>(),
prop::sample::select("AQgw+/=-_ *".chars().collect::<Vec<char>>()),
)),
) {
let mut encoded = encode(&bytes);
let mutated = mutation.is_some() && !encoded.is_empty();
if let (Some((at, with)), true) = (mutation, mutated) {
let i = at.index(encoded.len());
encoded.replace_range(i..=i, with.encode_utf8(&mut [0; 4]));
}
let mut out = prior;
out.reserve_exact(spare);
match check_planned_capacity(encoded.as_bytes(), &mut out) {
Ok(()) if !mutated => prop_assert_eq!(out, bytes),
Ok(()) => {}
Err(_) => prop_assert!(out.iter().all(|&b| b == 0)),
}
}
}
}
}