#![forbid(unsafe_code)]
use oxicrypto_core::{
CryptoError, PasswordHash as PasswordHashTrait, PasswordHashParams, SecretVec, Zeroize,
};
use sha2::{Digest, Sha256, Sha512};
pub const BALLOON_DELTA: u64 = 3;
trait BalloonHash {
const DIGEST_LEN: usize;
fn hash_into(data: &[u8], out: &mut [u8]);
}
struct Sha256Hash;
struct Sha512Hash;
impl BalloonHash for Sha256Hash {
const DIGEST_LEN: usize = 32;
fn hash_into(data: &[u8], out: &mut [u8]) {
let digest = Sha256::digest(data);
out.copy_from_slice(&digest);
}
}
impl BalloonHash for Sha512Hash {
const DIGEST_LEN: usize = 64;
fn hash_into(data: &[u8], out: &mut [u8]) {
let digest = Sha512::digest(data);
out.copy_from_slice(&digest);
}
}
struct HashInput {
buf: Vec<u8>,
}
impl HashInput {
fn new() -> Self {
Self {
buf: Vec::with_capacity(160),
}
}
fn clear(&mut self) {
self.buf.clear();
}
fn push_u64(&mut self, value: u64) {
self.buf.extend_from_slice(&value.to_le_bytes());
}
fn push_bytes(&mut self, bytes: &[u8]) {
self.buf.extend_from_slice(bytes);
}
fn as_slice(&self) -> &[u8] {
&self.buf
}
}
fn le_digest_mod(digest: &[u8], modulus: u64) -> u64 {
let m = u128::from(modulus);
let mut acc: u128 = 0;
for &byte in digest.iter().rev() {
acc = (acc * 256 + u128::from(byte)) % m;
}
acc as u64
}
fn balloon_core<H: BalloonHash>(
password: &[u8],
salt: &[u8],
space_cost: u64,
time_cost: u64,
out: &mut [u8],
) -> Result<(), CryptoError> {
if space_cost == 0 || time_cost == 0 {
return Err(CryptoError::BadInput);
}
if out.len() != H::DIGEST_LEN {
return Err(CryptoError::BadInput);
}
let digest_len = H::DIGEST_LEN;
let total_bytes = (space_cost as usize)
.checked_mul(digest_len)
.ok_or(CryptoError::BadInput)?;
let mut work = ZeroizingBuf::new(total_bytes);
let buf = work.as_mut_slice();
let mut input = HashInput::new();
let mut digest = ZeroizingBuf::new(digest_len);
let mut cnt: u64 = 0;
input.clear();
input.push_u64(cnt);
input.push_bytes(password);
input.push_bytes(salt);
H::hash_into(input.as_slice(), &mut buf[0..digest_len]);
cnt += 1;
for m in 1..(space_cost as usize) {
let prev_start = (m - 1) * digest_len;
input.clear();
input.push_u64(cnt);
digest
.as_mut_slice()
.copy_from_slice(&buf[prev_start..prev_start + digest_len]);
input.push_bytes(digest.as_slice());
let cur_start = m * digest_len;
H::hash_into(
input.as_slice(),
&mut buf[cur_start..cur_start + digest_len],
);
cnt += 1;
}
let space_usize = space_cost as usize;
for round in 0..time_cost {
for m in 0..space_usize {
let prev_idx = if m == 0 { space_usize - 1 } else { m - 1 };
let prev_start = prev_idx * digest_len;
let cur_start = m * digest_len;
input.clear();
input.push_u64(cnt);
let mut prev_block = [0u8; 64];
let mut cur_block = [0u8; 64];
prev_block[..digest_len].copy_from_slice(&buf[prev_start..prev_start + digest_len]);
cur_block[..digest_len].copy_from_slice(&buf[cur_start..cur_start + digest_len]);
input.push_bytes(&prev_block[..digest_len]);
input.push_bytes(&cur_block[..digest_len]);
H::hash_into(
input.as_slice(),
&mut buf[cur_start..cur_start + digest_len],
);
cnt += 1;
for i in 0..BALLOON_DELTA {
input.clear();
input.push_u64(round);
input.push_u64(m as u64);
input.push_u64(i);
let mut idx_block = [0u8; 64];
H::hash_into(input.as_slice(), &mut idx_block[..digest_len]);
input.clear();
input.push_u64(cnt);
input.push_bytes(salt);
input.push_bytes(&idx_block[..digest_len]);
H::hash_into(input.as_slice(), digest.as_mut_slice());
cnt += 1;
let other = le_digest_mod(digest.as_slice(), space_cost) as usize;
let other_start = other * digest_len;
let mut m_block = [0u8; 64];
let mut other_block = [0u8; 64];
m_block[..digest_len].copy_from_slice(&buf[cur_start..cur_start + digest_len]);
other_block[..digest_len]
.copy_from_slice(&buf[other_start..other_start + digest_len]);
input.clear();
input.push_u64(cnt);
input.push_bytes(&m_block[..digest_len]);
input.push_bytes(&other_block[..digest_len]);
H::hash_into(
input.as_slice(),
&mut buf[cur_start..cur_start + digest_len],
);
cnt += 1;
}
}
}
let last_start = (space_usize - 1) * digest_len;
out.copy_from_slice(&buf[last_start..last_start + digest_len]);
Ok(())
}
struct ZeroizingBuf {
bytes: Vec<u8>,
}
impl ZeroizingBuf {
fn new(len: usize) -> Self {
Self {
bytes: vec![0u8; len],
}
}
fn as_mut_slice(&mut self) -> &mut [u8] {
&mut self.bytes
}
fn as_slice(&self) -> &[u8] {
&self.bytes
}
}
impl Drop for ZeroizingBuf {
fn drop(&mut self) {
self.bytes.zeroize();
}
}
#[must_use = "balloon hash result must be checked"]
pub fn balloon_sha256(
password: &[u8],
salt: &[u8],
space_cost: u64,
time_cost: u64,
out: &mut [u8],
) -> Result<(), CryptoError> {
balloon_core::<Sha256Hash>(password, salt, space_cost, time_cost, out)
}
#[must_use = "balloon hash result must be checked"]
pub fn balloon_sha512(
password: &[u8],
salt: &[u8],
space_cost: u64,
time_cost: u64,
out: &mut [u8],
) -> Result<(), CryptoError> {
balloon_core::<Sha512Hash>(password, salt, space_cost, time_cost, out)
}
#[must_use = "derived key should be used"]
pub fn balloon_sha256_secret(
password: &[u8],
salt: &[u8],
space_cost: u64,
time_cost: u64,
) -> Result<SecretVec, CryptoError> {
let mut out = vec![0u8; Sha256Hash::DIGEST_LEN];
balloon_core::<Sha256Hash>(password, salt, space_cost, time_cost, &mut out)?;
Ok(SecretVec::new(out))
}
#[must_use = "derived key should be used"]
pub fn balloon_sha512_secret(
password: &[u8],
salt: &[u8],
space_cost: u64,
time_cost: u64,
) -> Result<SecretVec, CryptoError> {
let mut out = vec![0u8; Sha512Hash::DIGEST_LEN];
balloon_core::<Sha512Hash>(password, salt, space_cost, time_cost, &mut out)?;
Ok(SecretVec::new(out))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BalloonVariant {
Sha256,
Sha512,
}
#[derive(Debug, Clone, Copy)]
pub struct BalloonParams {
pub space_cost: u64,
pub time_cost: u64,
}
impl BalloonParams {
pub fn new(space_cost: u64, time_cost: u64) -> Result<Self, CryptoError> {
if space_cost == 0 || time_cost == 0 {
return Err(CryptoError::BadInput);
}
Ok(Self {
space_cost,
time_cost,
})
}
#[must_use]
pub fn interactive() -> Self {
Self {
space_cost: 16_384,
time_cost: 3,
}
}
#[must_use]
pub fn moderate() -> Self {
Self {
space_cost: 65_536,
time_cost: 3,
}
}
#[must_use]
pub fn sensitive() -> Self {
Self {
space_cost: 262_144,
time_cost: 3,
}
}
}
impl PasswordHashParams for BalloonParams {
fn memory_cost(&self) -> Option<u32> {
let kib = self.space_cost.saturating_mul(32) / 1024;
u32::try_from(kib).ok()
}
fn time_cost(&self) -> Option<u32> {
u32::try_from(self.time_cost).ok()
}
fn parallelism(&self) -> Option<u32> {
Some(1)
}
}
#[derive(Debug, Clone, Copy)]
pub struct BalloonHasher {
pub variant: BalloonVariant,
pub params: BalloonParams,
}
impl BalloonHasher {
#[must_use]
pub fn new_sha256(params: BalloonParams) -> Self {
Self {
variant: BalloonVariant::Sha256,
params,
}
}
#[must_use]
pub fn new_sha512(params: BalloonParams) -> Self {
Self {
variant: BalloonVariant::Sha512,
params,
}
}
#[must_use]
pub fn output_len(&self) -> usize {
match self.variant {
BalloonVariant::Sha256 => Sha256Hash::DIGEST_LEN,
BalloonVariant::Sha512 => Sha512Hash::DIGEST_LEN,
}
}
}
impl PasswordHashTrait for BalloonHasher {
fn name(&self) -> &'static str {
match self.variant {
BalloonVariant::Sha256 => "balloon-sha256",
BalloonVariant::Sha512 => "balloon-sha512",
}
}
fn hash_password(
&self,
password: &[u8],
salt: &[u8],
_params: &dyn PasswordHashParams,
out: &mut [u8],
) -> Result<(), CryptoError> {
match self.variant {
BalloonVariant::Sha256 => balloon_sha256(
password,
salt,
self.params.space_cost,
self.params.time_cost,
out,
),
BalloonVariant::Sha512 => balloon_sha512(
password,
salt,
self.params.space_cost,
self.params.time_cost,
out,
),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn determinism_same_inputs() {
let mut a = [0u8; 32];
let mut b = [0u8; 32];
balloon_sha256(b"password", b"salt", 8, 3, &mut a).expect("a");
balloon_sha256(b"password", b"salt", 8, 3, &mut b).expect("b");
assert_eq!(a, b, "balloon must be deterministic");
assert_ne!(a, [0u8; 32]);
}
#[test]
fn different_salt_differs() {
let mut a = [0u8; 32];
let mut b = [0u8; 32];
balloon_sha256(b"password", b"salt", 8, 3, &mut a).expect("a");
balloon_sha256(b"password", b"pepper", 8, 3, &mut b).expect("b");
assert_ne!(a, b, "different salt must change output");
}
#[test]
fn rejects_zero_space_cost() {
let mut out = [0u8; 32];
assert_eq!(
balloon_sha256(b"pw", b"salt", 0, 3, &mut out),
Err(CryptoError::BadInput)
);
}
#[test]
fn rejects_zero_time_cost() {
let mut out = [0u8; 32];
assert_eq!(
balloon_sha256(b"pw", b"salt", 8, 0, &mut out),
Err(CryptoError::BadInput)
);
}
#[test]
fn rejects_wrong_output_len() {
let mut short = [0u8; 16];
assert_eq!(
balloon_sha256(b"pw", b"salt", 8, 3, &mut short),
Err(CryptoError::BadInput)
);
let mut long = [0u8; 64];
assert_eq!(
balloon_sha256(b"pw", b"salt", 8, 3, &mut long),
Err(CryptoError::BadInput)
);
}
#[test]
fn space_cost_one_is_valid() {
let mut out = [0u8; 32];
balloon_sha256(b"pw", b"salt", 1, 2, &mut out).expect("space_cost=1");
assert_ne!(out, [0u8; 32]);
}
#[test]
fn sha512_variant_runs() {
let mut out = [0u8; 64];
balloon_sha512(b"password", b"salt", 8, 3, &mut out).expect("sha512");
assert_ne!(out, [0u8; 64]);
}
#[test]
fn secret_wrappers_match_buffer_api() {
let mut direct = [0u8; 32];
balloon_sha256(b"pw", b"salt", 8, 3, &mut direct).expect("direct");
let secret = balloon_sha256_secret(b"pw", b"salt", 8, 3).expect("secret");
assert_eq!(secret.as_bytes(), &direct[..]);
let mut direct512 = [0u8; 64];
balloon_sha512(b"pw", b"salt", 8, 3, &mut direct512).expect("direct512");
let secret512 = balloon_sha512_secret(b"pw", b"salt", 8, 3).expect("secret512");
assert_eq!(secret512.as_bytes(), &direct512[..]);
}
#[test]
fn le_digest_mod_matches_reference_semantics() {
let mut d = [0u8; 32];
d[0] = 1;
assert_eq!(le_digest_mod(&d, 8), 1);
let mut d2 = [0u8; 32];
d2[1] = 1;
assert_eq!(le_digest_mod(&d2, 8), 0);
assert_eq!(le_digest_mod(&d, 1), 0);
}
#[test]
fn params_validation_and_presets() {
assert!(BalloonParams::new(0, 1).is_err());
assert!(BalloonParams::new(1, 0).is_err());
assert!(BalloonParams::new(8, 3).is_ok());
let i = BalloonParams::interactive();
let m = BalloonParams::moderate();
let s = BalloonParams::sensitive();
assert!(s.space_cost > m.space_cost);
assert!(m.space_cost > i.space_cost);
assert_eq!(i.parallelism(), Some(1));
assert!(i.memory_cost().is_some());
assert_eq!(i.time_cost(), Some(3));
}
#[test]
fn hasher_trait_surface() {
let hasher = BalloonHasher::new_sha256(BalloonParams {
space_cost: 8,
time_cost: 3,
});
assert_eq!(hasher.name(), "balloon-sha256");
assert_eq!(hasher.output_len(), 32);
let mut out = [0u8; 32];
hasher
.hash_password(b"pw", b"salt", &hasher.params, &mut out)
.expect("hash");
let mut direct = [0u8; 32];
balloon_sha256(b"pw", b"salt", 8, 3, &mut direct).expect("direct");
assert_eq!(out, direct, "hasher must match standalone fn");
let hasher512 = BalloonHasher::new_sha512(BalloonParams {
space_cost: 8,
time_cost: 3,
});
assert_eq!(hasher512.name(), "balloon-sha512");
assert_eq!(hasher512.output_len(), 64);
}
#[test]
fn verify_password_round_trip() {
use crate::verify_password;
let hasher = BalloonHasher::new_sha256(BalloonParams {
space_cost: 8,
time_cost: 3,
});
let salt = b"0123456789abcdef";
let mut expected = [0u8; 32];
hasher
.hash_password(b"correct horse", salt, &hasher.params, &mut expected)
.expect("hash");
verify_password(&hasher, b"correct horse", salt, &expected).expect("must accept");
assert!(verify_password(&hasher, b"wrong", salt, &expected).is_err());
}
}