From 7d35551fa7c1dc3e66b3458e67dda83ad6a38a35 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 21 Feb 2026 15:58:16 -0700 Subject: [PATCH] ref(auth/csvauth): separate Login, Token, and ServiceAccount files --- auth/csvauth/csvauth.go | 118 -------------------------------- auth/csvauth/login.go | 45 ++++++++++++ auth/csvauth/service_account.go | 36 ++++++++++ auth/csvauth/token.go | 52 ++++++++++++++ 4 files changed, 133 insertions(+), 118 deletions(-) create mode 100644 auth/csvauth/login.go create mode 100644 auth/csvauth/service_account.go create mode 100644 auth/csvauth/token.go diff --git a/auth/csvauth/csvauth.go b/auth/csvauth/csvauth.go index 89e60a6..de0add1 100644 --- a/auth/csvauth/csvauth.go +++ b/auth/csvauth/csvauth.go @@ -4,19 +4,15 @@ import ( "bytes" "crypto/aes" "crypto/cipher" - "crypto/hmac" "crypto/pbkdf2" "crypto/rand" "crypto/sha1" "crypto/sha256" - "encoding/base64" "encoding/csv" "errors" "fmt" "hash" "io" - "iter" - "maps" "os" "slices" "strconv" @@ -302,93 +298,6 @@ func gcmEncrypt(aes128key [16]byte, gcmNonce [12]byte, secret string) ([]byte, e return ciphertext, nil } -// CredentialKeys returns the names that serve as IDs for each of the login credentials -func (a *Auth) CredentialKeys() iter.Seq[Name] { - a.mux.Lock() - defer a.mux.Unlock() - return maps.Keys(a.credentials) -} - -func (a *Auth) LoadToken(secret string) (Credential, error) { - hashID := a.tokenCacheID(secret) - - a.mux.Lock() - c, ok := a.tokens[hashID] - a.mux.Unlock() - - if !ok { - return Credential{}, ErrNotFound - } - - if c.plain == "" { - var err error - if c.plain, err = a.maybeDecryptCredential(c); err != nil { - return Credential{}, err - } - } - - if err := c.Verify("", secret); err != nil { - return Credential{}, err - } - - return c, nil -} - -func (a *Auth) LoadCredential(name Name) (Credential, error) { - a.mux.Lock() - c, ok := a.credentials[name] - a.mux.Unlock() - if !ok { - return c, ErrNotFound - } - - var err error - if c.plain, err = a.maybeDecryptCredential(c); err != nil { - return c, err - } - - return c, nil -} - -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 - - if c.Purpose == PurposeToken { - a.tokens[c.hashID] = c - } - return nil -} - -// CredentialKeys returns the names that serve as IDs for each of the login credentials -func (a *Auth) ServiceAccountKeys() iter.Seq[Purpose] { - a.mux.Lock() - defer a.mux.Unlock() - return maps.Keys(a.serviceAccounts) -} - -func (a *Auth) LoadServiceAccount(purpose Purpose) (Credential, error) { - a.mux.Lock() - c, ok := a.serviceAccounts[purpose] - a.mux.Unlock() - if !ok { - return c, ErrNotFound - } - - var err error - if c.plain, err = a.maybeDecryptCredential(c); err != nil { - return c, err - } - - return c, nil -} - func (a *Auth) maybeDecryptCredential(c Credential) (secretValue, error) { switch c.Params[0] { case "aes-128-gcm": @@ -422,13 +331,6 @@ func (a *Auth) gcmDecrypt(aes128key [16]byte, gcmNonce [12]byte, derived []byte) return string(plaintext), nil } -func (a *Auth) CacheServiceAccount(c Credential) error { - a.mux.Lock() - defer a.mux.Unlock() - a.serviceAccounts[c.Purpose] = c - return nil -} - // Verify checks Basic Auth credentials, i.e. as decoded from Authorization Basic . // It also supports tokens. In short: // - if : and 'user' is found, then "login" credentials @@ -459,26 +361,6 @@ func (a *Auth) Verify(name, secret string) error { return ErrNotFound } -// VerifyToken uses a short, but timing-safe hash to find the token, -// and then verifies it with HMAC -func (a *Auth) VerifyToken(secret string) error { - _, err := a.LoadToken(secret) - return err -} - -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 -} - // Verify checks Basic Auth credentials // (name is ignored, as it is assumed to have been used for lookup) func (c Credential) Verify(_, secret string) error { diff --git a/auth/csvauth/login.go b/auth/csvauth/login.go new file mode 100644 index 0000000..9e6c0b7 --- /dev/null +++ b/auth/csvauth/login.go @@ -0,0 +1,45 @@ +package csvauth + +import ( + "iter" + "maps" +) + +// CredentialKeys returns the names that serve as IDs for each of the login credentials +func (a *Auth) CredentialKeys() iter.Seq[Name] { + a.mux.Lock() + defer a.mux.Unlock() + return maps.Keys(a.credentials) +} + +func (a *Auth) LoadCredential(name Name) (Credential, error) { + a.mux.Lock() + c, ok := a.credentials[name] + a.mux.Unlock() + if !ok { + return c, ErrNotFound + } + + var err error + if c.plain, err = a.maybeDecryptCredential(c); err != nil { + return c, err + } + + return c, nil +} + +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 + + if c.Purpose == PurposeToken { + a.tokens[c.hashID] = c + } + return nil +} diff --git a/auth/csvauth/service_account.go b/auth/csvauth/service_account.go new file mode 100644 index 0000000..c071582 --- /dev/null +++ b/auth/csvauth/service_account.go @@ -0,0 +1,36 @@ +package csvauth + +import ( + "iter" + "maps" +) + +// CredentialKeys returns the names that serve as IDs for each of the login credentials +func (a *Auth) ServiceAccountKeys() iter.Seq[Purpose] { + a.mux.Lock() + defer a.mux.Unlock() + return maps.Keys(a.serviceAccounts) +} + +func (a *Auth) LoadServiceAccount(purpose Purpose) (Credential, error) { + a.mux.Lock() + c, ok := a.serviceAccounts[purpose] + a.mux.Unlock() + if !ok { + return c, ErrNotFound + } + + var err error + if c.plain, err = a.maybeDecryptCredential(c); err != nil { + return c, err + } + + return c, nil +} + +func (a *Auth) CacheServiceAccount(c Credential) error { + a.mux.Lock() + defer a.mux.Unlock() + a.serviceAccounts[c.Purpose] = c + return nil +} diff --git a/auth/csvauth/token.go b/auth/csvauth/token.go new file mode 100644 index 0000000..421addc --- /dev/null +++ b/auth/csvauth/token.go @@ -0,0 +1,52 @@ +package csvauth + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" +) + +func (a *Auth) LoadToken(secret string) (Credential, error) { + hashID := a.tokenCacheID(secret) + + a.mux.Lock() + c, ok := a.tokens[hashID] + a.mux.Unlock() + + if !ok { + return Credential{}, ErrNotFound + } + + if c.plain == "" { + var err error + if c.plain, err = a.maybeDecryptCredential(c); err != nil { + return Credential{}, err + } + } + + if err := c.Verify("", secret); err != nil { + return Credential{}, err + } + + return c, nil +} + +// VerifyToken uses a short, but timing-safe hash to find the token, +// and then verifies it with HMAC +func (a *Auth) VerifyToken(secret string) error { + _, err := a.LoadToken(secret) + return err +} + +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 +}