aboutsummaryrefslogtreecommitdiff
path: root/weed/kms/gcp/gcp_kms.go
diff options
context:
space:
mode:
Diffstat (limited to 'weed/kms/gcp/gcp_kms.go')
-rw-r--r--weed/kms/gcp/gcp_kms.go349
1 files changed, 349 insertions, 0 deletions
diff --git a/weed/kms/gcp/gcp_kms.go b/weed/kms/gcp/gcp_kms.go
new file mode 100644
index 000000000..5380a7aeb
--- /dev/null
+++ b/weed/kms/gcp/gcp_kms.go
@@ -0,0 +1,349 @@
+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,
+ }
+}