aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--weed/s3api/s3_constants/header.go10
-rw-r--r--weed/s3api/s3_sse_c_range_test.go3
-rw-r--r--weed/s3api/s3_sse_copy_test.go2
-rw-r--r--weed/s3api/s3_sse_metadata.go152
-rw-r--r--weed/s3api/s3_sse_s3.go25
-rw-r--r--weed/s3api/s3_sse_s3_integration_test.go325
-rw-r--r--weed/s3api/s3_sse_s3_test.go984
-rw-r--r--weed/s3api/s3_sse_test_utils_test.go2
-rw-r--r--weed/s3api/s3_validation_utils.go27
-rw-r--r--weed/s3api/s3api_key_rotation.go6
-rw-r--r--weed/s3api/s3api_object_handlers.go407
-rw-r--r--weed/s3api/s3api_object_handlers_copy.go8
-rw-r--r--weed/s3api/s3api_streaming_copy.go10
-rw-r--r--weed/s3api/s3err/s3api_errors.go6
-rw-r--r--weed/server/filer_server_handlers_read.go9
-rw-r--r--weed/server/filer_server_handlers_write_autochunk.go10
-rw-r--r--weed/topology/volume_growth_reservation_test.go32
17 files changed, 1762 insertions, 256 deletions
diff --git a/weed/s3api/s3_constants/header.go b/weed/s3api/s3_constants/header.go
index 86863f257..82a270111 100644
--- a/weed/s3api/s3_constants/header.go
+++ b/weed/s3api/s3_constants/header.go
@@ -94,6 +94,9 @@ const (
AmzEncryptedDataKey = "x-amz-encrypted-data-key"
AmzEncryptionContextMeta = "x-amz-encryption-context"
+ // SeaweedFS internal metadata prefix (used to filter internal headers from client responses)
+ SeaweedFSInternalPrefix = "x-seaweedfs-"
+
// SeaweedFS internal metadata keys for encryption (prefixed to avoid automatic HTTP header conversion)
SeaweedFSSSEKMSKey = "x-seaweedfs-sse-kms-key" // Key for storing serialized SSE-KMS metadata
SeaweedFSSSES3Key = "x-seaweedfs-sse-s3-key" // Key for storing serialized SSE-S3 metadata
@@ -157,3 +160,10 @@ var PassThroughHeaders = map[string]string{
"response-content-type": "Content-Type",
"response-expires": "Expires",
}
+
+// IsSeaweedFSInternalHeader checks if a header key is a SeaweedFS internal header
+// that should be filtered from client responses.
+// Header names are case-insensitive in HTTP, so this function normalizes to lowercase.
+func IsSeaweedFSInternalHeader(headerKey string) bool {
+ return strings.HasPrefix(strings.ToLower(headerKey), SeaweedFSInternalPrefix)
+}
diff --git a/weed/s3api/s3_sse_c_range_test.go b/weed/s3api/s3_sse_c_range_test.go
index 318771d8c..b704c39af 100644
--- a/weed/s3api/s3_sse_c_range_test.go
+++ b/weed/s3api/s3_sse_c_range_test.go
@@ -56,7 +56,8 @@ func TestSSECRangeRequestsSupported(t *testing.T) {
}
rec := httptest.NewRecorder()
w := recorderFlusher{rec}
- statusCode, _ := s3a.handleSSECResponse(req, proxyResponse, w)
+ // Pass nil for entry since this test focuses on Range request handling
+ statusCode, _ := s3a.handleSSECResponse(req, proxyResponse, w, nil)
// Range requests should now be allowed to proceed (will be handled by filer layer)
// The exact status code depends on the object existence and filer response
diff --git a/weed/s3api/s3_sse_copy_test.go b/weed/s3api/s3_sse_copy_test.go
index 35839a704..b377b45a9 100644
--- a/weed/s3api/s3_sse_copy_test.go
+++ b/weed/s3api/s3_sse_copy_test.go
@@ -43,7 +43,7 @@ func TestSSECObjectCopy(t *testing.T) {
// Test copy strategy determination
sourceMetadata := make(map[string][]byte)
- StoreIVInMetadata(sourceMetadata, iv)
+ StoreSSECIVInMetadata(sourceMetadata, iv)
sourceMetadata[s3_constants.AmzServerSideEncryptionCustomerAlgorithm] = []byte("AES256")
sourceMetadata[s3_constants.AmzServerSideEncryptionCustomerKeyMD5] = []byte(sourceKey.KeyMD5)
diff --git a/weed/s3api/s3_sse_metadata.go b/weed/s3api/s3_sse_metadata.go
index 8b641f150..7cb695251 100644
--- a/weed/s3api/s3_sse_metadata.go
+++ b/weed/s3api/s3_sse_metadata.go
@@ -2,158 +2,28 @@ package s3api
import (
"encoding/base64"
- "encoding/json"
"fmt"
-)
-
-// SSE metadata keys for storing encryption information in entry metadata
-const (
- // MetaSSEIV is the initialization vector used for encryption
- MetaSSEIV = "X-SeaweedFS-Server-Side-Encryption-Iv"
-
- // MetaSSEAlgorithm is the encryption algorithm used
- MetaSSEAlgorithm = "X-SeaweedFS-Server-Side-Encryption-Algorithm"
-
- // MetaSSECKeyMD5 is the MD5 hash of the SSE-C customer key
- MetaSSECKeyMD5 = "X-SeaweedFS-Server-Side-Encryption-Customer-Key-MD5"
-
- // MetaSSEKMSKeyID is the KMS key ID used for encryption
- MetaSSEKMSKeyID = "X-SeaweedFS-Server-Side-Encryption-KMS-Key-Id"
-
- // MetaSSEKMSEncryptedKey is the encrypted data key from KMS
- MetaSSEKMSEncryptedKey = "X-SeaweedFS-Server-Side-Encryption-KMS-Encrypted-Key"
-
- // MetaSSEKMSContext is the encryption context for KMS
- MetaSSEKMSContext = "X-SeaweedFS-Server-Side-Encryption-KMS-Context"
- // MetaSSES3KeyID is the key ID for SSE-S3 encryption
- MetaSSES3KeyID = "X-SeaweedFS-Server-Side-Encryption-S3-Key-Id"
+ "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
)
-// StoreIVInMetadata stores the IV in entry metadata as base64 encoded string
-func StoreIVInMetadata(metadata map[string][]byte, iv []byte) {
+// StoreSSECIVInMetadata stores the SSE-C IV in entry metadata as base64 encoded string
+// Used by SSE-C for storing IV in entry.Extended
+func StoreSSECIVInMetadata(metadata map[string][]byte, iv []byte) {
if len(iv) > 0 {
- metadata[MetaSSEIV] = []byte(base64.StdEncoding.EncodeToString(iv))
+ metadata[s3_constants.SeaweedFSSSEIV] = []byte(base64.StdEncoding.EncodeToString(iv))
}
}
-// GetIVFromMetadata retrieves the IV from entry metadata
-func GetIVFromMetadata(metadata map[string][]byte) ([]byte, error) {
- if ivBase64, exists := metadata[MetaSSEIV]; exists {
+// GetSSECIVFromMetadata retrieves the SSE-C IV from entry metadata
+// Used by SSE-C for retrieving IV from entry.Extended
+func GetSSECIVFromMetadata(metadata map[string][]byte) ([]byte, error) {
+ if ivBase64, exists := metadata[s3_constants.SeaweedFSSSEIV]; exists {
iv, err := base64.StdEncoding.DecodeString(string(ivBase64))
if err != nil {
- return nil, fmt.Errorf("failed to decode IV from metadata: %w", err)
+ return nil, fmt.Errorf("failed to decode SSE-C IV from metadata: %w", err)
}
return iv, nil
}
- return nil, fmt.Errorf("IV not found in metadata")
-}
-
-// StoreSSECMetadata stores SSE-C related metadata
-func StoreSSECMetadata(metadata map[string][]byte, iv []byte, keyMD5 string) {
- StoreIVInMetadata(metadata, iv)
- metadata[MetaSSEAlgorithm] = []byte("AES256")
- if keyMD5 != "" {
- metadata[MetaSSECKeyMD5] = []byte(keyMD5)
- }
-}
-
-// StoreSSEKMSMetadata stores SSE-KMS related metadata
-func StoreSSEKMSMetadata(metadata map[string][]byte, iv []byte, keyID string, encryptedKey []byte, context map[string]string) {
- StoreIVInMetadata(metadata, iv)
- metadata[MetaSSEAlgorithm] = []byte("aws:kms")
- if keyID != "" {
- metadata[MetaSSEKMSKeyID] = []byte(keyID)
- }
- if len(encryptedKey) > 0 {
- metadata[MetaSSEKMSEncryptedKey] = []byte(base64.StdEncoding.EncodeToString(encryptedKey))
- }
- if len(context) > 0 {
- // Marshal context to JSON to handle special characters correctly
- contextBytes, err := json.Marshal(context)
- if err == nil {
- metadata[MetaSSEKMSContext] = contextBytes
- }
- // Note: json.Marshal for map[string]string should never fail, but we handle it gracefully
- }
-}
-
-// StoreSSES3Metadata stores SSE-S3 related metadata
-func StoreSSES3Metadata(metadata map[string][]byte, iv []byte, keyID string) {
- StoreIVInMetadata(metadata, iv)
- metadata[MetaSSEAlgorithm] = []byte("AES256")
- if keyID != "" {
- metadata[MetaSSES3KeyID] = []byte(keyID)
- }
-}
-
-// GetSSECMetadata retrieves SSE-C metadata
-func GetSSECMetadata(metadata map[string][]byte) (iv []byte, keyMD5 string, err error) {
- iv, err = GetIVFromMetadata(metadata)
- if err != nil {
- return nil, "", err
- }
-
- if keyMD5Bytes, exists := metadata[MetaSSECKeyMD5]; exists {
- keyMD5 = string(keyMD5Bytes)
- }
-
- return iv, keyMD5, nil
-}
-
-// GetSSEKMSMetadata retrieves SSE-KMS metadata
-func GetSSEKMSMetadata(metadata map[string][]byte) (iv []byte, keyID string, encryptedKey []byte, context map[string]string, err error) {
- iv, err = GetIVFromMetadata(metadata)
- if err != nil {
- return nil, "", nil, nil, err
- }
-
- if keyIDBytes, exists := metadata[MetaSSEKMSKeyID]; exists {
- keyID = string(keyIDBytes)
- }
-
- if encKeyBase64, exists := metadata[MetaSSEKMSEncryptedKey]; exists {
- encryptedKey, err = base64.StdEncoding.DecodeString(string(encKeyBase64))
- if err != nil {
- return nil, "", nil, nil, fmt.Errorf("failed to decode encrypted key: %w", err)
- }
- }
-
- // Parse context from JSON
- if contextBytes, exists := metadata[MetaSSEKMSContext]; exists {
- context = make(map[string]string)
- if err := json.Unmarshal(contextBytes, &context); err != nil {
- return nil, "", nil, nil, fmt.Errorf("failed to parse KMS context JSON: %w", err)
- }
- }
-
- return iv, keyID, encryptedKey, context, nil
-}
-
-// GetSSES3Metadata retrieves SSE-S3 metadata
-func GetSSES3Metadata(metadata map[string][]byte) (iv []byte, keyID string, err error) {
- iv, err = GetIVFromMetadata(metadata)
- if err != nil {
- return nil, "", err
- }
-
- if keyIDBytes, exists := metadata[MetaSSES3KeyID]; exists {
- keyID = string(keyIDBytes)
- }
-
- return iv, keyID, nil
-}
-
-// IsSSEEncrypted checks if the metadata indicates any form of SSE encryption
-func IsSSEEncrypted(metadata map[string][]byte) bool {
- _, exists := metadata[MetaSSEIV]
- return exists
-}
-
-// GetSSEAlgorithm returns the SSE algorithm from metadata
-func GetSSEAlgorithm(metadata map[string][]byte) string {
- if alg, exists := metadata[MetaSSEAlgorithm]; exists {
- return string(alg)
- }
- return ""
+ return nil, fmt.Errorf("SSE-C IV not found in metadata")
}
diff --git a/weed/s3api/s3_sse_s3.go b/weed/s3api/s3_sse_s3.go
index bb563eee5..bc648205e 100644
--- a/weed/s3api/s3_sse_s3.go
+++ b/weed/s3api/s3_sse_s3.go
@@ -485,6 +485,31 @@ func GetSSES3KeyFromMetadata(metadata map[string][]byte, keyManager *SSES3KeyMan
return DeserializeSSES3Metadata(keyData, keyManager)
}
+// GetSSES3IV extracts the IV for single-part SSE-S3 objects
+// Priority: 1) object-level metadata (for inline/small files), 2) first chunk metadata
+func GetSSES3IV(entry *filer_pb.Entry, sseS3Key *SSES3Key, keyManager *SSES3KeyManager) ([]byte, error) {
+ // First check if IV is in the object-level key (for small/inline files)
+ if len(sseS3Key.IV) > 0 {
+ return sseS3Key.IV, nil
+ }
+
+ // Fallback: Get IV from first chunk's metadata (for chunked files)
+ if len(entry.GetChunks()) > 0 {
+ chunk := entry.GetChunks()[0]
+ if len(chunk.GetSseMetadata()) > 0 {
+ chunkKey, err := DeserializeSSES3Metadata(chunk.GetSseMetadata(), keyManager)
+ if err != nil {
+ return nil, fmt.Errorf("failed to deserialize chunk SSE-S3 metadata: %w", err)
+ }
+ if len(chunkKey.IV) > 0 {
+ return chunkKey.IV, nil
+ }
+ }
+ }
+
+ return nil, fmt.Errorf("SSE-S3 IV not found in object or chunk metadata")
+}
+
// CreateSSES3EncryptedReaderWithBaseIV creates an encrypted reader using a base IV for multipart upload consistency.
// The returned IV is the offset-derived IV, calculated from the input baseIV and offset.
func CreateSSES3EncryptedReaderWithBaseIV(reader io.Reader, key *SSES3Key, baseIV []byte, offset int64) (io.Reader, []byte /* derivedIV */, error) {
diff --git a/weed/s3api/s3_sse_s3_integration_test.go b/weed/s3api/s3_sse_s3_integration_test.go
new file mode 100644
index 000000000..8232aea7f
--- /dev/null
+++ b/weed/s3api/s3_sse_s3_integration_test.go
@@ -0,0 +1,325 @@
+package s3api
+
+import (
+ "bytes"
+ "io"
+ "testing"
+
+ "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
+ "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
+)
+
+// NOTE: These are integration tests that test the end-to-end encryption/decryption flow.
+// Full HTTP handler tests (PUT -> GET) would require a complete mock server with filer,
+// which is complex to set up. These tests focus on the critical decrypt path.
+
+// TestSSES3EndToEndSmallFile tests the complete encryption->storage->decryption cycle for small inline files
+// This test would have caught the IV retrieval bug for inline files
+func TestSSES3EndToEndSmallFile(t *testing.T) {
+ // Initialize global SSE-S3 key manager
+ globalSSES3KeyManager = NewSSES3KeyManager()
+ defer func() {
+ globalSSES3KeyManager = NewSSES3KeyManager()
+ }()
+
+ // Set up the key manager with a super key for testing
+ keyManager := GetSSES3KeyManager()
+ keyManager.superKey = make([]byte, 32)
+ for i := range keyManager.superKey {
+ keyManager.superKey[i] = byte(i)
+ }
+
+ testCases := []struct {
+ name string
+ data []byte
+ }{
+ {"tiny file (10 bytes)", []byte("test12345")},
+ {"small file (50 bytes)", []byte("This is a small test file for SSE-S3 encryption")},
+ {"medium file (256 bytes)", bytes.Repeat([]byte("a"), 256)},
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ // Step 1: Encrypt (simulates what happens during PUT)
+ sseS3Key, err := GenerateSSES3Key()
+ if err != nil {
+ t.Fatalf("Failed to generate SSE-S3 key: %v", err)
+ }
+
+ encryptedReader, iv, err := CreateSSES3EncryptedReader(bytes.NewReader(tc.data), sseS3Key)
+ if err != nil {
+ t.Fatalf("Failed to create encrypted reader: %v", err)
+ }
+
+ encryptedData, err := io.ReadAll(encryptedReader)
+ if err != nil {
+ t.Fatalf("Failed to read encrypted data: %v", err)
+ }
+
+ // Store IV in the key (this is critical for inline files!)
+ sseS3Key.IV = iv
+
+ // Serialize the metadata (this is stored in entry.Extended)
+ serializedMetadata, err := SerializeSSES3Metadata(sseS3Key)
+ if err != nil {
+ t.Fatalf("Failed to serialize SSE-S3 metadata: %v", err)
+ }
+
+ // Step 2: Simulate storage (inline file - no chunks)
+ // For inline files, data is in Content, metadata in Extended
+ mockEntry := &filer_pb.Entry{
+ Extended: map[string][]byte{
+ s3_constants.SeaweedFSSSES3Key: serializedMetadata,
+ s3_constants.AmzServerSideEncryption: []byte("AES256"),
+ },
+ Content: encryptedData,
+ Chunks: []*filer_pb.FileChunk{}, // Critical: inline files have NO chunks
+ }
+
+ // Step 3: Decrypt (simulates what happens during GET)
+ // This tests the IV retrieval path for inline files
+
+ // First, deserialize metadata from storage
+ retrievedKeyData := mockEntry.Extended[s3_constants.SeaweedFSSSES3Key]
+ retrievedKey, err := DeserializeSSES3Metadata(retrievedKeyData, keyManager)
+ if err != nil {
+ t.Fatalf("Failed to deserialize SSE-S3 metadata: %v", err)
+ }
+
+ // CRITICAL TEST: For inline files, IV must be in object-level metadata
+ var retrievedIV []byte
+ if len(retrievedKey.IV) > 0 {
+ // Success path: IV found in object-level key
+ retrievedIV = retrievedKey.IV
+ } else if len(mockEntry.GetChunks()) > 0 {
+ // Fallback path: would check chunks (but inline files have no chunks)
+ t.Fatal("Inline file should have IV in object-level metadata, not chunks")
+ }
+
+ if len(retrievedIV) == 0 {
+ // THIS IS THE BUG WE FIXED: inline files had no way to get IV!
+ t.Fatal("Failed to retrieve IV for inline file - this is the bug we fixed!")
+ }
+
+ // Now decrypt with the retrieved IV
+ decryptedReader, err := CreateSSES3DecryptedReader(bytes.NewReader(encryptedData), retrievedKey, retrievedIV)
+ if err != nil {
+ t.Fatalf("Failed to create decrypted reader: %v", err)
+ }
+
+ decryptedData, err := io.ReadAll(decryptedReader)
+ if err != nil {
+ t.Fatalf("Failed to read decrypted data: %v", err)
+ }
+
+ // Verify decrypted data matches original
+ if !bytes.Equal(decryptedData, tc.data) {
+ t.Errorf("Decrypted data doesn't match original.\nExpected: %q\nGot: %q", tc.data, decryptedData)
+ }
+ })
+ }
+}
+
+// TestSSES3EndToEndChunkedFile tests the complete flow for chunked files
+func TestSSES3EndToEndChunkedFile(t *testing.T) {
+ // Initialize global SSE-S3 key manager
+ globalSSES3KeyManager = NewSSES3KeyManager()
+ defer func() {
+ globalSSES3KeyManager = NewSSES3KeyManager()
+ }()
+
+ keyManager := GetSSES3KeyManager()
+ keyManager.superKey = make([]byte, 32)
+ for i := range keyManager.superKey {
+ keyManager.superKey[i] = byte(i)
+ }
+
+ // Generate SSE-S3 key
+ sseS3Key, err := GenerateSSES3Key()
+ if err != nil {
+ t.Fatalf("Failed to generate SSE-S3 key: %v", err)
+ }
+
+ // Create test data for two chunks
+ chunk1Data := []byte("This is chunk 1 data for SSE-S3 encryption test")
+ chunk2Data := []byte("This is chunk 2 data for SSE-S3 encryption test")
+
+ // Encrypt chunk 1
+ encryptedReader1, iv1, err := CreateSSES3EncryptedReader(bytes.NewReader(chunk1Data), sseS3Key)
+ if err != nil {
+ t.Fatalf("Failed to create encrypted reader for chunk 1: %v", err)
+ }
+ encryptedChunk1, _ := io.ReadAll(encryptedReader1)
+
+ // Encrypt chunk 2
+ encryptedReader2, iv2, err := CreateSSES3EncryptedReader(bytes.NewReader(chunk2Data), sseS3Key)
+ if err != nil {
+ t.Fatalf("Failed to create encrypted reader for chunk 2: %v", err)
+ }
+ encryptedChunk2, _ := io.ReadAll(encryptedReader2)
+
+ // Create metadata for each chunk
+ chunk1Key := &SSES3Key{
+ Key: sseS3Key.Key,
+ IV: iv1,
+ Algorithm: sseS3Key.Algorithm,
+ KeyID: sseS3Key.KeyID,
+ }
+ chunk2Key := &SSES3Key{
+ Key: sseS3Key.Key,
+ IV: iv2,
+ Algorithm: sseS3Key.Algorithm,
+ KeyID: sseS3Key.KeyID,
+ }
+
+ serializedChunk1Meta, _ := SerializeSSES3Metadata(chunk1Key)
+ serializedChunk2Meta, _ := SerializeSSES3Metadata(chunk2Key)
+ serializedObjMeta, _ := SerializeSSES3Metadata(sseS3Key)
+
+ // Create mock entry with chunks
+ mockEntry := &filer_pb.Entry{
+ Extended: map[string][]byte{
+ s3_constants.SeaweedFSSSES3Key: serializedObjMeta,
+ s3_constants.AmzServerSideEncryption: []byte("AES256"),
+ },
+ Chunks: []*filer_pb.FileChunk{
+ {
+ FileId: "chunk1,123",
+ Offset: 0,
+ Size: uint64(len(encryptedChunk1)),
+ SseType: filer_pb.SSEType_SSE_S3,
+ SseMetadata: serializedChunk1Meta,
+ },
+ {
+ FileId: "chunk2,456",
+ Offset: int64(len(chunk1Data)),
+ Size: uint64(len(encryptedChunk2)),
+ SseType: filer_pb.SSEType_SSE_S3,
+ SseMetadata: serializedChunk2Meta,
+ },
+ },
+ }
+
+ // Verify multipart detection
+ sses3Chunks := 0
+ for _, chunk := range mockEntry.GetChunks() {
+ if chunk.GetSseType() == filer_pb.SSEType_SSE_S3 && len(chunk.GetSseMetadata()) > 0 {
+ sses3Chunks++
+ }
+ }
+
+ isMultipart := sses3Chunks > 1
+ if !isMultipart {
+ t.Error("Expected multipart SSE-S3 object detection")
+ }
+
+ if sses3Chunks != 2 {
+ t.Errorf("Expected 2 SSE-S3 chunks, got %d", sses3Chunks)
+ }
+
+ // Verify each chunk has valid metadata with IV
+ for i, chunk := range mockEntry.GetChunks() {
+ deserializedKey, err := DeserializeSSES3Metadata(chunk.GetSseMetadata(), keyManager)
+ if err != nil {
+ t.Errorf("Failed to deserialize chunk %d metadata: %v", i, err)
+ }
+ if len(deserializedKey.IV) == 0 {
+ t.Errorf("Chunk %d has no IV", i)
+ }
+
+ // Decrypt this chunk to verify it works
+ var chunkData []byte
+ if i == 0 {
+ chunkData = encryptedChunk1
+ } else {
+ chunkData = encryptedChunk2
+ }
+
+ decryptedReader, err := CreateSSES3DecryptedReader(bytes.NewReader(chunkData), deserializedKey, deserializedKey.IV)
+ if err != nil {
+ t.Errorf("Failed to decrypt chunk %d: %v", i, err)
+ continue
+ }
+
+ decrypted, _ := io.ReadAll(decryptedReader)
+ var expectedData []byte
+ if i == 0 {
+ expectedData = chunk1Data
+ } else {
+ expectedData = chunk2Data
+ }
+
+ if !bytes.Equal(decrypted, expectedData) {
+ t.Errorf("Chunk %d decryption failed", i)
+ }
+ }
+}
+
+// TestSSES3EndToEndWithDetectPrimaryType tests that type detection works correctly for different scenarios
+func TestSSES3EndToEndWithDetectPrimaryType(t *testing.T) {
+ s3a := &S3ApiServer{}
+
+ testCases := []struct {
+ name string
+ entry *filer_pb.Entry
+ expectedType string
+ shouldBeSSES3 bool
+ }{
+ {
+ name: "Inline SSE-S3 file (no chunks)",
+ entry: &filer_pb.Entry{
+ Extended: map[string][]byte{
+ s3_constants.AmzServerSideEncryption: []byte("AES256"),
+ },
+ Attributes: &filer_pb.FuseAttributes{},
+ Content: []byte("encrypted data"),
+ Chunks: []*filer_pb.FileChunk{},
+ },
+ expectedType: s3_constants.SSETypeS3,
+ shouldBeSSES3: true,
+ },
+ {
+ name: "Single chunk SSE-S3 file",
+ entry: &filer_pb.Entry{
+ Extended: map[string][]byte{
+ s3_constants.AmzServerSideEncryption: []byte("AES256"),
+ },
+ Attributes: &filer_pb.FuseAttributes{},
+ Chunks: []*filer_pb.FileChunk{
+ {
+ FileId: "1,123",
+ SseType: filer_pb.SSEType_SSE_S3,
+ SseMetadata: []byte("metadata"),
+ },
+ },
+ },
+ expectedType: s3_constants.SSETypeS3,
+ shouldBeSSES3: true,
+ },
+ {
+ name: "SSE-KMS file (has KMS key ID)",
+ entry: &filer_pb.Entry{
+ Extended: map[string][]byte{
+ s3_constants.AmzServerSideEncryption: []byte("AES256"),
+ s3_constants.AmzServerSideEncryptionAwsKmsKeyId: []byte("kms-key-123"),
+ },
+ Attributes: &filer_pb.FuseAttributes{},
+ Chunks: []*filer_pb.FileChunk{},
+ },
+ expectedType: s3_constants.SSETypeKMS,
+ shouldBeSSES3: false,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ detectedType := s3a.detectPrimarySSEType(tc.entry)
+ if detectedType != tc.expectedType {
+ t.Errorf("Expected type %s, got %s", tc.expectedType, detectedType)
+ }
+ if (detectedType == s3_constants.SSETypeS3) != tc.shouldBeSSES3 {
+ t.Errorf("SSE-S3 detection mismatch: expected %v, got %v", tc.shouldBeSSES3, detectedType == s3_constants.SSETypeS3)
+ }
+ })
+ }
+}
diff --git a/weed/s3api/s3_sse_s3_test.go b/weed/s3api/s3_sse_s3_test.go
new file mode 100644
index 000000000..391692921
--- /dev/null
+++ b/weed/s3api/s3_sse_s3_test.go
@@ -0,0 +1,984 @@
+package s3api
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
+ "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
+)
+
+// TestSSES3EncryptionDecryption tests basic SSE-S3 encryption and decryption
+func TestSSES3EncryptionDecryption(t *testing.T) {
+ // Generate SSE-S3 key
+ sseS3Key, err := GenerateSSES3Key()
+ if err != nil {
+ t.Fatalf("Failed to generate SSE-S3 key: %v", err)
+ }
+
+ // Test data
+ testData := []byte("Hello, World! This is a test of SSE-S3 encryption.")
+
+ // Create encrypted reader
+ dataReader := bytes.NewReader(testData)
+ encryptedReader, iv, err := CreateSSES3EncryptedReader(dataReader, sseS3Key)
+ if err != nil {
+ t.Fatalf("Failed to create encrypted reader: %v", err)
+ }
+
+ // Read encrypted data
+ encryptedData, err := io.ReadAll(encryptedReader)
+ if err != nil {
+ t.Fatalf("Failed to read encrypted data: %v", err)
+ }
+
+ // Verify data is actually encrypted (different from original)
+ if bytes.Equal(encryptedData, testData) {
+ t.Error("Data doesn't appear to be encrypted")
+ }
+
+ // Create decrypted reader
+ encryptedReader2 := bytes.NewReader(encryptedData)
+ decryptedReader, err := CreateSSES3DecryptedReader(encryptedReader2, sseS3Key, iv)
+ if err != nil {
+ t.Fatalf("Failed to create decrypted reader: %v", err)
+ }
+
+ // Read decrypted data
+ decryptedData, err := io.ReadAll(decryptedReader)
+ if err != nil {
+ t.Fatalf("Failed to read decrypted data: %v", err)
+ }
+
+ // Verify decrypted data matches original
+ if !bytes.Equal(decryptedData, testData) {
+ t.Errorf("Decrypted data doesn't match original.\nOriginal: %s\nDecrypted: %s", testData, decryptedData)
+ }
+}
+
+// TestSSES3IsRequestInternal tests detection of SSE-S3 requests
+func TestSSES3IsRequestInternal(t *testing.T) {
+ testCases := []struct {
+ name string
+ headers map[string]string
+ expected bool
+ }{
+ {
+ name: "Valid SSE-S3 request",
+ headers: map[string]string{
+ s3_constants.AmzServerSideEncryption: "AES256",
+ },
+ expected: true,
+ },
+ {
+ name: "No SSE headers",
+ headers: map[string]string{},
+ expected: false,
+ },
+ {
+ name: "SSE-KMS request",
+ headers: map[string]string{
+ s3_constants.AmzServerSideEncryption: "aws:kms",
+ },
+ expected: false,
+ },
+ {
+ name: "SSE-C request",
+ headers: map[string]string{
+ s3_constants.AmzServerSideEncryptionCustomerAlgorithm: "AES256",
+ },
+ expected: false,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ req := &http.Request{Header: make(http.Header)}
+ for k, v := range tc.headers {
+ req.Header.Set(k, v)
+ }
+
+ result := IsSSES3RequestInternal(req)
+ if result != tc.expected {
+ t.Errorf("Expected %v, got %v", tc.expected, result)
+ }
+ })
+ }
+}
+
+// TestSSES3MetadataSerialization tests SSE-S3 metadata serialization and deserialization
+func TestSSES3MetadataSerialization(t *testing.T) {
+ // Initialize global key manager
+ globalSSES3KeyManager = NewSSES3KeyManager()
+ defer func() {
+ globalSSES3KeyManager = NewSSES3KeyManager()
+ }()
+
+ // Set up the key manager with a super key for testing
+ keyManager := GetSSES3KeyManager()
+ keyManager.superKey = make([]byte, 32)
+ for i := range keyManager.superKey {
+ keyManager.superKey[i] = byte(i)
+ }
+
+ // Generate SSE-S3 key
+ sseS3Key, err := GenerateSSES3Key()
+ if err != nil {
+ t.Fatalf("Failed to generate SSE-S3 key: %v", err)
+ }
+
+ // Add IV to the key
+ sseS3Key.IV = make([]byte, 16)
+ for i := range sseS3Key.IV {
+ sseS3Key.IV[i] = byte(i * 2)
+ }
+
+ // Serialize metadata
+ serialized, err := SerializeSSES3Metadata(sseS3Key)
+ if err != nil {
+ t.Fatalf("Failed to serialize SSE-S3 metadata: %v", err)
+ }
+
+ if len(serialized) == 0 {
+ t.Error("Serialized metadata is empty")
+ }
+
+ // Deserialize metadata
+ deserializedKey, err := DeserializeSSES3Metadata(serialized, keyManager)
+ if err != nil {
+ t.Fatalf("Failed to deserialize SSE-S3 metadata: %v", err)
+ }
+
+ // Verify key matches
+ if !bytes.Equal(deserializedKey.Key, sseS3Key.Key) {
+ t.Error("Deserialized key doesn't match original key")
+ }
+
+ // Verify IV matches
+ if !bytes.Equal(deserializedKey.IV, sseS3Key.IV) {
+ t.Error("Deserialized IV doesn't match original IV")
+ }
+
+ // Verify algorithm matches
+ if deserializedKey.Algorithm != sseS3Key.Algorithm {
+ t.Errorf("Algorithm mismatch: expected %s, got %s", sseS3Key.Algorithm, deserializedKey.Algorithm)
+ }
+
+ // Verify key ID matches
+ if deserializedKey.KeyID != sseS3Key.KeyID {
+ t.Errorf("Key ID mismatch: expected %s, got %s", sseS3Key.KeyID, deserializedKey.KeyID)
+ }
+}
+
+// TestDetectPrimarySSETypeS3 tests detection of SSE-S3 as primary encryption type
+func TestDetectPrimarySSETypeS3(t *testing.T) {
+ s3a := &S3ApiServer{}
+
+ testCases := []struct {
+ name string
+ entry *filer_pb.Entry
+ expected string
+ }{
+ {
+ name: "Single SSE-S3 chunk",
+ entry: &filer_pb.Entry{
+ Extended: map[string][]byte{
+ s3_constants.AmzServerSideEncryption: []byte("AES256"),
+ },
+ Attributes: &filer_pb.FuseAttributes{},
+ Chunks: []*filer_pb.FileChunk{
+ {
+ FileId: "1,123",
+ Offset: 0,
+ Size: 1024,
+ SseType: filer_pb.SSEType_SSE_S3,
+ SseMetadata: []byte("metadata"),
+ },
+ },
+ },
+ expected: s3_constants.SSETypeS3,
+ },
+ {
+ name: "Multiple SSE-S3 chunks",
+ entry: &filer_pb.Entry{
+ Extended: map[string][]byte{
+ s3_constants.AmzServerSideEncryption: []byte("AES256"),
+ },
+ Attributes: &filer_pb.FuseAttributes{},
+ Chunks: []*filer_pb.FileChunk{
+ {
+ FileId: "1,123",
+ Offset: 0,
+ Size: 1024,
+ SseType: filer_pb.SSEType_SSE_S3,
+ SseMetadata: []byte("metadata1"),
+ },
+ {
+ FileId: "2,456",
+ Offset: 1024,
+ Size: 1024,
+ SseType: filer_pb.SSEType_SSE_S3,
+ SseMetadata: []byte("metadata2"),
+ },
+ },
+ },
+ expected: s3_constants.SSETypeS3,
+ },
+ {
+ name: "Mixed SSE-S3 and SSE-KMS chunks (SSE-S3 majority)",
+ entry: &filer_pb.Entry{
+ Extended: map[string][]byte{
+ s3_constants.AmzServerSideEncryption: []byte("AES256"),
+ },
+ Attributes: &filer_pb.FuseAttributes{},
+ Chunks: []*filer_pb.FileChunk{
+ {
+ FileId: "1,123",
+ Offset: 0,
+ Size: 1024,
+ SseType: filer_pb.SSEType_SSE_S3,
+ SseMetadata: []byte("metadata1"),
+ },
+ {
+ FileId: "2,456",
+ Offset: 1024,
+ Size: 1024,
+ SseType: filer_pb.SSEType_SSE_S3,
+ SseMetadata: []byte("metadata2"),
+ },
+ {
+ FileId: "3,789",
+ Offset: 2048,
+ Size: 1024,
+ SseType: filer_pb.SSEType_SSE_KMS,
+ SseMetadata: []byte("metadata3"),
+ },
+ },
+ },
+ expected: s3_constants.SSETypeS3,
+ },
+ {
+ name: "No chunks, SSE-S3 metadata without KMS key ID",
+ entry: &filer_pb.Entry{
+ Extended: map[string][]byte{
+ s3_constants.AmzServerSideEncryption: []byte("AES256"),
+ },
+ Attributes: &filer_pb.FuseAttributes{},
+ Chunks: []*filer_pb.FileChunk{},
+ },
+ expected: s3_constants.SSETypeS3,
+ },
+ {
+ name: "No chunks, SSE-KMS metadata with KMS key ID",
+ entry: &filer_pb.Entry{
+ Extended: map[string][]byte{
+ s3_constants.AmzServerSideEncryption: []byte("AES256"),
+ s3_constants.AmzServerSideEncryptionAwsKmsKeyId: []byte("test-key-id"),
+ },
+ Attributes: &filer_pb.FuseAttributes{},
+ Chunks: []*filer_pb.FileChunk{},
+ },
+ expected: s3_constants.SSETypeKMS,
+ },
+ {
+ name: "SSE-C chunks",
+ entry: &filer_pb.Entry{
+ Extended: map[string][]byte{
+ s3_constants.AmzServerSideEncryptionCustomerAlgorithm: []byte("AES256"),
+ },
+ Attributes: &filer_pb.FuseAttributes{},
+ Chunks: []*filer_pb.FileChunk{
+ {
+ FileId: "1,123",
+ Offset: 0,
+ Size: 1024,
+ SseType: filer_pb.SSEType_SSE_C,
+ SseMetadata: []byte("metadata"),
+ },
+ },
+ },
+ expected: s3_constants.SSETypeC,
+ },
+ {
+ name: "Unencrypted",
+ entry: &filer_pb.Entry{
+ Extended: map[string][]byte{},
+ Attributes: &filer_pb.FuseAttributes{},
+ Chunks: []*filer_pb.FileChunk{
+ {
+ FileId: "1,123",
+ Offset: 0,
+ Size: 1024,
+ },
+ },
+ },
+ expected: "None",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ result := s3a.detectPrimarySSEType(tc.entry)
+ if result != tc.expected {
+ t.Errorf("Expected %s, got %s", tc.expected, result)
+ }
+ })
+ }
+}
+
+// TestAddSSES3HeadersToResponse tests that SSE-S3 headers are added to responses
+func TestAddSSES3HeadersToResponse(t *testing.T) {
+ s3a := &S3ApiServer{}
+
+ entry := &filer_pb.Entry{
+ Extended: map[string][]byte{
+ s3_constants.AmzServerSideEncryption: []byte("AES256"),
+ },
+ Attributes: &filer_pb.FuseAttributes{},
+ Chunks: []*filer_pb.FileChunk{
+ {
+ FileId: "1,123",
+ Offset: 0,
+ Size: 1024,
+ SseType: filer_pb.SSEType_SSE_S3,
+ SseMetadata: []byte("metadata"),
+ },
+ },
+ }
+
+ proxyResponse := &http.Response{
+ Header: make(http.Header),
+ }
+
+ s3a.addSSEHeadersToResponse(proxyResponse, entry)
+
+ algorithm := proxyResponse.Header.Get(s3_constants.AmzServerSideEncryption)
+ if algorithm != "AES256" {
+ t.Errorf("Expected SSE algorithm AES256, got %s", algorithm)
+ }
+
+ // Should NOT have SSE-C or SSE-KMS specific headers
+ if proxyResponse.Header.Get(s3_constants.AmzServerSideEncryptionCustomerAlgorithm) != "" {
+ t.Error("Should not have SSE-C customer algorithm header")
+ }
+
+ if proxyResponse.Header.Get(s3_constants.AmzServerSideEncryptionAwsKmsKeyId) != "" {
+ t.Error("Should not have SSE-KMS key ID header")
+ }
+}
+
+// TestSSES3EncryptionWithBaseIV tests multipart encryption with base IV
+func TestSSES3EncryptionWithBaseIV(t *testing.T) {
+ // Generate SSE-S3 key
+ sseS3Key, err := GenerateSSES3Key()
+ if err != nil {
+ t.Fatalf("Failed to generate SSE-S3 key: %v", err)
+ }
+
+ // Generate base IV
+ baseIV := make([]byte, 16)
+ for i := range baseIV {
+ baseIV[i] = byte(i)
+ }
+
+ // Test data for two parts
+ testData1 := []byte("Part 1 of multipart upload test.")
+ testData2 := []byte("Part 2 of multipart upload test.")
+
+ // Encrypt part 1 at offset 0
+ dataReader1 := bytes.NewReader(testData1)
+ encryptedReader1, iv1, err := CreateSSES3EncryptedReaderWithBaseIV(dataReader1, sseS3Key, baseIV, 0)
+ if err != nil {
+ t.Fatalf("Failed to create encrypted reader for part 1: %v", err)
+ }
+
+ encryptedData1, err := io.ReadAll(encryptedReader1)
+ if err != nil {
+ t.Fatalf("Failed to read encrypted data for part 1: %v", err)
+ }
+
+ // Encrypt part 2 at offset (simulating second part)
+ dataReader2 := bytes.NewReader(testData2)
+ offset2 := int64(len(testData1))
+ encryptedReader2, iv2, err := CreateSSES3EncryptedReaderWithBaseIV(dataReader2, sseS3Key, baseIV, offset2)
+ if err != nil {
+ t.Fatalf("Failed to create encrypted reader for part 2: %v", err)
+ }
+
+ encryptedData2, err := io.ReadAll(encryptedReader2)
+ if err != nil {
+ t.Fatalf("Failed to read encrypted data for part 2: %v", err)
+ }
+
+ // IVs should be different (offset-based)
+ if bytes.Equal(iv1, iv2) {
+ t.Error("IVs should be different for different offsets")
+ }
+
+ // Decrypt part 1
+ decryptedReader1, err := CreateSSES3DecryptedReader(bytes.NewReader(encryptedData1), sseS3Key, iv1)
+ if err != nil {
+ t.Fatalf("Failed to create decrypted reader for part 1: %v", err)
+ }
+
+ decryptedData1, err := io.ReadAll(decryptedReader1)
+ if err != nil {
+ t.Fatalf("Failed to read decrypted data for part 1: %v", err)
+ }
+
+ // Decrypt part 2
+ decryptedReader2, err := CreateSSES3DecryptedReader(bytes.NewReader(encryptedData2), sseS3Key, iv2)
+ if err != nil {
+ t.Fatalf("Failed to create decrypted reader for part 2: %v", err)
+ }
+
+ decryptedData2, err := io.ReadAll(decryptedReader2)
+ if err != nil {
+ t.Fatalf("Failed to read decrypted data for part 2: %v", err)
+ }
+
+ // Verify decrypted data matches original
+ if !bytes.Equal(decryptedData1, testData1) {
+ t.Errorf("Decrypted part 1 doesn't match original.\nOriginal: %s\nDecrypted: %s", testData1, decryptedData1)
+ }
+
+ if !bytes.Equal(decryptedData2, testData2) {
+ t.Errorf("Decrypted part 2 doesn't match original.\nOriginal: %s\nDecrypted: %s", testData2, decryptedData2)
+ }
+}
+
+// TestSSES3WrongKeyDecryption tests that wrong key fails decryption
+func TestSSES3WrongKeyDecryption(t *testing.T) {
+ // Generate two different keys
+ sseS3Key1, err := GenerateSSES3Key()
+ if err != nil {
+ t.Fatalf("Failed to generate SSE-S3 key 1: %v", err)
+ }
+
+ sseS3Key2, err := GenerateSSES3Key()
+ if err != nil {
+ t.Fatalf("Failed to generate SSE-S3 key 2: %v", err)
+ }
+
+ // Test data
+ testData := []byte("Secret data encrypted with key 1")
+
+ // Encrypt with key 1
+ dataReader := bytes.NewReader(testData)
+ encryptedReader, iv, err := CreateSSES3EncryptedReader(dataReader, sseS3Key1)
+ if err != nil {
+ t.Fatalf("Failed to create encrypted reader: %v", err)
+ }
+
+ encryptedData, err := io.ReadAll(encryptedReader)
+ if err != nil {
+ t.Fatalf("Failed to read encrypted data: %v", err)
+ }
+
+ // Try to decrypt with key 2 (wrong key)
+ decryptedReader, err := CreateSSES3DecryptedReader(bytes.NewReader(encryptedData), sseS3Key2, iv)
+ if err != nil {
+ t.Fatalf("Failed to create decrypted reader: %v", err)
+ }
+
+ decryptedData, err := io.ReadAll(decryptedReader)
+ if err != nil {
+ t.Fatalf("Failed to read decrypted data: %v", err)
+ }
+
+ // Decrypted data should NOT match original (wrong key produces garbage)
+ if bytes.Equal(decryptedData, testData) {
+ t.Error("Decryption with wrong key should not produce correct plaintext")
+ }
+}
+
+// TestSSES3KeyGeneration tests SSE-S3 key generation
+func TestSSES3KeyGeneration(t *testing.T) {
+ // Generate multiple keys
+ keys := make([]*SSES3Key, 10)
+ for i := range keys {
+ key, err := GenerateSSES3Key()
+ if err != nil {
+ t.Fatalf("Failed to generate SSE-S3 key %d: %v", i, err)
+ }
+ keys[i] = key
+
+ // Verify key properties
+ if len(key.Key) != SSES3KeySize {
+ t.Errorf("Key %d has wrong size: expected %d, got %d", i, SSES3KeySize, len(key.Key))
+ }
+
+ if key.Algorithm != SSES3Algorithm {
+ t.Errorf("Key %d has wrong algorithm: expected %s, got %s", i, SSES3Algorithm, key.Algorithm)
+ }
+
+ if key.KeyID == "" {
+ t.Errorf("Key %d has empty key ID", i)
+ }
+ }
+
+ // Verify keys are unique
+ for i := 0; i < len(keys); i++ {
+ for j := i + 1; j < len(keys); j++ {
+ if bytes.Equal(keys[i].Key, keys[j].Key) {
+ t.Errorf("Keys %d and %d are identical (should be unique)", i, j)
+ }
+ if keys[i].KeyID == keys[j].KeyID {
+ t.Errorf("Key IDs %d and %d are identical (should be unique)", i, j)
+ }
+ }
+ }
+}
+
+// TestSSES3VariousSizes tests SSE-S3 encryption/decryption with various data sizes
+func TestSSES3VariousSizes(t *testing.T) {
+ sizes := []int{1, 15, 16, 17, 100, 1024, 4096, 1048576}
+
+ for _, size := range sizes {
+ t.Run(fmt.Sprintf("size_%d", size), func(t *testing.T) {
+ // Generate test data
+ testData := make([]byte, size)
+ for i := range testData {
+ testData[i] = byte(i % 256)
+ }
+
+ // Generate key
+ sseS3Key, err := GenerateSSES3Key()
+ if err != nil {
+ t.Fatalf("Failed to generate SSE-S3 key: %v", err)
+ }
+
+ // Encrypt
+ dataReader := bytes.NewReader(testData)
+ encryptedReader, iv, err := CreateSSES3EncryptedReader(dataReader, sseS3Key)
+ if err != nil {
+ t.Fatalf("Failed to create encrypted reader: %v", err)
+ }
+
+ encryptedData, err := io.ReadAll(encryptedReader)
+ if err != nil {
+ t.Fatalf("Failed to read encrypted data: %v", err)
+ }
+
+ // Verify encrypted size matches original
+ if len(encryptedData) != size {
+ t.Errorf("Encrypted size mismatch: expected %d, got %d", size, len(encryptedData))
+ }
+
+ // Decrypt
+ decryptedReader, err := CreateSSES3DecryptedReader(bytes.NewReader(encryptedData), sseS3Key, iv)
+ if err != nil {
+ t.Fatalf("Failed to create decrypted reader: %v", err)
+ }
+
+ decryptedData, err := io.ReadAll(decryptedReader)
+ if err != nil {
+ t.Fatalf("Failed to read decrypted data: %v", err)
+ }
+
+ // Verify
+ if !bytes.Equal(decryptedData, testData) {
+ t.Errorf("Decrypted data doesn't match original for size %d", size)
+ }
+ })
+ }
+}
+
+// TestSSES3ResponseHeaders tests that SSE-S3 response headers are set correctly
+func TestSSES3ResponseHeaders(t *testing.T) {
+ w := httptest.NewRecorder()
+
+ // Simulate setting SSE-S3 response headers
+ w.Header().Set(s3_constants.AmzServerSideEncryption, SSES3Algorithm)
+
+ // Verify headers
+ algorithm := w.Header().Get(s3_constants.AmzServerSideEncryption)
+ if algorithm != "AES256" {
+ t.Errorf("Expected algorithm AES256, got %s", algorithm)
+ }
+
+ // Should NOT have customer key headers
+ if w.Header().Get(s3_constants.AmzServerSideEncryptionCustomerAlgorithm) != "" {
+ t.Error("Should not have SSE-C customer algorithm header")
+ }
+
+ if w.Header().Get(s3_constants.AmzServerSideEncryptionCustomerKeyMD5) != "" {
+ t.Error("Should not have SSE-C customer key MD5 header")
+ }
+
+ // Should NOT have KMS key ID
+ if w.Header().Get(s3_constants.AmzServerSideEncryptionAwsKmsKeyId) != "" {
+ t.Error("Should not have SSE-KMS key ID header")
+ }
+}
+
+// TestSSES3IsEncryptedInternal tests detection of SSE-S3 encryption from metadata
+func TestSSES3IsEncryptedInternal(t *testing.T) {
+ testCases := []struct {
+ name string
+ metadata map[string][]byte
+ expected bool
+ }{
+ {
+ name: "Empty metadata",
+ metadata: map[string][]byte{},
+ expected: false,
+ },
+ {
+ name: "Valid SSE-S3 metadata",
+ metadata: map[string][]byte{
+ s3_constants.AmzServerSideEncryption: []byte("AES256"),
+ },
+ expected: true,
+ },
+ {
+ name: "SSE-KMS metadata",
+ metadata: map[string][]byte{
+ s3_constants.AmzServerSideEncryption: []byte("aws:kms"),
+ },
+ expected: false,
+ },
+ {
+ name: "SSE-C metadata",
+ metadata: map[string][]byte{
+ s3_constants.AmzServerSideEncryptionCustomerAlgorithm: []byte("AES256"),
+ },
+ expected: false,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ result := IsSSES3EncryptedInternal(tc.metadata)
+ if result != tc.expected {
+ t.Errorf("Expected %v, got %v", tc.expected, result)
+ }
+ })
+ }
+}
+
+// TestSSES3InvalidMetadataDeserialization tests error handling for invalid metadata
+func TestSSES3InvalidMetadataDeserialization(t *testing.T) {
+ keyManager := NewSSES3KeyManager()
+ keyManager.superKey = make([]byte, 32)
+
+ testCases := []struct {
+ name string
+ metadata []byte
+ shouldError bool
+ }{
+ {
+ name: "Empty metadata",
+ metadata: []byte{},
+ shouldError: true,
+ },
+ {
+ name: "Invalid JSON",
+ metadata: []byte("not valid json"),
+ shouldError: true,
+ },
+ {
+ name: "Missing keyId",
+ metadata: []byte(`{"algorithm":"AES256"}`),
+ shouldError: true,
+ },
+ {
+ name: "Invalid base64 encrypted DEK",
+ metadata: []byte(`{"keyId":"test","algorithm":"AES256","encryptedDEK":"not-valid-base64!","nonce":"dGVzdA=="}`),
+ shouldError: true,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ _, err := DeserializeSSES3Metadata(tc.metadata, keyManager)
+ if tc.shouldError && err == nil {
+ t.Error("Expected error but got none")
+ }
+ if !tc.shouldError && err != nil {
+ t.Errorf("Unexpected error: %v", err)
+ }
+ })
+ }
+}
+
+// TestGetSSES3Headers tests SSE-S3 header generation
+func TestGetSSES3Headers(t *testing.T) {
+ headers := GetSSES3Headers()
+
+ if len(headers) == 0 {
+ t.Error("Expected headers to be non-empty")
+ }
+
+ algorithm, exists := headers[s3_constants.AmzServerSideEncryption]
+ if !exists {
+ t.Error("Expected AmzServerSideEncryption header to exist")
+ }
+
+ if algorithm != "AES256" {
+ t.Errorf("Expected algorithm AES256, got %s", algorithm)
+ }
+}
+
+// TestProcessSSES3Request tests processing of SSE-S3 requests
+func TestProcessSSES3Request(t *testing.T) {
+ // Initialize global key manager
+ globalSSES3KeyManager = NewSSES3KeyManager()
+ defer func() {
+ globalSSES3KeyManager = NewSSES3KeyManager()
+ }()
+
+ // Set up the key manager with a super key for testing
+ keyManager := GetSSES3KeyManager()
+ keyManager.superKey = make([]byte, 32)
+ for i := range keyManager.superKey {
+ keyManager.superKey[i] = byte(i)
+ }
+
+ // Create SSE-S3 request
+ req := httptest.NewRequest("PUT", "/bucket/object", nil)
+ req.Header.Set(s3_constants.AmzServerSideEncryption, "AES256")
+
+ // Process request
+ metadata, err := ProcessSSES3Request(req)
+ if err != nil {
+ t.Fatalf("Failed to process SSE-S3 request: %v", err)
+ }
+
+ if metadata == nil {
+ t.Fatal("Expected metadata to be non-nil")
+ }
+
+ // Verify metadata contains SSE algorithm
+ if sseAlgo, exists := metadata[s3_constants.AmzServerSideEncryption]; !exists {
+ t.Error("Expected SSE algorithm in metadata")
+ } else if string(sseAlgo) != "AES256" {
+ t.Errorf("Expected AES256, got %s", string(sseAlgo))
+ }
+
+ // Verify metadata contains key data
+ if _, exists := metadata[s3_constants.SeaweedFSSSES3Key]; !exists {
+ t.Error("Expected SSE-S3 key data in metadata")
+ }
+}
+
+// TestGetSSES3KeyFromMetadata tests extraction of SSE-S3 key from metadata
+func TestGetSSES3KeyFromMetadata(t *testing.T) {
+ // Initialize global key manager
+ globalSSES3KeyManager = NewSSES3KeyManager()
+ defer func() {
+ globalSSES3KeyManager = NewSSES3KeyManager()
+ }()
+
+ // Set up the key manager with a super key for testing
+ keyManager := GetSSES3KeyManager()
+ keyManager.superKey = make([]byte, 32)
+ for i := range keyManager.superKey {
+ keyManager.superKey[i] = byte(i)
+ }
+
+ // Generate and serialize key
+ sseS3Key, err := GenerateSSES3Key()
+ if err != nil {
+ t.Fatalf("Failed to generate SSE-S3 key: %v", err)
+ }
+
+ sseS3Key.IV = make([]byte, 16)
+ for i := range sseS3Key.IV {
+ sseS3Key.IV[i] = byte(i)
+ }
+
+ serialized, err := SerializeSSES3Metadata(sseS3Key)
+ if err != nil {
+ t.Fatalf("Failed to serialize SSE-S3 metadata: %v", err)
+ }
+
+ metadata := map[string][]byte{
+ s3_constants.SeaweedFSSSES3Key: serialized,
+ }
+
+ // Extract key
+ extractedKey, err := GetSSES3KeyFromMetadata(metadata, keyManager)
+ if err != nil {
+ t.Fatalf("Failed to get SSE-S3 key from metadata: %v", err)
+ }
+
+ // Verify key matches
+ if !bytes.Equal(extractedKey.Key, sseS3Key.Key) {
+ t.Error("Extracted key doesn't match original key")
+ }
+
+ if !bytes.Equal(extractedKey.IV, sseS3Key.IV) {
+ t.Error("Extracted IV doesn't match original IV")
+ }
+}
+
+// TestSSES3EnvelopeEncryption tests that envelope encryption works correctly
+func TestSSES3EnvelopeEncryption(t *testing.T) {
+ // Initialize key manager with a super key
+ keyManager := NewSSES3KeyManager()
+ keyManager.superKey = make([]byte, 32)
+ for i := range keyManager.superKey {
+ keyManager.superKey[i] = byte(i + 100)
+ }
+
+ // Generate a DEK
+ dek := make([]byte, 32)
+ for i := range dek {
+ dek[i] = byte(i)
+ }
+
+ // Encrypt DEK with super key
+ encryptedDEK, nonce, err := keyManager.encryptKeyWithSuperKey(dek)
+ if err != nil {
+ t.Fatalf("Failed to encrypt DEK: %v", err)
+ }
+
+ if len(encryptedDEK) == 0 {
+ t.Error("Encrypted DEK is empty")
+ }
+
+ if len(nonce) == 0 {
+ t.Error("Nonce is empty")
+ }
+
+ // Decrypt DEK with super key
+ decryptedDEK, err := keyManager.decryptKeyWithSuperKey(encryptedDEK, nonce)
+ if err != nil {
+ t.Fatalf("Failed to decrypt DEK: %v", err)
+ }
+
+ // Verify DEK matches
+ if !bytes.Equal(decryptedDEK, dek) {
+ t.Error("Decrypted DEK doesn't match original DEK")
+ }
+}
+
+// TestValidateSSES3Key tests SSE-S3 key validation
+func TestValidateSSES3Key(t *testing.T) {
+ testCases := []struct {
+ name string
+ key *SSES3Key
+ shouldError bool
+ errorMsg string
+ }{
+ {
+ name: "Nil key",
+ key: nil,
+ shouldError: true,
+ errorMsg: "SSE-S3 key cannot be nil",
+ },
+ {
+ name: "Valid key",
+ key: &SSES3Key{
+ Key: make([]byte, 32),
+ KeyID: "test-key",
+ Algorithm: "AES256",
+ },
+ shouldError: false,
+ },
+ {
+ name: "Valid key with IV",
+ key: &SSES3Key{
+ Key: make([]byte, 32),
+ KeyID: "test-key",
+ Algorithm: "AES256",
+ IV: make([]byte, 16),
+ },
+ shouldError: false,
+ },
+ {
+ name: "Invalid key size (too small)",
+ key: &SSES3Key{
+ Key: make([]byte, 16),
+ KeyID: "test-key",
+ Algorithm: "AES256",
+ },
+ shouldError: true,
+ errorMsg: "invalid SSE-S3 key size",
+ },
+ {
+ name: "Invalid key size (too large)",
+ key: &SSES3Key{
+ Key: make([]byte, 64),
+ KeyID: "test-key",
+ Algorithm: "AES256",
+ },
+ shouldError: true,
+ errorMsg: "invalid SSE-S3 key size",
+ },
+ {
+ name: "Nil key bytes",
+ key: &SSES3Key{
+ Key: nil,
+ KeyID: "test-key",
+ Algorithm: "AES256",
+ },
+ shouldError: true,
+ errorMsg: "SSE-S3 key bytes cannot be nil",
+ },
+ {
+ name: "Empty key ID",
+ key: &SSES3Key{
+ Key: make([]byte, 32),
+ KeyID: "",
+ Algorithm: "AES256",
+ },
+ shouldError: true,
+ errorMsg: "SSE-S3 key ID cannot be empty",
+ },
+ {
+ name: "Invalid algorithm",
+ key: &SSES3Key{
+ Key: make([]byte, 32),
+ KeyID: "test-key",
+ Algorithm: "INVALID",
+ },
+ shouldError: true,
+ errorMsg: "invalid SSE-S3 algorithm",
+ },
+ {
+ name: "Invalid IV length",
+ key: &SSES3Key{
+ Key: make([]byte, 32),
+ KeyID: "test-key",
+ Algorithm: "AES256",
+ IV: make([]byte, 8), // Wrong size
+ },
+ shouldError: true,
+ errorMsg: "invalid SSE-S3 IV length",
+ },
+ {
+ name: "Empty IV is allowed (set during encryption)",
+ key: &SSES3Key{
+ Key: make([]byte, 32),
+ KeyID: "test-key",
+ Algorithm: "AES256",
+ IV: []byte{}, // Empty is OK
+ },
+ shouldError: false,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ err := ValidateSSES3Key(tc.key)
+ if tc.shouldError {
+ if err == nil {
+ t.Error("Expected error but got none")
+ } else if tc.errorMsg != "" && !strings.Contains(err.Error(), tc.errorMsg) {
+ t.Errorf("Expected error containing %q, got: %v", tc.errorMsg, err)
+ }
+ } else {
+ if err != nil {
+ t.Errorf("Unexpected error: %v", err)
+ }
+ }
+ })
+ }
+}
diff --git a/weed/s3api/s3_sse_test_utils_test.go b/weed/s3api/s3_sse_test_utils_test.go
index 1c57be791..a4c52994a 100644
--- a/weed/s3api/s3_sse_test_utils_test.go
+++ b/weed/s3api/s3_sse_test_utils_test.go
@@ -115,7 +115,7 @@ func CreateTestMetadataWithSSEC(keyPair *TestKeyPair) map[string][]byte {
for i := range iv {
iv[i] = byte(i)
}
- StoreIVInMetadata(metadata, iv)
+ StoreSSECIVInMetadata(metadata, iv)
return metadata
}
diff --git a/weed/s3api/s3_validation_utils.go b/weed/s3api/s3_validation_utils.go
index da53342b1..e0e80d0a8 100644
--- a/weed/s3api/s3_validation_utils.go
+++ b/weed/s3api/s3_validation_utils.go
@@ -66,10 +66,35 @@ func ValidateSSECKey(customerKey *SSECustomerKey) error {
return nil
}
-// ValidateSSES3Key validates that an SSE-S3 key is not nil
+// ValidateSSES3Key validates that an SSE-S3 key has valid structure and contents
func ValidateSSES3Key(sseKey *SSES3Key) error {
if sseKey == nil {
return fmt.Errorf("SSE-S3 key cannot be nil")
}
+
+ // Validate key bytes
+ if sseKey.Key == nil {
+ return fmt.Errorf("SSE-S3 key bytes cannot be nil")
+ }
+ if len(sseKey.Key) != SSES3KeySize {
+ return fmt.Errorf("invalid SSE-S3 key size: expected %d bytes, got %d", SSES3KeySize, len(sseKey.Key))
+ }
+
+ // Validate algorithm
+ if sseKey.Algorithm != SSES3Algorithm {
+ return fmt.Errorf("invalid SSE-S3 algorithm: expected %q, got %q", SSES3Algorithm, sseKey.Algorithm)
+ }
+
+ // Validate key ID (should not be empty)
+ if sseKey.KeyID == "" {
+ return fmt.Errorf("SSE-S3 key ID cannot be empty")
+ }
+
+ // IV validation is optional during key creation - it will be set during encryption
+ // If IV is set, validate its length
+ if len(sseKey.IV) > 0 && len(sseKey.IV) != s3_constants.AESBlockSize {
+ return fmt.Errorf("invalid SSE-S3 IV length: expected %d bytes, got %d", s3_constants.AESBlockSize, len(sseKey.IV))
+ }
+
return nil
}
diff --git a/weed/s3api/s3api_key_rotation.go b/weed/s3api/s3api_key_rotation.go
index e8d29ff7a..499505678 100644
--- a/weed/s3api/s3api_key_rotation.go
+++ b/weed/s3api/s3api_key_rotation.go
@@ -100,9 +100,9 @@ func (s3a *S3ApiServer) rotateSSEKMSMetadataOnly(entry *filer_pb.Entry, srcKeyID
// rotateSSECChunks re-encrypts all chunks with new SSE-C key
func (s3a *S3ApiServer) rotateSSECChunks(entry *filer_pb.Entry, sourceKey, destKey *SSECustomerKey) ([]*filer_pb.FileChunk, error) {
// Get IV from entry metadata
- iv, err := GetIVFromMetadata(entry.Extended)
+ iv, err := GetSSECIVFromMetadata(entry.Extended)
if err != nil {
- return nil, fmt.Errorf("get IV from metadata: %w", err)
+ return nil, fmt.Errorf("get SSE-C IV from metadata: %w", err)
}
var rotatedChunks []*filer_pb.FileChunk
@@ -125,7 +125,7 @@ func (s3a *S3ApiServer) rotateSSECChunks(entry *filer_pb.Entry, sourceKey, destK
if entry.Extended == nil {
entry.Extended = make(map[string][]byte)
}
- StoreIVInMetadata(entry.Extended, newIV)
+ StoreSSECIVInMetadata(entry.Extended, newIV)
entry.Extended[s3_constants.AmzServerSideEncryptionCustomerAlgorithm] = []byte("AES256")
entry.Extended[s3_constants.AmzServerSideEncryptionCustomerKeyMD5] = []byte(destKey.KeyMD5)
diff --git a/weed/s3api/s3api_object_handlers.go b/weed/s3api/s3api_object_handlers.go
index f30522292..163633e22 100644
--- a/weed/s3api/s3api_object_handlers.go
+++ b/weed/s3api/s3api_object_handlers.go
@@ -278,11 +278,11 @@ func (s3a *S3ApiServer) GetObjectHandler(w http.ResponseWriter, r *http.Request)
glog.V(1).Infof("GetObject: bucket %s, object %s, versioningConfigured=%v, versionId=%s", bucket, object, versioningConfigured, versionId)
var destUrl string
+ var entry *filer_pb.Entry // Declare entry at function scope for SSE processing
if versioningConfigured {
// Handle versioned GET - all versions are stored in .versions directory
var targetVersionId string
- var entry *filer_pb.Entry
if versionId != "" {
// Request for specific version
@@ -363,6 +363,14 @@ func (s3a *S3ApiServer) GetObjectHandler(w http.ResponseWriter, r *http.Request)
}
}
+ // Fetch the correct entry for SSE processing (respects versionId)
+ objectEntryForSSE, err := s3a.getObjectEntryForSSE(r, versioningConfigured, entry)
+ if err != nil {
+ glog.Errorf("GetObjectHandler: %v", err)
+ s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
+ return
+ }
+
s3a.proxyToFiler(w, r, destUrl, false, func(proxyResponse *http.Response, w http.ResponseWriter) (statusCode int, bytesTransferred int64) {
// Restore the original Range header for SSE processing
if sseObject && originalRangeHeader != "" {
@@ -371,14 +379,12 @@ func (s3a *S3ApiServer) GetObjectHandler(w http.ResponseWriter, r *http.Request)
}
// Add SSE metadata headers based on object metadata before SSE processing
- bucket, object := s3_constants.GetBucketAndObject(r)
- objectPath := fmt.Sprintf("%s/%s%s", s3a.option.BucketsPath, bucket, object)
- if objectEntry, err := s3a.getEntry("", objectPath); err == nil {
- s3a.addSSEHeadersToResponse(proxyResponse, objectEntry)
+ if objectEntryForSSE != nil {
+ s3a.addSSEHeadersToResponse(proxyResponse, objectEntryForSSE)
}
// Handle SSE decryption (both SSE-C and SSE-KMS) if needed
- return s3a.handleSSEResponse(r, proxyResponse, w)
+ return s3a.handleSSEResponse(r, proxyResponse, w, objectEntryForSSE)
})
}
@@ -422,11 +428,11 @@ func (s3a *S3ApiServer) HeadObjectHandler(w http.ResponseWriter, r *http.Request
}
var destUrl string
+ var entry *filer_pb.Entry // Declare entry at function scope for SSE processing
if versioningConfigured {
// Handle versioned HEAD - all versions are stored in .versions directory
var targetVersionId string
- var entry *filer_pb.Entry
if versionId != "" {
// Request for specific version
@@ -488,9 +494,17 @@ func (s3a *S3ApiServer) HeadObjectHandler(w http.ResponseWriter, r *http.Request
destUrl = s3a.toFilerUrl(bucket, object)
}
+ // Fetch the correct entry for SSE processing (respects versionId)
+ objectEntryForSSE, err := s3a.getObjectEntryForSSE(r, versioningConfigured, entry)
+ if err != nil {
+ glog.Errorf("HeadObjectHandler: %v", err)
+ s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
+ return
+ }
+
s3a.proxyToFiler(w, r, destUrl, false, func(proxyResponse *http.Response, w http.ResponseWriter) (statusCode int, bytesTransferred int64) {
// Handle SSE validation (both SSE-C and SSE-KMS) for HEAD requests
- return s3a.handleSSEResponse(r, proxyResponse, w)
+ return s3a.handleSSEResponse(r, proxyResponse, w, objectEntryForSSE)
})
}
@@ -646,20 +660,53 @@ func writeFinalResponse(w http.ResponseWriter, proxyResponse *http.Response, bod
return statusCode, bytesTransferred
}
-func passThroughResponse(proxyResponse *http.Response, w http.ResponseWriter) (statusCode int, bytesTransferred int64) {
- // Capture existing CORS headers that may have been set by middleware
- capturedCORSHeaders := captureCORSHeaders(w, corsHeaders)
+// getObjectEntryForSSE fetches the correct filer entry for SSE processing
+// For versioned objects, it reuses the already-fetched entry
+// For non-versioned objects, it fetches the entry from the filer
+func (s3a *S3ApiServer) getObjectEntryForSSE(r *http.Request, versioningConfigured bool, versionedEntry *filer_pb.Entry) (*filer_pb.Entry, error) {
+ if versioningConfigured {
+ // For versioned objects, we already have the correct entry
+ return versionedEntry, nil
+ }
- // Copy headers from proxy response
+ // For non-versioned objects, fetch the entry
+ bucket, object := s3_constants.GetBucketAndObject(r)
+ objectPath := fmt.Sprintf("%s/%s%s", s3a.option.BucketsPath, bucket, object)
+ fetchedEntry, err := s3a.getEntry("", objectPath)
+ if err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
+ return nil, fmt.Errorf("failed to get entry for SSE check %s: %w", objectPath, err)
+ }
+ return fetchedEntry, nil
+}
+
+// copyResponseHeaders copies headers from proxy response to the response writer,
+// excluding internal SeaweedFS headers and optionally excluding body-related headers
+func copyResponseHeaders(w http.ResponseWriter, proxyResponse *http.Response, excludeBodyHeaders bool) {
for k, v := range proxyResponse.Header {
+ // Always exclude internal SeaweedFS headers
+ if s3_constants.IsSeaweedFSInternalHeader(k) {
+ continue
+ }
+ // Optionally exclude body-related headers that might change after decryption
+ if excludeBodyHeaders && (k == "Content-Length" || k == "Content-Encoding") {
+ continue
+ }
w.Header()[k] = v
}
+}
+
+func passThroughResponse(proxyResponse *http.Response, w http.ResponseWriter) (statusCode int, bytesTransferred int64) {
+ // Capture existing CORS headers that may have been set by middleware
+ capturedCORSHeaders := captureCORSHeaders(w, corsHeaders)
+
+ // Copy headers from proxy response (excluding internal SeaweedFS headers)
+ copyResponseHeaders(w, proxyResponse, false)
return writeFinalResponse(w, proxyResponse, proxyResponse.Body, capturedCORSHeaders)
}
// handleSSECResponse handles SSE-C decryption and response processing
-func (s3a *S3ApiServer) handleSSECResponse(r *http.Request, proxyResponse *http.Response, w http.ResponseWriter) (statusCode int, bytesTransferred int64) {
+func (s3a *S3ApiServer) handleSSECResponse(r *http.Request, proxyResponse *http.Response, w http.ResponseWriter, entry *filer_pb.Entry) (statusCode int, bytesTransferred int64) {
// Check if the object has SSE-C metadata
sseAlgorithm := proxyResponse.Header.Get(s3_constants.AmzServerSideEncryptionCustomerAlgorithm)
sseKeyMD5 := proxyResponse.Header.Get(s3_constants.AmzServerSideEncryptionCustomerKeyMD5)
@@ -692,9 +739,8 @@ func (s3a *S3ApiServer) handleSSECResponse(r *http.Request, proxyResponse *http.
// Range requests will be handled by the filer layer with proper offset-based decryption
// Check if this is a chunked or small content SSE-C object
- bucket, object := s3_constants.GetBucketAndObject(r)
- objectPath := fmt.Sprintf("%s/%s%s", s3a.option.BucketsPath, bucket, object)
- if entry, err := s3a.getEntry("", objectPath); err == nil {
+ // Use the entry parameter passed from the caller (avoids redundant lookup)
+ if entry != nil {
// Check for SSE-C chunks
sseCChunks := 0
for _, chunk := range entry.GetChunks() {
@@ -716,10 +762,8 @@ func (s3a *S3ApiServer) handleSSECResponse(r *http.Request, proxyResponse *http.
// Capture existing CORS headers
capturedCORSHeaders := captureCORSHeaders(w, corsHeaders)
- // Copy headers from proxy response
- for k, v := range proxyResponse.Header {
- w.Header()[k] = v
- }
+ // Copy headers from proxy response (excluding internal SeaweedFS headers)
+ copyResponseHeaders(w, proxyResponse, false)
// Set proper headers for range requests
rangeHeader := r.Header.Get("Range")
@@ -785,12 +829,8 @@ func (s3a *S3ApiServer) handleSSECResponse(r *http.Request, proxyResponse *http.
// Capture existing CORS headers that may have been set by middleware
capturedCORSHeaders := captureCORSHeaders(w, corsHeaders)
- // Copy headers from proxy response (excluding body-related headers that might change)
- for k, v := range proxyResponse.Header {
- if k != "Content-Length" && k != "Content-Encoding" {
- w.Header()[k] = v
- }
- }
+ // Copy headers from proxy response (excluding body-related headers that might change and internal SeaweedFS headers)
+ copyResponseHeaders(w, proxyResponse, true)
// Set correct Content-Length for SSE-C (only for full object requests)
// With IV stored in metadata, the encrypted length equals the original length
@@ -821,29 +861,37 @@ func (s3a *S3ApiServer) handleSSECResponse(r *http.Request, proxyResponse *http.
}
// handleSSEResponse handles both SSE-C and SSE-KMS decryption/validation and response processing
-func (s3a *S3ApiServer) handleSSEResponse(r *http.Request, proxyResponse *http.Response, w http.ResponseWriter) (statusCode int, bytesTransferred int64) {
+// The objectEntry parameter should be the correct entry for the requested version (if versioned)
+func (s3a *S3ApiServer) handleSSEResponse(r *http.Request, proxyResponse *http.Response, w http.ResponseWriter, objectEntry *filer_pb.Entry) (statusCode int, bytesTransferred int64) {
// Check what the client is expecting based on request headers
clientExpectsSSEC := IsSSECRequest(r)
// Check what the stored object has in headers (may be conflicting after copy)
kmsMetadataHeader := proxyResponse.Header.Get(s3_constants.SeaweedFSSSEKMSKeyHeader)
- sseAlgorithm := proxyResponse.Header.Get(s3_constants.AmzServerSideEncryptionCustomerAlgorithm)
- // Get actual object state by examining chunks (most reliable for cross-encryption)
- bucket, object := s3_constants.GetBucketAndObject(r)
- objectPath := fmt.Sprintf("%s/%s%s", s3a.option.BucketsPath, bucket, object)
+ // Detect actual object SSE type from the provided entry (respects versionId)
actualObjectType := "Unknown"
- if objectEntry, err := s3a.getEntry("", objectPath); err == nil {
+ if objectEntry != nil {
actualObjectType = s3a.detectPrimarySSEType(objectEntry)
}
+ // If objectEntry is nil, we cannot determine SSE type from chunks
+ // This should only happen for 404s which will be handled by the proxy
+ if objectEntry == nil {
+ glog.V(4).Infof("Object entry not available for SSE routing, passing through")
+ return passThroughResponse(proxyResponse, w)
+ }
+
// Route based on ACTUAL object type (from chunks) rather than conflicting headers
if actualObjectType == s3_constants.SSETypeC && clientExpectsSSEC {
// Object is SSE-C and client expects SSE-C → SSE-C handler
- return s3a.handleSSECResponse(r, proxyResponse, w)
+ return s3a.handleSSECResponse(r, proxyResponse, w, objectEntry)
} else if actualObjectType == s3_constants.SSETypeKMS && !clientExpectsSSEC {
// Object is SSE-KMS and client doesn't expect SSE-C → SSE-KMS handler
- return s3a.handleSSEKMSResponse(r, proxyResponse, w, kmsMetadataHeader)
+ return s3a.handleSSEKMSResponse(r, proxyResponse, w, objectEntry, kmsMetadataHeader)
+ } else if actualObjectType == s3_constants.SSETypeS3 && !clientExpectsSSEC {
+ // Object is SSE-S3 and client doesn't expect SSE-C → SSE-S3 handler
+ return s3a.handleSSES3Response(r, proxyResponse, w, objectEntry)
} else if actualObjectType == "None" && !clientExpectsSSEC {
// Object is unencrypted and client doesn't expect SSE-C → pass through
return passThroughResponse(proxyResponse, w)
@@ -855,24 +903,23 @@ func (s3a *S3ApiServer) handleSSEResponse(r *http.Request, proxyResponse *http.R
// Object is SSE-KMS but client provides SSE-C headers → Error
s3err.WriteErrorResponse(w, r, s3err.ErrSSECustomerKeyMissing)
return http.StatusBadRequest, 0
+ } else if actualObjectType == s3_constants.SSETypeS3 && clientExpectsSSEC {
+ // Object is SSE-S3 but client provides SSE-C headers → Error (mismatched encryption)
+ s3err.WriteErrorResponse(w, r, s3err.ErrSSEEncryptionTypeMismatch)
+ return http.StatusBadRequest, 0
} else if actualObjectType == "None" && clientExpectsSSEC {
// Object is unencrypted but client provides SSE-C headers → Error
s3err.WriteErrorResponse(w, r, s3err.ErrSSECustomerKeyMissing)
return http.StatusBadRequest, 0
}
- // Fallback for edge cases - use original logic with header-based detection
- if clientExpectsSSEC && sseAlgorithm != "" {
- return s3a.handleSSECResponse(r, proxyResponse, w)
- } else if !clientExpectsSSEC && kmsMetadataHeader != "" {
- return s3a.handleSSEKMSResponse(r, proxyResponse, w, kmsMetadataHeader)
- } else {
- return passThroughResponse(proxyResponse, w)
- }
+ // Unknown state - pass through and let proxy handle it
+ glog.V(4).Infof("Unknown SSE state: objectType=%s, clientExpectsSSEC=%v", actualObjectType, clientExpectsSSEC)
+ return passThroughResponse(proxyResponse, w)
}
// handleSSEKMSResponse handles SSE-KMS decryption and response processing
-func (s3a *S3ApiServer) handleSSEKMSResponse(r *http.Request, proxyResponse *http.Response, w http.ResponseWriter, kmsMetadataHeader string) (statusCode int, bytesTransferred int64) {
+func (s3a *S3ApiServer) handleSSEKMSResponse(r *http.Request, proxyResponse *http.Response, w http.ResponseWriter, entry *filer_pb.Entry, kmsMetadataHeader string) (statusCode int, bytesTransferred int64) {
// Deserialize SSE-KMS metadata
kmsMetadataBytes, err := base64.StdEncoding.DecodeString(kmsMetadataHeader)
if err != nil {
@@ -893,10 +940,8 @@ func (s3a *S3ApiServer) handleSSEKMSResponse(r *http.Request, proxyResponse *htt
// Capture existing CORS headers that may have been set by middleware
capturedCORSHeaders := captureCORSHeaders(w, corsHeaders)
- // Copy headers from proxy response
- for k, v := range proxyResponse.Header {
- w.Header()[k] = v
- }
+ // Copy headers from proxy response (excluding internal SeaweedFS headers)
+ copyResponseHeaders(w, proxyResponse, false)
// Add SSE-KMS response headers
AddSSEKMSResponseHeaders(w, sseKMSKey)
@@ -908,20 +953,16 @@ func (s3a *S3ApiServer) handleSSEKMSResponse(r *http.Request, proxyResponse *htt
// We need to check the object structure to determine if it's multipart encrypted
isMultipartSSEKMS := false
- if sseKMSKey != nil {
- // Get the object entry to check chunk structure
- bucket, object := s3_constants.GetBucketAndObject(r)
- objectPath := fmt.Sprintf("%s/%s%s", s3a.option.BucketsPath, bucket, object)
- if entry, err := s3a.getEntry("", objectPath); err == nil {
- // Check for multipart SSE-KMS
- sseKMSChunks := 0
- for _, chunk := range entry.GetChunks() {
- if chunk.GetSseType() == filer_pb.SSEType_SSE_KMS && len(chunk.GetSseMetadata()) > 0 {
- sseKMSChunks++
- }
+ if sseKMSKey != nil && entry != nil {
+ // Use the entry parameter passed from the caller (avoids redundant lookup)
+ // Check for multipart SSE-KMS
+ sseKMSChunks := 0
+ for _, chunk := range entry.GetChunks() {
+ if chunk.GetSseType() == filer_pb.SSEType_SSE_KMS && len(chunk.GetSseMetadata()) > 0 {
+ sseKMSChunks++
}
- isMultipartSSEKMS = sseKMSChunks > 1
}
+ isMultipartSSEKMS = sseKMSChunks > 1
}
var decryptedReader io.Reader
@@ -950,12 +991,8 @@ func (s3a *S3ApiServer) handleSSEKMSResponse(r *http.Request, proxyResponse *htt
// Capture existing CORS headers that may have been set by middleware
capturedCORSHeaders := captureCORSHeaders(w, corsHeaders)
- // Copy headers from proxy response (excluding body-related headers that might change)
- for k, v := range proxyResponse.Header {
- if k != "Content-Length" && k != "Content-Encoding" {
- w.Header()[k] = v
- }
- }
+ // Copy headers from proxy response (excluding body-related headers that might change and internal SeaweedFS headers)
+ copyResponseHeaders(w, proxyResponse, true)
// Set correct Content-Length for SSE-KMS
if proxyResponse.Header.Get("Content-Range") == "" {
@@ -971,6 +1008,99 @@ func (s3a *S3ApiServer) handleSSEKMSResponse(r *http.Request, proxyResponse *htt
return writeFinalResponse(w, proxyResponse, decryptedReader, capturedCORSHeaders)
}
+// handleSSES3Response handles SSE-S3 decryption and response processing
+func (s3a *S3ApiServer) handleSSES3Response(r *http.Request, proxyResponse *http.Response, w http.ResponseWriter, entry *filer_pb.Entry) (statusCode int, bytesTransferred int64) {
+
+ // For HEAD requests, we don't need to decrypt the body, just add response headers
+ if r.Method == "HEAD" {
+ // Capture existing CORS headers that may have been set by middleware
+ capturedCORSHeaders := captureCORSHeaders(w, corsHeaders)
+
+ // Copy headers from proxy response (excluding internal SeaweedFS headers)
+ copyResponseHeaders(w, proxyResponse, false)
+
+ // Add SSE-S3 response headers
+ w.Header().Set(s3_constants.AmzServerSideEncryption, SSES3Algorithm)
+
+ return writeFinalResponse(w, proxyResponse, proxyResponse.Body, capturedCORSHeaders)
+ }
+
+ // For GET requests, check if this is a multipart SSE-S3 object
+ isMultipartSSES3 := false
+ sses3Chunks := 0
+ for _, chunk := range entry.GetChunks() {
+ if chunk.GetSseType() == filer_pb.SSEType_SSE_S3 && len(chunk.GetSseMetadata()) > 0 {
+ sses3Chunks++
+ }
+ }
+ isMultipartSSES3 = sses3Chunks > 1
+
+ var decryptedReader io.Reader
+ if isMultipartSSES3 {
+ // Handle multipart SSE-S3 objects - each chunk needs independent decryption
+ multipartReader, decErr := s3a.createMultipartSSES3DecryptedReader(r, entry)
+ if decErr != nil {
+ glog.Errorf("Failed to create multipart SSE-S3 decrypted reader: %v", decErr)
+ s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
+ return http.StatusInternalServerError, 0
+ }
+ decryptedReader = multipartReader
+ glog.V(3).Infof("Using multipart SSE-S3 decryption for object")
+ } else {
+ // Handle single-part SSE-S3 objects
+ // Extract SSE-S3 key from metadata
+ keyManager := GetSSES3KeyManager()
+ if keyData, exists := entry.Extended[s3_constants.SeaweedFSSSES3Key]; !exists {
+ glog.Errorf("SSE-S3 key metadata not found in object entry")
+ s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
+ return http.StatusInternalServerError, 0
+ } else {
+ sseS3Key, err := DeserializeSSES3Metadata(keyData, keyManager)
+ if err != nil {
+ glog.Errorf("Failed to deserialize SSE-S3 metadata: %v", err)
+ s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
+ return http.StatusInternalServerError, 0
+ }
+
+ // Extract IV from metadata using helper function
+ iv, err := GetSSES3IV(entry, sseS3Key, keyManager)
+ if err != nil {
+ glog.Errorf("Failed to get SSE-S3 IV: %v", err)
+ s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
+ return http.StatusInternalServerError, 0
+ }
+
+ singlePartReader, decErr := CreateSSES3DecryptedReader(proxyResponse.Body, sseS3Key, iv)
+ if decErr != nil {
+ glog.Errorf("Failed to create SSE-S3 decrypted reader: %v", decErr)
+ s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
+ return http.StatusInternalServerError, 0
+ }
+ decryptedReader = singlePartReader
+ glog.V(3).Infof("Using single-part SSE-S3 decryption for object")
+ }
+ }
+
+ // Capture existing CORS headers that may have been set by middleware
+ capturedCORSHeaders := captureCORSHeaders(w, corsHeaders)
+
+ // Copy headers from proxy response (excluding body-related headers that might change and internal SeaweedFS headers)
+ copyResponseHeaders(w, proxyResponse, true)
+
+ // Set correct Content-Length for SSE-S3
+ if proxyResponse.Header.Get("Content-Range") == "" {
+ // For full object requests, encrypted length equals original length
+ if contentLengthStr := proxyResponse.Header.Get("Content-Length"); contentLengthStr != "" {
+ w.Header().Set("Content-Length", contentLengthStr)
+ }
+ }
+
+ // Add SSE-S3 response headers
+ w.Header().Set(s3_constants.AmzServerSideEncryption, SSES3Algorithm)
+
+ return writeFinalResponse(w, proxyResponse, decryptedReader, capturedCORSHeaders)
+}
+
// addObjectLockHeadersToResponse extracts object lock metadata from entry Extended attributes
// and adds the appropriate S3 headers to the response
func (s3a *S3ApiServer) addObjectLockHeadersToResponse(w http.ResponseWriter, entry *filer_pb.Entry) {
@@ -1049,6 +1179,10 @@ func (s3a *S3ApiServer) addSSEHeadersToResponse(proxyResponse *http.Response, en
proxyResponse.Header.Set(s3_constants.AmzServerSideEncryptionAwsKmsKeyId, string(kmsKeyID))
}
+ case s3_constants.SSETypeS3:
+ // Add only SSE-S3 headers
+ proxyResponse.Header.Set(s3_constants.AmzServerSideEncryption, SSES3Algorithm)
+
default:
// Unencrypted or unknown - don't set any SSE headers
}
@@ -1063,10 +1197,26 @@ func (s3a *S3ApiServer) detectPrimarySSEType(entry *filer_pb.Entry) string {
hasSSEC := entry.Extended[s3_constants.AmzServerSideEncryptionCustomerAlgorithm] != nil
hasSSEKMS := entry.Extended[s3_constants.AmzServerSideEncryption] != nil
- if hasSSEC && !hasSSEKMS {
+ // Check for SSE-S3: algorithm is AES256 but no customer key
+ if hasSSEKMS && !hasSSEC {
+ // Distinguish SSE-S3 from SSE-KMS: check the algorithm value and the presence of a KMS key ID
+ sseAlgo := string(entry.Extended[s3_constants.AmzServerSideEncryption])
+ switch sseAlgo {
+ case s3_constants.SSEAlgorithmAES256:
+ // Could be SSE-S3 or SSE-KMS, check for KMS key ID
+ if _, hasKMSKey := entry.Extended[s3_constants.AmzServerSideEncryptionAwsKmsKeyId]; hasKMSKey {
+ return s3_constants.SSETypeKMS
+ }
+ // No KMS key, this is SSE-S3
+ return s3_constants.SSETypeS3
+ case s3_constants.SSEAlgorithmKMS:
+ return s3_constants.SSETypeKMS
+ default:
+ // Unknown or unsupported algorithm
+ return "None"
+ }
+ } else if hasSSEC && !hasSSEKMS {
return s3_constants.SSETypeC
- } else if hasSSEKMS && !hasSSEC {
- return s3_constants.SSETypeKMS
} else if hasSSEC && hasSSEKMS {
// Both present - this should only happen during cross-encryption copies
// Use content to determine actual encryption state
@@ -1084,24 +1234,39 @@ func (s3a *S3ApiServer) detectPrimarySSEType(entry *filer_pb.Entry) string {
// Count chunk types to determine primary (multipart objects)
ssecChunks := 0
ssekmsChunks := 0
+ sses3Chunks := 0
for _, chunk := range entry.GetChunks() {
switch chunk.GetSseType() {
case filer_pb.SSEType_SSE_C:
ssecChunks++
case filer_pb.SSEType_SSE_KMS:
- ssekmsChunks++
+ if len(chunk.GetSseMetadata()) > 0 {
+ ssekmsChunks++
+ }
+ case filer_pb.SSEType_SSE_S3:
+ if len(chunk.GetSseMetadata()) > 0 {
+ sses3Chunks++
+ }
}
}
// Primary type is the one with more chunks
- if ssecChunks > ssekmsChunks {
+ // Note: Tie-breaking follows precedence order SSE-C > SSE-KMS > SSE-S3
+ // Mixed encryption in an object indicates potential corruption and should not occur in normal operation
+ if ssecChunks > ssekmsChunks && ssecChunks > sses3Chunks {
return s3_constants.SSETypeC
- } else if ssekmsChunks > ssecChunks {
+ } else if ssekmsChunks > ssecChunks && ssekmsChunks > sses3Chunks {
return s3_constants.SSETypeKMS
+ } else if sses3Chunks > ssecChunks && sses3Chunks > ssekmsChunks {
+ return s3_constants.SSETypeS3
} else if ssecChunks > 0 {
- // Equal number, prefer SSE-C (shouldn't happen in practice)
+ // Equal number or ties - precedence: SSE-C first
return s3_constants.SSETypeC
+ } else if ssekmsChunks > 0 {
+ return s3_constants.SSETypeKMS
+ } else if sses3Chunks > 0 {
+ return s3_constants.SSETypeS3
}
return "None"
@@ -1150,21 +1315,9 @@ func (s3a *S3ApiServer) createMultipartSSEKMSDecryptedReader(r *http.Request, pr
}
}
- // Fallback to object-level metadata (legacy support)
- if chunkSSEKMSKey == nil {
- objectMetadataHeader := proxyResponse.Header.Get(s3_constants.SeaweedFSSSEKMSKeyHeader)
- if objectMetadataHeader != "" {
- kmsMetadataBytes, decodeErr := base64.StdEncoding.DecodeString(objectMetadataHeader)
- if decodeErr == nil {
- kmsKey, _ := DeserializeSSEKMSMetadata(kmsMetadataBytes)
- if kmsKey != nil {
- // For object-level metadata (legacy), use absolute file offset as fallback
- kmsKey.ChunkOffset = chunk.GetOffset()
- chunkSSEKMSKey = kmsKey
- }
- }
- }
- }
+ // Note: No fallback to object-level metadata for multipart objects
+ // Each chunk in a multipart SSE-KMS object must have its own unique IV
+ // Falling back to object-level metadata could lead to IV reuse or incorrect decryption
if chunkSSEKMSKey == nil {
return nil, fmt.Errorf("no SSE-KMS metadata found for chunk %s in multipart object", chunk.GetFileIdString())
@@ -1189,6 +1342,86 @@ func (s3a *S3ApiServer) createMultipartSSEKMSDecryptedReader(r *http.Request, pr
return multiReader, nil
}
+// createMultipartSSES3DecryptedReader creates a reader for multipart SSE-S3 objects
+func (s3a *S3ApiServer) createMultipartSSES3DecryptedReader(r *http.Request, entry *filer_pb.Entry) (io.Reader, error) {
+ // Sort chunks by offset to ensure correct order
+ chunks := entry.GetChunks()
+ sort.Slice(chunks, func(i, j int) bool {
+ return chunks[i].GetOffset() < chunks[j].GetOffset()
+ })
+
+ // Create readers for each chunk, decrypting them independently
+ var readers []io.Reader
+ keyManager := GetSSES3KeyManager()
+
+ for _, chunk := range chunks {
+ // Get this chunk's encrypted data
+ chunkReader, err := s3a.createEncryptedChunkReader(chunk)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create chunk reader: %v", err)
+ }
+
+ // Handle based on chunk's encryption type
+ if chunk.GetSseType() == filer_pb.SSEType_SSE_S3 {
+ var chunkSSES3Key *SSES3Key
+
+ // Check if this chunk has per-chunk SSE-S3 metadata
+ if len(chunk.GetSseMetadata()) > 0 {
+ // Use the per-chunk SSE-S3 metadata
+ sseKey, err := DeserializeSSES3Metadata(chunk.GetSseMetadata(), keyManager)
+ if err != nil {
+ glog.Errorf("Failed to deserialize per-chunk SSE-S3 metadata for chunk %s: %v", chunk.GetFileIdString(), err)
+ chunkReader.Close()
+ return nil, fmt.Errorf("failed to deserialize SSE-S3 metadata: %v", err)
+ }
+ chunkSSES3Key = sseKey
+ }
+
+ // Note: No fallback to object-level metadata for multipart objects
+ // Each chunk in a multipart SSE-S3 object must have its own unique IV
+ // Falling back to object-level metadata could lead to IV reuse or incorrect decryption
+
+ if chunkSSES3Key == nil {
+ chunkReader.Close()
+ return nil, fmt.Errorf("no SSE-S3 metadata found for chunk %s in multipart object", chunk.GetFileIdString())
+ }
+
+ // Extract IV from chunk metadata
+ if len(chunkSSES3Key.IV) == 0 {
+ chunkReader.Close()
+ return nil, fmt.Errorf("no IV found in SSE-S3 metadata for chunk %s", chunk.GetFileIdString())
+ }
+
+ // Create decrypted reader for this chunk
+ decryptedChunkReader, decErr := CreateSSES3DecryptedReader(chunkReader, chunkSSES3Key, chunkSSES3Key.IV)
+ if decErr != nil {
+ chunkReader.Close()
+ return nil, fmt.Errorf("failed to decrypt chunk: %v", decErr)
+ }
+
+ // Use the streaming decrypted reader directly, ensuring the underlying chunkReader can be closed
+ readers = append(readers, struct {
+ io.Reader
+ io.Closer
+ }{
+ Reader: decryptedChunkReader,
+ Closer: chunkReader,
+ })
+ glog.V(4).Infof("Added streaming decrypted reader for chunk %s in multipart SSE-S3 object", chunk.GetFileIdString())
+ } else {
+ // Non-SSE-S3 chunk (unencrypted or other encryption type), use as-is
+ readers = append(readers, chunkReader)
+ glog.V(4).Infof("Added passthrough reader for non-SSE-S3 chunk %s (type: %v)", chunk.GetFileIdString(), chunk.GetSseType())
+ }
+ }
+
+ // Combine all decrypted chunk readers into a single stream
+ multiReader := NewMultipartSSEReader(readers)
+ glog.V(3).Infof("Created multipart SSE-S3 decrypted reader with %d chunks", len(readers))
+
+ return multiReader, nil
+}
+
// createEncryptedChunkReader creates a reader for a single encrypted chunk
func (s3a *S3ApiServer) createEncryptedChunkReader(chunk *filer_pb.FileChunk) (io.ReadCloser, error) {
// Get chunk URL
diff --git a/weed/s3api/s3api_object_handlers_copy.go b/weed/s3api/s3api_object_handlers_copy.go
index a71b52a39..65de55d1e 100644
--- a/weed/s3api/s3api_object_handlers_copy.go
+++ b/weed/s3api/s3api_object_handlers_copy.go
@@ -1152,7 +1152,7 @@ func (s3a *S3ApiServer) copyMultipartSSECChunks(entry *filer_pb.Entry, copySourc
dstMetadata := make(map[string][]byte)
if destKey != nil && len(destIV) > 0 {
// Store the IV and SSE-C headers for single-part compatibility
- StoreIVInMetadata(dstMetadata, destIV)
+ StoreSSECIVInMetadata(dstMetadata, destIV)
dstMetadata[s3_constants.AmzServerSideEncryptionCustomerAlgorithm] = []byte("AES256")
dstMetadata[s3_constants.AmzServerSideEncryptionCustomerKeyMD5] = []byte(destKey.KeyMD5)
glog.V(2).Infof("Prepared multipart SSE-C destination metadata: %s", dstPath)
@@ -1504,7 +1504,7 @@ func (s3a *S3ApiServer) copyMultipartCrossEncryption(entry *filer_pb.Entry, r *h
if len(dstChunks) > 0 && dstChunks[0].GetSseType() == filer_pb.SSEType_SSE_C && len(dstChunks[0].GetSseMetadata()) > 0 {
if ssecMetadata, err := DeserializeSSECMetadata(dstChunks[0].GetSseMetadata()); err == nil {
if iv, ivErr := base64.StdEncoding.DecodeString(ssecMetadata.IV); ivErr == nil {
- StoreIVInMetadata(dstMetadata, iv)
+ StoreSSECIVInMetadata(dstMetadata, iv)
dstMetadata[s3_constants.AmzServerSideEncryptionCustomerAlgorithm] = []byte("AES256")
dstMetadata[s3_constants.AmzServerSideEncryptionCustomerKeyMD5] = []byte(destSSECKey.KeyMD5)
}
@@ -1772,7 +1772,7 @@ func (s3a *S3ApiServer) copyChunksWithSSEC(entry *filer_pb.Entry, r *http.Reques
dstMetadata := make(map[string][]byte)
if destKey != nil && len(destIV) > 0 {
// Store the IV
- StoreIVInMetadata(dstMetadata, destIV)
+ StoreSSECIVInMetadata(dstMetadata, destIV)
// Store SSE-C algorithm and key MD5 for proper metadata
dstMetadata[s3_constants.AmzServerSideEncryptionCustomerAlgorithm] = []byte("AES256")
@@ -1855,7 +1855,7 @@ func (s3a *S3ApiServer) copyChunkWithReencryption(chunk *filer_pb.FileChunk, cop
// Decrypt if source is encrypted
if copySourceKey != nil {
// Get IV from source metadata
- srcIV, err := GetIVFromMetadata(srcMetadata)
+ srcIV, err := GetSSECIVFromMetadata(srcMetadata)
if err != nil {
return nil, fmt.Errorf("failed to get IV from metadata: %w", err)
}
diff --git a/weed/s3api/s3api_streaming_copy.go b/weed/s3api/s3api_streaming_copy.go
index 7c52a918c..49480b6ea 100644
--- a/weed/s3api/s3api_streaming_copy.go
+++ b/weed/s3api/s3api_streaming_copy.go
@@ -256,7 +256,7 @@ func (scm *StreamingCopyManager) createDecryptionReader(reader io.Reader, encSpe
case EncryptionTypeSSEC:
if sourceKey, ok := encSpec.SourceKey.(*SSECustomerKey); ok {
// Get IV from metadata
- iv, err := GetIVFromMetadata(encSpec.SourceMetadata)
+ iv, err := GetSSECIVFromMetadata(encSpec.SourceMetadata)
if err != nil {
return nil, fmt.Errorf("get IV from metadata: %w", err)
}
@@ -272,10 +272,10 @@ func (scm *StreamingCopyManager) createDecryptionReader(reader io.Reader, encSpe
case EncryptionTypeSSES3:
if sseKey, ok := encSpec.SourceKey.(*SSES3Key); ok {
- // Get IV from metadata
- iv, err := GetIVFromMetadata(encSpec.SourceMetadata)
- if err != nil {
- return nil, fmt.Errorf("get IV from metadata: %w", err)
+ // For SSE-S3, the IV is stored within the SSES3Key metadata, not as separate metadata
+ iv := sseKey.IV
+ if len(iv) == 0 {
+ return nil, fmt.Errorf("SSE-S3 key is missing IV for streaming copy")
}
return CreateSSES3DecryptedReader(reader, sseKey, iv)
}
diff --git a/weed/s3api/s3err/s3api_errors.go b/weed/s3api/s3err/s3api_errors.go
index 24f8e1b56..0d354ee8c 100644
--- a/weed/s3api/s3err/s3api_errors.go
+++ b/weed/s3api/s3err/s3api_errors.go
@@ -129,6 +129,7 @@ const (
ErrSSECustomerKeyMD5Mismatch
ErrSSECustomerKeyMissing
ErrSSECustomerKeyNotNeeded
+ ErrSSEEncryptionTypeMismatch
// SSE-KMS related errors
ErrKMSKeyNotFound
@@ -540,6 +541,11 @@ var errorCodeResponse = map[ErrorCode]APIError{
Description: "The object was not encrypted with customer provided keys.",
HTTPStatusCode: http.StatusBadRequest,
},
+ ErrSSEEncryptionTypeMismatch: {
+ Code: "InvalidRequest",
+ Description: "The encryption method specified in the request does not match the encryption method used to encrypt the object.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
// SSE-KMS error responses
ErrKMSKeyNotFound: {
diff --git a/weed/server/filer_server_handlers_read.go b/weed/server/filer_server_handlers_read.go
index ab474eef0..92aadcfc8 100644
--- a/weed/server/filer_server_handlers_read.go
+++ b/weed/server/filer_server_handlers_read.go
@@ -192,9 +192,9 @@ func (fs *FilerServer) GetOrHeadHandler(w http.ResponseWriter, r *http.Request)
// print out the header from extended properties
for k, v := range entry.Extended {
- if !strings.HasPrefix(k, "xattr-") && !strings.HasPrefix(k, "x-seaweedfs-") {
+ if !strings.HasPrefix(k, "xattr-") && !s3_constants.IsSeaweedFSInternalHeader(k) {
// "xattr-" prefix is set in filesys.XATTR_PREFIX
- // "x-seaweedfs-" prefix is for internal metadata that should not become HTTP headers
+ // IsSeaweedFSInternalHeader filters internal metadata that should not become HTTP headers
w.Header().Set(k, string(v))
}
}
@@ -241,6 +241,11 @@ func (fs *FilerServer) GetOrHeadHandler(w http.ResponseWriter, r *http.Request)
w.Header().Set(s3_constants.SeaweedFSSSEKMSKeyHeader, kmsBase64)
}
+ if _, exists := entry.Extended[s3_constants.SeaweedFSSSES3Key]; exists {
+ // Set standard S3 SSE-S3 response header (not the internal SeaweedFS header)
+ w.Header().Set(s3_constants.AmzServerSideEncryption, s3_constants.SSEAlgorithmAES256)
+ }
+
SetEtag(w, etag)
filename := entry.Name()
diff --git a/weed/server/filer_server_handlers_write_autochunk.go b/weed/server/filer_server_handlers_write_autochunk.go
index ca36abcac..d2b3d8b52 100644
--- a/weed/server/filer_server_handlers_write_autochunk.go
+++ b/weed/server/filer_server_handlers_write_autochunk.go
@@ -377,6 +377,16 @@ func (fs *FilerServer) saveMetaData(ctx context.Context, r *http.Request, fileNa
}
}
+ if sseS3Header := r.Header.Get(s3_constants.SeaweedFSSSES3Key); sseS3Header != "" {
+ // Decode base64-encoded S3 metadata and store
+ if s3Data, err := base64.StdEncoding.DecodeString(sseS3Header); err == nil {
+ entry.Extended[s3_constants.SeaweedFSSSES3Key] = s3Data
+ glog.V(4).Infof("Stored SSE-S3 metadata for %s", entry.FullPath)
+ } else {
+ glog.Errorf("Failed to decode SSE-S3 metadata header for %s: %v", entry.FullPath, err)
+ }
+ }
+
dbErr := fs.filer.CreateEntry(ctx, entry, false, false, nil, skipCheckParentDirEntry(r), so.MaxFileNameLength)
// In test_bucket_listv2_delimiter_basic, the valid object key is the parent folder
if dbErr != nil && strings.HasSuffix(dbErr.Error(), " is a file") && isS3Request(r) {
diff --git a/weed/topology/volume_growth_reservation_test.go b/weed/topology/volume_growth_reservation_test.go
index 442995b80..a29d924bd 100644
--- a/weed/topology/volume_growth_reservation_test.go
+++ b/weed/topology/volume_growth_reservation_test.go
@@ -181,23 +181,35 @@ func TestVolumeGrowth_ConcurrentAllocationPreventsRaceCondition(t *testing.T) {
wg.Wait()
- // With reservation system, only 5 requests should succeed (capacity limit)
- // The rest should fail due to insufficient capacity
- if successCount.Load() != 5 {
- t.Errorf("Expected exactly 5 successful reservations, got %d", successCount.Load())
+ // Collect results
+ successes := successCount.Load()
+ failures := failureCount.Load()
+ total := successes + failures
+
+ if total != concurrentRequests {
+ t.Fatalf("Expected %d total attempts recorded, got %d", concurrentRequests, total)
+ }
+
+ // At most the available capacity should succeed
+ const capacity = 5
+ if successes > capacity {
+ t.Errorf("Expected no more than %d successful reservations, got %d", capacity, successes)
}
- if failureCount.Load() != 5 {
- t.Errorf("Expected exactly 5 failed reservations, got %d", failureCount.Load())
+ // We should see at least the remaining attempts fail
+ minExpectedFailures := concurrentRequests - capacity
+ if failures < int32(minExpectedFailures) {
+ t.Errorf("Expected at least %d failed reservations, got %d", minExpectedFailures, failures)
}
- // Verify final state
+ // Verify final state matches the number of successful allocations
finalAvailable := dn.AvailableSpaceFor(option)
- if finalAvailable != 0 {
- t.Errorf("Expected 0 available space after all allocations, got %d", finalAvailable)
+ expectedAvailable := int64(capacity - successes)
+ if finalAvailable != expectedAvailable {
+ t.Errorf("Expected %d available space after allocations, got %d", expectedAvailable, finalAvailable)
}
- t.Logf("Concurrent test completed: %d successes, %d failures", successCount.Load(), failureCount.Load())
+ t.Logf("Concurrent test completed: %d successes, %d failures", successes, failures)
}
func TestVolumeGrowth_ReservationFailureRollback(t *testing.T) {