mirror of
https://github.com/restic/rest-server.git
synced 2025-10-19 07:33:21 +00:00
Cache successful basic auth credentials for a minute
This stores a hash of the username + password in map which is indexed by the username. Indexing by username avoids accidentally introducing a timing side-channel as a successful/failed lookup only provides information on whether a cache entry exists for a username or not. Hashing the username and password together makes it simple to get a constant-time string comparison as we no longer have to worry about string length differences. Expriy is done by a goroutine which every few seconds checks for expired cache entries and removes those.
This commit is contained in:
parent
b0036d006b
commit
0bdc420e75
1 changed files with 62 additions and 4 deletions
66
htpasswd.go
66
htpasswd.go
|
@ -26,6 +26,8 @@ THE SOFTWARE.
|
|||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"encoding/csv"
|
||||
"log"
|
||||
|
@ -42,8 +44,18 @@ import (
|
|||
// CheckInterval represents how often we check for changes in htpasswd file.
|
||||
const CheckInterval = 30 * time.Second
|
||||
|
||||
// PasswordCacheDuration represents how long authentication credentials are
|
||||
// cached in memory after they were successfully verified. This allows avoiding
|
||||
// repeatedly verifying the same authentication credentials.
|
||||
const PasswordCacheDuration = time.Minute
|
||||
|
||||
// Lookup passwords in a htpasswd file. The entries must have been created with -s for SHA encryption.
|
||||
|
||||
type cacheEntry struct {
|
||||
expiry time.Time
|
||||
verifier []byte
|
||||
}
|
||||
|
||||
// HtpasswdFile is a map for usernames to passwords.
|
||||
type HtpasswdFile struct {
|
||||
mutex sync.Mutex
|
||||
|
@ -51,6 +63,7 @@ type HtpasswdFile struct {
|
|||
stat os.FileInfo
|
||||
throttle chan struct{}
|
||||
users map[string]string
|
||||
cache map[string]cacheEntry
|
||||
}
|
||||
|
||||
// NewHtpasswdFromFile reads the users and passwords from a htpasswd file and returns them. If an error is encountered,
|
||||
|
@ -68,6 +81,7 @@ func NewHtpasswdFromFile(path string) (*HtpasswdFile, error) {
|
|||
path: path,
|
||||
stat: stat,
|
||||
throttle: make(chan struct{}),
|
||||
cache: make(map[string]cacheEntry),
|
||||
}
|
||||
|
||||
if err := h.Reload(); err != nil {
|
||||
|
@ -76,6 +90,7 @@ func NewHtpasswdFromFile(path string) (*HtpasswdFile, error) {
|
|||
|
||||
// Start a goroutine that limits reload checks to once per CheckInterval
|
||||
go h.throttleTimer()
|
||||
go h.expiryTimer()
|
||||
|
||||
go func() {
|
||||
for range c {
|
||||
|
@ -100,6 +115,20 @@ func (h *HtpasswdFile) throttleTimer() {
|
|||
}
|
||||
}
|
||||
|
||||
func (h *HtpasswdFile) expiryTimer() {
|
||||
for {
|
||||
time.Sleep(5 * time.Second)
|
||||
now := time.Now()
|
||||
h.mutex.Lock()
|
||||
for user, entry := range h.cache {
|
||||
if entry.expiry.After(now) {
|
||||
delete(h.cache, user)
|
||||
}
|
||||
}
|
||||
h.mutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
var validUsernameRegexp = regexp.MustCompile(`^[\p{L}\d@._-]+$`)
|
||||
|
||||
// Reload reloads the htpasswd file. If the reload fails, the Users map is not changed and the error is returned.
|
||||
|
@ -130,6 +159,7 @@ func (h *HtpasswdFile) Reload() error {
|
|||
|
||||
// Replace the Users map
|
||||
h.mutex.Lock()
|
||||
h.cache = make(map[string]cacheEntry)
|
||||
h.users = users
|
||||
h.mutex.Unlock()
|
||||
|
||||
|
@ -177,30 +207,58 @@ func (h *HtpasswdFile) ReloadCheck() error {
|
|||
func (h *HtpasswdFile) Validate(user string, password string) bool {
|
||||
_ = h.ReloadCheck()
|
||||
|
||||
hash := sha256.New()
|
||||
// hash.Write can never fail
|
||||
_, _ = hash.Write([]byte(user))
|
||||
_, _ = hash.Write([]byte(":"))
|
||||
_, _ = hash.Write([]byte(password))
|
||||
|
||||
h.mutex.Lock()
|
||||
// avoid race conditions with cache replacements
|
||||
cache := h.cache
|
||||
realPassword, exists := h.users[user]
|
||||
entry, cacheExists := h.cache[user]
|
||||
h.mutex.Unlock()
|
||||
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
if cacheExists && subtle.ConstantTimeCompare(entry.verifier, hash.Sum(nil)) == 1 {
|
||||
return true
|
||||
}
|
||||
|
||||
var shaRe = regexp.MustCompile(`^{SHA}`)
|
||||
var bcrRe = regexp.MustCompile(`^\$2b\$|^\$2a\$|^\$2y\$`)
|
||||
|
||||
isValid := false
|
||||
|
||||
switch {
|
||||
case shaRe.MatchString(realPassword):
|
||||
d := sha1.New()
|
||||
_, _ = d.Write([]byte(password))
|
||||
if realPassword[5:] == base64.StdEncoding.EncodeToString(d.Sum(nil)) {
|
||||
return true
|
||||
isValid = true
|
||||
}
|
||||
case bcrRe.MatchString(realPassword):
|
||||
err := bcrypt.CompareHashAndPassword([]byte(realPassword), []byte(password))
|
||||
if err == nil {
|
||||
return true
|
||||
isValid = true
|
||||
}
|
||||
}
|
||||
log.Printf("Invalid htpasswd entry for %s.", user)
|
||||
return false
|
||||
|
||||
if !isValid {
|
||||
log.Printf("Invalid htpasswd entry for %s.", user)
|
||||
return false
|
||||
}
|
||||
|
||||
h.mutex.Lock()
|
||||
// repurpose mutex to prevent concurrent cache updates
|
||||
cache[user] = cacheEntry{
|
||||
verifier: hash.Sum(nil),
|
||||
expiry: time.Now().Add(PasswordCacheDuration),
|
||||
}
|
||||
h.mutex.Unlock()
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue