diff --git a/auth/csvauth/csvauth.go b/auth/csvauth/csvauth.go index 9e302c2..d42584e 100644 --- a/auth/csvauth/csvauth.go +++ b/auth/csvauth/csvauth.go @@ -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 +} diff --git a/auth/csvauth/csvauth_test.go b/auth/csvauth/csvauth_test.go index dfc22e7..360032c 100644 --- a/auth/csvauth/csvauth_test.go +++ b/auth/csvauth/csvauth_test.go @@ -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) diff --git a/auth/csvauth/login.go b/auth/csvauth/login.go index 9e6c0b7..4c971ee 100644 --- a/auth/csvauth/login.go +++ b/auth/csvauth/login.go @@ -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) +} diff --git a/auth/csvauth/token.go b/auth/csvauth/token.go index 4eb0802..5bde479 100644 --- a/auth/csvauth/token.go +++ b/auth/csvauth/token.go @@ -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) }