aboutsummaryrefslogtreecommitdiff
path: root/weed/s3api
diff options
context:
space:
mode:
Diffstat (limited to 'weed/s3api')
-rw-r--r--weed/s3api/s3_constants/header.go11
-rw-r--r--weed/s3api/s3_sse_c.go275
-rw-r--r--weed/s3api/s3_sse_c_range_test.go63
-rw-r--r--weed/s3api/s3_sse_c_test.go412
-rw-r--r--weed/s3api/s3api_object_handlers.go134
-rw-r--r--weed/s3api/s3api_object_handlers_copy.go155
-rw-r--r--weed/s3api/s3api_object_handlers_put.go19
-rw-r--r--weed/s3api/s3err/s3api_errors.go34
8 files changed, 1083 insertions, 20 deletions
diff --git a/weed/s3api/s3_constants/header.go b/weed/s3api/s3_constants/header.go
index 52bcda548..f291c8c45 100644
--- a/weed/s3api/s3_constants/header.go
+++ b/weed/s3api/s3_constants/header.go
@@ -64,6 +64,17 @@ const (
AmzCopySourceIfUnmodifiedSince = "X-Amz-Copy-Source-If-Unmodified-Since"
AmzMpPartsCount = "X-Amz-Mp-Parts-Count"
+
+ // S3 Server-Side Encryption with Customer-provided Keys (SSE-C)
+ AmzServerSideEncryptionCustomerAlgorithm = "X-Amz-Server-Side-Encryption-Customer-Algorithm"
+ AmzServerSideEncryptionCustomerKey = "X-Amz-Server-Side-Encryption-Customer-Key"
+ AmzServerSideEncryptionCustomerKeyMD5 = "X-Amz-Server-Side-Encryption-Customer-Key-MD5"
+ AmzServerSideEncryptionContext = "X-Amz-Server-Side-Encryption-Context"
+
+ // S3 SSE-C copy source headers
+ AmzCopySourceServerSideEncryptionCustomerAlgorithm = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm"
+ AmzCopySourceServerSideEncryptionCustomerKey = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key"
+ AmzCopySourceServerSideEncryptionCustomerKeyMD5 = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-MD5"
)
// Non-Standard S3 HTTP request constants
diff --git a/weed/s3api/s3_sse_c.go b/weed/s3api/s3_sse_c.go
new file mode 100644
index 000000000..3e7d6fc02
--- /dev/null
+++ b/weed/s3api/s3_sse_c.go
@@ -0,0 +1,275 @@
+package s3api
+
+import (
+ "bytes"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/md5"
+ "crypto/rand"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
+ "github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
+)
+
+const (
+ // SSE-C constants
+ SSECustomerAlgorithmAES256 = "AES256"
+ SSECustomerKeySize = 32 // 256 bits
+ AESBlockSize = 16 // AES block size in bytes
+)
+
+// SSE-C related errors
+var (
+ ErrInvalidRequest = errors.New("invalid request")
+ ErrInvalidEncryptionAlgorithm = errors.New("invalid encryption algorithm")
+ ErrInvalidEncryptionKey = errors.New("invalid encryption key")
+ ErrSSECustomerKeyMD5Mismatch = errors.New("customer key MD5 mismatch")
+ ErrSSECustomerKeyMissing = errors.New("customer key missing")
+ ErrSSECustomerKeyNotNeeded = errors.New("customer key not needed")
+)
+
+// SSECustomerKey represents a customer-provided encryption key for SSE-C
+type SSECustomerKey struct {
+ Algorithm string
+ Key []byte
+ KeyMD5 string
+}
+
+// SSECDecryptedReader wraps an io.Reader to provide SSE-C decryption
+type SSECDecryptedReader struct {
+ reader io.Reader
+ cipher cipher.Stream
+ customerKey *SSECustomerKey
+ first bool
+}
+
+// IsSSECRequest checks if the request contains SSE-C headers
+func IsSSECRequest(r *http.Request) bool {
+ return r.Header.Get(s3_constants.AmzServerSideEncryptionCustomerAlgorithm) != ""
+}
+
+// validateAndParseSSECHeaders does the core validation and parsing logic
+func validateAndParseSSECHeaders(algorithm, key, keyMD5 string) (*SSECustomerKey, error) {
+ if algorithm == "" && key == "" && keyMD5 == "" {
+ return nil, nil // No SSE-C headers
+ }
+
+ if algorithm == "" || key == "" || keyMD5 == "" {
+ return nil, ErrInvalidRequest
+ }
+
+ if algorithm != SSECustomerAlgorithmAES256 {
+ return nil, ErrInvalidEncryptionAlgorithm
+ }
+
+ // Decode and validate key
+ keyBytes, err := base64.StdEncoding.DecodeString(key)
+ if err != nil {
+ return nil, ErrInvalidEncryptionKey
+ }
+
+ if len(keyBytes) != SSECustomerKeySize {
+ return nil, ErrInvalidEncryptionKey
+ }
+
+ // Validate key MD5 (base64-encoded MD5 of the raw key bytes; case-sensitive)
+ sum := md5.Sum(keyBytes)
+ expectedMD5 := base64.StdEncoding.EncodeToString(sum[:])
+ if keyMD5 != expectedMD5 {
+ return nil, ErrSSECustomerKeyMD5Mismatch
+ }
+
+ return &SSECustomerKey{
+ Algorithm: algorithm,
+ Key: keyBytes,
+ KeyMD5: keyMD5,
+ }, nil
+}
+
+// ValidateSSECHeaders validates SSE-C headers in the request
+func ValidateSSECHeaders(r *http.Request) error {
+ algorithm := r.Header.Get(s3_constants.AmzServerSideEncryptionCustomerAlgorithm)
+ key := r.Header.Get(s3_constants.AmzServerSideEncryptionCustomerKey)
+ keyMD5 := r.Header.Get(s3_constants.AmzServerSideEncryptionCustomerKeyMD5)
+
+ _, err := validateAndParseSSECHeaders(algorithm, key, keyMD5)
+ return err
+}
+
+// ParseSSECHeaders parses and validates SSE-C headers from the request
+func ParseSSECHeaders(r *http.Request) (*SSECustomerKey, error) {
+ algorithm := r.Header.Get(s3_constants.AmzServerSideEncryptionCustomerAlgorithm)
+ key := r.Header.Get(s3_constants.AmzServerSideEncryptionCustomerKey)
+ keyMD5 := r.Header.Get(s3_constants.AmzServerSideEncryptionCustomerKeyMD5)
+
+ return validateAndParseSSECHeaders(algorithm, key, keyMD5)
+}
+
+// ParseSSECCopySourceHeaders parses and validates SSE-C copy source headers from the request
+func ParseSSECCopySourceHeaders(r *http.Request) (*SSECustomerKey, error) {
+ algorithm := r.Header.Get(s3_constants.AmzCopySourceServerSideEncryptionCustomerAlgorithm)
+ key := r.Header.Get(s3_constants.AmzCopySourceServerSideEncryptionCustomerKey)
+ keyMD5 := r.Header.Get(s3_constants.AmzCopySourceServerSideEncryptionCustomerKeyMD5)
+
+ return validateAndParseSSECHeaders(algorithm, key, keyMD5)
+}
+
+// CreateSSECEncryptedReader creates a new encrypted reader for SSE-C
+func CreateSSECEncryptedReader(r io.Reader, customerKey *SSECustomerKey) (io.Reader, error) {
+ if customerKey == nil {
+ return r, nil
+ }
+
+ // Create AES cipher
+ block, err := aes.NewCipher(customerKey.Key)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create AES cipher: %v", err)
+ }
+
+ // Generate random IV
+ iv := make([]byte, AESBlockSize)
+ if _, err := io.ReadFull(rand.Reader, iv); err != nil {
+ return nil, fmt.Errorf("failed to generate IV: %v", err)
+ }
+
+ // Create CTR mode cipher
+ stream := cipher.NewCTR(block, iv)
+
+ // The encrypted stream is the IV (initialization vector) followed by the encrypted data.
+ // The IV is randomly generated for each encryption operation and must be unique and unpredictable.
+ // This is critical for the security of AES-CTR mode: reusing an IV with the same key breaks confidentiality.
+ // By prepending the IV to the ciphertext, the decryptor can extract the IV to initialize the cipher.
+ // Note: AES-CTR provides confidentiality only; use an additional MAC if integrity is required.
+ // We model this with an io.MultiReader (IV first) and a cipher.StreamReader (encrypted payload).
+ return io.MultiReader(bytes.NewReader(iv), &cipher.StreamReader{S: stream, R: r}), nil
+}
+
+// CreateSSECDecryptedReader creates a new decrypted reader for SSE-C
+func CreateSSECDecryptedReader(r io.Reader, customerKey *SSECustomerKey) (io.Reader, error) {
+ if customerKey == nil {
+ return r, nil
+ }
+
+ return &SSECDecryptedReader{
+ reader: r,
+ customerKey: customerKey,
+ cipher: nil, // Will be initialized when we read the IV
+ first: true,
+ }, nil
+}
+
+// Read implements io.Reader for SSECDecryptedReader
+func (r *SSECDecryptedReader) Read(p []byte) (n int, err error) {
+ if r.first {
+ // First read: extract IV and initialize cipher
+ r.first = false
+ iv := make([]byte, AESBlockSize)
+
+ // Read IV from the beginning of the data
+ _, err = io.ReadFull(r.reader, iv)
+ if err != nil {
+ return 0, fmt.Errorf("failed to read IV: %v", err)
+ }
+
+ // Create cipher with the extracted IV
+ block, err := aes.NewCipher(r.customerKey.Key)
+ if err != nil {
+ return 0, fmt.Errorf("failed to create AES cipher: %v", err)
+ }
+ r.cipher = cipher.NewCTR(block, iv)
+ }
+
+ // Decrypt data
+ n, err = r.reader.Read(p)
+ if n > 0 {
+ r.cipher.XORKeyStream(p[:n], p[:n])
+ }
+ return n, err
+}
+
+// GetSourceSSECInfo extracts SSE-C information from source object metadata
+func GetSourceSSECInfo(metadata map[string][]byte) (algorithm string, keyMD5 string, isEncrypted bool) {
+ if alg, exists := metadata[s3_constants.AmzServerSideEncryptionCustomerAlgorithm]; exists {
+ algorithm = string(alg)
+ }
+ if md5, exists := metadata[s3_constants.AmzServerSideEncryptionCustomerKeyMD5]; exists {
+ keyMD5 = string(md5)
+ }
+ isEncrypted = algorithm != "" && keyMD5 != ""
+ return
+}
+
+// CanDirectCopySSEC determines if we can directly copy chunks without decrypt/re-encrypt
+func CanDirectCopySSEC(srcMetadata map[string][]byte, copySourceKey *SSECustomerKey, destKey *SSECustomerKey) bool {
+ _, srcKeyMD5, srcEncrypted := GetSourceSSECInfo(srcMetadata)
+
+ // Case 1: Source unencrypted, destination unencrypted -> Direct copy
+ if !srcEncrypted && destKey == nil {
+ return true
+ }
+
+ // Case 2: Source encrypted, same key for decryption and destination -> Direct copy
+ if srcEncrypted && copySourceKey != nil && destKey != nil {
+ // Same key if MD5 matches exactly (base64 encoding is case-sensitive)
+ return copySourceKey.KeyMD5 == srcKeyMD5 &&
+ destKey.KeyMD5 == srcKeyMD5
+ }
+
+ // All other cases require decrypt/re-encrypt
+ return false
+}
+
+// SSECCopyStrategy represents the strategy for copying SSE-C objects
+type SSECCopyStrategy int
+
+const (
+ SSECCopyDirect SSECCopyStrategy = iota // Direct chunk copy (fast)
+ SSECCopyReencrypt // Decrypt and re-encrypt (slow)
+)
+
+// DetermineSSECCopyStrategy determines the optimal copy strategy
+func DetermineSSECCopyStrategy(srcMetadata map[string][]byte, copySourceKey *SSECustomerKey, destKey *SSECustomerKey) (SSECCopyStrategy, error) {
+ _, srcKeyMD5, srcEncrypted := GetSourceSSECInfo(srcMetadata)
+
+ // Validate source key if source is encrypted
+ if srcEncrypted {
+ if copySourceKey == nil {
+ return SSECCopyReencrypt, ErrSSECustomerKeyMissing
+ }
+ if copySourceKey.KeyMD5 != srcKeyMD5 {
+ return SSECCopyReencrypt, ErrSSECustomerKeyMD5Mismatch
+ }
+ } else if copySourceKey != nil {
+ // Source not encrypted but copy source key provided
+ return SSECCopyReencrypt, ErrSSECustomerKeyNotNeeded
+ }
+
+ if CanDirectCopySSEC(srcMetadata, copySourceKey, destKey) {
+ return SSECCopyDirect, nil
+ }
+
+ return SSECCopyReencrypt, nil
+}
+
+// MapSSECErrorToS3Error maps SSE-C custom errors to S3 API error codes
+func MapSSECErrorToS3Error(err error) s3err.ErrorCode {
+ switch err {
+ case ErrInvalidEncryptionAlgorithm:
+ return s3err.ErrInvalidEncryptionAlgorithm
+ case ErrInvalidEncryptionKey:
+ return s3err.ErrInvalidEncryptionKey
+ case ErrSSECustomerKeyMD5Mismatch:
+ return s3err.ErrSSECustomerKeyMD5Mismatch
+ case ErrSSECustomerKeyMissing:
+ return s3err.ErrSSECustomerKeyMissing
+ case ErrSSECustomerKeyNotNeeded:
+ return s3err.ErrSSECustomerKeyNotNeeded
+ default:
+ return s3err.ErrInvalidRequest
+ }
+}
diff --git a/weed/s3api/s3_sse_c_range_test.go b/weed/s3api/s3_sse_c_range_test.go
new file mode 100644
index 000000000..456231074
--- /dev/null
+++ b/weed/s3api/s3_sse_c_range_test.go
@@ -0,0 +1,63 @@
+package s3api
+
+import (
+ "bytes"
+ "crypto/md5"
+ "encoding/base64"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/gorilla/mux"
+ "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
+)
+
+// ResponseRecorder that also implements http.Flusher
+type recorderFlusher struct{ *httptest.ResponseRecorder }
+
+func (r recorderFlusher) Flush() {}
+
+// TestSSECRangeRequestsNotSupported verifies that HTTP Range requests are rejected
+// for SSE-C encrypted objects because the IV is required at the beginning of the stream
+func TestSSECRangeRequestsNotSupported(t *testing.T) {
+ // Create a request with Range header and valid SSE-C headers
+ req := httptest.NewRequest(http.MethodGet, "/b/o", nil)
+ req.Header.Set("Range", "bytes=10-20")
+ req.Header.Set(s3_constants.AmzServerSideEncryptionCustomerAlgorithm, "AES256")
+
+ key := make([]byte, 32)
+ for i := range key {
+ key[i] = byte(i)
+ }
+ s := md5.Sum(key)
+ keyMD5 := base64.StdEncoding.EncodeToString(s[:])
+
+ req.Header.Set(s3_constants.AmzServerSideEncryptionCustomerKey, base64.StdEncoding.EncodeToString(key))
+ req.Header.Set(s3_constants.AmzServerSideEncryptionCustomerKeyMD5, keyMD5)
+
+ // Attach mux vars to avoid panic in error writer
+ req = mux.SetURLVars(req, map[string]string{"bucket": "b", "object": "o"})
+
+ // Create a mock HTTP response that simulates SSE-C encrypted object metadata
+ proxyResponse := &http.Response{
+ StatusCode: 200,
+ Header: make(http.Header),
+ Body: io.NopCloser(bytes.NewReader([]byte("mock encrypted data"))),
+ }
+ proxyResponse.Header.Set(s3_constants.AmzServerSideEncryptionCustomerAlgorithm, "AES256")
+ proxyResponse.Header.Set(s3_constants.AmzServerSideEncryptionCustomerKeyMD5, keyMD5)
+
+ // Call the function under test
+ s3a := &S3ApiServer{}
+ rec := httptest.NewRecorder()
+ w := recorderFlusher{rec}
+ statusCode, _ := s3a.handleSSECResponse(req, proxyResponse, w)
+
+ if statusCode != http.StatusRequestedRangeNotSatisfiable {
+ t.Fatalf("expected status %d, got %d", http.StatusRequestedRangeNotSatisfiable, statusCode)
+ }
+ if rec.Result().StatusCode != http.StatusRequestedRangeNotSatisfiable {
+ t.Fatalf("writer status expected %d, got %d", http.StatusRequestedRangeNotSatisfiable, rec.Result().StatusCode)
+ }
+}
diff --git a/weed/s3api/s3_sse_c_test.go b/weed/s3api/s3_sse_c_test.go
new file mode 100644
index 000000000..51c536445
--- /dev/null
+++ b/weed/s3api/s3_sse_c_test.go
@@ -0,0 +1,412 @@
+package s3api
+
+import (
+ "bytes"
+ "crypto/md5"
+ "encoding/base64"
+ "fmt"
+ "io"
+ "net/http"
+ "testing"
+
+ "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
+)
+
+func base64MD5(b []byte) string {
+ s := md5.Sum(b)
+ return base64.StdEncoding.EncodeToString(s[:])
+}
+
+func TestSSECHeaderValidation(t *testing.T) {
+ // Test valid SSE-C headers
+ req := &http.Request{Header: make(http.Header)}
+
+ key := make([]byte, 32) // 256-bit key
+ for i := range key {
+ key[i] = byte(i)
+ }
+
+ keyBase64 := base64.StdEncoding.EncodeToString(key)
+ md5sum := md5.Sum(key)
+ keyMD5 := base64.StdEncoding.EncodeToString(md5sum[:])
+
+ req.Header.Set(s3_constants.AmzServerSideEncryptionCustomerAlgorithm, "AES256")
+ req.Header.Set(s3_constants.AmzServerSideEncryptionCustomerKey, keyBase64)
+ req.Header.Set(s3_constants.AmzServerSideEncryptionCustomerKeyMD5, keyMD5)
+
+ // Test validation
+ err := ValidateSSECHeaders(req)
+ if err != nil {
+ t.Errorf("Expected valid headers, got error: %v", err)
+ }
+
+ // Test parsing
+ customerKey, err := ParseSSECHeaders(req)
+ if err != nil {
+ t.Errorf("Expected successful parsing, got error: %v", err)
+ }
+
+ if customerKey == nil {
+ t.Error("Expected customer key, got nil")
+ }
+
+ if customerKey.Algorithm != "AES256" {
+ t.Errorf("Expected algorithm AES256, got %s", customerKey.Algorithm)
+ }
+
+ if !bytes.Equal(customerKey.Key, key) {
+ t.Error("Key doesn't match original")
+ }
+
+ if customerKey.KeyMD5 != keyMD5 {
+ t.Errorf("Expected key MD5 %s, got %s", keyMD5, customerKey.KeyMD5)
+ }
+}
+
+func TestSSECCopySourceHeaders(t *testing.T) {
+ // Test valid SSE-C copy source headers
+ req := &http.Request{Header: make(http.Header)}
+
+ key := make([]byte, 32) // 256-bit key
+ for i := range key {
+ key[i] = byte(i) + 1 // Different from regular test
+ }
+
+ keyBase64 := base64.StdEncoding.EncodeToString(key)
+ md5sum2 := md5.Sum(key)
+ keyMD5 := base64.StdEncoding.EncodeToString(md5sum2[:])
+
+ req.Header.Set(s3_constants.AmzCopySourceServerSideEncryptionCustomerAlgorithm, "AES256")
+ req.Header.Set(s3_constants.AmzCopySourceServerSideEncryptionCustomerKey, keyBase64)
+ req.Header.Set(s3_constants.AmzCopySourceServerSideEncryptionCustomerKeyMD5, keyMD5)
+
+ // Test parsing copy source headers
+ customerKey, err := ParseSSECCopySourceHeaders(req)
+ if err != nil {
+ t.Errorf("Expected successful copy source parsing, got error: %v", err)
+ }
+
+ if customerKey == nil {
+ t.Error("Expected customer key from copy source headers, got nil")
+ }
+
+ if customerKey.Algorithm != "AES256" {
+ t.Errorf("Expected algorithm AES256, got %s", customerKey.Algorithm)
+ }
+
+ if !bytes.Equal(customerKey.Key, key) {
+ t.Error("Copy source key doesn't match original")
+ }
+
+ // Test that regular headers don't interfere with copy source headers
+ regularKey, err := ParseSSECHeaders(req)
+ if err != nil {
+ t.Errorf("Regular header parsing should not fail: %v", err)
+ }
+
+ if regularKey != nil {
+ t.Error("Expected nil for regular headers when only copy source headers are present")
+ }
+}
+
+func TestSSECHeaderValidationErrors(t *testing.T) {
+ tests := []struct {
+ name string
+ algorithm string
+ key string
+ keyMD5 string
+ wantErr error
+ }{
+ {
+ name: "invalid algorithm",
+ algorithm: "AES128",
+ key: base64.StdEncoding.EncodeToString(make([]byte, 32)),
+ keyMD5: base64MD5(make([]byte, 32)),
+ wantErr: ErrInvalidEncryptionAlgorithm,
+ },
+ {
+ name: "invalid key length",
+ algorithm: "AES256",
+ key: base64.StdEncoding.EncodeToString(make([]byte, 16)),
+ keyMD5: base64MD5(make([]byte, 16)),
+ wantErr: ErrInvalidEncryptionKey,
+ },
+ {
+ name: "mismatched MD5",
+ algorithm: "AES256",
+ key: base64.StdEncoding.EncodeToString(make([]byte, 32)),
+ keyMD5: "wrong==md5",
+ wantErr: ErrSSECustomerKeyMD5Mismatch,
+ },
+ {
+ name: "incomplete headers",
+ algorithm: "AES256",
+ key: "",
+ keyMD5: "",
+ wantErr: ErrInvalidRequest,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ req := &http.Request{Header: make(http.Header)}
+
+ if tt.algorithm != "" {
+ req.Header.Set(s3_constants.AmzServerSideEncryptionCustomerAlgorithm, tt.algorithm)
+ }
+ if tt.key != "" {
+ req.Header.Set(s3_constants.AmzServerSideEncryptionCustomerKey, tt.key)
+ }
+ if tt.keyMD5 != "" {
+ req.Header.Set(s3_constants.AmzServerSideEncryptionCustomerKeyMD5, tt.keyMD5)
+ }
+
+ err := ValidateSSECHeaders(req)
+ if err != tt.wantErr {
+ t.Errorf("Expected error %v, got %v", tt.wantErr, err)
+ }
+ })
+ }
+}
+
+func TestSSECEncryptionDecryption(t *testing.T) {
+ // Create customer key
+ key := make([]byte, 32)
+ for i := range key {
+ key[i] = byte(i)
+ }
+
+ md5sumKey := md5.Sum(key)
+ customerKey := &SSECustomerKey{
+ Algorithm: "AES256",
+ Key: key,
+ KeyMD5: base64.StdEncoding.EncodeToString(md5sumKey[:]),
+ }
+
+ // Test data
+ testData := []byte("Hello, World! This is a test of SSE-C encryption.")
+
+ // Create encrypted reader
+ dataReader := bytes.NewReader(testData)
+ encryptedReader, err := CreateSSECEncryptedReader(dataReader, customerKey)
+ 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[16:], testData) { // Skip IV
+ t.Error("Data doesn't appear to be encrypted")
+ }
+
+ // Create decrypted reader
+ encryptedReader2 := bytes.NewReader(encryptedData)
+ decryptedReader, err := CreateSSECDecryptedReader(encryptedReader2, customerKey)
+ 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)
+ }
+}
+
+func TestSSECIsSSECRequest(t *testing.T) {
+ // Test with SSE-C headers
+ req := &http.Request{Header: make(http.Header)}
+ req.Header.Set(s3_constants.AmzServerSideEncryptionCustomerAlgorithm, "AES256")
+
+ if !IsSSECRequest(req) {
+ t.Error("Expected IsSSECRequest to return true when SSE-C headers are present")
+ }
+
+ // Test without SSE-C headers
+ req2 := &http.Request{Header: make(http.Header)}
+ if IsSSECRequest(req2) {
+ t.Error("Expected IsSSECRequest to return false when no SSE-C headers are present")
+ }
+}
+
+// Test encryption with different data sizes (similar to s3tests)
+func TestSSECEncryptionVariousSizes(t *testing.T) {
+ sizes := []int{1, 13, 1024, 1024 * 1024} // 1B, 13B, 1KB, 1MB
+
+ for _, size := range sizes {
+ t.Run(fmt.Sprintf("size_%d", size), func(t *testing.T) {
+ // Create customer key
+ key := make([]byte, 32)
+ for i := range key {
+ key[i] = byte(i + size) // Make key unique per test
+ }
+
+ md5sumDyn := md5.Sum(key)
+ customerKey := &SSECustomerKey{
+ Algorithm: "AES256",
+ Key: key,
+ KeyMD5: base64.StdEncoding.EncodeToString(md5sumDyn[:]),
+ }
+
+ // Create test data of specified size
+ testData := make([]byte, size)
+ for i := range testData {
+ testData[i] = byte('A' + (i % 26)) // Pattern of A-Z
+ }
+
+ // Encrypt
+ dataReader := bytes.NewReader(testData)
+ encryptedReader, err := CreateSSECEncryptedReader(dataReader, customerKey)
+ 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 IV is present and data is encrypted
+ if len(encryptedData) < AESBlockSize {
+ t.Fatalf("Encrypted data too short, missing IV")
+ }
+
+ if len(encryptedData) != size+AESBlockSize {
+ t.Errorf("Expected encrypted data length %d, got %d", size+AESBlockSize, len(encryptedData))
+ }
+
+ // Decrypt
+ encryptedReader2 := bytes.NewReader(encryptedData)
+ decryptedReader, err := CreateSSECDecryptedReader(encryptedReader2, customerKey)
+ 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, testData) {
+ t.Errorf("Decrypted data doesn't match original for size %d", size)
+ }
+ })
+ }
+}
+
+func TestSSECEncryptionWithNilKey(t *testing.T) {
+ testData := []byte("test data")
+ dataReader := bytes.NewReader(testData)
+
+ // Test encryption with nil key (should pass through)
+ encryptedReader, err := CreateSSECEncryptedReader(dataReader, nil)
+ if err != nil {
+ t.Fatalf("Failed to create encrypted reader with nil key: %v", err)
+ }
+
+ result, err := io.ReadAll(encryptedReader)
+ if err != nil {
+ t.Fatalf("Failed to read from pass-through reader: %v", err)
+ }
+
+ if !bytes.Equal(result, testData) {
+ t.Error("Data should pass through unchanged when key is nil")
+ }
+
+ // Test decryption with nil key (should pass through)
+ dataReader2 := bytes.NewReader(testData)
+ decryptedReader, err := CreateSSECDecryptedReader(dataReader2, nil)
+ if err != nil {
+ t.Fatalf("Failed to create decrypted reader with nil key: %v", err)
+ }
+
+ result2, err := io.ReadAll(decryptedReader)
+ if err != nil {
+ t.Fatalf("Failed to read from pass-through reader: %v", err)
+ }
+
+ if !bytes.Equal(result2, testData) {
+ t.Error("Data should pass through unchanged when key is nil")
+ }
+}
+
+// TestSSECEncryptionSmallBuffers tests the fix for the critical bug where small buffers
+// could corrupt the data stream when reading in chunks smaller than the IV size
+func TestSSECEncryptionSmallBuffers(t *testing.T) {
+ testData := []byte("This is a test message for small buffer reads")
+
+ // Create customer key
+ key := make([]byte, 32)
+ for i := range key {
+ key[i] = byte(i)
+ }
+
+ md5sumKey3 := md5.Sum(key)
+ customerKey := &SSECustomerKey{
+ Algorithm: "AES256",
+ Key: key,
+ KeyMD5: base64.StdEncoding.EncodeToString(md5sumKey3[:]),
+ }
+
+ // Create encrypted reader
+ dataReader := bytes.NewReader(testData)
+ encryptedReader, err := CreateSSECEncryptedReader(dataReader, customerKey)
+ if err != nil {
+ t.Fatalf("Failed to create encrypted reader: %v", err)
+ }
+
+ // Read with very small buffers (smaller than IV size of 16 bytes)
+ var encryptedData []byte
+ smallBuffer := make([]byte, 5) // Much smaller than 16-byte IV
+
+ for {
+ n, err := encryptedReader.Read(smallBuffer)
+ if n > 0 {
+ encryptedData = append(encryptedData, smallBuffer[:n]...)
+ }
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ t.Fatalf("Error reading encrypted data: %v", err)
+ }
+ }
+
+ // Verify the encrypted data starts with 16-byte IV
+ if len(encryptedData) < 16 {
+ t.Fatalf("Encrypted data too short, expected at least 16 bytes for IV, got %d", len(encryptedData))
+ }
+
+ // Expected total size: 16 bytes (IV) + len(testData)
+ expectedSize := 16 + len(testData)
+ if len(encryptedData) != expectedSize {
+ t.Errorf("Expected encrypted data size %d, got %d", expectedSize, len(encryptedData))
+ }
+
+ // Decrypt and verify
+ encryptedReader2 := bytes.NewReader(encryptedData)
+ decryptedReader, err := CreateSSECDecryptedReader(encryptedReader2, customerKey)
+ 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)
+ }
+
+ if !bytes.Equal(decryptedData, testData) {
+ t.Errorf("Decrypted data doesn't match original.\nOriginal: %s\nDecrypted: %s", testData, decryptedData)
+ }
+}
diff --git a/weed/s3api/s3api_object_handlers.go b/weed/s3api/s3api_object_handlers.go
index 70d36cd7e..bde5764f6 100644
--- a/weed/s3api/s3api_object_handlers.go
+++ b/weed/s3api/s3api_object_handlers.go
@@ -328,7 +328,10 @@ func (s3a *S3ApiServer) GetObjectHandler(w http.ResponseWriter, r *http.Request)
destUrl = s3a.toFilerUrl(bucket, object)
}
- s3a.proxyToFiler(w, r, destUrl, false, passThroughResponse)
+ s3a.proxyToFiler(w, r, destUrl, false, func(proxyResponse *http.Response, w http.ResponseWriter) (statusCode int, bytesTransferred int64) {
+ // Handle SSE-C decryption if needed
+ return s3a.handleSSECResponse(r, proxyResponse, w)
+ })
}
func (s3a *S3ApiServer) HeadObjectHandler(w http.ResponseWriter, r *http.Request) {
@@ -423,7 +426,10 @@ func (s3a *S3ApiServer) HeadObjectHandler(w http.ResponseWriter, r *http.Request
destUrl = s3a.toFilerUrl(bucket, object)
}
- s3a.proxyToFiler(w, r, destUrl, false, passThroughResponse)
+ s3a.proxyToFiler(w, r, destUrl, false, func(proxyResponse *http.Response, w http.ResponseWriter) (statusCode int, bytesTransferred int64) {
+ // Handle SSE-C validation for HEAD requests
+ return s3a.handleSSECResponse(r, proxyResponse, w)
+ })
}
func (s3a *S3ApiServer) proxyToFiler(w http.ResponseWriter, r *http.Request, destUrl string, isWrite bool, responseFn func(proxyResponse *http.Response, w http.ResponseWriter) (statusCode int, bytesTransferred int64)) {
@@ -555,34 +561,134 @@ func restoreCORSHeaders(w http.ResponseWriter, capturedCORSHeaders map[string]st
}
}
-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
- for k, v := range proxyResponse.Header {
- w.Header()[k] = v
- }
-
+// writeFinalResponse handles the common response writing logic shared between
+// passThroughResponse and handleSSECResponse
+func writeFinalResponse(w http.ResponseWriter, proxyResponse *http.Response, bodyReader io.Reader, capturedCORSHeaders map[string]string) (statusCode int, bytesTransferred int64) {
// Restore CORS headers that were set by middleware
restoreCORSHeaders(w, capturedCORSHeaders)
if proxyResponse.Header.Get("Content-Range") != "" && proxyResponse.StatusCode == 200 {
- w.WriteHeader(http.StatusPartialContent)
statusCode = http.StatusPartialContent
} else {
statusCode = proxyResponse.StatusCode
}
w.WriteHeader(statusCode)
+
+ // Stream response data
buf := mem.Allocate(128 * 1024)
defer mem.Free(buf)
- bytesTransferred, err := io.CopyBuffer(w, proxyResponse.Body, buf)
+ bytesTransferred, err := io.CopyBuffer(w, bodyReader, buf)
if err != nil {
- glog.V(1).Infof("passthrough response read %d bytes: %v", bytesTransferred, err)
+ glog.V(1).Infof("response read %d bytes: %v", bytesTransferred, err)
}
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)
+
+ // Copy headers from proxy response
+ for k, v := range proxyResponse.Header {
+ w.Header()[k] = v
+ }
+
+ 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) {
+ // Check if the object has SSE-C metadata
+ sseAlgorithm := proxyResponse.Header.Get(s3_constants.AmzServerSideEncryptionCustomerAlgorithm)
+ sseKeyMD5 := proxyResponse.Header.Get(s3_constants.AmzServerSideEncryptionCustomerKeyMD5)
+ isObjectEncrypted := sseAlgorithm != "" && sseKeyMD5 != ""
+
+ // Parse SSE-C headers from request once (avoid duplication)
+ customerKey, err := ParseSSECHeaders(r)
+ if err != nil {
+ errCode := MapSSECErrorToS3Error(err)
+ s3err.WriteErrorResponse(w, r, errCode)
+ return http.StatusBadRequest, 0
+ }
+
+ if isObjectEncrypted {
+ // This object was encrypted with SSE-C, validate customer key
+ if customerKey == nil {
+ s3err.WriteErrorResponse(w, r, s3err.ErrSSECustomerKeyMissing)
+ return http.StatusBadRequest, 0
+ }
+
+ // SSE-C MD5 is base64 and case-sensitive
+ if customerKey.KeyMD5 != sseKeyMD5 {
+ // For GET/HEAD requests, AWS S3 returns 403 Forbidden for a key mismatch.
+ s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
+ return http.StatusForbidden, 0
+ }
+
+ // SSE-C encrypted objects do not support HTTP Range requests because the 16-byte IV
+ // is required at the beginning of the stream for proper decryption
+ if r.Header.Get("Range") != "" {
+ s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRange)
+ return http.StatusRequestedRangeNotSatisfiable, 0
+ }
+
+ // Create decrypted reader
+ decryptedReader, decErr := CreateSSECDecryptedReader(proxyResponse.Body, customerKey)
+ if decErr != nil {
+ glog.Errorf("Failed to create SSE-C decrypted reader: %v", decErr)
+ s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
+ return http.StatusInternalServerError, 0
+ }
+
+ // 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
+ }
+ }
+
+ // Set correct Content-Length for SSE-C (only for full object requests)
+ // Range requests are complex with SSE-C because the entire object needs decryption
+ if proxyResponse.Header.Get("Content-Range") == "" {
+ // Full object request: subtract 16-byte IV from encrypted length
+ if contentLengthStr := proxyResponse.Header.Get("Content-Length"); contentLengthStr != "" {
+ encryptedLength, err := strconv.ParseInt(contentLengthStr, 10, 64)
+ if err != nil {
+ glog.Errorf("Invalid Content-Length header for SSE-C object: %v", err)
+ s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
+ return http.StatusInternalServerError, 0
+ }
+ originalLength := encryptedLength - 16
+ if originalLength < 0 {
+ glog.Errorf("Encrypted object length (%d) is less than IV size (16 bytes)", encryptedLength)
+ s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
+ return http.StatusInternalServerError, 0
+ }
+ w.Header().Set("Content-Length", strconv.FormatInt(originalLength, 10))
+ }
+ }
+ // For range requests, let the actual bytes transferred determine the response length
+
+ // Add SSE-C response headers
+ w.Header().Set(s3_constants.AmzServerSideEncryptionCustomerAlgorithm, sseAlgorithm)
+ w.Header().Set(s3_constants.AmzServerSideEncryptionCustomerKeyMD5, sseKeyMD5)
+
+ return writeFinalResponse(w, proxyResponse, decryptedReader, capturedCORSHeaders)
+ } else {
+ // Object is not encrypted, but check if customer provided SSE-C headers unnecessarily
+ if customerKey != nil {
+ s3err.WriteErrorResponse(w, r, s3err.ErrSSECustomerKeyNotNeeded)
+ return http.StatusBadRequest, 0
+ }
+
+ // Normal pass-through response
+ return passThroughResponse(proxyResponse, w)
+ }
+}
+
// 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) {
diff --git a/weed/s3api/s3api_object_handlers_copy.go b/weed/s3api/s3api_object_handlers_copy.go
index 888b38e94..18159ab17 100644
--- a/weed/s3api/s3api_object_handlers_copy.go
+++ b/weed/s3api/s3api_object_handlers_copy.go
@@ -1,8 +1,10 @@
package s3api
import (
+ "bytes"
"context"
"fmt"
+ "io"
"net/http"
"net/url"
"strconv"
@@ -160,11 +162,17 @@ func (s3a *S3ApiServer) CopyObjectHandler(w http.ResponseWriter, r *http.Request
// Just copy the entry structure without chunks for zero-size files
dstEntry.Chunks = nil
} else {
- // Replicate chunks for files with content
- dstChunks, err := s3a.copyChunks(entry, r.URL.Path)
+ // Handle SSE-C copy with smart fast/slow path selection
+ dstChunks, err := s3a.copyChunksWithSSEC(entry, r)
if err != nil {
- glog.Errorf("CopyObjectHandler copy chunks error: %v", err)
- s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
+ glog.Errorf("CopyObjectHandler copy chunks with SSE-C error: %v", err)
+ // Use shared error mapping helper
+ errCode := MapSSECErrorToS3Error(err)
+ // For copy operations, if the error is not recognized, use InternalError
+ if errCode == s3err.ErrInvalidRequest {
+ errCode = s3err.ErrInternalError
+ }
+ s3err.WriteErrorResponse(w, r, errCode)
return
}
dstEntry.Chunks = dstChunks
@@ -591,7 +599,8 @@ func processMetadataBytes(reqHeader http.Header, existing map[string][]byte, rep
// copyChunks replicates chunks from source entry to destination entry
func (s3a *S3ApiServer) copyChunks(entry *filer_pb.Entry, dstPath string) ([]*filer_pb.FileChunk, error) {
dstChunks := make([]*filer_pb.FileChunk, len(entry.GetChunks()))
- executor := util.NewLimitedConcurrentExecutor(4) // Limit to 4 concurrent operations
+ const defaultChunkCopyConcurrency = 4
+ executor := util.NewLimitedConcurrentExecutor(defaultChunkCopyConcurrency) // Limit to configurable concurrent operations
errChan := make(chan error, len(entry.GetChunks()))
for i, chunk := range entry.GetChunks() {
@@ -777,7 +786,8 @@ func (s3a *S3ApiServer) copyChunksForRange(entry *filer_pb.Entry, startOffset, e
// Copy the relevant chunks using a specialized method for range copies
dstChunks := make([]*filer_pb.FileChunk, len(relevantChunks))
- executor := util.NewLimitedConcurrentExecutor(4)
+ const defaultChunkCopyConcurrency = 4
+ executor := util.NewLimitedConcurrentExecutor(defaultChunkCopyConcurrency)
errChan := make(chan error, len(relevantChunks))
// Create a map to track original chunks for each relevant chunk
@@ -997,3 +1007,136 @@ func (s3a *S3ApiServer) downloadChunkData(srcUrl string, offset, size int64) ([]
}
return chunkData, nil
}
+
+// copyChunksWithSSEC handles SSE-C aware copying with smart fast/slow path selection
+func (s3a *S3ApiServer) copyChunksWithSSEC(entry *filer_pb.Entry, r *http.Request) ([]*filer_pb.FileChunk, error) {
+ // Parse SSE-C headers
+ copySourceKey, err := ParseSSECCopySourceHeaders(r)
+ if err != nil {
+ return nil, err
+ }
+
+ destKey, err := ParseSSECHeaders(r)
+ if err != nil {
+ return nil, err
+ }
+
+ // Determine copy strategy
+ strategy, err := DetermineSSECCopyStrategy(entry.Extended, copySourceKey, destKey)
+ if err != nil {
+ return nil, err
+ }
+
+ glog.V(2).Infof("SSE-C copy strategy for %s: %v", r.URL.Path, strategy)
+
+ switch strategy {
+ case SSECCopyDirect:
+ // FAST PATH: Direct chunk copy
+ glog.V(2).Infof("Using fast path: direct chunk copy for %s", r.URL.Path)
+ return s3a.copyChunks(entry, r.URL.Path)
+
+ case SSECCopyReencrypt:
+ // SLOW PATH: Decrypt and re-encrypt
+ glog.V(2).Infof("Using slow path: decrypt/re-encrypt for %s", r.URL.Path)
+ return s3a.copyChunksWithReencryption(entry, copySourceKey, destKey, r.URL.Path)
+
+ default:
+ return nil, fmt.Errorf("unknown SSE-C copy strategy: %v", strategy)
+ }
+}
+
+// copyChunksWithReencryption handles the slow path: decrypt source and re-encrypt for destination
+func (s3a *S3ApiServer) copyChunksWithReencryption(entry *filer_pb.Entry, copySourceKey *SSECustomerKey, destKey *SSECustomerKey, dstPath string) ([]*filer_pb.FileChunk, error) {
+ dstChunks := make([]*filer_pb.FileChunk, len(entry.GetChunks()))
+ const defaultChunkCopyConcurrency = 4
+ executor := util.NewLimitedConcurrentExecutor(defaultChunkCopyConcurrency) // Limit to configurable concurrent operations
+ errChan := make(chan error, len(entry.GetChunks()))
+
+ for i, chunk := range entry.GetChunks() {
+ chunkIndex := i
+ executor.Execute(func() {
+ dstChunk, err := s3a.copyChunkWithReencryption(chunk, copySourceKey, destKey, dstPath)
+ if err != nil {
+ errChan <- fmt.Errorf("chunk %d: %v", chunkIndex, err)
+ return
+ }
+ dstChunks[chunkIndex] = dstChunk
+ errChan <- nil
+ })
+ }
+
+ // Wait for all operations to complete and check for errors
+ for i := 0; i < len(entry.GetChunks()); i++ {
+ if err := <-errChan; err != nil {
+ return nil, err
+ }
+ }
+
+ return dstChunks, nil
+}
+
+// copyChunkWithReencryption copies a single chunk with decrypt/re-encrypt
+func (s3a *S3ApiServer) copyChunkWithReencryption(chunk *filer_pb.FileChunk, copySourceKey *SSECustomerKey, destKey *SSECustomerKey, dstPath string) (*filer_pb.FileChunk, error) {
+ // Create destination chunk
+ dstChunk := s3a.createDestinationChunk(chunk, chunk.Offset, chunk.Size)
+
+ // Prepare chunk copy (assign new volume and get source URL)
+ assignResult, srcUrl, err := s3a.prepareChunkCopy(chunk.GetFileIdString(), dstPath)
+ if err != nil {
+ return nil, err
+ }
+
+ // Set file ID on destination chunk
+ if err := s3a.setChunkFileId(dstChunk, assignResult); err != nil {
+ return nil, err
+ }
+
+ // Download encrypted chunk data
+ encryptedData, err := s3a.downloadChunkData(srcUrl, 0, int64(chunk.Size))
+ if err != nil {
+ return nil, fmt.Errorf("download encrypted chunk data: %w", err)
+ }
+
+ var finalData []byte
+
+ // Decrypt if source is encrypted
+ if copySourceKey != nil {
+ decryptedReader, decErr := CreateSSECDecryptedReader(bytes.NewReader(encryptedData), copySourceKey)
+ if decErr != nil {
+ return nil, fmt.Errorf("create decrypted reader: %w", decErr)
+ }
+
+ decryptedData, readErr := io.ReadAll(decryptedReader)
+ if readErr != nil {
+ return nil, fmt.Errorf("decrypt chunk data: %w", readErr)
+ }
+ finalData = decryptedData
+ } else {
+ // Source is unencrypted
+ finalData = encryptedData
+ }
+
+ // Re-encrypt if destination should be encrypted
+ if destKey != nil {
+ encryptedReader, encErr := CreateSSECEncryptedReader(bytes.NewReader(finalData), destKey)
+ if encErr != nil {
+ return nil, fmt.Errorf("create encrypted reader: %w", encErr)
+ }
+
+ reencryptedData, readErr := io.ReadAll(encryptedReader)
+ if readErr != nil {
+ return nil, fmt.Errorf("re-encrypt chunk data: %w", readErr)
+ }
+ finalData = reencryptedData
+
+ // Update chunk size to include IV
+ dstChunk.Size = uint64(len(finalData))
+ }
+
+ // Upload the processed data
+ if err := s3a.uploadChunkData(finalData, assignResult); err != nil {
+ return nil, fmt.Errorf("upload processed chunk data: %w", err)
+ }
+
+ return dstChunk, nil
+}
diff --git a/weed/s3api/s3api_object_handlers_put.go b/weed/s3api/s3api_object_handlers_put.go
index 3d8a62b09..63972bcd6 100644
--- a/weed/s3api/s3api_object_handlers_put.go
+++ b/weed/s3api/s3api_object_handlers_put.go
@@ -190,6 +190,25 @@ func (s3a *S3ApiServer) PutObjectHandler(w http.ResponseWriter, r *http.Request)
func (s3a *S3ApiServer) putToFiler(r *http.Request, uploadUrl string, dataReader io.Reader, destination string, bucket string) (etag string, code s3err.ErrorCode) {
+ // Handle SSE-C encryption if requested
+ customerKey, err := ParseSSECHeaders(r)
+ if err != nil {
+ glog.Errorf("SSE-C header validation failed: %v", err)
+ // Use shared error mapping helper
+ errCode := MapSSECErrorToS3Error(err)
+ return "", errCode
+ }
+
+ // Apply SSE-C encryption if customer key is provided
+ if customerKey != nil {
+ encryptedReader, encErr := CreateSSECEncryptedReader(dataReader, customerKey)
+ if encErr != nil {
+ glog.Errorf("Failed to create SSE-C encrypted reader: %v", encErr)
+ return "", s3err.ErrInternalError
+ }
+ dataReader = encryptedReader
+ }
+
hash := md5.New()
var body = io.TeeReader(dataReader, hash)
diff --git a/weed/s3api/s3err/s3api_errors.go b/weed/s3api/s3err/s3api_errors.go
index 4bb63d67f..6833a498a 100644
--- a/weed/s3api/s3err/s3api_errors.go
+++ b/weed/s3api/s3err/s3api_errors.go
@@ -116,6 +116,13 @@ const (
ErrInvalidRetentionPeriod
ErrObjectLockConfigurationNotFoundError
ErrInvalidUnorderedWithDelimiter
+
+ // SSE-C related errors
+ ErrInvalidEncryptionAlgorithm
+ ErrInvalidEncryptionKey
+ ErrSSECustomerKeyMD5Mismatch
+ ErrSSECustomerKeyMissing
+ ErrSSECustomerKeyNotNeeded
)
// Error message constants for checksum validation
@@ -471,6 +478,33 @@ var errorCodeResponse = map[ErrorCode]APIError{
Description: "Unordered listing cannot be used with delimiter",
HTTPStatusCode: http.StatusBadRequest,
},
+
+ // SSE-C related error mappings
+ ErrInvalidEncryptionAlgorithm: {
+ Code: "InvalidEncryptionAlgorithmError",
+ Description: "The encryption algorithm specified is not valid.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidEncryptionKey: {
+ Code: "InvalidArgument",
+ Description: "Invalid encryption key. Encryption key must be 256-bit AES256.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrSSECustomerKeyMD5Mismatch: {
+ Code: "InvalidArgument",
+ Description: "The provided customer encryption key MD5 does not match the key.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrSSECustomerKeyMissing: {
+ Code: "InvalidArgument",
+ Description: "Requests specifying Server Side Encryption with Customer provided keys must provide the customer key.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrSSECustomerKeyNotNeeded: {
+ Code: "InvalidArgument",
+ Description: "The object was not encrypted with customer provided keys.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
}
// GetAPIError provides API Error for input API error code.