From c52f30898bed1b4ff14e3c0c90f34732eac91d74 Mon Sep 17 00:00:00 2001 From: Steve Cliff Date: Mon, 22 Jun 2026 22:47:05 +0100 Subject: [PATCH] feat(crypto): named-var key loaders (admin/agent) + NewDEK Replace KeyFromEnv with AgentKeyFromEnv/AdminKeyFromEnv reading EMCLI_KEY and EMCLI_ADMIN_KEY; add NewDEK for envelope encryption. Seal/Open double as DEK wrap/unwrap. Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/crypto/crypto.go | 33 +++++++++++++++++++--------- internal/crypto/crypto_test.go | 39 +++++++++++++++++++++++++--------- 2 files changed, 52 insertions(+), 20 deletions(-) diff --git a/internal/crypto/crypto.go b/internal/crypto/crypto.go index 1d00c10..04ce2ec 100644 --- a/internal/crypto/crypto.go +++ b/internal/crypto/crypto.go @@ -7,28 +7,41 @@ import ( "crypto/rand" "encoding/base64" "errors" + "fmt" "io" "os" ) -var ( - ErrNoKey = errors.New("EMCLI_KEY is not set") - ErrBadKey = errors.New("EMCLI_KEY must be base64 of exactly 32 bytes") -) - -// KeyFromEnv reads and validates the AES-256 key from EMCLI_KEY. -func KeyFromEnv() ([]byte, error) { - raw := os.Getenv("EMCLI_KEY") +// keyFromEnv reads and validates a base64-encoded 32-byte AES key from the +// named environment variable. Errors name the variable so callers get a +// role-appropriate message. +func keyFromEnv(varName string) ([]byte, error) { + raw := os.Getenv(varName) if raw == "" { - return nil, ErrNoKey + return nil, fmt.Errorf("%s is not set", varName) } key, err := base64.StdEncoding.DecodeString(raw) if err != nil || len(key) != 32 { - return nil, ErrBadKey + return nil, fmt.Errorf("%s must be base64 of exactly 32 bytes", varName) } return key, nil } +// AgentKeyFromEnv reads the agent KEK from EMCLI_KEY (agent commands only). +func AgentKeyFromEnv() ([]byte, error) { return keyFromEnv("EMCLI_KEY") } + +// AdminKeyFromEnv reads the admin KEK from EMCLI_ADMIN_KEY (all commands). +func AdminKeyFromEnv() ([]byte, error) { return keyFromEnv("EMCLI_ADMIN_KEY") } + +// NewDEK returns a fresh random 32-byte data-encryption key. +func NewDEK() ([]byte, error) { + dek := make([]byte, 32) + if _, err := io.ReadFull(rand.Reader, dek); err != nil { + return nil, err + } + return dek, nil +} + func newGCM(key []byte) (cipher.AEAD, error) { block, err := aes.NewCipher(key) if err != nil { diff --git a/internal/crypto/crypto_test.go b/internal/crypto/crypto_test.go index 62e86f7..69c1d0f 100644 --- a/internal/crypto/crypto_test.go +++ b/internal/crypto/crypto_test.go @@ -3,6 +3,7 @@ package crypto import ( "bytes" "encoding/base64" + "strings" "testing" ) @@ -50,20 +51,38 @@ func TestOpenWrongKeyFails(t *testing.T) { } } -func TestKeyFromEnv(t *testing.T) { - t.Setenv("EMCLI_KEY", base64.StdEncoding.EncodeToString(testKey())) - k, err := KeyFromEnv() - if err != nil || len(k) != 32 { - t.Fatalf("KeyFromEnv: key=%d err=%v", len(k), err) +func TestAgentAndAdminKeyFromEnv(t *testing.T) { + good := base64.StdEncoding.EncodeToString(testKey()) + + t.Setenv("EMCLI_KEY", good) + if k, err := AgentKeyFromEnv(); err != nil || len(k) != 32 { + t.Fatalf("AgentKeyFromEnv: key=%d err=%v", len(k), err) + } + t.Setenv("EMCLI_ADMIN_KEY", good) + if k, err := AdminKeyFromEnv(); err != nil || len(k) != 32 { + t.Fatalf("AdminKeyFromEnv: key=%d err=%v", len(k), err) } - t.Setenv("EMCLI_KEY", "") - if _, err := KeyFromEnv(); err != ErrNoKey { - t.Fatalf("empty key: want ErrNoKey, got %v", err) + t.Setenv("EMCLI_ADMIN_KEY", "") + if _, err := AdminKeyFromEnv(); err == nil || + !strings.Contains(err.Error(), "EMCLI_ADMIN_KEY") { + t.Fatalf("empty admin key: want EMCLI_ADMIN_KEY error, got %v", err) } t.Setenv("EMCLI_KEY", base64.StdEncoding.EncodeToString([]byte("tooshort"))) - if _, err := KeyFromEnv(); err != ErrBadKey { - t.Fatalf("short key: want ErrBadKey, got %v", err) + if _, err := AgentKeyFromEnv(); err == nil || + !strings.Contains(err.Error(), "32 bytes") { + t.Fatalf("short key: want length error, got %v", err) + } +} + +func TestNewDEKIsRandom32(t *testing.T) { + a, err := NewDEK() + if err != nil || len(a) != 32 { + t.Fatalf("NewDEK: len=%d err=%v", len(a), err) + } + b, _ := NewDEK() + if bytes.Equal(a, b) { + t.Fatal("two DEKs must differ") } }