mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-12 01:07:35 +02:00
* s3api: reject SSE headers PutObject cannot honor, as CopyObject does PutObject and CreateMultipartUpload did not check the server-side encryption headers they were given. An x-amz-server-side-encryption value that names no method, such as "aes:kms", matched none of the SSE paths, so the object was stored unencrypted and the request answered 200. SSE-C together with x-amz-server-side-encryption was also accepted, and one of the two silently won. CopyObject already rejects both through validateEncryptionCompatibility. Run the same check, with the same error codes, before PutObject and CreateMultipartUpload store anything. * s3api: answer rejected SSE headers with InvalidArgument, as S3 does S3 rejects an unknown x-amz-server-side-encryption value and SSE-C combined with another method with InvalidArgument. PutObject and CreateMultipartUpload now return that code, with S3's messages, through two new error codes; CopyObject keeps its own. Also run ceph/s3-tests' test_put_obj_enc_conflict_c_s3, _c_kms and _bad_enc_kms in CI, which check both handlers' responses end to end. * s3api: close the remaining ways a PUT could skip requested encryption - A repeated x-amz-server-side-encryption header is rejected: the encryption paths apply only the first value, so extra values could hide the method the client asked for. - KMS options (key id, encryption context, bucket key) are rejected unless the method is aws:kms, matching the S3 InvalidArgument error. - CreateMultipartUpload validates before auto-create, so a refused upload cannot leave a bucket behind. - Directory markers encrypt their inline content through the shared SSE path and record the same entry metadata as regular objects, instead of storing requested-encrypted bytes in plaintext. * s3api: read back what the SSE marker write stores - Repeated SSE-C and KMS option headers are rejected alongside a repeated x-amz-server-side-encryption, closing the same first-value bypass for customer-key and KMS fields. - Algorithm validity is checked before KMS options so an unsupported value keeps the "not supported" error. - serveDirectoryContent decrypts marker content with the stored SSE metadata and returns the SSE headers, so an encrypted marker reads back what was written; its Content-Length now reflects the bytes actually served. * s3api: HEAD of a marker skips decryption and keeps the stored size HEAD returns no body, so it now runs only the SSE-C key check instead of decrypting — matching HeadObjectHandler and avoiding a KMS round-trip — and chunk-backed directory entries report Attributes.FileSize again rather than the length of their (empty) inline content. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3api: keep GET Content-Length to the bytes serveDirectoryContent writes The FileSize override described chunk-backed markers on HEAD, but GET sends only the inline content, so it promised bytes it never wrote. * s3api: stream a chunk-backed directory on GET like any object A directory promoted over an uploaded object keeps the object chunks with empty inline content, so serving only entry.Content made GET deliver nothing while HEAD reported FileSize. GET now routes those entries through the regular volume-server stream, keeping the two methods consistent. * s3api: answer a busy volume read with RequestBytesExceed --------- Co-authored-by: Chris Lu <chris.lu@gmail.com> Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com> Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
296 lines
11 KiB
Go
296 lines
11 KiB
Go
package s3api
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
|
)
|
|
|
|
// PutToFilerEncryptionResult holds the result of encryption processing
|
|
type PutToFilerEncryptionResult struct {
|
|
DataReader io.Reader
|
|
SSEType string
|
|
CustomerKey *SSECustomerKey
|
|
SSEIV []byte
|
|
SSEKMSKey *SSEKMSKey
|
|
SSES3Key *SSES3Key
|
|
SSEKMSMetadata []byte
|
|
SSES3Metadata []byte
|
|
}
|
|
|
|
// handleSSECEncryption processes SSE-C encryption for the data reader
|
|
func (s3a *S3ApiServer) handleSSECEncryption(r *http.Request, dataReader io.Reader) (io.Reader, *SSECustomerKey, []byte, s3err.ErrorCode) {
|
|
// Handle SSE-C encryption if requested
|
|
customerKey, err := ParseSSECHeaders(r)
|
|
if err != nil {
|
|
glog.Errorf("SSE-C header validation failed: %v", err)
|
|
// Use shared error mapping helper
|
|
errCode := MapSSECErrorToS3Error(err)
|
|
return nil, nil, nil, errCode
|
|
}
|
|
|
|
// Apply SSE-C encryption if customer key is provided
|
|
var sseIV []byte
|
|
if customerKey != nil {
|
|
encryptedReader, iv, encErr := CreateSSECEncryptedReader(dataReader, customerKey)
|
|
if encErr != nil {
|
|
return nil, nil, nil, s3err.ErrInternalError
|
|
}
|
|
dataReader = encryptedReader
|
|
sseIV = iv
|
|
}
|
|
|
|
return dataReader, customerKey, sseIV, s3err.ErrNone
|
|
}
|
|
|
|
// handleSSEKMSEncryption processes SSE-KMS encryption for the data reader
|
|
func (s3a *S3ApiServer) handleSSEKMSEncryption(r *http.Request, dataReader io.Reader, partOffset int64) (io.Reader, *SSEKMSKey, []byte, s3err.ErrorCode) {
|
|
// Handle SSE-KMS encryption if requested
|
|
if !IsSSEKMSRequest(r) {
|
|
return dataReader, nil, nil, s3err.ErrNone
|
|
}
|
|
|
|
glog.V(3).Infof("handleSSEKMSEncryption: SSE-KMS request detected, processing encryption")
|
|
|
|
// Parse SSE-KMS headers
|
|
keyID := r.Header.Get(s3_constants.AmzServerSideEncryptionAwsKmsKeyId)
|
|
bucketKeyEnabled := strings.ToLower(r.Header.Get(s3_constants.AmzServerSideEncryptionBucketKeyEnabled)) == "true"
|
|
|
|
// Build encryption context
|
|
bucket, object := s3_constants.GetBucketAndObject(r)
|
|
encryptionContext := BuildEncryptionContext(bucket, object, bucketKeyEnabled)
|
|
|
|
// Add any user-provided encryption context
|
|
if contextHeader := r.Header.Get(s3_constants.AmzServerSideEncryptionContext); contextHeader != "" {
|
|
userContext, err := parseEncryptionContext(contextHeader)
|
|
if err != nil {
|
|
return nil, nil, nil, s3err.ErrInvalidRequest
|
|
}
|
|
// Merge user context with default context
|
|
for k, v := range userContext {
|
|
encryptionContext[k] = v
|
|
}
|
|
}
|
|
|
|
// Check if a base IV is provided (for multipart uploads)
|
|
var encryptedReader io.Reader
|
|
var sseKey *SSEKMSKey
|
|
var encErr error
|
|
|
|
baseIVHeader := r.Header.Get(s3_constants.SeaweedFSSSEKMSBaseIVHeader)
|
|
if baseIVHeader != "" {
|
|
// Decode the base IV from the header
|
|
baseIV, decodeErr := base64.StdEncoding.DecodeString(baseIVHeader)
|
|
if decodeErr != nil {
|
|
glog.Errorf("handleSSEKMSEncryption: failed to decode base IV: %v", decodeErr)
|
|
return nil, nil, nil, s3err.ErrInternalError
|
|
}
|
|
if len(baseIV) != 16 {
|
|
glog.Errorf("handleSSEKMSEncryption: invalid base IV length: %d (expected 16)", len(baseIV))
|
|
return nil, nil, nil, s3err.ErrInternalError
|
|
}
|
|
// Use the provided base IV with unique part offset for multipart upload consistency
|
|
glog.V(4).Infof("handleSSEKMSEncryption: creating encrypted reader with baseIV=%x, partOffset=%d", baseIV[:8], partOffset)
|
|
encryptedReader, sseKey, encErr = CreateSSEKMSEncryptedReaderWithBaseIVAndOffset(dataReader, keyID, encryptionContext, bucketKeyEnabled, baseIV, partOffset)
|
|
} else {
|
|
// Generate a new IV for single-part uploads
|
|
glog.V(4).Infof("handleSSEKMSEncryption: creating encrypted reader for single-part (no base IV)")
|
|
encryptedReader, sseKey, encErr = CreateSSEKMSEncryptedReaderWithBucketKey(dataReader, keyID, encryptionContext, bucketKeyEnabled)
|
|
}
|
|
|
|
if encErr != nil {
|
|
glog.Errorf("handleSSEKMSEncryption: encryption failed: %v", encErr)
|
|
return nil, nil, nil, s3err.ErrInternalError
|
|
}
|
|
glog.V(3).Infof("handleSSEKMSEncryption: encryption successful, keyID=%s", keyID)
|
|
|
|
// Prepare SSE-KMS metadata for later header setting
|
|
sseKMSMetadata, metaErr := SerializeSSEKMSMetadata(sseKey)
|
|
if metaErr != nil {
|
|
return nil, nil, nil, s3err.ErrInternalError
|
|
}
|
|
|
|
return encryptedReader, sseKey, sseKMSMetadata, s3err.ErrNone
|
|
}
|
|
|
|
// handleSSES3MultipartEncryption handles multipart upload logic for SSE-S3 encryption
|
|
func (s3a *S3ApiServer) handleSSES3MultipartEncryption(r *http.Request, dataReader io.Reader, partOffset int64) (io.Reader, *SSES3Key, s3err.ErrorCode) {
|
|
keyDataHeader := r.Header.Get(s3_constants.SeaweedFSSSES3KeyDataHeader)
|
|
baseIVHeader := r.Header.Get(s3_constants.SeaweedFSSSES3BaseIVHeader)
|
|
|
|
glog.V(4).Infof("handleSSES3MultipartEncryption: using provided key and base IV for multipart part")
|
|
|
|
// Decode the key data
|
|
keyData, decodeErr := base64.StdEncoding.DecodeString(keyDataHeader)
|
|
if decodeErr != nil {
|
|
return nil, nil, s3err.ErrInternalError
|
|
}
|
|
|
|
// Deserialize the SSE-S3 key
|
|
keyManager := GetSSES3KeyManager()
|
|
key, deserializeErr := DeserializeSSES3Metadata(keyData, keyManager)
|
|
if deserializeErr != nil {
|
|
return nil, nil, s3err.ErrInternalError
|
|
}
|
|
|
|
// Decode the base IV
|
|
baseIV, decodeErr := base64.StdEncoding.DecodeString(baseIVHeader)
|
|
if decodeErr != nil || len(baseIV) != s3_constants.AESBlockSize {
|
|
return nil, nil, s3err.ErrInternalError
|
|
}
|
|
|
|
// Use the provided base IV with unique part offset for multipart upload consistency
|
|
// CRITICAL: Capture the derived IV returned by CreateSSES3EncryptedReaderWithBaseIV
|
|
// This function calculates adjustedIV = calculateIVWithOffset(baseIV, partOffset)
|
|
// We MUST store this derived IV in metadata, not the base IV, for decryption to work
|
|
encryptedReader, derivedIV, encErr := CreateSSES3EncryptedReaderWithBaseIV(dataReader, key, baseIV, partOffset)
|
|
if encErr != nil {
|
|
return nil, nil, s3err.ErrInternalError
|
|
}
|
|
|
|
// Update the key with the derived IV so it gets serialized into chunk metadata
|
|
// This ensures decryption uses the correct offset-adjusted IV
|
|
key.IV = derivedIV
|
|
|
|
glog.V(4).Infof("handleSSES3MultipartEncryption: using base IV %x, derived IV %x for offset %d",
|
|
baseIV[:8], derivedIV[:8], partOffset)
|
|
return encryptedReader, key, s3err.ErrNone
|
|
}
|
|
|
|
// handleSSES3SinglePartEncryption handles single-part upload logic for SSE-S3 encryption
|
|
func (s3a *S3ApiServer) handleSSES3SinglePartEncryption(dataReader io.Reader) (io.Reader, *SSES3Key, s3err.ErrorCode) {
|
|
glog.V(4).Infof("handleSSES3SinglePartEncryption: generating new key for single-part upload")
|
|
|
|
keyManager := GetSSES3KeyManager()
|
|
key, err := keyManager.GetOrCreateKey("")
|
|
if err != nil {
|
|
return nil, nil, s3err.ErrInternalError
|
|
}
|
|
|
|
// Create encrypted reader
|
|
encryptedReader, iv, encErr := CreateSSES3EncryptedReader(dataReader, key)
|
|
if encErr != nil {
|
|
return nil, nil, s3err.ErrInternalError
|
|
}
|
|
|
|
// Store IV on the key object for later decryption
|
|
key.IV = iv
|
|
|
|
// Store the key for later use
|
|
keyManager.StoreKey(key)
|
|
|
|
return encryptedReader, key, s3err.ErrNone
|
|
}
|
|
|
|
// handleSSES3Encryption processes SSE-S3 encryption for the data reader
|
|
func (s3a *S3ApiServer) handleSSES3Encryption(r *http.Request, dataReader io.Reader, partOffset int64) (io.Reader, *SSES3Key, []byte, s3err.ErrorCode) {
|
|
if !IsSSES3RequestInternal(r) {
|
|
return dataReader, nil, nil, s3err.ErrNone
|
|
}
|
|
|
|
glog.V(3).Infof("handleSSES3Encryption: SSE-S3 request detected, processing encryption")
|
|
|
|
var encryptedReader io.Reader
|
|
var sseS3Key *SSES3Key
|
|
var errCode s3err.ErrorCode
|
|
|
|
// Check if this is multipart upload (key data and base IV provided)
|
|
keyDataHeader := r.Header.Get(s3_constants.SeaweedFSSSES3KeyDataHeader)
|
|
baseIVHeader := r.Header.Get(s3_constants.SeaweedFSSSES3BaseIVHeader)
|
|
|
|
if keyDataHeader != "" && baseIVHeader != "" {
|
|
// Multipart upload: use provided key and base IV
|
|
encryptedReader, sseS3Key, errCode = s3a.handleSSES3MultipartEncryption(r, dataReader, partOffset)
|
|
} else {
|
|
// Single-part upload: generate new key and IV
|
|
encryptedReader, sseS3Key, errCode = s3a.handleSSES3SinglePartEncryption(dataReader)
|
|
}
|
|
|
|
if errCode != s3err.ErrNone {
|
|
return nil, nil, nil, errCode
|
|
}
|
|
|
|
// Prepare SSE-S3 metadata for later header setting
|
|
sseS3Metadata, metaErr := SerializeSSES3Metadata(sseS3Key)
|
|
if metaErr != nil {
|
|
return nil, nil, nil, s3err.ErrInternalError
|
|
}
|
|
|
|
glog.V(3).Infof("handleSSES3Encryption: prepared SSE-S3 metadata for object")
|
|
return encryptedReader, sseS3Key, sseS3Metadata, s3err.ErrNone
|
|
}
|
|
|
|
// storeSSEMetadata records the encryption result on the entry, in the same
|
|
// extended attributes the GET and HEAD handlers read back.
|
|
func storeSSEMetadata(entry *filer_pb.Entry, sseResult *PutToFilerEncryptionResult) {
|
|
if sseResult == nil {
|
|
return
|
|
}
|
|
if sseResult.CustomerKey != nil && len(sseResult.SSEIV) > 0 {
|
|
entry.Extended[s3_constants.SeaweedFSSSEIV] = sseResult.SSEIV
|
|
entry.Extended[s3_constants.AmzServerSideEncryptionCustomerAlgorithm] = []byte(s3_constants.SSEAlgorithmAES256)
|
|
entry.Extended[s3_constants.AmzServerSideEncryptionCustomerKeyMD5] = []byte(sseResult.CustomerKey.KeyMD5)
|
|
}
|
|
if sseResult.SSEKMSKey != nil {
|
|
entry.Extended[s3_constants.SeaweedFSSSEKMSKey] = sseResult.SSEKMSMetadata
|
|
entry.Extended[s3_constants.AmzServerSideEncryption] = []byte(s3_constants.SSEAlgorithmKMS)
|
|
entry.Extended[s3_constants.AmzServerSideEncryptionAwsKmsKeyId] = []byte(sseResult.SSEKMSKey.KeyID)
|
|
}
|
|
if sseResult.SSES3Key != nil && len(sseResult.SSES3Metadata) > 0 {
|
|
entry.Extended[s3_constants.SeaweedFSSSES3Key] = sseResult.SSES3Metadata
|
|
entry.Extended[s3_constants.AmzServerSideEncryption] = []byte(s3_constants.SSEAlgorithmAES256)
|
|
}
|
|
}
|
|
|
|
// handleAllSSEEncryption processes all SSE types in sequence and returns the final encrypted reader
|
|
// This eliminates repetitive dataReader assignments and centralizes SSE processing
|
|
func (s3a *S3ApiServer) handleAllSSEEncryption(r *http.Request, dataReader io.Reader, partOffset int64) (*PutToFilerEncryptionResult, s3err.ErrorCode) {
|
|
result := &PutToFilerEncryptionResult{
|
|
DataReader: dataReader,
|
|
}
|
|
|
|
// Handle SSE-C encryption first
|
|
encryptedReader, customerKey, sseIV, errCode := s3a.handleSSECEncryption(r, result.DataReader)
|
|
if errCode != s3err.ErrNone {
|
|
return nil, errCode
|
|
}
|
|
result.DataReader = encryptedReader
|
|
result.CustomerKey = customerKey
|
|
result.SSEIV = sseIV
|
|
|
|
// Handle SSE-KMS encryption
|
|
encryptedReader, sseKMSKey, sseKMSMetadata, errCode := s3a.handleSSEKMSEncryption(r, result.DataReader, partOffset)
|
|
if errCode != s3err.ErrNone {
|
|
return nil, errCode
|
|
}
|
|
result.DataReader = encryptedReader
|
|
result.SSEKMSKey = sseKMSKey
|
|
result.SSEKMSMetadata = sseKMSMetadata
|
|
|
|
// Handle SSE-S3 encryption
|
|
encryptedReader, sseS3Key, sseS3Metadata, errCode := s3a.handleSSES3Encryption(r, result.DataReader, partOffset)
|
|
if errCode != s3err.ErrNone {
|
|
return nil, errCode
|
|
}
|
|
result.DataReader = encryptedReader
|
|
result.SSES3Key = sseS3Key
|
|
result.SSES3Metadata = sseS3Metadata
|
|
|
|
// Set SSE type for response headers
|
|
if customerKey != nil {
|
|
result.SSEType = s3_constants.SSETypeC
|
|
} else if sseKMSKey != nil {
|
|
result.SSEType = s3_constants.SSETypeKMS
|
|
} else if sseS3Key != nil {
|
|
result.SSEType = s3_constants.SSETypeS3
|
|
}
|
|
|
|
return result, s3err.ErrNone
|
|
}
|