You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user