You've already forked agentic-coding-workflow
104 lines
3.6 KiB
Go
104 lines
3.6 KiB
Go
package crypto
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
)
|
|
|
|
// Provider resolves the raw 32-byte AES-256 key material for a given key
|
|
// version. The key material itself never lives in the DB — only the version
|
|
// number (in crypto_keys + the per-row key_version column) is persisted; the
|
|
// provider maps that version back to actual bytes (env var, Vault, KMS, ...).
|
|
type Provider interface {
|
|
// KeyMaterial returns the 32-byte key for the given version, or an error if
|
|
// the version is unknown / unavailable. Implementations may cache.
|
|
KeyMaterial(ctx context.Context, version int) ([]byte, error)
|
|
}
|
|
|
|
// EnvProvider maps key versions to env vars. Version 1 is APP_MASTER_KEY
|
|
// (the historical single key); higher versions read APP_MASTER_KEY_V<n> so a
|
|
// rotation can stage the next key alongside the current one. The constructor
|
|
// also accepts an explicit version-1 key so callers that already decoded
|
|
// cfg.MasterKey do not re-read the env.
|
|
type EnvProvider struct {
|
|
mu sync.RWMutex
|
|
keys map[int][]byte
|
|
}
|
|
|
|
// NewEnvProvider builds an EnvProvider seeded with the version-1 key (typically
|
|
// the already-decoded cfg.MasterKey). A nil/empty v1 key falls back to reading
|
|
// APP_MASTER_KEY lazily on first KeyMaterial(1).
|
|
func NewEnvProvider(v1 []byte) *EnvProvider {
|
|
p := &EnvProvider{keys: map[int][]byte{}}
|
|
if len(v1) > 0 {
|
|
p.keys[1] = append([]byte(nil), v1...)
|
|
}
|
|
return p
|
|
}
|
|
|
|
// KeyMaterial returns the cached key for version, loading it from the matching
|
|
// env var on first use. Version 1 → APP_MASTER_KEY; version n>1 →
|
|
// APP_MASTER_KEY_V<n>. The value must be 64 hex chars (32 bytes).
|
|
func (p *EnvProvider) KeyMaterial(_ context.Context, version int) ([]byte, error) {
|
|
if version < 1 {
|
|
return nil, fmt.Errorf("crypto: invalid key version %d", version)
|
|
}
|
|
p.mu.RLock()
|
|
k, ok := p.keys[version]
|
|
p.mu.RUnlock()
|
|
if ok {
|
|
return k, nil
|
|
}
|
|
|
|
envName := "APP_MASTER_KEY"
|
|
if version > 1 {
|
|
envName = fmt.Sprintf("APP_MASTER_KEY_V%d", version)
|
|
}
|
|
raw := os.Getenv(envName)
|
|
if raw == "" {
|
|
return nil, fmt.Errorf("crypto: key version %d unavailable (%s unset)", version, envName)
|
|
}
|
|
key, err := hex.DecodeString(raw)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("crypto: %s invalid hex: %w", envName, err)
|
|
}
|
|
if len(key) != 32 {
|
|
return nil, fmt.Errorf("crypto: %s must be 32 bytes (got %d)", envName, len(key))
|
|
}
|
|
p.mu.Lock()
|
|
p.keys[version] = key
|
|
p.mu.Unlock()
|
|
return key, nil
|
|
}
|
|
|
|
// SetKey installs key material for a version explicitly (used in tests and when
|
|
// a KMS-fetched key is handed to an EnvProvider-style cache).
|
|
func (p *EnvProvider) SetKey(version int, key []byte) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
p.keys[version] = append([]byte(nil), key...)
|
|
}
|
|
|
|
// VaultProvider / KMSProvider are deferred: their interface is identical to
|
|
// EnvProvider (KeyMaterial(ctx, version) -> []byte), so wiring a real
|
|
// HashiCorp Vault transit / cloud KMS client is a drop-in replacement. They are
|
|
// declared here so config.Provider can select "vault"/"kms" without a build
|
|
// break; until a real client is wired they delegate to a backing Provider.
|
|
|
|
// DelegatingProvider wraps another Provider; it is the shape both VaultProvider
|
|
// and KMSProvider take until their remote clients are implemented.
|
|
type DelegatingProvider struct{ inner Provider }
|
|
|
|
// NewDelegatingProvider returns a Provider that forwards to inner.
|
|
func NewDelegatingProvider(inner Provider) *DelegatingProvider {
|
|
return &DelegatingProvider{inner: inner}
|
|
}
|
|
|
|
// KeyMaterial forwards to the wrapped provider.
|
|
func (d *DelegatingProvider) KeyMaterial(ctx context.Context, version int) ([]byte, error) {
|
|
return d.inner.KeyMaterial(ctx, version)
|
|
}
|