Sodium::Pwhash create keys based on time cost.
Add #craete_key and #craete_kdf.
This commit is contained in:
parent
5196ee992e
commit
c3a9fe178a
@ -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
|
||||||
|
@ -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
|
derive_key key, m, salt, pass
|
||||||
if LibSodium.crypto_pwhash(key.to_slice, key.bytesize, pass, pass.bytesize, salt, @opslimit, @memlimit, m) != 0
|
key.readonly
|
||||||
raise Sodium::Error.new("crypto_pwhash_str")
|
end
|
||||||
end
|
|
||||||
key
|
private def derive_key(key : SecureBuffer, m : Mode, salt : Bytes | String, pass : Bytes | String) : Nil
|
||||||
else
|
if LibSodium.crypto_pwhash(key.to_slice, key.bytesize, pass.to_slice, pass.bytesize, salt.to_slice, @opslimit, @memlimit, m) != 0
|
||||||
raise ArgumentError.new("mode not set")
|
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
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user