#[derive(Debug, Copy, Clone, Eq, PartialEq, thiserror::Error)]
pub enum Error {
#[error("salt must be at least 4 bytes long")]
InvalidSalt,
#[error("space must be greater than the digest length")]
InvalidSpace,
#[error("time must be greater than or equal to 1")]
InvalidTime,
#[error("invalid format is passed to Balloon")]
InvalidFormat,
#[error("invalid format is passed to Balloon")]
VerificationFailed,
}
pub use blake3::Hash;
use subtle::ConstantTimeEq;
use core::convert::TryInto;
use digest::{generic_array::GenericArray, Digest};
pub struct Balloon<D>
where
D: Digest,
{
buffer: Vec<GenericArray<u8, D::OutputSize>>,
digest: D,
space: usize,
time: usize,
delta: usize,
}
impl<D> Balloon<D>
where
D: Digest,
{
pub fn new(space: usize, time: usize, delta: usize) -> Self {
Balloon {
buffer: vec![Default::default(); space],
digest: D::new(),
space,
time,
delta,
}
}
pub fn reconfigure(&mut self, space: usize, time: usize, delta: usize) {
self.space = space;
self.time = time;
self.delta = delta;
}
pub fn process(&mut self, pass: &[u8], salt: &[u8]) -> GenericArray<u8, D::OutputSize> {
let Balloon {
buffer,
digest,
space,
time,
delta,
} = self;
let (space, time, delta) = (*space, *time, *delta);
assert!(space > 0);
buffer.resize_with(space, Default::default);
let mut counter: u64 = 0;
digest.update(&counter.to_le_bytes());
counter += 1;
digest.update(salt);
digest.update(pass);
buffer[0] = digest.finalize_reset();
for i in 1..space {
digest.update(&counter.to_le_bytes());
counter += 1;
digest.update(&buffer[i - 1]);
buffer[i] = digest.finalize_reset();
}
for t in 0..time {
for m in 0..space {
digest.update(&counter.to_le_bytes());
counter += 1;
digest.update(&buffer[(space - 1 + m) % space]);
buffer[m] = digest.finalize_reset();
for i in 0..delta {
digest.update(&counter.to_le_bytes());
counter += 1;
digest.update(salt);
digest.update(&(t as u64).to_le_bytes());
digest.update(&(m as u64).to_le_bytes());
digest.update(&(i as u64).to_le_bytes());
let x = u64::from_le_bytes(
digest.finalize_reset()[..8]
.try_into()
.expect("digest contains less than 8 bytes?"),
) as usize;
digest.update(&counter.to_le_bytes());
counter += 1;
digest.update(&buffer[m]);
digest.update(&buffer[x % space]);
buffer[m] = digest.finalize_reset();
} } }
buffer[space - 1].clone()
}
}
pub fn balloon(
passy: &[u8],
salty: &[u8],
space: usize,
time: usize,
delta: usize,
) -> Result<Hash, Error> {
if space < 1 {
return Err(Error::InvalidSpace);
}
if time < 1 {
return Err(Error::InvalidTime);
}
if salty.len() < 4 {
return Err(Error::InvalidSalt);
}
let mut ctx = Balloon::<blake3::Hasher>::new(space, time, delta);
let res = ctx.process(passy, salty);
Ok(blake3::hash(&res))
}
pub fn verify(
val: &Hash,
passy: &[u8],
salty: &[u8],
space: usize,
time: usize,
delta: usize,
) -> Result<(), Error> {
match balloon(passy, salty, space, time, delta) {
Ok(res) => {
match compare_ct(res.as_bytes(), val.as_bytes()) {
Some(_) => Err(Error::VerificationFailed),
None => Ok(()),
}
}
Err(e) => Err(e),
}
}
pub fn compare_ct(a: &[u8], b: &[u8]) -> Option<Error> {
if a.len() != b.len() {
return Some(Error::InvalidFormat);
}
if a.ct_eq(b).unwrap_u8() == 1 {
None
} else {
return Some(Error::InvalidFormat);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works2() {
let password = [0u8, 1u8, 2u8, 3u8, 0u8, 1u8, 2u8, 3u8];
let salt = [0u8, 1u8, 2u8, 3u8, 3u8];
let test = balloon(&password, &salt, 24, 18, 5).unwrap();
assert!(verify(&test, &password, &salt, 24, 18, 5).is_ok());
}
}