Sodium::Pwhash create keys based on time cost.
Add #craete_key and #craete_kdf.master
parent
5196ee992e
commit
c3a9fe178a
|
@ -98,6 +98,23 @@ describe Sodium::Pwhash do
|
|||
kdf = pwhash.derive_kdf salt, "foo", 32
|
||||
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
|
||||
test_vectors "modular_crypt_argon2i_hashes.json", Sodium::Pwhash::Mode::Argon2i13
|
||||
test_vectors "modular_crypt_argon2id_hashes.json", Sodium::Pwhash::Mode::Argon2id13
|
||||
|
|
|
@ -31,7 +31,7 @@ module Sodium
|
|||
Argon2id13 = 2
|
||||
|
||||
# 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
|
||||
end
|
||||
end
|
||||
|
@ -40,10 +40,21 @@ module Sodium
|
|||
# Specified in bytes.
|
||||
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
|
||||
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:
|
||||
# * the result of a memory-hard, CPU-intensive hash function applied to the password
|
||||
# * the automatically generated salt used for the previous computation
|
||||
|
@ -69,6 +80,7 @@ module Sodium
|
|||
self
|
||||
end
|
||||
|
||||
# Check if a password verification string str matches the parameters opslimit and memlimit, and the current default algorithm.
|
||||
def needs_rehash?(str) : Bool
|
||||
# BUG: verify str length
|
||||
case LibSodium.crypto_pwhash_str_needs_rehash(str, @opslimit, @memlimit)
|
||||
|
@ -81,30 +93,108 @@ module Sodium
|
|||
end
|
||||
end
|
||||
|
||||
# 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 set_params(secs, *, min_mem = MEMLIMIT_MIN, max_mem = 256*1024*1024)
|
||||
# end
|
||||
|
||||
def derive_key(salt, pass, key_bytes)
|
||||
derive_key salt.to_slice, pass.to_slice, key_bytes
|
||||
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
|
||||
m = mode || raise ArgumentError.new("mode not set")
|
||||
|
||||
if m = mode
|
||||
key = SecureBuffer.new key_bytes
|
||||
if LibSodium.crypto_pwhash(key.to_slice, key.bytesize, pass, pass.bytesize, salt, @opslimit, @memlimit, m) != 0
|
||||
raise Sodium::Error.new("crypto_pwhash_str")
|
||||
end
|
||||
key
|
||||
else
|
||||
raise ArgumentError.new("mode not set")
|
||||
key = SecureBuffer.new key_bytes
|
||||
derive_key key, m, salt, pass
|
||||
key.readonly
|
||||
end
|
||||
|
||||
private def derive_key(key : SecureBuffer, m : Mode, salt : Bytes | String, pass : Bytes | String) : Nil
|
||||
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
|
||||
|
||||
# Derives a key using derive_key and returns KDF.new(key)
|
||||
def derive_kdf(salt, pass, key_bytes) : Kdf
|
||||
key = derive_key salt, pass, key_bytes
|
||||
private def time_derive_key(key, m, salt, pass)
|
||||
# TODO: switch to CPU time
|
||||
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
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in New Issue