Files
zeekay baebcfa594 fix: add GOWORK/CGO env to all CI workflows and fix go fmt
- Add GOWORK=off and CGO_ENABLED=0 to lint.yml, unit-test.yml, ci.yml, build.yml
- Remove -race flag from ci.yml (incompatible with CGO_ENABLED=0)
- Fix go fmt formatting in 22 Go files
2026-01-28 10:21:23 -08:00

757 lines
19 KiB
Go

// Copyright (C) 2022-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package kms
import (
"context"
"crypto/aes"
"crypto/cipher"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"sync"
"time"
)
// KeyType represents the type of cryptographic key.
type KeyType string
const (
KeyTypeAES256 KeyType = "aes-256-gcm"
KeyTypeRSA3072 KeyType = "rsa-3072"
KeyTypeRSA4096 KeyType = "rsa-4096"
KeyTypeECDSAP256 KeyType = "ecdsa-p256"
KeyTypeECDSAP384 KeyType = "ecdsa-p384"
KeyTypeEdDSA KeyType = "ed25519"
)
// KeyUsage represents what a key can be used for.
type KeyUsage string
const (
KeyUsageEncryptDecrypt KeyUsage = "encrypt-decrypt"
KeyUsageSignVerify KeyUsage = "sign-verify"
KeyUsageMPC KeyUsage = "mpc"
)
// KeyStatus represents the current state of a key.
type KeyStatus string
const (
KeyStatusActive KeyStatus = "active"
KeyStatusInactive KeyStatus = "inactive"
KeyStatusDeleted KeyStatus = "deleted"
KeyStatusPending KeyStatus = "pending" // For MPC key generation
)
// Key represents a cryptographic key in the KMS.
type Key struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Type KeyType `json:"type"`
Usage KeyUsage `json:"usage"`
Status KeyStatus `json:"status"`
Version int `json:"version"`
OrgID string `json:"orgId,omitempty"`
ProjectID string `json:"projectId,omitempty"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
// For MPC keys
Threshold int `json:"threshold,omitempty"`
TotalShares int `json:"totalShares,omitempty"`
ShareHolders []string `json:"shareHolders,omitempty"`
}
// KeyMaterial holds the encrypted key material.
type KeyMaterial struct {
KeyID string `json:"keyId"`
Version int `json:"version"`
EncryptedKey []byte `json:"encryptedKey"` // Encrypted with root key
EncryptedPrivate []byte `json:"encryptedPrivate"` // For asymmetric keys
PublicKey []byte `json:"publicKey"` // Public key (if asymmetric)
Nonce []byte `json:"nonce"`
Created time.Time `json:"created"`
}
// Secret represents a stored secret.
type Secret struct {
ID string `json:"id"`
Name string `json:"name"`
Version int `json:"version"`
KeyID string `json:"keyId"` // KMS key used for encryption
Environment string `json:"environment"` // dev, staging, prod
Path string `json:"path"` // Folder path
Value []byte `json:"value"` // Encrypted value
Nonce []byte `json:"nonce"`
Tags []string `json:"tags,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
OrgID string `json:"orgId,omitempty"`
ProjectID string `json:"projectId,omitempty"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
}
// KMS provides key management and encryption services.
type KMS struct {
store StorageBackend
rootKey []byte // 32-byte root encryption key
rootCipher cipher.AEAD
mu sync.RWMutex
}
// Config holds KMS configuration.
type Config struct {
Store StorageBackend
RootKey []byte // Must be 32 bytes for AES-256
DataDir string // Only used if Store is nil (creates BadgerStore)
InMemory bool
Compression bool
}
// New creates a new KMS instance.
func New(cfg *Config) (*KMS, error) {
if len(cfg.RootKey) != 32 {
return nil, fmt.Errorf("root key must be 32 bytes")
}
store := cfg.Store
if store == nil {
badgerCfg := &BadgerConfig{
Dir: cfg.DataDir,
InMemory: cfg.InMemory,
SyncWrites: true,
Compression: cfg.Compression,
}
var err error
store, err = NewBadgerStore(badgerCfg)
if err != nil {
return nil, fmt.Errorf("failed to create store: %w", err)
}
}
// Create root cipher
block, err := aes.NewCipher(cfg.RootKey)
if err != nil {
return nil, fmt.Errorf("failed to create root cipher: %w", err)
}
rootCipher, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("failed to create GCM: %w", err)
}
return &KMS{
store: store,
rootKey: cfg.RootKey,
rootCipher: rootCipher,
}, nil
}
// Key prefix constants
const (
keyPrefix = "kms/key/"
keyMaterialPrefix = "kms/material/"
secretPrefix = "kms/secret/"
)
// GenerateKey generates a new cryptographic key.
func (k *KMS) GenerateKey(ctx context.Context, name string, keyType KeyType, usage KeyUsage, opts *KeyOptions) (*Key, error) {
k.mu.Lock()
defer k.mu.Unlock()
keyID := generateID(16)
now := time.Now()
key := &Key{
ID: keyID,
Name: name,
Type: keyType,
Usage: usage,
Status: KeyStatusActive,
Version: 1,
Created: now,
Updated: now,
}
if opts != nil {
key.Description = opts.Description
key.OrgID = opts.OrgID
key.ProjectID = opts.ProjectID
key.Metadata = opts.Metadata
if opts.ExpiresIn > 0 {
exp := now.Add(opts.ExpiresIn)
key.ExpiresAt = &exp
}
}
// Generate key material
material, err := k.generateKeyMaterial(keyID, keyType)
if err != nil {
return nil, fmt.Errorf("failed to generate key material: %w", err)
}
// Save key
if err := SetJSON(ctx, k.store, keyPrefix+keyID, key); err != nil {
return nil, fmt.Errorf("failed to save key: %w", err)
}
// Save encrypted key material
if err := SetJSON(ctx, k.store, keyMaterialPrefix+keyID+"/1", material); err != nil {
return nil, fmt.Errorf("failed to save key material: %w", err)
}
return key, nil
}
// KeyOptions holds optional parameters for key generation.
type KeyOptions struct {
Description string
OrgID string
ProjectID string
Metadata map[string]string
ExpiresIn time.Duration
}
// generateKeyMaterial creates and encrypts key material.
func (k *KMS) generateKeyMaterial(keyID string, keyType KeyType) (*KeyMaterial, error) {
material := &KeyMaterial{
KeyID: keyID,
Version: 1,
Created: time.Now(),
}
var keyBytes []byte
var privateBytes []byte
var publicBytes []byte
switch keyType {
case KeyTypeAES256:
keyBytes = make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, keyBytes); err != nil {
return nil, err
}
case KeyTypeRSA3072:
key, err := rsa.GenerateKey(rand.Reader, 3072)
if err != nil {
return nil, err
}
privateBytes = x509.MarshalPKCS1PrivateKey(key)
publicBytes, err = x509.MarshalPKIXPublicKey(&key.PublicKey)
if err != nil {
return nil, err
}
case KeyTypeRSA4096:
key, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return nil, err
}
privateBytes = x509.MarshalPKCS1PrivateKey(key)
publicBytes, err = x509.MarshalPKIXPublicKey(&key.PublicKey)
if err != nil {
return nil, err
}
case KeyTypeECDSAP256:
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
privateBytes, err = x509.MarshalECPrivateKey(key)
if err != nil {
return nil, err
}
publicBytes, err = x509.MarshalPKIXPublicKey(&key.PublicKey)
if err != nil {
return nil, err
}
case KeyTypeECDSAP384:
key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err != nil {
return nil, err
}
privateBytes, err = x509.MarshalECPrivateKey(key)
if err != nil {
return nil, err
}
publicBytes, err = x509.MarshalPKIXPublicKey(&key.PublicKey)
if err != nil {
return nil, err
}
case KeyTypeEdDSA:
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, err
}
privateBytes = priv
publicBytes = pub
default:
return nil, fmt.Errorf("unsupported key type: %s", keyType)
}
// Generate nonce for encryption
nonce := make([]byte, k.rootCipher.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
material.Nonce = nonce
// Encrypt key material with root key
if len(keyBytes) > 0 {
material.EncryptedKey = k.rootCipher.Seal(nil, nonce, keyBytes, nil)
}
if len(privateBytes) > 0 {
material.EncryptedPrivate = k.rootCipher.Seal(nil, nonce, privateBytes, nil)
}
material.PublicKey = publicBytes
return material, nil
}
// GetKey retrieves a key by ID.
func (k *KMS) GetKey(ctx context.Context, keyID string) (*Key, error) {
return GetJSON[Key](ctx, k.store, keyPrefix+keyID)
}
// GetKeyByName retrieves a key by name.
func (k *KMS) GetKeyByName(ctx context.Context, name string) (*Key, error) {
keys, err := k.ListKeys(ctx, "")
if err != nil {
return nil, err
}
for _, key := range keys {
if key.Name == name {
return key, nil
}
}
return nil, ErrKeyNotFound
}
// ListKeys lists all keys, optionally filtered by prefix.
func (k *KMS) ListKeys(ctx context.Context, prefix string) ([]*Key, error) {
var keys []*Key
err := k.store.Scan(ctx, keyPrefix+prefix, func(key string, value []byte) error {
var kmsKey Key
if err := json.Unmarshal(value, &kmsKey); err != nil {
return nil // Skip invalid entries
}
if kmsKey.Status != KeyStatusDeleted {
keys = append(keys, &kmsKey)
}
return nil
})
return keys, err
}
// DeleteKey soft-deletes a key.
func (k *KMS) DeleteKey(ctx context.Context, keyID string) error {
k.mu.Lock()
defer k.mu.Unlock()
key, err := k.GetKey(ctx, keyID)
if err != nil {
return err
}
key.Status = KeyStatusDeleted
key.Updated = time.Now()
return SetJSON(ctx, k.store, keyPrefix+keyID, key)
}
// Encrypt encrypts data using the specified key.
func (k *KMS) Encrypt(ctx context.Context, keyID string, plaintext []byte) ([]byte, error) {
key, err := k.GetKey(ctx, keyID)
if err != nil {
return nil, err
}
if key.Usage != KeyUsageEncryptDecrypt {
return nil, fmt.Errorf("key %s cannot be used for encryption", keyID)
}
if key.Status != KeyStatusActive {
return nil, fmt.Errorf("key %s is not active", keyID)
}
// Get key material
material, err := k.getKeyMaterial(ctx, keyID, key.Version)
if err != nil {
return nil, err
}
// Decrypt the key material
decryptedKey, err := k.rootCipher.Open(nil, material.Nonce, material.EncryptedKey, nil)
if err != nil {
return nil, fmt.Errorf("failed to decrypt key material: %w", err)
}
// Create cipher from decrypted key
block, err := aes.NewCipher(decryptedKey)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
// Generate nonce for this encryption
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
// Encrypt
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
// Prepend key version for decryption
result := &EncryptedData{
KeyID: keyID,
KeyVersion: key.Version,
Data: ciphertext,
}
return json.Marshal(result)
}
// EncryptedData represents encrypted data with metadata.
type EncryptedData struct {
KeyID string `json:"keyId"`
KeyVersion int `json:"keyVersion"`
Data []byte `json:"data"`
}
// Decrypt decrypts data.
func (k *KMS) Decrypt(ctx context.Context, ciphertext []byte) ([]byte, error) {
var encrypted EncryptedData
if err := json.Unmarshal(ciphertext, &encrypted); err != nil {
return nil, fmt.Errorf("invalid ciphertext format: %w", err)
}
// Get key material
material, err := k.getKeyMaterial(ctx, encrypted.KeyID, encrypted.KeyVersion)
if err != nil {
return nil, err
}
// Decrypt the key material
decryptedKey, err := k.rootCipher.Open(nil, material.Nonce, material.EncryptedKey, nil)
if err != nil {
return nil, fmt.Errorf("failed to decrypt key material: %w", err)
}
// Create cipher from decrypted key
block, err := aes.NewCipher(decryptedKey)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
// Extract nonce from ciphertext
nonceSize := gcm.NonceSize()
if len(encrypted.Data) < nonceSize {
return nil, fmt.Errorf("ciphertext too short")
}
nonce, data := encrypted.Data[:nonceSize], encrypted.Data[nonceSize:]
// Decrypt
return gcm.Open(nil, nonce, data, nil)
}
// getKeyMaterial retrieves and returns key material.
func (k *KMS) getKeyMaterial(ctx context.Context, keyID string, version int) (*KeyMaterial, error) {
key := fmt.Sprintf("%s%s/%d", keyMaterialPrefix, keyID, version)
return GetJSON[KeyMaterial](ctx, k.store, key)
}
// GetPublicKey returns the public key for an asymmetric key.
func (k *KMS) GetPublicKey(ctx context.Context, keyID string) ([]byte, error) {
key, err := k.GetKey(ctx, keyID)
if err != nil {
return nil, err
}
material, err := k.getKeyMaterial(ctx, keyID, key.Version)
if err != nil {
return nil, err
}
if len(material.PublicKey) == 0 {
return nil, fmt.Errorf("key %s is not an asymmetric key", keyID)
}
// Return PEM-encoded public key
return pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Bytes: material.PublicKey,
}), nil
}
// Sign signs data using an asymmetric key.
func (k *KMS) Sign(ctx context.Context, keyID string, data []byte) ([]byte, error) {
key, err := k.GetKey(ctx, keyID)
if err != nil {
return nil, err
}
if key.Usage != KeyUsageSignVerify {
return nil, fmt.Errorf("key %s cannot be used for signing", keyID)
}
material, err := k.getKeyMaterial(ctx, keyID, key.Version)
if err != nil {
return nil, err
}
// Decrypt private key
privateBytes, err := k.rootCipher.Open(nil, material.Nonce, material.EncryptedPrivate, nil)
if err != nil {
return nil, fmt.Errorf("failed to decrypt private key: %w", err)
}
hash := sha256.Sum256(data)
switch key.Type {
case KeyTypeRSA3072, KeyTypeRSA4096:
privateKey, err := x509.ParsePKCS1PrivateKey(privateBytes)
if err != nil {
return nil, err
}
return rsa.SignPKCS1v15(rand.Reader, privateKey, 0, hash[:])
case KeyTypeECDSAP256, KeyTypeECDSAP384:
privateKey, err := x509.ParseECPrivateKey(privateBytes)
if err != nil {
return nil, err
}
return ecdsa.SignASN1(rand.Reader, privateKey, hash[:])
case KeyTypeEdDSA:
return ed25519.Sign(privateBytes, data), nil
default:
return nil, fmt.Errorf("unsupported key type for signing: %s", key.Type)
}
}
// Verify verifies a signature.
func (k *KMS) Verify(ctx context.Context, keyID string, data, signature []byte) (bool, error) {
key, err := k.GetKey(ctx, keyID)
if err != nil {
return false, err
}
material, err := k.getKeyMaterial(ctx, keyID, key.Version)
if err != nil {
return false, err
}
hash := sha256.Sum256(data)
switch key.Type {
case KeyTypeRSA3072, KeyTypeRSA4096:
pub, err := x509.ParsePKIXPublicKey(material.PublicKey)
if err != nil {
return false, err
}
rsaPub := pub.(*rsa.PublicKey)
err = rsa.VerifyPKCS1v15(rsaPub, 0, hash[:], signature)
return err == nil, nil
case KeyTypeECDSAP256, KeyTypeECDSAP384:
pub, err := x509.ParsePKIXPublicKey(material.PublicKey)
if err != nil {
return false, err
}
ecdsaPub := pub.(*ecdsa.PublicKey)
return ecdsa.VerifyASN1(ecdsaPub, hash[:], signature), nil
case KeyTypeEdDSA:
return ed25519.Verify(material.PublicKey, data, signature), nil
default:
return false, fmt.Errorf("unsupported key type for verification: %s", key.Type)
}
}
// Secret Management
// CreateSecret creates a new secret.
func (k *KMS) CreateSecret(ctx context.Context, name string, value []byte, opts *SecretOptions) (*Secret, error) {
k.mu.Lock()
defer k.mu.Unlock()
secretID := generateID(16)
now := time.Now()
// Get or create encryption key
keyID := ""
if opts != nil && opts.KeyID != "" {
keyID = opts.KeyID
} else {
// Use default project key or create one
key, err := k.GetKeyByName(ctx, "default")
if err == ErrKeyNotFound {
key, err = k.GenerateKey(ctx, "default", KeyTypeAES256, KeyUsageEncryptDecrypt, nil)
}
if err != nil {
return nil, fmt.Errorf("failed to get encryption key: %w", err)
}
keyID = key.ID
}
// Encrypt the secret value
encrypted, err := k.Encrypt(ctx, keyID, value)
if err != nil {
return nil, fmt.Errorf("failed to encrypt secret: %w", err)
}
secret := &Secret{
ID: secretID,
Name: name,
Version: 1,
KeyID: keyID,
Value: encrypted,
Created: now,
Updated: now,
}
if opts != nil {
secret.Environment = opts.Environment
secret.Path = opts.Path
secret.Tags = opts.Tags
secret.Metadata = opts.Metadata
secret.OrgID = opts.OrgID
secret.ProjectID = opts.ProjectID
}
if err := SetJSON(ctx, k.store, secretPrefix+secretID, secret); err != nil {
return nil, fmt.Errorf("failed to save secret: %w", err)
}
return secret, nil
}
// SecretOptions holds options for secret creation.
type SecretOptions struct {
KeyID string
Environment string
Path string
Tags []string
Metadata map[string]string
OrgID string
ProjectID string
}
// GetSecret retrieves a secret by ID.
func (k *KMS) GetSecret(ctx context.Context, secretID string) (*Secret, error) {
return GetJSON[Secret](ctx, k.store, secretPrefix+secretID)
}
// GetSecretValue retrieves and decrypts a secret value.
func (k *KMS) GetSecretValue(ctx context.Context, secretID string) ([]byte, error) {
secret, err := k.GetSecret(ctx, secretID)
if err != nil {
return nil, err
}
return k.Decrypt(ctx, secret.Value)
}
// ListSecrets lists secrets, optionally filtered by environment or path.
func (k *KMS) ListSecrets(ctx context.Context, env, path string) ([]*Secret, error) {
var secrets []*Secret
err := k.store.Scan(ctx, secretPrefix, func(key string, value []byte) error {
var secret Secret
if err := json.Unmarshal(value, &secret); err != nil {
return nil
}
if (env == "" || secret.Environment == env) && (path == "" || secret.Path == path) {
secrets = append(secrets, &secret)
}
return nil
})
return secrets, err
}
// UpdateSecret updates a secret's value.
func (k *KMS) UpdateSecret(ctx context.Context, secretID string, newValue []byte) (*Secret, error) {
k.mu.Lock()
defer k.mu.Unlock()
secret, err := k.GetSecret(ctx, secretID)
if err != nil {
return nil, err
}
// Encrypt the new value
encrypted, err := k.Encrypt(ctx, secret.KeyID, newValue)
if err != nil {
return nil, fmt.Errorf("failed to encrypt secret: %w", err)
}
secret.Value = encrypted
secret.Version++
secret.Updated = time.Now()
if err := SetJSON(ctx, k.store, secretPrefix+secretID, secret); err != nil {
return nil, fmt.Errorf("failed to save secret: %w", err)
}
return secret, nil
}
// DeleteSecret deletes a secret.
func (k *KMS) DeleteSecret(ctx context.Context, secretID string) error {
return k.store.Delete(ctx, secretPrefix+secretID)
}
// Close closes the KMS and underlying storage.
func (k *KMS) Close() error {
return k.store.Close()
}
// Helper functions
func generateID(length int) string {
bytes := make([]byte, length)
rand.Read(bytes)
return hex.EncodeToString(bytes)
}
// EncodeBase64 encodes bytes to base64.
func EncodeBase64(data []byte) string {
return base64.StdEncoding.EncodeToString(data)
}
// DecodeBase64 decodes base64 to bytes.
func DecodeBase64(s string) ([]byte, error) {
return base64.StdEncoding.DecodeString(s)
}