0.0
The project is in a healthy, maintained state
Pure Ruby implementation of the Balloon Hashing algorithm (Boneh, Corrigan-Gibbs, Schechter 2016). Supports SHA-256, SHA-512, and BLAKE2b. Memory-hard with provable protection against sequential attacks.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

>= 2.0
~> 5.0
~> 13.0

Runtime

>= 0
 Project Readme

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) #=> false

Usage

Basic API

The simplest way to use the gem — module-level convenience methods with sensible defaults:

hash = BalloonHashing.create("password")
BalloonHashing.verify("password", hash) #=> true

Custom 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) #=> true

Supported 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)        #=> true

Upgrading 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
end

Strict 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 match

Pepper (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) #=> true

Note: The pepper must be the same for both create and verify. 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 bytes

This 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_cost is capped at 2^24 (~16 million blocks)
  • t_cost is capped at 2^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 strings

verify 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_compare to 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_cleanse or sodium_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 test

References

License

Released under the MIT License.