mirror of
https://github.com/therootcompany/golib.git
synced 2026-03-02 23:57:59 +00:00
fix(auth/csvauth): make username lookups timing safe
This commit is contained in:
parent
1789c92815
commit
737f3b0057
@ -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
|
||||
}
|
||||
|
||||
@ -33,9 +33,10 @@ func TestCredentialCreationAndVerification(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("%s/%s", tc.purpose, tc.name), func(t *testing.T) {
|
||||
var key [16]byte
|
||||
a := &Auth{
|
||||
aes128key: key,
|
||||
credentials: make(map[Name]Credential),
|
||||
serviceAccounts: make(map[Purpose]Credential),
|
||||
aes128key: key,
|
||||
credentials: make(map[Name]Credential),
|
||||
hashedCredentials: make(map[string]Credential),
|
||||
serviceAccounts: make(map[Purpose]Credential),
|
||||
}
|
||||
secret := tc.extra
|
||||
c := a.NewCredential(tc.purpose, tc.name, secret, tc.params, tc.roles, tc.extra)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user