|
|
@ -199,7 +199,9 @@ func (s3a *S3ApiServer) CopyObjectHandler(w http.ResponseWriter, r *http.Request |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// Process metadata and tags and apply to destination
|
|
|
// Process metadata and tags and apply to destination
|
|
|
processedMetadata, tagErr := processMetadataBytes(r.Header, entry.Extended, replaceMeta, replaceTagging) |
|
|
|
|
|
|
|
|
// Use dstEntry.Extended (already filtered) as the source, not entry.Extended,
|
|
|
|
|
|
// to preserve the encryption header filtering. Fixes GitHub #7562.
|
|
|
|
|
|
processedMetadata, tagErr := processMetadataBytes(r.Header, dstEntry.Extended, replaceMeta, replaceTagging) |
|
|
if tagErr != nil { |
|
|
if tagErr != nil { |
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidCopySource) |
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidCopySource) |
|
|
return |
|
|
return |
|
|
@ -1522,7 +1524,7 @@ func (s3a *S3ApiServer) copyMultipartSSECChunk(chunk *filer_pb.FileChunk, copySo |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// copyMultipartCrossEncryption handles all cross-encryption and decrypt-only copy scenarios
|
|
|
// copyMultipartCrossEncryption handles all cross-encryption and decrypt-only copy scenarios
|
|
|
// This unified function supports: SSE-C↔SSE-KMS, SSE-C→Plain, SSE-KMS→Plain
|
|
|
|
|
|
|
|
|
// This unified function supports: SSE-C↔SSE-KMS↔SSE-S3, and any→Plain
|
|
|
func (s3a *S3ApiServer) copyMultipartCrossEncryption(entry *filer_pb.Entry, r *http.Request, state *EncryptionState, dstBucket, dstPath string) ([]*filer_pb.FileChunk, map[string][]byte, error) { |
|
|
func (s3a *S3ApiServer) copyMultipartCrossEncryption(entry *filer_pb.Entry, r *http.Request, state *EncryptionState, dstBucket, dstPath string) ([]*filer_pb.FileChunk, map[string][]byte, error) { |
|
|
var dstChunks []*filer_pb.FileChunk |
|
|
var dstChunks []*filer_pb.FileChunk |
|
|
|
|
|
|
|
|
@ -1531,6 +1533,7 @@ func (s3a *S3ApiServer) copyMultipartCrossEncryption(entry *filer_pb.Entry, r *h |
|
|
var destKMSKeyID string |
|
|
var destKMSKeyID string |
|
|
var destKMSEncryptionContext map[string]string |
|
|
var destKMSEncryptionContext map[string]string |
|
|
var destKMSBucketKeyEnabled bool |
|
|
var destKMSBucketKeyEnabled bool |
|
|
|
|
|
var destSSES3Key *SSES3Key |
|
|
|
|
|
|
|
|
if state.DstSSEC { |
|
|
if state.DstSSEC { |
|
|
var err error |
|
|
var err error |
|
|
@ -1544,7 +1547,13 @@ func (s3a *S3ApiServer) copyMultipartCrossEncryption(entry *filer_pb.Entry, r *h |
|
|
if err != nil { |
|
|
if err != nil { |
|
|
return nil, nil, fmt.Errorf("failed to parse destination SSE-KMS headers: %w", err) |
|
|
return nil, nil, fmt.Errorf("failed to parse destination SSE-KMS headers: %w", err) |
|
|
} |
|
|
} |
|
|
} else { |
|
|
|
|
|
|
|
|
} else if state.DstSSES3 { |
|
|
|
|
|
// Generate SSE-S3 key for destination
|
|
|
|
|
|
var err error |
|
|
|
|
|
destSSES3Key, err = GenerateSSES3Key() |
|
|
|
|
|
if err != nil { |
|
|
|
|
|
return nil, nil, fmt.Errorf("failed to generate SSE-S3 key: %w", err) |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// Parse source encryption parameters
|
|
|
// Parse source encryption parameters
|
|
|
@ -1563,13 +1572,19 @@ func (s3a *S3ApiServer) copyMultipartCrossEncryption(entry *filer_pb.Entry, r *h |
|
|
var err error |
|
|
var err error |
|
|
|
|
|
|
|
|
if chunk.GetSseType() == filer_pb.SSEType_SSE_C { |
|
|
if chunk.GetSseType() == filer_pb.SSEType_SSE_C { |
|
|
copiedChunk, err = s3a.copyCrossEncryptionChunk(chunk, sourceSSECKey, destSSECKey, destKMSKeyID, destKMSEncryptionContext, destKMSBucketKeyEnabled, dstPath, dstBucket, state) |
|
|
|
|
|
|
|
|
copiedChunk, err = s3a.copyCrossEncryptionChunk(chunk, sourceSSECKey, destSSECKey, destKMSKeyID, destKMSEncryptionContext, destKMSBucketKeyEnabled, destSSES3Key, dstPath, dstBucket, state) |
|
|
} else if chunk.GetSseType() == filer_pb.SSEType_SSE_KMS { |
|
|
} else if chunk.GetSseType() == filer_pb.SSEType_SSE_KMS { |
|
|
copiedChunk, err = s3a.copyCrossEncryptionChunk(chunk, nil, destSSECKey, destKMSKeyID, destKMSEncryptionContext, destKMSBucketKeyEnabled, dstPath, dstBucket, state) |
|
|
|
|
|
|
|
|
copiedChunk, err = s3a.copyCrossEncryptionChunk(chunk, nil, destSSECKey, destKMSKeyID, destKMSEncryptionContext, destKMSBucketKeyEnabled, destSSES3Key, dstPath, dstBucket, state) |
|
|
|
|
|
} else if chunk.GetSseType() == filer_pb.SSEType_SSE_S3 { |
|
|
|
|
|
copiedChunk, err = s3a.copyCrossEncryptionChunk(chunk, nil, destSSECKey, destKMSKeyID, destKMSEncryptionContext, destKMSBucketKeyEnabled, destSSES3Key, dstPath, dstBucket, state) |
|
|
|
|
|
} else { |
|
|
|
|
|
// Unencrypted chunk - may need encryption if destination requires it
|
|
|
|
|
|
if state.DstSSEC || state.DstSSEKMS || state.DstSSES3 { |
|
|
|
|
|
copiedChunk, err = s3a.copyCrossEncryptionChunk(chunk, nil, destSSECKey, destKMSKeyID, destKMSEncryptionContext, destKMSBucketKeyEnabled, destSSES3Key, dstPath, dstBucket, state) |
|
|
} else { |
|
|
} else { |
|
|
// Unencrypted chunk, copy directly
|
|
|
|
|
|
copiedChunk, err = s3a.copySingleChunk(chunk, dstPath) |
|
|
copiedChunk, err = s3a.copySingleChunk(chunk, dstPath) |
|
|
} |
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
if err != nil { |
|
|
if err != nil { |
|
|
return nil, nil, fmt.Errorf("failed to copy chunk %s: %w", chunk.GetFileIdString(), err) |
|
|
return nil, nil, fmt.Errorf("failed to copy chunk %s: %w", chunk.GetFileIdString(), err) |
|
|
@ -1619,6 +1634,40 @@ func (s3a *S3ApiServer) copyMultipartCrossEncryption(entry *filer_pb.Entry, r *h |
|
|
} else { |
|
|
} else { |
|
|
glog.Errorf("Failed to serialize SSE-KMS metadata: %v", serErr) |
|
|
glog.Errorf("Failed to serialize SSE-KMS metadata: %v", serErr) |
|
|
} |
|
|
} |
|
|
|
|
|
} else if state.DstSSES3 && destSSES3Key != nil { |
|
|
|
|
|
// For SSE-S3 destination, create object-level metadata
|
|
|
|
|
|
var sses3Metadata *SSES3Key |
|
|
|
|
|
if len(dstChunks) == 0 { |
|
|
|
|
|
// Handle 0-byte files - generate IV for metadata even though there's no content to encrypt
|
|
|
|
|
|
if entry.Attributes.FileSize != 0 { |
|
|
|
|
|
return nil, nil, fmt.Errorf("internal error: no chunks created for non-empty SSE-S3 destination object") |
|
|
|
|
|
} |
|
|
|
|
|
// Generate IV for 0-byte object metadata
|
|
|
|
|
|
iv := make([]byte, s3_constants.AESBlockSize) |
|
|
|
|
|
if _, err := io.ReadFull(rand.Reader, iv); err != nil { |
|
|
|
|
|
return nil, nil, fmt.Errorf("generate IV for 0-byte object: %w", err) |
|
|
|
|
|
} |
|
|
|
|
|
destSSES3Key.IV = iv |
|
|
|
|
|
sses3Metadata = destSSES3Key |
|
|
|
|
|
} else { |
|
|
|
|
|
// For non-empty objects, use the first chunk's metadata
|
|
|
|
|
|
if dstChunks[0].GetSseType() != filer_pb.SSEType_SSE_S3 || len(dstChunks[0].GetSseMetadata()) == 0 { |
|
|
|
|
|
return nil, nil, fmt.Errorf("internal error: first chunk is missing expected SSE-S3 metadata for destination object") |
|
|
|
|
|
} |
|
|
|
|
|
keyManager := GetSSES3KeyManager() |
|
|
|
|
|
var err error |
|
|
|
|
|
sses3Metadata, err = DeserializeSSES3Metadata(dstChunks[0].GetSseMetadata(), keyManager) |
|
|
|
|
|
if err != nil { |
|
|
|
|
|
return nil, nil, fmt.Errorf("failed to deserialize SSE-S3 metadata from first chunk: %w", err) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
// Use the derived key with its IV for object-level metadata
|
|
|
|
|
|
keyData, serErr := SerializeSSES3Metadata(sses3Metadata) |
|
|
|
|
|
if serErr != nil { |
|
|
|
|
|
return nil, nil, fmt.Errorf("failed to serialize SSE-S3 metadata: %w", serErr) |
|
|
|
|
|
} |
|
|
|
|
|
dstMetadata[s3_constants.SeaweedFSSSES3Key] = keyData |
|
|
|
|
|
dstMetadata[s3_constants.AmzServerSideEncryption] = []byte("AES256") |
|
|
} |
|
|
} |
|
|
// For unencrypted destination, no metadata needed (dstMetadata remains empty)
|
|
|
// For unencrypted destination, no metadata needed (dstMetadata remains empty)
|
|
|
|
|
|
|
|
|
@ -1626,7 +1675,7 @@ func (s3a *S3ApiServer) copyMultipartCrossEncryption(entry *filer_pb.Entry, r *h |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// copyCrossEncryptionChunk handles copying a single chunk with cross-encryption support
|
|
|
// copyCrossEncryptionChunk handles copying a single chunk with cross-encryption support
|
|
|
func (s3a *S3ApiServer) copyCrossEncryptionChunk(chunk *filer_pb.FileChunk, sourceSSECKey *SSECustomerKey, destSSECKey *SSECustomerKey, destKMSKeyID string, destKMSEncryptionContext map[string]string, destKMSBucketKeyEnabled bool, dstPath, dstBucket string, state *EncryptionState) (*filer_pb.FileChunk, error) { |
|
|
|
|
|
|
|
|
func (s3a *S3ApiServer) copyCrossEncryptionChunk(chunk *filer_pb.FileChunk, sourceSSECKey *SSECustomerKey, destSSECKey *SSECustomerKey, destKMSKeyID string, destKMSEncryptionContext map[string]string, destKMSBucketKeyEnabled bool, destSSES3Key *SSES3Key, dstPath, dstBucket string, state *EncryptionState) (*filer_pb.FileChunk, error) { |
|
|
// Create destination chunk
|
|
|
// Create destination chunk
|
|
|
dstChunk := s3a.createDestinationChunk(chunk, chunk.Offset, chunk.Size) |
|
|
dstChunk := s3a.createDestinationChunk(chunk, chunk.Offset, chunk.Size) |
|
|
|
|
|
|
|
|
@ -1726,6 +1775,30 @@ func (s3a *S3ApiServer) copyCrossEncryptionChunk(chunk *filer_pb.FileChunk, sour |
|
|
previewLen = len(finalData) |
|
|
previewLen = len(finalData) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
} else if chunk.GetSseType() == filer_pb.SSEType_SSE_S3 { |
|
|
|
|
|
// Decrypt SSE-S3 source
|
|
|
|
|
|
if len(chunk.GetSseMetadata()) == 0 { |
|
|
|
|
|
return nil, fmt.Errorf("SSE-S3 chunk missing per-chunk metadata") |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
keyManager := GetSSES3KeyManager() |
|
|
|
|
|
sourceSSEKey, err := DeserializeSSES3Metadata(chunk.GetSseMetadata(), keyManager) |
|
|
|
|
|
if err != nil { |
|
|
|
|
|
return nil, fmt.Errorf("failed to deserialize SSE-S3 metadata: %w", err) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
decryptedReader, decErr := CreateSSES3DecryptedReader(bytes.NewReader(encryptedData), sourceSSEKey, sourceSSEKey.IV) |
|
|
|
|
|
if decErr != nil { |
|
|
|
|
|
return nil, fmt.Errorf("create SSE-S3 decrypted reader: %w", decErr) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
decryptedData, readErr := io.ReadAll(decryptedReader) |
|
|
|
|
|
if readErr != nil { |
|
|
|
|
|
return nil, fmt.Errorf("decrypt SSE-S3 chunk data: %w", readErr) |
|
|
|
|
|
} |
|
|
|
|
|
finalData = decryptedData |
|
|
|
|
|
glog.V(4).Infof("Decrypted SSE-S3 chunk, size: %d", len(finalData)) |
|
|
|
|
|
|
|
|
} else { |
|
|
} else { |
|
|
// Source is unencrypted
|
|
|
// Source is unencrypted
|
|
|
finalData = encryptedData |
|
|
finalData = encryptedData |
|
|
@ -1787,6 +1860,36 @@ func (s3a *S3ApiServer) copyCrossEncryptionChunk(chunk *filer_pb.FileChunk, sour |
|
|
dstChunk.SseMetadata = kmsMetadata |
|
|
dstChunk.SseMetadata = kmsMetadata |
|
|
|
|
|
|
|
|
glog.V(4).Infof("Re-encrypted chunk with SSE-KMS") |
|
|
glog.V(4).Infof("Re-encrypted chunk with SSE-KMS") |
|
|
|
|
|
|
|
|
|
|
|
} else if state.DstSSES3 && destSSES3Key != nil { |
|
|
|
|
|
// Encrypt with SSE-S3
|
|
|
|
|
|
encryptedReader, iv, encErr := CreateSSES3EncryptedReader(bytes.NewReader(finalData), destSSES3Key) |
|
|
|
|
|
if encErr != nil { |
|
|
|
|
|
return nil, fmt.Errorf("create SSE-S3 encrypted reader: %w", encErr) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
reencryptedData, readErr := io.ReadAll(encryptedReader) |
|
|
|
|
|
if readErr != nil { |
|
|
|
|
|
return nil, fmt.Errorf("re-encrypt with SSE-S3: %w", readErr) |
|
|
|
|
|
} |
|
|
|
|
|
finalData = reencryptedData |
|
|
|
|
|
|
|
|
|
|
|
// Create per-chunk SSE-S3 metadata with chunk-specific IV
|
|
|
|
|
|
chunkSSEKey := &SSES3Key{ |
|
|
|
|
|
Key: destSSES3Key.Key, |
|
|
|
|
|
KeyID: destSSES3Key.KeyID, |
|
|
|
|
|
Algorithm: destSSES3Key.Algorithm, |
|
|
|
|
|
IV: iv, |
|
|
|
|
|
} |
|
|
|
|
|
sses3Metadata, err := SerializeSSES3Metadata(chunkSSEKey) |
|
|
|
|
|
if err != nil { |
|
|
|
|
|
return nil, fmt.Errorf("serialize SSE-S3 metadata: %w", err) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
dstChunk.SseType = filer_pb.SSEType_SSE_S3 |
|
|
|
|
|
dstChunk.SseMetadata = sses3Metadata |
|
|
|
|
|
|
|
|
|
|
|
glog.V(4).Infof("Re-encrypted chunk with SSE-S3") |
|
|
} |
|
|
} |
|
|
// For unencrypted destination, finalData remains as decrypted plaintext
|
|
|
// For unencrypted destination, finalData remains as decrypted plaintext
|
|
|
|
|
|
|
|
|
|