repositories / bugabinga.net
bugabinga.net
personal infrastructure for bugabinga!
owned by admin
services/toad/internal/credential/credential.go
Raw// Package credential owns deployment tokens as runtime state.
// Values are shown once at issue time and stored only as hashes, so infrastructure
// configuration, OpenTofu state, and Git never carry a usable credential.
package credential
import (
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"bugabinga.net/toad/internal/store"
)
var (
ErrUnknownToken = errors.New("unknown token")
name = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,63}$`)
identifier = regexp.MustCompile(`^[0-9a-f]{16}$`)
)
// Token is the stored form. The secret itself is never persisted.
type Token struct {
ID string `json:"id"`
Label string `json:"label,omitempty"`
Hash string `json:"hash"`
CreatedAt time.Time `json:"createdAt"`
}
// Public is the reportable form, safe for logs and listings.
type Public struct {
ID string `json:"id"`
Label string `json:"label,omitempty"`
CreatedAt time.Time `json:"createdAt"`
}
type Store struct {
Root string
Now func() time.Time
mu sync.Mutex
}
func New(dataDir string) *Store {
return &Store{Root: filepath.Join(dataDir, "credentials"), Now: func() time.Time { return time.Now().UTC() }}
}
// Issue returns the only copy of the token value that will ever exist.
func (s *Store) Issue(service, label string) (string, Public, error) {
if err := validate(service, label); err != nil {
return "", Public{}, err
}
id, secret, err := generate()
if err != nil {
return "", Public{}, err
}
digest := sha256.Sum256([]byte(secret))
token := Token{ID: id, Label: label, Hash: hex.EncodeToString(digest[:]), CreatedAt: s.now()}
s.mu.Lock()
defer s.mu.Unlock()
tokens, err := s.read(service)
if err != nil {
return "", Public{}, err
}
if err := s.write(service, append(tokens, token)); err != nil {
return "", Public{}, err
}
return id + "." + secret, public(token), nil
}
func (s *Store) List(service string) ([]Public, error) {
if !name.MatchString(service) {
return nil, fmt.Errorf("invalid service %q", service)
}
s.mu.Lock()
defer s.mu.Unlock()
tokens, err := s.read(service)
if err != nil {
return nil, err
}
listed := make([]Public, 0, len(tokens))
for _, token := range tokens {
listed = append(listed, public(token))
}
return listed, nil
}
func (s *Store) Revoke(service, id string) error {
if !name.MatchString(service) {
return fmt.Errorf("invalid service %q", service)
}
if !identifier.MatchString(id) {
return ErrUnknownToken
}
s.mu.Lock()
defer s.mu.Unlock()
tokens, err := s.read(service)
if err != nil {
return err
}
kept := make([]Token, 0, len(tokens))
for _, token := range tokens {
if token.ID != id {
kept = append(kept, token)
}
}
if len(kept) == len(tokens) {
return ErrUnknownToken
}
return s.write(service, kept)
}
// Verify reports whether presented authorizes exactly this service.
func (s *Store) Verify(service, presented string) bool {
if !name.MatchString(service) {
return false
}
id, secret, found := strings.Cut(presented, ".")
if !found || !identifier.MatchString(id) || secret == "" {
return false
}
s.mu.Lock()
tokens, err := s.read(service)
s.mu.Unlock()
if err != nil {
return false
}
digest := sha256.Sum256([]byte(secret))
expected := hex.EncodeToString(digest[:])
authorized := false
for _, token := range tokens {
// Compare every candidate so a match does not shorten the loop.
if token.ID == id && subtle.ConstantTimeCompare([]byte(token.Hash), []byte(expected)) == 1 {
authorized = true
}
}
return authorized
}
func (s *Store) path(service string) string { return filepath.Join(s.Root, service+".json") }
func (s *Store) read(service string) ([]Token, error) {
data, err := os.ReadFile(s.path(service))
if os.IsNotExist(err) {
return nil, nil
}
if err != nil {
return nil, err
}
var document struct {
Tokens []Token `json:"tokens"`
}
if err := json.Unmarshal(data, &document); err != nil {
return nil, fmt.Errorf("credentials for %s: %w", service, err)
}
return document.Tokens, nil
}
func (s *Store) write(service string, tokens []Token) error {
if err := os.MkdirAll(s.Root, 0o750); err != nil {
return err
}
data, err := json.MarshalIndent(struct {
Tokens []Token `json:"tokens"`
}{Tokens: tokens}, "", " ")
if err != nil {
return err
}
return store.WriteFileAtomic(s.path(service), append(data, '\n'))
}
func (s *Store) now() time.Time {
if s.Now == nil {
return time.Now().UTC()
}
return s.Now().UTC()
}
func validate(service, label string) error {
if !name.MatchString(service) {
return fmt.Errorf("invalid service %q", service)
}
if len(label) > 64 || strings.ContainsAny(label, "\n\r\t") {
return fmt.Errorf("invalid label %q", label)
}
return nil
}
func generate() (string, string, error) {
var raw [8 + 32]byte
if _, err := rand.Read(raw[:]); err != nil {
return "", "", err
}
return hex.EncodeToString(raw[:8]), base64.RawURLEncoding.EncodeToString(raw[8:]), nil
}
func public(token Token) Public {
return Public{ID: token.ID, Label: token.Label, CreatedAt: token.CreatedAt}
}