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 "Usage: #{PROGRAM_NAME} time_min [time_max] [mem_max]"
puts "\ttime is in seconds"
puts "\tmem is in K"
puts "\tmem is in bytes"
exit 1
end
@ -14,7 +14,7 @@ time_limit = if t = ARGV.shift?
else
time_min * 4
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
pass = "1234"
@ -24,10 +24,10 @@ loop do
loop do
# p pwhash
t = Time.measure { pwhash.hash_str pass }.to_f
t = Time.measure { pwhash.store pass }.to_f
s = String.build do |sb|
sb << "mem_limit "
sb << "%7d" % pwhash.memlimit
sb << "%7d" % (pwhash.memlimit / 1024)
sb << "K ops_limit "
sb << "%7d" % pwhash.opslimit
sb << " "
@ -41,6 +41,7 @@ loop do
puts ""
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
end

View File

@ -1,15 +1,20 @@
require "../spec_helper"
describe Cox::Pwhash do
it "hashes and verifies a password" do
private def pw_min
pwhash = Cox::Pwhash.new
# set to minimum to speed up tests
pwhash.memlimit = Cox::Pwhash::MEMLIMIT_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"
hash = pwhash.hash_str pass
hash = pwhash.store pass
pwhash.verify hash, pass
expect_raises(Cox::Pwhash::PasswordVerifyError) do
pwhash.verify hash, "5678"
@ -19,4 +24,27 @@ describe Cox::Pwhash do
pwhash.opslimit = Cox::Pwhash::OPSLIMIT_MAX
pwhash.needs_rehash?(hash).should be_true
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

View File

@ -21,6 +21,11 @@ module Cox
fun crypto_pwhash_opslimit_sensitive() : LibC::SizeT
fun crypto_pwhash_opslimit_max() : 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_bytes : LibC::SizeT
fun crypto_generichash_blake2b_bytes_min : LibC::SizeT
@ -109,6 +114,17 @@ module Cox
key : Pointer(LibC::UChar)
) : 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(
outstr : Pointer(LibC::UChar),
pass : Pointer(LibC::UChar),

View File

@ -1,4 +1,7 @@
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 PasswordVerifyError < Cox::Error
end
@ -13,10 +16,24 @@ module Cox
MEMLIMIT_MAX = LibSodium.crypto_pwhash_memlimit_max
MEMLIMIT_INTERACTIVE = LibSodium.crypto_pwhash_memlimit_interactive
enum Algorithm
Argon2i13 = 1
Argon2id13 = 2
end
property opslimit = OPSLIMIT_INTERACTIVE
# Specified in bytes.
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
if LibSodium.crypto_pwhash_str(outstr, pass, pass.bytesize, @opslimit, @memlimit) != 0
raise Cox::Error.new("crypto_pwhash_str")
@ -24,17 +41,20 @@ module Cox
outstr
end
# Verify a password against a stored String.
# raises PasswordVerifyError on failure.
def verify(str, pass)
# BUG: verify str length
case LibSodium.crypto_pwhash_str_verify(str, pass, pass.bytesize)
when 0
true
# Passed
else
raise PasswordVerifyError.new
end
self
end
def needs_rehash?(str)
def needs_rehash?(str) : Bool
# BUG: verify str length
case LibSodium.crypto_pwhash_str_needs_rehash(str, @opslimit, @memlimit)
when 0
@ -45,5 +65,29 @@ module Cox
raise Cox::Error.new("crypto_pwhash_str_needs_rehash")
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