Sodium::Pwhash create keys based on time cost.

Add #craete_key and #craete_kdf.
This commit is contained in:
Didactic Drunk 2019-09-12 22:28:49 -07:00
parent 5196ee992e
commit c3a9fe178a
2 changed files with 124 additions and 17 deletions

View File

@ -98,6 +98,23 @@ describe Sodium::Pwhash do
kdf = pwhash.derive_kdf salt, "foo", 32 kdf = pwhash.derive_kdf salt, "foo", 32
end end
it "creates a key and sets parameters by time" do
pwhash = pw_min
tcost = 0.2
pwhash.tcost = tcost
salt = pwhash.random_salt
key, pwcreate = pwhash.create_key salt, "foo", 32
pwcreate.memlimit.should be > pwhash.memlimit
pwcreate.opslimit.should be > pwhash.opslimit
ts = Time.measure do
key.should eq pwcreate.derive_key(salt, "foo", 32)
end
# ts should be within +|- 10%. allow up to 20%
(ts.to_f - tcost).abs.should be < (tcost * 0.2)
end
it "PyNaCl key vectors" do it "PyNaCl key vectors" do
test_vectors "modular_crypt_argon2i_hashes.json", Sodium::Pwhash::Mode::Argon2i13 test_vectors "modular_crypt_argon2i_hashes.json", Sodium::Pwhash::Mode::Argon2i13
test_vectors "modular_crypt_argon2id_hashes.json", Sodium::Pwhash::Mode::Argon2id13 test_vectors "modular_crypt_argon2id_hashes.json", Sodium::Pwhash::Mode::Argon2id13

View File

