81 lines
2.3 KiB
Go
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
|
|
}
|