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.
		
		
		
		
		
			
		
			
				
					
					
						
							349 lines
						
					
					
						
							11 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							349 lines
						
					
					
						
							11 KiB
						
					
					
				| package gcp | |
| 
 | |
| import ( | |
| 	"context" | |
| 	"crypto/rand" | |
| 	"encoding/base64" | |
| 	"fmt" | |
| 	"strings" | |
| 	"time" | |
| 
 | |
| 	"google.golang.org/api/option" | |
| 
 | |
| 	kms "cloud.google.com/go/kms/apiv1" | |
| 	"cloud.google.com/go/kms/apiv1/kmspb" | |
| 
 | |
| 	"github.com/seaweedfs/seaweedfs/weed/glog" | |
| 	seaweedkms "github.com/seaweedfs/seaweedfs/weed/kms" | |
| 	"github.com/seaweedfs/seaweedfs/weed/util" | |
| ) | |
| 
 | |
| func init() { | |
| 	// Register the Google Cloud KMS provider | |
| 	seaweedkms.RegisterProvider("gcp", NewGCPKMSProvider) | |
| } | |
| 
 | |
| // GCPKMSProvider implements the KMSProvider interface using Google Cloud KMS | |
| type GCPKMSProvider struct { | |
| 	client    *kms.KeyManagementClient | |
| 	projectID string | |
| } | |
| 
 | |
| // GCPKMSConfig contains configuration for the Google Cloud KMS provider | |
| type GCPKMSConfig struct { | |
| 	ProjectID             string `json:"project_id"`              // GCP project ID | |
| 	CredentialsFile       string `json:"credentials_file"`        // Path to service account JSON file | |
| 	CredentialsJSON       string `json:"credentials_json"`        // Service account JSON content (base64 encoded) | |
| 	UseDefaultCredentials bool   `json:"use_default_credentials"` // Use default GCP credentials (metadata service, gcloud, etc.) | |
| 	RequestTimeout        int    `json:"request_timeout"`         // Request timeout in seconds (default: 30) | |
| } | |
| 
 | |
| // NewGCPKMSProvider creates a new Google Cloud KMS provider | |
| func NewGCPKMSProvider(config util.Configuration) (seaweedkms.KMSProvider, error) { | |
| 	if config == nil { | |
| 		return nil, fmt.Errorf("Google Cloud KMS configuration is required") | |
| 	} | |
| 
 | |
| 	// Extract configuration | |
| 	projectID := config.GetString("project_id") | |
| 	if projectID == "" { | |
| 		return nil, fmt.Errorf("project_id is required for Google Cloud KMS provider") | |
| 	} | |
| 
 | |
| 	credentialsFile := config.GetString("credentials_file") | |
| 	credentialsJSON := config.GetString("credentials_json") | |
| 	useDefaultCredentials := config.GetBool("use_default_credentials") | |
| 
 | |
| 	requestTimeout := config.GetInt("request_timeout") | |
| 	if requestTimeout == 0 { | |
| 		requestTimeout = 30 // Default 30 seconds | |
| 	} | |
| 
 | |
| 	// Prepare client options | |
| 	var clientOptions []option.ClientOption | |
| 
 | |
| 	// Configure credentials | |
| 	if credentialsFile != "" { | |
| 		clientOptions = append(clientOptions, option.WithCredentialsFile(credentialsFile)) | |
| 		glog.V(1).Infof("GCP KMS: Using credentials file %s", credentialsFile) | |
| 	} else if credentialsJSON != "" { | |
| 		// Decode base64 credentials if provided | |
| 		credBytes, err := base64.StdEncoding.DecodeString(credentialsJSON) | |
| 		if err != nil { | |
| 			return nil, fmt.Errorf("failed to decode credentials JSON: %w", err) | |
| 		} | |
| 		clientOptions = append(clientOptions, option.WithCredentialsJSON(credBytes)) | |
| 		glog.V(1).Infof("GCP KMS: Using provided credentials JSON") | |
| 	} else if !useDefaultCredentials { | |
| 		return nil, fmt.Errorf("either credentials_file, credentials_json, or use_default_credentials=true must be provided") | |
| 	} else { | |
| 		glog.V(1).Infof("GCP KMS: Using default credentials") | |
| 	} | |
| 
 | |
| 	// Set request timeout | |
| 	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(requestTimeout)*time.Second) | |
| 	defer cancel() | |
| 
 | |
