#![cfg_attr(not(feature = "std"), no_std)]
#![deny(unsafe_code)]
#![deny(missing_docs)]
#![deny(clippy::all)]
#![deny(clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
#[cfg(feature = "alloc")]
extern crate alloc;
#[cfg(all(target_arch = "wasm32", not(feature = "allow-wasm32-best-effort-wipe")))]
compile_error!(
"base64-ng: wasm32 builds use a compiler-fence-only wipe barrier that cannot \
constrain downstream wasm runtime JITs. Enable \
`allow-wasm32-best-effort-wipe` to accept this limitation and use \
caller-owned, platform-approved zeroization for high-assurance wasm deployments."
);
#[cfg(all(
not(miri),
not(feature = "allow-compiler-fence-only-wipe"),
not(any(
target_arch = "aarch64",
target_arch = "arm",
target_arch = "riscv32",
target_arch = "riscv64",
target_arch = "wasm32",
target_arch = "x86",
target_arch = "x86_64",
))
))]
compile_error!(
"base64-ng: this architecture has no native hardware wipe barrier in \
base64-ng. Enable `allow-compiler-fence-only-wipe` only after reviewing \
docs/UNSAFE.md and applying platform-approved memory hygiene controls."
);
mod alphabet;
mod buffers;
mod cleanup;
pub mod ct;
mod errors;
mod length;
mod profiles;
mod scalar;
mod wrap;
pub use alphabet::{
Alphabet, AlphabetError, Bcrypt, Crypt, Standard, UrlSafe, decode_alphabet_byte,
validate_alphabet,
};
pub(crate) use alphabet::{encode_base64_value, encode_base64_value_runtime};
pub use buffers::{DecodedBuffer, EncodedBuffer, ExposedDecodedArray, ExposedEncodedArray};
#[cfg(feature = "alloc")]
pub use buffers::{ExposedSecretString, ExposedSecretVec, SecretBuffer};
pub(crate) use cleanup::{wipe_bytes, wipe_tail};
#[cfg(feature = "alloc")]
pub(crate) use cleanup::{wipe_vec_all, wipe_vec_spare_capacity};
pub(crate) use ct::{
constant_time_eq_fixed_width_array, constant_time_eq_public_len, ct_mask_eq_u8, ct_mask_lt_u8,
};
#[cfg(test)]
pub(crate) use ct::{ct_padded_final_quantum, report_ct_error};
pub use errors::{DecodeError, EncodeError};
pub use length::{
LineEnding, LineWrap, checked_encoded_len, checked_wrapped_encoded_len, decoded_capacity,
decoded_len, encoded_len, wrapped_encoded_len,
};
pub(crate) use length::{decoded_len_padded, decoded_len_unpadded};
pub use profiles::{BCRYPT, CRYPT, MIME, PEM, PEM_CRLF, Profile};
#[cfg(kani)]
pub(crate) use scalar::decode_byte;
pub(crate) use scalar::{
decode_chunk, decode_tail_unpadded, read_quad, validate_chunk, validate_decode,
validate_tail_unpadded,
};
pub(crate) use wrap::{
compact_wrapped_input, decode_legacy_to_slice, decode_wrapped_to_slice, is_legacy_whitespace,
validate_legacy_decode, validate_wrapped_decode, write_wrapped_byte, write_wrapped_bytes,
};
#[cfg(feature = "simd")]
mod simd;
pub mod runtime;
#[cfg(feature = "stream")]
pub mod stream;
#[doc(alias = "ct")]
#[doc(alias = "constant_time")]
#[doc(alias = "sensitive")]
pub const STANDARD: Engine<Standard, true> = Engine::new();
#[doc(alias = "ct")]
#[doc(alias = "constant_time")]
#[doc(alias = "sensitive")]
pub const STANDARD_NO_PAD: Engine<Standard, false> = Engine::new();
#[doc(alias = "ct")]
#[doc(alias = "constant_time")]
#[doc(alias = "sensitive")]
pub const URL_SAFE: Engine<UrlSafe, true> = Engine::new();
#[doc(alias = "ct")]
#[doc(alias = "constant_time")]
#[doc(alias = "sensitive")]
pub const URL_SAFE_NO_PAD: Engine<UrlSafe, false> = Engine::new();
#[doc(alias = "ct")]
#[doc(alias = "constant_time")]
#[doc(alias = "sensitive")]
pub const BCRYPT_NO_PAD: Engine<Bcrypt, false> = Engine::new();
#[doc(alias = "ct")]
#[doc(alias = "constant_time")]
#[doc(alias = "sensitive")]
pub const CRYPT_NO_PAD: Engine<Crypt, false> = Engine::new();
#[cfg(feature = "alloc")]
pub fn encode(input: &[u8]) -> Result<alloc::string::String, EncodeError> {
STANDARD.encode_string(input)
}
#[cfg(feature = "alloc")]
#[must_use = "handle decode errors; use crate::ct for secret-bearing payloads"]
pub fn decode(input: impl AsRef<[u8]>) -> Result<alloc::vec::Vec<u8>, DecodeError> {
STANDARD.decode_vec(input.as_ref())
}
#[must_use]
pub fn constant_time_eq_fixed_width<const N: usize>(left: &[u8; N], right: &[u8; N]) -> bool {
constant_time_eq_fixed_width_array(left, right)
}
#[must_use]
pub fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
constant_time_eq_public_len(left, right)
}
pub struct Engine<A, const PAD: bool> {
alphabet: core::marker::PhantomData<A>,
}
impl<A, const PAD: bool> Clone for Engine<A, PAD> {
fn clone(&self) -> Self {
*self
}
}
impl<A, const PAD: bool> Copy for Engine<A, PAD> {}
impl<A, const PAD: bool> core::fmt::Debug for Engine<A, PAD> {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter
.debug_struct("Engine")
.field("padded", &PAD)
.finish()
}
}
impl<A, const PAD: bool> core::fmt::Display for Engine<A, PAD> {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(formatter, "padded={PAD}")
}
}
impl<A, const PAD: bool> Default for Engine<A, PAD> {
fn default() -> Self {
Self {
alphabet: core::marker::PhantomData,
}
}
}
impl<A, const PAD: bool> Eq for Engine<A, PAD> {}
impl<A, const PAD: bool> PartialEq for Engine<A, PAD> {
fn eq(&self, _other: &Self) -> bool {
true
}
}
impl<A, const PAD: bool> Engine<A, PAD>
where
A: Alphabet,
{
#[must_use]
pub const fn new() -> Self {
Self {
alphabet: core::marker::PhantomData,
}
}
#[must_use]
pub const fn is_padded(&self) -> bool {
PAD
}
#[must_use]
pub const fn profile(&self) -> Profile<A, PAD> {
Profile::new(*self, None)
}
#[must_use]
pub const fn ct_decoder(&self) -> ct::CtEngine<A, PAD> {
ct::CtEngine::new()
}
#[cfg(feature = "stream")]
#[must_use]
pub fn encoder_writer<W>(&self, inner: W) -> stream::Encoder<W, A, PAD> {
stream::Encoder::new(inner, *self)
}
#[cfg(feature = "stream")]
#[must_use]
pub fn decoder_writer<W>(&self, inner: W) -> stream::Decoder<W, A, PAD> {
stream::Decoder::new(inner, *self)
}
#[cfg(feature = "stream")]
#[must_use]
pub fn encoder_reader<R>(&self, inner: R) -> stream::EncoderReader<R, A, PAD> {
stream::EncoderReader::new(inner, *self)
}
#[cfg(feature = "stream")]
#[must_use]
pub fn decoder_reader<R>(&self, inner: R) -> stream::DecoderReader<R, A, PAD> {
stream::DecoderReader::new(inner, *self)
}
pub const fn encoded_len(&self, input_len: usize) -> Result<usize, EncodeError> {
encoded_len(input_len, PAD)
}
#[must_use]
pub const fn checked_encoded_len(&self, input_len: usize) -> Option<usize> {
checked_encoded_len(input_len, PAD)
}
pub const fn wrapped_encoded_len(
&self,
input_len: usize,
wrap: LineWrap,
) -> Result<usize, EncodeError> {
wrapped_encoded_len(input_len, PAD, wrap)
}
#[must_use]
pub const fn checked_wrapped_encoded_len(
&self,
input_len: usize,
wrap: LineWrap,
) -> Option<usize> {
checked_wrapped_encoded_len(input_len, PAD, wrap)
}
pub fn decoded_len(&self, input: &[u8]) -> Result<usize, DecodeError> {
decoded_len(input, PAD)
}
pub fn decoded_len_legacy(&self, input: &[u8]) -> Result<usize, DecodeError> {
validate_legacy_decode::<A, PAD>(input)
}
pub fn decoded_len_wrapped(&self, input: &[u8], wrap: LineWrap) -> Result<usize, DecodeError> {
validate_wrapped_decode::<A, PAD>(input, wrap)
}
pub fn validate_result(&self, input: &[u8]) -> Result<(), DecodeError> {
validate_decode::<A, PAD>(input).map(|_| ())
}
#[must_use]
pub fn validate(&self, input: &[u8]) -> bool {
self.validate_result(input).is_ok()
}
pub fn validate_legacy_result(&self, input: &[u8]) -> Result<(), DecodeError> {
validate_legacy_decode::<A, PAD>(input).map(|_| ())
}
#[must_use]
pub fn validate_legacy(&self, input: &[u8]) -> bool {
self.validate_legacy_result(input).is_ok()
}
pub fn validate_wrapped_result(&self, input: &[u8], wrap: LineWrap) -> Result<(), DecodeError> {
validate_wrapped_decode::<A, PAD>(input, wrap).map(|_| ())
}
#[must_use]
pub fn validate_wrapped(&self, input: &[u8], wrap: LineWrap) -> bool {
self.validate_wrapped_result(input, wrap).is_ok()
}
#[must_use]
pub const fn encode_array<const INPUT_LEN: usize, const OUTPUT_LEN: usize>(
&self,
input: &[u8; INPUT_LEN],
) -> [u8; OUTPUT_LEN] {
let Some(required) = checked_encoded_len(INPUT_LEN, PAD) else {
panic!("encoded base64 length overflows usize");
};
assert!(
required == OUTPUT_LEN,
"base64 output array has incorrect length"
);
let mut output = [0u8; OUTPUT_LEN];
let mut read = 0;
let mut write = 0;
while INPUT_LEN - read >= 3 {
let b0 = input[read];
let b1 = input[read + 1];
let b2 = input[read + 2];
output[write] = encode_base64_value::<A>(b0 >> 2);
output[write + 1] = encode_base64_value::<A>(((b0 & 0b0000_0011) << 4) | (b1 >> 4));
output[write + 2] = encode_base64_value::<A>(((b1 & 0b0000_1111) << 2) | (b2 >> 6));
output[write + 3] = encode_base64_value::<A>(b2 & 0b0011_1111);
read += 3;
write += 4;
}
match INPUT_LEN - read {
0 => {}
1 => {
let b0 = input[read];
output[write] = encode_base64_value::<A>(b0 >> 2);
output[write + 1] = encode_base64_value::<A>((b0 & 0b0000_0011) << 4);
write += 2;
if PAD {
output[write] = b'=';
output[write + 1] = b'=';
}
}
2 => {
let b0 = input[read];
let b1 = input[read + 1];
output[write] = encode_base64_value::<A>(b0 >> 2);
output[write + 1] = encode_base64_value::<A>(((b0 & 0b0000_0011) << 4) | (b1 >> 4));
output[write + 2] = encode_base64_value::<A>((b1 & 0b0000_1111) << 2);
if PAD {
output[write + 3] = b'=';
}
}
_ => unreachable!(),
}
output
}
pub fn encode_slice(&self, input: &[u8], output: &mut [u8]) -> Result<usize, EncodeError> {
scalar::encode_slice::<A, PAD>(input, output)
}
pub fn encode_slice_wrapped(
&self,
input: &[u8],
output: &mut [u8],
wrap: LineWrap,
) -> Result<usize, EncodeError> {
let required = self.wrapped_encoded_len(input.len(), wrap)?;
if output.len() < required {
return Err(EncodeError::OutputTooSmall {
required,
available: output.len(),
});
}
let encoded_len =
checked_encoded_len(input.len(), PAD).ok_or(EncodeError::LengthOverflow)?;
if encoded_len == 0 {
return Ok(0);
}
let combined_required = match required.checked_add(encoded_len) {
Some(len) => len,
None => usize::MAX,
};
if output.len() < combined_required {
let mut scratch = [0u8; 1024];
let mut input_offset = 0;
let mut output_offset = 0;
let mut column = 0;
while input_offset < input.len() {
let remaining = input.len() - input_offset;
let mut take = remaining.min(768);
if remaining > take {
take -= take % 3;
}
if take == 0 {
take = remaining;
}
let encoded = match self
.encode_slice(&input[input_offset..input_offset + take], &mut scratch)
{
Ok(encoded) => encoded,
Err(err) => {
wipe_bytes(&mut scratch);
return Err(err);
}
};
if let Err(err) = write_wrapped_bytes(
&scratch[..encoded],
output,
&mut output_offset,
&mut column,
wrap,
) {
wipe_bytes(&mut scratch);
return Err(err);
}
wipe_bytes(&mut scratch[..encoded]);
input_offset += take;
}
Ok(output_offset)
} else {
let encoded =
self.encode_slice(input, &mut output[required..required + encoded_len])?;
let mut output_offset = 0;
let mut column = 0;
let mut read = required;
while read < required + encoded {
let byte = output[read];
write_wrapped_byte(byte, output, &mut output_offset, &mut column, wrap)?;
read += 1;
}
wipe_bytes(&mut output[required..required + encoded]);
Ok(output_offset)
}
}
pub fn encode_slice_wrapped_clear_tail(
&self,
input: &[u8],
output: &mut [u8],
wrap: LineWrap,
) -> Result<usize, EncodeError> {
let written = match self.encode_slice_wrapped(input, output, wrap) {
Ok(written) => written,
Err(err) => {
wipe_bytes(output);
return Err(err);
}
};
wipe_tail(output, written);
Ok(written)
}
pub fn encode_wrapped_buffer<const CAP: usize>(
&self,
input: &[u8],
wrap: LineWrap,
) -> Result<EncodedBuffer<CAP>, EncodeError> {
let mut output = EncodedBuffer::new();
let written =
match self.encode_slice_wrapped_clear_tail(input, output.as_mut_capacity(), wrap) {
Ok(written) => written,
Err(err) => {
output.clear();
return Err(err);
}
};
output.set_filled(written)?;
Ok(output)
}
#[cfg(feature = "alloc")]
#[must_use = "for secret-bearing payloads use encode_wrapped_secret, which returns a redacted buffer with drop-time cleanup"]
pub fn encode_wrapped_vec(
&self,
input: &[u8],
wrap: LineWrap,
) -> Result<alloc::vec::Vec<u8>, EncodeError> {
let required = self.wrapped_encoded_len(input.len(), wrap)?;
let mut output = alloc::vec![0; required];
let written = self.encode_slice_wrapped(input, &mut output, wrap)?;
output.truncate(written);
Ok(output)
}
#[cfg(feature = "alloc")]
pub fn encode_wrapped_string(
&self,
input: &[u8],
wrap: LineWrap,
) -> Result<alloc::string::String, EncodeError> {
let output = self.encode_wrapped_vec(input, wrap)?;
match alloc::string::String::from_utf8(output) {
Ok(output) => Ok(output),
Err(_) => unreachable!("base64 encoder produced non-UTF-8 output"),
}
}
#[cfg(feature = "alloc")]
pub fn encode_wrapped_secret(
&self,
input: &[u8],
wrap: LineWrap,
) -> Result<SecretBuffer, EncodeError> {
self.encode_wrapped_vec(input, wrap)
.map(SecretBuffer::from_vec)
}
pub fn encode_slice_clear_tail(
&self,
input: &[u8],
output: &mut [u8],
) -> Result<usize, EncodeError> {
let written = match self.encode_slice(input, output) {
Ok(written) => written,
Err(err) => {
wipe_bytes(output);
return Err(err);
}
};
wipe_tail(output, written);
Ok(written)
}
pub fn encode_buffer<const CAP: usize>(
&self,
input: &[u8],
) -> Result<EncodedBuffer<CAP>, EncodeError> {
let mut output = EncodedBuffer::new();
let written = match self.encode_slice_clear_tail(input, output.as_mut_capacity()) {
Ok(written) => written,
Err(err) => {
output.clear();
return Err(err);
}
};
output.set_filled(written)?;
Ok(output)
}
#[cfg(feature = "alloc")]
#[must_use = "for secret-bearing payloads use encode_secret, which returns a redacted buffer with drop-time cleanup"]
pub fn encode_vec(&self, input: &[u8]) -> Result<alloc::vec::Vec<u8>, EncodeError> {
let required = checked_encoded_len(input.len(), PAD).ok_or(EncodeError::LengthOverflow)?;
let mut output = alloc::vec![0; required];
let written = self.encode_slice(input, &mut output)?;
output.truncate(written);
Ok(output)
}
#[cfg(feature = "alloc")]
pub fn encode_secret(&self, input: &[u8]) -> Result<SecretBuffer, EncodeError> {
self.encode_vec(input).map(SecretBuffer::from_vec)
}
#[cfg(feature = "alloc")]
pub fn encode_string(&self, input: &[u8]) -> Result<alloc::string::String, EncodeError> {
let output = self.encode_vec(input)?;
match alloc::string::String::from_utf8(output) {
Ok(output) => Ok(output),
Err(_) => unreachable!("base64 encoder produced non-UTF-8 output"),
}
}
pub fn encode_in_place<'a>(
&self,
buffer: &'a mut [u8],
input_len: usize,
) -> Result<&'a mut [u8], EncodeError> {
if input_len > buffer.len() {
return Err(EncodeError::InputTooLarge {
input_len,
buffer_len: buffer.len(),
});
}
let required = checked_encoded_len(input_len, PAD).ok_or(EncodeError::LengthOverflow)?;
if buffer.len() < required {
return Err(EncodeError::OutputTooSmall {
required,
available: buffer.len(),
});
}
let mut read = input_len;
let mut write = required;
match input_len % 3 {
0 => {}
1 => {
read -= 1;
let b0 = buffer[read];
if PAD {
write -= 4;
buffer[write] = encode_base64_value_runtime::<A>(b0 >> 2);
buffer[write + 1] = encode_base64_value_runtime::<A>((b0 & 0b0000_0011) << 4);
buffer[write + 2] = b'=';
buffer[write + 3] = b'=';
} else {
write -= 2;
buffer[write] = encode_base64_value_runtime::<A>(b0 >> 2);
buffer[write + 1] = encode_base64_value_runtime::<A>((b0 & 0b0000_0011) << 4);
}
}
2 => {
read -= 2;
let b0 = buffer[read];
let b1 = buffer[read + 1];
if PAD {
write -= 4;
buffer[write] = encode_base64_value_runtime::<A>(b0 >> 2);
buffer[write + 1] =
encode_base64_value_runtime::<A>(((b0 & 0b0000_0011) << 4) | (b1 >> 4));
buffer[write + 2] = encode_base64_value_runtime::<A>((b1 & 0b0000_1111) << 2);
buffer[write + 3] = b'=';
} else {
write -= 3;
buffer[write] = encode_base64_value_runtime::<A>(b0 >> 2);
buffer[write + 1] =
encode_base64_value_runtime::<A>(((b0 & 0b0000_0011) << 4) | (b1 >> 4));
buffer[write + 2] = encode_base64_value_runtime::<A>((b1 & 0b0000_1111) << 2);
}
}
_ => unreachable!(),
}
while read > 0 {
read -= 3;
write -= 4;
let b0 = buffer[read];
let b1 = buffer[read + 1];
let b2 = buffer[read + 2];
buffer[write] = encode_base64_value_runtime::<A>(b0 >> 2);
buffer[write + 1] =
encode_base64_value_runtime::<A>(((b0 & 0b0000_0011) << 4) | (b1 >> 4));
buffer[write + 2] =
encode_base64_value_runtime::<A>(((b1 & 0b0000_1111) << 2) | (b2 >> 6));
buffer[write + 3] = encode_base64_value_runtime::<A>(b2 & 0b0011_1111);
}
assert_eq!(
write, 0,
"encode_in_place invariant violated: right-to-left loop did not complete"
);
Ok(&mut buffer[..required])
}
pub fn encode_in_place_clear_tail<'a>(
&self,
buffer: &'a mut [u8],
input_len: usize,
) -> Result<&'a mut [u8], EncodeError> {
let len = match self.encode_in_place(buffer, input_len) {
Ok(encoded) => encoded.len(),
Err(err) => {
wipe_bytes(buffer);
return Err(err);
}
};
wipe_tail(buffer, len);
Ok(&mut buffer[..len])
}
#[must_use = "handle decode errors; use crate::ct for secret-bearing payloads"]
pub fn decode_slice(&self, input: &[u8], output: &mut [u8]) -> Result<usize, DecodeError> {
scalar::decode_slice::<A, PAD>(input, output)
}
pub fn decode_slice_clear_tail(
&self,
input: &[u8],
output: &mut [u8],
) -> Result<usize, DecodeError> {
let written = match self.decode_slice(input, output) {
Ok(written) => written,
Err(err) => {
wipe_bytes(output);
return Err(err);
}
};
wipe_tail(output, written);
Ok(written)
}
pub fn decode_buffer<const CAP: usize>(
&self,
input: &[u8],
) -> Result<DecodedBuffer<CAP>, DecodeError> {
let mut output = DecodedBuffer::new();
let written = match self.decode_slice_clear_tail(input, output.as_mut_capacity()) {
Ok(written) => written,
Err(err) => {
output.clear();
return Err(err);
}
};
output.set_filled(written)?;
Ok(output)
}
#[must_use = "handle decode errors; use crate::ct for secret-bearing payloads"]
pub fn decode_slice_legacy(
&self,
input: &[u8],
output: &mut [u8],
) -> Result<usize, DecodeError> {
let required = validate_legacy_decode::<A, PAD>(input)?;
if output.len() < required {
return Err(DecodeError::OutputTooSmall {
required,
available: output.len(),
});
}
decode_legacy_to_slice::<A, PAD>(input, output)
}
pub fn decode_slice_legacy_clear_tail(
&self,
input: &[u8],
output: &mut [u8],
) -> Result<usize, DecodeError> {
let written = match self.decode_slice_legacy(input, output) {
Ok(written) => written,
Err(err) => {
wipe_bytes(output);
return Err(err);
}
};
wipe_tail(output, written);
Ok(written)
}
pub fn decode_buffer_legacy<const CAP: usize>(
&self,
input: &[u8],
) -> Result<DecodedBuffer<CAP>, DecodeError> {
let mut output = DecodedBuffer::new();
let written = match self.decode_slice_legacy_clear_tail(input, output.as_mut_capacity()) {
Ok(written) => written,
Err(err) => {
output.clear();
return Err(err);
}
};
output.set_filled(written)?;
Ok(output)
}
#[must_use = "handle decode errors; use crate::ct for secret-bearing payloads"]
pub fn decode_slice_wrapped(
&self,
input: &[u8],
output: &mut [u8],
wrap: LineWrap,
) -> Result<usize, DecodeError> {
let required = validate_wrapped_decode::<A, PAD>(input, wrap)?;
if output.len() < required {
return Err(DecodeError::OutputTooSmall {
required,
available: output.len(),
});
}
decode_wrapped_to_slice::<A, PAD>(input, output, wrap)
}
pub fn decode_slice_wrapped_clear_tail(
&self,
input: &[u8],
output: &mut [u8],
wrap: LineWrap,
) -> Result<usize, DecodeError> {
let written = match self.decode_slice_wrapped(input, output, wrap) {
Ok(written) => written,
Err(err) => {
wipe_bytes(output);
return Err(err);
}
};
wipe_tail(output, written);
Ok(written)
}
pub fn decode_wrapped_buffer<const CAP: usize>(
&self,
input: &[u8],
wrap: LineWrap,
) -> Result<DecodedBuffer<CAP>, DecodeError> {
let mut output = DecodedBuffer::new();
let written =
match self.decode_slice_wrapped_clear_tail(input, output.as_mut_capacity(), wrap) {
Ok(written) => written,
Err(err) => {
output.clear();
return Err(err);
}
};
output.set_filled(written)?;
Ok(output)
}
#[cfg(feature = "alloc")]
#[must_use = "for secret-bearing payloads use decode_secret, which returns a redacted buffer with drop-time cleanup"]
pub fn decode_vec(&self, input: &[u8]) -> Result<alloc::vec::Vec<u8>, DecodeError> {
let required = validate_decode::<A, PAD>(input)?;
let mut output = alloc::vec![0; required];
let written = match self.decode_slice(input, &mut output) {
Ok(written) => written,
Err(err) => {
wipe_bytes(&mut output);
return Err(err);
}
};
output.truncate(written);
Ok(output)
}
#[cfg(feature = "alloc")]
pub fn decode_secret(&self, input: &[u8]) -> Result<SecretBuffer, DecodeError> {
self.decode_vec(input).map(SecretBuffer::from_vec)
}
#[cfg(feature = "alloc")]
#[must_use = "for secret-bearing payloads use decode_secret_legacy, which returns a redacted buffer with drop-time cleanup"]
pub fn decode_vec_legacy(&self, input: &[u8]) -> Result<alloc::vec::Vec<u8>, DecodeError> {
let required = validate_legacy_decode::<A, PAD>(input)?;
let mut output = alloc::vec![0; required];
let written = match self.decode_slice_legacy(input, &mut output) {
Ok(written) => written,
Err(err) => {
wipe_bytes(&mut output);
return Err(err);
}
};
output.truncate(written);
Ok(output)
}
#[cfg(feature = "alloc")]
pub fn decode_secret_legacy(&self, input: &[u8]) -> Result<SecretBuffer, DecodeError> {
self.decode_vec_legacy(input).map(SecretBuffer::from_vec)
}
#[cfg(feature = "alloc")]
#[must_use = "for secret-bearing payloads use decode_wrapped_secret, which returns a redacted buffer with drop-time cleanup"]
pub fn decode_wrapped_vec(
&self,
input: &[u8],
wrap: LineWrap,
) -> Result<alloc::vec::Vec<u8>, DecodeError> {
let required = validate_wrapped_decode::<A, PAD>(input, wrap)?;
let mut output = alloc::vec![0; required];
let written = match self.decode_slice_wrapped(input, &mut output, wrap) {
Ok(written) => written,
Err(err) => {
wipe_bytes(&mut output);
return Err(err);
}
};
output.truncate(written);
Ok(output)
}
#[cfg(feature = "alloc")]
pub fn decode_wrapped_secret(
&self,
input: &[u8],
wrap: LineWrap,
) -> Result<SecretBuffer, DecodeError> {
self.decode_wrapped_vec(input, wrap)
.map(SecretBuffer::from_vec)
}
pub fn decode_in_place_wrapped<'a>(
&self,
buffer: &'a mut [u8],
wrap: LineWrap,
) -> Result<&'a mut [u8], DecodeError> {
let _required = validate_wrapped_decode::<A, PAD>(buffer, wrap)?;
let compacted = compact_wrapped_input(buffer, wrap)?;
let len = Self::decode_slice_to_start(&mut buffer[..compacted])?;
Ok(&mut buffer[..len])
}
pub fn decode_in_place_wrapped_clear_tail<'a>(
&self,
buffer: &'a mut [u8],
wrap: LineWrap,
) -> Result<&'a mut [u8], DecodeError> {
if let Err(err) = validate_wrapped_decode::<A, PAD>(buffer, wrap) {
wipe_bytes(buffer);
return Err(err);
}
let compacted = match compact_wrapped_input(buffer, wrap) {
Ok(compacted) => compacted,
Err(err) => {
wipe_bytes(buffer);
return Err(err);
}
};
let len = match Self::decode_slice_to_start(&mut buffer[..compacted]) {
Ok(len) => len,
Err(err) => {
wipe_bytes(buffer);
return Err(err);
}
};
wipe_tail(buffer, len);
Ok(&mut buffer[..len])
}
pub fn decode_in_place<'a>(&self, buffer: &'a mut [u8]) -> Result<&'a mut [u8], DecodeError> {
let len = Self::decode_slice_to_start(buffer)?;
Ok(&mut buffer[..len])
}
pub fn decode_in_place_clear_tail<'a>(
&self,
buffer: &'a mut [u8],
) -> Result<&'a mut [u8], DecodeError> {
let len = match Self::decode_slice_to_start(buffer) {
Ok(len) => len,
Err(err) => {
wipe_bytes(buffer);
return Err(err);
}
};
wipe_tail(buffer, len);
Ok(&mut buffer[..len])
}
pub fn decode_in_place_legacy<'a>(
&self,
buffer: &'a mut [u8],
) -> Result<&'a mut [u8], DecodeError> {
let _required = validate_legacy_decode::<A, PAD>(buffer)?;
let mut write = 0;
let mut read = 0;
while read < buffer.len() {
let byte = buffer[read];
if !is_legacy_whitespace(byte) {
buffer[write] = byte;
write += 1;
}
read += 1;
}
let len = Self::decode_slice_to_start(&mut buffer[..write])?;
Ok(&mut buffer[..len])
}
pub fn decode_in_place_legacy_clear_tail<'a>(
&self,
buffer: &'a mut [u8],
) -> Result<&'a mut [u8], DecodeError> {
if let Err(err) = validate_legacy_decode::<A, PAD>(buffer) {
wipe_bytes(buffer);
return Err(err);
}
let mut write = 0;
let mut read = 0;
while read < buffer.len() {
let byte = buffer[read];
if !is_legacy_whitespace(byte) {
buffer[write] = byte;
write += 1;
}
read += 1;
}
let len = match Self::decode_slice_to_start(&mut buffer[..write]) {
Ok(len) => len,
Err(err) => {
wipe_bytes(buffer);
return Err(err);
}
};
wipe_tail(buffer, len);
Ok(&mut buffer[..len])
}
fn decode_slice_to_start(buffer: &mut [u8]) -> Result<usize, DecodeError> {
let _required = validate_decode::<A, PAD>(buffer)?;
let input_len = buffer.len();
let mut read = 0;
let mut write = 0;
while read + 4 <= input_len {
let chunk = read_quad(buffer, read)?;
let available = buffer.len();
let output_tail = buffer.get_mut(write..).ok_or(DecodeError::OutputTooSmall {
required: write,
available,
})?;
let written = decode_chunk::<A, PAD>(chunk, output_tail)
.map_err(|err| err.with_index_offset(read))?;
read += 4;
write += written;
if written < 3 {
if read != input_len {
return Err(DecodeError::InvalidPadding { index: read - 4 });
}
return Ok(write);
}
}
let rem = input_len - read;
if rem == 0 {
return Ok(write);
}
if PAD {
return Err(DecodeError::InvalidLength);
}
let mut tail = [0u8; 3];
tail[..rem].copy_from_slice(&buffer[read..input_len]);
decode_tail_unpadded::<A>(&tail[..rem], &mut buffer[write..])
.map_err(|err| err.with_index_offset(read))
.map(|n| write + n)
}
}
#[cfg(kani)]
mod kani_proofs {
use super::{
STANDARD, Standard, checked_encoded_len, ct, decode_byte, decode_chunk,
decode_tail_unpadded, decoded_capacity, validate_tail_unpadded,
};
#[kani::proof]
fn checked_encoded_len_is_bounded_for_small_inputs() {
let len = usize::from(kani::any::<u8>());
let padded = kani::any::<bool>();
let encoded = checked_encoded_len(len, padded).expect("u8 input length cannot overflow");
assert!(encoded >= len);
assert!(encoded <= len / 3 * 4 + 4);
}
#[kani::proof]
fn decoded_capacity_is_bounded_for_small_inputs() {
let len = usize::from(kani::any::<u8>());
let capacity = decoded_capacity(len);
assert!(capacity <= len / 4 * 3 + 2);
}
#[kani::proof]
#[kani::unwind(70)]
fn standard_in_place_decode_returns_prefix_within_buffer() {
let mut buffer = kani::any::<[u8; 8]>();
let result = STANDARD.decode_in_place(&mut buffer);
if let Ok(decoded) = result {
assert!(decoded.len() <= 8);
}
}
#[kani::proof]
#[kani::unwind(70)]
fn standard_decode_slice_returns_written_within_output() {
let input = kani::any::<[u8; 4]>();
let mut output = kani::any::<[u8; 3]>();
let result = STANDARD.decode_slice(&input, &mut output);
if let Ok(written) = result {
assert!(written <= output.len());
}
}
#[kani::proof]
#[kani::unwind(70)]
fn standard_decode_chunk_returns_written_within_output() {
let input = kani::any::<[u8; 4]>();
let mut output = kani::any::<[u8; 3]>();
let result = decode_chunk::<Standard, true>(input, &mut output);
if let Ok(written) = result {
assert!(written <= output.len());
assert!(written <= 3);
}
}
#[kani::proof]
#[kani::unwind(70)]
fn standard_decode_chunk_bit_packing_matches_decoded_values() {
let input = kani::any::<[u8; 4]>();
let mut output = kani::any::<[u8; 3]>();
let result = decode_chunk::<Standard, true>(input, &mut output);
if let Ok(written) = result {
let v0 = decode_byte::<Standard>(input[0], 0).expect("successful chunk has v0");
let v1 = decode_byte::<Standard>(input[1], 1).expect("successful chunk has v1");
assert!(output[0] == ((v0 << 2) | (v1 >> 4)));
if written >= 2 {
let v2 = decode_byte::<Standard>(input[2], 2).expect("successful chunk has v2");
assert!(output[1] == ((v1 << 4) | (v2 >> 2)));
}
if written == 3 {
let v2 = decode_byte::<Standard>(input[2], 2).expect("successful chunk has v2");
let v3 = decode_byte::<Standard>(input[3], 3).expect("successful chunk has v3");
assert!(output[2] == ((v2 << 6) | v3));
}
}
}
#[kani::proof]
#[kani::unwind(70)]
fn standard_validate_tail_unpadded_accepts_or_rejects_without_panic() {
let input = kani::any::<[u8; 3]>();
let len = usize::from(kani::any::<u8>() % 4);
let result = validate_tail_unpadded::<Standard>(&input[..len]);
if result.is_ok() {
assert!(len == 0 || len == 2 || len == 3);
}
}
#[kani::proof]
#[kani::unwind(70)]
fn standard_decode_two_byte_tail_returns_written_within_output() {
let input = kani::any::<[u8; 2]>();
let mut output = kani::any::<[u8; 1]>();
let result = decode_tail_unpadded::<Standard>(&input, &mut output);
if let Ok(written) = result {
assert!(written <= output.len());
assert!(written == 1);
}
}
#[kani::proof]
#[kani::unwind(70)]
fn standard_decode_three_byte_tail_returns_written_within_output() {
let input = kani::any::<[u8; 3]>();
let mut output = kani::any::<[u8; 2]>();
let result = decode_tail_unpadded::<Standard>(&input, &mut output);
if let Ok(written) = result {
assert!(written <= output.len());
assert!(written == 2);
}
}
#[kani::proof]
#[kani::unwind(70)]
fn standard_decode_slice_clear_tail_clears_output_on_error() {
let input = kani::any::<[u8; 4]>();
let mut output = kani::any::<[u8; 3]>();
let result = STANDARD.decode_slice_clear_tail(&input, &mut output);
if result.is_err() {
assert!(output.iter().all(|byte| *byte == 0));
}
}
#[kani::proof]
#[kani::unwind(70)]
fn standard_encode_slice_returns_written_within_output() {
let input = kani::any::<[u8; 3]>();
let mut output = kani::any::<[u8; 4]>();
let result = STANDARD.encode_slice(&input, &mut output);
if let Ok(written) = result {
assert!(written <= output.len());
}
}
#[kani::proof]
#[kani::unwind(70)]
fn standard_encode_in_place_returns_prefix_within_buffer() {
let mut buffer = kani::any::<[u8; 8]>();
let input_len = usize::from(kani::any::<u8>() % 9);
let result = STANDARD.encode_in_place(&mut buffer, input_len);
if let Ok(encoded) = result {
assert!(encoded.len() <= 8);
}
}
#[kani::proof]
#[kani::unwind(70)]
fn standard_clear_tail_decode_clears_buffer_on_error() {
let mut buffer = kani::any::<[u8; 4]>();
let result = STANDARD.decode_in_place_clear_tail(&mut buffer);
if result.is_err() {
assert!(buffer.iter().all(|byte| *byte == 0));
}
}
#[kani::proof]
#[kani::unwind(70)]
fn ct_standard_decode_slice_returns_written_within_output() {
let input = kani::any::<[u8; 4]>();
let mut output = kani::any::<[u8; 3]>();
let result = ct::STANDARD.decode_slice_clear_tail(&input, &mut output);
if let Ok(written) = result {
assert!(written <= output.len());
}
}
#[kani::proof]
#[kani::unwind(70)]
fn ct_standard_decode_slice_clear_tail_clears_output_on_error() {
let input = kani::any::<[u8; 4]>();
let mut output = kani::any::<[u8; 3]>();
let result = ct::STANDARD.decode_slice_clear_tail(&input, &mut output);
if result.is_err() {
assert!(output.iter().all(|byte| *byte == 0));
}
}
#[kani::proof]
#[kani::unwind(70)]
fn ct_standard_decode_in_place_clear_tail_clears_buffer_on_error() {
let mut buffer = kani::any::<[u8; 4]>();
let result = ct::STANDARD.decode_in_place_clear_tail(&mut buffer);
if result.is_err() {
assert!(buffer.iter().all(|byte| *byte == 0));
}
}
#[kani::proof]
#[kani::unwind(70)]
fn ct_standard_validate_matches_decode_for_one_quantum() {
let input = kani::any::<[u8; 4]>();
let mut output = kani::any::<[u8; 3]>();
let validate_ok = ct::STANDARD.validate_result(&input).is_ok();
let decode_ok = ct::STANDARD
.decode_slice_clear_tail(&input, &mut output)
.is_ok();
assert!(validate_ok == decode_ok);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fill_pattern(output: &mut [u8], seed: usize) {
for (index, byte) in output.iter_mut().enumerate() {
let value = (index * 73 + seed * 19) % 256;
*byte = u8::try_from(value).unwrap();
}
}
fn assert_encode_backend_matches_scalar<A, const PAD: bool>(input: &[u8])
where
A: Alphabet,
{
let engine = Engine::<A, PAD>::new();
let mut dispatched = [0x55; 256];
let mut scalar = [0xaa; 256];
let dispatched_result = engine.encode_slice(input, &mut dispatched);
let scalar_result = scalar::scalar_reference_encode_slice::<A, PAD>(input, &mut scalar);
assert_eq!(dispatched_result, scalar_result);
if let Ok(written) = dispatched_result {
assert_eq!(&dispatched[..written], &scalar[..written]);
}
let required = checked_encoded_len(input.len(), PAD).unwrap();
if required > 0 {
let mut dispatched_short = [0x55; 256];
let mut scalar_short = [0xaa; 256];
let available = required - 1;
assert_eq!(
engine.encode_slice(input, &mut dispatched_short[..available]),
scalar::scalar_reference_encode_slice::<A, PAD>(
input,
&mut scalar_short[..available],
)
);
}
}
fn assert_decode_backend_matches_scalar<A, const PAD: bool>(input: &[u8])
where
A: Alphabet,
{
let engine = Engine::<A, PAD>::new();
let mut dispatched = [0x55; 128];
let mut scalar = [0xaa; 128];
let dispatched_result = engine.decode_slice(input, &mut dispatched);
let scalar_result = scalar::scalar_reference_decode_slice::<A, PAD>(input, &mut scalar);
assert_eq!(dispatched_result, scalar_result);
if let Ok(written) = dispatched_result {
assert_eq!(&dispatched[..written], &scalar[..written]);
if written > 0 {
let mut dispatched_short = [0x55; 128];
let mut scalar_short = [0xaa; 128];
let available = written - 1;
assert_eq!(
engine.decode_slice(input, &mut dispatched_short[..available]),
scalar::scalar_reference_decode_slice::<A, PAD>(
input,
&mut scalar_short[..available],
)
);
}
}
}
fn assert_backend_round_trip_matches_scalar<A, const PAD: bool>(input: &[u8])
where
A: Alphabet,
{
assert_encode_backend_matches_scalar::<A, PAD>(input);
let mut encoded = [0; 256];
let encoded_len =
scalar::scalar_reference_encode_slice::<A, PAD>(input, &mut encoded).unwrap();
assert_decode_backend_matches_scalar::<A, PAD>(&encoded[..encoded_len]);
}
fn assert_standard_decode_chunk_matches_input(input: &[u8]) {
let mut encoded = [0u8; 4];
let encoded_len = STANDARD.encode_slice(input, &mut encoded).unwrap();
assert_eq!(encoded_len, 4);
let chunk = [encoded[0], encoded[1], encoded[2], encoded[3]];
let mut decoded = [0u8; 3];
let decoded_len = decode_chunk::<Standard, true>(chunk, &mut decoded).unwrap();
assert_eq!(decoded_len, input.len());
assert_eq!(&decoded[..decoded_len], input);
}
#[test]
fn backend_dispatch_matches_scalar_reference_for_canonical_inputs() {
let mut input = [0; 128];
for input_len in 0..=input.len() {
fill_pattern(&mut input[..input_len], input_len);
let input = &input[..input_len];
assert_backend_round_trip_matches_scalar::<Standard, true>(input);
assert_backend_round_trip_matches_scalar::<Standard, false>(input);
assert_backend_round_trip_matches_scalar::<UrlSafe, true>(input);
assert_backend_round_trip_matches_scalar::<UrlSafe, false>(input);
}
}
#[test]
fn backend_dispatch_matches_scalar_reference_for_malformed_inputs() {
for input in [
&b"Z"[..],
b"====",
b"AA=A",
b"Zh==",
b"Zm9=",
b"Zm9v$g==",
b"Zm9vZh==",
] {
assert_decode_backend_matches_scalar::<Standard, true>(input);
}
for input in [&b"Z"[..], b"AA=A", b"Zh", b"Zm9", b"Zm9vYg$"] {
assert_decode_backend_matches_scalar::<Standard, false>(input);
}
assert_decode_backend_matches_scalar::<UrlSafe, true>(b"AA+A");
assert_decode_backend_matches_scalar::<UrlSafe, false>(b"AA/A");
assert_decode_backend_matches_scalar::<Standard, true>(b"AA-A");
assert_decode_backend_matches_scalar::<Standard, false>(b"AA_A");
}
#[test]
fn decode_chunk_bit_packing_matches_exhaustive_small_inputs() {
for byte in u8::MIN..=u8::MAX {
assert_standard_decode_chunk_matches_input(&[byte]);
}
for first in u8::MIN..=u8::MAX {
for second in u8::MIN..=u8::MAX {
assert_standard_decode_chunk_matches_input(&[first, second]);
}
}
}
#[test]
fn decode_chunk_bit_packing_matches_representative_full_quanta() {
const SAMPLES: [u8; 16] = [
0, 1, 2, 15, 16, 31, 32, 63, 64, 95, 127, 128, 191, 192, 254, 255,
];
for first in SAMPLES {
for second in SAMPLES {
for third in SAMPLES {
assert_standard_decode_chunk_matches_input(&[first, second, third]);
}
}
}
}
#[test]
fn ct_padded_final_quantum_fails_closed_for_invalid_padding_count() {
let (_, invalid_byte, invalid_padding, written) =
ct_padded_final_quantum::<Standard>(*b"ABCD", 3);
assert_ne!(invalid_byte, 0);
assert_ne!(invalid_padding, 0);
assert_eq!(written, 0);
assert_eq!(
report_ct_error(invalid_byte, invalid_padding),
Err(DecodeError::InvalidInput)
);
}
#[cfg(feature = "simd")]
#[test]
fn simd_dispatch_scaffold_keeps_scalar_active() {
assert_eq!(simd::active_backend(), simd::ActiveBackend::Scalar);
let _candidate = simd::detected_candidate();
}
#[test]
fn encodes_standard_vectors() {
let vectors = [
(&b""[..], &b""[..]),
(&b"f"[..], &b"Zg=="[..]),
(&b"fo"[..], &b"Zm8="[..]),
(&b"foo"[..], &b"Zm9v"[..]),
(&b"foob"[..], &b"Zm9vYg=="[..]),
(&b"fooba"[..], &b"Zm9vYmE="[..]),
(&b"foobar"[..], &b"Zm9vYmFy"[..]),
];
for (input, expected) in vectors {
let mut output = [0u8; 16];
let written = STANDARD.encode_slice(input, &mut output).unwrap();
assert_eq!(&output[..written], expected);
}
}
#[test]
fn decodes_standard_vectors() {
let vectors = [
(&b""[..], &b""[..]),
(&b"Zg=="[..], &b"f"[..]),
(&b"Zm8="[..], &b"fo"[..]),
(&b"Zm9v"[..], &b"foo"[..]),
(&b"Zm9vYg=="[..], &b"foob"[..]),
(&b"Zm9vYmE="[..], &b"fooba"[..]),
(&b"Zm9vYmFy"[..], &b"foobar"[..]),
];
for (input, expected) in vectors {
let mut output = [0u8; 16];
let written = STANDARD.decode_slice(input, &mut output).unwrap();
assert_eq!(&output[..written], expected);
}
}
#[test]
fn supports_unpadded_url_safe() {
let mut encoded = [0u8; 16];
let written = URL_SAFE_NO_PAD
.encode_slice(b"\xfb\xff", &mut encoded)
.unwrap();
assert_eq!(&encoded[..written], b"-_8");
let mut decoded = [0u8; 2];
let written = URL_SAFE_NO_PAD
.decode_slice(&encoded[..written], &mut decoded)
.unwrap();
assert_eq!(&decoded[..written], b"\xfb\xff");
}
#[test]
fn decodes_in_place() {
let mut buffer = *b"Zm9vYmFy";
let decoded = STANDARD_NO_PAD.decode_in_place(&mut buffer).unwrap();
assert_eq!(decoded, b"foobar");
}
#[test]
fn rejects_non_canonical_padding_bits() {
let mut output = [0u8; 4];
assert_eq!(
STANDARD.decode_slice(b"Zh==", &mut output),
Err(DecodeError::InvalidPadding { index: 1 })
);
assert_eq!(
STANDARD.decode_slice(b"Zm9=", &mut output),
Err(DecodeError::InvalidPadding { index: 2 })
);
}
}