PWhash add key derivation.

This commit is contained in:
Didactic Drunk 2019-06-27 08:03:33 -07:00
parent 3a078523f5
commit 8da7fb47ec
4 changed files with 99 additions and 10 deletions

View File

@ -4,7 +4,7 @@ if ARGV.empty?
puts "Help select Pwhash ops/mem limits for your application." puts "Help select Pwhash ops/mem limits for your application."
puts "Usage: #{PROGRAM_NAME} time_min [time_max] [mem_max]" puts "Usage: #{PROGRAM_NAME} time_min [time_max] [mem_max]"
puts "\ttime is in seconds" puts "\ttime is in seconds"
puts "\tmem is in K" puts "\tmem is in bytes"
exit 1 exit 1
end end
@ -14,7 +14,7 @@ time_limit = if t = ARGV.shift?
else else
time_min * 4 time_min * 4
end end
mem_limit = (ARGV.shift?.try &.to_i || (128*1024)).to_u64 mem_limit = (ARGV.shift?.try &.to_i || (Cox::Pwhash::MEMLIMIT_MAX)).to_u64
pwhash = Cox::Pwhash.new pwhash = Cox::Pwhash.new
pass = "1234" pass = "1234"
@ -24,10 +24,10 @@ loop do
loop do loop do
# p pwhash # p pwhash
t = Time.measure { pwhash.hash_str pass }.to_f t = Time.measure { pwhash.store pass }.to_f
s = String.build do |sb| s = String.build do |sb|
sb << "mem_limit " sb << "mem_limit "
sb << "%7d" % pwhash.memlimit sb << "%7d" % (pwhash.memlimit / 1024)
sb << "K ops_limit " sb << "K ops_limit "
sb << "%7d" % pwhash.opslimit sb << "%7d" % pwhash.opslimit
sb << " " sb << " "
@ -41,6 +41,7 @@ loop do
puts "" puts ""
break if pwhash.memlimit >= mem_limit break if pwhash.memlimit >= mem_limit
break if pwhash.opslimit == Cox::Pwhash::OPSLIMIT_MIN # Couldn't get past 1 iteration before going over time.
pwhash.memlimit *= 2 pwhash.memlimit *= 2
end end

View File

@ -1,15 +1,20 @@
require "../spec_helper" require "../spec_helper"
describe Cox::Pwhash do private def pw_min
it "hashes and verifies a password" do
pwhash = Cox::Pwhash.new pwhash = Cox::Pwhash.new
# set to minimum to speed up tests # set to minimum to speed up tests
pwhash.memlimit = Cox::Pwhash::MEMLIMIT_MIN pwhash.memlimit = Cox::Pwhash::MEMLIMIT_MIN
pwhash.opslimit = Cox::Pwhash::OPSLIMIT_MIN pwhash.opslimit = Cox::Pwhash::OPSLIMIT_MIN
pwhash
end
describe Cox::Pwhash do
it "hashes and verifies a password" do
pwhash = pw_min
pass = "1234" pass = "1234"
hash = pwhash.hash_str pass hash = pwhash.store pass
pwhash.verify hash, pass pwhash.verify hash, pass
expect_raises(Cox::Pwhash::PasswordVerifyError) do expect_raises(Cox::Pwhash::PasswordVerifyError) do
pwhash.verify hash, "5678" pwhash.verify hash, "5678"
@ -19,4 +24,27 @@ describe Cox::Pwhash do
pwhash.opslimit = Cox::Pwhash::OPSLIMIT_MAX pwhash.opslimit = Cox::Pwhash::OPSLIMIT_MAX
pwhash.needs_rehash?(hash).should be_true pwhash.needs_rehash?(hash).should be_true
end end
it "key_derive fails without an algorithm" do
pwhash = pw_min
expect_raises(ArgumentError) do
pwhash.key_derive pwhash.salt, "foo", 16
end
end
it "derives a key from a password" do
pwhash = pw_min
pwhash.algorithm = Cox::Pwhash::Algorithm::Argon2id13
salt = pwhash.salt
key1 = pwhash.key_derive salt, "foo", 16
key2 = pwhash.key_derive salt, "foo", 16
key3 = pwhash.key_derive salt, "bar", 16
key4 = pwhash.key_derive pwhash.salt, "foo", 16
key1.bytesize.should eq 16
key1.should eq key2
key1.should_not eq key3
key1.should_not eq key4
# BUG: validate against known passwords
end
end end

View File

