snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-16 03:23:42 +05:00
commit 89f5be005d
3997 changed files with 961970 additions and 0 deletions

389
weed/kms/aws/aws_kms.go Normal file
View File

@@ -0,0 +1,389 @@
package aws
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/kms"
"github.com/seaweedfs/seaweedfs/weed/glog"
seaweedkms "github.com/seaweedfs/seaweedfs/weed/kms"
"github.com/seaweedfs/seaweedfs/weed/util"
)
func init() {
// Register the AWS KMS provider
seaweedkms.RegisterProvider("aws", NewAWSKMSProvider)
}
// AWSKMSProvider implements the KMSProvider interface using AWS KMS
type AWSKMSProvider struct {
client *kms.KMS
region string
endpoint string // For testing with LocalStack or custom endpoints
}
// AWSKMSConfig contains configuration for the AWS KMS provider
type AWSKMSConfig struct {
Region string `json:"region"` // AWS region (e.g., "us-east-1")
AccessKey string `json:"access_key"` // AWS access key (optional if using IAM roles)
SecretKey string `json:"secret_key"` // AWS secret key (optional if using IAM roles)
SessionToken string `json:"session_token"` // AWS session token (optional for STS)
Endpoint string `json:"endpoint"` // Custom endpoint (optional, for LocalStack/testing)
Profile string `json:"profile"` // AWS profile name (optional)
RoleARN string `json:"role_arn"` // IAM role ARN to assume (optional)
ExternalID string `json:"external_id"` // External ID for role assumption (optional)
ConnectTimeout int `json:"connect_timeout"` // Connection timeout in seconds (default: 10)
RequestTimeout int `json:"request_timeout"` // Request timeout in seconds (default: 30)
MaxRetries int `json:"max_retries"` // Maximum number of retries (default: 3)
}
// NewAWSKMSProvider creates a new AWS KMS provider
func NewAWSKMSProvider(config util.Configuration) (seaweedkms.KMSProvider, error) {
if config == nil {
return nil, fmt.Errorf("AWS KMS configuration is required")
}
// Extract configuration
region := config.GetString("region")
if region == "" {
region = "us-east-1" // Default region
}
accessKey := config.GetString("access_key")
secretKey := config.GetString("secret_key")
sessionToken := config.GetString("session_token")
endpoint := config.GetString("endpoint")
profile := config.GetString("profile")
// Timeouts and retries
connectTimeout := config.GetInt("connect_timeout")
if connectTimeout == 0 {
connectTimeout = 10 // Default 10 seconds
}
requestTimeout := config.GetInt("request_timeout")
if requestTimeout == 0 {
requestTimeout = 30 // Default 30 seconds
}
maxRetries := config.GetInt("max_retries")
if maxRetries == 0 {
maxRetries = 3 // Default 3 retries
}
// Create AWS session
awsConfig := &aws.Config{
Region: aws.String(region),
MaxRetries: aws.Int(maxRetries),
HTTPClient: &http.Client{
Timeout: time.Duration(requestTimeout) * time.Second,
},
}
// Set custom endpoint if provided (for testing with LocalStack)
if endpoint != "" {
awsConfig.Endpoint = aws.String(endpoint)
awsConfig.DisableSSL = aws.Bool(strings.HasPrefix(endpoint, "http://"))
}
// Configure credentials
if accessKey != "" && secretKey != "" {
awsConfig.Credentials = credentials.NewStaticCredentials(accessKey, secretKey, sessionToken)
} else if profile != "" {
awsConfig.Credentials = credentials.NewSharedCredentials("", profile)
}
// If neither are provided, use default credential chain (IAM roles, etc.)
sess, err := session.NewSession(awsConfig)
if err != nil {
return nil, fmt.Errorf("failed to create AWS session: %w", err)
}
provider := &AWSKMSProvider{
client: kms.New(sess),
region: region,
endpoint: endpoint,
}
glog.V(1).Infof("AWS KMS provider initialized for region %s", region)
return provider, nil
}
// GenerateDataKey generates a new data encryption key using AWS KMS
func (p *AWSKMSProvider) 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 keySpec string
switch req.KeySpec {
case seaweedkms.KeySpecAES256:
keySpec = "AES_256"
default:
return nil, fmt.Errorf("unsupported key spec: %s", req.KeySpec)
}
// Build KMS request
kmsReq := &kms.GenerateDataKeyInput{
KeyId: aws.String(req.KeyID),
KeySpec: aws.String(keySpec),
}
// Add encryption context if provided
if len(req.EncryptionContext) > 0 {
kmsReq.EncryptionContext = aws.StringMap(req.EncryptionContext)
}
// Call AWS KMS
glog.V(4).Infof("AWS KMS: Generating data key for key ID %s", req.KeyID)
result, err := p.client.GenerateDataKeyWithContext(ctx, kmsReq)
if err != nil {
return nil, p.convertAWSError(err, req.KeyID)
}
// Extract the actual key ID from the response (resolves aliases)
actualKeyID := ""
if result.KeyId != nil {
actualKeyID = *result.KeyId
}
// Create standardized envelope format for consistent API behavior
envelopeBlob, err := seaweedkms.CreateEnvelope("aws", actualKeyID, base64.StdEncoding.EncodeToString(result.CiphertextBlob), nil)
if err != nil {
return nil, fmt.Errorf("failed to create ciphertext envelope: %w", err)
}
response := &seaweedkms.GenerateDataKeyResponse{
KeyID: actualKeyID,
Plaintext: result.Plaintext,
CiphertextBlob: envelopeBlob, // Store in standardized envelope format
}
glog.V(4).Infof("AWS KMS: Generated data key for key ID %s (actual: %s)", req.KeyID, actualKeyID)
return response, nil
}
// Decrypt decrypts an encrypted data key using AWS KMS
func (p *AWSKMSProvider) 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)
}
if envelope.Provider != "aws" {
return nil, fmt.Errorf("invalid provider in envelope: expected 'aws', got '%s'", envelope.Provider)
}
ciphertext, err := base64.StdEncoding.DecodeString(envelope.Ciphertext)
if err != nil {
return nil, fmt.Errorf("failed to decode ciphertext from envelope: %w", err)
}
// Build KMS request
kmsReq := &kms.DecryptInput{
CiphertextBlob: ciphertext,
}
// Add encryption context if provided
if len(req.EncryptionContext) > 0 {
kmsReq.EncryptionContext = aws.StringMap(req.EncryptionContext)
}
// Call AWS KMS
glog.V(4).Infof("AWS KMS: Decrypting data key (blob size: %d bytes)", len(req.CiphertextBlob))
result, err := p.client.DecryptWithContext(ctx, kmsReq)
if err != nil {
return nil, p.convertAWSError(err, "")
}
// Extract the key ID that was used for encryption
keyID := ""
if result.KeyId != nil {
keyID = *result.KeyId
}
response := &seaweedkms.DecryptResponse{
KeyID: keyID,
Plaintext: result.Plaintext,
}
glog.V(4).Infof("AWS KMS: Decrypted data key using key ID %s", keyID)
return response, nil
}
// DescribeKey validates that a key exists and returns its metadata
func (p *AWSKMSProvider) 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 KMS request
kmsReq := &kms.DescribeKeyInput{
KeyId: aws.String(req.KeyID),
}
// Call AWS KMS
glog.V(4).Infof("AWS KMS: Describing key %s", req.KeyID)
result, err := p.client.DescribeKeyWithContext(ctx, kmsReq)
if err != nil {
return nil, p.convertAWSError(err, req.KeyID)
}
if result.KeyMetadata == nil {
return nil, fmt.Errorf("no key metadata returned from AWS KMS")
}
metadata := result.KeyMetadata
response := &seaweedkms.DescribeKeyResponse{
KeyID: aws.StringValue(metadata.KeyId),
ARN: aws.StringValue(metadata.Arn),
Description: aws.StringValue(metadata.Description),
}
// Convert AWS key usage to our enum
if metadata.KeyUsage != nil {
switch *metadata.KeyUsage {
case "ENCRYPT_DECRYPT":
response.KeyUsage = seaweedkms.KeyUsageEncryptDecrypt
case "GENERATE_DATA_KEY":
response.KeyUsage = seaweedkms.KeyUsageGenerateDataKey
}
}
// Convert AWS key state to our enum
if metadata.KeyState != nil {
switch *metadata.KeyState {
case "Enabled":
response.KeyState = seaweedkms.KeyStateEnabled
case "Disabled":
response.KeyState = seaweedkms.KeyStateDisabled
case "PendingDeletion":
response.KeyState = seaweedkms.KeyStatePendingDeletion
case "Unavailable":
response.KeyState = seaweedkms.KeyStateUnavailable
}
}
// Convert AWS origin to our enum
if metadata.Origin != nil {
switch *metadata.Origin {
case "AWS_KMS":
response.Origin = seaweedkms.KeyOriginAWS
case "EXTERNAL":
response.Origin = seaweedkms.KeyOriginExternal
case "AWS_CLOUDHSM":
response.Origin = seaweedkms.KeyOriginCloudHSM
}
}
glog.V(4).Infof("AWS KMS: Described key %s (actual: %s, state: %s)", req.KeyID, response.KeyID, response.KeyState)
return response, nil
}
// GetKeyID resolves a key alias or ARN to the actual key ID
func (p *AWSKMSProvider) GetKeyID(ctx context.Context, keyIdentifier string) (string, error) {
if keyIdentifier == "" {
return "", fmt.Errorf("key identifier cannot be empty")
}
// Use DescribeKey to resolve the key identifier
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 *AWSKMSProvider) Close() error {
// AWS SDK clients don't require explicit cleanup
glog.V(2).Infof("AWS KMS provider closed")
return nil
}
// convertAWSError converts AWS KMS errors to our standard KMS errors
func (p *AWSKMSProvider) convertAWSError(err error, keyID string) error {
if awsErr, ok := err.(awserr.Error); ok {
switch awsErr.Code() {
case "NotFoundException":
return &seaweedkms.KMSError{
Code: seaweedkms.ErrCodeNotFoundException,
Message: awsErr.Message(),
KeyID: keyID,
}
case "DisabledException", "KeyUnavailableException":
return &seaweedkms.KMSError{
Code: seaweedkms.ErrCodeKeyUnavailable,
Message: awsErr.Message(),
KeyID: keyID,
}
case "AccessDeniedException":
return &seaweedkms.KMSError{
Code: seaweedkms.ErrCodeAccessDenied,
Message: awsErr.Message(),
KeyID: keyID,
}
case "InvalidKeyUsageException":
return &seaweedkms.KMSError{
Code: seaweedkms.ErrCodeInvalidKeyUsage,
Message: awsErr.Message(),
KeyID: keyID,
}
case "InvalidCiphertextException":
return &seaweedkms.KMSError{
Code: seaweedkms.ErrCodeInvalidCiphertext,
Message: awsErr.Message(),
KeyID: keyID,
}
case "KMSInternalException", "KMSInvalidStateException":
return &seaweedkms.KMSError{
Code: seaweedkms.ErrCodeKMSInternalFailure,
Message: awsErr.Message(),
KeyID: keyID,
}
default:
// For unknown AWS errors, wrap them as internal failures
return &seaweedkms.KMSError{
Code: seaweedkms.ErrCodeKMSInternalFailure,
Message: fmt.Sprintf("AWS KMS error %s: %s", awsErr.Code(), awsErr.Message()),
KeyID: keyID,
}
}
}
// For non-AWS errors (network issues, etc.), wrap as internal failure
return &seaweedkms.KMSError{
Code: seaweedkms.ErrCodeKMSInternalFailure,
Message: fmt.Sprintf("AWS KMS provider error: %v", err),
KeyID: keyID,
}
}

379
weed/kms/azure/azure_kms.go Normal file
View File

