From b4fe9ef1c3af15dadb47a01200c0ddd2bd33793a Mon Sep 17 00:00:00 2001 From: Didactic Drunk <1479616+didactic-drunk@users.noreply.github.com> Date: Mon, 16 Sep 2019 01:49:55 -0700 Subject: [PATCH] Split Sodium::Pwhash in to Sodium::Password::Hash and Sodium::Password::Key. --- spec/sodium/password_spec.cr | 177 +++++++++++++++++++++++++ spec/sodium/pwhash_spec.cr | 147 --------------------- src/sodium/password.cr | 33 +++++ src/sodium/password/abstract.cr | 38 ++++++ src/sodium/password/create_key.cr | 130 +++++++++++++++++++ src/sodium/password/hash.cr | 40 ++++++ src/sodium/password/key.cr | 45 +++++++ src/sodium/password/mode.cr | 12 ++ src/sodium/password/params.cr | 40 ++++++ src/sodium/pwhash.cr | 206 ------------------------------ 10 files changed, 515 insertions(+), 353 deletions(-) create mode 100644 spec/sodium/password_spec.cr delete mode 100644 spec/sodium/pwhash_spec.cr create mode 100644 src/sodium/password.cr create mode 100644 src/sodium/password/abstract.cr create mode 100644 src/sodium/password/create_key.cr create mode 100644 src/sodium/password/hash.cr create mode 100644 src/sodium/password/key.cr create mode 100644 src/sodium/password/mode.cr create mode 100644 src/sodium/password/params.cr delete mode 100644 src/sodium/pwhash.cr diff --git a/spec/sodium/password_spec.cr b/spec/sodium/password_spec.cr new file mode 100644 index 0000000..64beaa3 --- /dev/null +++ b/spec/sodium/password_spec.cr @@ -0,0 +1,177 @@ +require "../spec_helper" +require "../../src/sodium/password" +require "../../src/sodium/kdf" +require "json" + +def test_vectors(filename, pwmode) + pwhash = Sodium::Password::Hash.new + pwkey = Sodium::Password::Key.new + + buf = File.read Path[__DIR__].join("..", "data", filename) + vectors = Array(Hash(String, String | Int32)).from_json(buf).map do |h| + { + salt: h["salt"].to_s, + pass: h["passwd"].to_s, + mode: h["mode"].to_s, + ops: h["iters"].to_i, + mem: h["maxmem"].to_i * 1024, + dgst_len: h["dgst_len"].to_i, + hash: h["pwhash"].to_s, + # h: h, + } + end + + vectors.each do |h| + case h[:mode] + when "argon2i" + pwhash.verify h[:hash], h[:pass] + when "argon2id" + pwhash.verify h[:hash], h[:pass] + when "raw" + pwkey.ops = h[:ops].to_u64 + pwkey.mem = h[:mem].to_u64 + pwkey.mode = pwmode + # p pwhash, h + key = pwkey.derive_key h[:pass], h[:dgst_len], salt: h[:salt].to_slice + key.should eq h[:hash].hexbytes + else + # p h + puts "unhandled mode #{h[:mode]}" + next + # raise "unhandled mode #{h[:mode]}" + end + end +end + +private def pw_min + pwhash = Sodium::Password::Hash.new + + # set to minimum to speed up tests + pwhash.mem = Sodium::Password::MEMLIMIT_MIN + pwhash.ops = Sodium::Password::OPSLIMIT_MIN + pwhash +end + +private def pk_min + pwkey = Sodium::Password::Key.new + + # set to minimum to speed up tests + pwkey.mem = Sodium::Password::MEMLIMIT_MIN + pwkey.ops = Sodium::Password::OPSLIMIT_MIN + pwkey +end + +describe Sodium::Password::Hash do + it "hashes and verifies a password" do + pwhash = pw_min + + pass = "1234" + hash = pwhash.create pass + pwhash.verify hash, pass + expect_raises(Sodium::Password::Error::Verify) do + pwhash.verify hash, "5678" + end + + pwhash.needs_rehash?(hash).should be_false + p pwhash + pwhash.ops = Sodium::Password::OPSLIMIT_MAX + p pwhash + pwhash.needs_rehash?(hash).should be_true + end + + it "PyNaCl key vectors" do + test_vectors "modular_crypt_argon2i_hashes.json", Sodium::Password::Mode::Argon2i13 + test_vectors "modular_crypt_argon2id_hashes.json", Sodium::Password::Mode::Argon2id13 + test_vectors "raw_argon2i_hashes.json", Sodium::Password::Mode::Argon2i13 + test_vectors "raw_argon2id_hashes.json", Sodium::Password::Mode::Argon2id13 + end + + # from libsodium/test/default/pwhash_argon2id.c + it "RbNaCl key vectors" do + pwhash = Sodium::Password::Key.new + pwhash.mode = Sodium::Password::Mode::Argon2id13 + pwhash.ops = 5_u64 + pwhash.mem = 7_256_678_u64 + key_len = 155 + + pass = "a347ae92bce9f80f6f595a4480fc9c2fe7e7d7148d371e9487d75f5c23008ffae0" \ + "65577a928febd9b1973a5a95073acdbeb6a030cfc0d79caa2dc5cd011cef02c08d" \ + "a232d76d52dfbca38ca8dcbd665b17d1665f7cf5fe59772ec909733b24de97d6f5" \ + "8d220b20c60d7c07ec1fd93c52c31020300c6c1facd77937a597c7a6".hexbytes + salt = "5541fbc995d5c197ba290346d2c559de".hexbytes + expected = "18acec5d6507739f203d1f5d9f1d862f7c2cdac4f19d2bdff64487e60d969e3ced6" \ + "15337b9eec6ac4461c6ca07f0939741e57c24d0005c7ea171a0ee1e7348249d135b" \ + "38f222e4dad7b9a033ed83f5ca27277393e316582033c74affe2566a2bea47f91f0" \ + "fd9fe49ece7e1f79f3ad6e9b23e0277c8ecc4b313225748dd2a80f5679534a0700e" \ + "246a79a49b3f74eb89ec6205fe1eeb941c73b1fcf1".hexbytes + + key = pwhash.derive_key pass, key_len, salt: salt + key.should eq expected + end +end + +describe Sodium::Password::Key::Create do + pending "derive_key fails without a mode" do + pwkey = pk_min + expect_raises(ArgumentError, /^missing mode$/) do + pwkey.derive_key "foo", 16 + end + end + + it "derive_key fails without a salt" do + pwkey = pk_min + expect_raises(ArgumentError, /^missing salt$/) do + pwkey.derive_key "foo", 16 + end + end + + it "derives a key from a password" do + pwkey = pk_min + pwkey.mode = Sodium::Password::Mode::Argon2id13 + salt = pwkey.random_salt + key1 = pwkey.derive_key "foo", 16, salt: salt + key2 = pwkey.derive_key "foo", 16, salt: salt + key3 = pwkey.derive_key "bar", 16, salt: salt + key4 = pwkey.derive_key "foo", 16, salt: pwkey.random_salt + + key1.bytesize.should eq 16 + key1.should eq key2 + key1.should_not eq key3 + key1.should_not eq key4 + end + + it "derives a kdf from a password" do + pwkey = pk_min + pwkey.mode = Sodium::Password::Mode::Argon2id13 + salt = pwkey.random_salt + kdf = pwkey.derive_kdf "foo", salt: salt + end + + it "creates and derives a key from a passord based on time" do + pass = "1234" + context = "8bytesss" + + ck = Sodium::Password::Key::Create.new + ck.tcost = 0.2 + ck.mem_max = Sodium::Password::MEMLIMIT_MIN * 2 + kdf1, params = ck.create_kdf pass + + pw = Sodium::Password::Key.from_params params.not_nil!.to_h + kdf2 = nil + ts = Time.measure do + kdf2 = pw.derive_kdf pass + end + kdf2 = kdf2.not_nil! + + # Check #create_kdf and #derive_kdf create the same subkeys + subkey1 = kdf1.derive context, 0, 16 + subkey2 = kdf2.derive context, 0, 16 + subkey1.should eq subkey2 + + # ts should be within +|- 10%. allow up to 20% + (ts.to_f - ck.tcost).abs.should be < (ck.tcost * 0.2) + end + + pending "implement auth" do + end +end diff --git a/spec/sodium/pwhash_spec.cr b/spec/sodium/pwhash_spec.cr deleted file mode 100644 index 4663f0a..0000000 --- a/spec/sodium/pwhash_spec.cr +++ /dev/null @@ -1,147 +0,0 @@ -require "../spec_helper" -require "../../src/sodium/pwhash" -require "../../src/sodium/kdf" -require "json" - -def test_vectors(filename, pwmode) - pwhash = Sodium::Pwhash.new - - buf = File.read Path[__DIR__].join("..", "data", filename) - vectors = Array(Hash(String, String | Int32)).from_json(buf).map do |h| - { - salt: h["salt"].to_s, - pass: h["passwd"].to_s, - mode: h["mode"].to_s, - ops: h["iters"].to_i, - mem: h["maxmem"].to_i * 1024, - dgst_len: h["dgst_len"].to_i, - hash: h["pwhash"].to_s, - # h: h, - } - end - - vectors.each do |h| - case h[:mode] - when "argon2i" - pwhash.mode = Sodium::Pwhash::Mode::Argon2i13 - pwhash.verify h[:hash], h[:pass] - when "argon2id" - pwhash.mode = Sodium::Pwhash::Mode::Argon2id13 - pwhash.verify h[:hash], h[:pass] - when "raw" - pwhash.opslimit = h[:ops].to_u64 - pwhash.memlimit = h[:mem].to_u64 - pwhash.mode = pwmode - # p pwhash, h - key = pwhash.derive_key salt: h[:salt].to_slice, pass: h[:pass], key_bytes: h[:dgst_len] - key.should eq h[:hash].hexbytes - else - # p h - puts "unhandled mode #{h[:mode]}" - next - # raise "unhandled mode #{h[:mode]}" - end - end -end - -private def pw_min - pwhash = Sodium::Pwhash.new - - # set to minimum to speed up tests - pwhash.memlimit = Sodium::Pwhash::MEMLIMIT_MIN - pwhash.opslimit = Sodium::Pwhash::OPSLIMIT_MIN - pwhash -end - -describe Sodium::Pwhash do - it "hashes and verifies a password" do - pwhash = pw_min - - pass = "1234" - hash = pwhash.create pass - pwhash.verify hash, pass - expect_raises(Sodium::Pwhash::PasswordVerifyError) do - pwhash.verify hash, "5678" - end - - pwhash.needs_rehash?(hash).should be_false - pwhash.opslimit = Sodium::Pwhash::OPSLIMIT_MAX - pwhash.needs_rehash?(hash).should be_true - end - - it "derive_key fails without a mode" do - pwhash = pw_min - expect_raises(ArgumentError) do - pwhash.derive_key pwhash.random_salt, "foo", 16 - end - end - - it "derives a key from a password" do - pwhash = pw_min - pwhash.mode = Sodium::Pwhash::Mode::Argon2id13 - salt = pwhash.random_salt - key1 = pwhash.derive_key salt, "foo", 16 - key2 = pwhash.derive_key salt, "foo", 16 - key3 = pwhash.derive_key salt, "bar", 16 - key4 = pwhash.derive_key pwhash.random_salt, "foo", 16 - - key1.bytesize.should eq 16 - key1.should eq key2 - key1.should_not eq key3 - key1.should_not eq key4 - end - - it "derives a kdf from a password" do - pwhash = pw_min - pwhash.mode = Sodium::Pwhash::Mode::Argon2id13 - salt = pwhash.random_salt - 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 - test_vectors "raw_argon2i_hashes.json", Sodium::Pwhash::Mode::Argon2i13 - test_vectors "raw_argon2id_hashes.json", Sodium::Pwhash::Mode::Argon2id13 - end - - # from libsodium/test/default/pwhash_argon2id.c - it "RbNaCl key vectors" do - pwhash = Sodium::Pwhash.new - pwhash.mode = Sodium::Pwhash::Mode::Argon2id13 - pwhash.opslimit = 5_u64 - pwhash.memlimit = 7_256_678_u64 - key_len = 155 - - pass = "a347ae92bce9f80f6f595a4480fc9c2fe7e7d7148d371e9487d75f5c23008ffae0" \ - "65577a928febd9b1973a5a95073acdbeb6a030cfc0d79caa2dc5cd011cef02c08d" \ - "a232d76d52dfbca38ca8dcbd665b17d1665f7cf5fe59772ec909733b24de97d6f5" \ - "8d220b20c60d7c07ec1fd93c52c31020300c6c1facd77937a597c7a6".hexbytes - salt = "5541fbc995d5c197ba290346d2c559de".hexbytes - expected = "18acec5d6507739f203d1f5d9f1d862f7c2cdac4f19d2bdff64487e60d969e3ced6" \ - "15337b9eec6ac4461c6ca07f0939741e57c24d0005c7ea171a0ee1e7348249d135b" \ - "38f222e4dad7b9a033ed83f5ca27277393e316582033c74affe2566a2bea47f91f0" \ - "fd9fe49ece7e1f79f3ad6e9b23e0277c8ecc4b313225748dd2a80f5679534a0700e" \ - "246a79a49b3f74eb89ec6205fe1eeb941c73b1fcf1".hexbytes - - key = pwhash.derive_key salt, pass, key_len - key.should eq expected - end -end diff --git a/src/sodium/password.cr b/src/sodium/password.cr new file mode 100644 index 0000000..242e9b8 --- /dev/null +++ b/src/sodium/password.cr @@ -0,0 +1,33 @@ +require "./lib_sodium" +require "./secure_buffer" + +# [Argon2 Password Hashing](https://libsodium.gitbook.io/doc/password_hashing/the_argon2i_function) +# * #store #verify #needs_rehash? are used together for password verification. +# * #derive_key is used on it's own to generate password based keys. +# +# **See `examples/pwhash_selector.cr` for help on selecting parameters.** +# +# ## Creating a key for encryption with auto set parameters based on time. +# +module Sodium::Password + OPSLIMIT_MIN = LibSodium.crypto_pwhash_opslimit_min + OPSLIMIT_INTERACTIVE = LibSodium.crypto_pwhash_opslimit_interactive + OPSLIMIT_MODERATE = LibSodium.crypto_pwhash_opslimit_moderate + OPSLIMIT_SENSITIVE = LibSodium.crypto_pwhash_opslimit_sensitive + OPSLIMIT_MAX = LibSodium.crypto_pwhash_opslimit_max + + MEMLIMIT_MIN = LibSodium.crypto_pwhash_memlimit_min + MEMLIMIT_INTERACTIVE = LibSodium.crypto_pwhash_memlimit_interactive + # Don't use this. Maximum of the library which is more ram than any computer. + MEMLIMIT_MAX = LibSodium.crypto_pwhash_memlimit_max + + SALT_SIZE = LibSodium.crypto_pwhash_saltbytes + STR_SIZE = LibSodium.crypto_pwhash_strbytes + + class Error < Exception + class Verify < Error + end + end +end + +require "./password/**" diff --git a/src/sodium/password/abstract.cr b/src/sodium/password/abstract.cr new file mode 100644 index 0000000..15aa3a6 --- /dev/null +++ b/src/sodium/password/abstract.cr @@ -0,0 +1,38 @@ +require "../lib_sodium" +require "../secure_buffer" + +abstract class Sodium::Password::Abstract + property ops = OPSLIMIT_INTERACTIVE + # Specified in bytes. + property mem = MEMLIMIT_INTERACTIVE + + # Returns a random salt for use with #derive_key + def random_salt + Random::Secure.random_bytes SALT_SIZE + end + + def self.from_params(hash) + pw = self.new + + pw.ops = hash["ops"].as(UInt64) + pw.mem = hash["mem"].as(UInt64) + + if pw.responds_to?(:mode=) && (mode = hash["mode"]?) + pw.mode = Mode.parse mode.as(String) + end + if pw.responds_to?(:salt=) && (salt = hash["salt"]?) + pw.salt = salt.as(Bytes) + end + if pw.responds_to?(:key_size=) && (key_size = hash["key_size"]?) + pw.key_size = key_size.as(Int32) + end + if pw.responds_to?(:tcost=) && (tcost = hash["tcost"]?) + pw.tcost = tcost.as(Float64) + end + if pw.responds_to?(:auth=) && (auth = hash["auth"]?) + pw.auth = auth.as(Bytes) + end + + pw + end +end diff --git a/src/sodium/password/create_key.cr b/src/sodium/password/create_key.cr new file mode 100644 index 0000000..7df8e9e --- /dev/null +++ b/src/sodium/password/create_key.cr @@ -0,0 +1,130 @@ +require "./key" + +# [Argon2 Password Hashing](https://libsodium.gitbook.io/doc/password_hashing/the_argon2i_function) +# +# ## Create a key for encryption with auto set parameters based on time. +# +# Usage: +# ``` +# pwkc = Sodium::Password::Key::Create.new +# +# # Take approximately 1 second to generate a key. +# pwkc.tcost = 1.0 +# +# # Memory use will end up between `mem_min` and `mem_max` +# pwkc.mem_min = 128 * 1024 * 1024 # 128M +# pwkc.mem_max = 256 * 1024 * 1024 # 256M +# +# kdf, params = pwkc.create_kdf pass +# # Or +# key, params = pwkc.create_key pass, 32 +# +# # Save params.[mode, ops, mem, salt, key_size] to derive the same key later. +# ``` +# +# ## Deriving a previously created key. +# +# Usage: +# ``` +# pwkey = Sodium::Password::Key.new +# pwkey.mode = Mode.parse serialized[:mode] +# pwkey.ops = serialized[:ops] +# pwkey.mem = serialized[:mem] +# pwkey.salt = serialized[:salt] +# key_size = serialized[:key_size] +# +# kdf = pwhash.derive_kdf pass +# # Or +# key = pwkey.derive_key pass, key_size +# ``` +class Sodium::Password::Key::Create + # Specified in seconds. + property tcost = 0.1 + + # Specified in bytes. + # Currently the libsodium default. May increase between version. + property mem_min = MEMLIMIT_MIN + + # Specified in bytes. + # Currently defaults to 256M. May increase between version. + property mem_max : UInt64 = 256_u64 * 1024 * 1024 + + property mode : Mode = Mode.default + + # * 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, ops and mem. + + # Returns a consistent key based on [salt, pass, key_size, mode] in a SecureBuffer **and** Params. + # + # Params are set to run in approximately `tcost` seconds. + # + # Make sure you store `Params` for later use with #derive_key. + def create_key(pass : Bytes | String, key_size, *, salt : Bytes | String | Nil = nil) : {SecureBuffer, Params} + pw = Key.new + salt ||= pw.random_salt + pw.salt = salt.to_slice + + key = create_key! pw, pass.to_slice, key_size + {key, pw.to_params(salt: pw.salt, key_size: key_size, tcost: @tcost)} + end + + def create_kdf(pass, *, salt : Bytes | String | Nil = nil) : {Kdf, Params} + key, params = create_key pass, Kdf::KEY_SIZE, salt: salt + {Kdf.new(key), params} + end + + protected def create_key!(pw, pass : Bytes, key_size : Int32) : SecureBuffer + pw.ops = OPSLIMIT_MIN + mem = MEMLIMIT_MIN + + key = SecureBuffer.new key_size + + nsamples = 10 + samples = nsamples.times.map do + ts = time_derive_key key, pw, 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) + pw.mem = (pw.mem * mult).clamp(@mem_min, @mem_max) + + last_mem = pw.mem + while pw.mem != @mem_max + ts = time_derive_key key, pw, pass + # tcost exceeded by mem_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_mem = pw.mem + # increments of 1K for compatibility with other libraries. + pw.mem = (((pw.mem / 1024).to_f * Math.max(1.1, (@tcost / ts.to_f / 3.0))).ceil.to_u64 * 1024).clamp(@mem_min, @mem_max) + # stopped making progress + break if pw.mem == last_mem + end + + last_ops = pw.ops + loop do + ts = time_derive_key key, pw, pass + # 90% is close enough + break if ts.to_f >= @tcost * 0.90 + last_ops = pw.ops + pw.ops = (pw.ops.to_f * Math.max(1.1, (@tcost / ts.to_f))).ceil.to_u64 + # stopped making progress + break if pw.ops == last_ops + end + + key.readonly + end + + private def time_derive_key(key : SecureBuffer, pw, pass) + # TODO: switch to CPU time + ts = Time.measure do + pw.derive_key key, pass + end + ts.to_f + end +end diff --git a/src/sodium/password/hash.cr b/src/sodium/password/hash.cr new file mode 100644 index 0000000..80b594f --- /dev/null +++ b/src/sodium/password/hash.cr @@ -0,0 +1,40 @@ +require "./abstract" + +module Sodium::Password + class Hash < Abstract + # 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 + # * the other parameters required to verify the password, including the algorithm identifier, its version, ops and mem. + def create(pass) + outstr = Bytes.new STR_SIZE + if LibSodium.crypto_pwhash_str(outstr, pass, pass.bytesize, @ops, @mem) != 0 + raise Sodium::Error.new("crypto_pwhash_str") + end + outstr + end + + # Verify a password against a stored String. + # raises PasswordVerifyError on failure. + def verify(str, pass) + # BUG: verify str length + r = LibSodium.crypto_pwhash_str_verify(str, pass, pass.bytesize) + raise Error::Verify.new if r != 0 + self + end + + # Check if a password verification string str matches the parameters ops and mem, and the current default algorithm. + def needs_rehash?(str) : Bool + # BUG: verify str length + case LibSodium.crypto_pwhash_str_needs_rehash(str, @ops, @mem) + when 0 + false + when 1 + true + else + raise Sodium::Error.new("crypto_pwhash_str_needs_rehash") + end + end + end +end diff --git a/src/sodium/password/key.cr b/src/sodium/password/key.cr new file mode 100644 index 0000000..bca8445 --- /dev/null +++ b/src/sodium/password/key.cr @@ -0,0 +1,45 @@ +require "./abstract" +require "./mode" +require "../kdf" + +module Sodium::Password + # See `Sodium::Password::Key::Create` + # + # TODO: Usage example using the same params with multiple passwords. + class Key < Abstract + # Used by and must be set before calling #derive_key + property mode : Mode = Mode.default + + property salt : Bytes? + + # Must set a mode before calling. + def derive_key(pass : Bytes | String, key_bytes : Int32, *, salt : String | Bytes | Nil = nil) : SecureBuffer + key = SecureBuffer.new key_bytes + derive_key key, pass, salt: salt + key.readonly + end + + def derive_kdf(pass, *, salt = nil) : Kdf + key = derive_key pass, Kdf::KEY_SIZE, salt: salt + Kdf.new key + end + + # :nodoc: + def derive_key(key : SecureBuffer, pass : Bytes | String, *, salt : Bytes? = nil) : Nil + m = mode || raise ArgumentError.new("mode not set") + + salt ||= @salt + raise ArgumentError.new("missing salt") unless salt + salt = salt.not_nil! + raise "salt expected #{SALT_SIZE} bytes, got #{salt.bytesize} " if salt.bytesize != SALT_SIZE + + if LibSodium.crypto_pwhash(key.to_slice, key.bytesize, pass.to_slice, pass.bytesize, salt.to_slice, @ops, @mem, m) != 0 + raise Sodium::Error.new("crypto_pwhash") + end + end + + def to_params(*, salt = nil, key_size = nil, tcost : Float64? = nil) + Params.new @mode, @ops, @mem, salt: salt, key_size: key_size, tcost: tcost + end + end +end diff --git a/src/sodium/password/mode.cr b/src/sodium/password/mode.cr new file mode 100644 index 0000000..41f2245 --- /dev/null +++ b/src/sodium/password/mode.cr @@ -0,0 +1,12 @@ +require "../lib_sodium" + +enum Sodium::Password::Mode + # Use the most recent algorithm Argon2id13 for new applications. + Argon2i13 = 1 + Argon2id13 = 2 + + # The currently recommended algorithm, which can change from one version of libsodium to another. + def self.default + self.new LibSodium.crypto_pwhash_alg_default + end +end diff --git a/src/sodium/password/params.cr b/src/sodium/password/params.cr new file mode 100644 index 0000000..4c394c0 --- /dev/null +++ b/src/sodium/password/params.cr @@ -0,0 +1,40 @@ +# Contains the params necessary for #derive_key. +class Sodium::Password::Params + property mode : Mode + property ops : UInt64 + property mem : UInt64 + property salt : Bytes? + property key_size : Int32? + property tcost : Float64? + property auth : Bytes? + + def initialize(@mode, @ops, @mem, @salt = nil, @key_size = nil, @tcost = nil, @auth = nil) + end + + def to_h + hash = ::Hash(String, Int32 | UInt64 | String | Bytes | Float64).new initial_capacity: 5 + hash["ops"] = @ops + hash["mem"] = @mem + if m = @mode + hash["mode"] = m.to_s + end + if s = @salt + hash["salt"] = s + end + if tc = @tcost + hash["tcost"] = tc + end + if ks = @key_size + hash["key_size"] = ks + end + if au = @auth + hash["auth"] = au + end + + hash + end + + def self.from_h(hash) + self.new Pwhash::Mode.parse(hash["mode"]), hash["ops"], hash["mem"], hash["tcost"]?, hash["salt"]? + end +end diff --git a/src/sodium/pwhash.cr b/src/sodium/pwhash.cr deleted file mode 100644 index c75c083..0000000 --- a/src/sodium/pwhash.cr +++ /dev/null @@ -1,206 +0,0 @@ -require "./lib_sodium" -require "./secure_buffer" - -module Sodium - # [Argon2 Password Hashing](https://libsodium.gitbook.io/doc/password_hashing/the_argon2i_function) - # * #store #verify #needs_rehash? are used together for password verification. - # * #derive_key is used on it's own to generate password based keys. - # - # **See `examples/pwhash_selector.cr` for help on selecting parameters.** - class Pwhash - class PasswordVerifyError < Sodium::Error - end - - OPSLIMIT_MIN = LibSodium.crypto_pwhash_opslimit_min - OPSLIMIT_INTERACTIVE = LibSodium.crypto_pwhash_opslimit_interactive - OPSLIMIT_MODERATE = LibSodium.crypto_pwhash_opslimit_moderate - OPSLIMIT_SENSITIVE = LibSodium.crypto_pwhash_opslimit_sensitive - OPSLIMIT_MAX = LibSodium.crypto_pwhash_opslimit_max - - MEMLIMIT_MIN = LibSodium.crypto_pwhash_memlimit_min - MEMLIMIT_INTERACTIVE = LibSodium.crypto_pwhash_memlimit_interactive - # Don't use this. Maximum of the library which is more ram than any computer. - MEMLIMIT_MAX = LibSodium.crypto_pwhash_memlimit_max - - SALT_SIZE = LibSodium.crypto_pwhash_saltbytes - STR_SIZE = LibSodium.crypto_pwhash_strbytes - - # Use the most recent algorithm Argon2id13 for new applications. - enum Mode - Argon2i13 = 1 - Argon2id13 = 2 - - # The currently recommended algorithm, which can change from one version of libsodium to another. - def self.default - Mode.new LibSodium.crypto_pwhash_alg_default - end - end - - property opslimit = OPSLIMIT_INTERACTIVE - # 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 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 - # * the other parameters required to verify the password, including the algorithm identifier, its version, opslimit and memlimit. - def create(pass) - outstr = Bytes.new STR_SIZE - if LibSodium.crypto_pwhash_str(outstr, pass, pass.bytesize, @opslimit, @memlimit) != 0 - raise Sodium::Error.new("crypto_pwhash_str") - end - 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 - # Passed - else - raise PasswordVerifyError.new - end - 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) - when 0 - false - when 1 - true - else - raise Sodium::Error.new("crypto_pwhash_str_needs_rehash") - end - end - - # 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 - - # 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") - - 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 - - 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 - - # Returns a random salt for use with #derive_key - def random_salt - Random::Secure.random_bytes SALT_SIZE - end - end -end