| 	// Create KMS client | |
| 	client, err := kms.NewKeyManagementClient(ctx, clientOptions...) | |
| 	if err != nil { | |
| 		return nil, fmt.Errorf("failed to create Google Cloud KMS client: %w", err) | |
| 	} | |
| 
 | |
| 	provider := &GCPKMSProvider{ | |
| 		client:    client, | |
| 		projectID: projectID, | |
| 	} | |
| 
 | |
| 	glog.V(1).Infof("Google Cloud KMS provider initialized for project %s", projectID) | |
| 	return provider, nil | |
| } | |
| 
 | |
| // GenerateDataKey generates a new data encryption key using Google Cloud KMS | |
| func (p *GCPKMSProvider) GenerateDataKey(ctx context.Context, req *seaweedkms.GenerateDataKeyRequest) (*seaweedkms.GenerateDataKeyResponse, error) { | |
| 	if req == nil { | |
| 		return nil, fmt.Errorf("GenerateDataKeyRequest cannot be nil") | |
| 	} | |
| 
 | |
| 	if req.KeyID == "" { | |
| 		return nil, fmt.Errorf("KeyID is required") | |
| 	} | |
| 
 | |
| 	// Validate key spec | |
| 	var keySize int | |
| 	switch req.KeySpec { | |
| 	case seaweedkms.KeySpecAES256: | |
| 		keySize = 32 // 256 bits | |
| 	default: | |
| 		return nil, fmt.Errorf("unsupported key spec: %s", req.KeySpec) | |
| 	} | |
| 
 | |
| 	// Generate data key locally (GCP KMS doesn't have GenerateDataKey like AWS) | |
| 	dataKey := make([]byte, keySize) | |
| 	if _, err := rand.Read(dataKey); err != nil { | |
| 		return nil, fmt.Errorf("failed to generate random data key: %w", err) | |
| 	} | |
| 
 | |
| 	// Encrypt the data key using GCP KMS | |
| 	glog.V(4).Infof("GCP KMS: Encrypting data key using key %s", req.KeyID) | |
| 
 | |
| 	// Build the encryption request | |
| 	encryptReq := &kmspb.EncryptRequest{ | |
| 		Name:      req.KeyID, | |
| 		Plaintext: dataKey, | |
| 	} | |
| 
 | |
| 	// Add additional authenticated data from encryption context | |
| 	if len(req.EncryptionContext) > 0 { | |
| 		// Convert encryption context to additional authenticated data | |
| 		aad := p.encryptionContextToAAD(req.EncryptionContext) | |
| 		encryptReq.AdditionalAuthenticatedData = []byte(aad) | |
| 	} | |
| 
 | |
| 	// Call GCP KMS to encrypt the data key | |
| 	encryptResp, err := p.client.Encrypt(ctx, encryptReq) | |
| 	if err != nil { | |
| 		return nil, p.convertGCPError(err, req.KeyID) | |
| 	} | |
| 
 | |
| 	// Create standardized envelope format for consistent API behavior | |
| 	envelopeBlob, err := seaweedkms.CreateEnvelope("gcp", encryptResp.Name, string(encryptResp.Ciphertext), nil) | |
| 	if err != nil { | |
| 		return nil, fmt.Errorf("failed to create ciphertext envelope: %w", err) | |
| 	} | |
| 
 | |
| 	response := &seaweedkms.GenerateDataKeyResponse{ | |
| 		KeyID:          encryptResp.Name, // GCP returns the full resource name | |
| 		Plaintext:      dataKey, | |
| 		CiphertextBlob: envelopeBlob, // Store in standardized envelope format | |
| 	} | |
| 
 | |
| 	glog.V(4).Infof("GCP KMS: Generated and encrypted data key using key %s", req.KeyID) | |
| 	return response, nil | |
| } | |
| 
 | |