@@ -0,0 +1,379 @@
//go:build azurekms
package azure
import (
"context"
"crypto/rand"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys"
"github.com/seaweedfs/seaweedfs/weed/glog"
seaweedkms "github.com/seaweedfs/seaweedfs/weed/kms"
"github.com/seaweedfs/seaweedfs/weed/util"
)
func init() {
// Register the Azure Key Vault provider
seaweedkms.RegisterProvider("azure", NewAzureKMSProvider)
}
// AzureKMSProvider implements the KMSProvider interface using Azure Key Vault
type AzureKMSProvider struct {
client *azkeys.Client
vaultURL string
tenantID string
clientID string
clientSecret string
}
// AzureKMSConfig contains configuration for the Azure Key Vault provider
type AzureKMSConfig struct {
VaultURL string `json:"vault_url"` // Azure Key Vault URL (e.g., "https://myvault.vault.azure.net/")
TenantID string `json:"tenant_id"` // Azure AD tenant ID
ClientID string `json:"client_id"` // Service principal client ID
ClientSecret string `json:"client_secret"` // Service principal client secret
Certificate string `json:"certificate"` // Certificate path for cert-based auth (alternative to client secret)
UseDefaultCreds bool `json:"use_default_creds"` // Use default Azure credentials (managed identity)
RequestTimeout int `json:"request_timeout"` // Request timeout in seconds (default: 30)
}
// NewAzureKMSProvider creates a new Azure Key Vault provider
func NewAzureKMSProvider(config util.Configuration) (seaweedkms.KMSProvider, error) {
if config == nil {
return nil, fmt.Errorf("Azure Key Vault configuration is required")
}
// Extract configuration
vaultURL := config.GetString("vault_url")
if vaultURL == "" {
return nil, fmt.Errorf("vault_url is required for Azure Key Vault provider")
}
tenantID := config.GetString("tenant_id")
clientID := config.GetString("client_id")
clientSecret := config.GetString("client_secret")
useDefaultCreds := config.GetBool("use_default_creds")
requestTimeout := config.GetInt("request_timeout")
if requestTimeout == 0 {
requestTimeout = 30 // Default 30 seconds
}
// Create credential based on configuration
var credential azcore.TokenCredential
var err error
if useDefaultCreds {
// Use default Azure credentials (managed identity, Azure CLI, etc.)
credential, err = azidentity.NewDefaultAzureCredential(nil)
if err != nil {
return nil, fmt.Errorf("failed to create default Azure credentials: %w", err)
}
glog.V(1).Infof("Azure KMS: Using default Azure credentials")
} else if clientID != "" && clientSecret != "" {
// Use service principal credentials
if tenantID == "" {
return nil, fmt.Errorf("tenant_id is required when using client credentials")
}
credential, err = azidentity.NewClientSecretCredential(tenantID, clientID, clientSecret, nil)
if err != nil {
return nil, fmt.Errorf("failed to create Azure client secret credential: %w", err)
}
glog.V(1).Infof("Azure KMS: Using client secret credentials for client ID %s", clientID)
} else {
return nil, fmt.Errorf("either use_default_creds=true or client_id+client_secret must be provided")
}
// Create Key Vault client
clientOptions := &azkeys.ClientOptions{
ClientOptions: azcore.ClientOptions{
PerCallPolicies: []policy.Policy{},
Transport: &http.Client{
Timeout: time.Duration(requestTimeout) * time.Second,
},
},
}
client, err := azkeys.NewClient(vaultURL, credential, clientOptions)
if err != nil {
return nil, fmt.Errorf("failed to create Azure Key Vault client: %w", err)
}
provider := &AzureKMSProvider{
client: client,
vaultURL: vaultURL,
tenantID: tenantID,
clientID: clientID,
clientSecret: clientSecret,
}
glog.V(1).Infof("Azure Key Vault provider initialized for vault %s", vaultURL)
return provider, nil
}
// GenerateDataKey generates a new data encryption key using Azure Key Vault
func (p *AzureKMSProvider) 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 (Azure Key Vault 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 Azure Key Vault
glog.V(4).Infof("Azure KMS: Encrypting data key using key %s", req.KeyID)
// Prepare encryption parameters
algorithm := azkeys.JSONWebKeyEncryptionAlgorithmRSAOAEP256
encryptParams := azkeys.KeyOperationsParameters{
Algorithm: &algorithm, // Default encryption algorithm
Value: dataKey,
}
// Add encryption context as Additional Authenticated Data (AAD) if provided
if len(req.EncryptionContext) > 0 {
// Marshal encryption context to JSON for deterministic AAD
aadBytes, err := json.Marshal(req.EncryptionContext)
if err != nil {
return nil, fmt.Errorf("failed to marshal encryption context: %w", err)
}
encryptParams.AAD = aadBytes
glog.V(4).Infof("Azure KMS: Using encryption context as AAD for key %s", req.KeyID)
}
// Call Azure Key Vault to encrypt the data key
encryptResult, err := p.client.Encrypt(ctx, req.KeyID, "", encryptParams, nil)
if err != nil {
return nil, p.convertAzureError(err, req.KeyID)
}
// Get the actual key ID from the response
actualKeyID := req.KeyID
if encryptResult.KID != nil {
actualKeyID = string(*encryptResult.KID)
}
// Create standardized envelope format for consistent API behavior
envelopeBlob, err := seaweedkms.CreateEnvelope("azure", actualKeyID, string(encryptResult.Result), nil)
if err != nil {
return nil, fmt.Errorf("failed to create ciphertext envelope: %w", err)
}
response := &seaweedkms.GenerateDataKeyResponse{
KeyID: actualKeyID,
Plaintext: dataKey,
CiphertextBlob: envelopeBlob, // Store in standardized envelope format
}
glog.V(4).Infof("Azure KMS: Generated and encrypted data key using key %s", actualKeyID)
return response, nil
}
// Decrypt decrypts an encrypted data key using Azure Key Vault
func (p *AzureKMSProvider) 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)
}
keyID := envelope.KeyID
if keyID == "" {
return nil, fmt.Errorf("envelope missing key ID")
}
// Convert string back to bytes
ciphertext := []byte(envelope.Ciphertext)
// Prepare decryption parameters
decryptAlgorithm := azkeys.JSONWebKeyEncryptionAlgorithmRSAOAEP256
decryptParams := azkeys.KeyOperationsParameters{
Algorithm: &decryptAlgorithm, // Must match encryption algorithm
Value: ciphertext,
}
// Add encryption context as Additional Authenticated Data (AAD) if provided
if len(req.EncryptionContext) > 0 {
// Marshal encryption context to JSON for deterministic AAD (must match encryption)
aadBytes, err := json.Marshal(req.EncryptionContext)
if err != nil {
return nil, fmt.Errorf("failed to marshal encryption context: %w", err)
}
decryptParams.AAD = aadBytes
glog.V(4).Infof("Azure KMS: Using encryption context as AAD for decryption of key %s", keyID)
}
// Call Azure Key Vault to decrypt the data key
glog.V(4).Infof("Azure KMS: Decrypting data key using key %s", keyID)
decryptResult, err := p.client.Decrypt(ctx, keyID, "", decryptParams, nil)
if err != nil {
return nil, p.convertAzureError(err, keyID)
}
// Get the actual key ID from the response
actualKeyID := keyID
if decryptResult.KID != nil {
actualKeyID = string(*decryptResult.KID)
}
response := &seaweedkms.DecryptResponse{
KeyID: actualKeyID,
Plaintext: decryptResult.Result,
}
glog.V(4).Infof("Azure KMS: Decrypted data key using key %s", actualKeyID)
return response, nil
}
// DescribeKey validates that a key exists and returns its metadata
func (p *AzureKMSProvider) 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")
}
// Get key from Azure Key Vault
glog.V(4).Infof("Azure KMS: Describing key %s", req.KeyID)
result, err := p.client.GetKey(ctx, req.KeyID, "", nil)
if err != nil {
return nil, p.convertAzureError(err, req.KeyID)
}
if result.Key == nil {
return nil, fmt.Errorf("no key returned from Azure Key Vault")
}
key := result.Key
response := &seaweedkms.DescribeKeyResponse{
KeyID: req.KeyID,
Description: "Azure Key Vault key", // Azure doesn't provide description in the same way
}
// Set ARN-like identifier for Azure
if key.KID != nil {
response.ARN = string(*key.KID)
response.KeyID = string(*key.KID)
}
// Set key usage based on key operations
if key.KeyOps != nil && len(key.KeyOps) > 0 {
// Azure keys can have multiple operations, check if encrypt/decrypt are supported
for _, op := range key.KeyOps {
if op != nil && (*op == string(azkeys.JSONWebKeyOperationEncrypt) || *op == string(azkeys.JSONWebKeyOperationDecrypt)) {
response.KeyUsage = seaweedkms.KeyUsageEncryptDecrypt
break
}
}
}
// Set key state based on enabled status
if result.Attributes != nil {
if result.Attributes.Enabled != nil && *result.Attributes.Enabled {
response.KeyState = seaweedkms.KeyStateEnabled
} else {
response.KeyState = seaweedkms.KeyStateDisabled
}
}
// Azure Key Vault keys are managed by Azure
response.Origin = seaweedkms.KeyOriginAzure
glog.V(4).Infof("Azure KMS: Described key %s (state: %s)", req.KeyID, response.KeyState)
return response, nil
}
// GetKeyID resolves a key name to the full key identifier
func (p *AzureKMSProvider) GetKeyID(ctx context.Context, keyIdentifier string) (string, error) {
if keyIdentifier == "" {
return "", fmt.Errorf("key identifier cannot be empty")
}
// Use DescribeKey to resolve and validate the key identifier
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 *AzureKMSProvider) Close() error {
// Azure SDK clients don't require explicit cleanup
glog.V(2).Infof("Azure Key Vault provider closed")
return nil
}
// convertAzureError converts Azure Key Vault errors to our standard KMS errors
func (p *AzureKMSProvider) convertAzureError(err error, keyID string) error {
// Azure SDK uses different error types, need to check for specific conditions
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 Azure Key Vault: %v", err),
KeyID: keyID,
}
}
if strings.Contains(errMsg, "access") || strings.Contains(errMsg, "Forbidden") || strings.Contains(errMsg, "Unauthorized") {
return &seaweedkms.KMSError{
Code: seaweedkms.ErrCodeAccessDenied,
Message: fmt.Sprintf("Access denied to Azure Key Vault: %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 Azure Key Vault: %v", err),
KeyID: keyID,
}
}
// For unknown errors, wrap as internal failure
return &seaweedkms.KMSError{
Code: seaweedkms.ErrCodeKMSInternalFailure,
Message: fmt.Sprintf("Azure Key Vault error: %v", err),
KeyID: keyID,
}
}

245
weed/kms/cache.go Normal file
View File

