Updated PBKDF2 hasher with more complex format handling
This commit is contained in:
parent
a7134c1c90
commit
6480eb86e2
2
go.mod
2
go.mod
|
@ -1,6 +1,6 @@
|
|||
module github.com/iegomez/mosquitto-go-auth
|
||||
|
||||
go 1.18
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
|
|
7
go.sum
7
go.sum
|
@ -16,6 +16,7 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m
|
|||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||
|
@ -43,6 +44,7 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
|
|||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190328170749-bb2674552d8f h1:4Gslotqbs16iAg+1KR/XdabIfq8TlAWHdwS5QJFksLc=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190328170749-bb2674552d8f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
|
@ -73,8 +75,11 @@ github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJ
|
|||
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
|
||||
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
||||
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
|
@ -231,9 +236,11 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
|
|||
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
|
||||
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
|
|
@ -126,22 +126,29 @@ func TestArgon2ID(t *testing.T) {
|
|||
|
||||
func TestPBKDF2(t *testing.T) {
|
||||
password := "test-password"
|
||||
b64Hasher := NewPBKDF2Hasher(defaultPBKDF2SaltSize, defaultPBKDF2Iterations, defaultPBKDF2Algorithm, Base64, defaultPBKDF2KeyLen)
|
||||
utf8Hasher := NewPBKDF2Hasher(defaultPBKDF2SaltSize, defaultPBKDF2Iterations, defaultPBKDF2Algorithm, UTF8, defaultPBKDF2KeyLen)
|
||||
|
||||
// Test base64.
|
||||
hasher := NewPBKDF2Hasher(defaultPBKDF2SaltSize, defaultPBKDF2Iterations, defaultPBKDF2Algorithm, Base64, defaultPBKDF2KeyLen)
|
||||
t.Run("OlderFormat", func(t *testing.T) {
|
||||
t.Run("Base64", func(t *testing.T) {
|
||||
passwordHash, err := b64Hasher.Hash(password)
|
||||
|
||||
passwordHash, err := hasher.Hash(password)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, b64Hasher.Compare(password, passwordHash))
|
||||
assert.False(t, b64Hasher.Compare("other", passwordHash))
|
||||
})
|
||||
t.Run("UTF8", func(t *testing.T) {
|
||||
passwordHash, err := utf8Hasher.Hash(password)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, hasher.Compare(password, passwordHash))
|
||||
assert.False(t, hasher.Compare("other", passwordHash))
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, utf8Hasher.Compare(password, passwordHash))
|
||||
assert.False(t, utf8Hasher.Compare("other", passwordHash))
|
||||
})
|
||||
})
|
||||
|
||||
// Test UTF8.
|
||||
hasher = NewPBKDF2Hasher(defaultPBKDF2SaltSize, defaultPBKDF2Iterations, defaultPBKDF2Algorithm, UTF8, defaultPBKDF2KeyLen)
|
||||
|
||||
passwordHash, err = hasher.Hash(password)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, hasher.Compare(password, passwordHash))
|
||||
assert.False(t, hasher.Compare("other", passwordHash))
|
||||
t.Run("PHC-SF-Spec", func(t *testing.T) {
|
||||
passwordHash := "$pbkdf2-sha512$i=10000,l=32$/DsNR8DBuoF/MxzLY+QVaw$YNfYNfT+6yT2blLrXKKR8Ll+aesgHYqSOtFTBsyscRM"
|
||||
assert.True(t, b64Hasher.Compare(password, passwordHash))
|
||||
assert.False(t, b64Hasher.Compare("other", passwordHash))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
@ -23,13 +24,13 @@ type pbkdf2Hasher struct {
|
|||
keyLen int
|
||||
}
|
||||
|
||||
func NewPBKDF2Hasher(saltSize int, iterations int, algorithm string, saltEncoding string, keylen int) HashComparer {
|
||||
func NewPBKDF2Hasher(saltSize int, iterations int, algorithm string, saltEncoding string, keyLen int) HashComparer {
|
||||
return pbkdf2Hasher{
|
||||
saltSize: saltSize,
|
||||
iterations: iterations,
|
||||
algorithm: algorithm,
|
||||
saltEncoding: preferredEncoding(saltEncoding),
|
||||
keyLen: keylen,
|
||||
keyLen: keyLen,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,20 +38,18 @@ func NewPBKDF2Hasher(saltSize int, iterations int, algorithm string, saltEncodin
|
|||
* PBKDF2 methods are adapted from github.com/brocaar/chirpstack-application-server, some comments included.
|
||||
*/
|
||||
|
||||
// Hash function reference may be found at https://github.com/brocaar/chirpstack-application-server/blob/master/internal/storage/user.go#L421.
|
||||
|
||||
// Generate the hash of a password for storage in the database.
|
||||
// NOTE: We store the details of the hashing algorithm with the hash itself,
|
||||
// making it easy to recreate the hash for password checking, even if we change
|
||||
// the default criteria here.
|
||||
// Hash function generates a hash of the supplied password. The hash
|
||||
// can then be stored directly in the database. The return hash will
|
||||
// contain options according to the PHC String format found at
|
||||
// https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md
|
||||
func (h pbkdf2Hasher) Hash(password string) (string, error) {
|
||||
// Generate a random salt value with the given salt size.
|
||||
salt := make([]byte, h.saltSize)
|
||||
_, err := rand.Read(salt)
|
||||
|
||||
// We need to ensure that salt doesn contain $, which is 36 in decimal.
|
||||
// So we check if there'sbyte that represents $ and change it with a random number in the range 0-35
|
||||
//// This is far from ideal, but should be good enough with a reasonable salt size.
|
||||
// We need to ensure that salt doesn't contain $, which is 36 in decimal.
|
||||
// So we check if there's byte that represents $ and change it with a random number in the range 0-35
|
||||
// // This is far from ideal, but should be good enough with a reasonable salt size.
|
||||
for i := 0; i < len(salt); i++ {
|
||||
if salt[i] == 36 {
|
||||
n, err := rand.Int(rand.Reader, big.NewInt(35))
|
||||
|
@ -69,52 +68,116 @@ func (h pbkdf2Hasher) Hash(password string) (string, error) {
|
|||
return h.hashWithSalt(password, salt, h.iterations, h.algorithm, h.keyLen), nil
|
||||
}
|
||||
|
||||
// HashCompare verifies that passed password hashes to the same value as the
|
||||
// Compare verifies that passed password hashes to the same value as the
|
||||
// passed passwordHash.
|
||||
// Reference: https://github.com/brocaar/chirpstack-application-server/blob/master/internal/storage/user.go#L458.
|
||||
// Parsing reference: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md
|
||||
func (h pbkdf2Hasher) Compare(password string, passwordHash string) bool {
|
||||
hashSplit := strings.Split(passwordHash, "$")
|
||||
hashSplit := h.getFields(passwordHash)
|
||||
|
||||
if len(hashSplit) != 5 {
|
||||
log.Errorf("invalid PBKDF2 hash supplied, expected length 5, got: %d", len(hashSplit))
|
||||
return false
|
||||
}
|
||||
|
||||
algorithm := hashSplit[1]
|
||||
|
||||
iterations, err := strconv.Atoi(hashSplit[2])
|
||||
if err != nil {
|
||||
log.Errorf("iterations error: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
var salt []byte
|
||||
switch h.saltEncoding {
|
||||
case UTF8:
|
||||
salt = []byte(hashSplit[3])
|
||||
default:
|
||||
salt, err = base64.StdEncoding.DecodeString(hashSplit[3])
|
||||
var (
|
||||
err error
|
||||
algorithm string
|
||||
paramString string
|
||||
hashedPassword []byte
|
||||
salt []byte
|
||||
iterations int
|
||||
keyLen int
|
||||
)
|
||||
if hashSplit[0] == "PBKDF2" {
|
||||
algorithm = hashSplit[1]
|
||||
iterations, err = strconv.Atoi(hashSplit[2])
|
||||
if err != nil {
|
||||
log.Errorf("base64 salt error: %s", err)
|
||||
log.Errorf("iterations error: %s", err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
hashedPassword, err := base64.StdEncoding.DecodeString(hashSplit[4])
|
||||
if err != nil {
|
||||
log.Errorf("base64 hash decoding error: %s", err)
|
||||
switch h.saltEncoding {
|
||||
case UTF8:
|
||||
salt = []byte(hashSplit[3])
|
||||
default:
|
||||
var err error
|
||||
salt, err = base64.StdEncoding.DecodeString(hashSplit[3])
|
||||
if err != nil {
|
||||
log.Errorf("base64 salt error: %s", err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
hashedPassword, err = base64.StdEncoding.DecodeString(hashSplit[4])
|
||||
if err != nil {
|
||||
log.Errorf("base64 hash decoding error: %s", err)
|
||||
return false
|
||||
}
|
||||
keyLen = len(hashedPassword)
|
||||
|
||||
} else if hashSplit[0] == "pbkdf2-sha512" {
|
||||
algorithm = "sha512"
|
||||
paramString = hashSplit[1]
|
||||
|
||||
opts := strings.Split(paramString, ",")
|
||||
for _, opt := range opts {
|
||||
parts := strings.Split(opt, "=")
|
||||
for i := 0; i < len(parts); i += 2 {
|
||||
key := parts[i]
|
||||
val := parts[i+1]
|
||||
switch key {
|
||||
case "i":
|
||||
iterations, _ = strconv.Atoi(val)
|
||||
case "l":
|
||||
keyLen, _ = strconv.Atoi(val)
|
||||
default:
|
||||
log.Errorf("unknown options key (\"%s\")", key)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch h.saltEncoding {
|
||||
case UTF8:
|
||||
salt = []byte(hashSplit[2])
|
||||
default:
|
||||
var err error
|
||||
salt, err = base64.StdEncoding.WithPadding(base64.NoPadding).DecodeString(hashSplit[2])
|
||||
if err != nil {
|
||||
log.Errorf("base64 salt error: %s", err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
hashedPassword, err = base64.StdEncoding.WithPadding(base64.NoPadding).DecodeString(hashSplit[3])
|
||||
} else {
|
||||
log.Errorf("invalid PBKDF2 hash supplied, unrecognized format \"%s\"", hashSplit[0])
|
||||
return false
|
||||
}
|
||||
|
||||
keylen := len(hashedPassword)
|
||||
newHash := h.hashWithSalt(password, salt, iterations, algorithm, keyLen)
|
||||
hashSplit = h.getFields(newHash)
|
||||
newHashedPassword, err := base64.StdEncoding.DecodeString(hashSplit[4])
|
||||
if err != nil {
|
||||
log.Errorf("base64 salt error: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return passwordHash == h.hashWithSalt(password, salt, iterations, algorithm, keylen)
|
||||
return slices.Compare(hashedPassword, newHashedPassword) == 0
|
||||
}
|
||||
|
||||
func (h pbkdf2Hasher) getFields(passwordHash string) []string {
|
||||
hashSplit := strings.FieldsFunc(passwordHash, func(r rune) bool {
|
||||
switch r {
|
||||
case '$':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})
|
||||
return hashSplit
|
||||
}
|
||||
|
||||
// Reference: https://github.com/brocaar/chirpstack-application-server/blob/master/internal/storage/user.go#L432.
|
||||
func (h pbkdf2Hasher) hashWithSalt(password string, salt []byte, iterations int, algorithm string, keylen int) string {
|
||||
// Generate the hashed password. This should be a little painful, adjust ITERATIONS
|
||||
// if it needs performance tweeking. Greatly depends on the hardware.
|
||||
// if it needs performance tweaking. Greatly depends on the hardware.
|
||||
// NOTE: We store these details with the returned hashed, so changes will not
|
||||
// affect our ability to do password compares.
|
||||
shaHash := sha512.New
|
||||
|
|
Loading…
Reference in New Issue