Compare to check what messed up the refresh check.

This commit is contained in:
Ignacio Gómez 2024-02-24 00:07:03 -03:00
parent a7134c1c90
commit 0fcf12663d
8 changed files with 161 additions and 107 deletions

View File

@ -42,4 +42,13 @@ service:
clean:
rm -f go-auth.h
rm -f go-auth.so
rm -f pw
rm -f pw
docker-build:
docker build -t mosquitto-go-auth.test -f Dockerfile.runtest .
docker-test:
docker run --rm mosquitto-go-auth.test ./run-test-in-docker.sh
docker-test-local:
docker run -v $(pwd):/app --rm -ti mosquitto-go-auth.test ./run-test-in-docker.sh

136
cache/cache.go vendored
View File

@ -18,23 +18,25 @@ import (
// redisCache stores necessary values for Redis cache
type redisStore struct {
authExpiration time.Duration
aclExpiration time.Duration
authJitter time.Duration
aclJitter time.Duration
refreshExpiration bool
client bes.RedisClient
h hash.Hash
client bes.RedisClient
h hash.Hash
options Options
}
type goStore struct {
authExpiration time.Duration
aclExpiration time.Duration
authJitter time.Duration
aclJitter time.Duration
refreshExpiration bool
client *goCache.Cache
h hash.Hash
client *goCache.Cache
h hash.Hash
options Options
}
type Options struct {
AuthExpiration time.Duration
AclExpiration time.Duration
SuperuserExpiration time.Duration
AuthJitter time.Duration
AclJitter time.Duration
SuperuserJitter time.Duration
RefreshExpiration bool
}
const (
@ -46,47 +48,41 @@ type Store interface {
CheckAuthRecord(ctx context.Context, username, password string) (bool, bool)
SetACLRecord(ctx context.Context, username, topic, clientid string, acc int, granted string) error
CheckACLRecord(ctx context.Context, username, topic, clientid string, acc int) (bool, bool)
SetSuperuserRecord(ctx context.Context, username, password, granted string) error
CheckSuperuserRecord(ctx context.Context, username, password string) (bool, bool)
Connect(ctx context.Context, reset bool) bool
Close()
}
// NewGoStore initializes a cache using go-cache as the store.
func NewGoStore(authExpiration, aclExpiration, authJitter, aclJitter time.Duration, refreshExpiration bool) *goStore {
func NewGoStore(options Options) *goStore {
// TODO: support hydrating the cache to retain previous values.
return &goStore{
authExpiration: authExpiration,
aclExpiration: aclExpiration,
authJitter: authJitter,
aclJitter: aclJitter,
refreshExpiration: refreshExpiration,
client: goCache.New(time.Second*defaultExpiration, time.Second*(defaultExpiration*2)),
h: sha1.New(),
client: goCache.New(time.Second*defaultExpiration, time.Second*(defaultExpiration*2)),
h: sha1.New(),
options: options,
}
}
// NewSingleRedisStore initializes a cache using a single Redis instance as the store.
func NewSingleRedisStore(host, port, password string, db int, authExpiration, aclExpiration, authJitter, aclJitter time.Duration, refreshExpiration bool) *redisStore {
func NewSingleRedisStore(host, port, password string, db int, options Options) *redisStore {
addr := fmt.Sprintf("%s:%s", host, port)
redisClient := goredis.NewClient(&goredis.Options{
Addr: addr,
Password: password, // no password set
DB: db, // use default db
Password: password,
DB: db,
})
//If cache is on, try to start redis.
return &redisStore{
authExpiration: authExpiration,
aclExpiration: aclExpiration,
authJitter: authJitter,
aclJitter: aclJitter,
refreshExpiration: refreshExpiration,
client: bes.SingleRedisClient{redisClient},
h: sha1.New(),
client: bes.SingleRedisClient{redisClient},
h: sha1.New(),
options: options,
}
}
// NewSingleRedisStore initializes a cache using a Redis Cluster as the store.
func NewRedisClusterStore(password string, addresses []string, authExpiration, aclExpiration, authJitter, aclJitter time.Duration, refreshExpiration bool) *redisStore {
func NewRedisClusterStore(password string, addresses []string, options Options) *redisStore {
clusterClient := goredis.NewClusterClient(
&goredis.ClusterOptions{
Addrs: addresses,
@ -94,13 +90,9 @@ func NewRedisClusterStore(password string, addresses []string, authExpiration, a
})
return &redisStore{
authExpiration: authExpiration,
aclExpiration: aclExpiration,
authJitter: authJitter,
aclJitter: aclJitter,
refreshExpiration: refreshExpiration,
client: clusterClient,
h: sha1.New(),
client: clusterClient,
h: sha1.New(),
options: options,
}
}
@ -110,9 +102,15 @@ func toAuthRecord(username, password string, h hash.Hash) string {
return b64.StdEncoding.EncodeToString(sum)
}
func toSuperuserRecord(username, password string, h hash.Hash) string {
sum := h.Sum([]byte(fmt.Sprintf("superuser-%s-%s", username, password)))
log.Debugf("to superuser record: %v\n", sum)
return b64.StdEncoding.EncodeToString(sum)
}
func toACLRecord(username, topic, clientid string, acc int, h hash.Hash) string {
sum := h.Sum([]byte(fmt.Sprintf("acl-%s-%s-%s-%d", username, topic, clientid, acc)))
log.Debugf("to auth record: %v\n", sum)
log.Debugf("to acl record: %v\n", sum)
return b64.StdEncoding.EncodeToString(sum)
}
@ -179,13 +177,19 @@ func (s *redisStore) Close() {
// CheckAuthRecord checks if the username/password pair is present in the cache. Return if it's present and, if so, if it was granted privileges
func (s *goStore) CheckAuthRecord(ctx context.Context, username, password string) (bool, bool) {
record := toAuthRecord(username, password, s.h)
return s.checkRecord(ctx, record, expirationWithJitter(s.authExpiration, s.authJitter))
return s.checkRecord(ctx, record, expirationWithJitter(s.options.AuthExpiration, s.options.AuthJitter))
}
//CheckAclCache checks if the username/topic/clientid/acc mix is present in the cache. Return if it's present and, if so, if it was granted privileges.
// CheckAclRecord checks if the username/topic/clientid/acc mix is present in the cache. Return if it's present and, if so, if it was granted privileges.
func (s *goStore) CheckACLRecord(ctx context.Context, username, topic, clientid string, acc int) (bool, bool) {
record := toACLRecord(username, topic, clientid, acc, s.h)
return s.checkRecord(ctx, record, expirationWithJitter(s.aclExpiration, s.aclJitter))
return s.checkRecord(ctx, record, expirationWithJitter(s.options.AclExpiration, s.options.AclJitter))
}
// CheckSuperuserRecord checks if the username is in the superuser cache. Return if it's present and, if so, if it was granted privileges.
func (s *goStore) CheckSuperuserRecord(ctx context.Context, username, password string) (bool, bool) {
record := toSuperuserRecord(username, password, s.h)
return s.checkRecord(ctx, record, expirationWithJitter(s.options.SuperuserExpiration, s.options.SuperuserJitter))
}
func (s *goStore) checkRecord(ctx context.Context, record string, expirationTime time.Duration) (bool, bool) {
@ -198,7 +202,7 @@ func (s *goStore) checkRecord(ctx context.Context, record string, expirationTime
granted = true
}
if s.refreshExpiration {
if s.options.RefreshExpiration {
s.client.Set(record, value, expirationTime)
}
}
@ -208,13 +212,19 @@ func (s *goStore) checkRecord(ctx context.Context, record string, expirationTime
// CheckAuthRecord checks if the username/password pair is present in the cache. Return if it's present and, if so, if it was granted privileges
func (s *redisStore) CheckAuthRecord(ctx context.Context, username, password string) (bool, bool) {
record := toAuthRecord(username, password, s.h)
return s.checkRecord(ctx, record, s.authExpiration)
return s.checkRecord(ctx, record, expirationWithJitter(s.options.AuthExpiration, s.options.AuthJitter))
}
//CheckAclCache checks if the username/topic/clientid/acc mix is present in the cache. Return if it's present and, if so, if it was granted privileges.
// CheckAclRecord checks if the username/topic/clientid/acc mix is present in the cache. Return if it's present and, if so, if it was granted privileges.
func (s *redisStore) CheckACLRecord(ctx context.Context, username, topic, clientid string, acc int) (bool, bool) {
record := toACLRecord(username, topic, clientid, acc, s.h)
return s.checkRecord(ctx, record, s.aclExpiration)
return s.checkRecord(ctx, record, expirationWithJitter(s.options.AclExpiration, s.options.AclJitter))
}
// CheckSuperuserRecord checks if the username is in the superuser cache. Return if it's present and, if so, if it was granted privileges.
func (s *redisStore) CheckSuperuserRecord(ctx context.Context, username, password string) (bool, bool) {
record := toSuperuserRecord(username, password, s.h)
return s.checkRecord(ctx, record, expirationWithJitter(s.options.SuperuserExpiration, s.options.SuperuserJitter))
}
func (s *redisStore) checkRecord(ctx context.Context, record string, expirationTime time.Duration) (bool, bool) {
@ -244,7 +254,7 @@ func (s *redisStore) getAndRefresh(ctx context.Context, record string, expiratio
return false, false, err
}
if s.refreshExpiration {
if s.options.RefreshExpiration {
_, err = s.client.Expire(ctx, record, expirationTime).Result()
if err != nil {
return false, false, err
@ -261,15 +271,23 @@ func (s *redisStore) getAndRefresh(ctx context.Context, record string, expiratio
// SetAuthRecord sets a pair, granted option and expiration time.
func (s *goStore) SetAuthRecord(ctx context.Context, username, password string, granted string) error {
record := toAuthRecord(username, password, s.h)
s.client.Set(record, granted, expirationWithJitter(s.authExpiration, s.authJitter))
s.client.Set(record, granted, expirationWithJitter(s.options.AuthExpiration, s.options.AuthJitter))
return nil
}
//SetAclCache sets a mix, granted option and expiration time.
// SetAclRecord sets a mix, granted option and expiration time.
func (s *goStore) SetACLRecord(ctx context.Context, username, topic, clientid string, acc int, granted string) error {
record := toACLRecord(username, topic, clientid, acc, s.h)
s.client.Set(record, granted, expirationWithJitter(s.aclExpiration, s.aclJitter))
s.client.Set(record, granted, expirationWithJitter(s.options.AclExpiration, s.options.AclJitter))
return nil
}
// SetSuperuserRecord sets a pair, granted option and expiration time.
func (s *goStore) SetSuperuserRecord(ctx context.Context, username, password string, granted string) error {
record := toSuperuserRecord(username, password, s.h)
s.client.Set(record, granted, expirationWithJitter(s.options.AuthExpiration, s.options.AuthJitter))
return nil
}
@ -277,13 +295,19 @@ func (s *goStore) SetACLRecord(ctx context.Context, username, topic, clientid st
// SetAuthRecord sets a pair, granted option and expiration time.
func (s *redisStore) SetAuthRecord(ctx context.Context, username, password string, granted string) error {
record := toAuthRecord(username, password, s.h)
return s.setRecord(ctx, record, granted, expirationWithJitter(s.authExpiration, s.authJitter))
return s.setRecord(ctx, record, granted, expirationWithJitter(s.options.AuthExpiration, s.options.AuthJitter))
}
//SetAclCache sets a mix, granted option and expiration time.
// SetAclRecord sets a mix, granted option and expiration time.
func (s *redisStore) SetACLRecord(ctx context.Context, username, topic, clientid string, acc int, granted string) error {
record := toACLRecord(username, topic, clientid, acc, s.h)
return s.setRecord(ctx, record, granted, expirationWithJitter(s.aclExpiration, s.aclJitter))
return s.setRecord(ctx, record, granted, expirationWithJitter(s.options.AclExpiration, s.options.AclJitter))
}
// SetSuperuserRecord sets a pair, granted option and expiration time.
func (s *redisStore) SetSuperuserRecord(ctx context.Context, username, password string, granted string) error {
record := toSuperuserRecord(username, password, s.h)
return s.setRecord(ctx, record, granted, expirationWithJitter(s.options.AuthExpiration, s.options.AuthJitter))
}
func (s *redisStore) setRecord(ctx context.Context, record, granted string, expirationTime time.Duration) error {

72
cache/cache_test.go vendored
View File

@ -41,18 +41,30 @@ func TestExpirationWithoutJitter(t *testing.T) {
func TestGoStore(t *testing.T) {
authExpiration := 100 * time.Millisecond
aclExpiration := 100 * time.Millisecond
superuserExpiration := 100 * time.Millisecond
authJitter := 10 * time.Millisecond
aclJitter := 10 * time.Millisecond
superuserJitter := 10 * time.Millisecond
refreshExpiration := false
store := NewGoStore(authExpiration, aclExpiration, authJitter, aclJitter, refreshExpiration)
options := Options{
AuthExpiration: authExpiration,
AclExpiration: aclExpiration,
AuthJitter: authJitter,
AclJitter: aclJitter,
SuperuserExpiration: superuserExpiration,
SuperuserJitter: superuserJitter,
RefreshExpiration: refreshExpiration,
}
store := NewGoStore(options)
ctx := context.Background()
assert.Equal(t, authExpiration, store.authExpiration)
assert.Equal(t, aclExpiration, store.aclExpiration)
assert.Equal(t, authJitter, store.authJitter)
assert.Equal(t, aclJitter, store.aclJitter)
assert.Equal(t, authExpiration, store.options.AuthExpiration)
assert.Equal(t, aclExpiration, store.options.AclExpiration)
assert.Equal(t, authJitter, store.options.AuthJitter)
assert.Equal(t, aclJitter, store.options.AclJitter)
assert.True(t, store.Connect(ctx, false))
@ -128,7 +140,7 @@ func TestGoStore(t *testing.T) {
assert.False(t, granted)
// Check expiration is refreshed.
store = NewGoStore(authExpiration, aclExpiration, authExpiration, aclJitter, true)
store = NewGoStore(options)
// Test granted access.
err = store.SetAuthRecord(ctx, username, password, "true")
@ -159,18 +171,30 @@ func TestGoStore(t *testing.T) {
func TestRedisSingleStore(t *testing.T) {
authExpiration := 1000 * time.Millisecond
aclExpiration := 1000 * time.Millisecond
superuserExpiration := 1000 * time.Millisecond
authJitter := 100 * time.Millisecond
aclJitter := 100 * time.Millisecond
superuserJitter := 100 * time.Millisecond
refreshExpiration := false
store := NewSingleRedisStore("localhost", "6379", "", 3, authExpiration, aclExpiration, authJitter, aclJitter, refreshExpiration)
options := Options{
AuthExpiration: authExpiration,
AclExpiration: aclExpiration,
SuperuserExpiration: superuserExpiration,
AuthJitter: authJitter,
AclJitter: aclJitter,
SuperuserJitter: superuserJitter,
RefreshExpiration: refreshExpiration,
}
store := NewSingleRedisStore("localhost", "6379", "", 3, options)
ctx := context.Background()
assert.Equal(t, authExpiration, store.authExpiration)
assert.Equal(t, aclExpiration, store.aclExpiration)
assert.Equal(t, authJitter, store.authJitter)
assert.Equal(t, aclJitter, store.aclJitter)
assert.Equal(t, authExpiration, store.options.AuthExpiration)
assert.Equal(t, aclExpiration, store.options.AclExpiration)
assert.Equal(t, authJitter, store.options.AuthJitter)
assert.Equal(t, aclJitter, store.options.AclJitter)
assert.True(t, store.Connect(ctx, false))
@ -223,7 +247,7 @@ func TestRedisSingleStore(t *testing.T) {
assert.False(t, granted)
// Check expiration is refreshed.
store = NewSingleRedisStore("localhost", "6379", "", 3, authExpiration, aclExpiration, authJitter, aclJitter, true)
store = NewSingleRedisStore("localhost", "6379", "", 3, options)
// Test granted access.
err = store.SetAuthRecord(ctx, username, password, "true")
@ -254,19 +278,31 @@ func TestRedisSingleStore(t *testing.T) {
func TestRedisClusterStore(t *testing.T) {
authExpiration := 1000 * time.Millisecond
aclExpiration := 1000 * time.Millisecond
superuserExpiration := 1000 * time.Millisecond
authJitter := 100 * time.Millisecond
aclJitter := 100 * time.Millisecond
superuserJitter := 100 * time.Millisecond
refreshExpiration := false
options := Options{
AuthExpiration: authExpiration,
AclExpiration: aclExpiration,
SuperuserExpiration: superuserExpiration,
AuthJitter: authJitter,
AclJitter: aclJitter,
SuperuserJitter: superuserJitter,
RefreshExpiration: refreshExpiration,
}
addresses := []string{"localhost:7000", "localhost:7001", "localhost:7002"}
store := NewRedisClusterStore("", addresses, authExpiration, aclExpiration, authJitter, aclJitter, refreshExpiration)
store := NewRedisClusterStore("", addresses, options)
ctx := context.Background()
assert.Equal(t, authExpiration, store.authExpiration)
assert.Equal(t, aclExpiration, store.aclExpiration)
assert.Equal(t, authJitter, store.authJitter)
assert.Equal(t, aclJitter, store.aclJitter)
assert.Equal(t, authExpiration, store.options.AuthExpiration)
assert.Equal(t, aclExpiration, store.options.AclExpiration)
assert.Equal(t, authJitter, store.options.AuthJitter)
assert.Equal(t, aclJitter, store.options.AclJitter)
assert.True(t, store.Connect(ctx, false))
@ -318,7 +354,7 @@ func TestRedisClusterStore(t *testing.T) {
assert.True(t, present)
assert.False(t, granted)
store = NewRedisClusterStore("", addresses, authExpiration, aclExpiration, authJitter, aclJitter, true)
store = NewRedisClusterStore("", addresses, options)
// Test granted access.
err = store.SetAuthRecord(ctx, username, password, "true")

View File

@ -185,6 +185,14 @@ func setCache(authOpts map[string]string) {
refreshExpiration = true
}
options := cache.Options{
AclExpiration: time.Duration(aclCacheSeconds) * time.Second,
AuthExpiration: time.Duration(authCacheSeconds) * time.Second,
AuthJitter: time.Duration(authJitterSeconds) * time.Second,
AclJitter: time.Duration(aclJitterSeconds) * time.Second,
RefreshExpiration: refreshExpiration,
}
switch authOpts["cache_type"] {
case "redis":
host := "localhost"
@ -219,11 +227,7 @@ func setCache(authOpts map[string]string) {
authPlugin.cache = cache.NewRedisClusterStore(
password,
addresses,
time.Duration(authCacheSeconds)*time.Second,
time.Duration(aclCacheSeconds)*time.Second,
time.Duration(authJitterSeconds)*time.Second,
time.Duration(aclJitterSeconds)*time.Second,
refreshExpiration,
options,
)
} else {
@ -249,21 +253,13 @@ func setCache(authOpts map[string]string) {
port,
password,
db,
time.Duration(authCacheSeconds)*time.Second,
time.Duration(aclCacheSeconds)*time.Second,
time.Duration(authJitterSeconds)*time.Second,
time.Duration(aclJitterSeconds)*time.Second,
refreshExpiration,
options,
)
}
default:
authPlugin.cache = cache.NewGoStore(
time.Duration(authCacheSeconds)*time.Second,
time.Duration(aclCacheSeconds)*time.Second,
time.Duration(authJitterSeconds)*time.Second,
time.Duration(aclJitterSeconds)*time.Second,
refreshExpiration,
options,
)
}

View File

@ -89,11 +89,7 @@ func (h argon2IDHasher) Compare(password string, passwordHash string) bool {
keylen := uint32(len(extractedHash))
newHash := argon2.IDKey([]byte(password), salt, iterations, memory, parallelism, keylen)
if subtle.ConstantTimeCompare(newHash, extractedHash) == 1 {
return true
}
return false
return subtle.ConstantTimeCompare(newHash, extractedHash) == 1
}
func (h argon2IDHasher) hashWithSalt(password string, salt []byte, memory uint32, iterations int, parallelism uint8, keylen int) string {

View File

@ -23,8 +23,6 @@ func (h bcryptHasher) Hash(password string) (string, error) {
// Compare checks that a bcrypt generated password matches the password hash.
func (h bcryptHasher) Compare(password, passwordHash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(password))
if err != nil {
return false
}
return true
return err != nil
}

View File

@ -61,9 +61,9 @@ func preferredEncoding(saltEncoding string) string {
// Empty backend: use whatever plugin wise hashing options are present by returning whole opts.
// Backend present: check if there's a backend_hasher option:
// - Yes: return a new map with whatever hashing options are present for the given backend and hasher
// (defaults will be used for missing options).
// - No: use whatever plugin wise hashing options are present by returning whole opts.
// - Yes: return a new map with whatever hashing options are present for the given backend and hasher
// (defaults will be used for missing options).
// - No: use whatever plugin wise hashing options are present by returning whole opts.
func processHashOpts(authOpts map[string]string, backend string) map[string]string {
// Return authOpts if no backend given.
@ -145,6 +145,4 @@ func NewHasher(authOpts map[string]string, backend string) HashComparer {
saltEncoding := opts["hasher_salt_encoding"]
return NewPBKDF2Hasher(saltSize, iterations, algorithm, saltEncoding, keyLen)
return nil
}

View File

@ -28,7 +28,6 @@ func TestNewHasher(t *testing.T) {
assert.Equal(t, Base64, pHasher.saltEncoding)
// Check that options are set correctly.
authOpts = make(map[string]string)
authOpts = map[string]string{
"hasher": Pbkdf2Opt,
"hasher_algorithm": SHA256,
@ -60,7 +59,6 @@ func TestNewHasher(t *testing.T) {
assert.Equal(t, defaultArgon2IDParallelism, aHasher.parallelism)
assert.Equal(t, defaultArgon2IDSaltSize, aHasher.saltSize)
authOpts = make(map[string]string)
authOpts = map[string]string{
"hasher": Argon2IDOpt,
"hasher_iterations": "100",
@ -89,7 +87,6 @@ func TestNewHasher(t *testing.T) {
assert.Equal(t, bHasher.cost, defaultBcryptCost)
// Check that options are set correctly.
authOpts = make(map[string]string)
authOpts = map[string]string{
"hasher": BcryptOpt,
"hasher_cost": "15",