use alloc::string::String;
use alloc::vec::Vec;
use crate::base64::Base64Backend;
use crate::error::Error;
use crate::params::{Algorithm, Memory, Params, TagLen, Version, validate_inputs};
#[inline]
const fn eq(x: u32, y: u32) -> u32 {
((0u32.wrapping_sub(x ^ y) >> 8) & 0xFF) ^ 0xFF
}
#[inline]
const fn gt(x: u32, y: u32) -> u32 {
(y.wrapping_sub(x) >> 8) & 0xFF
}
#[inline]
const fn ge(x: u32, y: u32) -> u32 {
gt(y, x) ^ 0xFF
}
#[inline]
const fn lt(x: u32, y: u32) -> u32 {
gt(y, x)
}
#[inline]
const fn le(x: u32, y: u32) -> u32 {
ge(y, x)
}
#[inline]
const fn b64_byte_to_char(x: u32) -> u8 {
let a_off = b'A' as u32;
let lower_off = (b'a' as u32).wrapping_sub(26);
let digit_off = (b'0' as u32).wrapping_sub(52);
let c = (lt(x, 26) & x.wrapping_add(a_off))
| (ge(x, 26) & lt(x, 52) & x.wrapping_add(lower_off))
| (ge(x, 52) & lt(x, 62) & x.wrapping_add(digit_off))
| (eq(x, 62) & b'+' as u32)
| (eq(x, 63) & b'/' as u32);
c as u8
}
#[inline]
const fn b64_char_to_byte(c: u32) -> u32 {
let a_off = b'A' as u32;
let lower_off = (b'a' as u32).wrapping_sub(26);
let digit_off = (b'0' as u32).wrapping_sub(52);
let x = (ge(c, b'A' as u32) & le(c, b'Z' as u32) & c.wrapping_sub(a_off))
| (ge(c, b'a' as u32) & le(c, b'z' as u32) & c.wrapping_sub(lower_off))
| (ge(c, b'0' as u32) & le(c, b'9' as u32) & c.wrapping_sub(digit_off))
| (eq(c, b'+' as u32) & 62)
| (eq(c, b'/' as u32) & 63);
x | (eq(x, 0) & (eq(c, b'A' as u32) ^ 0xFF))
}
#[must_use]
pub const fn b64_len(len: u32) -> usize {
b64_len_usize(len as usize)
}
#[inline]
const fn b64_len_usize(len: usize) -> usize {
let mut olen = (len / 3) << 2;
match len % 3 {
2 => olen += 3,
1 => olen += 2,
_ => {}
}
olen
}
#[must_use]
pub const fn num_len(num: u32) -> usize {
let mut len = 1usize;
let mut n = num;
while n >= 10 {
len += 1;
n /= 10;
}
len
}
#[must_use]
pub fn encoded_len(
algorithm: Algorithm,
t_cost: u32,
m_cost: u32,
lanes: u32,
salt_len: u32,
hash_len: u32,
) -> usize {
"$$v=$m=,t=,p=$$".len()
+ algorithm.as_str().len()
+ num_len(t_cost)
+ num_len(m_cost)
+ num_len(lanes)
+ b64_len(salt_len)
+ b64_len(hash_len)
+ num_len(Version::DEFAULT.as_u32())
+ 1
}
fn encoded_len_usize(
algorithm: Algorithm,
t_cost: u32,
m_cost: u32,
lanes: u32,
salt_len: usize,
hash_len: usize,
) -> usize {
"$$v=$m=,t=,p=$$".len()
+ algorithm.as_str().len()
+ num_len(t_cost)
+ num_len(m_cost)
+ num_len(lanes)
+ b64_len_usize(salt_len)
+ b64_len_usize(hash_len)
+ num_len(Version::DEFAULT.as_u32())
+ 1
}
pub fn to_base64(dst: &mut [u8], src: &[u8]) -> Result<usize, Error> {
if src.len() < crate::base64::MIN_ENCODE_LEN {
return to_base64_scalar(dst, src);
}
let backend = crate::base64::base64_backend();
unsafe { to_base64_with_backend(dst, src, backend) }
}
#[inline(always)]
fn to_base64_scalar(dst: &mut [u8], src: &[u8]) -> Result<usize, Error> {
let olen = b64_len_usize(src.len());
if dst.len() <= olen {
return Err(Error::EncodingFail);
}
let mut acc: u32 = 0;
let mut acc_len: u32 = 0;
let mut written = 0usize;
for &byte in src {
acc = (acc << 8) | byte as u32;
acc_len += 8;
while acc_len >= 6 {
acc_len -= 6;
dst[written] = b64_byte_to_char((acc >> acc_len) & 0x3f);
written += 1;
}
}
if acc_len > 0 {
dst[written] = b64_byte_to_char((acc << (6 - acc_len)) & 0x3f);
written += 1;
}
debug_assert_eq!(written, olen);
Ok(written)
}
#[inline]
pub unsafe fn to_base64_with_backend(
dst: &mut [u8],
src: &[u8],
backend: Base64Backend,
) -> Result<usize, Error> {
if backend == Base64Backend::Scalar {
return to_base64_scalar(dst, src);
}
let olen = b64_len_usize(src.len());
if dst.len() <= olen {
return Err(Error::EncodingFail);
}
let (consumed, mut written) = unsafe { crate::base64::encode_prefix(backend, dst, src) };
let mut acc: u32 = 0;
let mut acc_len: u32 = 0;
for &byte in &src[consumed..] {
acc = (acc << 8) | byte as u32;
acc_len += 8;
while acc_len >= 6 {
acc_len -= 6;
dst[written] = b64_byte_to_char((acc >> acc_len) & 0x3F);
written += 1;
}
}
if acc_len > 0 {
dst[written] = b64_byte_to_char((acc << (6 - acc_len)) & 0x3F);
written += 1;
}
debug_assert!(written == olen);
Ok(written)
}
pub fn from_base64(dst: &mut [u8], src: &[u8]) -> Result<(usize, usize), Error> {
if src.len() < crate::base64::MIN_DECODE_LEN {
return from_base64_scalar(dst, src);
}
let backend = crate::base64::base64_backend();
unsafe { from_base64_with_backend(dst, src, backend) }
}
#[inline(always)]
fn from_base64_scalar(dst: &mut [u8], src: &[u8]) -> Result<(usize, usize), Error> {
let mut consumed = 0usize;
let mut len = 0usize;
let mut acc: u32 = 0;
let mut acc_len: u32 = 0;
loop {
let c = match src.get(consumed) {
Some(&byte) => byte as u32,
None => 0,
};
let d = b64_char_to_byte(c);
if d == 0xFF {
break;
}
consumed += 1;
acc = (acc << 6) | d;
acc_len += 6;
if acc_len >= 8 {
acc_len -= 8;
if len >= dst.len() {
return Err(Error::DecodingFail);
}
dst[len] = ((acc >> acc_len) & 0xFF) as u8;
len += 1;
}
}
if acc_len > 4 || (acc & ((1u32 << acc_len) - 1)) != 0 {
return Err(Error::DecodingFail);
}
Ok((len, consumed))
}
#[inline]
pub unsafe fn from_base64_with_backend(
dst: &mut [u8],
src: &[u8],
backend: Base64Backend,
) -> Result<(usize, usize), Error> {
if backend == Base64Backend::Scalar {
return from_base64_scalar(dst, src);
}
let (mut consumed, mut len) = unsafe { crate::base64::decode_prefix(backend, dst, src) };
let mut acc: u32 = 0;
let mut acc_len: u32 = 0;
loop {
let c = match src.get(consumed) {
Some(&byte) => byte as u32,
None => 0,
};
let d = b64_char_to_byte(c);
if d == 0xFF {
break;
}
consumed += 1;
acc = (acc << 6) | d;
acc_len += 6;
if acc_len >= 8 {
acc_len -= 8;
if len >= dst.len() {
return Err(Error::DecodingFail);
}
dst[len] = ((acc >> acc_len) & 0xFF) as u8;
len += 1;
}
}
if acc_len > 4 || (acc & ((1u32 << acc_len) - 1)) != 0 {
return Err(Error::DecodingFail);
}
Ok((len, consumed))
}
fn decode_decimal(src: &[u8]) -> Option<(u64, usize)> {
let mut acc: u64 = 0;
let mut i = 0usize;
while let Some(&c) = src.get(i) {
if !c.is_ascii_digit() {
break;
}
let digit = (c - b'0') as u64;
if acc > u64::MAX / 10 {
return None;
}
acc *= 10;
if digit > u64::MAX - acc {
return None;
}
acc += digit;
i += 1;
}
if i == 0 {
return None;
}
if src[0] == b'0' && i != 1 {
return None;
}
Some((acc, i))
}
struct Writer<'a> {
dst: &'a mut [u8],
pos: usize,
}
impl Writer<'_> {
fn put(&mut self, bytes: &[u8]) -> Result<(), Error> {
let remaining = self.dst.len() - self.pos;
if bytes.len() >= remaining {
return Err(Error::EncodingFail);
}
let end = self.pos + bytes.len();
self.dst[self.pos..end].copy_from_slice(bytes);
self.pos = end;
Ok(())
}
fn put_u32(&mut self, value: u32) -> Result<(), Error> {
let mut buf = [0u8; 10];
let mut i = buf.len();
let mut n = value;
loop {
i -= 1;
buf[i] = b'0' + (n % 10) as u8;
n /= 10;
if n == 0 {
break;
}
}
self.put(&buf[i..])
}
fn put_base64(&mut self, src: &[u8]) -> Result<(), Error> {
let written = to_base64(&mut self.dst[self.pos..], src)?;
self.pos += written;
Ok(())
}
}
fn validate_for_string(params: &Params, salt_len: usize, hash_len: usize) -> Result<(), Error> {
validate_inputs(
hash_len,
0,
salt_len,
0,
0,
params.memory_kib(),
params.passes(),
params.lanes(),
params.threads(),
)
}
pub fn encode_string(
dst: &mut [u8],
algorithm: Algorithm,
version: Version,
params: &Params,
salt: &[u8],
hash: &[u8],
) -> Result<usize, Error> {
validate_for_string(params, salt.len(), hash.len())?;
let mut w = Writer { dst, pos: 0 };
w.put(b"$")?;
w.put(algorithm.as_str().as_bytes())?;
w.put(b"$v=")?;
w.put_u32(version.as_u32())?;
w.put(b"$m=")?;
w.put_u32(params.memory_kib())?;
w.put(b",t=")?;
w.put_u32(params.passes())?;
w.put(b",p=")?;
w.put_u32(params.lanes())?;
w.put(b"$")?;
w.put_base64(salt)?;
w.put(b"$")?;
w.put_base64(hash)?;
Ok(w.pos)
}
pub fn encode_string_alloc(
algorithm: Algorithm,
version: Version,
params: &Params,
salt: &[u8],
hash: &[u8],
) -> Result<String, Error> {
validate_for_string(params, salt.len(), hash.len())?;
let capacity = encoded_len_usize(
algorithm,
params.passes(),
params.memory_kib(),
params.lanes(),
salt.len(),
hash.len(),
);
let mut buf = alloc_zeroed_vec(capacity)?;
let written = encode_string(&mut buf, algorithm, version, params, salt, hash)?;
buf.truncate(written);
String::from_utf8(buf).map_err(|_| Error::EncodingFail)
}
fn alloc_zeroed_vec(len: usize) -> Result<Vec<u8>, Error> {
let mut v = Vec::new();
v.try_reserve(len)
.map_err(|_| Error::MemoryAllocationError)?;
v.resize(len, 0);
Ok(v)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Decoded {
pub algorithm: Algorithm,
pub version: Version,
pub params: Params,
pub salt: Vec<u8>,
pub hash: Vec<u8>,
}
#[inline]
fn rest(src: &[u8], pos: usize) -> &[u8] {
src.get(pos..).unwrap_or(&[])
}
fn expect(src: &[u8], pos: &mut usize, prefix: &[u8]) -> Result<(), Error> {
if rest(src, *pos).starts_with(prefix) {
*pos += prefix.len();
Ok(())
} else {
Err(Error::DecodingFail)
}
}
fn expect_opt(src: &[u8], pos: &mut usize, prefix: &[u8]) -> bool {
if rest(src, *pos).starts_with(prefix) {
*pos += prefix.len();
true
} else {
false
}
}
fn decimal_u32(src: &[u8], pos: &mut usize) -> Result<u32, Error> {
let (value, consumed) = decode_decimal(rest(src, *pos)).ok_or(Error::DecodingFail)?;
if value > u32::MAX as u64 {
return Err(Error::DecodingFail);
}
*pos += consumed;
Ok(value as u32)
}
fn decode_bin(src: &[u8], pos: &mut usize) -> Result<Vec<u8>, Error> {
let tail = rest(src, *pos);
let max_len = tail.len() / 4 * 3 + 3;
let mut buf = alloc_zeroed_vec(max_len)?;
let (written, consumed) = from_base64(&mut buf, tail)?;
if written > u32::MAX as usize {
return Err(Error::DecodingFail);
}
buf.truncate(written);
*pos += consumed;
Ok(buf)
}
pub fn decode_string(encoded: &str, algorithm: Algorithm) -> Result<Decoded, Error> {
let src = encoded.as_bytes();
let mut pos = 0usize;
if src.len() > u32::MAX as usize {
return Err(Error::DecodingFail);
}
expect(src, &mut pos, b"$")?;
expect(src, &mut pos, algorithm.as_str().as_bytes())?;
let mut version_value = Version::V0x10.as_u32();
if expect_opt(src, &mut pos, b"$v=") {
version_value = decimal_u32(src, &mut pos)?;
}
expect(src, &mut pos, b"$m=")?;
let m_cost = decimal_u32(src, &mut pos)?;
expect(src, &mut pos, b",t=")?;
let t_cost = decimal_u32(src, &mut pos)?;
expect(src, &mut pos, b",p=")?;
let lanes = decimal_u32(src, &mut pos)?;
let threads = lanes;
expect(src, &mut pos, b"$")?;
let salt = decode_bin(src, &mut pos)?;
expect(src, &mut pos, b"$")?;
let hash = decode_bin(src, &mut pos)?;
validate_inputs(
hash.len(),
0,
salt.len(),
0,
0,
m_cost,
t_cost,
lanes,
threads,
)?;
if pos != src.len() {
return Err(Error::DecodingFail);
}
let version = Version::from_u32(version_value).ok_or(Error::DecodingFail)?;
let params = Params::builder()
.memory(Memory::kib(u64::from(m_cost)))
.passes(t_cost)
.lanes(lanes)
.threads(threads)
.tag_len(TagLen::bytes(hash.len() as u64))
.build()?;
Ok(Decoded {
algorithm,
version,
params,
salt,
hash,
})
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
const V13_ARGON2I: &str = "$argon2i$v=19$m=65536,t=2,p=1$c29tZXNhbHQ\
$wWKIMhR9lyDFvRz9YTZweHKfbftvj+qf+YFY4NeBbtA";
const V10_ARGON2I: &str = "$argon2i$m=65536,t=2,p=1$c29tZXNhbHQ\
$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ";
const V13_ARGON2ID: &str = "$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQ\
$CTFhFdXPJO1aFaMaO6Mm5c8y7cJHAph8ArZWb2GRPPc";
const V10_TAG: [u8; 32] = [
0xf6, 0xc4, 0xdb, 0x4a, 0x54, 0xe2, 0xa3, 0x70, 0x62, 0x7a, 0xff, 0x3d, 0xb6, 0x17, 0x6b,
0x94, 0xa2, 0xa2, 0x09, 0xa6, 0x2c, 0x8e, 0x36, 0x15, 0x27, 0x11, 0x80, 0x2f, 0x7b, 0x30,
0xc6, 0x94,
];
fn b64(src: &[u8]) -> Vec<u8> {
let mut out = vec![0u8; b64_len_usize(src.len()) + 1];
let n = to_base64(&mut out, src).expect("buffer sized by b64_len");
out.truncate(n);
out
}
fn unb64(src: &[u8]) -> Result<Vec<u8>, Error> {
let mut out = vec![0u8; src.len()];
let (n, consumed) = from_base64(&mut out, src)?;
assert_eq!(consumed, src.len(), "test inputs are pure base64");
out.truncate(n);
Ok(out)
}
fn b64_with_backend(
dst: &mut [u8],
src: &[u8],
backend: Base64Backend,
) -> Result<usize, Error> {
assert!(backend.is_available());
unsafe { to_base64_with_backend(dst, src, backend) }
}
fn unb64_with_backend(
dst: &mut [u8],
src: &[u8],
backend: Base64Backend,
) -> Result<(usize, usize), Error> {
assert!(backend.is_available());
unsafe { from_base64_with_backend(dst, src, backend) }
}
#[test]
fn b64_len_matches_c() {
assert_eq!(b64_len(0), 0);
assert_eq!(b64_len(1), 2);
assert_eq!(b64_len(2), 3);
assert_eq!(b64_len(3), 4);
assert_eq!(b64_len(4), 6);
assert_eq!(b64_len(8), 11);
assert_eq!(b64_len(32), 43);
for len in 0u32..64 {
let src = vec![0xABu8; len as usize];
assert_eq!(b64(&src).len(), b64_len(len), "len {len}");
}
}
#[test]
fn num_len_matches_c() {
assert_eq!(num_len(0), 1);
assert_eq!(num_len(9), 1);
assert_eq!(num_len(10), 2);
assert_eq!(num_len(19), 2);
assert_eq!(num_len(65536), 5);
assert_eq!(num_len(u32::MAX), 10);
}
#[test]
fn encoded_len_matches_the_c_vector() {
let n = encoded_len(Algorithm::Argon2id, 2, 65536, 1, 8, 32);
assert_eq!(n, V13_ARGON2ID.len() + 1);
assert_eq!(n, 87);
assert_eq!(
encoded_len(Algorithm::Argon2i, 2, 65536, 1, 8, 32),
V13_ARGON2I.len() + 1
);
}
#[test]
fn encoded_len_is_symmetric_in_m_and_t() {
assert_eq!(encoded_len(Algorithm::Argon2id, 3, 65536, 1, 16, 32), 98);
assert_eq!(encoded_len(Algorithm::Argon2id, 65536, 3, 1, 16, 32), 98);
assert_eq!(encoded_len(Algorithm::Argon2id, 2, 65536, 1, 8, 32), 87);
assert_eq!(encoded_len(Algorithm::Argon2id, 65536, 2, 1, 8, 32), 87);
assert_eq!(encoded_len(Algorithm::Argon2id, 1, u32::MAX, 1, 16, 32), 103);
assert_eq!(encoded_len(Algorithm::Argon2id, u32::MAX, 1, 1, 16, 32), 103);
const BOUNDARIES: [u32; 22] = [
0,
1,
9,
10,
99,
100,
999,
1_000,
9_999,
10_000,
99_999,
100_000,
999_999,
1_000_000,
9_999_999,
10_000_000,
99_999_999,
100_000_000,
999_999_999,
1_000_000_000,
65536,
u32::MAX,
];
for algorithm in [Algorithm::Argon2d, Algorithm::Argon2i, Algorithm::Argon2id] {
for t_cost in BOUNDARIES {
for m_cost in BOUNDARIES {
assert_eq!(
encoded_len(algorithm, t_cost, m_cost, 1, 16, 32),
encoded_len(algorithm, m_cost, t_cost, 1, 16, 32),
"{algorithm:?} t_cost={t_cost} m_cost={m_cost}"
);
}
}
}
}
#[test]
fn b64_tables_are_the_standard_alphabet() {
const ALPHABET: &[u8; 64] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
for (value, &ch) in ALPHABET.iter().enumerate() {
assert_eq!(b64_byte_to_char(value as u32), ch, "value {value}");
assert_eq!(b64_char_to_byte(ch as u32), value as u32, "char {ch}");
}
}
#[test]
fn b64_char_to_byte_rejects_everything_else() {
assert_eq!(b64_char_to_byte(b'A' as u32), 0);
for c in 0u32..256 {
let is_b64 = (c as u8).is_ascii_alphanumeric() || c == b'+' as u32 || c == b'/' as u32;
if !is_b64 {
assert_eq!(b64_char_to_byte(c), 0xFF, "char {c} must be invalid");
}
}
assert_eq!(b64_char_to_byte(b'=' as u32), 0xFF); assert_eq!(b64_char_to_byte(0), 0xFF); assert_eq!(b64_char_to_byte(b'$' as u32), 0xFF);
for c in 0x80u32..256 {
assert_eq!(b64_char_to_byte(c), 0xFF, "byte {c:#04x} must be invalid");
}
}
#[rustfmt::skip]
const C_CHAR_TO_BYTE_ASCII: [u32; 128] = [
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 62, 255, 255, 255, 63,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 255, 255, 255, 255, 255, 255,
255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 255, 255, 255, 255, 255,
255, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 255, 255, 255, 255, 255,
];
#[test]
fn b64_char_to_byte_matches_the_c_dump() {
for (c, &want) in C_CHAR_TO_BYTE_ASCII.iter().enumerate() {
assert_eq!(b64_char_to_byte(c as u32), want, "char {c}");
}
}
#[rustfmt::skip]
const C_BYTE_TO_CHAR: [u8; 64] = [
65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80,
81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102,
103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118,
119, 120, 121, 122, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 43, 47,
];
#[test]
fn b64_byte_to_char_matches_the_c_dump() {
for (x, &want) in C_BYTE_TO_CHAR.iter().enumerate() {
assert_eq!(b64_byte_to_char(x as u32), want, "value {x}");
}
}
#[test]
fn non_ascii_bytes_are_rejected_in_a_field() {
let utf8 = "$argon2i$v=19$m=65536,t=2,p=1$é9tZXNhbHQ\
$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ";
assert_eq!(
decode_string(utf8, Algorithm::Argon2i),
Err(Error::DecodingFail)
);
let slashes = "$argon2i$v=19$m=65536,t=2,p=1$//9tZXNhbHQ\
$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ";
let d = decode_string(slashes, Algorithm::Argon2i).unwrap();
assert_eq!(d.salt, [0xff, 0xff, 0x6d, 0x65, 0x73, 0x61, 0x6c, 0x74]);
}
#[test]
fn base64_known_vectors() {
assert_eq!(b64(b"somesalt"), b"c29tZXNhbHQ");
assert_eq!(b64(b"diffsalt"), b"ZGlmZnNhbHQ");
assert_eq!(
b64(&V10_TAG),
b"9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ"
);
assert_eq!(unb64(b"c29tZXNhbHQ").unwrap(), b"somesalt");
assert_eq!(
unb64(b"9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ").unwrap(),
&V10_TAG
);
}
#[test]
fn base64_round_trips_every_short_length() {
for len in 0usize..96 {
let src: Vec<u8> = (0..len)
.map(|i| (i as u8).wrapping_mul(37) ^ 0x5A)
.collect();
let encoded = b64(&src);
assert_eq!(unb64(&encoded).unwrap(), src, "len {len}");
}
}
#[test]
fn every_base64_backend_matches_scalar_across_lengths() {
for len in 0usize..=512 {
let src: Vec<u8> = (0..len)
.map(|i| (i as u8).wrapping_mul(197) ^ (len as u8).wrapping_mul(11))
.collect();
let capacity = b64_len_usize(len) + 1;
let mut expected = vec![0xa5; capacity];
let expected_len =
b64_with_backend(&mut expected, &src, Base64Backend::Scalar).unwrap();
for &backend in Base64Backend::ALL {
if !backend.is_available()
|| (cfg!(miri) && backend != Base64Backend::Scalar)
{
continue;
}
let mut actual = vec![0xa5; capacity];
let actual_len = b64_with_backend(&mut actual, &src, backend).unwrap();
assert_eq!(actual_len, expected_len, "{backend} length {len}");
assert_eq!(actual, expected, "{backend} bytes at length {len}");
let mut scalar_decoded = vec![0x5a; len];
let scalar_result = unb64_with_backend(
&mut scalar_decoded,
&expected[..expected_len],
Base64Backend::Scalar,
);
let mut simd_decoded = vec![0x5a; len];
let simd_result = unb64_with_backend(
&mut simd_decoded,
&expected[..expected_len],
backend,
);
assert_eq!(simd_result, scalar_result, "{backend} decode length {len}");
assert_eq!(simd_decoded, scalar_decoded, "{backend} decode bytes {len}");
assert_eq!(simd_decoded, src, "{backend} round trip {len}");
}
}
}
#[test]
fn every_base64_backend_matches_scalar_on_invalid_bytes_and_short_outputs() {
let raw: Vec<u8> = (0..96)
.map(|i| (i as u8).wrapping_mul(37) ^ 0x5a)
.collect();
let encoded = b64(&raw);
for &backend in Base64Backend::ALL {
if !backend.is_available() || (cfg!(miri) && backend != Base64Backend::Scalar) {
continue;
}
for pos in 0..encoded.len() {
for invalid in [0, b'$', b'=', 0x80, 0xff] {
let mut input = encoded.clone();
input[pos] = invalid;
let mut expected = vec![0xa5; raw.len()];
let expected_result = unb64_with_backend(
&mut expected,
&input,
Base64Backend::Scalar,
);
let mut actual = vec![0xa5; raw.len()];
let actual_result = unb64_with_backend(&mut actual, &input, backend);
assert_eq!(actual_result, expected_result, "{backend} pos {pos} byte {invalid:#x}");
assert_eq!(actual, expected, "{backend} output at pos {pos} byte {invalid:#x}");
}
}
for dst_len in 0..raw.len() {
let mut expected = vec![0xa5; dst_len];
let expected_result = unb64_with_backend(
&mut expected,
&encoded,
Base64Backend::Scalar,
);
let mut actual = vec![0xa5; dst_len];
let actual_result = unb64_with_backend(&mut actual, &encoded, backend);
assert_eq!(actual_result, expected_result, "{backend} dst length {dst_len}");
assert_eq!(actual, expected, "{backend} dst bytes at length {dst_len}");
}
}
}
#[test]
fn to_base64_rejects_a_tight_buffer() {
let mut exact = [0u8; 11];
assert_eq!(to_base64(&mut exact, b"somesalt"), Err(Error::EncodingFail));
let mut roomy = [0u8; 12];
assert_eq!(to_base64(&mut roomy, b"somesalt"), Ok(11));
assert_eq!(to_base64(&mut [], b""), Err(Error::EncodingFail));
assert_eq!(to_base64(&mut [0u8; 1], b""), Ok(0));
}
#[test]
fn from_base64_stops_at_the_first_non_b64_char() {
let mut out = [0u8; 16];
let (written, consumed) = from_base64(&mut out, b"c29tZXNhbHQ$rest").unwrap();
assert_eq!(written, 8);
assert_eq!(consumed, 11);
assert_eq!(&out[..8], b"somesalt");
let (written, consumed) = from_base64(&mut out, b"$tag").unwrap();
assert_eq!((written, consumed), (0, 0));
}
#[test]
fn from_base64_rejects_leftover_bits() {
let mut out = [0u8; 16];
assert_eq!(
from_base64(&mut out, b"AAAAA"),
Err(Error::DecodingFail),
"acc_len > 4"
);
assert_eq!(from_base64(&mut out, b"A"), Err(Error::DecodingFail));
assert_eq!(from_base64(&mut out, b"AB"), Err(Error::DecodingFail));
assert_eq!(from_base64(&mut out, b"AA"), Ok((1, 2)));
assert_eq!(from_base64(&mut out, b"AAB"), Err(Error::DecodingFail));
assert_eq!(from_base64(&mut out, b"AAA"), Ok((2, 3)));
}
#[test]
fn from_base64_rejects_a_short_buffer() {
let mut out = [0u8; 4];
assert_eq!(
from_base64(&mut out, b"c29tZXNhbHQ"),
Err(Error::DecodingFail)
);
let mut exact = [0u8; 8];
assert_eq!(from_base64(&mut exact, b"c29tZXNhbHQ"), Ok((8, 11)));
assert_eq!(&exact, b"somesalt");
let mut tight = [0u8; 7];
assert_eq!(
from_base64(&mut tight, b"c29tZXNhbHQ"),
Err(Error::DecodingFail)
);
}
#[test]
fn decode_decimal_matches_c() {
assert_eq!(decode_decimal(b"0"), Some((0, 1)));
assert_eq!(decode_decimal(b"19"), Some((19, 2)));
assert_eq!(decode_decimal(b"65536,t=2"), Some((65536, 5)));
assert_eq!(decode_decimal(b"4294967295"), Some((4294967295, 10)));
assert_eq!(decode_decimal(b""), None);
assert_eq!(decode_decimal(b"$"), None);
assert_eq!(decode_decimal(b"x1"), None);
assert_eq!(decode_decimal(b"01"), None);
assert_eq!(decode_decimal(b"00"), None);
assert_eq!(decode_decimal(b"0019"), None);
assert_eq!(
decode_decimal(b"18446744073709551615"),
Some((u64::MAX, 20))
);
assert_eq!(decode_decimal(b"18446744073709551616"), None);
assert_eq!(decode_decimal(b"99999999999999999999999"), None);
}
#[test]
fn decode_decimal_matches_the_c_dump() {
type Case = (&'static [u8], Option<(u64, usize)>);
let cases: &[Case] = &[
(b"", None),
(b"0", Some((0, 1))),
(b"00", None),
(b"000", None),
(b"01", None),
(b"0019", None),
(b"1", Some((1, 1))),
(b"9", Some((9, 1))),
(b"19", Some((19, 2))),
(b"10", Some((10, 2))),
(b"007", None),
(b"4294967295", Some((4294967295, 10))),
(b"4294967296", Some((4294967296, 10))),
(b"9223372036854775807", Some((9223372036854775807, 19))),
(b"18446744073709551615", Some((18446744073709551615, 20))),
(b"18446744073709551616", None),
(b"18446744073709551620", None),
(b"19999999999999999999", None),
(b"20000000000000000000", None),
(b"99999999999999999999", None),
(b"1000000000000000000000000", None),
(b"65536,t=2", Some((65536, 5))),
(b"1,t=2", Some((1, 1))),
(b"x", None),
(b"1x", Some((1, 1))),
(b" 1", None),
(b"+1", None),
(b"-1", None),
(b"1 ", Some((1, 1))),
(b"0x10", Some((0, 1))),
(b"12$", Some((12, 2))),
(b"0,", Some((0, 1))),
(b"10$", Some((10, 2))),
(b"$", None),
(b"1234567890", Some((1234567890, 10))),
];
for (input, want) in cases {
assert_eq!(
decode_decimal(input),
*want,
"input {:?}",
core::str::from_utf8(input).unwrap_or("<non-utf8>")
);
}
}
#[test]
fn decimal_fields_are_u32_bounded() {
let too_big = "$argon2i$v=19$m=4294967296,t=2,p=1$c29tZXNhbHQ\
$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ";
assert_eq!(
decode_string(too_big, Algorithm::Argon2i),
Err(Error::DecodingFail)
);
let leading_zero = "$argon2i$v=19$m=065536,t=2,p=1$c29tZXNhbHQ\
$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ";
assert_eq!(
decode_string(leading_zero, Algorithm::Argon2i),
Err(Error::DecodingFail)
);
}
#[test]
fn encode_matches_the_official_vectors() {
let params = Params::builder()
.memory(Memory::kib(65536))
.passes(2)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.unwrap();
let tag = unb64(b"wWKIMhR9lyDFvRz9YTZweHKfbftvj+qf+YFY4NeBbtA").unwrap();
let encoded = encode_string_alloc(
Algorithm::Argon2i,
Version::V0x13,
¶ms,
b"somesalt",
&tag,
)
.unwrap();
assert_eq!(encoded, V13_ARGON2I);
let tag = unb64(b"CTFhFdXPJO1aFaMaO6Mm5c8y7cJHAph8ArZWb2GRPPc").unwrap();
let encoded = encode_string_alloc(
Algorithm::Argon2id,
Version::V0x13,
¶ms,
b"somesalt",
&tag,
)
.unwrap();
assert_eq!(encoded, V13_ARGON2ID);
let encoded = encode_string_alloc(
Algorithm::Argon2i,
Version::V0x10,
¶ms,
b"somesalt",
&V10_TAG,
)
.unwrap();
assert_eq!(
encoded,
"$argon2i$v=16$m=65536,t=2,p=1$c29tZXNhbHQ\
$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ"
);
}
#[test]
fn encode_needs_encoded_len_bytes_exactly() {
let params = Params::builder()
.memory(Memory::kib(65536))
.passes(2)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.unwrap();
let want = encoded_len(Algorithm::Argon2i, 2, 65536, 1, 8, 32);
assert_eq!(want, V13_ARGON2I.len() + 1);
let mut buf = vec![0u8; want];
let n = encode_string(
&mut buf,
Algorithm::Argon2i,
Version::V0x13,
¶ms,
b"somesalt",
&V10_TAG,
)
.unwrap();
assert_eq!(n, want - 1);
let mut buf = vec![0u8; want - 1];
assert_eq!(
encode_string(
&mut buf,
Algorithm::Argon2i,
Version::V0x13,
¶ms,
b"somesalt",
&V10_TAG,
),
Err(Error::EncodingFail)
);
}
#[test]
fn encode_validates_first() {
let params = Params::builder()
.memory(Memory::kib(65536))
.passes(2)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.unwrap();
assert_eq!(
encode_string_alloc(
Algorithm::Argon2i,
Version::V0x13,
¶ms,
b"short",
&V10_TAG
),
Err(Error::SaltTooShort)
);
assert_eq!(
encode_string_alloc(
Algorithm::Argon2i,
Version::V0x13,
¶ms,
b"somesalt",
&[0u8; 3]
),
Err(Error::OutputTooShort)
);
}
#[test]
fn decode_the_v13_vector() {
let d = decode_string(V13_ARGON2I, Algorithm::Argon2i).unwrap();
assert_eq!(d.algorithm, Algorithm::Argon2i);
assert_eq!(d.version, Version::V0x13);
assert_eq!(d.params.memory_kib(), 65536);
assert_eq!(d.params.passes(), 2);
assert_eq!(d.params.lanes(), 1);
assert_eq!(d.params.tag_len_bytes(), 32);
assert_eq!(d.salt, b"somesalt");
assert_eq!(
d.hash,
unb64(b"wWKIMhR9lyDFvRz9YTZweHKfbftvj+qf+YFY4NeBbtA").unwrap()
);
}
#[test]
fn decode_defaults_the_version_to_0x10() {
let d = decode_string(V10_ARGON2I, Algorithm::Argon2i).unwrap();
assert_eq!(d.version, Version::V0x10);
assert_eq!(d.salt, b"somesalt");
assert_eq!(d.hash, &V10_TAG);
}
#[test]
fn decode_sets_threads_to_lanes() {
let s = "$argon2id$v=19$m=65536,t=2,p=4$c29tZXNhbHQ\
$CTFhFdXPJO1aFaMaO6Mm5c8y7cJHAph8ArZWb2GRPPc";
let d = decode_string(s, Algorithm::Argon2id).unwrap();
assert_eq!(d.params.lanes(), 4);
assert_eq!(d.params.threads(), 4);
}
#[test]
fn encode_decode_round_trip() {
let salt = b"0123456789abcdef";
let tag: Vec<u8> = (0u8..48).collect();
for algorithm in Algorithm::ALL {
for version in Version::ALL {
for lanes in [1u32, 2, 255] {
let params = Params::builder()
.memory(Memory::kib(1 << 16))
.passes(3)
.lanes(lanes)
.tag_len(TagLen::bytes(tag.len() as u64))
.build()
.unwrap();
let encoded =
encode_string_alloc(algorithm, version, ¶ms, salt, &tag).unwrap();
let d = decode_string(&encoded, algorithm).unwrap();
assert_eq!(d.algorithm, algorithm);
assert_eq!(d.version, version);
assert_eq!(d.params, params);
assert_eq!(d.salt, salt);
assert_eq!(d.hash, tag);
assert_eq!(
encode_string_alloc(d.algorithm, d.version, &d.params, &d.salt, &d.hash)
.unwrap(),
encoded
);
}
}
}
}
#[test]
fn decode_rejects_a_missing_dollar_before_the_salt() {
let v10 = "$argon2i$m=65536,t=2,p=1c29tZXNhbHQ\
$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ";
assert_eq!(
decode_string(v10, Algorithm::Argon2i),
Err(Error::DecodingFail)
);
let v13 = "$argon2i$v=19$m=65536,t=2,p=1c29tZXNhbHQ\
$wWKIMhR9lyDFvRz9YTZweHKfbftvj+qf+YFY4NeBbtA";
assert_eq!(
decode_string(v13, Algorithm::Argon2i),
Err(Error::DecodingFail)
);
}
#[test]
fn decode_rejects_a_missing_dollar_before_the_tag() {
let v10 = "$argon2i$m=65536,t=2,p=1$c29tZXNhbHQ\
9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ";
assert_eq!(
decode_string(v10, Algorithm::Argon2i),
Err(Error::DecodingFail)
);
let v13 = "$argon2i$v=19$m=65536,t=2,p=1$c29tZXNhbHQ\
wWKIMhR9lyDFvRz9YTZweHKfbftvj+qf+YFY4NeBbtA";
assert_eq!(
decode_string(v13, Algorithm::Argon2i),
Err(Error::DecodingFail)
);
}
#[test]
fn decode_reports_salt_too_short_not_decoding_fail() {
let v10 = "$argon2i$m=65536,t=2,p=1$\
$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ";
assert_eq!(
decode_string(v10, Algorithm::Argon2i),
Err(Error::SaltTooShort)
);
let v13 = "$argon2i$v=19$m=65536,t=2,p=1$\
$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ";
assert_eq!(
decode_string(v13, Algorithm::Argon2i),
Err(Error::SaltTooShort)
);
let short = "$argon2i$v=19$m=65536,t=2,p=1$c2hvcnRz\
$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ";
assert_eq!(
decode_string(short, Algorithm::Argon2i),
Err(Error::SaltTooShort)
);
}
#[test]
fn decode_argon2i_is_a_prefix_of_argon2id() {
assert_eq!(
decode_string(V13_ARGON2ID, Algorithm::Argon2i),
Err(Error::DecodingFail)
);
let v10_id = "$argon2id$m=65536,t=2,p=1$c29tZXNhbHQ\
$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ";
assert_eq!(
decode_string(v10_id, Algorithm::Argon2i),
Err(Error::DecodingFail)
);
assert_eq!(
decode_string(V13_ARGON2I, Algorithm::Argon2id),
Err(Error::DecodingFail)
);
assert_eq!(
decode_string(V13_ARGON2I, Algorithm::Argon2d),
Err(Error::DecodingFail)
);
assert!(decode_string(V13_ARGON2ID, Algorithm::Argon2id).is_ok());
}
#[test]
fn decode_rejects_structural_damage() {
let cases: &[&str] = &[
"",
"$",
"argon2i$v=19$m=65536,t=2,p=1$c29tZXNhbHQ$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ",
"$argon2i",
"$argon2i$v=19",
"$argon2i$v=19$m=65536,t=2,p=1",
"$argon2i$v=19$m=65536,t=2,p=1$c29tZXNhbHQ",
"$argon2i$v=19$m=x,t=2,p=1$c29tZXNhbHQ$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ",
"$argon2i$v=19$t=2,m=65536,p=1$c29tZXNhbHQ$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ",
"$argon2i$v=19$m=65536,t=2,p=1$c29tZXNhbHQ$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ$",
"$argon2i$v=19$m=65536,t=2,p=1$c29tZXNhbHQ$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ ",
"$argon2i$v=19$m=65536,t=2,p=1$c29tZXNhbHQ=$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ",
];
for case in cases {
assert_eq!(
decode_string(case, Algorithm::Argon2i),
Err(Error::DecodingFail),
"expected DecodingFail for {case:?}"
);
}
}
#[test]
fn decode_surfaces_the_c_validation_codes() {
assert_eq!(
decode_string(
"$argon2i$v=19$m=65536,t=2,p=1$c29tZXNhbHQ$AAAA",
Algorithm::Argon2i
),
Err(Error::OutputTooShort)
);
assert_eq!(
decode_string(
"$argon2i$v=19$m=1,t=2,p=1$c29tZXNhbHQ$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ",
Algorithm::Argon2i
),
Err(Error::MemoryTooLittle)
);
assert_eq!(
decode_string(
"$argon2i$v=19$m=16,t=2,p=4$c29tZXNhbHQ$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ",
Algorithm::Argon2i
),
Err(Error::MemoryTooLittle)
);
assert_eq!(
decode_string(
"$argon2i$v=19$m=65536,t=0,p=1$c29tZXNhbHQ$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ",
Algorithm::Argon2i
),
Err(Error::TimeTooSmall)
);
assert_eq!(
decode_string(
"$argon2i$v=19$m=65536,t=2,p=0$c29tZXNhbHQ$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ",
Algorithm::Argon2i
),
Err(Error::LanesTooFew)
);
#[cfg(target_pointer_width = "64")]
assert_eq!(
decode_string(
"$argon2i$v=19$m=4294967295,t=2,p=16777216$c29tZXNhbHQ$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ",
Algorithm::Argon2i
),
Err(Error::LanesTooMany)
);
#[cfg(target_pointer_width = "32")]
assert_eq!(
decode_string(
"$argon2i$v=19$m=4294967295,t=2,p=16777216$c29tZXNhbHQ$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ",
Algorithm::Argon2i
),
Err(Error::MemoryTooMuch)
);
}
#[test]
fn validation_runs_before_the_trailing_character_check() {
let s = "$argon2i$v=19$m=65536,t=2,p=1$\
$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ!!!";
assert_eq!(
decode_string(s, Algorithm::Argon2i),
Err(Error::SaltTooShort)
);
}
#[test]
fn decode_rejects_an_unrepresentable_version() {
let s = "$argon2i$v=99$m=65536,t=2,p=1$c29tZXNhbHQ\
$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ";
assert_eq!(
decode_string(s, Algorithm::Argon2i),
Err(Error::DecodingFail)
);
let s = "$argon2i$v=99$m=65536,t=2,p=1$$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ";
assert_eq!(
decode_string(s, Algorithm::Argon2i),
Err(Error::SaltTooShort)
);
}
#[test]
fn decode_accepts_a_long_salt_and_tag() {
let salt: Vec<u8> = (0u8..=255).collect();
let tag: Vec<u8> = (0u8..=200).rev().collect();
let params = Params::builder()
.memory(Memory::kib(1 << 16))
.passes(1)
.lanes(1)
.tag_len(TagLen::bytes(tag.len() as u64))
.build()
.unwrap();
let encoded =
encode_string_alloc(Algorithm::Argon2d, Version::V0x13, ¶ms, &salt, &tag).unwrap();
let d = decode_string(&encoded, Algorithm::Argon2d).unwrap();
assert_eq!(d.salt, salt);
assert_eq!(d.hash, tag);
assert_eq!(d.params.tag_len_bytes(), tag.len());
}
}