Skip to content

msuliq/yescrypt

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

yescrypt

A Ruby C extension wrapping the yescrypt password hashing algorithm.

yescrypt is the default password hash in modern Linux distributions (glibc 2.36+). It extends scrypt with pwxform for stronger time-memory tradeoff resistance, making it significantly more costly for attackers using GPUs or custom hardware.

Features

  • High-level create / verify API modeled after bcrypt-ruby
  • Low-level kdf access for custom use cases
  • Constant-time hash comparison via OpenSSL.fixed_length_secure_compare
  • Thread-safe: releases the GVL during hashing so other Ruby threads can run
  • GC-compaction safe with pinned string references
  • Sensitive buffers are zeroed after use
  • Supports all three yescrypt flavors: WORM, RW, and DEFAULTS

Requirements

  • Ruby >= 2.7.2
  • A C99-compatible compiler (gcc, clang, etc.)

Installation

Add to your Gemfile:

gem "yescrypt"

Then run:

bundle install

Or install directly:

gem install yescrypt

The C extension compiles automatically during installation. No external libraries are needed -- yescrypt, SHA-256, HMAC, and PBKDF2 are all bundled.

Quick Start

require "yescrypt"

# Hash a password (uses secure defaults: N=2^12, r=32, p=1)
hash = Yescrypt.create("my secret password")
# => "$y$j..."

# Verify a password against a stored hash
Yescrypt.verify("my secret password", hash)  # => true
Yescrypt.verify("wrong password", hash)       # => false

# Check if a hash needs rehashing (e.g., after changing cost parameters)
Yescrypt.cost_matches?(hash)  # => true (matches current defaults)
Yescrypt.cost_matches?(hash, n_log2: 14, r: 32, p: 1)  # => false

API Reference

Yescrypt.create(password, **options)

Hash a password and return an encoded string suitable for storage.

hash = Yescrypt.create("password")
hash = Yescrypt.create("password", n_log2: 14, r: 32, p: 1, t: 0, flags: Yescrypt::DEFAULTS)

Parameters:

Parameter Type Default Description
password String (required) The plaintext password
n_log2: Integer 12 Log2 of the block count (memory cost). N = 2^n_log2
r: Integer 32 Block size parameter (affects memory per block)
p: Integer 1 Parallelism parameter
t: Integer 0 Time parameter (extra mixing rounds)
flags: Integer Yescrypt::DEFAULTS (2) Flavor flags (see Flavors)

Returns: A frozen, US-ASCII encoded string starting with $y$.

A random 16-byte salt is generated automatically using SecureRandom.

Yescrypt.verify(password, hash)

Verify a plaintext password against an encoded hash.

Yescrypt.verify("password", hash)  # => true or false

Returns false for any invalid input (wrong types, malformed hash, wrong password) rather than raising exceptions. Uses constant-time comparison to prevent timing attacks.

Yescrypt.cost_matches?(hash, **options)

Check whether an existing hash was generated with the given parameters. Useful for determining if a password needs rehashing after a cost parameter change.

if Yescrypt.verify(password, stored_hash)
  unless Yescrypt.cost_matches?(stored_hash, n_log2: 14)
    # Rehash with new cost parameters
    new_hash = Yescrypt.create(password, n_log2: 14)
    save_hash(new_hash)
  end
end

Parameters: Same keyword arguments as create. Omitted parameters default to the current defaults.

Returns: true if all parameters (n_log2, r, p, t, flags) match, false otherwise.

Yescrypt.default_params

Returns a frozen hash of the current default parameters:

Yescrypt.default_params
# => { n_log2: 12, r: 32, p: 1, t: 0, flags: 2 }

Yescrypt.decode_params(hash)

Parse the parameters from an encoded yescrypt hash string.

params = Yescrypt.decode_params(hash)
# => { n_log2: 12, r: 32, p: 1, t: 0, flags: 2 }

Returns: A frozen hash with :n_log2, :r, :p, :t, :flags keys, or nil if the string is not a valid yescrypt hash.

Yescrypt.gensalt(**options)

Generate a setting/salt string for low-level use. You typically don't need this directly -- create calls it internally.

salt = SecureRandom.random_bytes(16)
setting = Yescrypt.gensalt(n_log2: 12, r: 32, p: 1, t: 0, flags: Yescrypt::DEFAULTS, salt: salt)

The salt: keyword is required and must be a binary String (typically 16 random bytes).

Yescrypt.kdf(password, salt, **options)

Low-level KDF function that returns raw hash bytes. Unlike create, this gives you direct control over all parameters and returns unencoded binary output.

raw = Yescrypt.kdf("password", "salt" * 4,
  n: 4096, r: 32, p: 1, flags: Yescrypt::DEFAULTS, t: 0, outlen: 32)
raw.bytesize  # => 32

Parameters:

Parameter Type Default Description
password String (required) The plaintext password
salt String (required) Salt bytes (any length)
n: Integer (required) Block count (must be a power of 2, >= 2)
r: Integer (required) Block size parameter
p: Integer (required) Parallelism parameter
flags: Integer (required) Flavor flags
t: Integer 0 Time parameter
outlen: Integer 32 Output length in bytes (1..1024)

