Skip to content

Commit

Permalink
base64ct: bcrypt encoding (#237)
Browse files Browse the repository at this point in the history
Adds the Base64 variant used by bcrypt
  • Loading branch information
tarcieri committed Jan 31, 2021
1 parent ff20f14 commit 094bfc6
Show file tree
Hide file tree
Showing 12 changed files with 589 additions and 390 deletions.
132 changes: 67 additions & 65 deletions base64ct/src/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ use alloc::vec::Vec;

/// Decode the provided Base64 string into the provided destination buffer.
#[inline(always)]
pub(crate) fn decode(
pub(crate) fn decode<F>(
src: impl AsRef<[u8]>,
dst: &mut [u8],
padded: bool,
hi_bytes: (u8, u8),
) -> Result<&[u8], Error> {
decode_6bits: F,
) -> Result<&[u8], Error>
where
F: Fn(u8) -> i16 + Copy,
{
let mut src = src.as_ref();

let mut err = if padded {
Expand All @@ -35,7 +38,7 @@ pub(crate) fn decode(
let mut src_chunks = src.chunks_exact(4);
let mut dst_chunks = dst.chunks_exact_mut(3);
for (s, d) in (&mut src_chunks).zip(&mut dst_chunks) {
err |= decode_3bytes(s, d, hi_bytes);
err |= decode_3bytes(s, d, decode_6bits);
}
let src_rem = src_chunks.remainder();
let dst_rem = dst_chunks.into_remainder();
Expand All @@ -44,7 +47,7 @@ pub(crate) fn decode(
let mut tmp_out = [0u8; 3];
let mut tmp_in = [b'A'; 4];
tmp_in[..src_rem.len()].copy_from_slice(src_rem);
err |= decode_3bytes(&tmp_in, &mut tmp_out, hi_bytes);
err |= decode_3bytes(&tmp_in, &mut tmp_out, decode_6bits);
dst_rem.copy_from_slice(&tmp_out[..dst_rem.len()]);

if err == 0 {
Expand All @@ -56,11 +59,14 @@ pub(crate) fn decode(

/// Decode Base64-encoded string in-place.
#[inline(always)]
pub(crate) fn decode_in_place(
pub(crate) fn decode_in_place<F>(
mut buf: &mut [u8],
padded: bool,
hi_bytes: (u8, u8),
) -> Result<&[u8], InvalidEncodingError> {
decode_6bits: F,
) -> Result<&[u8], InvalidEncodingError>
where
F: Fn(u8) -> i16 + Copy,
{
// TODO: eliminate unsafe code when compiler will be smart enough to
// eliminate bound checks, see: https://github.com/rust-lang/rust/issues/80963
let mut err = if padded {
Expand All @@ -86,7 +92,7 @@ pub(crate) fn decode_in_place(
let p4 = buf.as_ptr().add(4 * chunk) as *const [u8; 4];

let mut tmp_out = [0u8; 3];
err |= decode_3bytes(&*p4, &mut tmp_out, hi_bytes);
err |= decode_3bytes(&*p4, &mut tmp_out, decode_6bits);
*p3 = tmp_out;
}
}
Expand All @@ -101,7 +107,7 @@ pub(crate) fn decode_in_place(
tmp_in[..src_rem_len].copy_from_slice(&buf[src_rem_pos..]);
let mut tmp_out = [0u8; 3];

err |= decode_3bytes(&tmp_in, &mut tmp_out, hi_bytes);
err |= decode_3bytes(&tmp_in, &mut tmp_out, decode_6bits);

if err == 0 {
// SAFETY: `dst_rem_len` is always smaller than 4, so we don't
Expand All @@ -128,9 +134,12 @@ pub(crate) fn decode_in_place(
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
#[inline(always)]
pub(crate) fn decode_vec(input: &str, padded: bool, hi_bytes: (u8, u8)) -> Result<Vec<u8>, Error> {
pub(crate) fn decode_vec<F>(input: &str, padded: bool, decode_6bits: F) -> Result<Vec<u8>, Error>
where
F: Fn(u8) -> i16 + Copy,
{
let mut output = vec![0u8; decoded_len(input.len())];
let len = decode(input, &mut output, padded, hi_bytes)?.len();
let len = decode(input, &mut output, padded, decode_6bits)?.len();

if len <= output.len() {
output.truncate(len);
Expand All @@ -140,42 +149,6 @@ pub(crate) fn decode_vec(input: &str, padded: bool, hi_bytes: (u8, u8)) -> Resul
}
}

/// Validate padding is well-formed and compute unpadded length.
///
/// Returns length-related errors eagerly as a [`Result`], and data-dependent
/// errors (i.e. malformed padding bytes) as `i16` to be combined with other
/// encoding-related errors prior to branching.
#[inline(always)]
fn decode_padding(input: &[u8]) -> Result<(usize, i16), InvalidEncodingError> {
if input.len() % 4 != 0 {
return Err(InvalidEncodingError);
}

let unpadded_len = match *input {
[.., b0, b1] => {
let pad_len = match_eq_ct(b0, PAD, 1) + match_eq_ct(b1, PAD, 1);
input.len() - pad_len as usize
}
_ => input.len(),
};

let padding_len = input.len() - unpadded_len;

let err = match *input {
[.., b0] if padding_len == 1 => match_eq_ct(b0, PAD, 1) ^ 1,
[.., b0, b1] if padding_len == 2 => (match_eq_ct(b0, PAD, 1) & match_eq_ct(b1, PAD, 1)) ^ 1,
_ => {
if padding_len == 0 {
0
} else {
return Err(InvalidEncodingError);
}
}
};

Ok((unpadded_len, err))
}

/// Get the length of the output from decoding the provided *unpadded*
/// Base64-encoded input (use [`unpadded_len_ct`] to compute this value for
/// a padded input)
Expand All @@ -191,14 +164,17 @@ fn decoded_len(input_len: usize) -> usize {
}

#[inline(always)]
fn decode_3bytes(src: &[u8], dst: &mut [u8], hi_bytes: (u8, u8)) -> i16 {
fn decode_3bytes<F>(src: &[u8], dst: &mut [u8], decode_6bits: F) -> i16
where
F: Fn(u8) -> i16 + Copy,
{
debug_assert_eq!(src.len(), 4);
debug_assert!(dst.len() >= 3, "dst too short: {}", dst.len());

let c0 = decode_6bits(src[0], hi_bytes);
let c1 = decode_6bits(src[1], hi_bytes);
let c2 = decode_6bits(src[2], hi_bytes);
let c3 = decode_6bits(src[3], hi_bytes);
let c0 = decode_6bits(src[0]);
let c1 = decode_6bits(src[1]);
let c2 = decode_6bits(src[2]);
let c3 = decode_6bits(src[3]);

dst[0] = ((c0 << 2) | (c1 >> 4)) as u8;
dst[1] = ((c1 << 4) | (c2 >> 2)) as u8;
Expand All @@ -207,19 +183,9 @@ fn decode_3bytes(src: &[u8], dst: &mut [u8], hi_bytes: (u8, u8)) -> i16 {
((c0 | c1 | c2 | c3) >> 8) & 1
}

#[inline(always)]
fn decode_6bits(src: u8, hi_bytes: (u8, u8)) -> i16 {
let mut res: i16 = -1;
res += match_range_ct(src, 0x41..0x5a, src as i16 - 64);
res += match_range_ct(src, 0x61..0x7a, src as i16 - 70);
res += match_range_ct(src, 0x30..0x39, src as i16 + 5);
res += match_eq_ct(src, hi_bytes.0, 63);
res + match_eq_ct(src, hi_bytes.1, 64)
}

/// Match that a byte falls within a provided range.
#[inline(always)]
fn match_range_ct(input: u8, range: Range<u8>, ret_on_match: i16) -> i16 {
pub(crate) fn match_range_ct(input: u8, range: Range<u8>, ret_on_match: i16) -> i16 {
// Compute exclusive range from inclusive one
let start = range.start as i16 - 1;
let end = range.end as i16 + 1;
Expand All @@ -229,6 +195,42 @@ fn match_range_ct(input: u8, range: Range<u8>, ret_on_match: i16) -> i16 {

/// Match a a byte equals a specified value.
#[inline(always)]
fn match_eq_ct(input: u8, expected: u8, ret_on_match: i16) -> i16 {
pub(crate) fn match_eq_ct(input: u8, expected: u8, ret_on_match: i16) -> i16 {
match_range_ct(input, expected..expected, ret_on_match)
}

/// Validate padding is well-formed and compute unpadded length.
///
/// Returns length-related errors eagerly as a [`Result`], and data-dependent
/// errors (i.e. malformed padding bytes) as `i16` to be combined with other
/// encoding-related errors prior to branching.
#[inline(always)]
fn decode_padding(input: &[u8]) -> Result<(usize, i16), InvalidEncodingError> {
if input.len() % 4 != 0 {
return Err(InvalidEncodingError);
}

let unpadded_len = match *input {
[.., b0, b1] => {
let pad_len = match_eq_ct(b0, PAD, 1) + match_eq_ct(b1, PAD, 1);
input.len() - pad_len as usize
}
_ => input.len(),
};

let padding_len = input.len() - unpadded_len;

let err = match *input {
[.., b0] if padding_len == 1 => match_eq_ct(b0, PAD, 1) ^ 1,
[.., b0, b1] if padding_len == 2 => (match_eq_ct(b0, PAD, 1) & match_eq_ct(b1, PAD, 1)) ^ 1,
_ => {
if padding_len == 0 {
0
} else {
return Err(InvalidEncodingError);
}
}
};

Ok((unpadded_len, err))
}
14 changes: 7 additions & 7 deletions base64ct/src/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,13 @@ where
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
#[inline(always)]
pub(crate) fn encode_string<F>(input: &[u8], padded: bool, f: F) -> String
pub(crate) fn encode_string<F>(input: &[u8], padded: bool, encode_6bits: F) -> String
where
F: Fn(i16) -> u8 + Copy,
{
let elen = encoded_len_inner(input.len(), padded).expect("input is too big");
let mut dst = vec![0u8; elen];
let res = encode(input, &mut dst, padded, f).expect("encoding error");
let res = encode(input, &mut dst, padded, encode_6bits).expect("encoding error");

debug_assert_eq!(elen, res.len());
debug_assert!(str::from_utf8(&dst).is_ok());
Expand All @@ -116,7 +116,7 @@ where
}

#[inline(always)]
fn encode_3bytes<F>(src: &[u8], dst: &mut [u8], f: F)
fn encode_3bytes<F>(src: &[u8], dst: &mut [u8], encode_6bits: F)
where
F: Fn(i16) -> u8 + Copy,
{
Expand All @@ -127,10 +127,10 @@ where
let b1 = src[1] as i16;
let b2 = src[2] as i16;

dst[0] = f(b0 >> 2);
dst[1] = f(((b0 << 4) | (b1 >> 4)) & 63);
dst[2] = f(((b1 << 2) | (b2 >> 6)) & 63);
dst[3] = f(b2 & 63);
dst[0] = encode_6bits(b0 >> 2);
dst[1] = encode_6bits(((b0 << 4) | (b1 >> 4)) & 63);
dst[2] = encode_6bits(((b1 << 2) | (b2 >> 6)) & 63);
dst[3] = encode_6bits(b2 & 63);
}

/// Match that the given input is greater than the provided threshold.
Expand Down
5 changes: 5 additions & 0 deletions base64ct/src/encoding.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//! Base64 encodings

pub mod bcrypt;
pub(crate) mod standard;
pub mod url;
77 changes: 77 additions & 0 deletions base64ct/src/encoding/bcrypt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//! bcrypt Base64 encoding
//!
//! ```text
//! ./ [A-Z] [a-z] [0-9]
//! 0x2e-0x2f, 0x41-0x5a, 0x61-0x7a, 0x30-0x39
//! ```

use crate::{
decoder::{self, match_range_ct},
encoder::{self, match_gt_ct},
Error, InvalidEncodingError, InvalidLengthError,
};

#[cfg(feature = "alloc")]
use alloc::{string::String, vec::Vec};

/// Decode a bcrypt Base64 string into the provided
/// destination buffer.
pub fn decode(src: impl AsRef<[u8]>, dst: &mut [u8]) -> Result<&[u8], Error> {
decoder::decode(src, dst, false, decode_6bits)
}

/// Decode a bcrypt Base64 string in-place.
pub fn decode_in_place(buf: &mut [u8]) -> Result<&[u8], InvalidEncodingError> {
decoder::decode_in_place(buf, false, decode_6bits)
}

/// Decode a bcrypt Base64 string into a byte vector.
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub fn decode_vec(input: &str) -> Result<Vec<u8>, Error> {
decoder::decode_vec(input, false, decode_6bits)
}

/// Encode the input byte slice as bcrypt Base64 with padding.
///
/// Writes the result into the provided destination slice, returning an
/// ASCII-encoded Base64 string value.
pub fn encode<'a>(src: &[u8], dst: &'a mut [u8]) -> Result<&'a str, InvalidLengthError> {
encoder::encode(src, dst, false, encode_6bits)
}

/// Encode input byte slice into a [`String`] containing bcrypt Base64
/// without padding.
///
/// # Panics
/// If `input` length is greater than `usize::MAX/4`.
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub fn encode_string(input: &[u8]) -> String {
encoder::encode_string(input, false, encode_6bits)
}

/// Get the length of Base64 produced by encoding the given bytes.
///
/// WARNING: this function will return `0` for lengths greater than `usize::MAX/4`!
pub fn encoded_len(bytes: &[u8]) -> usize {
encoder::encoded_len(bytes, false)
}

#[inline(always)]
fn decode_6bits(src: u8) -> i16 {
let mut res: i16 = -1;
res += match_range_ct(src, b'.'..b'/', src as i16 - 45);
res += match_range_ct(src, b'A'..b'Z', src as i16 - 62);
res += match_range_ct(src, b'a'..b'z', src as i16 - 68);
res + match_range_ct(src, b'0'..b'9', src as i16 + 7)
}

#[inline(always)]
fn encode_6bits(mut src: i16) -> u8 {
src += 0x2e;
src += match_gt_ct(src, 0x2f, 17);
src += match_gt_ct(src, 0x5a, 6);
src -= match_gt_ct(src, 0x7a, 75);
src as u8
}

0 comments on commit 094bfc6

Please sign in to comment.