oxicrypto_kdf/balloon.rs
1#![forbid(unsafe_code)]
2
3//! Balloon memory-hard password hashing for the OxiCrypto stack.
4//!
5//! Pure-Rust implementation of the **single-buffer Balloon** function
6//! (Algorithm 1) from Boneh, Corrigan-Gibbs & Schechter,
7//! *"Balloon Hashing: A Memory-Hard Function Providing Provable Protection
8//! Against Sequential Attacks"* (ASIACRYPT 2016,
9//! <https://eprint.iacr.org/2016/027>).
10//!
11//! Balloon is a memory-hard, cache-hard password-hashing / key-stretching
12//! function that can be built on top of any standard cryptographic hash. This
13//! module instantiates it over **SHA-256** ([`balloon_sha256`]) and
14//! **SHA-512** ([`balloon_sha512`]) using the [`sha2`] crate.
15//!
16//! # Construction
17//!
18//! Let `H` be the underlying hash, `cnt` a little-endian `u64` counter that is
19//! incremented after every hash invocation, `space_cost` (`s`) the number of
20//! hash-sized blocks held in memory, `time_cost` (`t`) the number of mixing
21//! rounds, and `delta = 3` the number of pseudo-random dependencies per block.
22//!
23//! 1. **Expand** — fill the working buffer:
24//! - `buf[0] = H(cnt++ ‖ password ‖ salt)`
25//! - `buf[m] = H(cnt++ ‖ buf[m-1])` for `m ∈ [1, s)`
26//! 2. **Mix** — for `round ∈ [0, t)`, for `m ∈ [0, s)`:
27//! - `buf[m] = H(cnt++ ‖ buf[(m-1) mod s] ‖ buf[m])`
28//! - then `delta` times (`i ∈ [0, delta)`):
29//! - `idx_block = H(LE64(round) ‖ LE64(m) ‖ LE64(i))`
30//! - `other = (H(cnt++ ‖ salt ‖ idx_block) interpreted as a little-endian
31//! integer) mod s`
32//! - `buf[m] = H(cnt++ ‖ buf[m] ‖ buf[other])`
33//! 3. **Extract** — output `buf[s-1]`.
34//!
35//! All integers fed to `H` are length-free, fixed-width **8-byte
36//! little-endian** values; byte strings are concatenated verbatim. This matches
37//! the authors' reference implementation byte-for-byte (verified against the
38//! published reference vectors — see `tests/kat_balloon.rs`).
39//!
40//! # Security parameters
41//!
42//! `space_cost` dominates the memory footprint (`space_cost × digest_len`
43//! bytes). Choose `space_cost` and `time_cost` so the product meets your
44//! latency/memory budget; the paper recommends `t ≥ 1` and a `space_cost`
45//! large enough to make the working set cache-hard (tens of thousands of
46//! blocks for password storage).
47//!
48//! The working buffer and the returned digest are wrapped in
49//! [`oxicrypto_core::SecretVec`] so intermediate key material is
50//! zeroized on drop.
51
52use oxicrypto_core::{
53 CryptoError, PasswordHash as PasswordHashTrait, PasswordHashParams, SecretVec, Zeroize,
54};
55use sha2::{Digest, Sha256, Sha512};
56
57/// Number of pseudo-random dependencies mixed into each block per round.
58///
59/// Fixed at `3` per the Balloon paper's recommended default (`delta = 3`).
60pub const BALLOON_DELTA: u64 = 3;
61
62// ---------------------------------------------------------------------------
63// Generic core over an abstract one-shot hash
64// ---------------------------------------------------------------------------
65
66/// A one-shot fixed-output hash used to instantiate Balloon.
67///
68/// Implemented for SHA-256 and SHA-512 below. The associated `DIGEST_LEN`
69/// lets the core size its working buffer without heap reallocation churn.
70trait BalloonHash {
71 /// Length of the digest in bytes.
72 const DIGEST_LEN: usize;
73
74 /// Hash `data` and write the digest into `out` (which is `DIGEST_LEN` long).
75 fn hash_into(data: &[u8], out: &mut [u8]);
76}
77
78/// SHA-256 instantiation marker.
79struct Sha256Hash;
80/// SHA-512 instantiation marker.
81struct Sha512Hash;
82
83impl BalloonHash for Sha256Hash {
84 const DIGEST_LEN: usize = 32;
85
86 fn hash_into(data: &[u8], out: &mut [u8]) {
87 let digest = Sha256::digest(data);
88 out.copy_from_slice(&digest);
89 }
90}
91
92impl BalloonHash for Sha512Hash {
93 const DIGEST_LEN: usize = 64;
94
95 fn hash_into(data: &[u8], out: &mut [u8]) {
96 let digest = Sha512::digest(data);
97 out.copy_from_slice(&digest);
98 }
99}
100
101/// A reusable hash-input scratch buffer.
102///
103/// Integers are appended as 8-byte little-endian values and byte slices are
104/// appended verbatim, exactly matching the reference Balloon serialization.
105struct HashInput {
106 buf: Vec<u8>,
107}
108
109impl HashInput {
110 fn new() -> Self {
111 Self {
112 buf: Vec::with_capacity(160),
113 }
114 }
115
116 /// Reset for a fresh hash invocation.
117 fn clear(&mut self) {
118 self.buf.clear();
119 }
120
121 /// Append a `u64` as 8 little-endian bytes (the reference counter/integer
122 /// encoding).
123 fn push_u64(&mut self, value: u64) {
124 self.buf.extend_from_slice(&value.to_le_bytes());
125 }
126
127 /// Append raw bytes verbatim.
128 fn push_bytes(&mut self, bytes: &[u8]) {
129 self.buf.extend_from_slice(bytes);
130 }
131
132 fn as_slice(&self) -> &[u8] {
133 &self.buf
134 }
135}
136
137/// Compute `int.from_bytes(digest, "little") mod modulus`.
138///
139/// `digest` is interpreted as a little-endian big integer; the result is taken
140/// modulo `modulus` without arbitrary-precision arithmetic by streaming from
141/// the most-significant byte (the last byte of a little-endian encoding) to the
142/// least-significant byte. This reproduces the reference implementation's
143/// `int.from_bytes(h, "little") % space_cost` exactly.
144fn le_digest_mod(digest: &[u8], modulus: u64) -> u64 {
145 // `modulus` is always `>= 1` here (validated by callers), so the running
146 // accumulator stays `< modulus <= u64::MAX`, and `acc * 256 + byte` cannot
147 // overflow because `acc <= modulus - 1` and `modulus` fits in `u64` with
148 // room: we reduce after each step. To stay overflow-safe for large moduli
149 // we use `u128` for the intermediate.
150 let m = u128::from(modulus);
151 let mut acc: u128 = 0;
152 for &byte in digest.iter().rev() {
153 acc = (acc * 256 + u128::from(byte)) % m;
154 }
155 // acc < m <= u64::MAX, so this never truncates.
156 acc as u64
157}
158
159/// Core single-buffer Balloon (Algorithm 1) over hash `H`.
160///
161/// Writes `H::DIGEST_LEN` bytes into `out`. `space_cost` and `time_cost` must
162/// be `>= 1`; `out.len()` must equal `H::DIGEST_LEN`.
163fn balloon_core<H: BalloonHash>(
164 password: &[u8],
165 salt: &[u8],
166 space_cost: u64,
167 time_cost: u64,
168 out: &mut [u8],
169) -> Result<(), CryptoError> {
170 if space_cost == 0 || time_cost == 0 {
171 return Err(CryptoError::BadInput);
172 }
173 if out.len() != H::DIGEST_LEN {
174 return Err(CryptoError::BadInput);
175 }
176
177 let digest_len = H::DIGEST_LEN;
178
179 // `space_cost` blocks must fit in addressable memory. Guard the allocation
180 // size up front so an absurd parameter returns an error instead of
181 // attempting a panicking allocation.
182 let total_bytes = (space_cost as usize)
183 .checked_mul(digest_len)
184 .ok_or(CryptoError::BadInput)?;
185
186 // Working buffer of `space_cost` contiguous digest-sized blocks, held in a
187 // zeroize-on-drop wrapper so all intermediate block material is wiped when
188 // this function returns (including via the `?` early exits below).
189 let mut work = ZeroizingBuf::new(total_bytes);
190 let buf = work.as_mut_slice();
191
192 let mut input = HashInput::new();
193 let mut digest = ZeroizingBuf::new(digest_len);
194 let mut cnt: u64 = 0;
195
196 // ── Expand ──────────────────────────────────────────────────────────────
197 // buf[0] = H(cnt++ ‖ password ‖ salt)
198 input.clear();
199 input.push_u64(cnt);
200 input.push_bytes(password);
201 input.push_bytes(salt);
202 H::hash_into(input.as_slice(), &mut buf[0..digest_len]);
203 cnt += 1;
204
205 // buf[m] = H(cnt++ ‖ buf[m-1]) for m in [1, space_cost)
206 for m in 1..(space_cost as usize) {
207 let prev_start = (m - 1) * digest_len;
208 input.clear();
209 input.push_u64(cnt);
210 // Read previous block into `digest` to avoid aliasing the &mut buf.
211 digest
212 .as_mut_slice()
213 .copy_from_slice(&buf[prev_start..prev_start + digest_len]);
214 input.push_bytes(digest.as_slice());
215 let cur_start = m * digest_len;
216 H::hash_into(
217 input.as_slice(),
218 &mut buf[cur_start..cur_start + digest_len],
219 );
220 cnt += 1;
221 }
222
223 // ── Mix ─────────────────────────────────────────────────────────────────
224 let space_usize = space_cost as usize;
225 for round in 0..time_cost {
226 for m in 0..space_usize {
227 // buf[m] = H(cnt++ ‖ buf[(m-1) mod space_cost] ‖ buf[m])
228 let prev_idx = if m == 0 { space_usize - 1 } else { m - 1 };
229 let prev_start = prev_idx * digest_len;
230 let cur_start = m * digest_len;
231
232 input.clear();
233 input.push_u64(cnt);
234 // Snapshot buf[prev] and buf[m] into owned bytes (prev may equal m
235 // when space_cost == 1).
236 let mut prev_block = [0u8; 64];
237 let mut cur_block = [0u8; 64];
238 prev_block[..digest_len].copy_from_slice(&buf[prev_start..prev_start + digest_len]);
239 cur_block[..digest_len].copy_from_slice(&buf[cur_start..cur_start + digest_len]);
240 input.push_bytes(&prev_block[..digest_len]);
241 input.push_bytes(&cur_block[..digest_len]);
242 H::hash_into(
243 input.as_slice(),
244 &mut buf[cur_start..cur_start + digest_len],
245 );
246 cnt += 1;
247
248 // delta pseudo-random dependencies.
249 for i in 0..BALLOON_DELTA {
250 // idx_block = H(LE64(round) ‖ LE64(m) ‖ LE64(i)) (no counter)
251 input.clear();
252 input.push_u64(round);
253 input.push_u64(m as u64);
254 input.push_u64(i);
255 let mut idx_block = [0u8; 64];
256 H::hash_into(input.as_slice(), &mut idx_block[..digest_len]);
257
258 // other = (H(cnt++ ‖ salt ‖ idx_block) as LE int) mod space_cost
259 input.clear();
260 input.push_u64(cnt);
261 input.push_bytes(salt);
262 input.push_bytes(&idx_block[..digest_len]);
263 H::hash_into(input.as_slice(), digest.as_mut_slice());
264 cnt += 1;
265 let other = le_digest_mod(digest.as_slice(), space_cost) as usize;
266
267 // buf[m] = H(cnt++ ‖ buf[m] ‖ buf[other])
268 let other_start = other * digest_len;
269 let mut m_block = [0u8; 64];
270 let mut other_block = [0u8; 64];
271 m_block[..digest_len].copy_from_slice(&buf[cur_start..cur_start + digest_len]);
272 other_block[..digest_len]
273 .copy_from_slice(&buf[other_start..other_start + digest_len]);
274 input.clear();
275 input.push_u64(cnt);
276 input.push_bytes(&m_block[..digest_len]);
277 input.push_bytes(&other_block[..digest_len]);
278 H::hash_into(
279 input.as_slice(),
280 &mut buf[cur_start..cur_start + digest_len],
281 );
282 cnt += 1;
283 }
284 }
285 }
286
287 // ── Extract ───────────────────────────────────────────────────────────────
288 let last_start = (space_usize - 1) * digest_len;
289 out.copy_from_slice(&buf[last_start..last_start + digest_len]);
290
291 // `work` and `digest` are zeroized on drop here.
292 Ok(())
293}
294
295/// A heap byte buffer that is zeroized on drop and offers in-place mutable
296/// slice access.
297///
298/// [`SecretVec`](oxicrypto_core::SecretVec) is intentionally append-free and
299/// exposes only an immutable view, so the Balloon mixing loop — which rewrites
300/// blocks in place — uses this local zeroize-on-drop newtype for its working
301/// memory and intermediate digest. The final output is still returned via
302/// `SecretVec` by the `*_secret` wrappers.
303///
304/// `Drop` zeroizes via [`oxicrypto_core::Zeroize`]; the derive macros are not
305/// used to avoid taking a direct `zeroize` dependency.
306struct ZeroizingBuf {
307 bytes: Vec<u8>,
308}
309
310impl ZeroizingBuf {
311 fn new(len: usize) -> Self {
312 Self {
313 bytes: vec![0u8; len],
314 }
315 }
316
317 fn as_mut_slice(&mut self) -> &mut [u8] {
318 &mut self.bytes
319 }
320
321 fn as_slice(&self) -> &[u8] {
322 &self.bytes
323 }
324}
325
326impl Drop for ZeroizingBuf {
327 fn drop(&mut self) {
328 self.bytes.zeroize();
329 }
330}
331
332// ---------------------------------------------------------------------------
333// Public function API
334// ---------------------------------------------------------------------------
335
336/// Balloon password hash over **SHA-256**, writing 32 bytes into `out`.
337///
338/// Implements the single-buffer Balloon (Algorithm 1) with `delta = 3`
339/// ([`BALLOON_DELTA`]).
340///
341/// # Arguments
342/// - `password` — secret password / input keying material
343/// - `salt` — salt (use a unique, random salt per password)
344/// - `space_cost` — number of 32-byte blocks held in memory (`>= 1`)
345/// - `time_cost` — number of mixing rounds (`>= 1`)
346/// - `out` — output buffer; **must be exactly 32 bytes**
347///
348/// # Errors
349/// Returns [`CryptoError::BadInput`] if `space_cost == 0`, `time_cost == 0`,
350/// `out.len() != 32`, or `space_cost` is so large the working buffer cannot be
351/// sized.
352#[must_use = "balloon hash result must be checked"]
353pub fn balloon_sha256(
354 password: &[u8],
355 salt: &[u8],
356 space_cost: u64,
357 time_cost: u64,
358 out: &mut [u8],
359) -> Result<(), CryptoError> {
360 balloon_core::<Sha256Hash>(password, salt, space_cost, time_cost, out)
361}
362
363/// Balloon password hash over **SHA-512**, writing 64 bytes into `out`.
364///
365/// Implements the single-buffer Balloon (Algorithm 1) with `delta = 3`
366/// ([`BALLOON_DELTA`]).
367///
368/// # Arguments
369/// - `password` — secret password / input keying material
370/// - `salt` — salt (use a unique, random salt per password)
371/// - `space_cost` — number of 64-byte blocks held in memory (`>= 1`)
372/// - `time_cost` — number of mixing rounds (`>= 1`)
373/// - `out` — output buffer; **must be exactly 64 bytes**
374///
375/// # Errors
376/// Returns [`CryptoError::BadInput`] if `space_cost == 0`, `time_cost == 0`,
377/// `out.len() != 64`, or `space_cost` is so large the working buffer cannot be
378/// sized.
379#[must_use = "balloon hash result must be checked"]
380pub fn balloon_sha512(
381 password: &[u8],
382 salt: &[u8],
383 space_cost: u64,
384 time_cost: u64,
385 out: &mut [u8],
386) -> Result<(), CryptoError> {
387 balloon_core::<Sha512Hash>(password, salt, space_cost, time_cost, out)
388}
389
390/// Balloon-SHA-256 hash returning the 32-byte digest wrapped in a
391/// [`SecretVec`] that zeroizes on drop.
392///
393/// # Errors
394/// See [`balloon_sha256`].
395#[must_use = "derived key should be used"]
396pub fn balloon_sha256_secret(
397 password: &[u8],
398 salt: &[u8],
399 space_cost: u64,
400 time_cost: u64,
401) -> Result<SecretVec, CryptoError> {
402 let mut out = vec![0u8; Sha256Hash::DIGEST_LEN];
403 balloon_core::<Sha256Hash>(password, salt, space_cost, time_cost, &mut out)?;
404 Ok(SecretVec::new(out))
405}
406
407/// Balloon-SHA-512 hash returning the 64-byte digest wrapped in a
408/// [`SecretVec`] that zeroizes on drop.
409///
410/// # Errors
411/// See [`balloon_sha512`].
412#[must_use = "derived key should be used"]
413pub fn balloon_sha512_secret(
414 password: &[u8],
415 salt: &[u8],
416 space_cost: u64,
417 time_cost: u64,
418) -> Result<SecretVec, CryptoError> {
419 let mut out = vec![0u8; Sha512Hash::DIGEST_LEN];
420 balloon_core::<Sha512Hash>(password, salt, space_cost, time_cost, &mut out)?;
421 Ok(SecretVec::new(out))
422}
423
424// ---------------------------------------------------------------------------
425// BalloonParams + BalloonHasher — PasswordHash trait surface
426// ---------------------------------------------------------------------------
427
428/// Underlying hash selector for [`BalloonHasher`].
429#[derive(Debug, Clone, Copy, PartialEq, Eq)]
430pub enum BalloonVariant {
431 /// Balloon over SHA-256 (32-byte output).
432 Sha256,
433 /// Balloon over SHA-512 (64-byte output).
434 Sha512,
435}
436
437/// Cost parameters for Balloon hashing.
438///
439/// Balloon's cost is governed by `space_cost` (memory, in digest-sized blocks)
440/// and `time_cost` (mixing rounds); `delta` is fixed at [`BALLOON_DELTA`].
441#[derive(Debug, Clone, Copy)]
442pub struct BalloonParams {
443 /// Number of digest-sized blocks held in memory (`>= 1`).
444 pub space_cost: u64,
445 /// Number of mixing rounds (`>= 1`).
446 pub time_cost: u64,
447}
448
449impl BalloonParams {
450 /// Create parameters, validating that both costs are `>= 1`.
451 ///
452 /// # Errors
453 /// Returns [`CryptoError::BadInput`] if `space_cost == 0` or
454 /// `time_cost == 0`.
455 pub fn new(space_cost: u64, time_cost: u64) -> Result<Self, CryptoError> {
456 if space_cost == 0 || time_cost == 0 {
457 return Err(CryptoError::BadInput);
458 }
459 Ok(Self {
460 space_cost,
461 time_cost,
462 })
463 }
464
465 /// Interactive login preset — `space_cost = 16384` blocks, `time_cost = 3`.
466 ///
467 /// With SHA-256 this is ≈ 512 KiB of working memory.
468 #[must_use]
469 pub fn interactive() -> Self {
470 Self {
471 space_cost: 16_384,
472 time_cost: 3,
473 }
474 }
475
476 /// Moderate preset — `space_cost = 65536` blocks, `time_cost = 3`.
477 ///
478 /// With SHA-256 this is ≈ 2 MiB of working memory.
479 #[must_use]
480 pub fn moderate() -> Self {
481 Self {
482 space_cost: 65_536,
483 time_cost: 3,
484 }
485 }
486
487 /// Sensitive (high-security) preset — `space_cost = 262144` blocks,
488 /// `time_cost = 3`.
489 ///
490 /// With SHA-256 this is ≈ 8 MiB of working memory.
491 #[must_use]
492 pub fn sensitive() -> Self {
493 Self {
494 space_cost: 262_144,
495 time_cost: 3,
496 }
497 }
498}
499
500impl PasswordHashParams for BalloonParams {
501 /// Memory cost expressed in KiB, assuming a 32-byte (SHA-256) block. This
502 /// is an approximation for reporting; the actual footprint for SHA-512 is
503 /// twice as large.
504 fn memory_cost(&self) -> Option<u32> {
505 let kib = self.space_cost.saturating_mul(32) / 1024;
506 u32::try_from(kib).ok()
507 }
508
509 fn time_cost(&self) -> Option<u32> {
510 u32::try_from(self.time_cost).ok()
511 }
512
513 fn parallelism(&self) -> Option<u32> {
514 // Single-buffer Balloon (Algorithm 1) is inherently sequential.
515 Some(1)
516 }
517}
518
519/// A Balloon password hasher bundling its variant and cost parameters.
520///
521/// Implements [`PasswordHash`](oxicrypto_core::PasswordHash) so it composes
522/// with [`crate::verify_password`].
523///
524/// # Design note — `params` argument is ignored
525/// [`PasswordHash::hash_password`](oxicrypto_core::PasswordHash::hash_password)
526/// accepts a `params: &dyn PasswordHashParams`, but this implementation uses
527/// `self.params` instead. The output length is fixed by the variant (32 bytes
528/// for SHA-256, 64 for SHA-512); `out` must match.
529#[derive(Debug, Clone, Copy)]
530pub struct BalloonHasher {
531 /// Underlying hash variant.
532 pub variant: BalloonVariant,
533 /// Cost parameters.
534 pub params: BalloonParams,
535}
536
537impl BalloonHasher {
538 /// Create a Balloon-SHA-256 hasher with the given parameters.
539 #[must_use]
540 pub fn new_sha256(params: BalloonParams) -> Self {
541 Self {
542 variant: BalloonVariant::Sha256,
543 params,
544 }
545 }
546
547 /// Create a Balloon-SHA-512 hasher with the given parameters.
548 #[must_use]
549 pub fn new_sha512(params: BalloonParams) -> Self {
550 Self {
551 variant: BalloonVariant::Sha512,
552 params,
553 }
554 }
555
556 /// Digest length in bytes for this hasher's variant.
557 #[must_use]
558 pub fn output_len(&self) -> usize {
559 match self.variant {
560 BalloonVariant::Sha256 => Sha256Hash::DIGEST_LEN,
561 BalloonVariant::Sha512 => Sha512Hash::DIGEST_LEN,
562 }
563 }
564}
565
566impl PasswordHashTrait for BalloonHasher {
567 fn name(&self) -> &'static str {
568 match self.variant {
569 BalloonVariant::Sha256 => "balloon-sha256",
570 BalloonVariant::Sha512 => "balloon-sha512",
571 }
572 }
573
574 fn hash_password(
575 &self,
576 password: &[u8],
577 salt: &[u8],
578 _params: &dyn PasswordHashParams,
579 out: &mut [u8],
580 ) -> Result<(), CryptoError> {
581 match self.variant {
582 BalloonVariant::Sha256 => balloon_sha256(
583 password,
584 salt,
585 self.params.space_cost,
586 self.params.time_cost,
587 out,
588 ),
589 BalloonVariant::Sha512 => balloon_sha512(
590 password,
591 salt,
592 self.params.space_cost,
593 self.params.time_cost,
594 out,
595 ),
596 }
597 }
598}
599
600#[cfg(test)]
601mod tests {
602 use super::*;
603
604 // Reference vectors are validated in tests/kat_balloon.rs; here we cover
605 // structural properties and the trait surface with tiny parameters.
606
607 #[test]
608 fn determinism_same_inputs() {
609 let mut a = [0u8; 32];
610 let mut b = [0u8; 32];
611 balloon_sha256(b"password", b"salt", 8, 3, &mut a).expect("a");
612 balloon_sha256(b"password", b"salt", 8, 3, &mut b).expect("b");
613 assert_eq!(a, b, "balloon must be deterministic");
614 assert_ne!(a, [0u8; 32]);
615 }
616
617 #[test]
618 fn different_salt_differs() {
619 let mut a = [0u8; 32];
620 let mut b = [0u8; 32];
621 balloon_sha256(b"password", b"salt", 8, 3, &mut a).expect("a");
622 balloon_sha256(b"password", b"pepper", 8, 3, &mut b).expect("b");
623 assert_ne!(a, b, "different salt must change output");
624 }
625
626 #[test]
627 fn rejects_zero_space_cost() {
628 let mut out = [0u8; 32];
629 assert_eq!(
630 balloon_sha256(b"pw", b"salt", 0, 3, &mut out),
631 Err(CryptoError::BadInput)
632 );
633 }
634
635 #[test]
636 fn rejects_zero_time_cost() {
637 let mut out = [0u8; 32];
638 assert_eq!(
639 balloon_sha256(b"pw", b"salt", 8, 0, &mut out),
640 Err(CryptoError::BadInput)
641 );
642 }
643
644 #[test]
645 fn rejects_wrong_output_len() {
646 let mut short = [0u8; 16];
647 assert_eq!(
648 balloon_sha256(b"pw", b"salt", 8, 3, &mut short),
649 Err(CryptoError::BadInput)
650 );
651 let mut long = [0u8; 64];
652 assert_eq!(
653 balloon_sha256(b"pw", b"salt", 8, 3, &mut long),
654 Err(CryptoError::BadInput)
655 );
656 }
657
658 #[test]
659 fn space_cost_one_is_valid() {
660 // space_cost == 1 means buf[(m-1) mod 1] == buf[0] == buf[m]; the
661 // construction must still run without panic.
662 let mut out = [0u8; 32];
663 balloon_sha256(b"pw", b"salt", 1, 2, &mut out).expect("space_cost=1");
664 assert_ne!(out, [0u8; 32]);
665 }
666
667 #[test]
668 fn sha512_variant_runs() {
669 let mut out = [0u8; 64];
670 balloon_sha512(b"password", b"salt", 8, 3, &mut out).expect("sha512");
671 assert_ne!(out, [0u8; 64]);
672 }
673
674 #[test]
675 fn secret_wrappers_match_buffer_api() {
676 let mut direct = [0u8; 32];
677 balloon_sha256(b"pw", b"salt", 8, 3, &mut direct).expect("direct");
678 let secret = balloon_sha256_secret(b"pw", b"salt", 8, 3).expect("secret");
679 assert_eq!(secret.as_bytes(), &direct[..]);
680
681 let mut direct512 = [0u8; 64];
682 balloon_sha512(b"pw", b"salt", 8, 3, &mut direct512).expect("direct512");
683 let secret512 = balloon_sha512_secret(b"pw", b"salt", 8, 3).expect("secret512");
684 assert_eq!(secret512.as_bytes(), &direct512[..]);
685 }
686
687 #[test]
688 fn le_digest_mod_matches_reference_semantics() {
689 // int.from_bytes([1,0,0,...], "little") == 1, mod 8 == 1.
690 let mut d = [0u8; 32];
691 d[0] = 1;
692 assert_eq!(le_digest_mod(&d, 8), 1);
693 // int.from_bytes([0,1,0,...], "little") == 256, mod 8 == 0.
694 let mut d2 = [0u8; 32];
695 d2[1] = 1;
696 assert_eq!(le_digest_mod(&d2, 8), 0);
697 // mod 1 is always 0.
698 assert_eq!(le_digest_mod(&d, 1), 0);
699 }
700
701 #[test]
702 fn params_validation_and_presets() {
703 assert!(BalloonParams::new(0, 1).is_err());
704 assert!(BalloonParams::new(1, 0).is_err());
705 assert!(BalloonParams::new(8, 3).is_ok());
706 let i = BalloonParams::interactive();
707 let m = BalloonParams::moderate();
708 let s = BalloonParams::sensitive();
709 assert!(s.space_cost > m.space_cost);
710 assert!(m.space_cost > i.space_cost);
711 assert_eq!(i.parallelism(), Some(1));
712 assert!(i.memory_cost().is_some());
713 assert_eq!(i.time_cost(), Some(3));
714 }
715
716 #[test]
717 fn hasher_trait_surface() {
718 let hasher = BalloonHasher::new_sha256(BalloonParams {
719 space_cost: 8,
720 time_cost: 3,
721 });
722 assert_eq!(hasher.name(), "balloon-sha256");
723 assert_eq!(hasher.output_len(), 32);
724 let mut out = [0u8; 32];
725 hasher
726 .hash_password(b"pw", b"salt", &hasher.params, &mut out)
727 .expect("hash");
728 let mut direct = [0u8; 32];
729 balloon_sha256(b"pw", b"salt", 8, 3, &mut direct).expect("direct");
730 assert_eq!(out, direct, "hasher must match standalone fn");
731
732 let hasher512 = BalloonHasher::new_sha512(BalloonParams {
733 space_cost: 8,
734 time_cost: 3,
735 });
736 assert_eq!(hasher512.name(), "balloon-sha512");
737 assert_eq!(hasher512.output_len(), 64);
738 }
739
740 #[test]
741 fn verify_password_round_trip() {
742 use crate::verify_password;
743 let hasher = BalloonHasher::new_sha256(BalloonParams {
744 space_cost: 8,
745 time_cost: 3,
746 });
747 let salt = b"0123456789abcdef";
748 let mut expected = [0u8; 32];
749 hasher
750 .hash_password(b"correct horse", salt, &hasher.params, &mut expected)
751 .expect("hash");
752 verify_password(&hasher, b"correct horse", salt, &expected).expect("must accept");
753 assert!(verify_password(&hasher, b"wrong", salt, &expected).is_err());
754 }
755}