diff --git a/auth/credential_store.go b/auth/credential_store.go index 8491ff51..e173c8e2 100644 --- a/auth/credential_store.go +++ b/auth/credential_store.go @@ -5,6 +5,7 @@ package auth import ( "encoding/json" "io" + "sync" "golang.org/x/crypto/bcrypt" ) @@ -41,6 +42,43 @@ type BasicAuther interface { BasicAuth() (string, string, bool) } +// HashCache store hash values for users. Safe for use from multiple goroutines. +type HashCache struct { + mu sync.RWMutex + m map[string]map[string]bool +} + +// NewHashCache returns a instantiated HashCache +func NewHashCache() *HashCache { + return &HashCache{ + m: make(map[string]map[string]bool), + } +} + +// Check returns whether hash is valid for username. +func (h *HashCache) Check(username, hash string) bool { + h.mu.RLock() + defer h.mu.RUnlock() + m, ok := h.m[username] + if !ok { + return false + } + + _, ok = m[hash] + return ok +} + +// Store stores the given hash as a valid hash for username. +func (h *HashCache) Store(username, hash string) { + h.mu.Lock() + defer h.mu.Unlock() + _, ok := h.m[username] + if !ok { + h.m[username] = make(map[string]bool) + } + h.m[username][hash] = true +} + // Credential represents authentication and authorization configuration for a single user. type Credential struct { Username string `json:"username,omitempty"` @@ -52,13 +90,18 @@ type Credential struct { type CredentialsStore struct { store map[string]string perms map[string]map[string]bool + + UseCache bool + hashCache *HashCache } // NewCredentialsStore returns a new instance of a CredentialStore. func NewCredentialsStore() *CredentialsStore { return &CredentialsStore{ - store: make(map[string]string), - perms: make(map[string]map[string]bool), + store: make(map[string]string), + perms: make(map[string]map[string]bool), + hashCache: NewHashCache(), + UseCache: true, } } @@ -99,8 +142,27 @@ func (c *CredentialsStore) Check(username, password string) bool { if !ok { return false } - return password == pw || - bcrypt.CompareHashAndPassword([]byte(pw), []byte(password)) == nil + + // Simple match with plaintext password in creds? + if password == pw { + return true + } + + // Maybe the given a password is a hash -- check if the hash is good + // for the given user. + if c.UseCache && c.hashCache.Check(username, password) { + return true + } + + // Next, what's in the file may be hashed, so hash the given password + // and compare. + if bcrypt.CompareHashAndPassword([]byte(pw), []byte(password)) != nil { + return false + } + + // It's good -- cache that result for this user. + c.hashCache.Store(username, password) + return true } // Password returns the password for the given user. diff --git a/auth/credential_store_test.go b/auth/credential_store_test.go index d2c9c684..466572b0 100644 --- a/auth/credential_store_test.go +++ b/auth/credential_store_test.go @@ -15,6 +15,53 @@ func (t *testBasicAuther) BasicAuth() (string, string, bool) { return t.username, t.password, t.ok } +func Test_HashCache(t *testing.T) { + hc := NewHashCache() + + if hc.Check("user", "hash1") { + t.Fatalf("hash cache check OK for empty cache") + } + if hc.Check("user", "") { + t.Fatalf("hash cache check OK for empty cache") + } + if hc.Check("", "") { + t.Fatalf("hash cache check OK for empty cache") + } + + hc.Store("user1", "hash1") + if !hc.Check("user1", "hash1") { + t.Fatalf("hash cache check not OK for user1") + } + if hc.Check("user", "hash1") { + t.Fatalf("hash cache check OK for bad user") + } + + hc.Store("user1", "hash2") + if !hc.Check("user1", "hash1") { + t.Fatalf("hash cache check not OK for user1") + } + if !hc.Check("user1", "hash2") { + t.Fatalf("hash cache check not OK for user1") + } + + hc.Store("user3", "hash3") + if !hc.Check("user1", "hash1") { + t.Fatalf("hash cache check not OK for user1") + } + if !hc.Check("user1", "hash2") { + t.Fatalf("hash cache check not OK for user1") + } + if hc.Check("user", "hash1") { + t.Fatalf("hash cache check OK for bad user") + } + if !hc.Check("user3", "hash3") { + t.Fatalf("hash cache check not OK for user3") + } + if hc.Check("user3", "hash1") { + t.Fatalf("hash cache check OK for user3, with bad hash") + } +} + func Test_AuthLoadSingle(t *testing.T) { const jsonStream = ` [