diff --git a/examples/pwhash_selector.cr b/examples/pwhash_selector.cr index 75b9fce..aad764d 100644 --- a/examples/pwhash_selector.cr +++ b/examples/pwhash_selector.cr @@ -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 diff --git a/spec/cox/pwhash_spec.cr b/spec/cox/pwhash_spec.cr index b7f61c9..28be1a7 100644 --- a/spec/cox/pwhash_spec.cr +++ b/spec/cox/pwhash_spec.cr @@ -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 diff --git a/src/cox/lib_sodium.cr b/src/cox/lib_sodium.cr index 97c3256..bd5d51c 100644 --- a/src/cox/lib_sodium.cr +++ b/src/cox/lib_sodium.cr @@ -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), diff --git a/src/cox/pwhash.cr b/src/cox/pwhash.cr index fb933c8..bd11be4 100644 --- a/src/cox/pwhash.cr +++ b/src/cox/pwhash.cr @@ -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