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.
		
		
		
		
		
			
		
			
				
					
					
						
							563 lines
						
					
					
						
							16 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							563 lines
						
					
					
						
							16 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"` | |
| } | |
| 
 | |
| 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 { | |
| 	// Configure on-demand key creation behavior | |
| 	// Default is already set in NewLocalKMSProvider, this allows override | |
| 	p.enableOnDemandCreate = config.GetBool("enableOnDemandCreate") | |
| 
 | |
| 	// TODO: Load pre-existing keys from configuration | |
| 	// For now, rely on default key creation in constructor | |
| 	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.KeyOriginAWS, | |
| 		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.KeyOriginAWS, | |
| 		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.KeyOriginAWS, | |
| 		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 | |
| }
 |