@@ -0,0 +1,245 @@
package kms
import (
"context"
"crypto/sha256"
"encoding/binary"
"hash"
"sort"
"sync"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
)
const (
// DefaultDataKeyCacheTTL is used when caching is enabled without an explicit TTL.
DefaultDataKeyCacheTTL = time.Hour
// DefaultDataKeyCacheSize bounds the number of cached decrypted data keys.
DefaultDataKeyCacheSize = 1000
)
// CachedKMSProvider wraps a KMSProvider with an in-memory cache of decrypted
// data keys. A KMS Decrypt call is deterministic for a given (ciphertext,
// encryption context) pair, so every read of the same SSE-KMS object would
// otherwise repeat an identical, network-bound KMS round-trip. Caching the
// plaintext data key removes that round-trip on the read hot path — this is
// what the cache_enabled / cache_ttl provider settings control.
//
// Only Decrypt is cached. GenerateDataKey must return a unique key per call,
// and DescribeKey / GetKeyID are cheap metadata lookups, so all three pass
// straight through to the wrapped provider via the embedded interface.
type CachedKMSProvider struct {
KMSProvider // wrapped provider; supplies the un-cached methods
ttl time.Duration
maxEntries int
mu sync.Mutex
entries map[string]*dataKeyCacheEntry
closed bool
}
type dataKeyCacheEntry struct {
keyID string
plaintext []byte
expiresAt time.Time
}
// NewCachedKMSProvider wraps provider with a decrypted-data-key cache. A
// non-positive ttl or maxEntries falls back to the package defaults.
func NewCachedKMSProvider(provider KMSProvider, ttl time.Duration, maxEntries int) *CachedKMSProvider {
if ttl <= 0 {
ttl = DefaultDataKeyCacheTTL
}
if maxEntries <= 0 {
maxEntries = DefaultDataKeyCacheSize
}
return &CachedKMSProvider{
KMSProvider: provider,
ttl: ttl,
maxEntries: maxEntries,
entries: make(map[string]*dataKeyCacheEntry),
}
}
// Decrypt returns the plaintext data key from cache when present, otherwise
// delegates to the wrapped provider and caches the result.
func (c *CachedKMSProvider) Decrypt(ctx context.Context, req *DecryptRequest) (*DecryptResponse, error) {
if req == nil || len(req.CiphertextBlob) == 0 {
// Let the wrapped provider produce its normal validation error.
return c.KMSProvider.Decrypt(ctx, req)
}
cacheKey := dataKeyCacheKey(req)
if resp := c.get(cacheKey); resp != nil {
glog.V(4).Infof("KMS data key cache hit")
return resp, nil
}
resp, err := c.KMSProvider.Decrypt(ctx, req)
if err != nil {
return nil, err
}
// Store our own copy, then hand the original back to the caller. The
// caller clears the plaintext it receives (ClearSensitiveData); keeping a
// private copy means later cache hits still return valid key material.
c.set(cacheKey, resp)
return resp, nil
}
// get returns a fresh copy of the cached response, or nil on miss/expiry. The
// copy is essential: callers clear the returned plaintext after use, which
// would otherwise wipe the cached key material for every subsequent reader.
func (c *CachedKMSProvider) get(cacheKey string) *DecryptResponse {
c.mu.Lock()
defer c.mu.Unlock()
entry, ok := c.entries[cacheKey]
if !ok {
return nil
}
if time.Now().After(entry.expiresAt) {
ClearSensitiveData(entry.plaintext)
delete(c.entries, cacheKey)
return nil
}
plaintext := make([]byte, len(entry.plaintext))
copy(plaintext, entry.plaintext)
return &DecryptResponse{KeyID: entry.keyID, Plaintext: plaintext}
}
// set stores a private copy of the plaintext data key.
func (c *CachedKMSProvider) set(cacheKey string, resp *DecryptResponse) {
if resp == nil {
return
}
plaintext := make([]byte, len(resp.Plaintext))
copy(plaintext, resp.Plaintext)
c.mu.Lock()
defer c.mu.Unlock()
if c.closed {
// A miss can finish its underlying Decrypt after Close scrubbed the
// map; don't repopulate it with key material that won't be cleared.
ClearSensitiveData(plaintext)
return
}
if old, exists := c.entries[cacheKey]; exists {
// Two readers can race on the same miss and both store; wipe the
// superseded key material instead of leaving it to linger in memory.
ClearSensitiveData(old.plaintext)
} else {
c.evictIfFullLocked()
}
c.entries[cacheKey] = &dataKeyCacheEntry{
keyID: resp.KeyID,
plaintext: plaintext,
expiresAt: time.Now().Add(c.ttl),
}
}
// evictIfFullLocked drops expired entries and, if still at capacity, the entry
// closest to expiry. Callers must hold c.mu.
func (c *CachedKMSProvider) evictIfFullLocked() {
if len(c.entries) < c.maxEntries {
return
}
now := time.Now()
for k, entry := range c.entries {
if now.After(entry.expiresAt) {
ClearSensitiveData(entry.plaintext)
delete(c.entries, k)
}
}
if len(c.entries) < c.maxEntries {
return
}
// Still full: evict the soonest-to-expire entry.
var oldestKey string
var oldestAt time.Time
for k, entry := range c.entries {
if oldestKey == "" || entry.expiresAt.Before(oldestAt) {
oldestKey = k
oldestAt = entry.expiresAt
}
}
if oldestKey != "" {
ClearSensitiveData(c.entries[oldestKey].plaintext)
delete(c.entries, oldestKey)
}
}
// Close clears cached key material and closes the wrapped provider.
func (c *CachedKMSProvider) Close() error {
c.mu.Lock()
c.closed = true
for k, entry := range c.entries {
ClearSensitiveData(entry.plaintext)
delete(c.entries, k)
}
c.mu.Unlock()
return c.KMSProvider.Close()
}
// sha256Pool reuses hash states across cache-key computations so the read hot
// path doesn't allocate a fresh one on every Decrypt.
var sha256Pool = sync.Pool{
New: func() interface{} { return sha256.New() },
}
// dataKeyCacheKey derives a stable cache key from the ciphertext blob and the
// encryption context. The context is part of the key because the same
// ciphertext decrypted under a different context is a different request, and
// caching by ciphertext alone would bypass the provider's context check.
func dataKeyCacheKey(req *DecryptRequest) string {
h := sha256Pool.Get().(hash.Hash)
// Length-prefix each field so distinct inputs cannot collide by concatenation.
writeLengthPrefixed(h, req.CiphertextBlob)
keys := make([]string, 0, len(req.EncryptionContext))
for k := range req.EncryptionContext {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
writeLengthPrefixed(h, []byte(k))
writeLengthPrefixed(h, []byte(req.EncryptionContext[k]))
}
key := string(h.Sum(nil))
h.Reset()
sha256Pool.Put(h)
return key
}
func writeLengthPrefixed(h hash.Hash, b []byte) {
var lenBuf [8]byte
binary.BigEndian.PutUint64(lenBuf[:], uint64(len(b)))
h.Write(lenBuf[:])
h.Write(b)
}
// wrapWithCacheIfEnabled returns provider wrapped in a data-key cache when the
// configuration enables caching, otherwise provider unchanged.
func wrapWithCacheIfEnabled(provider KMSProvider, config *KMSConfig) KMSProvider {
if provider == nil || config == nil || !config.CacheEnabled {
return provider
}
// Avoid double-wrapping if a cached provider is somehow reused.
if _, ok := provider.(*CachedKMSProvider); ok {
return provider
}
cached := NewCachedKMSProvider(provider, config.CacheTTL, config.MaxCacheSize)
glog.V(1).Infof("KMS data key caching enabled (ttl=%s, max=%d)", cached.ttl, cached.maxEntries)
return cached
}

273
weed/kms/cache_test.go Normal file
View File

