| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568 |
- 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
- }
|