| // Decrypt decrypts an encrypted data key using Google Cloud KMS | |
| func (p *GCPKMSProvider) Decrypt(ctx context.Context, req *seaweedkms.DecryptRequest) (*seaweedkms.DecryptResponse, error) { | |
| 	if req == nil { | |
| 		return nil, fmt.Errorf("DecryptRequest cannot be nil") | |
| 	} | |
| 
 | |
| 	if len(req.CiphertextBlob) == 0 { | |
| 		return nil, fmt.Errorf("CiphertextBlob cannot be empty") | |
| 	} | |
| 
 | |
| 	// Parse the ciphertext envelope to extract key information | |
| 	envelope, err := seaweedkms.ParseEnvelope(req.CiphertextBlob) | |
| 	if err != nil { | |
| 		return nil, fmt.Errorf("failed to parse ciphertext envelope: %w", err) | |
| 	} | |
| 
 | |
| 	keyName := envelope.KeyID | |
| 	if keyName == "" { | |
| 		return nil, fmt.Errorf("envelope missing key ID") | |
| 	} | |
| 
 | |
| 	// Convert string back to bytes | |
| 	ciphertext := []byte(envelope.Ciphertext) | |
| 
 | |
| 	// Build the decryption request | |
| 	decryptReq := &kmspb.DecryptRequest{ | |
| 		Name:       keyName, | |
| 		Ciphertext: ciphertext, | |
| 	} | |
| 
 | |
| 	// Add additional authenticated data from encryption context | |
| 	if len(req.EncryptionContext) > 0 { | |
| 		aad := p.encryptionContextToAAD(req.EncryptionContext) | |
| 		decryptReq.AdditionalAuthenticatedData = []byte(aad) | |
| 	} | |
| 
 | |
| 	// Call GCP KMS to decrypt the data key | |
| 	glog.V(4).Infof("GCP KMS: Decrypting data key using key %s", keyName) | |
| 	decryptResp, err := p.client.Decrypt(ctx, decryptReq) | |
| 	if err != nil { | |
| 		return nil, p.convertGCPError(err, keyName) | |
| 	} | |
| 
 | |
| 	response := &seaweedkms.DecryptResponse{ | |
| 		KeyID:     keyName, | |
| 		Plaintext: decryptResp.Plaintext, | |
| 	} | |
| 
 | |
| 	glog.V(4).Infof("GCP KMS: Decrypted data key using key %s", keyName) | |
| 	return response, nil | |
| } | |
| 
 | |
| // DescribeKey validates that a key exists and returns its metadata | |
| func (p *GCPKMSProvider) DescribeKey(ctx context.Context, req *seaweedkms.DescribeKeyRequest) (*seaweedkms.DescribeKeyResponse, error) { | |
| 	if req == nil { | |
| 		return nil, fmt.Errorf("DescribeKeyRequest cannot be nil") | |
| 	} | |
| 
 | |
| 	if req.KeyID == "" { | |
| 		return nil, fmt.Errorf("KeyID is required") | |
| 	} | |
| 
 | |
| 	// Build the request to get the crypto key | |
| 	getKeyReq := &kmspb.GetCryptoKeyRequest{ | |
| 		Name: req.KeyID, | |
| 	} | |
| 
 | |
| 	// Call GCP KMS to get key information | |
| 	glog.V(4).Infof("GCP KMS: Describing key %s", req.KeyID) | |
| 	key, err := p.client.GetCryptoKey(ctx, getKeyReq) | |
| 	if err != nil { | |
| 		return nil, p.convertGCPError(err, req.KeyID) | |
| 	} | |
| 
 | |
| 	response := &seaweedkms.DescribeKeyResponse{ | |
| 		KeyID:       key.Name, | |
| 		ARN:         key.Name, // GCP uses resource names instead of ARNs | |
| 		Description: "Google Cloud KMS key", | |
| 	} | |
| 
 | |
| 	// Map GCP key purpose to our usage enum | |
| 	if key.Purpose == kmspb.CryptoKey_ENCRYPT_DECRYPT { | |
| 		response.KeyUsage = seaweedkms.KeyUsageEncryptDecrypt | |
| 	} | |
| 
 | |
| 	// Map GCP key state to our state enum | |
| 	// Get the primary version to check its state | |
| 	if key.Primary != nil && key.Primary.State == kmspb.CryptoKeyVersion_ENABLED { | |
| 		response.KeyState = seaweedkms.KeyStateEnabled | |
| 	} else { | |
| 		response.KeyState = seaweedkms.KeyStateDisabled | |
| 	} | |
| 
 | |
| 	// GCP KMS keys are managed by Google Cloud | |
| 	response.Origin = seaweedkms.KeyOriginGCP | |
| 
 | |
| 	glog.V(4).Infof("GCP KMS: Described key %s (state: %s)", req.KeyID, response.KeyState) | |
| 	return response, nil | |
| } | |
| 
 | |