@@ -0,0 +1,273 @@
package kms
import (
"bytes"
"context"
"fmt"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/util"
)
// countingKMSProvider is a minimal KMSProvider that records how many times each
// method is invoked so tests can assert the cache actually avoids round-trips.
type countingKMSProvider struct {
decryptCalls int64
generateCalls int64
mu sync.Mutex
store map[string][]byte // ciphertext(string) -> plaintext data key
}
func newCountingKMSProvider() *countingKMSProvider {
return &countingKMSProvider{store: make(map[string][]byte)}
}
func (p *countingKMSProvider) GenerateDataKey(ctx context.Context, req *GenerateDataKeyRequest) (*GenerateDataKeyResponse, error) {
n := atomic.AddInt64(&p.generateCalls, 1)
plaintext := []byte(fmt.Sprintf("datakey-%d-abcdefghijklmnopqrstuv", n))[:32]
ciphertext := []byte(fmt.Sprintf("cipher-%d", n))
p.mu.Lock()
p.store[string(ciphertext)] = append([]byte(nil), plaintext...)
p.mu.Unlock()
return &GenerateDataKeyResponse{
KeyID: req.KeyID,
Plaintext: plaintext,
CiphertextBlob: ciphertext,
}, nil
}
func (p *countingKMSProvider) Decrypt(ctx context.Context, req *DecryptRequest) (*DecryptResponse, error) {
atomic.AddInt64(&p.decryptCalls, 1)
p.mu.Lock()
plaintext, ok := p.store[string(req.CiphertextBlob)]
p.mu.Unlock()
if !ok {
return nil, fmt.Errorf("ciphertext not found")
}
// Return a fresh copy, mirroring real providers that allocate per call.
return &DecryptResponse{
KeyID: "test-key",
Plaintext: append([]byte(nil), plaintext...),
}, nil
}
func (p *countingKMSProvider) DescribeKey(ctx context.Context, req *DescribeKeyRequest) (*DescribeKeyResponse, error) {
return &DescribeKeyResponse{KeyID: req.KeyID, KeyState: KeyStateEnabled, KeyUsage: KeyUsageEncryptDecrypt}, nil
}
func (p *countingKMSProvider) GetKeyID(ctx context.Context, keyIdentifier string) (string, error) {
return keyIdentifier, nil
}
func (p *countingKMSProvider) Close() error { return nil }
// TestCachedProviderDecryptHitsProviderOnce is the regression test for the
// reported bug: with caching enabled, repeated Decrypt of the same data key
// must consult the underlying KMS only once.
func TestCachedProviderDecryptHitsProviderOnce(t *testing.T) {
inner := newCountingKMSProvider()
cached := NewCachedKMSProvider(inner, time.Hour, 100)
ctx := context.Background()
dk, err := cached.GenerateDataKey(ctx, &GenerateDataKeyRequest{KeyID: "k1", KeySpec: KeySpecAES256})
if err != nil {
t.Fatalf("GenerateDataKey: %v", err)
}
req := &DecryptRequest{CiphertextBlob: dk.CiphertextBlob}
first, err := cached.Decrypt(ctx, req)
if err != nil {
t.Fatalf("first Decrypt: %v", err)
}
// Emulate the S3 read path clearing the returned plaintext after use. A
// correct cache must hand back an independent copy so this does not wipe
// the cached key material.
ClearSensitiveData(first.Plaintext)
second, err := cached.Decrypt(ctx, req)
if err != nil {
t.Fatalf("second Decrypt: %v", err)
}
if got := atomic.LoadInt64(&inner.decryptCalls); got != 1 {
t.Fatalf("expected underlying Decrypt to be called once, got %d", got)
}
// The cached result must still be the real data key. If the cache handed
// back the same buffer it stored, the ClearSensitiveData above would have
// zeroed it.
if bytes.Equal(second.Plaintext, make([]byte, len(second.Plaintext))) {
t.Fatalf("cached plaintext was zeroed by a previous caller clearing its copy")
}
}
// TestCachedProviderContextSensitivity ensures the encryption context is part
// of the cache key: a different context must not return a stale plaintext.
func TestCachedProviderContextSensitivity(t *testing.T) {
inner := newCountingKMSProvider()
cached := NewCachedKMSProvider(inner, time.Hour, 100)
ctx := context.Background()
dk, err := cached.GenerateDataKey(ctx, &GenerateDataKeyRequest{KeyID: "k1", KeySpec: KeySpecAES256})
if err != nil {
t.Fatalf("GenerateDataKey: %v", err)
}
// Same ciphertext, two different contexts. The counting provider ignores
// context, but the cache must still treat these as distinct entries so a
// real provider's context check is never short-circuited.
if _, err := cached.Decrypt(ctx, &DecryptRequest{CiphertextBlob: dk.CiphertextBlob, EncryptionContext: map[string]string{"a": "1"}}); err != nil {
t.Fatalf("Decrypt ctx a: %v", err)
}
if _, err := cached.Decrypt(ctx, &DecryptRequest{CiphertextBlob: dk.CiphertextBlob, EncryptionContext: map[string]string{"a": "2"}}); err != nil {
t.Fatalf("Decrypt ctx b: %v", err)
}
if got := atomic.LoadInt64(&inner.decryptCalls); got != 2 {
t.Fatalf("expected 2 underlying Decrypt calls for distinct contexts, got %d", got)
}
}
// TestCachedProviderTTLExpiry verifies expired entries fall through to the
// underlying provider.
func TestCachedProviderTTLExpiry(t *testing.T) {
inner := newCountingKMSProvider()
cached := NewCachedKMSProvider(inner, 20*time.Millisecond, 100)
ctx := context.Background()
dk, err := cached.GenerateDataKey(ctx, &GenerateDataKeyRequest{KeyID: "k1", KeySpec: KeySpecAES256})
if err != nil {
t.Fatalf("GenerateDataKey: %v", err)
}
req := &DecryptRequest{CiphertextBlob: dk.CiphertextBlob}
if _, err := cached.Decrypt(ctx, req); err != nil {
t.Fatalf("Decrypt: %v", err)
}
time.Sleep(40 * time.Millisecond)
if _, err := cached.Decrypt(ctx, req); err != nil {
t.Fatalf("Decrypt after expiry: %v", err)
}
if got := atomic.LoadInt64(&inner.decryptCalls); got != 2 {
t.Fatalf("expected 2 underlying Decrypt calls across TTL expiry, got %d", got)
}
}
// TestCachedProviderMaxEntries verifies the cache is size-bounded.
func TestCachedProviderMaxEntries(t *testing.T) {
inner := newCountingKMSProvider()
cached := NewCachedKMSProvider(inner, time.Hour, 2)
ctx := context.Background()
var blobs [][]byte
for i := 0; i < 3; i++ {
dk, err := cached.GenerateDataKey(ctx, &GenerateDataKeyRequest{KeyID: "k1", KeySpec: KeySpecAES256})
if err != nil {
t.Fatalf("GenerateDataKey: %v", err)
}
blobs = append(blobs, dk.CiphertextBlob)
if _, err := cached.Decrypt(ctx, &DecryptRequest{CiphertextBlob: dk.CiphertextBlob}); err != nil {
t.Fatalf("Decrypt: %v", err)
}
}
cached.mu.Lock()
size := len(cached.entries)
cached.mu.Unlock()
if size > 2 {
t.Fatalf("cache exceeded max entries: got %d, want <= 2", size)
}
}
// TestCachedProviderClearsOverwrittenPlaintext verifies that when an entry is
// replaced (two readers racing on the same miss), the superseded key material
// is zeroed rather than left lingering in memory.
func TestCachedProviderClearsOverwrittenPlaintext(t *testing.T) {
inner := newCountingKMSProvider()
cached := NewCachedKMSProvider(inner, time.Hour, 10)
cached.set("dupkey", &DecryptResponse{KeyID: "k", Plaintext: []byte("0123456789abcdef0123456789abcdef")})
cached.mu.Lock()
oldBuf := cached.entries["dupkey"].plaintext
cached.mu.Unlock()
cached.set("dupkey", &DecryptResponse{KeyID: "k", Plaintext: []byte("ffffffffffffffffffffffffffffffff")})
if !bytes.Equal(oldBuf, make([]byte, len(oldBuf))) {
t.Fatalf("superseded cache plaintext was not zeroed: %v", oldBuf)
}
}
// TestCachedProviderNoWriteAfterClose verifies a store that lands after Close
// does not repopulate the scrubbed cache with key material.
func TestCachedProviderNoWriteAfterClose(t *testing.T) {
inner := newCountingKMSProvider()
cached := NewCachedKMSProvider(inner, time.Hour, 10)
if err := cached.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
cached.set("k", &DecryptResponse{KeyID: "k", Plaintext: []byte("0123456789abcdef0123456789abcdef")})
cached.mu.Lock()
n := len(cached.entries)
cached.mu.Unlock()
if n != 0 {
t.Fatalf("cache repopulated after Close: %d entries", n)
}
}
// TestAddKMSProviderWiresCache is the wiring regression test: a provider
// configured with cache_enabled must actually be handed back wrapped in a
// caching provider (the bug was that the setting was parsed but never applied).
func TestAddKMSProviderWiresCache(t *testing.T) {
RegisterProvider("cache-wiring-test", func(config util.Configuration) (KMSProvider, error) {
return newCountingKMSProvider(), nil
})
mgr := &KMSManager{
providers: make(map[string]KMSProvider),
configs: make(map[string]*KMSConfig),
bucketKMS: make(map[string]string),
}
if err := mgr.AddKMSProvider("p-cached", &KMSConfig{
Provider: "cache-wiring-test",
CacheEnabled: true,
CacheTTL: time.Hour,
MaxCacheSize: 10,
}); err != nil {
t.Fatalf("AddKMSProvider (cached): %v", err)
}
p, err := mgr.GetKMSProviderByName("p-cached")
if err != nil {
t.Fatalf("GetKMSProviderByName: %v", err)
}
if _, ok := p.(*CachedKMSProvider); !ok {
t.Fatalf("expected *CachedKMSProvider when cache enabled, got %T", p)
}
if err := mgr.AddKMSProvider("p-plain", &KMSConfig{
Provider: "cache-wiring-test",
CacheEnabled: false,
}); err != nil {
t.Fatalf("AddKMSProvider (plain): %v", err)
}
p2, err := mgr.GetKMSProviderByName("p-plain")
if err != nil {
t.Fatalf("GetKMSProviderByName: %v", err)
}
if _, ok := p2.(*CachedKMSProvider); ok {
t.Fatalf("expected unwrapped provider when cache disabled, got %T", p2)
}
}

485
weed/kms/config.go Normal file
View File

