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.
- 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
Add to your Gemfile:
gem "balloon_hashing"Or install directly:
gem install balloon_hashing
Requirements: Ruby >= 2.7.0, OpenSSL
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) #=> falseThe simplest way to use the gem — module-level convenience methods with sensible defaults:
hash = BalloonHashing.create("password")
BalloonHashing.verify("password", hash) #=> trueAdjust 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) #=> true| 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 |
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) #=> trueWhen 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
endIf 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 matchAdd 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.
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.
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.
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.
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.
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).
All public APIs are thread-safe:
BalloonHashing::Core— each call allocates its own digest and buffer; no shared stateBalloonHashing::Password— all methods are stateless module functionsBalloonHashing::Hasher— instances are effectively immutable after construction
Hasher instances can be safely shared across threads and stored as constants or in application configuration.
- 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
git clone https://github.com/msuliq/balloon_hashing.git
cd balloon_hashing
bundle install
bundle exec rake test- Boneh, D., Corrigan-Gibbs, H., & Schechter, S. (2016). Balloon Hashing: A Memory-Hard Function Providing Provable Protection Against Sequential Attacks. IACR Cryptology ePrint Archive.
Released under the MIT License.