fix(auth/csvauth): make username lookups timing safe

This commit is contained in:
AJ ONeal 2026-02-26 02:15:22 -07:00
parent 1789c92815
commit 737f3b0057
No known key found for this signature in database
4 changed files with 49 additions and 25 deletions

View File

@ -4,10 +4,12 @@ import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/pbkdf2"
"crypto/rand"
"crypto/sha1"
"crypto/sha256"
"encoding/base64"
"encoding/csv"
"encoding/hex"
"errors"
@ -65,6 +67,7 @@ func NewNamedReadCloser(r io.ReadCloser, name string) NamedReadCloser {
type Auth struct {
aes128key [16]byte
credentials map[Name]Credential
hashedCredentials map[string]Credential
tokens map[string]Credential
serviceAccounts map[Purpose]Credential
mux sync.Mutex
@ -79,6 +82,7 @@ func New(aes128key []byte) *Auth {
return &Auth{
aes128key: aes128Arr,
credentials: map[Name]Credential{},
hashedCredentials: map[string]Credential{},
tokens: map[string]Credential{},
serviceAccounts: map[Purpose]Credential{},
BasicAuthTokenNames: []string{"", "api", "apikey"},
@ -140,9 +144,16 @@ func (a *Auth) LoadCSV(f NamedReadCloser, comma rune) error {
}
if _, ok := a.credentials[name]; ok {
fmt.Fprintf(os.Stderr, "overwriting cache of previous value for %s: %s\n", credential.Purpose, credential.Name)
fmt.Fprintf(os.Stderr, "overwriting plain cache of previous value for %s: %s\n", credential.Purpose, credential.Name)
}
a.credentials[name] = credential
nameID := a.nameCacheID(name)
if _, ok := a.hashedCredentials[nameID]; ok {
fmt.Fprintf(os.Stderr, "overwriting hashed cache of previous value for %s: %s\n", credential.Purpose, credential.Name)
}
a.hashedCredentials[nameID] = credential
if credential.Purpose == PurposeToken {
if _, ok := a.tokens[credential.hashID]; ok {
fmt.Fprintf(os.Stderr, "overwriting cache of previous value for %s: %s\n", credential.Purpose, credential.Name)
@ -364,7 +375,8 @@ func (a *Auth) Authenticate(name, secret string) (*Credential, error) {
a.mux.Lock()
defer a.mux.Unlock()
c, ok := a.credentials[name]
nameID := a.nameCacheID(name)
c, ok := a.hashedCredentials[nameID]
if ok {
if err := c.Verify(name, secret); err != nil {
return nil, err
@ -438,3 +450,16 @@ func (c Credential) Verify(_, secret string) error {
}
return ErrUnauthorized
}
func (a *Auth) cacheID(s string, n int) string {
key := a.aes128key[:]
mac := hmac.New(sha256.New, key)
message := []byte(s)
mac.Write(message)
// attack collisions are possible, but will still fail to pass HMAC
// practical collisions are not possible for the CSV use case
nameBytes := mac.Sum(nil)[:n]
name := base64.RawURLEncoding.EncodeToString(nameBytes)
return name
}

View File

@ -35,6 +35,7 @@ func TestCredentialCreationAndVerification(t *testing.T) {
a := &Auth{
aes128key: key,
credentials: make(map[Name]Credential),
hashedCredentials: make(map[string]Credential),
serviceAccounts: make(map[Purpose]Credential),
}
secret := tc.extra

View File

@ -5,6 +5,8 @@ import (
"maps"
)
const nameHashLen = 16
// CredentialKeys returns the names that serve as IDs for each of the login credentials
func (a *Auth) CredentialKeys() iter.Seq[Name] {
a.mux.Lock()
@ -13,8 +15,10 @@ func (a *Auth) CredentialKeys() iter.Seq[Name] {
}
func (a *Auth) LoadCredential(name Name) (Credential, error) {
nameID := a.nameCacheID(name)
a.mux.Lock()
c, ok := a.credentials[name]
c, ok := a.hashedCredentials[nameID]
a.mux.Unlock()
if !ok {
return c, ErrNotFound
@ -29,17 +33,24 @@ func (a *Auth) LoadCredential(name Name) (Credential, error) {
}
func (a *Auth) CacheCredential(c Credential) error {
a.mux.Lock()
defer a.mux.Unlock()
name := c.Name
if c.Purpose == PurposeToken {
name += hashIDSep + c.hashID
}
a.credentials[name] = c
nameID := a.nameCacheID(name)
a.mux.Lock()
defer a.mux.Unlock()
a.credentials[name] = c
if c.Purpose == PurposeToken {
a.tokens[c.hashID] = c
} else {
a.hashedCredentials[nameID] = c
}
return nil
}
func (a *Auth) nameCacheID(name string) string {
return a.cacheID(name, nameHashLen)
}

View File

@ -1,10 +1,6 @@
package csvauth
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
)
const tokenHashLen = 6
// Provided for consistency. Often better to use Authenticate("", token)
func (a *Auth) LoadToken(secret string) (Credential, error) {
@ -49,14 +45,5 @@ func (a *Auth) loadAndVerifyToken(secret string) (*Credential, error) {
}
func (a *Auth) tokenCacheID(secret string) string {
key := a.aes128key[:]
mac := hmac.New(sha256.New, key)
message := []byte(secret)
mac.Write(message)
// attack collisions are possible, but will still fail to pass HMAC
// practical collisions are not possible for the CSV use case
nameBytes := mac.Sum(nil)[:6]
name := base64.RawURLEncoding.EncodeToString(nameBytes)
return name
return a.cacheID(secret, tokenHashLen)
}