Files
chorus/internal/platform/apikey/credential.go
T

81 lines
2.3 KiB
Go

package apikey
import (
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"strings"
)
const (
tokenPrefix = "chorus_"
publicIDLength = 24
secretLength = 43
)
var ErrInvalidToken = errors.New("api key token is invalid")
type Credential struct {
Token string `json:"-"`
PublicID string
KeyPrefix string
SecretHash []byte `json:"-"`
}
type Parsed struct {
PublicID string
SecretHash []byte `json:"-"`
}
func Generate() (Credential, error) {
publicBytes := make([]byte, 18)
secretBytes := make([]byte, 32)
if _, err := rand.Read(publicBytes); err != nil {
return Credential{}, fmt.Errorf("generate api key public id: %w", err)
}
if _, err := rand.Read(secretBytes); err != nil {
return Credential{}, fmt.Errorf("generate api key secret: %w", err)
}
publicID := base64.RawURLEncoding.EncodeToString(publicBytes)
secret := base64.RawURLEncoding.EncodeToString(secretBytes)
prefix := tokenPrefix + publicID + "_"
hash := sha256.Sum256([]byte(secret))
return Credential{
Token: prefix + secret, PublicID: publicID, KeyPrefix: prefix,
SecretHash: append([]byte(nil), hash[:]...),
}, nil
}
func Parse(token string) (Parsed, error) {
if len(token) != len(tokenPrefix)+publicIDLength+1+secretLength || !strings.HasPrefix(token, tokenPrefix) {
return Parsed{}, ErrInvalidToken
}
publicStart := len(tokenPrefix)
separator := publicStart + publicIDLength
if token[separator] != '_' {
return Parsed{}, ErrInvalidToken
}
publicID := token[publicStart:separator]
secret := token[separator+1:]
publicBytes, publicErr := base64.RawURLEncoding.DecodeString(publicID)
secretBytes, secretErr := base64.RawURLEncoding.DecodeString(secret)
if publicErr != nil || secretErr != nil || len(publicBytes) != 18 || len(secretBytes) != 32 {
return Parsed{}, ErrInvalidToken
}
hash := sha256.Sum256([]byte(secret))
return Parsed{PublicID: publicID, SecretHash: append([]byte(nil), hash[:]...)}, nil
}
func Verify(token, expectedPublicID string, expectedHash []byte) bool {
parsed, err := Parse(token)
if err != nil || len(expectedPublicID) != publicIDLength || len(expectedHash) != sha256.Size {
return false
}
publicMatches := subtle.ConstantTimeCompare([]byte(parsed.PublicID), []byte(expectedPublicID))
hashMatches := subtle.ConstantTimeCompare(parsed.SecretHash, expectedHash)
return publicMatches&hashMatches == 1
}