Balloon Hashing
Pure Ruby implementation of the Balloon Hashing algorithm (Boneh, Corrigan-Gibbs, Schechter 2016) — a memory-hard password hashing function with provable security guarantees against sequential attacks.
Why Balloon Hashing?
- Memory-hard — resistant to GPU and ASIC attacks by requiring large amounts of memory to compute
- Provably secure — the only password hashing function with formal proofs of memory-hardness
- Simple design — built on standard cryptographic primitives (SHA-256, SHA-512, BLAKE2b) with no custom block ciphers
-
Tunable — independent space cost (
s_cost) and time cost (t_cost) parameters let you balance security against performance
Installation
Add to your Gemfile:
gem "balloon_hashing"Or install directly:
gem install balloon_hashing
Requirements: Ruby >= 2.7.0, OpenSSL
Quick Start
require "balloon_hashing"
# Hash a password
hash = BalloonHashing.create("my password")
# => "$balloon$v=1$alg=sha256,s=1024,t=3$..."
# Verify a password
BalloonHashing.verify("my password", hash) #=> true
BalloonHashing.verify("wrong password", hash) #=> falseUsage
Basic API
The simplest way to use the gem — module-level convenience methods with sensible defaults:
hash = BalloonHashing.create("password")
BalloonHashing.verify("password", hash) #=> trueCustom Parameters
Adjust the cost parameters and algorithm to fit your security requirements:
hash = BalloonHashing.create(
"password",
s_cost: 2048, # space cost — number of blocks in memory (default: 1024)
t_cost: 4, # time cost — number of mixing rounds (default: 3)
algorithm: "sha512" # hash algorithm (default: "sha256")
)
BalloonHashing.verify("password", hash) #=> trueSupported Algorithms
| Algorithm | Output Size | Notes |
|---|---|---|
sha256 |
32 bytes | Default, widely available |
sha512 |
64 bytes | Larger output, slightly slower |
blake2b |
64 bytes | Fast, requires OpenSSL with BLAKE2 support |
Instance-Based API with Hasher
For applications that need a configured hasher instance (e.g. different cost settings for different user roles):
hasher = BalloonHashing::Hasher.new(
s_cost: 2048,
t_cost: 4,
algorithm: "sha512"
)
hash = hasher.create("password")
hasher.verify("password", hash) #=> true
hasher.cost_matches?(hash) #=> trueUpgrading Cost Parameters (Rehash on Login)
When you increase cost parameters, existing hashes still verify but use the old settings. Use needs_rehash? to transparently upgrade hashes when users log in:
hasher = BalloonHashing::Hasher.new(s_cost: 2048, t_cost: 4)
if hasher.verify(password, stored_hash)
if hasher.needs_rehash?(stored_hash)
# Re-hash with current (stronger) parameters
stored_hash = hasher.create(password)
save_to_database(stored_hash)
end
log_user_in
endStrict Verification with verify!
If you want to ensure the hash was created with the hasher's exact configuration:
hasher = BalloonHashing::Hasher.new(s_cost: 2048, t_cost: 4)
hasher.verify("password", hash) # Works regardless of hash parameters
hasher.verify!("password", hash) # Raises BalloonHashing::InvalidHash if parameters don't matchPepper (Server-Side Secret)
Add an optional server-side secret that is mixed into the hash via HMAC but never stored in the output string. This means a leaked database alone is not enough to crack passwords:
# Module-level API
hash = BalloonHashing.create("password", pepper: ENV["PASSWORD_PEPPER"])
BalloonHashing.verify("password", hash, pepper: ENV["PASSWORD_PEPPER"])
# Instance-based API — pepper is set once at construction
hasher = BalloonHashing::Hasher.new(s_cost: 2048, t_cost: 4, pepper: ENV["PASSWORD_PEPPER"])
hash = hasher.create("password")
hasher.verify("password", hash) #=> trueNote: The pepper must be the same for both
createandverify. If you lose the pepper, all existing hashes become unverifiable. Store it securely (e.g. environment variable, secrets manager) separate from the database.
Low-Level Core API
For advanced use cases, you can call the core algorithm directly:
raw_hash = BalloonHashing::Core.balloon(
"password", # password (String)
salt, # salt (String, random bytes)
1024, # s_cost (Integer)
3, # t_cost (Integer)
"sha256" # algorithm (String)
)
# => raw binary hash bytesThis returns raw bytes and does not generate salts or produce encoded strings.
Hash Format
Encoded hashes follow a structured, self-describing format:
$balloon$v=1$alg=sha256,s=1024,t=3$<base64_salt>$<base64_hash>
| Field | Description |
|---|---|
$balloon$ |
Identifier prefix |
v=1 |
Format version |
alg=sha256 |
Hash algorithm |
s=1024 |
Space cost |
t=3 |
Time cost |
<base64_salt> |
Base64-encoded salt |
<base64_hash> |
Base64-encoded hash output |
The format is forward-compatible — unknown parameter keys are ignored during decoding, so future versions can add new parameters without breaking existing hashes.
Cost Parameter Guidance
The right cost parameters depend on your hardware and latency budget. As a starting point:
| Use Case | s_cost |
t_cost |
Notes |
|---|---|---|---|
| Interactive login | 1024 | 3 | ~default, suitable for web apps |
| Higher security | 2048–4096 | 3–4 | More memory, similar latency |
| Offline/batch | 8192+ | 4+ | When latency is not a concern |
s_cost (space cost) controls the number of hash-sized blocks held in memory. Each block is 32 bytes (SHA-256) or 64 bytes (SHA-512/BLAKE2b), so s_cost: 1024 with SHA-256 uses ~32 KB of memory.
t_cost (time cost) controls the number of mixing rounds over the buffer. Higher values increase computation time linearly.
Benchmark on your target hardware and choose the highest values that stay within your latency budget.
Safety Limits
To prevent accidental denial-of-service from misconfigured parameters:
-
s_costis capped at2^24(~16 million blocks) -
t_costis capped at2^20(~1 million rounds)
These limits apply to both direct calls and when decoding stored hashes, protecting against crafted hash strings with extreme values.
Error Handling
BalloonHashing::Error # Base error class (inherits StandardError)
BalloonHashing::InvalidHash # Raised for malformed or invalid encoded hash stringsverify and cost_matches? return false for invalid hashes rather than raising. If you need to distinguish "wrong password" from "corrupted hash", call the Hasher#verify! variant which raises InvalidHash on parameter mismatch.
create raises ArgumentError for invalid input (non-String password, unsupported algorithm, invalid cost values).
Thread Safety
All public APIs are thread-safe:
-
BalloonHashing::Core— each call allocates its own digest and buffer; no shared state -
BalloonHashing::Password— all methods are stateless module functions -
BalloonHashing::Hasher— instances are effectively immutable after construction
Hasher instances can be safely shared across threads and stored as constants or in application configuration.
Security Notes
-
Constant-time comparison — password verification uses
OpenSSL.fixed_length_secure_compareto prevent timing attacks -
Buffer zeroing — the in-memory hash buffer is zeroed after use on a best-effort basis. Ruby's garbage collector may retain copies of intermediate data. For applications requiring stronger memory protection guarantees, consider a C extension wrapping
OPENSSL_cleanseorsodium_memzero - Pepper via HMAC — when a pepper is provided, it is mixed using HMAC-SHA256 (not simple concatenation), eliminating length-ambiguity attacks. The pepper is never included in the encoded hash output
Development
git clone https://github.com/msuliq/balloon_hashing.git
cd balloon_hashing
bundle install
bundle exec rake testReferences
- Boneh, D., Corrigan-Gibbs, H., & Schechter, S. (2016). Balloon Hashing: A Memory-Hard Function Providing Provable Protection Against Sequential Attacks. IACR Cryptology ePrint Archive.
License
Released under the MIT License.