s3api: resolve volume data encryption in CopyObject SSE flows (#11646) (#11683)

* 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:
authored and GitHub committed 2026-10-10 10:59:36 +08:00
1 parent ea65746947
commit d3dc03c85a
8 files changed
+364 -6

No files matched your search

@@ -268,3 +268,109 @@ func TestGetEncryptedStreamFromVolumesRangesInlineContent(t *testing.T) {
})
}
}
func TestDetermineUnifiedCopyStrategySSES3(t *testing.T) {
testCases := []struct {
name string
state *EncryptionState
expected UnifiedCopyStrategy
}{
{
name: "SSE-S3 to SSE-S3 direct copy",
state: &EncryptionState{
SrcSSES3: true,
DstSSES3: true,
},
expected: CopyStrategyDirect,
},
{
name: "SSE-S3 to plain decrypt copy",
state: &EncryptionState{
SrcSSES3: true,
DstSSES3: false,
},
expected: CopyStrategyDecrypt,
},
{
name: "Plain to SSE-S3 encrypt copy",
state: &EncryptionState{
SrcSSES3: false,
DstSSES3: true,
},
expected: CopyStrategyEncrypt,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
strategy, err := DetermineUnifiedCopyStrategy(tc.state, nil, nil)
if err != nil {
t.Fatalf("DetermineUnifiedCopyStrategy failed: %v", err)
}
if strategy != tc.expected {
t.Errorf("expected strategy %v, got %v", tc.expected, strategy)
}
})
}
}
func TestDecryptChunkVolumeCipher(t *testing.T) {
s3a := &S3ApiServer{}
plainData := []byte("hello-seaweedfs-encrypted-volume-data-verification-test")
t.Run("unencrypted chunk passes through", func(t *testing.T) {
chunk := &filer_pb.FileChunk{
CipherKey: nil,
}
got, err := s3a.decryptChunkVolumeCipher(plainData, chunk)
if err != nil {
t.Fatalf("decryptChunkVolumeCipher failed: %v", err)
}
if !bytes.Equal(got, plainData) {
t.Fatalf("expected %q, got %q", plainData, got)
}
})
t.Run("volume cipher encrypted chunk decrypts correctly", func(t *testing.T) {
cipherKey := util.GenCipherKey()
encrypted, err := util.Encrypt(plainData, cipherKey)
if err != nil {
t.Fatalf("Encrypt failed: %v", err)
}
chunk := &filer_pb.FileChunk{
CipherKey: cipherKey,
}
got, err := s3a.decryptChunkVolumeCipher(encrypted, chunk)
if err != nil {
t.Fatalf("decryptChunkVolumeCipher failed: %v", err)
}
if !bytes.Equal(got, plainData) {
t.Fatalf("expected %q, got %q", plainData, got)
}
})
t.Run("volume cipher encrypted and compressed chunk decrypts correctly", func(t *testing.T) {
cipherKey := util.GenCipherKey()
compressed, err := util.GzipData(plainData)
if err != nil {
t.Fatalf("GzipData failed: %v", err)
}
encrypted, err := util.Encrypt(compressed, cipherKey)
if err != nil {
t.Fatalf("Encrypt failed: %v", err)
}
chunk := &filer_pb.FileChunk{
CipherKey: cipherKey,
IsCompressed: true,
}
got, err := s3a.decryptChunkVolumeCipher(encrypted, chunk)
if err != nil {
t.Fatalf("decryptChunkVolumeCipher failed: %v", err)
}
if !bytes.Equal(got, plainData) {
t.Fatalf("expected %q, got %q", plainData, got)
}
})
}