140 lines
3.4 KiB
Go
140 lines
3.4 KiB
Go
package ficha
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// GenerateKey returns a fresh 32-byte key from crypto/rand.
|
|
func GenerateKey() ([]byte, error) {
|
|
k := make([]byte, keySize)
|
|
if _, err := rand.Read(k); err != nil {
|
|
return nil, fmt.Errorf("ficha: generate key: %w", err)
|
|
}
|
|
return k, nil
|
|
}
|
|
|
|
// KeyRing holds the active signing key plus older keys still valid
|
|
// for decryption during rotation. Safe for concurrent use.
|
|
type KeyRing struct {
|
|
mu sync.RWMutex
|
|
active string
|
|
keys map[string][]byte
|
|
}
|
|
|
|
// NewKeyRing creates a KeyRing with one key, marked active.
|
|
func NewKeyRing(activeID string, activeKey []byte) (*KeyRing, error) {
|
|
if err := validateKeyID(activeID); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(activeKey) != keySize {
|
|
return nil, fmt.Errorf("ficha: key must be %d bytes, got %d", keySize, len(activeKey))
|
|
}
|
|
|
|
kr := &KeyRing{
|
|
active: activeID,
|
|
keys: make(map[string][]byte),
|
|
}
|
|
kr.keys[activeID] = append([]byte(nil), activeKey...)
|
|
return kr, nil
|
|
}
|
|
|
|
// Add registers a new key without changing the active one.
|
|
// Use this to introduce a new key before promoting it.
|
|
func (kr *KeyRing) Add(id string, key []byte) error {
|
|
if err := validateKeyID(id); err != nil {
|
|
return err
|
|
}
|
|
if len(key) != keySize {
|
|
return fmt.Errorf("ficha: key must be %d bytes, got %d", keySize, len(key))
|
|
}
|
|
|
|
kr.mu.Lock()
|
|
defer kr.mu.Unlock()
|
|
|
|
if _, exists := kr.keys[id]; exists {
|
|
return fmt.Errorf("ficha: key id %q already exists", id)
|
|
}
|
|
kr.keys[id] = append([]byte(nil), key...)
|
|
return nil
|
|
}
|
|
|
|
// SetActive promotes an already-registered key to active.
|
|
// New tokens will be issued under this key; existing tokens
|
|
// signed with previous keys remain valid until their expiry.
|
|
func (kr *KeyRing) SetActive(id string) error {
|
|
kr.mu.Lock()
|
|
defer kr.mu.Unlock()
|
|
|
|
if _, ok := kr.keys[id]; !ok {
|
|
return ErrUnknownKey
|
|
}
|
|
kr.active = id
|
|
return nil
|
|
}
|
|
|
|
// Remove deletes a key from the ring. Tokens signed with it
|
|
// will no longer validate. Cannot remove the active key.
|
|
func (kr *KeyRing) Remove(id string) error {
|
|
kr.mu.Lock()
|
|
defer kr.mu.Unlock()
|
|
|
|
if id == kr.active {
|
|
return errors.New("ficha: cannot remove active key")
|
|
}
|
|
if _, ok := kr.keys[id]; !ok {
|
|
return ErrUnknownKey
|
|
}
|
|
delete(kr.keys, id)
|
|
return nil
|
|
}
|
|
|
|
// Active returns the current active key ID and key bytes.
|
|
// The returned key slice is a copy and safe to retain.
|
|
func (kr *KeyRing) Active() (id string, key []byte) {
|
|
kr.mu.RLock()
|
|
defer kr.mu.RUnlock()
|
|
|
|
k := kr.keys[kr.active]
|
|
return kr.active, append([]byte(nil), k...)
|
|
}
|
|
|
|
// Get returns the key bytes for the given ID, or false if unknown.
|
|
// The returned key slice is a copy.
|
|
func (kr *KeyRing) Get(id string) ([]byte, bool) {
|
|
kr.mu.RLock()
|
|
defer kr.mu.RUnlock()
|
|
|
|
k, ok := kr.keys[id]
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
return append([]byte(nil), k...), true
|
|
}
|
|
|
|
// IDs returns all known key IDs in unspecified order.
|
|
func (kr *KeyRing) IDs() []string {
|
|
kr.mu.RLock()
|
|
defer kr.mu.RUnlock()
|
|
|
|
ids := make([]string, 0, len(kr.keys))
|
|
for id := range kr.keys {
|
|
ids = append(ids, id)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// validateKeyID enforces constraints needed by the wire format:
|
|
// non-empty, no dots (used as separator), no whitespace.
|
|
func validateKeyID(id string) error {
|
|
if id == "" {
|
|
return errors.New("ficha: key id cannot be empty")
|
|
}
|
|
if strings.ContainsAny(id, ". \t\r\n") {
|
|
return errors.New("ficha: key id cannot contain dots or whitespace")
|
|
}
|
|
return nil
|
|
}
|