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