Skip to content

Commit

Permalink
Initial scrypt support
Browse files Browse the repository at this point in the history
Support for the scrypt sequential memory hard password hashing function.
  • Loading branch information
tarcieri committed May 17, 2014
1 parent 0df19b1 commit ca00cbe
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 0 deletions.
4 changes: 4 additions & 0 deletions lib/rbnacl.rb
Expand Up @@ -63,6 +63,10 @@ class BadAuthenticatorError < CryptoError; end
require "rbnacl/hash/sha512"
require "rbnacl/hash/blake2b"

# Password hash function: scrypt
require "rbnacl/password_hash"
require "rbnacl/password_hash/scrypt"

# HMAC: SHA256 and SHA512256
require "rbnacl/hmac/sha256"
require "rbnacl/hmac/sha512256"
Expand Down
39 changes: 39 additions & 0 deletions lib/rbnacl/password_hash.rb
@@ -0,0 +1,39 @@
# encoding: binary
module RbNaCl
# Password hashing functions
#
# These hash functions are designed specifically for the purposes of securely
# storing passwords in a way that they can be checked against a supplied
# password but an attacker who obtains a hash cannot easily reverse them back
# into the original password.
#
# Unlike normal hash functions, which are intentionally designed to hash data
# as quickly as they can while remaining secure, password hashing functions
# are intentionally designed to be slow so they are hard for attackers to
# brute force.
#
# All password hashing functions take a "salt" value which should be randomly
# generated on a per-password basis (using RbNaCl::Random, accept no
# subsitutes)
#
# All of them also take a CPU work factor, which increases the amount of
# computation needed to produce the digest.
module PasswordHash
# scrypt: the original sequential memory hard password hashing function.
# This is also the only password hashing function supported by libsodium,
# but that's okay, because it's pretty awesome.
#
# @param [String] password to be hashed
# @param [String] salt to make the digest unique
# @param [Integer] opslimit the CPU cost (e.g. 2**20)
# @param [Integer] memlimit the memory cost (e.g. 2**24)
# @param [Integer] digest_size of the output
#
# @raise [CryptoError] If calculating the digest fails for some reason.
#
# @return [String] The scrypt digest as raw bytes
def self.scrypt(password, salt, opslimit, memlimit, digest_size = 64)
SCrypt.new(opslimit, memlimit, digest_size).digest(password, salt)
end
end
end
64 changes: 64 additions & 0 deletions lib/rbnacl/password_hash/scrypt.rb
@@ -0,0 +1,64 @@
# encoding: binary
module RbNaCl
module PasswordHash
# The scrypt sequential memory hard password hashing function
#
# scrypt is a password hash (or password based KDF). That is to say, where
# most hash functions are designed to be fast because hashing is often a
# bottleneck, scrypt is slow by design, because it's trying to "strengthen"
# the password by combining it with a random "salt" value then perform a
# series of operation on the result which are slow enough to defeat
# brute-force password cracking attempts.
#
# scrypt is similar to the bcrypt and pbkdf2 password hashes in that it's
# designed to strengthen passwords, but includes a new design element
# called "sequential memory hardness" which helps defeat attempts by
# attackers to compensate for their lack of memory (since they're typically
# on GPUs or FPGAs) with additional computation.
class SCrypt
extend Sodium

sodium_type :pwhash
sodium_primitive :scryptxsalsa208sha256

sodium_constant :SALTBYTES

sodium_function :scrypt,
:crypto_pwhash_scryptxsalsa208sha256,
[:pointer, :ulong_long, :pointer, :ulong_long, :pointer, :ulong_long, :size_t]



# Create a new SCrypt password hash object
#
# @param [Integer] opslimit the CPU cost (e.g. 2**20)
# @param [Integer] memlimit the memory cost (e.g. 2**24)
#
# @return [RbNaCl::PasswordHash::SCrypt] An SCrypt password hasher object
def initialize(opslimit, memlimit, digest_size = 64)
# TODO: sanity check these parameters
@opslimit, @memlimit = opslimit, memlimit

# TODO: check digest size validity
#raise LengthError, "digest size too short" if @digest_size < BYTES_MIN
#raise LengthError, "digest size too long" if @digest_size > BYTES_MAX

@digest_size = digest_size
end

# Calculate an scrypt digest for a given password and salt
#
# @param [String] password to be hashed
# @param [String] salt to make the digest unique
#
# @return [String] scrypt digest of the string as raw bytes
def digest(password, salt)
digest = Util.zeros(@digest_size)
salt = Util.check_string(salt, SALTBYTES, "salt")

self.class.scrypt(digest, @digest_size, password, password.bytesize, salt, @opslimit, @memlimit) || raise(CryptoError, "scrypt failed!")
digest
end
end
end
end
11 changes: 11 additions & 0 deletions lib/rbnacl/test_vectors.rb
Expand Up @@ -85,6 +85,17 @@ module RbNaCl
:blake2b_keyed_digest => "142709d62e28fcccd0af97fad0f8465b971e82201dc51070faa0372aa43e9248" +
"4be1c1e73ba10906d5d1853db6a4106e0a7bf9800d373d6dee2d46d62ef2a461",

# scrypt test vectors
# Taken from http://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01#page-14
:scrypt_password => "4a857e2ee8aa9b6056f2424e84d24a72473378906ee04a46cb05311502d5250b" +
"82ad86b83c8f20a23dbb74f6da60b0b6ecffd67134d45946ac8ebfb3064294bc" +
"097d43ced68642bfb8bbbdd0f50b30118f5e",
:scrypt_salt => "39d82eef32010b8b79cc5ba88ed539fbaba741100f2edbeca7cc171ffeabf258",
:scrypt_opslimit => 758010,
:scrypt_memlimit => 5432947,
:scrypt_digest => "bcc5c2fd785e4781d1201ed43d84925537e2a540d3de55f5812f29e9dd0a4a00" +
"451a5c8ddbb4862c03d45c75bf91b7fb49265feb667ad5c899fdbf2ca19eac67",

# Auth test vectors
# Taken from NaCl distribution
#
Expand Down
21 changes: 21 additions & 0 deletions spec/rbnacl/password_hash/scrypt_spec.rb
@@ -0,0 +1,21 @@
# encoding: binary
require 'spec_helper'

describe RbNaCl::PasswordHash::SCrypt do
let(:reference_password) { vector :scrypt_password }
let(:reference_salt) { vector :scrypt_salt }
let(:reference_opslimit) { RbNaCl::TestVectors[:scrypt_opslimit] }
let(:reference_memlimit) { RbNaCl::TestVectors[:scrypt_memlimit] }
let(:reference_digest) { vector :scrypt_digest }

it "calculates the correct diest for a reference password/salt" do
digest = RbNaCl::PasswordHash.scrypt(
reference_password,
reference_salt,
reference_opslimit,
reference_memlimit
)

expect(digest).to eq reference_digest
end
end

0 comments on commit ca00cbe

Please sign in to comment.