Compare commits

...

6 Commits

Author SHA1 Message Date
Saharat Saengsawang e435c51b6c
Merge 17c5f1e372 into afd1bd78b3 2024-03-08 16:03:28 +01:00
Ignacio Gómez afd1bd78b3
Merge pull request #319 from loffa/update-pbkdf2-hash
Updated PBKDF2 hasher with more complex format handling
2024-03-08 11:43:13 -03:00
Jesper Falk 05bbc60380 Lowered go version 2024-03-02 09:49:04 +01:00
Jesper Falk 6480eb86e2 Updated PBKDF2 hasher with more complex format handling 2024-02-27 16:11:08 +01:00
Saharat 17c5f1e372 fixed log format for MongoDB, and tls config 2024-01-25 15:38:36 -08:00
Saharat e2c3930ebf Fixed MongoDB insecureSkipVerify, Added MongoDB tls certificate, ca, key 2024-01-18 21:33:57 -08:00
4 changed files with 199 additions and 65 deletions

View File

@ -1275,8 +1275,11 @@ Options for `mongo` are the following:
| auth_opt_mongo_users | users | N | User collection |
| auth_opt_mongo_acls | acls | N | ACL collection |
| auth_opt_mongo_disable_superuser | true | N | Disable query to check for superuser |
| auth_opt_mongo_with_tls | false | N | Connect with TLS |
| auth_opt_mongo_insecure_skip_verify | false | N | Verify server's certificate chain |
| auth_opt_mongo_with_tls | false | N | Connect with TLS |
| auth_opt_mongo_tlsca | "" | N | TLS Certificate Authority (CA) |
| auth_opt_mongo_tlscert | "" | N | TLS Client Certificate |
| auth_opt_mongo_tlskey | "" | N | TLS Client Certificate Private Key |
If you experience any problem connecting to a replica set, please refer to [this issue](https://github.com/iegomez/mosquitto-go-auth/issues/32).

View File

@ -3,9 +3,11 @@ package backends
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"strings"
"time"
"os"
. "github.com/iegomez/mosquitto-go-auth/backends/constants"
"github.com/iegomez/mosquitto-go-auth/backends/topics"
@ -30,8 +32,11 @@ type Mongo struct {
Conn *mongo.Client
disableSuperuser bool
hasher hashing.HashComparer
withTLS bool
insecureSkipVerify bool
withTLS bool
TLSCa string
TLSCert string
TLSKey string
}
type MongoAcl struct {
@ -60,8 +65,11 @@ func NewMongo(authOpts map[string]string, logLevel log.Level, hasher hashing.Has
UsersCollection: "users",
AclsCollection: "acls",
hasher: hasher,
withTLS: false,
insecureSkipVerify: false,
withTLS: false,
TLSCa: "",
TLSCert: "",
TLSKey: "",
}
if authOpts["mongo_disable_superuser"] == "true" {
@ -100,14 +108,32 @@ func NewMongo(authOpts map[string]string, logLevel log.Level, hasher hashing.Has
m.AclsCollection = aclsCollection
}
if authOpts["mongo_use_tls"] == "true" {
m.withTLS = true
}
if authOpts["mongo_insecure_skip_verify"] == "true" {
m.insecureSkipVerify = true
}
useTlsClientCertificate := false
if authOpts["mongo_with_tls"] == "true" {
m.withTLS = true
}
if TLSCa, ok := authOpts["mongo_tlsca"]; ok {
m.TLSCa = TLSCa
useTlsClientCertificate = true
}
if TLSCert, ok := authOpts["mongo_tlscert"]; ok {
m.TLSCert = TLSCert
useTlsClientCertificate = true
}
if TLSKey, ok := authOpts["mongo_tlskey"]; ok {
m.TLSKey = TLSKey
useTlsClientCertificate = true
}
addr := fmt.Sprintf("mongodb://%s:%s", m.Host, m.Port)
to := 60 * time.Second
@ -117,7 +143,34 @@ func NewMongo(authOpts map[string]string, logLevel log.Level, hasher hashing.Has
}
if m.withTLS {
opts.TLSConfig = &tls.Config{}
log.Info("mongo backend: tls enabled")
opts.TLSConfig = &tls.Config{
InsecureSkipVerify: m.insecureSkipVerify,
}
if useTlsClientCertificate {
caCert, err := os.ReadFile(m.TLSCa)
if err != nil {
log.Errorf("mongo backend: tls error: %s", err)
}
caCertPool := x509.NewCertPool()
if ok := caCertPool.AppendCertsFromPEM(caCert); !ok {
log.Error("mongo backend: tls error: CA file must be in PEM format")
}
cert, err := tls.LoadX509KeyPair(m.TLSCert, m.TLSKey)
if err != nil {
log.Errorf("mongo backend: tls error: %s", err)
}
opts.TLSConfig = &tls.Config{
RootCAs: caCertPool,
Certificates: []tls.Certificate{cert},
InsecureSkipVerify: m.insecureSkipVerify,
}
}
}
opts.ApplyURI(addr)

View File

@ -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))
})
}

View File

@ -23,13 +23,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 +37,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 +67,125 @@ 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
}
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
}
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 h.compareBytes(hashedPassword, newHashedPassword)
}
func (h pbkdf2Hasher) compareBytes(a, b []byte) bool {
for i, x := range a {
if b[i] != x {
return false
}
}
return true
}
hashedPassword, err := base64.StdEncoding.DecodeString(hashSplit[4])
if err != nil {
log.Errorf("base64 hash decoding error: %s", err)
return false
}
keylen := len(hashedPassword)
return passwordHash == h.hashWithSalt(password, salt, iterations, algorithm, keylen)
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