You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1060 lines
36 KiB

package s3api
import (
"context"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"sort"
"strings"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/kms"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
)
// Compiled regex patterns for KMS key validation
var (
uuidRegex = regexp.MustCompile(`^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$`)
arnRegex = regexp.MustCompile(`^arn:aws:kms:[a-z0-9-]+:\d{12}:(key|alias)/.+$`)
)
// SSEKMSKey contains the metadata for an SSE-KMS encrypted object
type SSEKMSKey struct {
KeyID string // The KMS key ID used
EncryptedDataKey []byte // The encrypted data encryption key
EncryptionContext map[string]string // The encryption context used
BucketKeyEnabled bool // Whether S3 Bucket Keys are enabled
IV []byte // The initialization vector for encryption
ChunkOffset int64 // Offset of this chunk within the original part (for IV calculation)
}
// SSEKMSMetadata represents the metadata stored with SSE-KMS objects
type SSEKMSMetadata struct {
Algorithm string `json:"algorithm"` // "aws:kms"
KeyID string `json:"keyId"` // KMS key identifier
EncryptedDataKey string `json:"encryptedDataKey"` // Base64-encoded encrypted data key
EncryptionContext map[string]string `json:"encryptionContext"` // Encryption context
BucketKeyEnabled bool `json:"bucketKeyEnabled"` // S3 Bucket Key optimization
IV string `json:"iv"` // Base64-encoded initialization vector
PartOffset int64 `json:"partOffset"` // Offset within original multipart part (for IV calculation)
}
const (
// Default data key size (256 bits)
DataKeySize = 32
)
// Bucket key cache TTL (moved to be used with per-bucket cache)
const BucketKeyCacheTTL = time.Hour
// CreateSSEKMSEncryptedReader creates an encrypted reader using KMS envelope encryption
func CreateSSEKMSEncryptedReader(r io.Reader, keyID string, encryptionContext map[string]string) (io.Reader, *SSEKMSKey, error) {
return CreateSSEKMSEncryptedReaderWithBucketKey(r, keyID, encryptionContext, false)
}
// CreateSSEKMSEncryptedReaderWithBucketKey creates an encrypted reader with optional S3 Bucket Keys optimization
func CreateSSEKMSEncryptedReaderWithBucketKey(r io.Reader, keyID string, encryptionContext map[string]string, bucketKeyEnabled bool) (io.Reader, *SSEKMSKey, error) {
if bucketKeyEnabled {
// Use S3 Bucket Keys optimization - try to get or create a bucket-level data key
// Note: This is a simplified implementation. In practice, this would need
// access to the bucket name and S3ApiServer instance for proper per-bucket caching.
// For now, generate per-object keys (bucket key optimization disabled)
glog.V(2).Infof("Bucket key optimization requested but not fully implemented yet - using per-object keys")
bucketKeyEnabled = false
}
// Generate data key using common utility
dataKeyResult, err := generateKMSDataKey(keyID, encryptionContext)
if err != nil {
return nil, nil, err
}
// Ensure we clear the plaintext data key from memory when done
defer clearKMSDataKey(dataKeyResult)
// Generate a random IV for CTR mode
// Note: AES-CTR is used for object data encryption (not AES-GCM) because:
// 1. CTR mode supports streaming encryption for large objects
// 2. CTR mode supports range requests (seek to arbitrary positions)
// 3. This matches AWS S3 and other S3-compatible implementations
// The KMS data key encryption (separate layer) uses AES-GCM for authentication
iv := make([]byte, s3_constants.AESBlockSize)
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, nil, fmt.Errorf("failed to generate IV: %v", err)
}
// Create CTR mode cipher stream
stream := cipher.NewCTR(dataKeyResult.Block, iv)
// Create the SSE-KMS metadata using utility function
sseKey := createSSEKMSKey(dataKeyResult, encryptionContext, bucketKeyEnabled, iv, 0)
// The IV is stored in SSE key metadata, so the encrypted stream does not need to prepend the IV
// This ensures correct Content-Length for clients
encryptedReader := &cipher.StreamReader{S: stream, R: r}
// Store IV in the SSE key for metadata storage
sseKey.IV = iv
return encryptedReader, sseKey, nil
}
// CreateSSEKMSEncryptedReaderWithBaseIV creates an SSE-KMS encrypted reader using a provided base IV
// This is used for multipart uploads where all chunks need to use the same base IV
func CreateSSEKMSEncryptedReaderWithBaseIV(r io.Reader, keyID string, encryptionContext map[string]string, bucketKeyEnabled bool, baseIV []byte) (io.Reader, *SSEKMSKey, error) {
if err := ValidateIV(baseIV, "base IV"); err != nil {
return nil, nil, err
}
// Generate data key using common utility
dataKeyResult, err := generateKMSDataKey(keyID, encryptionContext)
if err != nil {
return nil, nil, err
}
// Ensure we clear the plaintext data key from memory when done
defer clearKMSDataKey(dataKeyResult)
// Use the provided base IV instead of generating a new one
iv := make([]byte, s3_constants.AESBlockSize)
copy(iv, baseIV)
// Create CTR mode cipher stream
stream := cipher.NewCTR(dataKeyResult.Block, iv)
// Create the SSE-KMS metadata using utility function
sseKey := createSSEKMSKey(dataKeyResult, encryptionContext, bucketKeyEnabled, iv, 0)
// The IV is stored in SSE key metadata, so the encrypted stream does not need to prepend the IV
// This ensures correct Content-Length for clients
encryptedReader := &cipher.StreamReader{S: stream, R: r}
// Store the base IV in the SSE key for metadata storage
sseKey.IV = iv
return encryptedReader, sseKey, nil
}
// CreateSSEKMSEncryptedReaderWithBaseIVAndOffset creates an SSE-KMS encrypted reader using a provided base IV and offset
// This is used for multipart uploads where all chunks need unique IVs to prevent IV reuse vulnerabilities
func CreateSSEKMSEncryptedReaderWithBaseIVAndOffset(r io.Reader, keyID string, encryptionContext map[string]string, bucketKeyEnabled bool, baseIV []byte, offset int64) (io.Reader, *SSEKMSKey, error) {
if err := ValidateIV(baseIV, "base IV"); err != nil {
return nil, nil, err
}
// Generate data key using common utility
dataKeyResult, err := generateKMSDataKey(keyID, encryptionContext)
if err != nil {
return nil, nil, err
}
// Ensure we clear the plaintext data key from memory when done
defer clearKMSDataKey(dataKeyResult)
// Calculate unique IV using base IV and offset to prevent IV reuse in multipart uploads
iv := calculateIVWithOffset(baseIV, offset)
// Create CTR mode cipher stream
stream := cipher.NewCTR(dataKeyResult.Block, iv)
// Create the SSE-KMS metadata using utility function
sseKey := createSSEKMSKey(dataKeyResult, encryptionContext, bucketKeyEnabled, iv, offset)
// The IV is stored in SSE key metadata, so the encrypted stream does not need to prepend the IV
// This ensures correct Content-Length for clients
encryptedReader := &cipher.StreamReader{S: stream, R: r}
return encryptedReader, sseKey, nil
}
// hashEncryptionContext creates a deterministic hash of the encryption context
func hashEncryptionContext(encryptionContext map[string]string) string {
if len(encryptionContext) == 0 {
return "empty"
}
// Create a deterministic representation of the context
hash := sha256.New()
// Sort keys to ensure deterministic hash
keys := make([]string, 0, len(encryptionContext))
for k := range encryptionContext {
keys = append(keys, k)
}
sort.Strings(keys)
// Hash the sorted key-value pairs
for _, k := range keys {
hash.Write([]byte(k))
hash.Write([]byte("="))
hash.Write([]byte(encryptionContext[k]))
hash.Write([]byte(";"))
}
return hex.EncodeToString(hash.Sum(nil))[:16] // Use first 16 chars for brevity
}
// getBucketDataKey retrieves or creates a cached bucket-level data key for SSE-KMS
// This is a simplified implementation that demonstrates the per-bucket caching concept
// In a full implementation, this would integrate with the actual bucket configuration system
func getBucketDataKey(bucketName, keyID string, encryptionContext map[string]string, bucketCache *BucketKMSCache) (*kms.GenerateDataKeyResponse, error) {
// Create context hash for cache key
contextHash := hashEncryptionContext(encryptionContext)
cacheKey := fmt.Sprintf("%s:%s", keyID, contextHash)
// Try to get from cache first if cache is available
if bucketCache != nil {
if cacheEntry, found := bucketCache.Get(cacheKey); found {
if dataKey, ok := cacheEntry.DataKey.(*kms.GenerateDataKeyResponse); ok {
glog.V(3).Infof("Using cached bucket key for bucket %s, keyID %s", bucketName, keyID)
return dataKey, nil
}
}
}
// Cache miss - generate new data key
kmsProvider := kms.GetGlobalKMS()
if kmsProvider == nil {
return nil, fmt.Errorf("KMS is not configured")
}
dataKeyReq := &kms.GenerateDataKeyRequest{
KeyID: keyID,
KeySpec: kms.KeySpecAES256,
EncryptionContext: encryptionContext,
}
ctx := context.Background()
dataKeyResp, err := kmsProvider.GenerateDataKey(ctx, dataKeyReq)
if err != nil {
return nil, fmt.Errorf("failed to generate bucket data key: %v", err)
}
// Cache the data key for future use if cache is available
if bucketCache != nil {
bucketCache.Set(cacheKey, keyID, dataKeyResp, BucketKeyCacheTTL)
glog.V(2).Infof("Generated and cached new bucket key for bucket %s, keyID %s", bucketName, keyID)
} else {
glog.V(2).Infof("Generated new bucket key for bucket %s, keyID %s (caching disabled)", bucketName, keyID)
}
return dataKeyResp, nil
}
// CreateSSEKMSEncryptedReaderForBucket creates an encrypted reader with bucket-specific caching
// This method is part of S3ApiServer to access bucket configuration and caching
func (s3a *S3ApiServer) CreateSSEKMSEncryptedReaderForBucket(r io.Reader, bucketName, keyID string, encryptionContext map[string]string, bucketKeyEnabled bool) (io.Reader, *SSEKMSKey, error) {
var dataKeyResp *kms.GenerateDataKeyResponse
var err error
if bucketKeyEnabled {
// Use S3 Bucket Keys optimization with persistent per-bucket caching
bucketCache, err := s3a.getBucketKMSCache(bucketName)
if err != nil {
glog.V(2).Infof("Failed to get bucket KMS cache for %s, falling back to per-object key: %v", bucketName, err)
bucketKeyEnabled = false
} else {
dataKeyResp, err = getBucketDataKey(bucketName, keyID, encryptionContext, bucketCache)
if err != nil {
// Fall back to per-object key generation if bucket key fails
glog.V(2).Infof("Bucket key generation failed for bucket %s, falling back to per-object key: %v", bucketName, err)
bucketKeyEnabled = false
}
}
}
if !bucketKeyEnabled {
// Generate a per-object data encryption key using KMS
kmsProvider := kms.GetGlobalKMS()
if kmsProvider == nil {
return nil, nil, fmt.Errorf("KMS is not configured")
}
dataKeyReq := &kms.GenerateDataKeyRequest{
KeyID: keyID,
KeySpec: kms.KeySpecAES256,
EncryptionContext: encryptionContext,
}
ctx := context.Background()
dataKeyResp, err = kmsProvider.GenerateDataKey(ctx, dataKeyReq)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate data key: %v", err)
}
}
// Ensure we clear the plaintext data key from memory when done
defer kms.ClearSensitiveData(dataKeyResp.Plaintext)
// Create AES cipher with the data key
block, err := aes.NewCipher(dataKeyResp.Plaintext)
if err != nil {
return nil, nil, fmt.Errorf("failed to create AES cipher: %v", err)
}
// Generate a random IV for CTR mode
iv := make([]byte, 16) // AES block size
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, nil, fmt.Errorf("failed to generate IV: %v", err)
}
// Create CTR mode cipher stream
stream := cipher.NewCTR(block, iv)
// Create the encrypting reader
sseKey := &SSEKMSKey{
KeyID: keyID,
EncryptedDataKey: dataKeyResp.CiphertextBlob,
EncryptionContext: encryptionContext,
BucketKeyEnabled: bucketKeyEnabled,
IV: iv,
}
return &cipher.StreamReader{S: stream, R: r}, sseKey, nil
}
// getBucketKMSCache gets or creates the persistent KMS cache for a bucket
func (s3a *S3ApiServer) getBucketKMSCache(bucketName string) (*BucketKMSCache, error) {
// Get bucket configuration
bucketConfig, errCode := s3a.getBucketConfig(bucketName)
if errCode != s3err.ErrNone {
if errCode == s3err.ErrNoSuchBucket {
return nil, fmt.Errorf("bucket %s does not exist", bucketName)
}
return nil, fmt.Errorf("failed to get bucket config: %v", errCode)
}
// Initialize KMS cache if it doesn't exist
if bucketConfig.KMSKeyCache == nil {
bucketConfig.KMSKeyCache = NewBucketKMSCache(bucketName, BucketKeyCacheTTL)
glog.V(3).Infof("Initialized new KMS cache for bucket %s", bucketName)
}
return bucketConfig.KMSKeyCache, nil
}
// CleanupBucketKMSCache performs cleanup of expired KMS keys for a specific bucket
func (s3a *S3ApiServer) CleanupBucketKMSCache(bucketName string) int {
bucketCache, err := s3a.getBucketKMSCache(bucketName)
if err != nil {
glog.V(3).Infof("Could not get KMS cache for bucket %s: %v", bucketName, err)
return 0
}
cleaned := bucketCache.CleanupExpired()
if cleaned > 0 {
glog.V(2).Infof("Cleaned up %d expired KMS keys for bucket %s", cleaned, bucketName)
}
return cleaned
}
// CleanupAllBucketKMSCaches performs cleanup of expired KMS keys for all buckets
func (s3a *S3ApiServer) CleanupAllBucketKMSCaches() int {
totalCleaned := 0
// Access the bucket config cache safely
if s3a.bucketConfigCache != nil {
s3a.bucketConfigCache.mutex.RLock()
bucketNames := make([]string, 0, len(s3a.bucketConfigCache.cache))
for bucketName := range s3a.bucketConfigCache.cache {
bucketNames = append(bucketNames, bucketName)
}
s3a.bucketConfigCache.mutex.RUnlock()
// Clean up each bucket's KMS cache
for _, bucketName := range bucketNames {
cleaned := s3a.CleanupBucketKMSCache(bucketName)
totalCleaned += cleaned
}
}
if totalCleaned > 0 {
glog.V(2).Infof("Cleaned up %d expired KMS keys across %d bucket caches", totalCleaned, len(s3a.bucketConfigCache.cache))
}
return totalCleaned
}
// CreateSSEKMSDecryptedReader creates a decrypted reader using KMS envelope encryption
func CreateSSEKMSDecryptedReader(r io.Reader, sseKey *SSEKMSKey) (io.Reader, error) {
kmsProvider := kms.GetGlobalKMS()
if kmsProvider == nil {
return nil, fmt.Errorf("KMS is not configured")
}
// Decrypt the data encryption key using KMS
decryptReq := &kms.DecryptRequest{
CiphertextBlob: sseKey.EncryptedDataKey,
EncryptionContext: sseKey.EncryptionContext,
}
ctx := context.Background()
decryptResp, err := kmsProvider.Decrypt(ctx, decryptReq)
if err != nil {
return nil, fmt.Errorf("failed to decrypt data key: %v", err)
}
// Ensure we clear the plaintext data key from memory when done
defer kms.ClearSensitiveData(decryptResp.Plaintext)
// Verify the key ID matches (security check)
if decryptResp.KeyID != sseKey.KeyID {
return nil, fmt.Errorf("KMS key ID mismatch: expected %s, got %s", sseKey.KeyID, decryptResp.KeyID)
}
// Use the IV from the SSE key metadata, calculating offset if this is a chunked part
if err := ValidateIV(sseKey.IV, "SSE key IV"); err != nil {
return nil, fmt.Errorf("invalid IV in SSE key: %w", err)
}
// Calculate the correct IV for this chunk's offset within the original part
var iv []byte
if sseKey.ChunkOffset > 0 {
iv = calculateIVWithOffset(sseKey.IV, sseKey.ChunkOffset)
glog.Infof("Using calculated IV with offset %d for chunk decryption", sseKey.ChunkOffset)
} else {
iv = sseKey.IV
// glog.Infof("Using base IV for chunk decryption (offset=0)")
}
// Create AES cipher with the decrypted data key
block, err := aes.NewCipher(decryptResp.Plaintext)
if err != nil {
return nil, fmt.Errorf("failed to create AES cipher: %v", err)
}
// Create CTR mode cipher stream for decryption
// Note: AES-CTR is used for object data decryption to match the encryption mode
stream := cipher.NewCTR(block, iv)
// Return the decrypted reader
return &cipher.StreamReader{S: stream, R: r}, nil
}
// ParseSSEKMSHeaders parses SSE-KMS headers from an HTTP request
func ParseSSEKMSHeaders(r *http.Request) (*SSEKMSKey, error) {
sseAlgorithm := r.Header.Get(s3_constants.AmzServerSideEncryption)
// Check if SSE-KMS is requested
if sseAlgorithm == "" {
return nil, nil // No SSE headers present
}
if sseAlgorithm != s3_constants.SSEAlgorithmKMS {
return nil, fmt.Errorf("invalid SSE algorithm: %s", sseAlgorithm)
}
keyID := r.Header.Get(s3_constants.AmzServerSideEncryptionAwsKmsKeyId)
encryptionContextHeader := r.Header.Get(s3_constants.AmzServerSideEncryptionContext)
bucketKeyEnabledHeader := r.Header.Get(s3_constants.AmzServerSideEncryptionBucketKeyEnabled)
// Parse encryption context if provided
var encryptionContext map[string]string
if encryptionContextHeader != "" {
// Decode base64-encoded JSON encryption context
contextBytes, err := base64.StdEncoding.DecodeString(encryptionContextHeader)
if err != nil {
return nil, fmt.Errorf("invalid encryption context format: %v", err)
}
if err := json.Unmarshal(contextBytes, &encryptionContext); err != nil {
return nil, fmt.Errorf("invalid encryption context JSON: %v", err)
}
}
// Parse bucket key enabled flag
bucketKeyEnabled := strings.ToLower(bucketKeyEnabledHeader) == "true"
sseKey := &SSEKMSKey{
KeyID: keyID,
EncryptionContext: encryptionContext,
BucketKeyEnabled: bucketKeyEnabled,
}
// Validate the parsed key including key ID format
if err := ValidateSSEKMSKeyInternal(sseKey); err != nil {
return nil, err
}
return sseKey, nil
}
// ValidateSSEKMSKey validates an SSE-KMS key configuration
func ValidateSSEKMSKeyInternal(sseKey *SSEKMSKey) error {
if err := ValidateSSEKMSKey(sseKey); err != nil {
return err
}
// An empty key ID is valid and means the default KMS key should be used.
if sseKey.KeyID != "" && !isValidKMSKeyID(sseKey.KeyID) {
return fmt.Errorf("invalid KMS key ID format: %s", sseKey.KeyID)
}
return nil
}
// BuildEncryptionContext creates the encryption context for S3 objects
func BuildEncryptionContext(bucketName, objectKey string, useBucketKey bool) map[string]string {
return kms.BuildS3EncryptionContext(bucketName, objectKey, useBucketKey)
}
// parseEncryptionContext parses the user-provided encryption context from base64 JSON
func parseEncryptionContext(contextHeader string) (map[string]string, error) {
if contextHeader == "" {
return nil, nil
}
// Decode base64
contextBytes, err := base64.StdEncoding.DecodeString(contextHeader)
if err != nil {
return nil, fmt.Errorf("invalid base64 encoding in encryption context: %w", err)
}
// Parse JSON
var context map[string]string
if err := json.Unmarshal(contextBytes, &context); err != nil {
return nil, fmt.Errorf("invalid JSON in encryption context: %w", err)
}
// Validate context keys and values
for k, v := range context {
if k == "" || v == "" {
return nil, fmt.Errorf("encryption context keys and values cannot be empty")
}
// AWS KMS has limits on context key/value length (256 chars each)
if len(k) > 256 || len(v) > 256 {
return nil, fmt.Errorf("encryption context key or value too long (max 256 characters)")
}
}
return context, nil
}
// SerializeSSEKMSMetadata serializes SSE-KMS metadata for storage in object metadata
func SerializeSSEKMSMetadata(sseKey *SSEKMSKey) ([]byte, error) {
if err := ValidateSSEKMSKey(sseKey); err != nil {
return nil, err
}
metadata := &SSEKMSMetadata{
Algorithm: s3_constants.SSEAlgorithmKMS,
KeyID: sseKey.KeyID,
EncryptedDataKey: base64.StdEncoding.EncodeToString(sseKey.EncryptedDataKey),
EncryptionContext: sseKey.EncryptionContext,
BucketKeyEnabled: sseKey.BucketKeyEnabled,
IV: base64.StdEncoding.EncodeToString(sseKey.IV), // Store IV for decryption
PartOffset: sseKey.ChunkOffset, // Store within-part offset
}
data, err := json.Marshal(metadata)
if err != nil {
return nil, fmt.Errorf("failed to marshal SSE-KMS metadata: %w", err)
}
glog.V(4).Infof("Serialized SSE-KMS metadata: keyID=%s, bucketKey=%t", sseKey.KeyID, sseKey.BucketKeyEnabled)
return data, nil
}
// DeserializeSSEKMSMetadata deserializes SSE-KMS metadata from storage and reconstructs the SSE-KMS key
func DeserializeSSEKMSMetadata(data []byte) (*SSEKMSKey, error) {
if len(data) == 0 {
return nil, fmt.Errorf("empty SSE-KMS metadata")
}
var metadata SSEKMSMetadata
if err := json.Unmarshal(data, &metadata); err != nil {
return nil, fmt.Errorf("failed to unmarshal SSE-KMS metadata: %w", err)
}
// Validate algorithm - be lenient with missing/empty algorithm for backward compatibility
if metadata.Algorithm != "" && metadata.Algorithm != s3_constants.SSEAlgorithmKMS {
return nil, fmt.Errorf("invalid SSE-KMS algorithm: %s", metadata.Algorithm)
}
// Set default algorithm if empty
if metadata.Algorithm == "" {
metadata.Algorithm = s3_constants.SSEAlgorithmKMS
}
// Decode the encrypted data key
encryptedDataKey, err := base64.StdEncoding.DecodeString(metadata.EncryptedDataKey)
if err != nil {
return nil, fmt.Errorf("failed to decode encrypted data key: %w", err)
}
// Decode the IV
var iv []byte
if metadata.IV != "" {
iv, err = base64.StdEncoding.DecodeString(metadata.IV)
if err != nil {
return nil, fmt.Errorf("failed to decode IV: %w", err)
}
}
sseKey := &SSEKMSKey{
KeyID: metadata.KeyID,
EncryptedDataKey: encryptedDataKey,
EncryptionContext: metadata.EncryptionContext,
BucketKeyEnabled: metadata.BucketKeyEnabled,
IV: iv, // Restore IV for decryption
ChunkOffset: metadata.PartOffset, // Use stored within-part offset
}
glog.V(4).Infof("Deserialized SSE-KMS metadata: keyID=%s, bucketKey=%t", sseKey.KeyID, sseKey.BucketKeyEnabled)
return sseKey, nil
}
// SSECMetadata represents SSE-C metadata for per-chunk storage (unified with SSE-KMS approach)
type SSECMetadata struct {
Algorithm string `json:"algorithm"` // SSE-C algorithm (always "AES256")
IV string `json:"iv"` // Base64-encoded initialization vector for this chunk
KeyMD5 string `json:"keyMD5"` // MD5 of the customer-provided key
PartOffset int64 `json:"partOffset"` // Offset within original multipart part (for IV calculation)
}
// SerializeSSECMetadata serializes SSE-C metadata for storage in chunk metadata
func SerializeSSECMetadata(iv []byte, keyMD5 string, partOffset int64) ([]byte, error) {
if err := ValidateIV(iv, "IV"); err != nil {
return nil, err
}
metadata := &SSECMetadata{
Algorithm: s3_constants.SSEAlgorithmAES256,
IV: base64.StdEncoding.EncodeToString(iv),
KeyMD5: keyMD5,
PartOffset: partOffset,
}
data, err := json.Marshal(metadata)
if err != nil {
return nil, fmt.Errorf("failed to marshal SSE-C metadata: %w", err)
}
glog.V(4).Infof("Serialized SSE-C metadata: keyMD5=%s, partOffset=%d", keyMD5, partOffset)
return data, nil
}
// DeserializeSSECMetadata deserializes SSE-C metadata from chunk storage
func DeserializeSSECMetadata(data []byte) (*SSECMetadata, error) {
if len(data) == 0 {
return nil, fmt.Errorf("empty SSE-C metadata")
}
var metadata SSECMetadata
if err := json.Unmarshal(data, &metadata); err != nil {
return nil, fmt.Errorf("failed to unmarshal SSE-C metadata: %w", err)
}
// Validate algorithm
if metadata.Algorithm != s3_constants.SSEAlgorithmAES256 {
return nil, fmt.Errorf("invalid SSE-C algorithm: %s", metadata.Algorithm)
}
// Validate IV
if metadata.IV == "" {
return nil, fmt.Errorf("missing IV in SSE-C metadata")
}
if _, err := base64.StdEncoding.DecodeString(metadata.IV); err != nil {
return nil, fmt.Errorf("invalid base64 IV in SSE-C metadata: %w", err)
}
glog.V(4).Infof("Deserialized SSE-C metadata: keyMD5=%s, partOffset=%d", metadata.KeyMD5, metadata.PartOffset)
return &metadata, nil
}
// AddSSEKMSResponseHeaders adds SSE-KMS response headers to an HTTP response
func AddSSEKMSResponseHeaders(w http.ResponseWriter, sseKey *SSEKMSKey) {
w.Header().Set(s3_constants.AmzServerSideEncryption, s3_constants.SSEAlgorithmKMS)
w.Header().Set(s3_constants.AmzServerSideEncryptionAwsKmsKeyId, sseKey.KeyID)
if len(sseKey.EncryptionContext) > 0 {
// Encode encryption context as base64 JSON
contextBytes, err := json.Marshal(sseKey.EncryptionContext)
if err == nil {
contextB64 := base64.StdEncoding.EncodeToString(contextBytes)
w.Header().Set(s3_constants.AmzServerSideEncryptionContext, contextB64)
} else {
glog.Errorf("Failed to encode encryption context: %v", err)
}
}
if sseKey.BucketKeyEnabled {
w.Header().Set(s3_constants.AmzServerSideEncryptionBucketKeyEnabled, "true")
}
}
// IsSSEKMSRequest checks if the request contains SSE-KMS headers
func IsSSEKMSRequest(r *http.Request) bool {
// If SSE-C headers are present, this is not an SSE-KMS request (they are mutually exclusive)
if r.Header.Get(s3_constants.AmzServerSideEncryptionCustomerAlgorithm) != "" {
return false
}
// According to AWS S3 specification, SSE-KMS is only valid when the encryption header
// is explicitly set to "aws:kms". The KMS key ID header alone is not sufficient.
sseAlgorithm := r.Header.Get(s3_constants.AmzServerSideEncryption)
return sseAlgorithm == s3_constants.SSEAlgorithmKMS
}
// IsSSEKMSEncrypted checks if the metadata indicates SSE-KMS encryption
func IsSSEKMSEncrypted(metadata map[string][]byte) bool {
if metadata == nil {
return false
}
// The canonical way to identify an SSE-KMS encrypted object is by this header.
if sseAlgorithm, exists := metadata[s3_constants.AmzServerSideEncryption]; exists {
return string(sseAlgorithm) == s3_constants.SSEAlgorithmKMS
}
return false
}
// IsAnySSEEncrypted checks if metadata indicates any type of SSE encryption
func IsAnySSEEncrypted(metadata map[string][]byte) bool {
if metadata == nil {
return false
}
// Check for any SSE type
if IsSSECEncrypted(metadata) {
return true
}
if IsSSEKMSEncrypted(metadata) {
return true
}
// Check for SSE-S3
if sseAlgorithm, exists := metadata[s3_constants.AmzServerSideEncryption]; exists {
return string(sseAlgorithm) == s3_constants.SSEAlgorithmAES256
}
return false
}
// MapKMSErrorToS3Error maps KMS errors to appropriate S3 error codes
func MapKMSErrorToS3Error(err error) s3err.ErrorCode {
if err == nil {
return s3err.ErrNone
}
// Check if it's a KMS error
kmsErr, ok := err.(*kms.KMSError)
if !ok {
return s3err.ErrInternalError
}
switch kmsErr.Code {
case kms.ErrCodeNotFoundException:
return s3err.ErrKMSKeyNotFound
case kms.ErrCodeAccessDenied:
return s3err.ErrKMSAccessDenied
case kms.ErrCodeKeyUnavailable:
return s3err.ErrKMSDisabled
case kms.ErrCodeInvalidKeyUsage:
return s3err.ErrKMSAccessDenied
case kms.ErrCodeInvalidCiphertext:
return s3err.ErrKMSInvalidCiphertext
default:
glog.Errorf("Unmapped KMS error: %s - %s", kmsErr.Code, kmsErr.Message)
return s3err.ErrInternalError
}
}
// SSEKMSCopyStrategy represents different strategies for copying SSE-KMS encrypted objects
type SSEKMSCopyStrategy int
const (
// SSEKMSCopyStrategyDirect - Direct chunk copy (same key, no re-encryption needed)
SSEKMSCopyStrategyDirect SSEKMSCopyStrategy = iota
// SSEKMSCopyStrategyDecryptEncrypt - Decrypt source and re-encrypt for destination
SSEKMSCopyStrategyDecryptEncrypt
)
// String returns string representation of the strategy
func (s SSEKMSCopyStrategy) String() string {
switch s {
case SSEKMSCopyStrategyDirect:
return "Direct"
case SSEKMSCopyStrategyDecryptEncrypt:
return "DecryptEncrypt"
default:
return "Unknown"
}
}
// GetSourceSSEKMSInfo extracts SSE-KMS information from source object metadata
func GetSourceSSEKMSInfo(metadata map[string][]byte) (keyID string, isEncrypted bool) {
if sseAlgorithm, exists := metadata[s3_constants.AmzServerSideEncryption]; exists && string(sseAlgorithm) == s3_constants.SSEAlgorithmKMS {
if kmsKeyID, exists := metadata[s3_constants.AmzServerSideEncryptionAwsKmsKeyId]; exists {
return string(kmsKeyID), true
}
return "", true // SSE-KMS with default key
}
return "", false
}
// CanDirectCopySSEKMS determines if we can directly copy chunks without decrypt/re-encrypt
func CanDirectCopySSEKMS(srcMetadata map[string][]byte, destKeyID string) bool {
srcKeyID, srcEncrypted := GetSourceSSEKMSInfo(srcMetadata)
// Case 1: Source unencrypted, destination unencrypted -> Direct copy
if !srcEncrypted && destKeyID == "" {
return true
}
// Case 2: Source encrypted with same KMS key as destination -> Direct copy
if srcEncrypted && destKeyID != "" {
// Same key if key IDs match (empty means default key)
return srcKeyID == destKeyID
}
// All other cases require decrypt/re-encrypt
return false
}
// DetermineSSEKMSCopyStrategy determines the optimal copy strategy for SSE-KMS
func DetermineSSEKMSCopyStrategy(srcMetadata map[string][]byte, destKeyID string) (SSEKMSCopyStrategy, error) {
if CanDirectCopySSEKMS(srcMetadata, destKeyID) {
return SSEKMSCopyStrategyDirect, nil
}
return SSEKMSCopyStrategyDecryptEncrypt, nil
}
// ParseSSEKMSCopyHeaders parses SSE-KMS headers from copy request
func ParseSSEKMSCopyHeaders(r *http.Request) (destKeyID string, encryptionContext map[string]string, bucketKeyEnabled bool, err error) {
// Check if this is an SSE-KMS request
if !IsSSEKMSRequest(r) {
return "", nil, false, nil
}
// Get destination KMS key ID
destKeyID = r.Header.Get(s3_constants.AmzServerSideEncryptionAwsKmsKeyId)
// Validate key ID if provided
if destKeyID != "" && !isValidKMSKeyID(destKeyID) {
return "", nil, false, fmt.Errorf("invalid KMS key ID: %s", destKeyID)
}
// Parse encryption context if provided
if contextHeader := r.Header.Get(s3_constants.AmzServerSideEncryptionContext); contextHeader != "" {
contextBytes, decodeErr := base64.StdEncoding.DecodeString(contextHeader)
if decodeErr != nil {
return "", nil, false, fmt.Errorf("invalid encryption context encoding: %v", decodeErr)
}
if unmarshalErr := json.Unmarshal(contextBytes, &encryptionContext); unmarshalErr != nil {
return "", nil, false, fmt.Errorf("invalid encryption context JSON: %v", unmarshalErr)
}
}
// Parse bucket key enabled flag
if bucketKeyHeader := r.Header.Get(s3_constants.AmzServerSideEncryptionBucketKeyEnabled); bucketKeyHeader != "" {
bucketKeyEnabled = strings.ToLower(bucketKeyHeader) == "true"
}
return destKeyID, encryptionContext, bucketKeyEnabled, nil
}
// UnifiedCopyStrategy represents all possible copy strategies across encryption types
type UnifiedCopyStrategy int
const (
// CopyStrategyDirect - Direct chunk copy (no encryption changes)
CopyStrategyDirect UnifiedCopyStrategy = iota
// CopyStrategyEncrypt - Encrypt during copy (plain → encrypted)
CopyStrategyEncrypt
// CopyStrategyDecrypt - Decrypt during copy (encrypted → plain)
CopyStrategyDecrypt
// CopyStrategyReencrypt - Decrypt and re-encrypt (different keys/methods)
CopyStrategyReencrypt
// CopyStrategyKeyRotation - Same object, different key (metadata-only update)
CopyStrategyKeyRotation
)
// String returns string representation of the unified strategy
func (s UnifiedCopyStrategy) String() string {
switch s {
case CopyStrategyDirect:
return "Direct"
case CopyStrategyEncrypt:
return "Encrypt"
case CopyStrategyDecrypt:
return "Decrypt"
case CopyStrategyReencrypt:
return "Reencrypt"
case CopyStrategyKeyRotation:
return "KeyRotation"
default:
return "Unknown"
}
}
// EncryptionState represents the encryption state of source and destination
type EncryptionState struct {
SrcSSEC bool
SrcSSEKMS bool
SrcSSES3 bool
DstSSEC bool
DstSSEKMS bool
DstSSES3 bool
SameObject bool
}
// IsSourceEncrypted returns true if source has any encryption
func (e *EncryptionState) IsSourceEncrypted() bool {
return e.SrcSSEC || e.SrcSSEKMS || e.SrcSSES3
}
// IsTargetEncrypted returns true if target should be encrypted
func (e *EncryptionState) IsTargetEncrypted() bool {
return e.DstSSEC || e.DstSSEKMS || e.DstSSES3
}
// DetermineUnifiedCopyStrategy determines the optimal copy strategy for all encryption types
func DetermineUnifiedCopyStrategy(state *EncryptionState, srcMetadata map[string][]byte, r *http.Request) (UnifiedCopyStrategy, error) {
// Key rotation: same object with different encryption
if state.SameObject && state.IsSourceEncrypted() && state.IsTargetEncrypted() {
// Check if it's actually a key change
if state.SrcSSEC && state.DstSSEC {
// SSE-C key rotation - need to compare keys
return CopyStrategyKeyRotation, nil
}
if state.SrcSSEKMS && state.DstSSEKMS {
// SSE-KMS key rotation - need to compare key IDs
srcKeyID, _ := GetSourceSSEKMSInfo(srcMetadata)
dstKeyID := r.Header.Get(s3_constants.AmzServerSideEncryptionAwsKmsKeyId)
if srcKeyID != dstKeyID {
return CopyStrategyKeyRotation, nil
}
}
}
// Direct copy: no encryption changes
if !state.IsSourceEncrypted() && !state.IsTargetEncrypted() {
return CopyStrategyDirect, nil
}
// Same encryption type and key
if state.SrcSSEKMS && state.DstSSEKMS {
srcKeyID, _ := GetSourceSSEKMSInfo(srcMetadata)
dstKeyID := r.Header.Get(s3_constants.AmzServerSideEncryptionAwsKmsKeyId)
if srcKeyID == dstKeyID {
return CopyStrategyDirect, nil
}
}
if state.SrcSSEC && state.DstSSEC {
// For SSE-C, we'd need to compare the actual keys, but we can't do that securely
// So we assume different keys and use reencrypt strategy
return CopyStrategyReencrypt, nil
}
// Encrypt: plain → encrypted
if !state.IsSourceEncrypted() && state.IsTargetEncrypted() {
return CopyStrategyEncrypt, nil
}
// Decrypt: encrypted → plain
if state.IsSourceEncrypted() && !state.IsTargetEncrypted() {
return CopyStrategyDecrypt, nil
}
// Reencrypt: different encryption types or keys
if state.IsSourceEncrypted() && state.IsTargetEncrypted() {
return CopyStrategyReencrypt, nil
}
return CopyStrategyDirect, nil
}
// DetectEncryptionState analyzes the source metadata and request headers to determine encryption state
func DetectEncryptionState(srcMetadata map[string][]byte, r *http.Request, srcPath, dstPath string) *EncryptionState {
state := &EncryptionState{
SrcSSEC: IsSSECEncrypted(srcMetadata),
SrcSSEKMS: IsSSEKMSEncrypted(srcMetadata),
SrcSSES3: IsSSES3EncryptedInternal(srcMetadata),
DstSSEC: IsSSECRequest(r),
DstSSEKMS: IsSSEKMSRequest(r),
DstSSES3: IsSSES3RequestInternal(r),
SameObject: srcPath == dstPath,
}
return state
}
// DetectEncryptionStateWithEntry analyzes the source entry and request headers to determine encryption state
// This version can detect multipart encrypted objects by examining chunks
func DetectEncryptionStateWithEntry(entry *filer_pb.Entry, r *http.Request, srcPath, dstPath string) *EncryptionState {
state := &EncryptionState{
SrcSSEC: IsSSECEncryptedWithEntry(entry),
SrcSSEKMS: IsSSEKMSEncryptedWithEntry(entry),
SrcSSES3: IsSSES3EncryptedInternal(entry.Extended),
DstSSEC: IsSSECRequest(r),
DstSSEKMS: IsSSEKMSRequest(r),
DstSSES3: IsSSES3RequestInternal(r),
SameObject: srcPath == dstPath,
}
return state
}
// IsSSEKMSEncryptedWithEntry detects SSE-KMS encryption from entry (including multipart objects)
func IsSSEKMSEncryptedWithEntry(entry *filer_pb.Entry) bool {
if entry == nil {
return false
}
// Check object-level metadata first
if IsSSEKMSEncrypted(entry.Extended) {
return true
}
// Check for multipart SSE-KMS by examining chunks
if len(entry.GetChunks()) > 0 {
for _, chunk := range entry.GetChunks() {
if chunk.GetSseType() == filer_pb.SSEType_SSE_KMS {
return true
}
}
}
return false
}
// IsSSECEncryptedWithEntry detects SSE-C encryption from entry (including multipart objects)
func IsSSECEncryptedWithEntry(entry *filer_pb.Entry) bool {
if entry == nil {
return false
}
// Check object-level metadata first
if IsSSECEncrypted(entry.Extended) {
return true
}
// Check for multipart SSE-C by examining chunks
if len(entry.GetChunks()) > 0 {
for _, chunk := range entry.GetChunks() {
if chunk.GetSseType() == filer_pb.SSEType_SSE_C {
return true
}
}
}
return false
}
// Helper functions for SSE-C detection are in s3_sse_c.go