@@ -0,0 +1,485 @@
package kms
import (
"context"
"fmt"
"sync"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/util"
)
// KMSManager manages KMS provider instances and configurations
type KMSManager struct {
mu sync.RWMutex
providers map[string]KMSProvider // provider name -> provider instance
configs map[string]*KMSConfig // provider name -> configuration
bucketKMS map[string]string // bucket name -> provider name
defaultKMS string // default KMS provider name
}
// KMSConfig represents a complete KMS provider configuration
type KMSConfig struct {
Provider string `json:"provider"` // Provider type (aws, azure, gcp, local)
Config map[string]interface{} `json:"config"` // Provider-specific configuration
CacheEnabled bool `json:"cache_enabled"` // Enable data key caching
CacheTTL time.Duration `json:"cache_ttl"` // Cache TTL (default: 1 hour)
MaxCacheSize int `json:"max_cache_size"` // Maximum cached keys (default: 1000)
}
// BucketKMSConfig represents KMS configuration for a specific bucket
type BucketKMSConfig struct {
Provider string `json:"provider"` // KMS provider to use
KeyID string `json:"key_id"` // Default KMS key ID for this bucket
BucketKey bool `json:"bucket_key"` // Enable S3 Bucket Keys optimization
Context map[string]string `json:"context"` // Additional encryption context
Enabled bool `json:"enabled"` // Whether KMS encryption is enabled
}
// configAdapter adapts KMSConfig.Config to util.Configuration interface
type configAdapter struct {
config map[string]interface{}
}
// GetConfigMap returns the underlying configuration map for direct access
func (c *configAdapter) GetConfigMap() map[string]interface{} {
return c.config
}
func (c *configAdapter) GetString(key string) string {
if val, ok := c.config[key]; ok {
if str, ok := val.(string); ok {
return str
}
}
return ""
}
func (c *configAdapter) GetBool(key string) bool {
if val, ok := c.config[key]; ok {
if b, ok := val.(bool); ok {
return b
}
}
return false
}
func (c *configAdapter) GetInt(key string) int {
if val, ok := c.config[key]; ok {
if i, ok := val.(int); ok {
return i
}
if f, ok := val.(float64); ok {
return int(f)
}
}
return 0
}
func (c *configAdapter) GetStringSlice(key string) []string {
if val, ok := c.config[key]; ok {
if slice, ok := val.([]string); ok {
return slice
}
if interfaceSlice, ok := val.([]interface{}); ok {
result := make([]string, len(interfaceSlice))
for i, v := range interfaceSlice {
if str, ok := v.(string); ok {
result[i] = str
}
}
return result
}
}
return nil
}
func (c *configAdapter) SetDefault(key string, value interface{}) {
if c.config == nil {
c.config = make(map[string]interface{})
}
if _, exists := c.config[key]; !exists {
c.config[key] = value
}
}
var (
globalKMSManager *KMSManager
globalKMSMutex sync.RWMutex
// Global KMS provider for legacy compatibility
globalKMSProvider KMSProvider
)
// InitializeGlobalKMS initializes the global KMS provider
func InitializeGlobalKMS(config *KMSConfig) error {
if config == nil || config.Provider == "" {
return fmt.Errorf("KMS configuration is required")
}
// Adapt the config to util.Configuration interface
var providerConfig util.Configuration
if config.Config != nil {
providerConfig = &configAdapter{config: config.Config}
}
provider, err := GetProvider(config.Provider, providerConfig)
if err != nil {
return err
}
provider = wrapWithCacheIfEnabled(provider, config)
globalKMSMutex.Lock()
defer globalKMSMutex.Unlock()
// Close existing provider if any
if globalKMSProvider != nil {
globalKMSProvider.Close()
}
globalKMSProvider = provider
return nil
}
// GetGlobalKMS returns the global KMS provider
func GetGlobalKMS() KMSProvider {
globalKMSMutex.RLock()
defer globalKMSMutex.RUnlock()
return globalKMSProvider
}
// IsKMSEnabled returns true if KMS is enabled globally
func IsKMSEnabled() bool {
return GetGlobalKMS() != nil
}
// SetGlobalKMSProvider sets the global KMS provider.
// This is mainly for backward compatibility.
func SetGlobalKMSProvider(provider KMSProvider) {
globalKMSMutex.Lock()
defer globalKMSMutex.Unlock()
// Close existing provider if any
if globalKMSProvider != nil {
globalKMSProvider.Close()
}
globalKMSProvider = provider
}
// InitializeKMSManager initializes the global KMS manager
func InitializeKMSManager() *KMSManager {
globalKMSMutex.Lock()
defer globalKMSMutex.Unlock()
if globalKMSManager == nil {
globalKMSManager = &KMSManager{
providers: make(map[string]KMSProvider),
configs: make(map[string]*KMSConfig),
bucketKMS: make(map[string]string),
}
glog.V(1).Infof("KMS Manager initialized")
}
return globalKMSManager
}
// GetKMSManager returns the global KMS manager
func GetKMSManager() *KMSManager {
globalKMSMutex.RLock()
manager := globalKMSManager
globalKMSMutex.RUnlock()
if manager == nil {
return InitializeKMSManager()
}
return manager
}
// AddKMSProvider adds a KMS provider configuration
func (km *KMSManager) AddKMSProvider(name string, config *KMSConfig) error {
if name == "" {
return fmt.Errorf("provider name cannot be empty")
}
if config == nil {
return fmt.Errorf("KMS configuration cannot be nil")
}
km.mu.Lock()
defer km.mu.Unlock()
// Close existing provider if it exists
if existingProvider, exists := km.providers[name]; exists {
if err := existingProvider.Close(); err != nil {
glog.Errorf("Failed to close existing KMS provider %s: %v", name, err)
}
}
// Create new provider instance
configAdapter := &configAdapter{config: config.Config}
provider, err := GetProvider(config.Provider, configAdapter)
if err != nil {
return fmt.Errorf("failed to create KMS provider %s: %w", name, err)
}
// Wrap with a decrypted-data-key cache when enabled so repeated reads of
// the same SSE-KMS object avoid a KMS round-trip per read.
provider = wrapWithCacheIfEnabled(provider, config)
// Store provider and configuration
km.providers[name] = provider
km.configs[name] = config
glog.V(1).Infof("Added KMS provider %s (type: %s)", name, config.Provider)
return nil
}
// SetDefaultKMSProvider sets the default KMS provider
func (km *KMSManager) SetDefaultKMSProvider(name string) error {
km.mu.RLock()
_, exists := km.providers[name]
km.mu.RUnlock()
if !exists {
return fmt.Errorf("KMS provider %s does not exist", name)
}
km.mu.Lock()
km.defaultKMS = name
km.mu.Unlock()
glog.V(1).Infof("Set default KMS provider to %s", name)
return nil
}
// SetBucketKMSProvider sets the KMS provider for a specific bucket
func (km *KMSManager) SetBucketKMSProvider(bucket, providerName string) error {
if bucket == "" {
return fmt.Errorf("bucket name cannot be empty")
}
km.mu.RLock()
_, exists := km.providers[providerName]
km.mu.RUnlock()
if !exists {
return fmt.Errorf("KMS provider %s does not exist", providerName)
}
km.mu.Lock()
km.bucketKMS[bucket] = providerName
km.mu.Unlock()
glog.V(2).Infof("Set KMS provider for bucket %s to %s", bucket, providerName)
return nil
}
// GetKMSProvider returns the KMS provider for a bucket (or default if not configured)
func (km *KMSManager) GetKMSProvider(bucket string) (KMSProvider, error) {
km.mu.RLock()
defer km.mu.RUnlock()
// Try bucket-specific provider first
if bucket != "" {
if providerName, exists := km.bucketKMS[bucket]; exists {
if provider, exists := km.providers[providerName]; exists {
return provider, nil
}
}
}
// Fall back to default provider
if km.defaultKMS != "" {
if provider, exists := km.providers[km.defaultKMS]; exists {
return provider, nil
}
}
// No provider configured
return nil, fmt.Errorf("no KMS provider configured for bucket %s", bucket)
}
// GetKMSProviderByName returns a specific KMS provider by name
func (km *KMSManager) GetKMSProviderByName(name string) (KMSProvider, error) {
km.mu.RLock()
defer km.mu.RUnlock()
provider, exists := km.providers[name]
if !exists {
return nil, fmt.Errorf("KMS provider %s not found", name)
}
return provider, nil
}
// ListKMSProviders returns all configured KMS provider names
func (km *KMSManager) ListKMSProviders() []string {
km.mu.RLock()
defer km.mu.RUnlock()
names := make([]string, 0, len(km.providers))
for name := range km.providers {
names = append(names, name)
}
return names
}
// GetBucketKMSProvider returns the KMS provider name for a bucket
func (km *KMSManager) GetBucketKMSProvider(bucket string) string {
km.mu.RLock()
defer km.mu.RUnlock()
if providerName, exists := km.bucketKMS[bucket]; exists {
return providerName
}
return km.defaultKMS
}
// RemoveKMSProvider removes a KMS provider
func (km *KMSManager) RemoveKMSProvider(name string) error {
km.mu.Lock()
defer km.mu.Unlock()
provider, exists := km.providers[name]
if !exists {
return fmt.Errorf("KMS provider %s does not exist", name)
}
// Close the provider
if err := provider.Close(); err != nil {
glog.Errorf("Failed to close KMS provider %s: %v", name, err)
}
// Remove from maps
delete(km.providers, name)
delete(km.configs, name)
// Remove from bucket associations
for bucket, providerName := range km.bucketKMS {
if providerName == name {
delete(km.bucketKMS, bucket)
}
}
// Clear default if it was this provider
if km.defaultKMS == name {
km.defaultKMS = ""
}
glog.V(1).Infof("Removed KMS provider %s", name)
return nil
}
// Close closes all KMS providers and cleans up resources
func (km *KMSManager) Close() error {
km.mu.Lock()
defer km.mu.Unlock()
var allErrors []error
for name, provider := range km.providers {
if err := provider.Close(); err != nil {
allErrors = append(allErrors, fmt.Errorf("failed to close KMS provider %s: %w", name, err))
}
}
// Clear all maps
km.providers = make(map[string]KMSProvider)
km.configs = make(map[string]*KMSConfig)
km.bucketKMS = make(map[string]string)
km.defaultKMS = ""
if len(allErrors) > 0 {
return fmt.Errorf("errors closing KMS providers: %v", allErrors)
}
glog.V(1).Infof("KMS Manager closed")
return nil
}
// GenerateDataKeyForBucket generates a data key using the appropriate KMS provider for a bucket
func (km *KMSManager) GenerateDataKeyForBucket(ctx context.Context, bucket, keyID string, keySpec KeySpec, encryptionContext map[string]string) (*GenerateDataKeyResponse, error) {
provider, err := km.GetKMSProvider(bucket)
if err != nil {
return nil, fmt.Errorf("failed to get KMS provider for bucket %s: %w", bucket, err)
}
req := &GenerateDataKeyRequest{
KeyID: keyID,
KeySpec: keySpec,
EncryptionContext: encryptionContext,
}
return provider.GenerateDataKey(ctx, req)
}
// DecryptForBucket decrypts a data key using the appropriate KMS provider for a bucket
func (km *KMSManager) DecryptForBucket(ctx context.Context, bucket string, ciphertextBlob []byte, encryptionContext map[string]string) (*DecryptResponse, error) {
provider, err := km.GetKMSProvider(bucket)
if err != nil {
return nil, fmt.Errorf("failed to get KMS provider for bucket %s: %w", bucket, err)
}
req := &DecryptRequest{
CiphertextBlob: ciphertextBlob,
EncryptionContext: encryptionContext,
}
return provider.Decrypt(ctx, req)
}
// ValidateKeyForBucket validates that a KMS key exists and is usable for a bucket
func (km *KMSManager) ValidateKeyForBucket(ctx context.Context, bucket, keyID string) error {
provider, err := km.GetKMSProvider(bucket)
if err != nil {
return fmt.Errorf("failed to get KMS provider for bucket %s: %w", bucket, err)
}
req := &DescribeKeyRequest{KeyID: keyID}
resp, err := provider.DescribeKey(ctx, req)
if err != nil {
return fmt.Errorf("failed to validate key %s for bucket %s: %w", keyID, bucket, err)
}
// Check key state
if resp.KeyState != KeyStateEnabled {
return fmt.Errorf("key %s is not enabled (state: %s)", keyID, resp.KeyState)
}
// Check key usage
if resp.KeyUsage != KeyUsageEncryptDecrypt && resp.KeyUsage != KeyUsageGenerateDataKey {
return fmt.Errorf("key %s cannot be used for encryption (usage: %s)", keyID, resp.KeyUsage)
}
return nil
}
// GetKMSHealth returns health status of all KMS providers
func (km *KMSManager) GetKMSHealth(ctx context.Context) map[string]error {
km.mu.RLock()
defer km.mu.RUnlock()
health := make(map[string]error)
for name, provider := range km.providers {
// Try to perform a basic operation to check health
// We'll use DescribeKey with a dummy key - the error will tell us if KMS is reachable
req := &DescribeKeyRequest{KeyID: "health-check-dummy-key"}
_, err := provider.DescribeKey(ctx, req)
// If it's a "not found" error, KMS is healthy but key doesn't exist (expected)
if kmsErr, ok := err.(*KMSError); ok && kmsErr.Code == ErrCodeNotFoundException {
health[name] = nil // Healthy
} else if err != nil {
health[name] = err // Unhealthy
} else {
health[name] = nil // Healthy (shouldn't happen with dummy key, but just in case)
}
}
return health
}

408
weed/kms/config_loader.go Normal file
View File

