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.go275
1 files changed, 275 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..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
+ }
+}