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
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)
|
|
}
|