@ -21,6 +21,11 @@ module Cox
fun crypto_pwhash_opslimit_sensitive() : LibC::SizeT fun crypto_pwhash_opslimit_sensitive() : LibC::SizeT
fun crypto_pwhash_opslimit_max() : LibC::SizeT fun crypto_pwhash_opslimit_max() : LibC::SizeT
fun crypto_pwhash_strbytes() : LibC::SizeT fun crypto_pwhash_strbytes() : LibC::SizeT
fun crypto_pwhash_alg_argon2i13() : LibC::Int
fun crypto_pwhash_alg_argon2id13() : LibC::Int
fun crypto_pwhash_saltbytes : LibC::SizeT
fun crypto_pwhash_bytes_min() : LibC::SizeT
fun crypto_pwhash_bytes_max() : LibC::SizeT
fun crypto_generichash_blake2b_statebytes : LibC::SizeT fun crypto_generichash_blake2b_statebytes : LibC::SizeT
fun crypto_generichash_blake2b_bytes : LibC::SizeT fun crypto_generichash_blake2b_bytes : LibC::SizeT
fun crypto_generichash_blake2b_bytes_min : LibC::SizeT fun crypto_generichash_blake2b_bytes_min : LibC::SizeT
@ -109,6 +114,17 @@ module Cox
key : Pointer(LibC::UChar) key : Pointer(LibC::UChar)
) : LibC::Int ) : LibC::Int
fun crypto_pwhash(
key : Pointer(LibC::UChar),
key_size : LibC::ULongLong,
pass : Pointer(LibC::UChar),
pass_size : LibC::ULongLong,
salt : Pointer(LibC::UChar),
optslimit : LibC::ULongLong,
memlimit : LibC::SizeT,
alg : LibC::Int,
) : LibC::Int
fun crypto_pwhash_str( fun crypto_pwhash_str(
outstr : Pointer(LibC::UChar), outstr : Pointer(LibC::UChar),
pass : Pointer(LibC::UChar), pass : Pointer(LibC::UChar),

View File

@ -1,4 +1,7 @@
module Cox module Cox
# [Argon2 Password Hashing](https://libsodium.gitbook.io/doc/password_hashing/the_argon2i_function)
# * #store #verify #needs_rehash? are used together for password verification.
# * #key_derive is used on it's own to generate password based keys.
class Pwhash class Pwhash
class PasswordVerifyError < Cox::Error class PasswordVerifyError < Cox::Error
end end
@ -13,10 +16,24 @@ module Cox
MEMLIMIT_MAX = LibSodium.crypto_pwhash_memlimit_max MEMLIMIT_MAX = LibSodium.crypto_pwhash_memlimit_max
MEMLIMIT_INTERACTIVE = LibSodium.crypto_pwhash_memlimit_interactive MEMLIMIT_INTERACTIVE = LibSodium.crypto_pwhash_memlimit_interactive
enum Algorithm
Argon2i13 = 1
Argon2id13 = 2
end
property opslimit = OPSLIMIT_INTERACTIVE property opslimit = OPSLIMIT_INTERACTIVE
# Specified in bytes.
property memlimit = MEMLIMIT_INTERACTIVE property memlimit = MEMLIMIT_INTERACTIVE
def hash_str(pass) # Used by and must be set before calling #key_derive
property algorithm : Algorithm?
# Apply the most recent password hashing algorithm agains 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
# * the other parameters required to verify the password, including the algorithm identifier, its version, opslimit and memlimit.
def store(pass)
outstr = Bytes.new LibSodium::PWHASH_STR_SIZE outstr = Bytes.new LibSodium::PWHASH_STR_SIZE
if LibSodium.crypto_pwhash_str(outstr, pass, pass.bytesize, @opslimit, @memlimit) != 0 if LibSodium.crypto_pwhash_str(outstr, pass, pass.bytesize, @opslimit, @memlimit) != 0
raise Cox::Error.new("crypto_pwhash_str") raise Cox::Error.new("crypto_pwhash_str")
@ -24,17 +41,20 @@ module Cox
outstr outstr
end end
# Verify a password against a stored String.
# raises PasswordVerifyError on failure.
def verify(str, pass) def verify(str, pass)
# BUG: verify str length # BUG: verify str length
case LibSodium.crypto_pwhash_str_verify(str, pass, pass.bytesize) case LibSodium.crypto_pwhash_str_verify(str, pass, pass.bytesize)
when 0 when 0
true # Passed
else else
raise PasswordVerifyError.new raise PasswordVerifyError.new
end end
self
end end
def needs_rehash?(str) 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)
when 0 when 0
@ -45,5 +65,29 @@ module Cox
raise Cox::Error.new("crypto_pwhash_str_needs_rehash") raise Cox::Error.new("crypto_pwhash_str_needs_rehash")
end end
end end
# Returns a consistent key based on [salt, pass, key_bytes, algorithm, ops_limit, mem_limit]
#
# Must set an algorithm before calling.
def key_derive(salt : Bytes, pass : Bytes, key_bytes) : Bytes
if alg = algorithm
key = Bytes.new key_bytes
if LibSodium.crypto_pwhash(key, key.bytesize, pass, pass.bytesize, salt, @opslimit, @memlimit, alg) != 0
raise Cox::Error.new("crypto_pwhash_str")
end
key
else
raise ArgumentError.new("algorithm not set")
end
end
def key_derive(salt, pass, key_bytes)
key_derive salt.to_slice, pass.to_slice, key_bytes
end
# Returns a random salt for use with #key_derive
def salt
Random::Secure.random_bytes LibSodium.crypto_pwhash_saltbytes
end
end end
end end