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"
|
"bytes"
|
||||||
"crypto/aes"
|
"crypto/aes"
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
|
"crypto/hmac"
|
||||||
"crypto/pbkdf2"
|
"crypto/pbkdf2"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
@ -65,6 +67,7 @@ func NewNamedReadCloser(r io.ReadCloser, name string) NamedReadCloser {
|
|||||||
type Auth struct {
|
type Auth struct {
|
||||||
aes128key [16]byte
|
aes128key [16]byte
|
||||||
credentials map[Name]Credential
|
credentials map[Name]Credential
|
||||||
|
hashedCredentials map[string]Credential
|
||||||
tokens map[string]Credential
|
tokens map[string]Credential
|
||||||
serviceAccounts map[Purpose]Credential
|
serviceAccounts map[Purpose]Credential
|
||||||
mux sync.Mutex
|
mux sync.Mutex
|
||||||
@ -79,6 +82,7 @@ func New(aes128key []byte) *Auth {
|
|||||||
return &Auth{
|
return &Auth{
|
||||||
aes128key: aes128Arr,
|
aes128key: aes128Arr,
|
||||||
credentials: map[Name]Credential{},
|
credentials: map[Name]Credential{},
|
||||||
|
hashedCredentials: map[string]Credential{},
|
||||||
tokens: map[string]Credential{},
|
tokens: map[string]Credential{},
|
||||||
serviceAccounts: map[Purpose]Credential{},
|
serviceAccounts: map[Purpose]Credential{},
|
||||||
BasicAuthTokenNames: []string{"", "api", "apikey"},
|
BasicAuthTokenNames: []string{"", "api", "apikey"},
|
||||||
@ -140,9 +144,16 @@ func (a *Auth) LoadCSV(f NamedReadCloser, comma rune) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := a.credentials[name]; ok {
|
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
|
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 credential.Purpose == PurposeToken {
|
||||||
if _, ok := a.tokens[credential.hashID]; ok {
|
if _, ok := a.tokens[credential.hashID]; ok {
|
||||||
fmt.Fprintf(os.Stderr, "overwriting cache of previous value for %s: %s\n", credential.Purpose, credential.Name)
|
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()
|
a.mux.Lock()
|
||||||
defer a.mux.Unlock()
|
defer a.mux.Unlock()
|
||||||
c, ok := a.credentials[name]
|
nameID := a.nameCacheID(name)
|
||||||
|
c, ok := a.hashedCredentials[nameID]
|
||||||
if ok {
|
if ok {
|
||||||
if err := c.Verify(name, secret); err != nil {
|
if err := c.Verify(name, secret); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -438,3 +450,16 @@ func (c Credential) Verify(_, secret string) error {
|
|||||||
}
|
}
|
||||||
return ErrUnauthorized
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -35,6 +35,7 @@ func TestCredentialCreationAndVerification(t *testing.T) {
|
|||||||
a := &Auth{
|
a := &Auth{
|
||||||
aes128key: key,
|
aes128key: key,
|
||||||
credentials: make(map[Name]Credential),
|
credentials: make(map[Name]Credential),
|
||||||
|
hashedCredentials: make(map[string]Credential),
|
||||||
serviceAccounts: make(map[Purpose]Credential),
|
serviceAccounts: make(map[Purpose]Credential),
|
||||||
}
|
}
|
||||||
secret := tc.extra
|
secret := tc.extra
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import (
|
|||||||
"maps"
|
"maps"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const nameHashLen = 16
|
||||||
|
|
||||||
// CredentialKeys returns the names that serve as IDs for each of the login credentials
|
// CredentialKeys returns the names that serve as IDs for each of the login credentials
|
||||||
func (a *Auth) CredentialKeys() iter.Seq[Name] {
|
func (a *Auth) CredentialKeys() iter.Seq[Name] {
|
||||||
a.mux.Lock()
|
a.mux.Lock()
|
||||||
@ -13,8 +15,10 @@ func (a *Auth) CredentialKeys() iter.Seq[Name] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *Auth) LoadCredential(name Name) (Credential, error) {
|
func (a *Auth) LoadCredential(name Name) (Credential, error) {
|
||||||
|
nameID := a.nameCacheID(name)
|
||||||
|
|
||||||
a.mux.Lock()
|
a.mux.Lock()
|
||||||
c, ok := a.credentials[name]
|
c, ok := a.hashedCredentials[nameID]
|
||||||
a.mux.Unlock()
|
a.mux.Unlock()
|
||||||
if !ok {
|
if !ok {
|
||||||
return c, ErrNotFound
|
return c, ErrNotFound
|
||||||
@ -29,17 +33,24 @@ func (a *Auth) LoadCredential(name Name) (Credential, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *Auth) CacheCredential(c Credential) error {
|
func (a *Auth) CacheCredential(c Credential) error {
|
||||||
a.mux.Lock()
|
|
||||||
defer a.mux.Unlock()
|
|
||||||
|
|
||||||
name := c.Name
|
name := c.Name
|
||||||
if c.Purpose == PurposeToken {
|
if c.Purpose == PurposeToken {
|
||||||
name += hashIDSep + c.hashID
|
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 {
|
if c.Purpose == PurposeToken {
|
||||||
a.tokens[c.hashID] = c
|
a.tokens[c.hashID] = c
|
||||||
|
} else {
|
||||||
|
a.hashedCredentials[nameID] = c
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Auth) nameCacheID(name string) string {
|
||||||
|
return a.cacheID(name, nameHashLen)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,10 +1,6 @@
|
|||||||
package csvauth
|
package csvauth
|
||||||
|
|
||||||
import (
|
const tokenHashLen = 6
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/base64"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Provided for consistency. Often better to use Authenticate("", token)
|
// Provided for consistency. Often better to use Authenticate("", token)
|
||||||
func (a *Auth) LoadToken(secret string) (Credential, error) {
|
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 {
|
func (a *Auth) tokenCacheID(secret string) string {
|
||||||
key := a.aes128key[:]
|
return a.cacheID(secret, tokenHashLen)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user