Skip to main content

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}