@ -16,15 +16,21 @@ package caddyauth
import (
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
weakrand "math/rand"
"net/http"
"sync"
"time"
"github.com/caddyserver/caddy/v2"
)
func init ( ) {
caddy . RegisterModule ( HTTPBasicAuth { } )
weakrand . Seed ( time . Now ( ) . UnixNano ( ) )
}
// HTTPBasicAuth facilitates HTTP basic authentication.
@ -38,6 +44,17 @@ type HTTPBasicAuth struct {
// The name of the realm. Default: restricted
Realm string ` json:"realm,omitempty" `
// If non-nil, a mapping of plaintext passwords to their
// hashes will be cached in memory (with random eviction).
// This can greatly improve the performance of traffic-heavy
// servers that use secure password hashing algorithms, with
// the downside that plaintext passwords will be stored in
// memory for a longer time (this should not be a problem
// as long as your machine is not compromised, at which point
// all bets are off, since basicauth necessitates plaintext
// passwords being received over the wire anyway).
HashCache * Cache ` json:"hash_cache,omitempty" `
Accounts map [ string ] Account ` json:"-" `
Hash Comparer ` json:"-" `
}
@ -99,6 +116,11 @@ func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error {
}
hba . AccountList = nil // allow GC to deallocate
if hba . HashCache != nil {
hba . HashCache . cache = make ( map [ string ] bool )
hba . HashCache . mu = new ( sync . Mutex )
}
return nil
}
@ -109,13 +131,11 @@ func (hba HTTPBasicAuth) Authenticate(w http.ResponseWriter, req *http.Request)
return hba . promptForCredentials ( w , nil )
}
plaintextPassword := [ ] byte ( plaintextPasswordStr )
account , accountExists := hba . Accounts [ username ]
// don't return early if account does not exist; we want
// to try to avoid side-channels that leak existence
same , err := hba . Hash . Compare ( account . password , plaintextPassword , account . salt )
same , err := hba . correctPassword ( account , [ ] byte ( plaintextPasswordStr ) )
if err != nil {
return hba . promptForCredentials ( w , err )
}
@ -126,6 +146,43 @@ func (hba HTTPBasicAuth) Authenticate(w http.ResponseWriter, req *http.Request)
return User { ID : username } , true , nil
}
func ( hba HTTPBasicAuth ) correctPassword ( account Account , plaintextPassword [ ] byte ) ( bool , error ) {
compare := func ( ) ( bool , error ) {
return hba . Hash . Compare ( account . password , plaintextPassword , account . salt )
}
// if no caching is enabled, simply return the result of hashing + comparing
if hba . HashCache == nil {
return compare ( )
}
// compute a cache key that is unique for these input parameters
cacheKey := hex . EncodeToString ( append ( append ( account . password , account . salt ... ) , plaintextPassword ... ) )
// fast track: if the result of the input is already cached, use it
hba . HashCache . mu . Lock ( )
same , ok := hba . HashCache . cache [ cacheKey ]
if ok {
hba . HashCache . mu . Unlock ( )
return same , nil
}
hba . HashCache . mu . Unlock ( )
// slow track: do the expensive op, then add it to the cache
same , err := compare ( )
if err != nil {
return false , err
}
hba . HashCache . mu . Lock ( )
if len ( hba . HashCache . cache ) >= 1000 {
hba . HashCache . makeRoom ( ) // keep cache size under control
}
hba . HashCache . cache [ cacheKey ] = same
hba . HashCache . mu . Unlock ( )
return same , nil
}
func ( hba HTTPBasicAuth ) promptForCredentials ( w http . ResponseWriter , err error ) ( User , bool , error ) {
// browsers show a message that says something like:
// "The website says: <realm>"
@ -138,6 +195,47 @@ func (hba HTTPBasicAuth) promptForCredentials(w http.ResponseWriter, err error)
return User { } , false , err
}
// Cache enables caching of basic auth results. This is especially
// helpful for secure password hashes which can be expensive to
// compute on every HTTP request.
type Cache struct {
mu * sync . Mutex
// map of concatenated hashed password + plaintext password + salt, to result
cache map [ string ] bool
}
// makeRoom deletes about 1/10 of the items in the cache
// in order to keep its size under control. It must not be
// called without a lock on c.mu.
func ( c * Cache ) makeRoom ( ) {
// we delete more than just 1 entry so that we don't have
// to do this on every request; assuming the capacity of
// the cache is on a long tail, we can save a lot of CPU
// time by doing a whole bunch of deletions now and then
// we won't have to do them again for a while
numToDelete := len ( c . cache ) / 10
if numToDelete < 1 {
numToDelete = 1
}
for deleted := 0 ; deleted <= numToDelete ; deleted ++ {
// Go maps are "nondeterministic" not actually random,
// so although we could just chop off the "front" of the
// map with less code, this is a heavily skewed eviction
// strategy; generating random numbers is cheap and
// ensures a much better distribution.
rnd := weakrand . Intn ( len ( c . cache ) )
i := 0
for key := range c . cache {
if i == rnd {
delete ( c . cache , key )
break
}
i ++
}
}
}
// Comparer is a type that can securely compare
// a plaintext password with a hashed password
// in constant-time. Comparers should hash the