You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

373 lines
14 KiB

package sse_test
import (
"bytes"
"context"
"crypto/md5"
"fmt"
"io"
"testing"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/stretchr/testify/require"
)
// TestSSEMultipartCopy tests copying multipart encrypted objects
func TestSSEMultipartCopy(t *testing.T) {
ctx := context.Background()
client, err := createS3Client(ctx, defaultConfig)
require.NoError(t, err, "Failed to create S3 client")
bucketName, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"sse-multipart-copy-")
require.NoError(t, err, "Failed to create test bucket")
defer cleanupTestBucket(ctx, client, bucketName)
// Generate test data for multipart upload (7.5MB)
originalData := generateTestData(7*1024*1024 + 512*1024)
originalMD5 := fmt.Sprintf("%x", md5.Sum(originalData))
t.Run("Copy SSE-C Multipart Object", func(t *testing.T) {
testSSECMultipartCopy(t, ctx, client, bucketName, originalData, originalMD5)
})
t.Run("Copy SSE-KMS Multipart Object", func(t *testing.T) {
testSSEKMSMultipartCopy(t, ctx, client, bucketName, originalData, originalMD5)
})
t.Run("Copy SSE-C to SSE-KMS", func(t *testing.T) {
testSSECToSSEKMSCopy(t, ctx, client, bucketName, originalData, originalMD5)
})
t.Run("Copy SSE-KMS to SSE-C", func(t *testing.T) {
testSSEKMSToSSECCopy(t, ctx, client, bucketName, originalData, originalMD5)
})
t.Run("Copy SSE-C to Unencrypted", func(t *testing.T) {
testSSECToUnencryptedCopy(t, ctx, client, bucketName, originalData, originalMD5)
})
t.Run("Copy SSE-KMS to Unencrypted", func(t *testing.T) {
testSSEKMSToUnencryptedCopy(t, ctx, client, bucketName, originalData, originalMD5)
})
}
// testSSECMultipartCopy tests copying SSE-C multipart objects with same key
func testSSECMultipartCopy(t *testing.T, ctx context.Context, client *s3.Client, bucketName string, originalData []byte, originalMD5 string) {
sseKey := generateSSECKey()
// Upload original multipart SSE-C object
sourceKey := "source-ssec-multipart-object"
err := uploadMultipartSSECObject(ctx, client, bucketName, sourceKey, originalData, *sseKey)
require.NoError(t, err, "Failed to upload source SSE-C multipart object")
// Copy with same SSE-C key
destKey := "dest-ssec-multipart-object"
_, err = client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(destKey),
CopySource: aws.String(fmt.Sprintf("%s/%s", bucketName, sourceKey)),
// Copy source SSE-C headers
CopySourceSSECustomerAlgorithm: aws.String("AES256"),
CopySourceSSECustomerKey: aws.String(sseKey.KeyB64),
CopySourceSSECustomerKeyMD5: aws.String(sseKey.KeyMD5),
// Destination SSE-C headers (same key)
SSECustomerAlgorithm: aws.String("AES256"),
SSECustomerKey: aws.String(sseKey.KeyB64),
SSECustomerKeyMD5: aws.String(sseKey.KeyMD5),
})
require.NoError(t, err, "Failed to copy SSE-C multipart object")
// Verify copied object
verifyEncryptedObject(t, ctx, client, bucketName, destKey, originalData, originalMD5, sseKey, nil)
}
// testSSEKMSMultipartCopy tests copying SSE-KMS multipart objects with same key
func testSSEKMSMultipartCopy(t *testing.T, ctx context.Context, client *s3.Client, bucketName string, originalData []byte, originalMD5 string) {
// Upload original multipart SSE-KMS object
sourceKey := "source-ssekms-multipart-object"
err := uploadMultipartSSEKMSObject(ctx, client, bucketName, sourceKey, "test-multipart-key", originalData)
require.NoError(t, err, "Failed to upload source SSE-KMS multipart object")
// Copy with same SSE-KMS key
destKey := "dest-ssekms-multipart-object"
_, err = client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(destKey),
CopySource: aws.String(fmt.Sprintf("%s/%s", bucketName, sourceKey)),
ServerSideEncryption: types.ServerSideEncryptionAwsKms,
SSEKMSKeyId: aws.String("test-multipart-key"),
BucketKeyEnabled: aws.Bool(false),
})
require.NoError(t, err, "Failed to copy SSE-KMS multipart object")
// Verify copied object
verifyEncryptedObject(t, ctx, client, bucketName, destKey, originalData, originalMD5, nil, aws.String("test-multipart-key"))
}
// testSSECToSSEKMSCopy tests copying SSE-C multipart objects to SSE-KMS
func testSSECToSSEKMSCopy(t *testing.T, ctx context.Context, client *s3.Client, bucketName string, originalData []byte, originalMD5 string) {
sseKey := generateSSECKey()
// Upload original multipart SSE-C object
sourceKey := "source-ssec-multipart-for-kms"
err := uploadMultipartSSECObject(ctx, client, bucketName, sourceKey, originalData, *sseKey)
require.NoError(t, err, "Failed to upload source SSE-C multipart object")
// Copy to SSE-KMS
destKey := "dest-ssekms-from-ssec"
_, err = client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(destKey),
CopySource: aws.String(fmt.Sprintf("%s/%s", bucketName, sourceKey)),
// Copy source SSE-C headers
CopySourceSSECustomerAlgorithm: aws.String("AES256"),
CopySourceSSECustomerKey: aws.String(sseKey.KeyB64),
CopySourceSSECustomerKeyMD5: aws.String(sseKey.KeyMD5),
// Destination SSE-KMS headers
ServerSideEncryption: types.ServerSideEncryptionAwsKms,
SSEKMSKeyId: aws.String("test-multipart-key"),
BucketKeyEnabled: aws.Bool(false),
})
require.NoError(t, err, "Failed to copy SSE-C to SSE-KMS")
// Verify copied object as SSE-KMS
verifyEncryptedObject(t, ctx, client, bucketName, destKey, originalData, originalMD5, nil, aws.String("test-multipart-key"))
}
// testSSEKMSToSSECCopy tests copying SSE-KMS multipart objects to SSE-C
func testSSEKMSToSSECCopy(t *testing.T, ctx context.Context, client *s3.Client, bucketName string, originalData []byte, originalMD5 string) {
sseKey := generateSSECKey()
// Upload original multipart SSE-KMS object
sourceKey := "source-ssekms-multipart-for-ssec"
err := uploadMultipartSSEKMSObject(ctx, client, bucketName, sourceKey, "test-multipart-key", originalData)
require.NoError(t, err, "Failed to upload source SSE-KMS multipart object")
// Copy to SSE-C
destKey := "dest-ssec-from-ssekms"
_, err = client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(destKey),
CopySource: aws.String(fmt.Sprintf("%s/%s", bucketName, sourceKey)),
// Destination SSE-C headers
SSECustomerAlgorithm: aws.String("AES256"),
SSECustomerKey: aws.String(sseKey.KeyB64),
SSECustomerKeyMD5: aws.String(sseKey.KeyMD5),
})
require.NoError(t, err, "Failed to copy SSE-KMS to SSE-C")
// Verify copied object as SSE-C
verifyEncryptedObject(t, ctx, client, bucketName, destKey, originalData, originalMD5, sseKey, nil)
}
// testSSECToUnencryptedCopy tests copying SSE-C multipart objects to unencrypted
func testSSECToUnencryptedCopy(t *testing.T, ctx context.Context, client *s3.Client, bucketName string, originalData []byte, originalMD5 string) {
sseKey := generateSSECKey()
// Upload original multipart SSE-C object
sourceKey := "source-ssec-multipart-for-plain"
err := uploadMultipartSSECObject(ctx, client, bucketName, sourceKey, originalData, *sseKey)
require.NoError(t, err, "Failed to upload source SSE-C multipart object")
// Copy to unencrypted
destKey := "dest-plain-from-ssec"
_, err = client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(destKey),
CopySource: aws.String(fmt.Sprintf("%s/%s", bucketName, sourceKey)),
// Copy source SSE-C headers
CopySourceSSECustomerAlgorithm: aws.String("AES256"),
CopySourceSSECustomerKey: aws.String(sseKey.KeyB64),
CopySourceSSECustomerKeyMD5: aws.String(sseKey.KeyMD5),
// No destination encryption headers
})
require.NoError(t, err, "Failed to copy SSE-C to unencrypted")
// Verify copied object as unencrypted
verifyEncryptedObject(t, ctx, client, bucketName, destKey, originalData, originalMD5, nil, nil)
}
// testSSEKMSToUnencryptedCopy tests copying SSE-KMS multipart objects to unencrypted
func testSSEKMSToUnencryptedCopy(t *testing.T, ctx context.Context, client *s3.Client, bucketName string, originalData []byte, originalMD5 string) {
// Upload original multipart SSE-KMS object
sourceKey := "source-ssekms-multipart-for-plain"
err := uploadMultipartSSEKMSObject(ctx, client, bucketName, sourceKey, "test-multipart-key", originalData)
require.NoError(t, err, "Failed to upload source SSE-KMS multipart object")
// Copy to unencrypted
destKey := "dest-plain-from-ssekms"
_, err = client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(destKey),
CopySource: aws.String(fmt.Sprintf("%s/%s", bucketName, sourceKey)),
// No destination encryption headers
})
require.NoError(t, err, "Failed to copy SSE-KMS to unencrypted")
// Verify copied object as unencrypted
verifyEncryptedObject(t, ctx, client, bucketName, destKey, originalData, originalMD5, nil, nil)
}
// uploadMultipartSSECObject uploads a multipart SSE-C object
func uploadMultipartSSECObject(ctx context.Context, client *s3.Client, bucketName, objectKey string, data []byte, sseKey SSECKey) error {
// Create multipart upload
createResp, err := client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
SSECustomerAlgorithm: aws.String("AES256"),
SSECustomerKey: aws.String(sseKey.KeyB64),
SSECustomerKeyMD5: aws.String(sseKey.KeyMD5),
})
if err != nil {
return err
}
uploadID := aws.ToString(createResp.UploadId)
// Upload parts
partSize := 5 * 1024 * 1024 // 5MB
var completedParts []types.CompletedPart
for i := 0; i < len(data); i += partSize {
end := i + partSize
if end > len(data) {
end = len(data)
}
partNumber := int32(len(completedParts) + 1)
partResp, err := client.UploadPart(ctx, &s3.UploadPartInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
PartNumber: aws.Int32(partNumber),
UploadId: aws.String(uploadID),
Body: bytes.NewReader(data[i:end]),
SSECustomerAlgorithm: aws.String("AES256"),
SSECustomerKey: aws.String(sseKey.KeyB64),
SSECustomerKeyMD5: aws.String(sseKey.KeyMD5),
})
if err != nil {
return err
}
completedParts = append(completedParts, types.CompletedPart{
ETag: partResp.ETag,
PartNumber: aws.Int32(partNumber),
})
}
// Complete multipart upload
_, err = client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
UploadId: aws.String(uploadID),
MultipartUpload: &types.CompletedMultipartUpload{
Parts: completedParts,
},
})
return err
}
// uploadMultipartSSEKMSObject uploads a multipart SSE-KMS object
func uploadMultipartSSEKMSObject(ctx context.Context, client *s3.Client, bucketName, objectKey, keyID string, data []byte) error {
// Create multipart upload
createResp, err := client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
ServerSideEncryption: types.ServerSideEncryptionAwsKms,
SSEKMSKeyId: aws.String(keyID),
BucketKeyEnabled: aws.Bool(false),
})
if err != nil {
return err
}
uploadID := aws.ToString(createResp.UploadId)
// Upload parts
partSize := 5 * 1024 * 1024 // 5MB
var completedParts []types.CompletedPart
for i := 0; i < len(data); i += partSize {
end := i + partSize
if end > len(data) {
end = len(data)
}
partNumber := int32(len(completedParts) + 1)
partResp, err := client.UploadPart(ctx, &s3.UploadPartInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
PartNumber: aws.Int32(partNumber),
UploadId: aws.String(uploadID),
Body: bytes.NewReader(data[i:end]),
})
if err != nil {
return err
}
completedParts = append(completedParts, types.CompletedPart{
ETag: partResp.ETag,
PartNumber: aws.Int32(partNumber),
})
}
// Complete multipart upload
_, err = client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
UploadId: aws.String(uploadID),
MultipartUpload: &types.CompletedMultipartUpload{
Parts: completedParts,
},
})
return err
}
// verifyEncryptedObject verifies that a copied object can be retrieved and matches the original data
func verifyEncryptedObject(t *testing.T, ctx context.Context, client *s3.Client, bucketName, objectKey string, expectedData []byte, expectedMD5 string, sseKey *SSECKey, kmsKeyID *string) {
var getInput *s3.GetObjectInput
if sseKey != nil {
// SSE-C object
getInput = &s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
SSECustomerAlgorithm: aws.String("AES256"),
SSECustomerKey: aws.String(sseKey.KeyB64),
SSECustomerKeyMD5: aws.String(sseKey.KeyMD5),
}
} else {
// SSE-KMS or unencrypted object
getInput = &s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
}
}
getResp, err := client.GetObject(ctx, getInput)
require.NoError(t, err, "Failed to retrieve copied object %s", objectKey)
defer getResp.Body.Close()
// Read and verify data
retrievedData, err := io.ReadAll(getResp.Body)
require.NoError(t, err, "Failed to read copied object data")
require.Equal(t, len(expectedData), len(retrievedData), "Data size mismatch for object %s", objectKey)
// Verify data using MD5
retrievedMD5 := fmt.Sprintf("%x", md5.Sum(retrievedData))
require.Equal(t, expectedMD5, retrievedMD5, "Data MD5 mismatch for object %s", objectKey)
// Verify encryption headers
if sseKey != nil {
require.Equal(t, "AES256", aws.ToString(getResp.SSECustomerAlgorithm), "SSE-C algorithm mismatch")
require.Equal(t, sseKey.KeyMD5, aws.ToString(getResp.SSECustomerKeyMD5), "SSE-C key MD5 mismatch")
} else if kmsKeyID != nil {
require.Equal(t, types.ServerSideEncryptionAwsKms, getResp.ServerSideEncryption, "SSE-KMS encryption mismatch")
require.Contains(t, aws.ToString(getResp.SSEKMSKeyId), *kmsKeyID, "SSE-KMS key ID mismatch")
}
t.Logf("✅ Successfully verified copied object %s: %d bytes, MD5=%s", objectKey, len(retrievedData), retrievedMD5)
}