package s3api import ( "fmt" "net/http" "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" "github.com/seaweedfs/seaweedfs/weed/s3api/s3err" ) // CopyValidationError represents validation errors during copy operations type CopyValidationError struct { Code s3err.ErrorCode Message string } func (e *CopyValidationError) Error() string { return e.Message } // ValidateCopyEncryption performs comprehensive validation of copy encryption parameters func ValidateCopyEncryption(srcMetadata map[string][]byte, headers http.Header) error { // Validate SSE-C copy requirements if err := validateSSECCopyRequirements(srcMetadata, headers); err != nil { return err } // Validate SSE-KMS copy requirements if err := validateSSEKMSCopyRequirements(srcMetadata, headers); err != nil { return err } // Validate incompatible encryption combinations if err := validateEncryptionCompatibility(headers); err != nil { return err } return nil } // validateSSECCopyRequirements validates SSE-C copy header requirements func validateSSECCopyRequirements(srcMetadata map[string][]byte, headers http.Header) error { srcIsSSEC := IsSSECEncrypted(srcMetadata) hasCopyHeaders := hasSSECCopyHeaders(headers) hasSSECHeaders := hasSSECHeaders(headers) // If source is SSE-C encrypted, copy headers are required if srcIsSSEC && !hasCopyHeaders { return &CopyValidationError{ Code: s3err.ErrInvalidRequest, Message: "SSE-C encrypted source requires copy source encryption headers", } } // If copy headers are provided, source must be SSE-C encrypted if hasCopyHeaders && !srcIsSSEC { return &CopyValidationError{ Code: s3err.ErrInvalidRequest, Message: "SSE-C copy headers provided but source is not SSE-C encrypted", } } // Validate copy header completeness if hasCopyHeaders { if err := validateSSECCopyHeaderCompleteness(headers); err != nil { return err } } // Validate destination SSE-C headers if present if hasSSECHeaders { if err := validateSSECHeaderCompleteness(headers); err != nil { return err } } return nil } // validateSSEKMSCopyRequirements validates SSE-KMS copy requirements func validateSSEKMSCopyRequirements(srcMetadata map[string][]byte, headers http.Header) error { dstIsSSEKMS := IsSSEKMSRequest(&http.Request{Header: headers}) // Validate KMS key ID format if provided if dstIsSSEKMS { keyID := headers.Get(s3_constants.AmzServerSideEncryptionAwsKmsKeyId) if keyID != "" && !isValidKMSKeyID(keyID) { return &CopyValidationError{ Code: s3err.ErrKMSKeyNotFound, Message: fmt.Sprintf("Invalid KMS key ID format: %s", keyID), } } } // Validate encryption context format if provided if contextHeader := headers.Get(s3_constants.AmzServerSideEncryptionContext); contextHeader != "" { if !dstIsSSEKMS { return &CopyValidationError{ Code: s3err.ErrInvalidRequest, Message: "Encryption context can only be used with SSE-KMS", } } // Validate base64 encoding and JSON format if err := validateEncryptionContext(contextHeader); err != nil { return &CopyValidationError{ Code: s3err.ErrInvalidRequest, Message: fmt.Sprintf("Invalid encryption context: %v", err), } } } return nil } // validateEncryptionCompatibility validates that encryption methods are not conflicting func validateEncryptionCompatibility(headers http.Header) error { sseAlgorithm := headers.Get(s3_constants.AmzServerSideEncryption) hasSSEC := hasSSECHeaders(headers) hasSSEKMS := sseAlgorithm == s3_constants.SSEAlgorithmKMS hasSSES3 := sseAlgorithm == s3_constants.SSEAlgorithmAES256 // Reject unsupported algorithms so they are never persisted as a bogus // destination header advertising encryption that was never applied. if sseAlgorithm != "" && !hasSSEKMS && !hasSSES3 { return &CopyValidationError{ Code: s3err.ErrInvalidEncryptionAlgorithm, Message: fmt.Sprintf("Unsupported server-side encryption algorithm: %s", sseAlgorithm), } } // Count how many encryption methods are specified encryptionCount := 0 if hasSSEC { encryptionCount++ } if hasSSEKMS { encryptionCount++ } if hasSSES3 { encryptionCount++ } // Only one encryption method should be specified if encryptionCount > 1 { return &CopyValidationError{ Code: s3err.ErrInvalidRequest, Message: "Multiple encryption methods specified - only one is allowed", } } return nil } // validateSSECCopyHeaderCompleteness validates that all required SSE-C copy headers are present func validateSSECCopyHeaderCompleteness(headers http.Header) error { algorithm := headers.Get(s3_constants.AmzCopySourceServerSideEncryptionCustomerAlgorithm) key := headers.Get(s3_constants.AmzCopySourceServerSideEncryptionCustomerKey) keyMD5 := headers.Get(s3_constants.AmzCopySourceServerSideEncryptionCustomerKeyMD5) if algorithm == "" { return &CopyValidationError{ Code: s3err.ErrInvalidRequest, Message: "SSE-C copy customer algorithm header is required", } } if key == "" { return &CopyValidationError{ Code: s3err.ErrInvalidRequest, Message: "SSE-C copy customer key header is required", } } if keyMD5 == "" { return &CopyValidationError{ Code: s3err.ErrInvalidRequest, Message: "SSE-C copy customer key MD5 header is required", } } // Validate algorithm if algorithm != "AES256" { return &CopyValidationError{ Code: s3err.ErrInvalidRequest, Message: fmt.Sprintf("Unsupported SSE-C algorithm: %s", algorithm), } } return nil } // validateSSECHeaderCompleteness validates that all required SSE-C headers are present func validateSSECHeaderCompleteness(headers http.Header) error { algorithm := headers.Get(s3_constants.AmzServerSideEncryptionCustomerAlgorithm) key := headers.Get(s3_constants.AmzServerSideEncryptionCustomerKey) keyMD5 := headers.Get(s3_constants.AmzServerSideEncryptionCustomerKeyMD5) if algorithm == "" { return &CopyValidationError{ Code: s3err.ErrInvalidRequest, Message: "SSE-C customer algorithm header is required", } } if key == "" { return &CopyValidationError{ Code: s3err.ErrInvalidRequest, Message: "SSE-C customer key header is required", } } if keyMD5 == "" { return &CopyValidationError{ Code: s3err.ErrInvalidRequest, Message: "SSE-C customer key MD5 header is required", } } // Validate algorithm if algorithm != "AES256" { return &CopyValidationError{ Code: s3err.ErrInvalidRequest, Message: fmt.Sprintf("Unsupported SSE-C algorithm: %s", algorithm), } } return nil } // Helper functions for header detection func hasSSECCopyHeaders(headers http.Header) bool { return headers.Get(s3_constants.AmzCopySourceServerSideEncryptionCustomerAlgorithm) != "" || headers.Get(s3_constants.AmzCopySourceServerSideEncryptionCustomerKey) != "" || headers.Get(s3_constants.AmzCopySourceServerSideEncryptionCustomerKeyMD5) != "" } func hasSSECHeaders(headers http.Header) bool { return headers.Get(s3_constants.AmzServerSideEncryptionCustomerAlgorithm) != "" || headers.Get(s3_constants.AmzServerSideEncryptionCustomerKey) != "" || headers.Get(s3_constants.AmzServerSideEncryptionCustomerKeyMD5) != "" } // validateEncryptionContext validates the encryption context header format func validateEncryptionContext(contextHeader string) error { // This would validate base64 encoding and JSON format // Implementation would decode base64 and parse JSON // For now, just check it's not empty if contextHeader == "" { return fmt.Errorf("encryption context cannot be empty") } return nil } // ValidateCopySource validates the copy source path. func ValidateCopySource(copySource string, srcBucket, srcObject string) error { return validateCopySource(copySource, srcBucket, srcObject, "") } func validateCopySource(copySource string, srcBucket, srcObject, srcVersionId string) error { if copySource == "" { return &CopyValidationError{ Code: s3err.ErrInvalidCopySource, Message: "Copy source header is required", } } if srcBucket == "" { return &CopyValidationError{ Code: s3err.ErrInvalidCopySource, Message: "Source bucket cannot be empty", } } if srcObject == "" { return &CopyValidationError{ Code: s3err.ErrInvalidCopySource, Message: "Source object cannot be empty", } } // `.`/`..` segments are collapsed by the filer's path join; reject them as // IsValidObjectKey does for the request URL so the source stays in-bucket. if !s3_constants.IsValidBucketName(srcBucket) || !s3_constants.IsValidObjectKey(srcObject) { return &CopyValidationError{ Code: s3err.ErrInvalidCopySource, Message: "Copy source contains invalid path segments", } } if !isValidVersionID(srcVersionId) { return &CopyValidationError{ Code: s3err.ErrInvalidCopySource, Message: "Copy source contains an invalid version ID", } } return nil } // ValidateCopyDestination validates the copy destination func ValidateCopyDestination(dstBucket, dstObject string) error { if dstBucket == "" { return &CopyValidationError{ Code: s3err.ErrInvalidRequest, Message: "Destination bucket cannot be empty", } } if dstObject == "" { return &CopyValidationError{ Code: s3err.ErrInvalidRequest, Message: "Destination object cannot be empty", } } return nil } // MapCopyValidationError maps validation errors to appropriate S3 error codes func MapCopyValidationError(err error) s3err.ErrorCode { if validationErr, ok := err.(*CopyValidationError); ok { return validationErr.Code } return s3err.ErrInvalidRequest }