@ -31,7 +31,7 @@ module Sodium
Argon2id13 = 2 Argon2id13 = 2
# The currently recommended algorithm, which can change from one version of libsodium to another. # The currently recommended algorithm, which can change from one version of libsodium to another.
def default def self.default
Mode.new LibSodium.crypto_pwhash_alg_default Mode.new LibSodium.crypto_pwhash_alg_default
end end
end end
@ -40,10 +40,21 @@ module Sodium
# Specified in bytes. # Specified in bytes.
property memlimit = MEMLIMIT_INTERACTIVE property memlimit = MEMLIMIT_INTERACTIVE
# Only used by create_key.
# Specified in seconds.
property tcost = 0.1
# Only used by create_key.
property memlimit_min = MEMLIMIT_MIN
# Only used by create_key.
# Specified in bytes.
# defaults to 256M.
# TODO: defaults to 1/4 RAM (not swap).
property memlimit_max : UInt64 = 256_u64 * 1024 * 1024
# Used by and must be set before calling #derive_key # Used by and must be set before calling #derive_key
property mode : Mode? property mode : Mode?
# Apply the most recent password hashing algorithm agains a password. # Apply the most recent password hashing algorithm against a password.
# Returns a opaque String which includes: # Returns a opaque String which includes:
# * the result of a memory-hard, CPU-intensive hash function applied to the password # * the result of a memory-hard, CPU-intensive hash function applied to the password
# * the automatically generated salt used for the previous computation # * the automatically generated salt used for the previous computation
@ -69,6 +80,7 @@ module Sodium
self self
end end
# Check if a password verification string str matches the parameters opslimit and memlimit, and the current default algorithm.
def needs_rehash?(str) : Bool def needs_rehash?(str) : Bool
# BUG: verify str length # BUG: verify str length
case LibSodium.crypto_pwhash_str_needs_rehash(str, @opslimit, @memlimit) case LibSodium.crypto_pwhash_str_needs_rehash(str, @opslimit, @memlimit)
@ -81,30 +93,108 @@ module Sodium
end end
end end
# Returns a consistent key based on [salt, pass, key_bytes, mode, ops_limit, mem_limit] in a SecureBuffer # def set_params(secs, *, min_mem = MEMLIMIT_MIN, max_mem = 256*1024*1024)
# # end
# Must set a mode before calling.
def derive_key(salt, pass, key_bytes) def derive_key(salt, pass, key_bytes)
derive_key salt.to_slice, pass.to_slice, key_bytes derive_key salt.to_slice, pass.to_slice, key_bytes
end end
def derive_key(salt : Bytes, pass : Bytes, key_bytes) : SecureBuffer # Returns a consistent key based on [salt, pass, key_bytes, mode, ops_limit, mem_limit] in a SecureBuffer
#
# Must set a mode before calling.
def derive_key(salt : Bytes | String, pass : Bytes | String, key_bytes) : SecureBuffer
raise "salt expected #{SALT_SIZE} bytes, got #{salt.bytesize} " if salt.bytesize != SALT_SIZE raise "salt expected #{SALT_SIZE} bytes, got #{salt.bytesize} " if salt.bytesize != SALT_SIZE
m = mode || raise ArgumentError.new("mode not set")
if m = mode
key = SecureBuffer.new key_bytes key = SecureBuffer.new key_bytes
if LibSodium.crypto_pwhash(key.to_slice, key.bytesize, pass, pass.bytesize, salt, @opslimit, @memlimit, m) != 0 derive_key key, m, salt, pass
raise Sodium::Error.new("crypto_pwhash_str") key.readonly
end end
key
else private def derive_key(key : SecureBuffer, m : Mode, salt : Bytes | String, pass : Bytes | String) : Nil
raise ArgumentError.new("mode not set") if LibSodium.crypto_pwhash(key.to_slice, key.bytesize, pass.to_slice, pass.bytesize, salt.to_slice, @opslimit, @memlimit, m) != 0
raise Sodium::Error.new("crypto_pwhash")
end end
end end
# Derives a key using derive_key and returns KDF.new(key) private def time_derive_key(key, m, salt, pass)
def derive_kdf(salt, pass, key_bytes) : Kdf # TODO: switch to CPU time
key = derive_key salt, pass, key_bytes ts = Time.measure do
derive_key key, m, salt, pass
end
ts
end
# Returns a consistent key based on [salt, pass, key_bytes, mode] in a SecureBuffer **and** a new `Pwhash` with new params.
# Params on the new `Pwhash` are set to run in approximately `tcost` seconds.
# Make sure you store `mode`, `opslimit` and `memlimit` for later use with #derive_key.
# `Mode` has #to_s and #from_s for use with configuration files or databases.
def create_key(salt : Bytes | String, pass : Bytes | String, key_bytes) : {SecureBuffer, self}
pw = dup
key = pw.create_key! salt, pass, key_bytes
{key, pw}
end
# :nodoc:
def create_key!(salt : Bytes | String, pass : Bytes | String, key_bytes) : SecureBuffer
m = self.mode ||= Mode.default
@opslimit = OPSLIMIT_MIN
@memlimit = MEMLIMIT_MIN
key = SecureBuffer.new key_bytes
nsamples = 10
samples = nsamples.times.map do
ts = time_derive_key key, m, salt, pass
end.to_a
mean = samples.sum / nsamples
return key.readonly if mean.to_f >= @tcost
# initial sample to avoid overshooting on busy systems
# round to nearest pow2 / 3
mult = Math.pw2ceil ((@tcost / 3.0 / mean.to_f).ceil.to_i)
@memlimit = (@memlimit * mult).clamp(@memlimit_min, @memlimit_max)
last_memlimit = @memlimit
while @memlimit != @memlimit_max
ts = time_derive_key key, m, salt, pass
# tcost exceeded by memlimit_min
return key.readonly if ts.to_f >= @tcost * 0.9
# / 3 to keep rounds > 1 mitigating attacks against argon with a low number of rounds
break if ts.to_f >= (@tcost / 3.0) * 0.9
last_memlimit = @memlimit
# increments of 1K for compatibility with other libraries.
@memlimit = (((@memlimit / 1024).to_f * Math.max(1.1, (@tcost / ts.to_f / 3.0))).ceil.to_u64 * 1024).clamp(@memlimit_min, @memlimit_max)
# stopped making progress
break if @memlimit == last_memlimit
end
last_opslimit = @opslimit
loop do
ts = time_derive_key key, m, salt, pass
# 90% is close enough
break if ts.to_f >= @tcost * 0.90
last_opslimit = @opslimit
@opslimit = (@opslimit.to_f * Math.max(1.1, (@tcost / ts.to_f))).ceil.to_u64
# stopped making progress
break if @opslimit == last_opslimit
end
key.readonly
end
# Creates a key using create_key and returns `{ KDF.new(key), Pwhash }`
# See #create_key for more details.
def create_kdf(salt, pass, key_bytes) : {Kdf, self}
key, pwhash = create_key salt.to_slice, pass.to_slice, key_bytes
{Kdf.new(key), pwhash}
end
# Derives a key using derive_key and returns `KDF.new(key)`
def derive_kdf(salt, pass, key_bytes)
key = derive_key salt.to_slice, pass.to_slice, key_bytes
Kdf.new key Kdf.new key
end end