(Feat): Initial Commit
This commit is contained in:
114
internal/util/hashing/argon2.go
Normal file
114
internal/util/hashing/argon2.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package hashing
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/argon2"
|
||||
)
|
||||
|
||||
// Inspired by: https://github.com/alexedwards/argon2id @ 2020-04-22T14:13:23ZZ
|
||||
|
||||
const (
|
||||
// Argon2HashID represents the hash ID set in the (pseudo) modular crypt format used to store the hashed password and params in a single string.
|
||||
Argon2HashID = "argon2id"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidArgon2Hash indicates the argon2id hash was malformed and could not be decoded.
|
||||
ErrInvalidArgon2Hash = errors.New("invalid argon2id hash")
|
||||
// ErrIncompatibleArgon2Version indicates the argon2id hash provided was generated with a different, incompatible argon2 version.
|
||||
ErrIncompatibleArgon2Version = errors.New("incompatible argon2 version")
|
||||
)
|
||||
|
||||
func HashPassword(password string, params *Argon2Params) (string, error) {
|
||||
salt, err := generateSalt(params.SaltLength)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
key := argon2.IDKey([]byte(password), salt, params.Time, params.Memory, params.Threads, params.KeyLength)
|
||||
|
||||
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
|
||||
b64Key := base64.RawStdEncoding.EncodeToString(key)
|
||||
|
||||
return fmt.Sprintf("$%s$v=%d$m=%d,t=%d,p=%d$%s$%s", Argon2HashID, argon2.Version, params.Memory, params.Time, params.Threads, b64Salt, b64Key), nil
|
||||
}
|
||||
|
||||
func ComparePasswordAndHash(password string, hash string) (bool, error) {
|
||||
params, salt, key, err := decodeArgon2Hash(hash)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
pKey := argon2.IDKey([]byte(password), salt, params.Time, params.Memory, params.Threads, params.KeyLength)
|
||||
|
||||
keyLen := len(key)
|
||||
pKeyLen := len(pKey)
|
||||
|
||||
if keyLen > math.MaxInt32 || pKeyLen > math.MaxInt32 {
|
||||
return false, ErrInvalidArgon2Hash
|
||||
}
|
||||
|
||||
if subtle.ConstantTimeEq(int32(keyLen), int32(pKeyLen)) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if subtle.ConstantTimeCompare(key, pKey) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
const (
|
||||
argon2HashParts = 6
|
||||
)
|
||||
|
||||
func decodeArgon2Hash(hash string) (*Argon2Params, []byte, []byte, error) {
|
||||
vals := strings.Split(hash, "$") // splits into array of 6 values, with val[0] being empty --> length/indicies "offset" by one
|
||||
if len(vals) != argon2HashParts {
|
||||
return nil, nil, nil, ErrInvalidArgon2Hash
|
||||
}
|
||||
if vals[1] != Argon2HashID {
|
||||
return nil, nil, nil, ErrInvalidArgon2Hash
|
||||
}
|
||||
|
||||
var version int
|
||||
_, err := fmt.Sscanf(vals[2], "v=%d", &version)
|
||||
if err != nil {
|
||||
return nil, nil, nil, ErrIncompatibleArgon2Version
|
||||
}
|
||||
if version != argon2.Version {
|
||||
return nil, nil, nil, ErrIncompatibleArgon2Version
|
||||
}
|
||||
|
||||
params := &Argon2Params{}
|
||||
_, err = fmt.Sscanf(vals[3], "m=%d,t=%d,p=%d", ¶ms.Memory, ¶ms.Time, ¶ms.Threads)
|
||||
if err != nil {
|
||||
return nil, nil, nil, ErrInvalidArgon2Hash
|
||||
}
|
||||
|
||||
salt, err := base64.RawStdEncoding.DecodeString(vals[4])
|
||||
if err != nil {
|
||||
return nil, nil, nil, ErrInvalidArgon2Hash
|
||||
}
|
||||
|
||||
key, err := base64.RawStdEncoding.DecodeString(vals[5])
|
||||
if err != nil {
|
||||
return nil, nil, nil, ErrInvalidArgon2Hash
|
||||
}
|
||||
|
||||
keyLength := len(key)
|
||||
if keyLength > math.MaxInt32 {
|
||||
return nil, nil, nil, ErrInvalidArgon2Hash
|
||||
}
|
||||
|
||||
params.KeyLength = uint32(keyLength)
|
||||
|
||||
return params, salt, key, nil
|
||||
}
|
||||
39
internal/util/hashing/argon2_params.go
Normal file
39
internal/util/hashing/argon2_params.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package hashing
|
||||
|
||||
import "allaboutapps.dev/aw/go-starter/internal/util"
|
||||
|
||||
var (
|
||||
// DefaultArgon2Params represents Argon2ID parameter recommendations in accordance with:
|
||||
// https://pkg.go.dev/golang.org/x/crypto@v0.0.0-20200420201142-3c4aac89819a/argon2?tab=doc#IDKey @ 2020-04-22T11:23:38Z
|
||||
DefaultArgon2Params = &Argon2Params{
|
||||
Time: 1, // 1 second
|
||||
//nolint:mnd
|
||||
Memory: 64 * 1024, // ~64MB memory costs
|
||||
//nolint:mnd
|
||||
Threads: 4, // 4 threads
|
||||
//nolint:mnd
|
||||
KeyLength: 32, // 256 bit key length
|
||||
//nolint:mnd
|
||||
SaltLength: 16, // 126 bit salt length
|
||||
}
|
||||
)
|
||||
|
||||
type Argon2Params struct {
|
||||
Time uint32
|
||||
Memory uint32
|
||||
Threads uint8
|
||||
KeyLength uint32
|
||||
SaltLength uint32
|
||||
}
|
||||
|
||||
func DefaultArgon2ParamsFromEnv() *Argon2Params {
|
||||
params := &Argon2Params{
|
||||
Time: util.GetEnvAsUint32("AUTH_HASHING_ARGON2_TIME", DefaultArgon2Params.Time),
|
||||
Memory: util.GetEnvAsUint32("AUTH_HASHING_ARGON2_MEMORY", DefaultArgon2Params.Memory),
|
||||
Threads: util.GetEnvAsUint8("AUTH_HASHING_ARGON2_THREADS", DefaultArgon2Params.Threads),
|
||||
KeyLength: util.GetEnvAsUint32("AUTH_HASHING_ARGON2_KEY_LENGTH", DefaultArgon2Params.KeyLength),
|
||||
SaltLength: util.GetEnvAsUint32("AUTH_HASHING_ARGON2_SALT_LENGTH", DefaultArgon2Params.SaltLength),
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
106
internal/util/hashing/argon2_test.go
Normal file
106
internal/util/hashing/argon2_test.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package hashing_test
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"allaboutapps.dev/aw/go-starter/internal/util/hashing"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHashPassword(t *testing.T) {
|
||||
hashRegex, err := regexp.Compile(`^\$argon2id\$v=19\$m=65536,t=1,p=4\$[A-Za-z0-9+/]{22}\$[A-Za-z0-9+/]{43}$`)
|
||||
require.NoError(t, err, "failed to compile hash regex")
|
||||
|
||||
hash1, err := hashing.HashPassword("t3stp4ssw0rd", hashing.DefaultArgon2Params)
|
||||
require.NoError(t, err, "failed to hash password")
|
||||
|
||||
assert.Truef(t, hashRegex.MatchString(hash1), "hash %q is not formatted properly", hash1)
|
||||
|
||||
hash2, err := hashing.HashPassword("t3stp4ssw0rd", hashing.DefaultArgon2Params)
|
||||
require.NoError(t, err, "failed to hash password")
|
||||
|
||||
assert.Truef(t, hashRegex.MatchString(hash2), "hash %q is not formatted properly", hash2)
|
||||
|
||||
assert.NotEqualf(t, hash1, hash2, "hashes %q and %q are not unique", hash1, hash2)
|
||||
}
|
||||
|
||||
func BenchmarkHashPassword(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := hashing.HashPassword("t3stp4ssw0rd", hashing.DefaultArgon2Params)
|
||||
if err != nil {
|
||||
b.Errorf("failed to hash password #%d: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
hash = "$argon2id$v=19$m=65536,t=1,p=4$c8FqPHMT83tyxE2v0xDAFw$s2qmbRoRRbfyLIVFUzRwzE7F8PLjchpLKaV7Wf7tHgk"
|
||||
)
|
||||
|
||||
func TestComparePasswordAndHash(t *testing.T) {
|
||||
match, err := hashing.ComparePasswordAndHash("t3stp4ssw0rd", hash)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, match)
|
||||
|
||||
match, err = hashing.ComparePasswordAndHash("wr0ngt3stp4ssw0rd", hash)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, match)
|
||||
}
|
||||
|
||||
func BenchmarkComparePasswordAndHash(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := hashing.ComparePasswordAndHash("t3stp4ssw0rd", hash)
|
||||
if err != nil {
|
||||
b.Errorf("failed to compare password and hash #%d: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCompareWrongPasswordAndHash(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := hashing.ComparePasswordAndHash("wr0ngt3stp4ssw0rd", hash)
|
||||
if err != nil {
|
||||
b.Errorf("failed to compare wrong password and hash #%d: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestComparePasswordAndInvalidHash(t *testing.T) {
|
||||
_, err := hashing.ComparePasswordAndHash("t3stp4ssw0rd", "$argon2i$v=19$m=65536,t=1,p=4$c8FqPHMT83tyxE2v0xDAFw$s2qmbRoRRbfyLIVFUzRwzE7F8PLjchpLKaV7Wf7tHgk")
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, hashing.ErrInvalidArgon2Hash, err)
|
||||
|
||||
_, err = hashing.ComparePasswordAndHash("t3stp4ssw0rd", "$argon2id$v=19$m=65536,t=1,p=4$c8FqPHMT83tyxE2v0xDAFw")
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, hashing.ErrInvalidArgon2Hash, err)
|
||||
|
||||
_, err = hashing.ComparePasswordAndHash("t3stp4ssw0rd", "$argon2id$v=20$m=65536,t=1,p=4$c8FqPHMT83tyxE2v0xDAFw$s2qmbRoRRbfyLIVFUzRwzE7F8PLjchpLKaV7Wf7tHgk")
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, hashing.ErrIncompatibleArgon2Version, err)
|
||||
|
||||
_, err = hashing.ComparePasswordAndHash("t3stp4ssw0rd", "$argon2id$v=a$m=65536,t=1,p=4$c8FqPHMT83tyxE2v0xDAFw$s2qmbRoRRbfyLIVFUzRwzE7F8PLjchpLKaV7Wf7tHgk")
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, hashing.ErrIncompatibleArgon2Version, err)
|
||||
|
||||
_, err = hashing.ComparePasswordAndHash("t3stp4ssw0rd", "$argon2id$v=19$m=a,t=1,p=4$c8FqPHMT83tyxE2v0xDAFw$s2qmbRoRRbfyLIVFUzRwzE7F8PLjchpLKaV7Wf7tHgk")
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, hashing.ErrInvalidArgon2Hash, err)
|
||||
|
||||
_, err = hashing.ComparePasswordAndHash("t3stp4ssw0rd", "$argon2id$v=19$m=65536,t=a,p=4$c8FqPHMT83tyxE2v0xDAFw$s2qmbRoRRbfyLIVFUzRwzE7F8PLjchpLKaV7Wf7tHgk")
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, hashing.ErrInvalidArgon2Hash, err)
|
||||
|
||||
_, err = hashing.ComparePasswordAndHash("t3stp4ssw0rd", "$argon2id$v=19$m=65536,t=1,p=a$c8FqPHMT83tyxE2v0xDAFw$s2qmbRoRRbfyLIVFUzRwzE7F8PLjchpLKaV7Wf7tHgk")
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, hashing.ErrInvalidArgon2Hash, err)
|
||||
|
||||
_, err = hashing.ComparePasswordAndHash("t3stp4ssw0rd", "$argon2id$v=19$m=65536,t=1,p=4$ä$s2qmbRoRRbfyLIVFUzRwzE7F8PLjchpLKaV7Wf7tHgk")
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, hashing.ErrInvalidArgon2Hash, err)
|
||||
|
||||
_, err = hashing.ComparePasswordAndHash("t3stp4ssw0rd", "$argon2id$v=19$m=65536,t=1,p=4$c8FqPHMT83tyxE2v0xDAFw$ä")
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, hashing.ErrInvalidArgon2Hash, err)
|
||||
}
|
||||
17
internal/util/hashing/util.go
Normal file
17
internal/util/hashing/util.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package hashing
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func generateSalt(n uint32) ([]byte, error) {
|
||||
result := make([]byte, n)
|
||||
|
||||
_, err := rand.Read(result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate salt: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
Reference in New Issue
Block a user