You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
568 lines
17 KiB
568 lines
17 KiB
package local
|
|
|
|
import (
|
|
"context"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/kms"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
// LocalKMSProvider implements a local, in-memory KMS for development and testing
|
|
// WARNING: This is NOT suitable for production use - keys are stored in memory
|
|
type LocalKMSProvider struct {
|
|
mu sync.RWMutex
|
|
keys map[string]*LocalKey
|
|
defaultKeyID string
|
|
enableOnDemandCreate bool // Whether to create keys on-demand for missing key IDs
|
|
}
|
|
|
|
// LocalKey represents a key stored in the local KMS
|
|
type LocalKey struct {
|
|
KeyID string `json:"keyId"`
|
|
ARN string `json:"arn"`
|
|
Description string `json:"description"`
|
|
KeyMaterial []byte `json:"keyMaterial"` // 256-bit master key
|
|
KeyUsage kms.KeyUsage `json:"keyUsage"`
|
|
KeyState kms.KeyState `json:"keyState"`
|
|
Origin kms.KeyOrigin `json:"origin"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
Aliases []string `json:"aliases"`
|
|
Metadata map[string]string `json:"metadata"`
|
|
}
|
|
|
|
// LocalKMSConfig contains configuration for the local KMS provider
|
|
type LocalKMSConfig struct {
|
|
DefaultKeyID string `json:"defaultKeyId"`
|
|
Keys map[string]*LocalKey `json:"keys"`
|
|
EnableOnDemandCreate bool `json:"enableOnDemandCreate"`
|
|
}
|
|
|
|
func init() {
|
|
// Register the local KMS provider
|
|
kms.RegisterProvider("local", NewLocalKMSProvider)
|
|
}
|
|
|
|
// NewLocalKMSProvider creates a new local KMS provider
|
|
func NewLocalKMSProvider(config util.Configuration) (kms.KMSProvider, error) {
|
|
provider := &LocalKMSProvider{
|
|
keys: make(map[string]*LocalKey),
|
|
enableOnDemandCreate: true, // Default to true for development/testing convenience
|
|
}
|
|
|
|
// Load configuration if provided
|
|
if config != nil {
|
|
if err := provider.loadConfig(config); err != nil {
|
|
return nil, fmt.Errorf("failed to load local KMS config: %v", err)
|
|
}
|
|
}
|
|
|
|
// Create a default key if none exists
|
|
if len(provider.keys) == 0 {
|
|
defaultKey, err := provider.createDefaultKey()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create default key: %v", err)
|
|
}
|
|
provider.defaultKeyID = defaultKey.KeyID
|
|
glog.V(1).Infof("Local KMS: Created default key %s", defaultKey.KeyID)
|
|
}
|
|
|
|
return provider, nil
|
|
}
|
|
|
|
// loadConfig loads configuration from the provided config
|
|
func (p *LocalKMSProvider) loadConfig(config util.Configuration) error {
|
|
if config == nil {
|
|
return nil
|
|
}
|
|
|
|
p.enableOnDemandCreate = config.GetBool("enableOnDemandCreate")
|
|
|
|
// TODO: Load pre-existing keys from configuration if provided
|
|
// For now, rely on default key creation in constructor
|
|
|
|
glog.V(2).Infof("Local KMS: enableOnDemandCreate = %v", p.enableOnDemandCreate)
|
|
return nil
|
|
}
|
|
|
|
// createDefaultKey creates a default master key for the local KMS
|
|
func (p *LocalKMSProvider) createDefaultKey() (*LocalKey, error) {
|
|
keyID, err := generateKeyID()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate key ID: %w", err)
|
|
}
|
|
keyMaterial := make([]byte, 32) // 256-bit key
|
|
if _, err := io.ReadFull(rand.Reader, keyMaterial); err != nil {
|
|
return nil, fmt.Errorf("failed to generate key material: %w", err)
|
|
}
|
|
|
|
key := &LocalKey{
|
|
KeyID: keyID,
|
|
ARN: fmt.Sprintf("arn:aws:kms:local:000000000000:key/%s", keyID),
|
|
Description: "Default local KMS key for SeaweedFS",
|
|
KeyMaterial: keyMaterial,
|
|
KeyUsage: kms.KeyUsageEncryptDecrypt,
|
|
KeyState: kms.KeyStateEnabled,
|
|
Origin: kms.KeyOriginLocal,
|
|
CreatedAt: time.Now(),
|
|
Aliases: []string{"alias/seaweedfs-default"},
|
|
Metadata: make(map[string]string),
|
|
}
|
|
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
p.keys[keyID] = key
|
|
|
|
// Also register aliases
|
|
for _, alias := range key.Aliases {
|
|
p.keys[alias] = key
|
|
}
|
|
|
|
return key, nil
|
|
}
|
|
|
|
// GenerateDataKey implements the KMSProvider interface
|
|
func (p *LocalKMSProvider) GenerateDataKey(ctx context.Context, req *kms.GenerateDataKeyRequest) (*kms.GenerateDataKeyResponse, error) {
|
|
if req.KeySpec != kms.KeySpecAES256 {
|
|
return nil, &kms.KMSError{
|
|
Code: kms.ErrCodeInvalidKeyUsage,
|
|
Message: fmt.Sprintf("Unsupported key spec: %s", req.KeySpec),
|
|
KeyID: req.KeyID,
|
|
}
|
|
}
|
|
|
|
// Resolve the key
|
|
key, err := p.getKey(req.KeyID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if key.KeyState != kms.KeyStateEnabled {
|
|
return nil, &kms.KMSError{
|
|
Code: kms.ErrCodeKeyUnavailable,
|
|
Message: fmt.Sprintf("Key %s is in state %s", key.KeyID, key.KeyState),
|
|
KeyID: key.KeyID,
|
|
}
|
|
}
|
|
|
|
// Generate a random 256-bit data key
|
|
dataKey := make([]byte, 32)
|
|
if _, err := io.ReadFull(rand.Reader, dataKey); err != nil {
|
|
return nil, &kms.KMSError{
|
|
Code: kms.ErrCodeKMSInternalFailure,
|
|
Message: "Failed to generate data key",
|
|
KeyID: key.KeyID,
|
|
}
|
|
}
|
|
|
|
// Encrypt the data key with the master key
|
|
encryptedDataKey, err := p.encryptDataKey(dataKey, key, req.EncryptionContext)
|
|
if err != nil {
|
|
kms.ClearSensitiveData(dataKey)
|
|
return nil, &kms.KMSError{
|
|
Code: kms.ErrCodeKMSInternalFailure,
|
|
Message: fmt.Sprintf("Failed to encrypt data key: %v", err),
|
|
KeyID: key.KeyID,
|
|
}
|
|
}
|
|
|
|
return &kms.GenerateDataKeyResponse{
|
|
KeyID: key.KeyID,
|
|
Plaintext: dataKey,
|
|
CiphertextBlob: encryptedDataKey,
|
|
}, nil
|
|
}
|
|
|
|
// Decrypt implements the KMSProvider interface
|
|
func (p *LocalKMSProvider) Decrypt(ctx context.Context, req *kms.DecryptRequest) (*kms.DecryptResponse, error) {
|
|
// Parse the encrypted data key to extract metadata
|
|
metadata, err := p.parseEncryptedDataKey(req.CiphertextBlob)
|
|
if err != nil {
|
|
return nil, &kms.KMSError{
|
|
Code: kms.ErrCodeInvalidCiphertext,
|
|
Message: fmt.Sprintf("Invalid ciphertext format: %v", err),
|
|
}
|
|
}
|
|
|
|
// Verify encryption context matches
|
|
if !p.encryptionContextMatches(metadata.EncryptionContext, req.EncryptionContext) {
|
|
return nil, &kms.KMSError{
|
|
Code: kms.ErrCodeInvalidCiphertext,
|
|
Message: "Encryption context mismatch",
|
|
KeyID: metadata.KeyID,
|
|
}
|
|
}
|
|
|
|
// Get the master key
|
|
key, err := p.getKey(metadata.KeyID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if key.KeyState != kms.KeyStateEnabled {
|
|
return nil, &kms.KMSError{
|
|
Code: kms.ErrCodeKeyUnavailable,
|
|
Message: fmt.Sprintf("Key %s is in state %s", key.KeyID, key.KeyState),
|
|
KeyID: key.KeyID,
|
|
}
|
|
}
|
|
|
|
// Decrypt the data key
|
|
dataKey, err := p.decryptDataKey(metadata, key)
|
|
if err != nil {
|
|
return nil, &kms.KMSError{
|
|
Code: kms.ErrCodeInvalidCiphertext,
|
|
Message: fmt.Sprintf("Failed to decrypt data key: %v", err),
|
|
KeyID: key.KeyID,
|
|
}
|
|
}
|
|
|
|
return &kms.DecryptResponse{
|
|
KeyID: key.KeyID,
|
|
Plaintext: dataKey,
|
|
}, nil
|
|
}
|
|
|
|
// DescribeKey implements the KMSProvider interface
|
|
func (p *LocalKMSProvider) DescribeKey(ctx context.Context, req *kms.DescribeKeyRequest) (*kms.DescribeKeyResponse, error) {
|
|
key, err := p.getKey(req.KeyID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &kms.DescribeKeyResponse{
|
|
KeyID: key.KeyID,
|
|
ARN: key.ARN,
|
|
Description: key.Description,
|
|
KeyUsage: key.KeyUsage,
|
|
KeyState: key.KeyState,
|
|
Origin: key.Origin,
|
|
}, nil
|
|
}
|
|
|
|
// GetKeyID implements the KMSProvider interface
|
|
func (p *LocalKMSProvider) GetKeyID(ctx context.Context, keyIdentifier string) (string, error) {
|
|
key, err := p.getKey(keyIdentifier)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return key.KeyID, nil
|
|
}
|
|
|
|
// Close implements the KMSProvider interface
|
|
func (p *LocalKMSProvider) Close() error {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
|
|
// Clear all key material from memory
|
|
for _, key := range p.keys {
|
|
kms.ClearSensitiveData(key.KeyMaterial)
|
|
}
|
|
p.keys = make(map[string]*LocalKey)
|
|
return nil
|
|
}
|
|
|
|
// getKey retrieves a key by ID or alias, creating it on-demand if it doesn't exist
|
|
func (p *LocalKMSProvider) getKey(keyIdentifier string) (*LocalKey, error) {
|
|
p.mu.RLock()
|
|
|
|
// Try direct lookup first
|
|
if key, exists := p.keys[keyIdentifier]; exists {
|
|
p.mu.RUnlock()
|
|
return key, nil
|
|
}
|
|
|
|
// Try with default key if no identifier provided
|
|
if keyIdentifier == "" && p.defaultKeyID != "" {
|
|
if key, exists := p.keys[p.defaultKeyID]; exists {
|
|
p.mu.RUnlock()
|
|
return key, nil
|
|
}
|
|
}
|
|
|
|
p.mu.RUnlock()
|
|
|
|
// Key doesn't exist - create on-demand if enabled and key identifier is reasonable
|
|
if keyIdentifier != "" && p.enableOnDemandCreate && p.isReasonableKeyIdentifier(keyIdentifier) {
|
|
glog.V(1).Infof("Creating on-demand local KMS key: %s", keyIdentifier)
|
|
key, err := p.CreateKeyWithID(keyIdentifier, fmt.Sprintf("Auto-created local KMS key: %s", keyIdentifier))
|
|
if err != nil {
|
|
return nil, &kms.KMSError{
|
|
Code: kms.ErrCodeKMSInternalFailure,
|
|
Message: fmt.Sprintf("Failed to create on-demand key %s: %v", keyIdentifier, err),
|
|
KeyID: keyIdentifier,
|
|
}
|
|
}
|
|
return key, nil
|
|
}
|
|
|
|
return nil, &kms.KMSError{
|
|
Code: kms.ErrCodeNotFoundException,
|
|
Message: fmt.Sprintf("Key not found: %s", keyIdentifier),
|
|
KeyID: keyIdentifier,
|
|
}
|
|
}
|
|
|
|
// isReasonableKeyIdentifier determines if a key identifier is reasonable for on-demand creation
|
|
func (p *LocalKMSProvider) isReasonableKeyIdentifier(keyIdentifier string) bool {
|
|
// Basic validation: reasonable length and character set
|
|
if len(keyIdentifier) < 3 || len(keyIdentifier) > 100 {
|
|
return false
|
|
}
|
|
|
|
// Allow alphanumeric characters, hyphens, underscores, and forward slashes
|
|
// This covers most reasonable key identifier formats without being overly restrictive
|
|
for _, r := range keyIdentifier {
|
|
if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') ||
|
|
(r >= '0' && r <= '9') || r == '-' || r == '_' || r == '/') {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Reject keys that start or end with separators
|
|
if keyIdentifier[0] == '-' || keyIdentifier[0] == '_' || keyIdentifier[0] == '/' ||
|
|
keyIdentifier[len(keyIdentifier)-1] == '-' || keyIdentifier[len(keyIdentifier)-1] == '_' || keyIdentifier[len(keyIdentifier)-1] == '/' {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// encryptedDataKeyMetadata represents the metadata stored with encrypted data keys
|
|
type encryptedDataKeyMetadata struct {
|
|
KeyID string `json:"keyId"`
|
|
EncryptionContext map[string]string `json:"encryptionContext"`
|
|
EncryptedData []byte `json:"encryptedData"`
|
|
Nonce []byte `json:"nonce"` // Renamed from IV to be more explicit about AES-GCM usage
|
|
}
|
|
|
|
// encryptDataKey encrypts a data key using the master key with AES-GCM for authenticated encryption
|
|
func (p *LocalKMSProvider) encryptDataKey(dataKey []byte, masterKey *LocalKey, encryptionContext map[string]string) ([]byte, error) {
|
|
block, err := aes.NewCipher(masterKey.KeyMaterial)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Generate a random nonce
|
|
nonce := make([]byte, gcm.NonceSize())
|
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Prepare additional authenticated data (AAD) from the encryption context
|
|
// Use deterministic marshaling to ensure consistent AAD
|
|
var aad []byte
|
|
if len(encryptionContext) > 0 {
|
|
var err error
|
|
aad, err = marshalEncryptionContextDeterministic(encryptionContext)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal encryption context for AAD: %w", err)
|
|
}
|
|
}
|
|
|
|
// Encrypt using AES-GCM
|
|
encryptedData := gcm.Seal(nil, nonce, dataKey, aad)
|
|
|
|
// Create metadata structure
|
|
metadata := &encryptedDataKeyMetadata{
|
|
KeyID: masterKey.KeyID,
|
|
EncryptionContext: encryptionContext,
|
|
EncryptedData: encryptedData,
|
|
Nonce: nonce,
|
|
}
|
|
|
|
// Serialize metadata to JSON
|
|
return json.Marshal(metadata)
|
|
}
|
|
|
|
// decryptDataKey decrypts a data key using the master key with AES-GCM for authenticated decryption
|
|
func (p *LocalKMSProvider) decryptDataKey(metadata *encryptedDataKeyMetadata, masterKey *LocalKey) ([]byte, error) {
|
|
block, err := aes.NewCipher(masterKey.KeyMaterial)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Prepare additional authenticated data (AAD)
|
|
var aad []byte
|
|
if len(metadata.EncryptionContext) > 0 {
|
|
var err error
|
|
aad, err = marshalEncryptionContextDeterministic(metadata.EncryptionContext)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal encryption context for AAD: %w", err)
|
|
}
|
|
}
|
|
|
|
// Decrypt using AES-GCM
|
|
nonce := metadata.Nonce
|
|
if len(nonce) != gcm.NonceSize() {
|
|
return nil, fmt.Errorf("invalid nonce size: expected %d, got %d", gcm.NonceSize(), len(nonce))
|
|
}
|
|
|
|
dataKey, err := gcm.Open(nil, nonce, metadata.EncryptedData, aad)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decrypt with GCM: %w", err)
|
|
}
|
|
|
|
return dataKey, nil
|
|
}
|
|
|
|
// parseEncryptedDataKey parses the encrypted data key blob
|
|
func (p *LocalKMSProvider) parseEncryptedDataKey(ciphertextBlob []byte) (*encryptedDataKeyMetadata, error) {
|
|
var metadata encryptedDataKeyMetadata
|
|
if err := json.Unmarshal(ciphertextBlob, &metadata); err != nil {
|
|
return nil, fmt.Errorf("failed to parse ciphertext blob: %v", err)
|
|
}
|
|
return &metadata, nil
|
|
}
|
|
|
|
// encryptionContextMatches checks if two encryption contexts match
|
|
func (p *LocalKMSProvider) encryptionContextMatches(ctx1, ctx2 map[string]string) bool {
|
|
if len(ctx1) != len(ctx2) {
|
|
return false
|
|
}
|
|
for k, v := range ctx1 {
|
|
if ctx2[k] != v {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// generateKeyID generates a random key ID
|
|
func generateKeyID() (string, error) {
|
|
// Generate a UUID-like key ID
|
|
b := make([]byte, 16)
|
|
if _, err := io.ReadFull(rand.Reader, b); err != nil {
|
|
return "", fmt.Errorf("failed to generate random bytes for key ID: %w", err)
|
|
}
|
|
|
|
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
|
|
b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]), nil
|
|
}
|
|
|
|
// CreateKey creates a new key in the local KMS (for testing)
|
|
func (p *LocalKMSProvider) CreateKey(description string, aliases []string) (*LocalKey, error) {
|
|
keyID, err := generateKeyID()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate key ID: %w", err)
|
|
}
|
|
keyMaterial := make([]byte, 32)
|
|
if _, err := io.ReadFull(rand.Reader, keyMaterial); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
key := &LocalKey{
|
|
KeyID: keyID,
|
|
ARN: fmt.Sprintf("arn:aws:kms:local:000000000000:key/%s", keyID),
|
|
Description: description,
|
|
KeyMaterial: keyMaterial,
|
|
KeyUsage: kms.KeyUsageEncryptDecrypt,
|
|
KeyState: kms.KeyStateEnabled,
|
|
Origin: kms.KeyOriginLocal,
|
|
CreatedAt: time.Now(),
|
|
Aliases: aliases,
|
|
Metadata: make(map[string]string),
|
|
}
|
|
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
|
|
p.keys[keyID] = key
|
|
for _, alias := range aliases {
|
|
// Ensure alias has proper format
|
|
if !strings.HasPrefix(alias, "alias/") {
|
|
alias = "alias/" + alias
|
|
}
|
|
p.keys[alias] = key
|
|
}
|
|
|
|
return key, nil
|
|
}
|
|
|
|
// CreateKeyWithID creates a key with a specific keyID (for testing only)
|
|
func (p *LocalKMSProvider) CreateKeyWithID(keyID, description string) (*LocalKey, error) {
|
|
keyMaterial := make([]byte, 32)
|
|
if _, err := io.ReadFull(rand.Reader, keyMaterial); err != nil {
|
|
return nil, fmt.Errorf("failed to generate key material: %w", err)
|
|
}
|
|
|
|
key := &LocalKey{
|
|
KeyID: keyID,
|
|
ARN: fmt.Sprintf("arn:aws:kms:local:000000000000:key/%s", keyID),
|
|
Description: description,
|
|
KeyMaterial: keyMaterial,
|
|
KeyUsage: kms.KeyUsageEncryptDecrypt,
|
|
KeyState: kms.KeyStateEnabled,
|
|
Origin: kms.KeyOriginLocal,
|
|
CreatedAt: time.Now(),
|
|
Aliases: []string{}, // No aliases by default
|
|
Metadata: make(map[string]string),
|
|
}
|
|
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
|
|
// Register key with the exact keyID provided
|
|
p.keys[keyID] = key
|
|
|
|
return key, nil
|
|
}
|
|
|
|
// marshalEncryptionContextDeterministic creates a deterministic byte representation of encryption context
|
|
// This ensures that the same encryption context always produces the same AAD for AES-GCM
|
|
func marshalEncryptionContextDeterministic(encryptionContext map[string]string) ([]byte, error) {
|
|
if len(encryptionContext) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// Sort keys to ensure deterministic output
|
|
keys := make([]string, 0, len(encryptionContext))
|
|
for k := range encryptionContext {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
// Build deterministic representation with proper JSON escaping
|
|
var buf strings.Builder
|
|
buf.WriteString("{")
|
|
for i, k := range keys {
|
|
if i > 0 {
|
|
buf.WriteString(",")
|
|
}
|
|
// Marshal key and value to get proper JSON string escaping
|
|
keyBytes, err := json.Marshal(k)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal encryption context key '%s': %w", k, err)
|
|
}
|
|
valueBytes, err := json.Marshal(encryptionContext[k])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal encryption context value for key '%s': %w", k, err)
|
|
}
|
|
buf.Write(keyBytes)
|
|
buf.WriteString(":")
|
|
buf.Write(valueBytes)
|
|
}
|
|
buf.WriteString("}")
|
|
|
|
return []byte(buf.String()), nil
|
|
}
|