diff options
Diffstat (limited to 'weed/kms/gcp/gcp_kms.go')
| -rw-r--r-- | weed/kms/gcp/gcp_kms.go | 349 |
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, + } +} |
