package s3api import ( "errors" "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 { // A repeated header is rejected rather than deduped: the encryption paths // apply only the first value, so extra values could hide the method the // client actually asked for. for _, name := range []string{ s3_constants.AmzServerSideEncryption, s3_constants.AmzServerSideEncryptionCustomerAlgorithm, s3_constants.AmzServerSideEncryptionCustomerKey, s3_constants.AmzServerSideEncryptionCustomerKeyMD5, s3_constants.AmzServerSideEncryptionAwsKmsKeyId, s3_constants.AmzServerSideEncryptionContext, s3_constants.AmzServerSideEncryptionBucketKeyEnabled, } { if len(headers.Values(name)) > 1 { return &CopyValidationError{ Code: s3err.ErrInvalidRequest, Message: fmt.Sprintf("Multiple %s headers specified - only one is allowed", name), } } } 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), } } // KMS options only apply to aws:kms; with another method or none they name // no method at all. if !hasSSEKMS && hasHeaderValue(headers, s3_constants.AmzServerSideEncryptionAwsKmsKeyId, s3_constants.AmzServerSideEncryptionContext, s3_constants.AmzServerSideEncryptionBucketKeyEnabled) { return &CopyValidationError{ Code: s3err.ErrInvalidRequest, Message: "KMS encryption options require the aws:kms encryption method", } } // Only one encryption method should be specified encryptionCount := 0 for _, specified := range []bool{hasSSEC, hasSSEKMS, hasSSES3} { if specified { encryptionCount++ } } if encryptionCount > 1 { return &CopyValidationError{ Code: s3err.ErrInvalidRequest, Message: "Multiple encryption methods specified - only one is allowed", } } return nil } // ValidateRequestEncryption rejects PutObject or CreateMultipartUpload // encryption headers that no single method can honor, with the InvalidArgument // codes S3 returns for them. func ValidateRequestEncryption(headers http.Header) s3err.ErrorCode { err := validateEncryptionCompatibility(headers) if err == nil { return s3err.ErrNone } var validationErr *CopyValidationError if errors.As(err, &validationErr) && validationErr.Code == s3err.ErrInvalidEncryptionAlgorithm { return s3err.ErrInvalidEncryptionMethod } return s3err.ErrIncompatibleEncryptionMethod } // 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 hasHeaderValue(headers http.Header, names ...string) bool { for _, name := range names { for _, value := range headers.Values(name) { if value != "" { return true } } } return false } func hasSSECCopyHeaders(headers http.Header) bool { return hasHeaderValue(headers, s3_constants.AmzCopySourceServerSideEncryptionCustomerAlgorithm, s3_constants.AmzCopySourceServerSideEncryptionCustomerKey, s3_constants.AmzCopySourceServerSideEncryptionCustomerKeyMD5) } func hasSSECHeaders(headers http.Header) bool { return hasHeaderValue(headers, s3_constants.AmzServerSideEncryptionCustomerAlgorithm, s3_constants.AmzServerSideEncryptionCustomerKey, 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 }