| // GetKeyID resolves a key name to the full resource name | |
| func (p *GCPKMSProvider) GetKeyID(ctx context.Context, keyIdentifier string) (string, error) { | |
| 	if keyIdentifier == "" { | |
| 		return "", fmt.Errorf("key identifier cannot be empty") | |
| 	} | |
| 
 | |
| 	// If it's already a full resource name, return as-is | |
| 	if strings.HasPrefix(keyIdentifier, "projects/") { | |
| 		return keyIdentifier, nil | |
| 	} | |
| 
 | |
| 	// Otherwise, try to construct the full resource name or validate via DescribeKey | |
| 	descReq := &seaweedkms.DescribeKeyRequest{KeyID: keyIdentifier} | |
| 	descResp, err := p.DescribeKey(ctx, descReq) | |
| 	if err != nil { | |
| 		return "", fmt.Errorf("failed to resolve key identifier %s: %w", keyIdentifier, err) | |
| 	} | |
| 
 | |
| 	return descResp.KeyID, nil | |
| } | |
| 
 | |
| // Close cleans up any resources used by the provider | |
| func (p *GCPKMSProvider) Close() error { | |
| 	if p.client != nil { | |
| 		err := p.client.Close() | |
| 		if err != nil { | |
| 			glog.Errorf("Error closing GCP KMS client: %v", err) | |
| 			return err | |
| 		} | |
| 	} | |
| 	glog.V(2).Infof("Google Cloud KMS provider closed") | |
| 	return nil | |
| } | |
| 
 | |
| // encryptionContextToAAD converts encryption context map to additional authenticated data | |
| // This is a simplified implementation - in production, you might want a more robust serialization | |
| func (p *GCPKMSProvider) encryptionContextToAAD(context map[string]string) string { | |
| 	if len(context) == 0 { | |
| 		return "" | |
| 	} | |
| 
 | |
| 	// Simple key=value&key=value format | |
| 	var parts []string | |
| 	for k, v := range context { | |
| 		parts = append(parts, fmt.Sprintf("%s=%s", k, v)) | |
| 	} | |
| 	return strings.Join(parts, "&") | |
| } | |
| 
 | |
| // convertGCPError converts Google Cloud KMS errors to our standard KMS errors | |
| func (p *GCPKMSProvider) convertGCPError(err error, keyID string) error { | |
| 	// Google Cloud SDK uses gRPC status codes | |
| 	errMsg := err.Error() | |
| 
 | |
| 	if strings.Contains(errMsg, "not found") || strings.Contains(errMsg, "NotFound") { | |
| 		return &seaweedkms.KMSError{ | |
| 			Code:    seaweedkms.ErrCodeNotFoundException, | |
| 			Message: fmt.Sprintf("Key not found in Google Cloud KMS: %v", err), | |
| 			KeyID:   keyID, | |
| 		} | |
| 	} | |
| 
 | |
| 	if strings.Contains(errMsg, "permission") || strings.Contains(errMsg, "access") || strings.Contains(errMsg, "Forbidden") { | |
| 		return &seaweedkms.KMSError{ | |
| 			Code:    seaweedkms.ErrCodeAccessDenied, | |
| 			Message: fmt.Sprintf("Access denied to Google Cloud KMS: %v", err), | |
| 			KeyID:   keyID, | |
| 		} | |
| 	} | |
| 
 | |
| 	if strings.Contains(errMsg, "disabled") || strings.Contains(errMsg, "unavailable") { | |
| 		return &seaweedkms.KMSError{ | |
| 			Code:    seaweedkms.ErrCodeKeyUnavailable, | |
| 			Message: fmt.Sprintf("Key unavailable in Google Cloud KMS: %v", err), | |
| 			KeyID:   keyID, | |
| 		} | |
| 	} | |
| 
 | |
| 	// For unknown errors, wrap as internal failure | |
| 	return &seaweedkms.KMSError{ | |
| 		Code:    seaweedkms.ErrCodeKMSInternalFailure, | |
| 		Message: fmt.Sprintf("Google Cloud KMS error: %v", err), | |
| 		KeyID:   keyID, | |
| 	} | |
| }
 |