@@ -0,0 +1,408 @@
package kms
import (
"context"
"fmt"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
)
// ViperConfig interface extends Configuration with additional methods needed for KMS configuration
type ViperConfig interface {
GetString(key string) string
GetBool(key string) bool
GetInt(key string) int
GetStringSlice(key string) []string
SetDefault(key string, value interface{})
GetStringMap(key string) map[string]interface{}
IsSet(key string) bool
}
// ConfigLoader handles loading KMS configurations from filer.toml
type ConfigLoader struct {
viper ViperConfig
manager *KMSManager
}
// NewConfigLoader creates a new KMS configuration loader
func NewConfigLoader(v ViperConfig) *ConfigLoader {
return &ConfigLoader{
viper: v,
manager: GetKMSManager(),
}
}
// LoadConfigurations loads all KMS provider configurations from filer.toml
func (loader *ConfigLoader) LoadConfigurations() error {
// Check if KMS section exists
if !loader.viper.IsSet("kms") {
glog.V(1).Infof("No KMS configuration found in filer.toml")
return nil
}
// Get the KMS configuration section
kmsConfig := loader.viper.GetStringMap("kms")
// Load global KMS settings
if err := loader.loadGlobalKMSSettings(kmsConfig); err != nil {
return fmt.Errorf("failed to load global KMS settings: %w", err)
}
// Load KMS providers
if providersConfig, exists := kmsConfig["providers"]; exists {
if providers, ok := providersConfig.(map[string]interface{}); ok {
if err := loader.loadKMSProviders(providers); err != nil {
return fmt.Errorf("failed to load KMS providers: %w", err)
}
}
}
// Set default provider after all providers are loaded
if err := loader.setDefaultProvider(); err != nil {
return fmt.Errorf("failed to set default KMS provider: %w", err)
}
// Initialize global KMS provider for backwards compatibility
if err := loader.initializeGlobalKMSProvider(); err != nil {
glog.Warningf("Failed to initialize global KMS provider: %v", err)
}
// Load bucket-specific KMS configurations
if bucketsConfig, exists := kmsConfig["buckets"]; exists {
if buckets, ok := bucketsConfig.(map[string]interface{}); ok {
if err := loader.loadBucketKMSConfigurations(buckets); err != nil {
return fmt.Errorf("failed to load bucket KMS configurations: %w", err)
}
}
}
glog.V(1).Infof("KMS configuration loaded successfully")
return nil
}
// loadGlobalKMSSettings loads global KMS settings
func (loader *ConfigLoader) loadGlobalKMSSettings(kmsConfig map[string]interface{}) error {
// Set default KMS provider if specified
if defaultProvider, exists := kmsConfig["default_provider"]; exists {
if providerName, ok := defaultProvider.(string); ok {
// We'll set this after providers are loaded
glog.V(2).Infof("Default KMS provider will be set to: %s", providerName)
}
}
return nil
}
// loadKMSProviders loads individual KMS provider configurations
func (loader *ConfigLoader) loadKMSProviders(providers map[string]interface{}) error {
for providerName, providerConfigInterface := range providers {
providerConfig, ok := providerConfigInterface.(map[string]interface{})
if !ok {
glog.Warningf("Invalid configuration for KMS provider %s", providerName)
continue
}
if err := loader.loadSingleKMSProvider(providerName, providerConfig); err != nil {
glog.Errorf("Failed to load KMS provider %s: %v", providerName, err)
continue
}
glog.V(1).Infof("Loaded KMS provider: %s", providerName)
}
return nil
}
// loadSingleKMSProvider loads a single KMS provider configuration
func (loader *ConfigLoader) loadSingleKMSProvider(providerName string, config map[string]interface{}) error {
// Get provider type
providerType, exists := config["type"]
if !exists {
return fmt.Errorf("provider type not specified for %s", providerName)
}
providerTypeStr, ok := providerType.(string)
if !ok {
return fmt.Errorf("invalid provider type for %s", providerName)
}
// Get provider-specific configuration
providerConfig := make(map[string]interface{})
for key, value := range config {
if key != "type" {
providerConfig[key] = value
}
}
// Set default cache settings if not specified
if _, exists := providerConfig["cache_enabled"]; !exists {
providerConfig["cache_enabled"] = true
}
if _, exists := providerConfig["cache_ttl"]; !exists {
providerConfig["cache_ttl"] = "1h"
}
if _, exists := providerConfig["max_cache_size"]; !exists {
providerConfig["max_cache_size"] = 1000
}
// Parse cache TTL
cacheTTL := time.Hour // default
if ttlStr, exists := providerConfig["cache_ttl"]; exists {
if ttlStrValue, ok := ttlStr.(string); ok {
if parsed, err := time.ParseDuration(ttlStrValue); err == nil {
cacheTTL = parsed
}
}
}
// Create KMS configuration
kmsConfig := &KMSConfig{
Provider: providerTypeStr,
Config: providerConfig,
CacheEnabled: getBoolFromConfig(providerConfig, "cache_enabled", true),
CacheTTL: cacheTTL,
MaxCacheSize: getIntFromConfig(providerConfig, "max_cache_size", 1000),
}
// Add the provider to the KMS manager
if err := loader.manager.AddKMSProvider(providerName, kmsConfig); err != nil {
return err
}
// Test the provider with a health check
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
health := loader.manager.GetKMSHealth(ctx)
if providerHealth, exists := health[providerName]; exists && providerHealth != nil {
glog.Warningf("KMS provider %s health check failed: %v", providerName, providerHealth)
}
return nil
}
// loadBucketKMSConfigurations loads bucket-specific KMS configurations
func (loader *ConfigLoader) loadBucketKMSConfigurations(buckets map[string]interface{}) error {
for bucketName, bucketConfigInterface := range buckets {
bucketConfig, ok := bucketConfigInterface.(map[string]interface{})
if !ok {
glog.Warningf("Invalid KMS configuration for bucket %s", bucketName)
continue
}
// Get provider for this bucket
if provider, exists := bucketConfig["provider"]; exists {
if providerName, ok := provider.(string); ok {
if err := loader.manager.SetBucketKMSProvider(bucketName, providerName); err != nil {
glog.Errorf("Failed to set KMS provider for bucket %s: %v", bucketName, err)
continue
}
glog.V(2).Infof("Set KMS provider for bucket %s to %s", bucketName, providerName)
}
}
}
return nil
}
// setDefaultProvider sets the default KMS provider after all providers are loaded
func (loader *ConfigLoader) setDefaultProvider() error {
kmsConfig := loader.viper.GetStringMap("kms")
if defaultProvider, exists := kmsConfig["default_provider"]; exists {
if providerName, ok := defaultProvider.(string); ok {
if err := loader.manager.SetDefaultKMSProvider(providerName); err != nil {
return fmt.Errorf("failed to set default KMS provider: %w", err)
}
glog.V(1).Infof("Set default KMS provider to: %s", providerName)
}
}
return nil
}
// initializeGlobalKMSProvider initializes the global KMS provider for backwards compatibility
func (loader *ConfigLoader) initializeGlobalKMSProvider() error {
// Get the default provider from the manager
defaultProviderName := ""
kmsConfig := loader.viper.GetStringMap("kms")
if defaultProvider, exists := kmsConfig["default_provider"]; exists {
if providerName, ok := defaultProvider.(string); ok {
defaultProviderName = providerName
}
}
if defaultProviderName == "" {
// If no default provider, try to use the first available provider
providers := loader.manager.ListKMSProviders()
if len(providers) > 0 {
defaultProviderName = providers[0]
}
}
if defaultProviderName == "" {
glog.V(2).Infof("No KMS providers configured, skipping global KMS initialization")
return nil
}
// Get the provider from the manager
provider, err := loader.manager.GetKMSProviderByName(defaultProviderName)
if err != nil {
return fmt.Errorf("failed to get KMS provider %s: %w", defaultProviderName, err)
}
// Set as global KMS provider
SetGlobalKMSProvider(provider)
glog.V(1).Infof("Initialized global KMS provider: %s", defaultProviderName)
return nil
}
// ValidateConfiguration validates the KMS configuration
func (loader *ConfigLoader) ValidateConfiguration() error {
providers := loader.manager.ListKMSProviders()
if len(providers) == 0 {
glog.V(1).Infof("No KMS providers configured")
return nil
}
// Test connectivity to all providers
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
health := loader.manager.GetKMSHealth(ctx)
hasHealthyProvider := false
for providerName, err := range health {
if err != nil {
glog.Warningf("KMS provider %s is unhealthy: %v", providerName, err)
} else {
hasHealthyProvider = true
glog.V(2).Infof("KMS provider %s is healthy", providerName)
}
}
if !hasHealthyProvider {
glog.Warningf("No healthy KMS providers found")
}
return nil
}
// LoadKMSFromConfig loads KMS configuration directly from parsed JSON data
func LoadKMSFromConfig(kmsConfig interface{}) error {
kmsMap, ok := kmsConfig.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid KMS configuration format")
}
// Create a direct config adapter that doesn't use Viper
// Wrap the KMS config under a "kms" key as expected by LoadConfigurations
wrappedConfig := map[string]interface{}{
"kms": kmsMap,
}
adapter := &directConfigAdapter{config: wrappedConfig}
loader := NewConfigLoader(adapter)
if err := loader.LoadConfigurations(); err != nil {
return err
}
return loader.ValidateConfiguration()
}
// directConfigAdapter implements ViperConfig interface for direct map access
type directConfigAdapter struct {
config map[string]interface{}
}
func (d *directConfigAdapter) GetStringMap(key string) map[string]interface{} {
if val, exists := d.config[key]; exists {
if mapVal, ok := val.(map[string]interface{}); ok {
return mapVal
}
}
return make(map[string]interface{})
}
func (d *directConfigAdapter) GetString(key string) string {
if val, exists := d.config[key]; exists {
if strVal, ok := val.(string); ok {
return strVal
}
}
return ""
}
func (d *directConfigAdapter) GetBool(key string) bool {
if val, exists := d.config[key]; exists {
if boolVal, ok := val.(bool); ok {
return boolVal
}
}
return false
}
func (d *directConfigAdapter) GetInt(key string) int {
if val, exists := d.config[key]; exists {
switch v := val.(type) {
case int:
return v
case float64:
return int(v)
}
}
return 0
}
func (d *directConfigAdapter) GetStringSlice(key string) []string {
if val, exists := d.config[key]; exists {
if sliceVal, ok := val.([]interface{}); ok {
result := make([]string, len(sliceVal))
for i, item := range sliceVal {
if strItem, ok := item.(string); ok {
result[i] = strItem
}
}
return result
}
if strSlice, ok := val.([]string); ok {
return strSlice
}
}
return []string{}
}
func (d *directConfigAdapter) SetDefault(key string, value interface{}) {
// For direct config adapter, we don't need to set defaults
// as the configuration is already parsed
}
func (d *directConfigAdapter) IsSet(key string) bool {
_, exists := d.config[key]
return exists
}
// Helper functions
func getBoolFromConfig(config map[string]interface{}, key string, defaultValue bool) bool {
if value, exists := config[key]; exists {
if boolValue, ok := value.(bool); ok {
return boolValue
}
}
return defaultValue
}
func getIntFromConfig(config map[string]interface{}, key string, defaultValue int) int {
if value, exists := config[key]; exists {
if intValue, ok := value.(int); ok {
return intValue
}
if floatValue, ok := value.(float64); ok {
return int(floatValue)
}
}
return defaultValue
}

79
weed/kms/envelope.go Normal file
View File

@@ -0,0 +1,79 @@
package kms
import (
"encoding/json"
"fmt"
)
// CiphertextEnvelope represents a standardized format for storing encrypted data
// along with the metadata needed for decryption. This ensures consistent API
// behavior across all KMS providers.
type CiphertextEnvelope struct {
// Provider identifies which KMS provider was used
Provider string `json:"provider"`
// KeyID is the identifier of the key used for encryption
KeyID string `json:"key_id"`
// Ciphertext is the encrypted data (base64 encoded for JSON compatibility)
Ciphertext string `json:"ciphertext"`
// Version allows for future format changes
Version int `json:"version"`
// ProviderSpecific contains provider-specific metadata if needed
ProviderSpecific map[string]interface{} `json:"provider_specific,omitempty"`
}
// CreateEnvelope creates a ciphertext envelope for consistent KMS provider behavior
func CreateEnvelope(provider, keyID, ciphertext string, providerSpecific map[string]interface{}) ([]byte, error) {
// Validate required fields
if provider == "" {
return nil, fmt.Errorf("provider cannot be empty")
}
if keyID == "" {
return nil, fmt.Errorf("keyID cannot be empty")
}
if ciphertext == "" {
return nil, fmt.Errorf("ciphertext cannot be empty")
}
envelope := CiphertextEnvelope{
Provider: provider,
KeyID: keyID,
Ciphertext: ciphertext,
Version: 1,
ProviderSpecific: providerSpecific,
}
return json.Marshal(envelope)
}
// ParseEnvelope parses a ciphertext envelope to extract key information
func ParseEnvelope(ciphertextBlob []byte) (*CiphertextEnvelope, error) {
if len(ciphertextBlob) == 0 {
return nil, fmt.Errorf("ciphertext blob cannot be empty")
}
// Parse as envelope format
var envelope CiphertextEnvelope
if err := json.Unmarshal(ciphertextBlob, &envelope); err != nil {
return nil, fmt.Errorf("failed to parse ciphertext envelope: %w", err)
}
// Validate required fields
if envelope.Provider == "" {
return nil, fmt.Errorf("envelope missing provider field")
}
if envelope.KeyID == "" {
return nil, fmt.Errorf("envelope missing key_id field")
}
if envelope.Ciphertext == "" {
return nil, fmt.Errorf("envelope missing ciphertext field")
}
if envelope.Version == 0 {
envelope.Version = 1 // Default to version 1
}
return &envelope, nil
}

138
weed/kms/envelope_test.go Normal file
View File

@@ -0,0 +1,138 @@
package kms
import (
"encoding/json"
"testing"
)
func TestCiphertextEnvelope_CreateAndParse(t *testing.T) {
// Test basic envelope creation and parsing
provider := "openbao"
keyID := "test-key-123"
ciphertext := "vault:v1:abcd1234encrypted"
providerSpecific := map[string]interface{}{
"transit_path": "transit",
"version": 1,
}
// Create envelope
envelopeBlob, err := CreateEnvelope(provider, keyID, ciphertext, providerSpecific)
if err != nil {
t.Fatalf("CreateEnvelope failed: %v", err)
}
// Verify it's valid JSON
var jsonCheck map[string]interface{}
if err := json.Unmarshal(envelopeBlob, &jsonCheck); err != nil {
t.Fatalf("Envelope is not valid JSON: %v", err)
}
// Parse envelope back
envelope, err := ParseEnvelope(envelopeBlob)
if err != nil {
t.Fatalf("ParseEnvelope failed: %v", err)
}
// Verify fields
if envelope.Provider != provider {
t.Errorf("Provider mismatch: expected %s, got %s", provider, envelope.Provider)
}
if envelope.KeyID != keyID {
t.Errorf("KeyID mismatch: expected %s, got %s", keyID, envelope.KeyID)
}
if envelope.Ciphertext != ciphertext {
t.Errorf("Ciphertext mismatch: expected %s, got %s", ciphertext, envelope.Ciphertext)
}
if envelope.Version != 1 {
t.Errorf("Version mismatch: expected 1, got %d", envelope.Version)
}
if envelope.ProviderSpecific == nil {
t.Error("ProviderSpecific is nil")
}
}
func TestCiphertextEnvelope_InvalidFormat(t *testing.T) {
// Test parsing invalid (non-envelope) ciphertext should fail
rawCiphertext := []byte("some-raw-data-not-json")
_, err := ParseEnvelope(rawCiphertext)
if err == nil {
t.Fatal("Expected error for invalid format, got none")
}
}
func TestCiphertextEnvelope_ValidationErrors(t *testing.T) {
// Test validation errors
testCases := []struct {
name string
provider string
keyID string
ciphertext string
expectError bool
}{
{"Valid", "openbao", "key1", "cipher1", false},
{"Empty provider", "", "key1", "cipher1", true},
{"Empty keyID", "openbao", "", "cipher1", true},
{"Empty ciphertext", "openbao", "key1", "", true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
envelopeBlob, err := CreateEnvelope(tc.provider, tc.keyID, tc.ciphertext, nil)
if err != nil && !tc.expectError {
t.Fatalf("Unexpected error in CreateEnvelope: %v", err)
}
if err == nil && tc.expectError {
t.Fatal("Expected error in CreateEnvelope but got none")
}
if !tc.expectError {
// Test parsing as well
_, err = ParseEnvelope(envelopeBlob)
if err != nil {
t.Fatalf("ParseEnvelope failed: %v", err)
}
}
})
}
}
func TestCiphertextEnvelope_MultipleProviders(t *testing.T) {
// Test with different providers to ensure API consistency
providers := []struct {
name string
keyID string
ciphertext string
}{
{"openbao", "transit/test-key", "vault:v1:encrypted123"},
{"gcp", "projects/test/locations/us/keyRings/ring/cryptoKeys/key", "gcp-encrypted-data"},
{"azure", "https://vault.vault.azure.net/keys/test/123", "azure-encrypted-bytes"},
{"aws", "arn:aws:kms:us-east-1:123:key/abc", "aws-encrypted-blob"},
}
for _, provider := range providers {
t.Run(provider.name, func(t *testing.T) {
// Create envelope
envelopeBlob, err := CreateEnvelope(provider.name, provider.keyID, provider.ciphertext, nil)
if err != nil {
t.Fatalf("CreateEnvelope failed for %s: %v", provider.name, err)
}
// Parse envelope
envelope, err := ParseEnvelope(envelopeBlob)
if err != nil {
t.Fatalf("ParseEnvelope failed for %s: %v", provider.name, err)
}
// Verify consistency
if envelope.Provider != provider.name {
t.Errorf("Provider mismatch for %s: expected %s, got %s",
provider.name, provider.name, envelope.Provider)
}
if envelope.KeyID != provider.keyID {
t.Errorf("KeyID mismatch for %s: expected %s, got %s",
provider.name, provider.keyID, envelope.KeyID)
}
})
}
}

