mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-16 02:58:35 +02:00
* s3api: resolve volume data encryption in CopyObject SSE flows (#11646) * s3api: honor bucket-default KMS key on copy and fix transformed-upload metadata - Synthesize the destination bucket's default encryption as request headers before any SSE evaluation, so the configured KMS key ID and bucket-key setting reach the copy paths instead of only a boolean. - uploadTransformedChunkData returns the upload result so callers record the uploader's cipher key AND compression decision; a wrongly cleared IsCompressed made transformed copies unreadable. - decompressChunkVolumeCipher fails loudly when a compressed chunk does not decompress, instead of uploading still-compressed bytes marked uncompressed. - copyMultipartSSECChunk now strips the volume cipher and re-encrypts on upload like the other transform paths. - The ciphered inner upload now uses the caller's private BytesBuffer. * s3api: extract copy bucket-default header synthesis for coverage Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3api: test bucket-default encryption header synthesis on copy Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3api: validate copy encryption headers before bucket defaults and resolve empty KMS key * s3api: name the AWS-managed SSE-KMS default key once --------- Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com> Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
8 files changed
+364
-6
No files matched your search
@@ -159,6 +159,30 @@ func (s3a *S3ApiServer) CopyObjectHandler(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the client's encryption headers as sent: synthesizing bucket
|
||||
// defaults first would launder a malformed request (a KMS option without
|
||||
// aws:kms, or a repeated header) into an accepted one.
|
||||
if errCode := ValidateRequestEncryption(r.Header); errCode != s3err.ErrNone {
|
||||
s3err.WriteErrorResponse(w, r, errCode)
|
||||
return
|
||||
}
|
||||
|
||||
// A copy request without SSE headers takes the destination bucket's
|
||||
// default encryption, including its configured KMS key and bucket-key
|
||||
// setting. Surface them as headers so every downstream parse sees them.
|
||||
if !IsSSECRequest(r) && r.Header.Get(s3_constants.AmzServerSideEncryption) == "" {
|
||||
encryptionConfig, encErr := s3a.GetBucketEncryptionConfig(dstBucket)
|
||||
switch {
|
||||
case encErr == nil:
|
||||
applyCopyBucketDefaultEncryptionHeaders(r, encryptionConfig)
|
||||
case errors.Is(encErr, ErrNoEncryptionConfig):
|
||||
default:
|
||||
glog.Errorf("CopyObjectHandler: read encryption config for bucket %s: %v", dstBucket, encErr)
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !isValidDirective(r.Header.Get(s3_constants.AmzUserMetaDirective)) {
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidMetadataDirective)
|
||||
return
|
||||
@@ -1783,6 +1807,47 @@ func (s3a *S3ApiServer) uploadChunkData(chunkData []byte, assignResult *filer_pb
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s3a *S3ApiServer) uploadTransformedChunkData(chunkData []byte, assignResult *filer_pb.AssignVolumeResponse) (*operation.UploadResult, error) {
|
||||
dstUrl := fmt.Sprintf("http://%s/%s", assignResult.Location.Url, assignResult.FileId)
|
||||
if assignResult.Fsync {
|
||||
dstUrl += "?fsync=true"
|
||||
}
|
||||
uploadOption := &operation.UploadOption{
|
||||
UploadUrl: dstUrl,
|
||||
Cipher: s3a.cipher,
|
||||
IsInputCompressed: false,
|
||||
MimeType: "",
|
||||
PairMap: nil,
|
||||
Jwt: security.EncodedJwt(assignResult.Auth),
|
||||
BytesBuffer: bytes.NewBuffer(make([]byte, 0, len(chunkData))),
|
||||
}
|
||||
uploader, err := operation.NewUploader()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create uploader: %w", err)
|
||||
}
|
||||
uploadResult, err := uploader.UploadData(context.Background(), chunkData, uploadOption)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("upload transformed chunk: %w", err)
|
||||
}
|
||||
return uploadResult, nil
|
||||
}
|
||||
|
||||
func (s3a *S3ApiServer) decryptChunkVolumeCipher(data []byte, chunk *filer_pb.FileChunk) ([]byte, error) {
|
||||
if len(chunk.CipherKey) == 0 {
|
||||
return data, nil
|
||||
}
|
||||
decrypted, err := util.Decrypt(data, util.CipherKey(chunk.CipherKey))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypt volume cipher: %w", err)
|
||||
}
|
||||
if chunk.IsCompressed {
|
||||
if decrypted, err = util.DecompressData(decrypted); err != nil {
|
||||
return nil, fmt.Errorf("decompress chunk: %w", err)
|
||||
}
|
||||
}
|
||||
return decrypted, nil
|
||||
}
|
||||
|
||||
// multipartFramingOverhead reserves space for the multipart wrapper
|
||||
// upload_content writes around chunkData (boundary + Content-Disposition +
|
||||
// optional Content-Type/Content-Encoding/Content-MD5 headers + trailing
|
||||
@@ -2012,6 +2077,10 @@ func (s3a *S3ApiServer) copyMultipartSSEKMSChunk(chunk *filer_pb.FileChunk, sour
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("download encrypted chunk data: %w", err)
|
||||
}
|
||||
encryptedData, err = s3a.decryptChunkVolumeCipher(encryptedData, chunk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var finalData []byte
|
||||
|
||||
@@ -2074,9 +2143,14 @@ func (s3a *S3ApiServer) copyMultipartSSEKMSChunk(chunk *filer_pb.FileChunk, sour
|
||||
}
|
||||
|
||||
// Upload the final data
|
||||
if err := s3a.uploadChunkData(finalData, assignResult, false); err != nil {
|
||||
uploadResult, err := s3a.uploadTransformedChunkData(finalData, assignResult)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("upload chunk data: %w", err)
|
||||
}
|
||||
if uploadResult != nil {
|
||||
dstChunk.CipherKey = uploadResult.CipherKey
|
||||
dstChunk.IsCompressed = uploadResult.Gzip > 0
|
||||
}
|
||||
|
||||
// Update chunk size
|
||||
dstChunk.Size = uint64(len(finalData))
|
||||
@@ -2109,6 +2183,10 @@ func (s3a *S3ApiServer) copyMultipartSSECChunk(chunk *filer_pb.FileChunk, copySo
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("download encrypted chunk data: %w", err)
|
||||
}
|
||||
encryptedData, err = s3a.decryptChunkVolumeCipher(encryptedData, chunk)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var finalData []byte
|
||||
var destIV []byte
|
||||
@@ -2204,9 +2282,14 @@ func (s3a *S3ApiServer) copyMultipartSSECChunk(chunk *filer_pb.FileChunk, copySo
|
||||
}
|
||||
|
||||
// Upload the final data
|
||||
if err := s3a.uploadChunkData(finalData, assignResult, false); err != nil {
|
||||
uploadResult, err := s3a.uploadTransformedChunkData(finalData, assignResult)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("upload chunk data: %w", err)
|
||||
}
|
||||
if uploadResult != nil {
|
||||
dstChunk.CipherKey = uploadResult.CipherKey
|
||||
dstChunk.IsCompressed = uploadResult.Gzip > 0
|
||||
}
|
||||
|
||||
// Update chunk size
|
||||
dstChunk.Size = uint64(len(finalData))
|
||||
@@ -2403,6 +2486,10 @@ func (s3a *S3ApiServer) copyCrossEncryptionChunk(chunk *filer_pb.FileChunk, sour
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("download encrypted chunk data: %w", err)
|
||||
}
|
||||
encryptedData, err = s3a.decryptChunkVolumeCipher(encryptedData, chunk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var finalData []byte
|
||||
|
||||
@@ -2593,9 +2680,14 @@ func (s3a *S3ApiServer) copyCrossEncryptionChunk(chunk *filer_pb.FileChunk, sour
|
||||
// For unencrypted destination, finalData remains as decrypted plaintext
|
||||
|
||||
// Upload the final data
|
||||
if err := s3a.uploadChunkData(finalData, assignResult, false); err != nil {
|
||||
uploadResult, err := s3a.uploadTransformedChunkData(finalData, assignResult)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("upload chunk data: %w", err)
|
||||
}
|
||||
if uploadResult != nil {
|
||||
dstChunk.CipherKey = uploadResult.CipherKey
|
||||
dstChunk.IsCompressed = uploadResult.Gzip > 0
|
||||
}
|
||||
|
||||
// Update chunk size
|
||||
dstChunk.Size = uint64(len(finalData))
|
||||
@@ -2748,6 +2840,10 @@ func (s3a *S3ApiServer) copyChunkWithReencryption(chunk *filer_pb.FileChunk, cop
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("download encrypted chunk data: %w", err)
|
||||
}
|
||||
encryptedData, err = s3a.decryptChunkVolumeCipher(encryptedData, chunk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var finalData []byte
|
||||
|
||||
@@ -2806,9 +2902,14 @@ func (s3a *S3ApiServer) copyChunkWithReencryption(chunk *filer_pb.FileChunk, cop
|
||||
}
|
||||
|
||||
// Upload the processed data
|
||||
if err := s3a.uploadChunkData(finalData, assignResult, false); err != nil {
|
||||
uploadResult, err := s3a.uploadTransformedChunkData(finalData, assignResult)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("upload processed chunk data: %w", err)
|
||||
}
|
||||
if uploadResult != nil {
|
||||
dstChunk.CipherKey = uploadResult.CipherKey
|
||||
dstChunk.IsCompressed = uploadResult.Gzip > 0
|
||||
}
|
||||
|
||||
return dstChunk, nil
|
||||
}
|
||||
@@ -3079,6 +3180,10 @@ func (s3a *S3ApiServer) copyChunkWithSSEKMSReencryption(chunk *filer_pb.FileChun
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("download chunk data: %w", err)
|
||||
}
|
||||
chunkData, err = s3a.decryptChunkVolumeCipher(chunkData, chunk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var finalData []byte
|
||||
|
||||
@@ -3162,9 +3267,14 @@ func (s3a *S3ApiServer) copyChunkWithSSEKMSReencryption(chunk *filer_pb.FileChun
|
||||
}
|
||||
|
||||
// Upload the processed data
|
||||
if err := s3a.uploadChunkData(finalData, assignResult, false); err != nil {
|
||||
uploadResult, err := s3a.uploadTransformedChunkData(finalData, assignResult)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("upload processed chunk data: %w", err)
|
||||
}
|
||||
if uploadResult != nil {
|
||||
dstChunk.CipherKey = uploadResult.CipherKey
|
||||
dstChunk.IsCompressed = uploadResult.Gzip > 0
|
||||
}
|
||||
|
||||
glog.V(3).Infof("Successfully processed SSE-KMS chunk re-encryption: src_key=%s, dst_key=%s, size=%d→%d",
|
||||
getKeyIDString(sourceSSEKey), destKeyID, len(chunkData), len(finalData))
|
||||
|
||||
Reference in new issue
Block a user