Split Sodium::Pwhash in to Sodium::Password::Hash and Sodium::Password::Key.
This commit is contained in:
parent
fbb7e9434a
commit
b4fe9ef1c3
177
spec/sodium/password_spec.cr
Normal file
177
spec/sodium/password_spec.cr
Normal file
@ -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
|
@ -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
|
|
33
src/sodium/password.cr
Normal file
33
src/sodium/password.cr
Normal file
@ -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/**"
|
38
src/sodium/password/abstract.cr
Normal file
38
src/sodium/password/abstract.cr
Normal file
@ -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
|
130
src/sodium/password/create_key.cr
Normal file
130
src/sodium/password/create_key.cr
Normal file
@ -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
|
40
src/sodium/password/hash.cr
Normal file
40
src/sodium/password/hash.cr
Normal file
@ -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
|
45
src/sodium/password/key.cr
Normal file
45
src/sodium/password/key.cr
Normal file
@ -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
|
12
src/sodium/password/mode.cr
Normal file
12
src/sodium/password/mode.cr
Normal file
@ -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
|
40
src/sodium/password/params.cr
Normal file
40
src/sodium/password/params.cr
Normal file
@ -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
|
@ -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
|
|
Loading…
Reference in New Issue
Block a user