349
weed/kms/gcp/gcp_kms.go Normal file
View File

@@ -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,
}
}

159
weed/kms/kms.go Normal file
View File

@@ -0,0 +1,159 @@
package kms
import (
"context"
"fmt"
)
// KMSProvider defines the interface for Key Management Service implementations
type KMSProvider interface {
// GenerateDataKey creates a new data encryption key encrypted under the specified KMS key
GenerateDataKey(ctx context.Context, req *GenerateDataKeyRequest) (*GenerateDataKeyResponse, error)
// Decrypt decrypts an encrypted data key using the KMS
Decrypt(ctx context.Context, req *DecryptRequest) (*DecryptResponse, error)
// DescribeKey validates that a key exists and returns its metadata
DescribeKey(ctx context.Context, req *DescribeKeyRequest) (*DescribeKeyResponse, error)
// GetKeyID resolves a key alias or ARN to the actual key ID
GetKeyID(ctx context.Context, keyIdentifier string) (string, error)
// Close cleans up any resources used by the provider
Close() error
}
// GenerateDataKeyRequest contains parameters for generating a data key
type GenerateDataKeyRequest struct {
KeyID string // KMS key identifier (ID, ARN, or alias)
KeySpec KeySpec // Specification for the data key
EncryptionContext map[string]string // Additional authenticated data
}
// GenerateDataKeyResponse contains the generated data key
type GenerateDataKeyResponse struct {
KeyID string // The actual KMS key ID used
Plaintext []byte // The plaintext data key (sensitive - clear from memory ASAP)
CiphertextBlob []byte // The encrypted data key for storage
}
// DecryptRequest contains parameters for decrypting a data key
type DecryptRequest struct {
CiphertextBlob []byte // The encrypted data key
EncryptionContext map[string]string // Must match the context used during encryption
}
// DecryptResponse contains the decrypted data key
type DecryptResponse struct {
KeyID string // The KMS key ID that was used for encryption
Plaintext []byte // The decrypted data key (sensitive - clear from memory ASAP)
}
// DescribeKeyRequest contains parameters for describing a key
type DescribeKeyRequest struct {
KeyID string // KMS key identifier (ID, ARN, or alias)
}
// DescribeKeyResponse contains key metadata
type DescribeKeyResponse struct {
KeyID string // The actual key ID
ARN string // The key ARN
Description string // Key description
KeyUsage KeyUsage // How the key can be used
KeyState KeyState // Current state of the key
Origin KeyOrigin // Where the key material originated
}
// KeySpec specifies the type of data key to generate
type KeySpec string
const (
KeySpecAES256 KeySpec = "AES_256" // 256-bit AES key
)
// KeyUsage specifies how a key can be used
type KeyUsage string
const (
KeyUsageEncryptDecrypt KeyUsage = "ENCRYPT_DECRYPT"
KeyUsageGenerateDataKey KeyUsage = "GENERATE_DATA_KEY"
)
// KeyState represents the current state of a KMS key
type KeyState string
const (
KeyStateEnabled KeyState = "Enabled"
KeyStateDisabled KeyState = "Disabled"
KeyStatePendingDeletion KeyState = "PendingDeletion"
KeyStateUnavailable KeyState = "Unavailable"
)
// KeyOrigin indicates where the key material came from
type KeyOrigin string
const (
KeyOriginAWS KeyOrigin = "AWS_KMS"
KeyOriginExternal KeyOrigin = "EXTERNAL"
KeyOriginCloudHSM KeyOrigin = "AWS_CLOUDHSM"
KeyOriginAzure KeyOrigin = "AZURE_KEY_VAULT"
KeyOriginGCP KeyOrigin = "GCP_KMS"
KeyOriginOpenBao KeyOrigin = "OPENBAO"
KeyOriginLocal KeyOrigin = "LOCAL"
)
// KMSError represents an error from the KMS service
type KMSError struct {
Code string // Error code (e.g., "KeyUnavailableException")
Message string // Human-readable error message
KeyID string // Key ID that caused the error (if applicable)
}
func (e *KMSError) Error() string {
if e.KeyID != "" {
return fmt.Sprintf("KMS error %s for key %s: %s", e.Code, e.KeyID, e.Message)
}
return fmt.Sprintf("KMS error %s: %s", e.Code, e.Message)
}
// Common KMS error codes
const (
ErrCodeKeyUnavailable = "KeyUnavailableException"
ErrCodeAccessDenied = "AccessDeniedException"
ErrCodeNotFoundException = "NotFoundException"
ErrCodeInvalidKeyUsage = "InvalidKeyUsageException"
ErrCodeKMSInternalFailure = "KMSInternalException"
ErrCodeInvalidCiphertext = "InvalidCiphertextException"
)
// EncryptionContextKey constants for building encryption context
const (
EncryptionContextS3ARN = "aws:s3:arn"
EncryptionContextS3Bucket = "aws:s3:bucket"
EncryptionContextS3Object = "aws:s3:object"
)
// BuildS3EncryptionContext creates the standard encryption context for S3 objects
// Following AWS S3 conventions from the documentation
func BuildS3EncryptionContext(bucketName, objectKey string, useBucketKey bool) map[string]string {
context := make(map[string]string)
if useBucketKey {
// When using S3 Bucket Keys, use bucket ARN as encryption context
context[EncryptionContextS3ARN] = fmt.Sprintf("arn:aws:s3:::%s", bucketName)
} else {
// For individual object encryption, use object ARN as encryption context
context[EncryptionContextS3ARN] = fmt.Sprintf("arn:aws:s3:::%s/%s", bucketName, objectKey)
}
return context
}
// ClearSensitiveData securely clears sensitive byte slices
func ClearSensitiveData(data []byte) {
if data != nil {
for i := range data {
data[i] = 0
}
}
}

568
weed/kms/local/local_kms.go Normal file
View File

@@ -0,0 +1,568 @@
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
}

View File

