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
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:
389
weed/kms/aws/aws_kms.go
Normal file
389
weed/kms/aws/aws_kms.go
Normal 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
379
weed/kms/azure/azure_kms.go
Normal 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
245
weed/kms/cache.go
Normal 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
273
weed/kms/cache_test.go
Normal 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
485
weed/kms/config.go
Normal 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
408
weed/kms/config_loader.go
Normal 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
79
weed/kms/envelope.go
Normal 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
138
weed/kms/envelope_test.go
Normal 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
349
weed/kms/gcp/gcp_kms.go
Normal 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
159
weed/kms/kms.go
Normal 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
568
weed/kms/local/local_kms.go
Normal 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
|
||||
}
|
||||
403
weed/kms/openbao/openbao_kms.go
Normal file
403
weed/kms/openbao/openbao_kms.go
Normal 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
145
weed/kms/registry.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user