aboutsummaryrefslogtreecommitdiff
path: root/weed/s3api/s3_sse_c.go
diff options
context:
space:
mode:
Diffstat (limited to 'weed/s3api/s3_sse_c.go')
-rw-r--r--weed/s3api/s3_sse_c.go344
1 files changed, 344 insertions, 0 deletions
diff --git a/weed/s3api/s3_sse_c.go b/weed/s3api/s3_sse_c.go
new file mode 100644
index 000000000..733ae764e
--- /dev/null
+++ b/weed/s3api/s3_sse_c.go
@@ -0,0 +1,344 @@
+package s3api
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/md5"
+ "crypto/rand"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/seaweedfs/seaweedfs/weed/glog"
+ "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
+ "github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
+)
+
+// SSECCopyStrategy represents different strategies for copying SSE-C objects
+type SSECCopyStrategy int
+
+const (
+ // SSECCopyStrategyDirect indicates the object can be copied directly without decryption
+ SSECCopyStrategyDirect SSECCopyStrategy = iota
+ // SSECCopyStrategyDecryptEncrypt indicates the object must be decrypted then re-encrypted
+ SSECCopyStrategyDecryptEncrypt
+)
+
+const (
+ // SSE-C constants
+ SSECustomerAlgorithmAES256 = s3_constants.SSEAlgorithmAES256
+ SSECustomerKeySize = 32 // 256 bits
+)
+
+// 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
+}
+
+// IsSSECRequest checks if the request contains SSE-C headers
+func IsSSECRequest(r *http.Request) bool {
+ // If SSE-KMS headers are present, this is not an SSE-C request (they are mutually exclusive)
+ sseAlgorithm := r.Header.Get(s3_constants.AmzServerSideEncryption)
+ if sseAlgorithm == "aws:kms" || r.Header.Get(s3_constants.AmzServerSideEncryptionAwsKmsKeyId) != "" {
+ return false
+ }
+
+ return r.Header.Get(s3_constants.AmzServerSideEncryptionCustomerAlgorithm) != ""
+}
+
+// IsSSECEncrypted checks if the metadata indicates SSE-C encryption
+func IsSSECEncrypted(metadata map[string][]byte) bool {
+ if metadata == nil {
+ return false
+ }
+
+ // Check for SSE-C specific metadata keys
+ if _, exists := metadata[s3_constants.AmzServerSideEncryptionCustomerAlgorithm]; exists {
+ return true
+ }
+ if _, exists := metadata[s3_constants.AmzServerSideEncryptionCustomerKeyMD5]; exists {
+ return true
+ }
+
+ return false
+}
+
+// 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[:])
+
+ // Debug logging for MD5 validation
+ glog.V(4).Infof("SSE-C MD5 validation: provided='%s', expected='%s', keyBytes=%x", keyMD5, expectedMD5, keyBytes)
+
+ if keyMD5 != expectedMD5 {
+ glog.Errorf("SSE-C MD5 mismatch: provided='%s', expected='%s'", 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
+// Returns the encrypted reader and the IV for metadata storage
+func CreateSSECEncryptedReader(r io.Reader, customerKey *SSECustomerKey) (io.Reader, []byte, error) {
+ if customerKey == nil {
+ return r, nil, nil
+ }
+
+ // Create AES cipher
+ block, err := aes.NewCipher(customerKey.Key)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to create AES cipher: %v", err)
+ }
+
+ // Generate random IV
+ iv := make([]byte, s3_constants.AESBlockSize)
+ if _, err := io.ReadFull(rand.Reader, iv); err != nil {
+ return nil, nil, fmt.Errorf("failed to generate IV: %v", err)
+ }
+
+ // Create CTR mode cipher
+ stream := cipher.NewCTR(block, iv)
+
+ // The IV is stored in metadata, so the encrypted stream does not need to prepend the IV
+ // This ensures correct Content-Length for clients
+ encryptedReader := &cipher.StreamReader{S: stream, R: r}
+
+ return encryptedReader, iv, nil
+}
+
+// CreateSSECDecryptedReader creates a new decrypted reader for SSE-C
+// The IV comes from metadata, not from the encrypted data stream
+func CreateSSECDecryptedReader(r io.Reader, customerKey *SSECustomerKey, iv []byte) (io.Reader, error) {
+ if customerKey == nil {
+ return r, nil
+ }
+
+ // IV must be provided from metadata
+ if err := ValidateIV(iv, "IV"); err != nil {
+ return nil, fmt.Errorf("invalid IV from metadata: %w", err)
+ }
+
+ // Create AES cipher
+ block, err := aes.NewCipher(customerKey.Key)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create AES cipher: %v", err)
+ }
+
+ // Create CTR mode cipher using the IV from metadata
+ stream := cipher.NewCTR(block, iv)
+
+ return &cipher.StreamReader{S: stream, R: r}, nil
+}
+
+// CreateSSECEncryptedReaderWithOffset creates an encrypted reader with a specific counter offset
+// This is used for chunk-level encryption where each chunk needs a different counter position
+func CreateSSECEncryptedReaderWithOffset(r io.Reader, customerKey *SSECustomerKey, iv []byte, counterOffset uint64) (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)
+ }
+
+ // Create CTR mode cipher with offset
+ stream := createCTRStreamWithOffset(block, iv, counterOffset)
+
+ return &cipher.StreamReader{S: stream, R: r}, nil
+}
+
+// CreateSSECDecryptedReaderWithOffset creates a decrypted reader with a specific counter offset
+func CreateSSECDecryptedReaderWithOffset(r io.Reader, customerKey *SSECustomerKey, iv []byte, counterOffset uint64) (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)
+ }
+
+ // Create CTR mode cipher with offset
+ stream := createCTRStreamWithOffset(block, iv, counterOffset)
+
+ return &cipher.StreamReader{S: stream, R: r}, nil
+}
+
+// createCTRStreamWithOffset creates a CTR stream positioned at a specific counter offset
+func createCTRStreamWithOffset(block cipher.Block, iv []byte, counterOffset uint64) cipher.Stream {
+ // Create a copy of the IV to avoid modifying the original
+ offsetIV := make([]byte, len(iv))
+ copy(offsetIV, iv)
+
+ // Calculate the counter offset in blocks (AES block size is 16 bytes)
+ blockOffset := counterOffset / 16
+
+ // Add the block offset to the counter portion of the IV
+ // In AES-CTR, the last 8 bytes of the IV are typically used as the counter
+ addCounterToIV(offsetIV, blockOffset)
+
+ return cipher.NewCTR(block, offsetIV)
+}
+
+// addCounterToIV adds a counter value to the IV (treating last 8 bytes as big-endian counter)
+func addCounterToIV(iv []byte, counter uint64) {
+ // Use the last 8 bytes as a big-endian counter
+ for i := 7; i >= 0; i-- {
+ carry := counter & 0xff
+ iv[len(iv)-8+i] += byte(carry)
+ if iv[len(iv)-8+i] >= byte(carry) {
+ break // No overflow
+ }
+ counter >>= 8
+ }
+}
+
+// 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
+}
+
+// Note: SSECCopyStrategy is defined above
+
+// 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 SSECCopyStrategyDecryptEncrypt, ErrSSECustomerKeyMissing
+ }
+ if copySourceKey.KeyMD5 != srcKeyMD5 {
+ return SSECCopyStrategyDecryptEncrypt, ErrSSECustomerKeyMD5Mismatch
+ }
+ } else if copySourceKey != nil {
+ // Source not encrypted but copy source key provided
+ return SSECCopyStrategyDecryptEncrypt, ErrSSECustomerKeyNotNeeded
+ }
+
+ if CanDirectCopySSEC(srcMetadata, copySourceKey, destKey) {
+ return SSECCopyStrategyDirect, nil
+ }
+
+ return SSECCopyStrategyDecryptEncrypt, 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
+ }
+}