Returns: A binary String of outlen bytes.

Note: n here is the actual block count (e.g., 4096), not the log2 value used in create (where you'd use n_log2: 12).

Flavors

yescrypt supports three operational flavors:

Constant Value Description
Yescrypt::WORM 0 Classic scrypt-compatible mode. Read-only memory access pattern.
Yescrypt::RW 1 yescrypt with read-write memory access. Stronger against TMTO attacks.
Yescrypt::DEFAULTS 2 Recommended mode. Includes RW access plus pwxform strengthening.

DEFAULTS is recommended for new applications. WORM is useful if you need scrypt compatibility.

Choosing Parameters

The cost parameters control the trade-off between security and performance:

  • n_log2 (memory cost): Each increment doubles memory usage and time. Start with 12 (4096 blocks) and increase if your server can handle it. 14 (16384 blocks) is a good choice for higher security.
  • r (block size): Controls memory per block (128 * r bytes). The default 32 (4 KB per block) is well-suited for modern CPUs. Increasing r increases memory bandwidth requirements.
  • p (parallelism): Number of independent mixing operations. Keep at 1 unless you specifically need parallel computation. Increasing p scales CPU time linearly without increasing memory.
  • t (time): Extra mixing rounds. Each increment of t adds N more mixing iterations. Keep at 0 unless you want to increase CPU time without increasing memory.

Memory usage

Approximate memory per hash operation: 128 * r * N bytes, where N = 2^n_log2.

n_log2 r Memory
10 8 1 MB
12 32 16 MB
14 32 64 MB
16 32 256 MB

Benchmarking

Test different parameters to find the right trade-off for your application:

require "benchmark"

[10, 12, 14].each do |n|
  time = Benchmark.realtime { Yescrypt.create("test", n_log2: n, r: 32, p: 1) }
  puts "n_log2=#{n}: #{(time * 1000).round}ms"
end

Thread Safety

yescrypt releases the Ruby GVL (Global VM Lock) during the CPU-intensive hashing computation. This means other Ruby threads can execute while a hash is being computed:

threads = 4.times.map do |i|
  Thread.new { Yescrypt.create("password_#{i}", n_log2: 4, r: 1, p: 1) }
end
hashes = threads.map(&:value)  # all 4 run concurrently

Each call allocates its own working memory, so there is no shared mutable state between threads.

Hash Format

The encoded hash string follows the crypt(3) modular format:

$y$j<params>$<salt>$<hash>
  • $y$ -- yescrypt identifier prefix
  • j -- flavor indicator
  • <params> -- base64-encoded N_log2, r, p, t, and flags
  • <salt> -- base64-encoded salt
  • <hash> -- base64-encoded 256-bit derived key

Example:

$y$jD5.7C....$aBcDeFgHiJkLmNoPqR..$xYzAbCdEfGhIjKlMnOpQrStUvWxYz01234567890AB

Error Handling

The gem defines a single exception class:

Yescrypt::Error < StandardError

Raised by low-level functions (kdf, gensalt, _hash_password) when the C library reports a failure (e.g., memory allocation failure, invalid setting string).

The high-level verify method never raises -- it returns false on any error. The high-level create method raises TypeError if the password is not a String, and may raise Yescrypt::Error on internal failures.

Comparison with Other Password Hashing Gems

Feature yescrypt bcrypt-ruby scrypt
Memory-hard Yes No Yes
TMTO resistance Strong (pwxform) N/A Moderate
Linux default (2024+) Yes No No
GVL released Yes Yes Yes
Constant-time verify Yes Yes Yes

Development

# Clone the repository
git clone https://github.com/msuliq/yescrypt.git
cd yescrypt

# Install dependencies
bundle install

# Compile the C extension
bundle exec rake compile

# Run tests
bundle exec rake test

# Run both (compile + test)
bundle exec rake

Project Structure

yescrypt/
  ext/yescrypt/          # C extension source
    extconf.rb           # Build configuration
    yescrypt_ext.c       # Ruby/C bridge
    yescrypt-opt.c       # Core yescrypt algorithm (smix, salsa20, pwxform)
    yescrypt-common.c    # Encoding, decoding, salt generation, yescrypt_r
    yescrypt.h           # Public C API header
    sha256.c             # SHA-256 / HMAC / PBKDF2 implementation
    sha256.h             # SHA-256 header
    insecure_memzero.h   # Secure memory zeroing
  lib/
    yescrypt.rb          # High-level Ruby API (create, verify, cost_matches?)
    yescrypt/version.rb  # Version constant
  test/
    yescrypt_test.rb     # Minitest test suite
    test_helper.rb       # Test setup

License

MIT License. See LICENSE for details.

The bundled yescrypt C implementation is based on work by Colin Percival and Alexander Peslyak, used under a permissive BSD-style license.

References

About

Ruby C extension wrapping the yescrypt password hashing algorithm

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors