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
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							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
 |