@@ -0,0 +1,403 @@
package openbao
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"time"
vault "github.com/hashicorp/vault/api"
"github.com/seaweedfs/seaweedfs/weed/glog"
seaweedkms "github.com/seaweedfs/seaweedfs/weed/kms"
"github.com/seaweedfs/seaweedfs/weed/util"
)
func init() {
// Register the OpenBao/Vault KMS provider
seaweedkms.RegisterProvider("openbao", NewOpenBaoKMSProvider)
seaweedkms.RegisterProvider("vault", NewOpenBaoKMSProvider) // Alias for compatibility
}
// OpenBaoKMSProvider implements the KMSProvider interface using OpenBao/Vault Transit engine
type OpenBaoKMSProvider struct {
client *vault.Client
transitPath string // Transit engine mount path (default: "transit")
address string
}
// OpenBaoKMSConfig contains configuration for the OpenBao/Vault KMS provider
type OpenBaoKMSConfig struct {
Address string `json:"address"` // Vault address (e.g., "http://localhost:8200")
Token string `json:"token"` // Vault token for authentication
RoleID string `json:"role_id"` // AppRole role ID (alternative to token)
SecretID string `json:"secret_id"` // AppRole secret ID (alternative to token)
TransitPath string `json:"transit_path"` // Transit engine mount path (default: "transit")
TLSSkipVerify bool `json:"tls_skip_verify"` // Skip TLS verification (for testing)
CACert string `json:"ca_cert"` // Path to CA certificate
ClientCert string `json:"client_cert"` // Path to client certificate
ClientKey string `json:"client_key"` // Path to client private key
RequestTimeout int `json:"request_timeout"` // Request timeout in seconds (default: 30)
}
// NewOpenBaoKMSProvider creates a new OpenBao/Vault KMS provider
func NewOpenBaoKMSProvider(config util.Configuration) (seaweedkms.KMSProvider, error) {
if config == nil {
return nil, fmt.Errorf("OpenBao/Vault KMS configuration is required")
}
// Extract configuration
address := config.GetString("address")
if address == "" {
address = "http://localhost:8200" // Default OpenBao address
}
token := config.GetString("token")
roleID := config.GetString("role_id")
secretID := config.GetString("secret_id")
transitPath := config.GetString("transit_path")
if transitPath == "" {
transitPath = "transit" // Default transit path
}
tlsSkipVerify := config.GetBool("tls_skip_verify")
caCert := config.GetString("ca_cert")
clientCert := config.GetString("client_cert")
clientKey := config.GetString("client_key")
requestTimeout := config.GetInt("request_timeout")
if requestTimeout == 0 {
requestTimeout = 30 // Default 30 seconds
}
// Create Vault client configuration
vaultConfig := vault.DefaultConfig()
vaultConfig.Address = address
vaultConfig.Timeout = time.Duration(requestTimeout) * time.Second
// Configure TLS
if tlsSkipVerify || caCert != "" || (clientCert != "" && clientKey != "") {
tlsConfig := &vault.TLSConfig{
Insecure: tlsSkipVerify,
}
if caCert != "" {
tlsConfig.CACert = caCert
}
if clientCert != "" && clientKey != "" {
tlsConfig.ClientCert = clientCert
tlsConfig.ClientKey = clientKey
}
if err := vaultConfig.ConfigureTLS(tlsConfig); err != nil {
return nil, fmt.Errorf("failed to configure TLS: %w", err)
}
}
// Create Vault client
client, err := vault.NewClient(vaultConfig)
if err != nil {
return nil, fmt.Errorf("failed to create OpenBao/Vault client: %w", err)
}
// Authenticate
if token != "" {
client.SetToken(token)
glog.V(1).Infof("OpenBao KMS: Using token authentication")
} else if roleID != "" && secretID != "" {
if err := authenticateAppRole(client, roleID, secretID); err != nil {
return nil, fmt.Errorf("failed to authenticate with AppRole: %w", err)
}
glog.V(1).Infof("OpenBao KMS: Using AppRole authentication")
} else {
return nil, fmt.Errorf("either token or role_id+secret_id must be provided")
}
provider := &OpenBaoKMSProvider{
client: client,
transitPath: transitPath,
address: address,
}
glog.V(1).Infof("OpenBao/Vault KMS provider initialized at %s", address)
return provider, nil
}
// authenticateAppRole authenticates using AppRole method
func authenticateAppRole(client *vault.Client, roleID, secretID string) error {
data := map[string]interface{}{
"role_id": roleID,
"secret_id": secretID,
}
secret, err := client.Logical().Write("auth/approle/login", data)
if err != nil {
return fmt.Errorf("AppRole authentication failed: %w", err)
}
if secret == nil || secret.Auth == nil {
return fmt.Errorf("AppRole authentication returned empty token")
}
client.SetToken(secret.Auth.ClientToken)
return nil
}
// GenerateDataKey generates a new data encryption key using OpenBao/Vault Transit
func (p *OpenBaoKMSProvider) 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 (similar to Azure/GCP approach)
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 OpenBao/Vault Transit
glog.V(4).Infof("OpenBao KMS: Encrypting data key using key %s", req.KeyID)
// Prepare encryption data
encryptData := map[string]interface{}{
"plaintext": base64.StdEncoding.EncodeToString(dataKey),
}
// Add encryption context if provided
if len(req.EncryptionContext) > 0 {
contextJSON, err := json.Marshal(req.EncryptionContext)
if err != nil {
return nil, fmt.Errorf("failed to marshal encryption context: %w", err)
}
encryptData["context"] = base64.StdEncoding.EncodeToString(contextJSON)
}
// Call OpenBao/Vault Transit encrypt endpoint
path := fmt.Sprintf("%s/encrypt/%s", p.transitPath, req.KeyID)
secret, err := p.client.Logical().WriteWithContext(ctx, path, encryptData)
if err != nil {
return nil, p.convertVaultError(err, req.KeyID)
}
if secret == nil || secret.Data == nil {
return nil, fmt.Errorf("no data returned from OpenBao/Vault encrypt operation")
}
ciphertext, ok := secret.Data["ciphertext"].(string)
if !ok {
return nil, fmt.Errorf("invalid ciphertext format from OpenBao/Vault")
}
// Create standardized envelope format for consistent API behavior
envelopeBlob, err := seaweedkms.CreateEnvelope("openbao", req.KeyID, ciphertext, nil)
if err != nil {
return nil, fmt.Errorf("failed to create ciphertext envelope: %w", err)
}
response := &seaweedkms.GenerateDataKeyResponse{
KeyID: req.KeyID,
Plaintext: dataKey,
CiphertextBlob: envelopeBlob, // Store in standardized envelope format
}
glog.V(4).Infof("OpenBao KMS: Generated and encrypted data key using key %s", req.KeyID)
return response, nil
}
// Decrypt decrypts an encrypted data key using OpenBao/Vault Transit
func (p *OpenBaoKMSProvider) 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)
}
keyID := envelope.KeyID
if keyID == "" {
return nil, fmt.Errorf("envelope missing key ID")
}
// Use the ciphertext from envelope
ciphertext := envelope.Ciphertext
// Prepare decryption data
decryptData := map[string]interface{}{
"ciphertext": ciphertext,
}
// Add encryption context if provided
if len(req.EncryptionContext) > 0 {
contextJSON, err := json.Marshal(req.EncryptionContext)
if err != nil {
return nil, fmt.Errorf("failed to marshal encryption context: %w", err)
}
decryptData["context"] = base64.StdEncoding.EncodeToString(contextJSON)
}
// Call OpenBao/Vault Transit decrypt endpoint
path := fmt.Sprintf("%s/decrypt/%s", p.transitPath, keyID)
glog.V(4).Infof("OpenBao KMS: Decrypting data key using key %s", keyID)
secret, err := p.client.Logical().WriteWithContext(ctx, path, decryptData)
if err != nil {
return nil, p.convertVaultError(err, keyID)
}
if secret == nil || secret.Data == nil {
return nil, fmt.Errorf("no data returned from OpenBao/Vault decrypt operation")
}
plaintextB64, ok := secret.Data["plaintext"].(string)
if !ok {
return nil, fmt.Errorf("invalid plaintext format from OpenBao/Vault")
}
plaintext, err := base64.StdEncoding.DecodeString(plaintextB64)
if err != nil {
return nil, fmt.Errorf("failed to decode plaintext from OpenBao/Vault: %w", err)
}
response := &seaweedkms.DecryptResponse{
KeyID: keyID,
Plaintext: plaintext,
}
glog.V(4).Infof("OpenBao KMS: Decrypted data key using key %s", keyID)
return response, nil
}
// DescribeKey validates that a key exists and returns its metadata
func (p *OpenBaoKMSProvider) 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")
}
// Get key information from OpenBao/Vault
path := fmt.Sprintf("%s/keys/%s", p.transitPath, req.KeyID)
glog.V(4).Infof("OpenBao KMS: Describing key %s", req.KeyID)
secret, err := p.client.Logical().ReadWithContext(ctx, path)
if err != nil {
return nil, p.convertVaultError(err, req.KeyID)
}
if secret == nil || secret.Data == nil {
return nil, &seaweedkms.KMSError{
Code: seaweedkms.ErrCodeNotFoundException,
Message: fmt.Sprintf("Key not found: %s", req.KeyID),
KeyID: req.KeyID,
}
}
response := &seaweedkms.DescribeKeyResponse{
KeyID: req.KeyID,
ARN: fmt.Sprintf("openbao:%s:key:%s", p.address, req.KeyID),
Description: "OpenBao/Vault Transit engine key",
}
// Check key type and set usage
if keyType, ok := secret.Data["type"].(string); ok {
if keyType == "aes256-gcm96" || keyType == "aes128-gcm96" || keyType == "chacha20-poly1305" {
response.KeyUsage = seaweedkms.KeyUsageEncryptDecrypt
} else {
// Default to data key generation if not an encrypt/decrypt type
response.KeyUsage = seaweedkms.KeyUsageGenerateDataKey
}
} else {
// If type is missing, default to data key generation
response.KeyUsage = seaweedkms.KeyUsageGenerateDataKey
}
// OpenBao/Vault keys are enabled by default (no disabled state in transit)
response.KeyState = seaweedkms.KeyStateEnabled
// Keys in OpenBao/Vault transit are service-managed
response.Origin = seaweedkms.KeyOriginOpenBao
glog.V(4).Infof("OpenBao KMS: Described key %s (state: %s)", req.KeyID, response.KeyState)
return response, nil
}
// GetKeyID resolves a key name (already the full key ID in OpenBao/Vault)
func (p *OpenBaoKMSProvider) GetKeyID(ctx context.Context, keyIdentifier string) (string, error) {
if keyIdentifier == "" {
return "", fmt.Errorf("key identifier cannot be empty")
}
// Use DescribeKey to validate the key exists
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 *OpenBaoKMSProvider) Close() error {
// OpenBao/Vault client doesn't require explicit cleanup
glog.V(2).Infof("OpenBao/Vault KMS provider closed")
return nil
}
// convertVaultError converts OpenBao/Vault errors to our standard KMS errors
func (p *OpenBaoKMSProvider) convertVaultError(err error, keyID string) error {
errMsg := err.Error()
if strings.Contains(errMsg, "not found") || strings.Contains(errMsg, "no handler") {
return &seaweedkms.KMSError{
Code: seaweedkms.ErrCodeNotFoundException,
Message: fmt.Sprintf("Key not found in OpenBao/Vault: %v", err),
KeyID: keyID,
}
}
if strings.Contains(errMsg, "permission") || strings.Contains(errMsg, "denied") || strings.Contains(errMsg, "forbidden") {
return &seaweedkms.KMSError{
Code: seaweedkms.ErrCodeAccessDenied,
Message: fmt.Sprintf("Access denied to OpenBao/Vault: %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 OpenBao/Vault: %v", err),
KeyID: keyID,
}
}
// For unknown errors, wrap as internal failure
return &seaweedkms.KMSError{
Code: seaweedkms.ErrCodeKMSInternalFailure,
Message: fmt.Sprintf("OpenBao/Vault error: %v", err),
KeyID: keyID,
}
}

145
weed/kms/registry.go Normal file
View File

@@ -0,0 +1,145 @@
package kms
import (
"context"
"errors"
"fmt"
"sync"
"github.com/seaweedfs/seaweedfs/weed/util"
)
// ProviderRegistry manages KMS provider implementations
type ProviderRegistry struct {
mu sync.RWMutex
providers map[string]ProviderFactory
instances map[string]KMSProvider
}
// ProviderFactory creates a new KMS provider instance
type ProviderFactory func(config util.Configuration) (KMSProvider, error)
var defaultRegistry = NewProviderRegistry()
// NewProviderRegistry creates a new provider registry
func NewProviderRegistry() *ProviderRegistry {
return &ProviderRegistry{
providers: make(map[string]ProviderFactory),
instances: make(map[string]KMSProvider),
}
}
// RegisterProvider registers a new KMS provider factory
func RegisterProvider(name string, factory ProviderFactory) {
defaultRegistry.RegisterProvider(name, factory)
}
// RegisterProvider registers a new KMS provider factory in this registry
func (r *ProviderRegistry) RegisterProvider(name string, factory ProviderFactory) {
r.mu.Lock()
defer r.mu.Unlock()
r.providers[name] = factory
}
// GetProvider returns a KMS provider instance, creating it if necessary
func GetProvider(name string, config util.Configuration) (KMSProvider, error) {
return defaultRegistry.GetProvider(name, config)
}
// GetProvider returns a KMS provider instance, creating it if necessary
func (r *ProviderRegistry) GetProvider(name string, config util.Configuration) (KMSProvider, error) {
r.mu.Lock()
defer r.mu.Unlock()
// Return existing instance if available
if instance, exists := r.instances[name]; exists {
return instance, nil
}
// Find the factory
factory, exists := r.providers[name]
if !exists {
return nil, fmt.Errorf("KMS provider '%s' not registered", name)
}
// Create new instance
instance, err := factory(config)
if err != nil {
return nil, fmt.Errorf("failed to create KMS provider '%s': %v", name, err)
}
// Cache the instance
r.instances[name] = instance
return instance, nil
}
// ListProviders returns the names of all registered providers
func ListProviders() []string {
return defaultRegistry.ListProviders()
}
// ListProviders returns the names of all registered providers
func (r *ProviderRegistry) ListProviders() []string {
r.mu.RLock()
defer r.mu.RUnlock()
names := make([]string, 0, len(r.providers))
for name := range r.providers {
names = append(names, name)
}
return names
}
// CloseAll closes all provider instances
func CloseAll() error {
return defaultRegistry.CloseAll()
}
// CloseAll closes all provider instances in this registry
func (r *ProviderRegistry) CloseAll() error {
r.mu.Lock()
defer r.mu.Unlock()
var allErrors []error
for name, instance := range r.instances {
if err := instance.Close(); err != nil {
allErrors = append(allErrors, fmt.Errorf("failed to close KMS provider '%s': %w", name, err))
}
}
// Clear the instances map
r.instances = make(map[string]KMSProvider)
return errors.Join(allErrors...)
}
// WithKMSProvider is a helper function to execute code with a KMS provider
func WithKMSProvider(name string, config util.Configuration, fn func(KMSProvider) error) error {
provider, err := GetProvider(name, config)
if err != nil {
return err
}
return fn(provider)
}
// TestKMSConnection tests the connection to a KMS provider
func TestKMSConnection(ctx context.Context, provider KMSProvider, testKeyID string) error {
if provider == nil {
return fmt.Errorf("KMS provider is nil")
}
// Try to describe a test key to verify connectivity
_, err := provider.DescribeKey(ctx, &DescribeKeyRequest{
KeyID: testKeyID,
})
if err != nil {
// If the key doesn't exist, that's still a successful connection test
if kmsErr, ok := err.(*KMSError); ok && kmsErr.Code == ErrCodeNotFoundException {
return nil
}
return fmt.Errorf("KMS connection test failed: %v", err)
}
return nil
}