package sse_test import ( "bytes" "context" "crypto/md5" "crypto/rand" "encoding/base64" "fmt" "io" "strings" "testing" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // assertDataEqual compares two byte slices using MD5 hashes and provides a concise error message func assertDataEqual(t *testing.T, expected, actual []byte, msgAndArgs ...interface{}) { if len(expected) == len(actual) && bytes.Equal(expected, actual) { return // Data matches, no need to fail } expectedMD5 := md5.Sum(expected) actualMD5 := md5.Sum(actual) // Create preview of first 1K bytes for debugging previewSize := 1024 if len(expected) < previewSize { previewSize = len(expected) } expectedPreview := expected[:previewSize] actualPreviewSize := previewSize if len(actual) < actualPreviewSize { actualPreviewSize = len(actual) } actualPreview := actual[:actualPreviewSize] // Format the assertion failure message msg := fmt.Sprintf("Data mismatch:\nExpected length: %d, MD5: %x\nActual length: %d, MD5: %x\nExpected preview (first %d bytes): %x\nActual preview (first %d bytes): %x", len(expected), expectedMD5, len(actual), actualMD5, len(expectedPreview), expectedPreview, len(actualPreview), actualPreview) if len(msgAndArgs) > 0 { if format, ok := msgAndArgs[0].(string); ok { msg = fmt.Sprintf(format, msgAndArgs[1:]...) + "\n" + msg } } t.Error(msg) } // min returns the minimum of two integers func min(a, b int) int { if a < b { return a } return b } // S3SSETestConfig holds configuration for S3 SSE integration tests type S3SSETestConfig struct { Endpoint string AccessKey string SecretKey string Region string BucketPrefix string UseSSL bool SkipVerifySSL bool } // Default test configuration var defaultConfig = &S3SSETestConfig{ Endpoint: "http://127.0.0.1:8333", AccessKey: "some_access_key1", SecretKey: "some_secret_key1", Region: "us-east-1", BucketPrefix: "test-sse-", UseSSL: false, SkipVerifySSL: true, } // Test data sizes for comprehensive coverage var testDataSizes = []int{ 0, // Empty file 1, // Single byte 16, // One AES block 31, // Just under two blocks 32, // Exactly two blocks 100, // Small file 1024, // 1KB 8192, // 8KB 64 * 1024, // 64KB 1024 * 1024, // 1MB } // SSECKey represents an SSE-C encryption key for testing type SSECKey struct { Key []byte KeyB64 string KeyMD5 string } // generateSSECKey generates a random SSE-C key for testing func generateSSECKey() *SSECKey { key := make([]byte, 32) // 256-bit key rand.Read(key) keyB64 := base64.StdEncoding.EncodeToString(key) keyMD5Hash := md5.Sum(key) keyMD5 := base64.StdEncoding.EncodeToString(keyMD5Hash[:]) return &SSECKey{ Key: key, KeyB64: keyB64, KeyMD5: keyMD5, } } // createS3Client creates an S3 client for testing func createS3Client(ctx context.Context, cfg *S3SSETestConfig) (*s3.Client, error) { customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) { return aws.Endpoint{ URL: cfg.Endpoint, HostnameImmutable: true, }, nil }) awsCfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(cfg.Region), config.WithEndpointResolverWithOptions(customResolver), config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider( cfg.AccessKey, cfg.SecretKey, "", )), ) if err != nil { return nil, err } return s3.NewFromConfig(awsCfg, func(o *s3.Options) { o.UsePathStyle = true }), nil } // generateTestData generates random test data of specified size func generateTestData(size int) []byte { data := make([]byte, size) rand.Read(data) return data } // createTestBucket creates a test bucket with a unique name func createTestBucket(ctx context.Context, client *s3.Client, prefix string) (string, error) { bucketName := fmt.Sprintf("%s%d", prefix, time.Now().UnixNano()) _, err := client.CreateBucket(ctx, &s3.CreateBucketInput{ Bucket: aws.String(bucketName), }) return bucketName, err } // cleanupTestBucket removes a test bucket and all its objects func cleanupTestBucket(ctx context.Context, client *s3.Client, bucketName string) error { // List and delete all objects first listResp, err := client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ Bucket: aws.String(bucketName), }) if err != nil { return err } if len(listResp.Contents) > 0 { var objectIds []types.ObjectIdentifier for _, obj := range listResp.Contents { objectIds = append(objectIds, types.ObjectIdentifier{ Key: obj.Key, }) } _, err = client.DeleteObjects(ctx, &s3.DeleteObjectsInput{ Bucket: aws.String(bucketName), Delete: &types.Delete{ Objects: objectIds, }, }) if err != nil { return err } } // Delete the bucket _, err = client.DeleteBucket(ctx, &s3.DeleteBucketInput{ Bucket: aws.String(bucketName), }) return err } // TestSSECIntegrationBasic tests basic SSE-C functionality end-to-end func TestSSECIntegrationBasic(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+"ssec-basic-") require.NoError(t, err, "Failed to create test bucket") defer cleanupTestBucket(ctx, client, bucketName) // Generate test key sseKey := generateSSECKey() testData := []byte("Hello, SSE-C integration test!") objectKey := "test-object-ssec" t.Run("PUT with SSE-C", func(t *testing.T) { // Upload object with SSE-C _, err := client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), Body: bytes.NewReader(testData), SSECustomerAlgorithm: aws.String("AES256"), SSECustomerKey: aws.String(sseKey.KeyB64), SSECustomerKeyMD5: aws.String(sseKey.KeyMD5), }) require.NoError(t, err, "Failed to upload SSE-C object") }) t.Run("GET with correct SSE-C key", func(t *testing.T) { // Retrieve object with correct key resp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), SSECustomerAlgorithm: aws.String("AES256"), SSECustomerKey: aws.String(sseKey.KeyB64), SSECustomerKeyMD5: aws.String(sseKey.KeyMD5), }) require.NoError(t, err, "Failed to retrieve SSE-C object") defer resp.Body.Close() // Verify decrypted content matches original retrievedData, err := io.ReadAll(resp.Body) require.NoError(t, err, "Failed to read retrieved data") assertDataEqual(t, testData, retrievedData, "Decrypted data does not match original") // Verify SSE headers are present assert.Equal(t, "AES256", aws.ToString(resp.SSECustomerAlgorithm)) assert.Equal(t, sseKey.KeyMD5, aws.ToString(resp.SSECustomerKeyMD5)) }) t.Run("GET without SSE-C key should fail", func(t *testing.T) { // Try to retrieve object without encryption key - should fail _, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), }) assert.Error(t, err, "Should fail to retrieve SSE-C object without key") }) t.Run("GET with wrong SSE-C key should fail", func(t *testing.T) { wrongKey := generateSSECKey() // Try to retrieve object with wrong key - should fail _, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), SSECustomerAlgorithm: aws.String("AES256"), SSECustomerKey: aws.String(wrongKey.KeyB64), SSECustomerKeyMD5: aws.String(wrongKey.KeyMD5), }) assert.Error(t, err, "Should fail to retrieve SSE-C object with wrong key") }) } // TestSSECIntegrationVariousDataSizes tests SSE-C with various data sizes func TestSSECIntegrationVariousDataSizes(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+"ssec-sizes-") require.NoError(t, err, "Failed to create test bucket") defer cleanupTestBucket(ctx, client, bucketName) sseKey := generateSSECKey() for _, size := range testDataSizes { t.Run(fmt.Sprintf("Size_%d_bytes", size), func(t *testing.T) { testData := generateTestData(size) objectKey := fmt.Sprintf("test-object-size-%d", size) // Upload with SSE-C _, err := client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), Body: bytes.NewReader(testData), SSECustomerAlgorithm: aws.String("AES256"), SSECustomerKey: aws.String(sseKey.KeyB64), SSECustomerKeyMD5: aws.String(sseKey.KeyMD5), }) require.NoError(t, err, "Failed to upload object of size %d", size) // Retrieve with SSE-C resp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), SSECustomerAlgorithm: aws.String("AES256"), SSECustomerKey: aws.String(sseKey.KeyB64), SSECustomerKeyMD5: aws.String(sseKey.KeyMD5), }) require.NoError(t, err, "Failed to retrieve object of size %d", size) defer resp.Body.Close() // Verify content matches retrievedData, err := io.ReadAll(resp.Body) require.NoError(t, err, "Failed to read retrieved data of size %d", size) assertDataEqual(t, testData, retrievedData, "Data mismatch for size %d", size) // Verify content length is correct (this would have caught the IV-in-stream bug!) assert.Equal(t, int64(size), aws.ToInt64(resp.ContentLength), "Content length mismatch for size %d", size) }) } } // TestSSEKMSIntegrationBasic tests basic SSE-KMS functionality end-to-end func TestSSEKMSIntegrationBasic(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+"ssekms-basic-") require.NoError(t, err, "Failed to create test bucket") defer cleanupTestBucket(ctx, client, bucketName) testData := []byte("Hello, SSE-KMS integration test!") objectKey := "test-object-ssekms" kmsKeyID := "test-key-123" // Test key ID t.Run("PUT with SSE-KMS", func(t *testing.T) { // Upload object with SSE-KMS _, err := client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), Body: bytes.NewReader(testData), ServerSideEncryption: types.ServerSideEncryptionAwsKms, SSEKMSKeyId: aws.String(kmsKeyID), }) require.NoError(t, err, "Failed to upload SSE-KMS object") }) t.Run("GET SSE-KMS object", func(t *testing.T) { // Retrieve object - no additional headers needed for GET resp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), }) require.NoError(t, err, "Failed to retrieve SSE-KMS object") defer resp.Body.Close() // Verify decrypted content matches original retrievedData, err := io.ReadAll(resp.Body) require.NoError(t, err, "Failed to read retrieved data") assertDataEqual(t, testData, retrievedData, "Decrypted data does not match original") // Verify SSE-KMS headers are present assert.Equal(t, types.ServerSideEncryptionAwsKms, resp.ServerSideEncryption) assert.Equal(t, kmsKeyID, aws.ToString(resp.SSEKMSKeyId)) }) t.Run("HEAD SSE-KMS object", func(t *testing.T) { // Test HEAD operation to verify metadata resp, err := client.HeadObject(ctx, &s3.HeadObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), }) require.NoError(t, err, "Failed to HEAD SSE-KMS object") // Verify SSE-KMS metadata assert.Equal(t, types.ServerSideEncryptionAwsKms, resp.ServerSideEncryption) assert.Equal(t, kmsKeyID, aws.ToString(resp.SSEKMSKeyId)) assert.Equal(t, int64(len(testData)), aws.ToInt64(resp.ContentLength)) }) } // TestSSEKMSIntegrationVariousDataSizes tests SSE-KMS with various data sizes func TestSSEKMSIntegrationVariousDataSizes(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+"ssekms-sizes-") require.NoError(t, err, "Failed to create test bucket") defer cleanupTestBucket(ctx, client, bucketName) kmsKeyID := "test-key-size-tests" for _, size := range testDataSizes { t.Run(fmt.Sprintf("Size_%d_bytes", size), func(t *testing.T) { testData := generateTestData(size) objectKey := fmt.Sprintf("test-object-kms-size-%d", size) // Upload with SSE-KMS _, err := client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), Body: bytes.NewReader(testData), ServerSideEncryption: types.ServerSideEncryptionAwsKms, SSEKMSKeyId: aws.String(kmsKeyID), }) require.NoError(t, err, "Failed to upload KMS object of size %d", size) // Retrieve with SSE-KMS resp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), }) require.NoError(t, err, "Failed to retrieve KMS object of size %d", size) defer resp.Body.Close() // Verify content matches retrievedData, err := io.ReadAll(resp.Body) require.NoError(t, err, "Failed to read retrieved KMS data of size %d", size) assertDataEqual(t, testData, retrievedData, "Data mismatch for KMS size %d", size) // Verify content length is correct assert.Equal(t, int64(size), aws.ToInt64(resp.ContentLength), "Content length mismatch for KMS size %d", size) }) } } // TestSSECObjectCopyIntegration tests SSE-C object copying end-to-end func TestSSECObjectCopyIntegration(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+"ssec-copy-") require.NoError(t, err, "Failed to create test bucket") defer cleanupTestBucket(ctx, client, bucketName) // Generate test keys sourceKey := generateSSECKey() destKey := generateSSECKey() testData := []byte("Hello, SSE-C copy integration test!") // Upload source object sourceObjectKey := "source-object" _, err = client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(sourceObjectKey), Body: bytes.NewReader(testData), SSECustomerAlgorithm: aws.String("AES256"), SSECustomerKey: aws.String(sourceKey.KeyB64), SSECustomerKeyMD5: aws.String(sourceKey.KeyMD5), }) require.NoError(t, err, "Failed to upload source SSE-C object") t.Run("Copy SSE-C to SSE-C with different key", func(t *testing.T) { destObjectKey := "dest-object-ssec" copySource := fmt.Sprintf("%s/%s", bucketName, sourceObjectKey) // Copy object with different SSE-C key _, err := client.CopyObject(ctx, &s3.CopyObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(destObjectKey), CopySource: aws.String(copySource), CopySourceSSECustomerAlgorithm: aws.String("AES256"), CopySourceSSECustomerKey: aws.String(sourceKey.KeyB64), CopySourceSSECustomerKeyMD5: aws.String(sourceKey.KeyMD5), SSECustomerAlgorithm: aws.String("AES256"), SSECustomerKey: aws.String(destKey.KeyB64), SSECustomerKeyMD5: aws.String(destKey.KeyMD5), }) require.NoError(t, err, "Failed to copy SSE-C object") // Retrieve copied object with destination key resp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(destObjectKey), SSECustomerAlgorithm: aws.String("AES256"), SSECustomerKey: aws.String(destKey.KeyB64), SSECustomerKeyMD5: aws.String(destKey.KeyMD5), }) require.NoError(t, err, "Failed to retrieve copied SSE-C object") defer resp.Body.Close() // Verify content matches original retrievedData, err := io.ReadAll(resp.Body) require.NoError(t, err, "Failed to read copied data") assertDataEqual(t, testData, retrievedData, "Copied data does not match original") }) t.Run("Copy SSE-C to plain", func(t *testing.T) { destObjectKey := "dest-object-plain" copySource := fmt.Sprintf("%s/%s", bucketName, sourceObjectKey) // Copy SSE-C object to plain object _, err := client.CopyObject(ctx, &s3.CopyObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(destObjectKey), CopySource: aws.String(copySource), CopySourceSSECustomerAlgorithm: aws.String("AES256"), CopySourceSSECustomerKey: aws.String(sourceKey.KeyB64), CopySourceSSECustomerKeyMD5: aws.String(sourceKey.KeyMD5), // No destination encryption headers = plain object }) require.NoError(t, err, "Failed to copy SSE-C to plain object") // Retrieve plain object (no encryption headers needed) resp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(destObjectKey), }) require.NoError(t, err, "Failed to retrieve plain copied object") defer resp.Body.Close() // Verify content matches original retrievedData, err := io.ReadAll(resp.Body) require.NoError(t, err, "Failed to read plain copied data") assertDataEqual(t, testData, retrievedData, "Plain copied data does not match original") }) } // TestSSEKMSObjectCopyIntegration tests SSE-KMS object copying end-to-end func TestSSEKMSObjectCopyIntegration(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+"ssekms-copy-") require.NoError(t, err, "Failed to create test bucket") defer cleanupTestBucket(ctx, client, bucketName) testData := []byte("Hello, SSE-KMS copy integration test!") sourceKeyID := "source-test-key-123" destKeyID := "dest-test-key-456" // Upload source object with SSE-KMS sourceObjectKey := "source-object-kms" _, err = client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(sourceObjectKey), Body: bytes.NewReader(testData), ServerSideEncryption: types.ServerSideEncryptionAwsKms, SSEKMSKeyId: aws.String(sourceKeyID), }) require.NoError(t, err, "Failed to upload source SSE-KMS object") t.Run("Copy SSE-KMS with different key", func(t *testing.T) { destObjectKey := "dest-object-kms" copySource := fmt.Sprintf("%s/%s", bucketName, sourceObjectKey) // Copy object with different SSE-KMS key _, err := client.CopyObject(ctx, &s3.CopyObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(destObjectKey), CopySource: aws.String(copySource), ServerSideEncryption: types.ServerSideEncryptionAwsKms, SSEKMSKeyId: aws.String(destKeyID), }) require.NoError(t, err, "Failed to copy SSE-KMS object") // Retrieve copied object resp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(destObjectKey), }) require.NoError(t, err, "Failed to retrieve copied SSE-KMS object") defer resp.Body.Close() // Verify content matches original retrievedData, err := io.ReadAll(resp.Body) require.NoError(t, err, "Failed to read copied KMS data") assertDataEqual(t, testData, retrievedData, "Copied KMS data does not match original") // Verify new key ID is used assert.Equal(t, destKeyID, aws.ToString(resp.SSEKMSKeyId)) }) } // TestSSEMultipartUploadIntegration tests SSE multipart uploads end-to-end func TestSSEMultipartUploadIntegration(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-") require.NoError(t, err, "Failed to create test bucket") defer cleanupTestBucket(ctx, client, bucketName) t.Run("SSE-C Multipart Upload", func(t *testing.T) { sseKey := generateSSECKey() objectKey := "multipart-ssec-object" // 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), }) require.NoError(t, err, "Failed to create SSE-C multipart upload") uploadID := aws.ToString(createResp.UploadId) // Upload parts partSize := 5 * 1024 * 1024 // 5MB part1Data := generateTestData(partSize) part2Data := generateTestData(partSize) // Upload part 1 part1Resp, err := client.UploadPart(ctx, &s3.UploadPartInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), PartNumber: aws.Int32(1), UploadId: aws.String(uploadID), Body: bytes.NewReader(part1Data), SSECustomerAlgorithm: aws.String("AES256"), SSECustomerKey: aws.String(sseKey.KeyB64), SSECustomerKeyMD5: aws.String(sseKey.KeyMD5), }) require.NoError(t, err, "Failed to upload part 1") // Upload part 2 part2Resp, err := client.UploadPart(ctx, &s3.UploadPartInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), PartNumber: aws.Int32(2), UploadId: aws.String(uploadID), Body: bytes.NewReader(part2Data), SSECustomerAlgorithm: aws.String("AES256"), SSECustomerKey: aws.String(sseKey.KeyB64), SSECustomerKeyMD5: aws.String(sseKey.KeyMD5), }) require.NoError(t, err, "Failed to upload part 2") // 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: []types.CompletedPart{ { ETag: part1Resp.ETag, PartNumber: aws.Int32(1), }, { ETag: part2Resp.ETag, PartNumber: aws.Int32(2), }, }, }, }) require.NoError(t, err, "Failed to complete SSE-C multipart upload") // Retrieve and verify the complete object resp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), SSECustomerAlgorithm: aws.String("AES256"), SSECustomerKey: aws.String(sseKey.KeyB64), SSECustomerKeyMD5: aws.String(sseKey.KeyMD5), }) require.NoError(t, err, "Failed to retrieve multipart SSE-C object") defer resp.Body.Close() retrievedData, err := io.ReadAll(resp.Body) require.NoError(t, err, "Failed to read multipart data") // Verify data matches concatenated parts expectedData := append(part1Data, part2Data...) assertDataEqual(t, expectedData, retrievedData, "Multipart data does not match original") assert.Equal(t, int64(len(expectedData)), aws.ToInt64(resp.ContentLength), "Multipart content length mismatch") }) t.Run("SSE-KMS Multipart Upload", func(t *testing.T) { kmsKeyID := "test-multipart-key" objectKey := "multipart-kms-object" // Create multipart upload createResp, err := client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), ServerSideEncryption: types.ServerSideEncryptionAwsKms, SSEKMSKeyId: aws.String(kmsKeyID), }) require.NoError(t, err, "Failed to create SSE-KMS multipart upload") uploadID := aws.ToString(createResp.UploadId) // Upload parts partSize := 5 * 1024 * 1024 // 5MB part1Data := generateTestData(partSize) part2Data := generateTestData(partSize / 2) // Different size // Upload part 1 part1Resp, err := client.UploadPart(ctx, &s3.UploadPartInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), PartNumber: aws.Int32(1), UploadId: aws.String(uploadID), Body: bytes.NewReader(part1Data), }) require.NoError(t, err, "Failed to upload KMS part 1") // Upload part 2 part2Resp, err := client.UploadPart(ctx, &s3.UploadPartInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), PartNumber: aws.Int32(2), UploadId: aws.String(uploadID), Body: bytes.NewReader(part2Data), }) require.NoError(t, err, "Failed to upload KMS part 2") // 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: []types.CompletedPart{ { ETag: part1Resp.ETag, PartNumber: aws.Int32(1), }, { ETag: part2Resp.ETag, PartNumber: aws.Int32(2), }, }, }, }) require.NoError(t, err, "Failed to complete SSE-KMS multipart upload") // Retrieve and verify the complete object resp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), }) require.NoError(t, err, "Failed to retrieve multipart SSE-KMS object") defer resp.Body.Close() retrievedData, err := io.ReadAll(resp.Body) require.NoError(t, err, "Failed to read multipart KMS data") // Verify data matches concatenated parts expectedData := append(part1Data, part2Data...) // Debug: Print some information about the sizes and first few bytes t.Logf("Expected data size: %d, Retrieved data size: %d", len(expectedData), len(retrievedData)) if len(expectedData) > 0 && len(retrievedData) > 0 { t.Logf("Expected first 32 bytes: %x", expectedData[:min(32, len(expectedData))]) t.Logf("Retrieved first 32 bytes: %x", retrievedData[:min(32, len(retrievedData))]) } assertDataEqual(t, expectedData, retrievedData, "Multipart KMS data does not match original") // Verify KMS metadata assert.Equal(t, types.ServerSideEncryptionAwsKms, resp.ServerSideEncryption) assert.Equal(t, kmsKeyID, aws.ToString(resp.SSEKMSKeyId)) }) } // TestDebugSSEMultipart helps debug the multipart SSE-KMS data mismatch func TestDebugSSEMultipart(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+"debug-multipart-") require.NoError(t, err, "Failed to create test bucket") defer cleanupTestBucket(ctx, client, bucketName) objectKey := "debug-multipart-object" kmsKeyID := "test-multipart-key" // Create multipart upload createResp, err := client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), ServerSideEncryption: types.ServerSideEncryptionAwsKms, SSEKMSKeyId: aws.String(kmsKeyID), }) require.NoError(t, err, "Failed to create SSE-KMS multipart upload") uploadID := aws.ToString(createResp.UploadId) // Upload two parts - exactly like the failing test partSize := 5 * 1024 * 1024 // 5MB part1Data := generateTestData(partSize) // 5MB part2Data := generateTestData(partSize / 2) // 2.5MB // Upload part 1 part1Resp, err := client.UploadPart(ctx, &s3.UploadPartInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), PartNumber: aws.Int32(1), UploadId: aws.String(uploadID), Body: bytes.NewReader(part1Data), }) require.NoError(t, err, "Failed to upload part 1") // Upload part 2 part2Resp, err := client.UploadPart(ctx, &s3.UploadPartInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), PartNumber: aws.Int32(2), UploadId: aws.String(uploadID), Body: bytes.NewReader(part2Data), }) require.NoError(t, err, "Failed to upload part 2") // 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: []types.CompletedPart{ {ETag: part1Resp.ETag, PartNumber: aws.Int32(1)}, {ETag: part2Resp.ETag, PartNumber: aws.Int32(2)}, }, }, }) require.NoError(t, err, "Failed to complete multipart upload") // Retrieve the object resp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), }) require.NoError(t, err, "Failed to retrieve object") defer resp.Body.Close() retrievedData, err := io.ReadAll(resp.Body) require.NoError(t, err, "Failed to read retrieved data") // Expected data expectedData := append(part1Data, part2Data...) t.Logf("=== DATA COMPARISON DEBUG ===") t.Logf("Expected size: %d, Retrieved size: %d", len(expectedData), len(retrievedData)) // Find exact point of divergence divergePoint := -1 minLen := len(expectedData) if len(retrievedData) < minLen { minLen = len(retrievedData) } for i := 0; i < minLen; i++ { if expectedData[i] != retrievedData[i] { divergePoint = i break } } if divergePoint >= 0 { t.Logf("Data diverges at byte %d (0x%x)", divergePoint, divergePoint) t.Logf("Expected: 0x%02x, Retrieved: 0x%02x", expectedData[divergePoint], retrievedData[divergePoint]) // Show context around divergence point start := divergePoint - 10 if start < 0 { start = 0 } end := divergePoint + 10 if end > minLen { end = minLen } t.Logf("Context [%d:%d]:", start, end) t.Logf("Expected: %x", expectedData[start:end]) t.Logf("Retrieved: %x", retrievedData[start:end]) // Identify chunk boundaries if divergePoint >= 4194304 { t.Logf("Divergence is in chunk 2 or 3 (after 4MB boundary)") } if divergePoint >= 5242880 { t.Logf("Divergence is in chunk 3 (part 2, after 5MB boundary)") } } else if len(expectedData) != len(retrievedData) { t.Logf("Data lengths differ but common part matches") } else { t.Logf("Data matches completely!") } // Test completed successfully t.Logf("SSE comparison test completed - data matches completely!") } // TestSSEErrorConditions tests various error conditions in SSE func TestSSEErrorConditions(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-errors-") require.NoError(t, err, "Failed to create test bucket") defer cleanupTestBucket(ctx, client, bucketName) t.Run("SSE-C Invalid Key Length", func(t *testing.T) { invalidKey := base64.StdEncoding.EncodeToString([]byte("too-short")) _, err := client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String("invalid-key-test"), Body: strings.NewReader("test"), SSECustomerAlgorithm: aws.String("AES256"), SSECustomerKey: aws.String(invalidKey), SSECustomerKeyMD5: aws.String("invalid-md5"), }) assert.Error(t, err, "Should fail with invalid SSE-C key") }) t.Run("SSE-KMS Invalid Key ID", func(t *testing.T) { // Empty key ID should be rejected _, err := client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String("invalid-kms-key-test"), Body: strings.NewReader("test"), ServerSideEncryption: types.ServerSideEncryptionAwsKms, SSEKMSKeyId: aws.String(""), // Invalid empty key }) assert.Error(t, err, "Should fail with empty KMS key ID") }) } // BenchmarkSSECThroughput benchmarks SSE-C throughput func BenchmarkSSECThroughput(b *testing.B) { ctx := context.Background() client, err := createS3Client(ctx, defaultConfig) require.NoError(b, err, "Failed to create S3 client") bucketName, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"ssec-bench-") require.NoError(b, err, "Failed to create test bucket") defer cleanupTestBucket(ctx, client, bucketName) sseKey := generateSSECKey() testData := generateTestData(1024 * 1024) // 1MB b.ResetTimer() b.SetBytes(int64(len(testData))) for i := 0; i < b.N; i++ { objectKey := fmt.Sprintf("bench-object-%d", i) // Upload _, err := client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), Body: bytes.NewReader(testData), SSECustomerAlgorithm: aws.String("AES256"), SSECustomerKey: aws.String(sseKey.KeyB64), SSECustomerKeyMD5: aws.String(sseKey.KeyMD5), }) require.NoError(b, err, "Failed to upload in benchmark") // Download resp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), SSECustomerAlgorithm: aws.String("AES256"), SSECustomerKey: aws.String(sseKey.KeyB64), SSECustomerKeyMD5: aws.String(sseKey.KeyMD5), }) require.NoError(b, err, "Failed to download in benchmark") _, err = io.ReadAll(resp.Body) require.NoError(b, err, "Failed to read data in benchmark") resp.Body.Close() } } // TestSSECRangeRequests tests SSE-C with HTTP Range requests func TestSSECRangeRequests(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+"ssec-range-") require.NoError(t, err, "Failed to create test bucket") defer cleanupTestBucket(ctx, client, bucketName) sseKey := generateSSECKey() // Create test data that's large enough for meaningful range tests testData := generateTestData(2048) // 2KB objectKey := "test-range-object" // Upload with SSE-C _, err = client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), Body: bytes.NewReader(testData), SSECustomerAlgorithm: aws.String("AES256"), SSECustomerKey: aws.String(sseKey.KeyB64), SSECustomerKeyMD5: aws.String(sseKey.KeyMD5), }) require.NoError(t, err, "Failed to upload SSE-C object") // Test various range requests testCases := []struct { name string start int64 end int64 }{ {"First 100 bytes", 0, 99}, {"Middle 100 bytes", 500, 599}, {"Last 100 bytes", int64(len(testData) - 100), int64(len(testData) - 1)}, {"Single byte", 42, 42}, {"Cross boundary", 15, 17}, // Test AES block boundary crossing } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Get range with SSE-C resp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), Range: aws.String(fmt.Sprintf("bytes=%d-%d", tc.start, tc.end)), SSECustomerAlgorithm: aws.String("AES256"), SSECustomerKey: aws.String(sseKey.KeyB64), SSECustomerKeyMD5: aws.String(sseKey.KeyMD5), }) require.NoError(t, err, "Failed to get range %d-%d from SSE-C object", tc.start, tc.end) defer resp.Body.Close() // Range requests should return partial content status // Note: AWS SDK Go v2 doesn't expose HTTP status code directly in GetObject response // The fact that we get a successful response with correct range data indicates 206 status // Read the range data rangeData, err := io.ReadAll(resp.Body) require.NoError(t, err, "Failed to read range data") // Verify content matches expected range expectedLength := tc.end - tc.start + 1 expectedData := testData[tc.start : tc.start+expectedLength] assertDataEqual(t, expectedData, rangeData, "Range data mismatch for %s", tc.name) // Verify content length header assert.Equal(t, expectedLength, aws.ToInt64(resp.ContentLength), "Content length mismatch for %s", tc.name) // Verify SSE headers are present assert.Equal(t, "AES256", aws.ToString(resp.SSECustomerAlgorithm)) assert.Equal(t, sseKey.KeyMD5, aws.ToString(resp.SSECustomerKeyMD5)) }) } } // TestSSEKMSRangeRequests tests SSE-KMS with HTTP Range requests func TestSSEKMSRangeRequests(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+"ssekms-range-") require.NoError(t, err, "Failed to create test bucket") defer cleanupTestBucket(ctx, client, bucketName) kmsKeyID := "test-range-key" // Create test data that's large enough for meaningful range tests testData := generateTestData(2048) // 2KB objectKey := "test-kms-range-object" // Upload with SSE-KMS _, err = client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), Body: bytes.NewReader(testData), ServerSideEncryption: types.ServerSideEncryptionAwsKms, SSEKMSKeyId: aws.String(kmsKeyID), }) require.NoError(t, err, "Failed to upload SSE-KMS object") // Test various range requests testCases := []struct { name string start int64 end int64 }{ {"First 100 bytes", 0, 99}, {"Middle 100 bytes", 500, 599}, {"Last 100 bytes", int64(len(testData) - 100), int64(len(testData) - 1)}, {"Single byte", 42, 42}, {"Cross boundary", 15, 17}, // Test AES block boundary crossing } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Get range with SSE-KMS (no additional headers needed for GET) resp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), Range: aws.String(fmt.Sprintf("bytes=%d-%d", tc.start, tc.end)), }) require.NoError(t, err, "Failed to get range %d-%d from SSE-KMS object", tc.start, tc.end) defer resp.Body.Close() // Range requests should return partial content status // Note: AWS SDK Go v2 doesn't expose HTTP status code directly in GetObject response // The fact that we get a successful response with correct range data indicates 206 status // Read the range data rangeData, err := io.ReadAll(resp.Body) require.NoError(t, err, "Failed to read range data") // Verify content matches expected range expectedLength := tc.end - tc.start + 1 expectedData := testData[tc.start : tc.start+expectedLength] assertDataEqual(t, expectedData, rangeData, "Range data mismatch for %s", tc.name) // Verify content length header assert.Equal(t, expectedLength, aws.ToInt64(resp.ContentLength), "Content length mismatch for %s", tc.name) // Verify SSE headers are present assert.Equal(t, types.ServerSideEncryptionAwsKms, resp.ServerSideEncryption) assert.Equal(t, kmsKeyID, aws.ToString(resp.SSEKMSKeyId)) }) } } // BenchmarkSSEKMSThroughput benchmarks SSE-KMS throughput func BenchmarkSSEKMSThroughput(b *testing.B) { ctx := context.Background() client, err := createS3Client(ctx, defaultConfig) require.NoError(b, err, "Failed to create S3 client") bucketName, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"ssekms-bench-") require.NoError(b, err, "Failed to create test bucket") defer cleanupTestBucket(ctx, client, bucketName) kmsKeyID := "bench-test-key" testData := generateTestData(1024 * 1024) // 1MB b.ResetTimer() b.SetBytes(int64(len(testData))) for i := 0; i < b.N; i++ { objectKey := fmt.Sprintf("bench-kms-object-%d", i) // Upload _, err := client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), Body: bytes.NewReader(testData), ServerSideEncryption: types.ServerSideEncryptionAwsKms, SSEKMSKeyId: aws.String(kmsKeyID), }) require.NoError(b, err, "Failed to upload in KMS benchmark") // Download resp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), }) require.NoError(b, err, "Failed to download in KMS benchmark") _, err = io.ReadAll(resp.Body) require.NoError(b, err, "Failed to read KMS data in benchmark") resp.Body.Close() } } // TestSSES3IntegrationBasic tests basic SSE-S3 upload and download functionality func TestSSES3IntegrationBasic(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, "sse-s3-basic") require.NoError(t, err, "Failed to create test bucket") defer cleanupTestBucket(ctx, client, bucketName) testData := []byte("Hello, SSE-S3! This is a test of server-side encryption with S3-managed keys.") objectKey := "test-sse-s3-object.txt" t.Run("SSE-S3 Upload", func(t *testing.T) { // Upload object with SSE-S3 _, err := client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), Body: bytes.NewReader(testData), ServerSideEncryption: types.ServerSideEncryptionAes256, }) require.NoError(t, err, "Failed to upload object with SSE-S3") }) t.Run("SSE-S3 Download", func(t *testing.T) { // Download and verify object resp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), }) require.NoError(t, err, "Failed to download SSE-S3 object") // Verify SSE-S3 headers in response assert.Equal(t, types.ServerSideEncryptionAes256, resp.ServerSideEncryption, "Server-side encryption header mismatch") // Read and verify content downloadedData, err := io.ReadAll(resp.Body) require.NoError(t, err, "Failed to read downloaded data") resp.Body.Close() assertDataEqual(t, testData, downloadedData, "Downloaded data doesn't match original") }) t.Run("SSE-S3 HEAD Request", func(t *testing.T) { // HEAD request should also return SSE headers resp, err := client.HeadObject(ctx, &s3.HeadObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), }) require.NoError(t, err, "Failed to HEAD SSE-S3 object") // Verify SSE-S3 headers assert.Equal(t, types.ServerSideEncryptionAes256, resp.ServerSideEncryption, "SSE-S3 header missing in HEAD response") }) } // TestSSES3IntegrationVariousDataSizes tests SSE-S3 with various data sizes func TestSSES3IntegrationVariousDataSizes(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, "sse-s3-sizes") require.NoError(t, err, "Failed to create test bucket") defer cleanupTestBucket(ctx, client, bucketName) // Test various data sizes including edge cases testSizes := []int{ 0, // Empty file 1, // Single byte 16, // One AES block 31, // Just under two blocks 32, // Exactly two blocks 100, // Small file 1024, // 1KB 8192, // 8KB 65536, // 64KB 1024 * 1024, // 1MB } for _, size := range testSizes { t.Run(fmt.Sprintf("Size_%d_bytes", size), func(t *testing.T) { testData := generateTestData(size) objectKey := fmt.Sprintf("test-sse-s3-%d.dat", size) // Upload with SSE-S3 _, err := client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), Body: bytes.NewReader(testData), ServerSideEncryption: types.ServerSideEncryptionAes256, }) require.NoError(t, err, "Failed to upload SSE-S3 object of size %d", size) // Download and verify resp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), }) require.NoError(t, err, "Failed to download SSE-S3 object of size %d", size) // Verify encryption headers assert.Equal(t, types.ServerSideEncryptionAes256, resp.ServerSideEncryption, "Missing SSE-S3 header for size %d", size) // Verify content downloadedData, err := io.ReadAll(resp.Body) require.NoError(t, err, "Failed to read downloaded data for size %d", size) resp.Body.Close() assertDataEqual(t, testData, downloadedData, "Data mismatch for size %d", size) }) } } // TestSSES3WithUserMetadata tests SSE-S3 with user-defined metadata func TestSSES3WithUserMetadata(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, "sse-s3-metadata") require.NoError(t, err, "Failed to create test bucket") defer cleanupTestBucket(ctx, client, bucketName) testData := []byte("SSE-S3 with custom metadata") objectKey := "test-object-with-metadata.txt" userMetadata := map[string]string{ "author": "test-user", "version": "1.0", "environment": "test", } t.Run("Upload with Metadata", func(t *testing.T) { // Upload object with SSE-S3 and user metadata _, err := client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), Body: bytes.NewReader(testData), ServerSideEncryption: types.ServerSideEncryptionAes256, Metadata: userMetadata, }) require.NoError(t, err, "Failed to upload object with SSE-S3 and metadata") }) t.Run("Verify Metadata and Encryption", func(t *testing.T) { // HEAD request to check metadata and encryption resp, err := client.HeadObject(ctx, &s3.HeadObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), }) require.NoError(t, err, "Failed to HEAD SSE-S3 object with metadata") // Verify SSE-S3 headers assert.Equal(t, types.ServerSideEncryptionAes256, resp.ServerSideEncryption, "SSE-S3 header missing with metadata") // Verify user metadata for key, expectedValue := range userMetadata { actualValue, exists := resp.Metadata[key] assert.True(t, exists, "Metadata key %s not found", key) assert.Equal(t, expectedValue, actualValue, "Metadata value mismatch for key %s", key) } }) t.Run("Download and Verify Content", func(t *testing.T) { // Download and verify content resp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), }) require.NoError(t, err, "Failed to download SSE-S3 object with metadata") // Verify SSE-S3 headers assert.Equal(t, types.ServerSideEncryptionAes256, resp.ServerSideEncryption, "SSE-S3 header missing in GET response") // Verify content downloadedData, err := io.ReadAll(resp.Body) require.NoError(t, err, "Failed to read downloaded data") resp.Body.Close() assertDataEqual(t, testData, downloadedData, "Downloaded data doesn't match original") }) } // TestSSES3RangeRequests tests SSE-S3 with HTTP range requests func TestSSES3RangeRequests(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, "sse-s3-range") require.NoError(t, err, "Failed to create test bucket") defer cleanupTestBucket(ctx, client, bucketName) // Create test data large enough to ensure multipart storage testData := generateTestData(1024 * 1024) // 1MB to ensure multipart chunking objectKey := "test-sse-s3-range.dat" // Upload object with SSE-S3 _, err = client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), Body: bytes.NewReader(testData), ServerSideEncryption: types.ServerSideEncryptionAes256, }) require.NoError(t, err, "Failed to upload SSE-S3 object for range testing") testCases := []struct { name string rangeHeader string expectedStart int expectedEnd int }{ {"First 100 bytes", "bytes=0-99", 0, 99}, {"Middle range", "bytes=100000-199999", 100000, 199999}, {"Last 100 bytes", "bytes=1048476-1048575", 1048476, 1048575}, {"From offset to end", "bytes=500000-", 500000, len(testData) - 1}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Request range resp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), Range: aws.String(tc.rangeHeader), }) require.NoError(t, err, "Failed to get range %s", tc.rangeHeader) // Verify SSE-S3 headers are present in range response assert.Equal(t, types.ServerSideEncryptionAes256, resp.ServerSideEncryption, "SSE-S3 header missing in range response") // Read range data rangeData, err := io.ReadAll(resp.Body) require.NoError(t, err, "Failed to read range data") resp.Body.Close() // Calculate expected data endIndex := tc.expectedEnd if tc.expectedEnd >= len(testData) { endIndex = len(testData) - 1 } expectedData := testData[tc.expectedStart : endIndex+1] // Verify range data assertDataEqual(t, expectedData, rangeData, "Range data mismatch for %s", tc.rangeHeader) }) } } // TestSSES3BucketDefaultEncryption tests bucket-level default encryption with SSE-S3 func TestSSES3BucketDefaultEncryption(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, "sse-s3-default") require.NoError(t, err, "Failed to create test bucket") defer cleanupTestBucket(ctx, client, bucketName) t.Run("Set Bucket Default Encryption", func(t *testing.T) { // Set bucket encryption configuration _, err := client.PutBucketEncryption(ctx, &s3.PutBucketEncryptionInput{ Bucket: aws.String(bucketName), ServerSideEncryptionConfiguration: &types.ServerSideEncryptionConfiguration{ Rules: []types.ServerSideEncryptionRule{ { ApplyServerSideEncryptionByDefault: &types.ServerSideEncryptionByDefault{ SSEAlgorithm: types.ServerSideEncryptionAes256, }, }, }, }, }) require.NoError(t, err, "Failed to set bucket default encryption") }) t.Run("Upload Object Without Encryption Headers", func(t *testing.T) { testData := []byte("This object should be automatically encrypted with SSE-S3 due to bucket default policy.") objectKey := "test-default-encrypted-object.txt" // Upload object WITHOUT any encryption headers _, err := client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), Body: bytes.NewReader(testData), // No ServerSideEncryption specified - should use bucket default }) require.NoError(t, err, "Failed to upload object without encryption headers") // Download and verify it was automatically encrypted resp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), }) require.NoError(t, err, "Failed to download object") // Verify SSE-S3 headers are present (indicating automatic encryption) assert.Equal(t, types.ServerSideEncryptionAes256, resp.ServerSideEncryption, "Object should have been automatically encrypted with SSE-S3") // Verify content is correct (decryption works) downloadedData, err := io.ReadAll(resp.Body) require.NoError(t, err, "Failed to read downloaded data") resp.Body.Close() assertDataEqual(t, testData, downloadedData, "Downloaded data doesn't match original") }) t.Run("Get Bucket Encryption Configuration", func(t *testing.T) { // Verify we can retrieve the bucket encryption configuration resp, err := client.GetBucketEncryption(ctx, &s3.GetBucketEncryptionInput{ Bucket: aws.String(bucketName), }) require.NoError(t, err, "Failed to get bucket encryption configuration") require.Len(t, resp.ServerSideEncryptionConfiguration.Rules, 1, "Should have one encryption rule") rule := resp.ServerSideEncryptionConfiguration.Rules[0] assert.Equal(t, types.ServerSideEncryptionAes256, rule.ApplyServerSideEncryptionByDefault.SSEAlgorithm, "Encryption algorithm should be AES256") }) t.Run("Delete Bucket Encryption Configuration", func(t *testing.T) { // Remove bucket encryption configuration _, err := client.DeleteBucketEncryption(ctx, &s3.DeleteBucketEncryptionInput{ Bucket: aws.String(bucketName), }) require.NoError(t, err, "Failed to delete bucket encryption configuration") // Verify it's removed by trying to get it (should fail) _, err = client.GetBucketEncryption(ctx, &s3.GetBucketEncryptionInput{ Bucket: aws.String(bucketName), }) require.Error(t, err, "Getting bucket encryption should fail after deletion") }) t.Run("Upload After Removing Default Encryption", func(t *testing.T) { testData := []byte("This object should NOT be encrypted after removing bucket default.") objectKey := "test-no-default-encryption.txt" // Upload object without encryption headers (should not be encrypted now) _, err := client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), Body: bytes.NewReader(testData), }) require.NoError(t, err, "Failed to upload object") // Verify it's NOT encrypted resp, err := client.HeadObject(ctx, &s3.HeadObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), }) require.NoError(t, err, "Failed to HEAD object") // ServerSideEncryption should be empty/nil when no encryption is applied assert.Empty(t, resp.ServerSideEncryption, "Object should not be encrypted after removing bucket default") }) } // TestSSES3MultipartUploads tests SSE-S3 multipart upload functionality func TestSSES3MultipartUploads(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-s3-multipart-") require.NoError(t, err, "Failed to create test bucket") defer cleanupTestBucket(ctx, client, bucketName) t.Run("Large_File_Multipart_Upload", func(t *testing.T) { objectKey := "test-sse-s3-multipart-large.dat" // Create 10MB test data to ensure multipart upload testData := generateTestData(10 * 1024 * 1024) // Upload with SSE-S3 _, err = client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), Body: bytes.NewReader(testData), ServerSideEncryption: types.ServerSideEncryptionAes256, }) require.NoError(t, err, "SSE-S3 multipart upload failed") // Verify encryption headers headResp, err := client.HeadObject(ctx, &s3.HeadObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), }) require.NoError(t, err, "Failed to head object") assert.Equal(t, types.ServerSideEncryptionAes256, headResp.ServerSideEncryption, "Expected SSE-S3 encryption") // Download and verify content getResp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), }) require.NoError(t, err, "Failed to download SSE-S3 multipart object") defer getResp.Body.Close() downloadedData, err := io.ReadAll(getResp.Body) require.NoError(t, err, "Failed to read downloaded data") assert.Equal(t, testData, downloadedData, "SSE-S3 multipart upload data should match") // Test range requests on multipart SSE-S3 object t.Run("Range_Request_On_Multipart", func(t *testing.T) { start := int64(1024 * 1024) // 1MB offset end := int64(2*1024*1024 - 1) // 2MB - 1 expectedLength := end - start + 1 rangeResp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), Range: aws.String(fmt.Sprintf("bytes=%d-%d", start, end)), }) require.NoError(t, err, "Failed to get range from SSE-S3 multipart object") defer rangeResp.Body.Close() rangeData, err := io.ReadAll(rangeResp.Body) require.NoError(t, err, "Failed to read range data") assert.Equal(t, expectedLength, int64(len(rangeData)), "Range length should match") // Verify range content matches original data expectedRange := testData[start : end+1] assert.Equal(t, expectedRange, rangeData, "Range content should match for SSE-S3 multipart object") }) }) t.Run("Explicit_Multipart_Upload_API", func(t *testing.T) { objectKey := "test-sse-s3-explicit-multipart.dat" testData := generateTestData(15 * 1024 * 1024) // 15MB // Create multipart upload with SSE-S3 createResp, err := client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), ServerSideEncryption: types.ServerSideEncryptionAes256, }) require.NoError(t, err, "Failed to create SSE-S3 multipart upload") uploadID := *createResp.UploadId var parts []types.CompletedPart // Upload parts (5MB each, except the last part) partSize := 5 * 1024 * 1024 for i := 0; i < len(testData); i += partSize { partNumber := int32(len(parts) + 1) endIdx := i + partSize if endIdx > len(testData) { endIdx = len(testData) } partData := testData[i:endIdx] uploadPartResp, 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(partData), }) require.NoError(t, err, "Failed to upload part %d", partNumber) parts = append(parts, types.CompletedPart{ ETag: uploadPartResp.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: parts, }, }) require.NoError(t, err, "Failed to complete SSE-S3 multipart upload") // Verify the completed object headResp, err := client.HeadObject(ctx, &s3.HeadObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), }) require.NoError(t, err, "Failed to head completed multipart object") assert.Equal(t, types.ServerSideEncryptionAes256, headResp.ServerSideEncryption, "Expected SSE-S3 encryption on completed multipart object") // Download and verify content getResp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), }) require.NoError(t, err, "Failed to download completed SSE-S3 multipart object") defer getResp.Body.Close() downloadedData, err := io.ReadAll(getResp.Body) require.NoError(t, err, "Failed to read downloaded data") assert.Equal(t, testData, downloadedData, "Explicit SSE-S3 multipart upload data should match") }) } // TestCrossSSECopy tests copying objects between different SSE encryption types func TestCrossSSECopy(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-cross-copy-") require.NoError(t, err, "Failed to create test bucket") defer cleanupTestBucket(ctx, client, bucketName) // Test data testData := []byte("Cross-SSE copy test data") // Generate proper SSE-C key sseKey := generateSSECKey() t.Run("SSE-S3_to_Unencrypted", func(t *testing.T) { sourceKey := "source-sse-s3-obj" destKey := "dest-unencrypted-obj" // Upload with SSE-S3 _, err = client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(sourceKey), Body: bytes.NewReader(testData), ServerSideEncryption: types.ServerSideEncryptionAes256, }) require.NoError(t, err, "SSE-S3 upload failed") // Copy to unencrypted _, err = client.CopyObject(ctx, &s3.CopyObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(destKey), CopySource: aws.String(fmt.Sprintf("%s/%s", bucketName, sourceKey)), }) require.NoError(t, err, "Copy SSE-S3 to unencrypted failed") // Verify destination is unencrypted and content matches getResp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(destKey), }) require.NoError(t, err, "GET failed") defer getResp.Body.Close() assert.Empty(t, getResp.ServerSideEncryption, "Should be unencrypted") downloadedData, err := io.ReadAll(getResp.Body) require.NoError(t, err, "Read failed") assertDataEqual(t, testData, downloadedData) }) t.Run("Unencrypted_to_SSE-S3", func(t *testing.T) { sourceKey := "source-unencrypted-obj" destKey := "dest-sse-s3-obj" // Upload unencrypted _, err = client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(sourceKey), Body: bytes.NewReader(testData), }) require.NoError(t, err, "Unencrypted upload failed") // Copy to SSE-S3 _, 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.ServerSideEncryptionAes256, }) require.NoError(t, err, "Copy unencrypted to SSE-S3 failed") // Verify destination is SSE-S3 encrypted and content matches getResp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(destKey), }) require.NoError(t, err, "GET failed") defer getResp.Body.Close() assert.Equal(t, types.ServerSideEncryptionAes256, getResp.ServerSideEncryption, "Expected SSE-S3") downloadedData, err := io.ReadAll(getResp.Body) require.NoError(t, err, "Read failed") assertDataEqual(t, testData, downloadedData) }) t.Run("SSE-C_to_SSE-S3", func(t *testing.T) { sourceKey := "source-sse-c-obj" destKey := "dest-sse-s3-obj" // Upload with SSE-C _, err = client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(sourceKey), Body: bytes.NewReader(testData), SSECustomerAlgorithm: aws.String("AES256"), SSECustomerKey: aws.String(sseKey.KeyB64), SSECustomerKeyMD5: aws.String(sseKey.KeyMD5), }) require.NoError(t, err, "SSE-C upload failed") // Copy to SSE-S3 _, err = client.CopyObject(ctx, &s3.CopyObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(destKey), CopySource: aws.String(fmt.Sprintf("%s/%s", bucketName, sourceKey)), CopySourceSSECustomerAlgorithm: aws.String("AES256"), CopySourceSSECustomerKey: aws.String(sseKey.KeyB64), CopySourceSSECustomerKeyMD5: aws.String(sseKey.KeyMD5), ServerSideEncryption: types.ServerSideEncryptionAes256, }) require.NoError(t, err, "Copy SSE-C to SSE-S3 failed") // Verify destination encryption and content headResp, err := client.HeadObject(ctx, &s3.HeadObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(destKey), }) require.NoError(t, err, "HEAD failed") assert.Equal(t, types.ServerSideEncryptionAes256, headResp.ServerSideEncryption, "Expected SSE-S3") getResp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(destKey), }) require.NoError(t, err, "GET failed") defer getResp.Body.Close() downloadedData, err := io.ReadAll(getResp.Body) require.NoError(t, err, "Read failed") assertDataEqual(t, testData, downloadedData) }) t.Run("SSE-S3_to_SSE-C", func(t *testing.T) { sourceKey := "source-sse-s3-obj" destKey := "dest-sse-c-obj" // Upload with SSE-S3 _, err = client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(sourceKey), Body: bytes.NewReader(testData), ServerSideEncryption: types.ServerSideEncryptionAes256, }) require.NoError(t, err, "Failed to upload SSE-S3 source object") // Copy to SSE-C _, err = client.CopyObject(ctx, &s3.CopyObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(destKey), CopySource: aws.String(fmt.Sprintf("%s/%s", bucketName, sourceKey)), SSECustomerAlgorithm: aws.String("AES256"), SSECustomerKey: aws.String(sseKey.KeyB64), SSECustomerKeyMD5: aws.String(sseKey.KeyMD5), }) require.NoError(t, err, "Copy SSE-S3 to SSE-C failed") // Verify destination encryption and content getResp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(destKey), SSECustomerAlgorithm: aws.String("AES256"), SSECustomerKey: aws.String(sseKey.KeyB64), SSECustomerKeyMD5: aws.String(sseKey.KeyMD5), }) require.NoError(t, err, "GET with SSE-C failed") defer getResp.Body.Close() assert.Equal(t, "AES256", aws.ToString(getResp.SSECustomerAlgorithm), "Expected SSE-C") downloadedData, err := io.ReadAll(getResp.Body) require.NoError(t, err, "Read failed") assertDataEqual(t, testData, downloadedData) }) } // REGRESSION TESTS FOR CRITICAL BUGS FIXED // These tests specifically target the IV storage bugs that were fixed // TestSSES3IVStorageRegression tests that IVs are properly stored for explicit SSE-S3 uploads // This test would have caught the critical bug where IVs were discarded in putToFiler func TestSSES3IVStorageRegression(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, "sse-s3-iv-regression") require.NoError(t, err, "Failed to create test bucket") defer cleanupTestBucket(ctx, client, bucketName) t.Run("Explicit SSE-S3 IV Storage and Retrieval", func(t *testing.T) { testData := []byte("This tests the critical IV storage bug that was fixed - the IV must be stored on the key object for decryption to work.") objectKey := "explicit-sse-s3-iv-test.txt" // Upload with explicit SSE-S3 header (this used to discard the IV) putResp, err := client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), Body: bytes.NewReader(testData), ServerSideEncryption: types.ServerSideEncryptionAes256, }) require.NoError(t, err, "Failed to upload explicit SSE-S3 object") // Verify PUT response has SSE-S3 headers assert.Equal(t, types.ServerSideEncryptionAes256, putResp.ServerSideEncryption, "PUT response should indicate SSE-S3") // Critical test: Download and decrypt the object // This would have FAILED with the original bug because IV was discarded getResp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), }) require.NoError(t, err, "Failed to download explicit SSE-S3 object") // Verify GET response has SSE-S3 headers assert.Equal(t, types.ServerSideEncryptionAes256, getResp.ServerSideEncryption, "GET response should indicate SSE-S3") // This is the critical test - verify data can be decrypted correctly downloadedData, err := io.ReadAll(getResp.Body) require.NoError(t, err, "Failed to read decrypted data") getResp.Body.Close() // This assertion would have FAILED with the original bug assertDataEqual(t, testData, downloadedData, "CRITICAL: Decryption failed - IV was not stored properly") }) t.Run("Multiple Explicit SSE-S3 Objects", func(t *testing.T) { // Test multiple objects to ensure each gets its own unique IV numObjects := 5 testDataSet := make([][]byte, numObjects) objectKeys := make([]string, numObjects) // Upload multiple objects with explicit SSE-S3 for i := 0; i < numObjects; i++ { testDataSet[i] = []byte(fmt.Sprintf("Test data for object %d - verifying unique IV storage", i)) objectKeys[i] = fmt.Sprintf("explicit-sse-s3-multi-%d.txt", i) _, err := client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKeys[i]), Body: bytes.NewReader(testDataSet[i]), ServerSideEncryption: types.ServerSideEncryptionAes256, }) require.NoError(t, err, "Failed to upload explicit SSE-S3 object %d", i) } // Download and verify each object decrypts correctly for i := 0; i < numObjects; i++ { getResp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKeys[i]), }) require.NoError(t, err, "Failed to download explicit SSE-S3 object %d", i) downloadedData, err := io.ReadAll(getResp.Body) require.NoError(t, err, "Failed to read decrypted data for object %d", i) getResp.Body.Close() assertDataEqual(t, testDataSet[i], downloadedData, "Decryption failed for object %d - IV not unique/stored", i) } }) } // TestSSES3BucketDefaultIVStorageRegression tests bucket default SSE-S3 IV storage // This test would have caught the critical bug where IVs were not stored on key objects in bucket defaults func TestSSES3BucketDefaultIVStorageRegression(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, "sse-s3-default-iv-regression") require.NoError(t, err, "Failed to create test bucket") defer cleanupTestBucket(ctx, client, bucketName) // Set bucket default encryption to SSE-S3 _, err = client.PutBucketEncryption(ctx, &s3.PutBucketEncryptionInput{ Bucket: aws.String(bucketName), ServerSideEncryptionConfiguration: &types.ServerSideEncryptionConfiguration{ Rules: []types.ServerSideEncryptionRule{ { ApplyServerSideEncryptionByDefault: &types.ServerSideEncryptionByDefault{ SSEAlgorithm: types.ServerSideEncryptionAes256, }, }, }, }, }) require.NoError(t, err, "Failed to set bucket default SSE-S3 encryption") t.Run("Bucket Default SSE-S3 IV Storage", func(t *testing.T) { testData := []byte("This tests the bucket default SSE-S3 IV storage bug - IV must be stored on key object for decryption.") objectKey := "bucket-default-sse-s3-iv-test.txt" // Upload WITHOUT encryption headers - should use bucket default SSE-S3 // This used to fail because applySSES3DefaultEncryption didn't store IV on key putResp, err := client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), Body: bytes.NewReader(testData), // No ServerSideEncryption specified - should use bucket default }) require.NoError(t, err, "Failed to upload object for bucket default SSE-S3") // Verify bucket default encryption was applied assert.Equal(t, types.ServerSideEncryptionAes256, putResp.ServerSideEncryption, "PUT response should show bucket default SSE-S3") // Critical test: Download and decrypt the object // This would have FAILED with the original bug because IV wasn't stored on key object getResp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), }) require.NoError(t, err, "Failed to download bucket default SSE-S3 object") // Verify GET response shows SSE-S3 was applied assert.Equal(t, types.ServerSideEncryptionAes256, getResp.ServerSideEncryption, "GET response should show SSE-S3") // This is the critical test - verify decryption works downloadedData, err := io.ReadAll(getResp.Body) require.NoError(t, err, "Failed to read decrypted data") getResp.Body.Close() // This assertion would have FAILED with the original bucket default bug assertDataEqual(t, testData, downloadedData, "CRITICAL: Bucket default SSE-S3 decryption failed - IV not stored on key object") }) t.Run("Multiple Bucket Default Objects", func(t *testing.T) { // Test multiple objects with bucket default encryption numObjects := 3 testDataSet := make([][]byte, numObjects) objectKeys := make([]string, numObjects) // Upload multiple objects without encryption headers for i := 0; i < numObjects; i++ { testDataSet[i] = []byte(fmt.Sprintf("Bucket default test data %d - verifying IV storage works", i)) objectKeys[i] = fmt.Sprintf("bucket-default-multi-%d.txt", i) _, err := client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKeys[i]), Body: bytes.NewReader(testDataSet[i]), // No encryption headers - bucket default should apply }) require.NoError(t, err, "Failed to upload bucket default object %d", i) } // Verify each object was encrypted and can be decrypted for i := 0; i < numObjects; i++ { getResp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKeys[i]), }) require.NoError(t, err, "Failed to download bucket default object %d", i) // Verify SSE-S3 was applied by bucket default assert.Equal(t, types.ServerSideEncryptionAes256, getResp.ServerSideEncryption, "Object %d should be SSE-S3 encrypted", i) downloadedData, err := io.ReadAll(getResp.Body) require.NoError(t, err, "Failed to read decrypted data for object %d", i) getResp.Body.Close() assertDataEqual(t, testDataSet[i], downloadedData, "Bucket default SSE-S3 decryption failed for object %d", i) } }) } // TestSSES3EdgeCaseRegression tests edge cases that could cause IV storage issues func TestSSES3EdgeCaseRegression(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, "sse-s3-edge-regression") require.NoError(t, err, "Failed to create test bucket") defer cleanupTestBucket(ctx, client, bucketName) t.Run("Empty Object SSE-S3", func(t *testing.T) { // Test edge case: empty objects with SSE-S3 (IV storage still required) objectKey := "empty-sse-s3-object" _, err := client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), Body: bytes.NewReader([]byte{}), ServerSideEncryption: types.ServerSideEncryptionAes256, }) require.NoError(t, err, "Failed to upload empty SSE-S3 object") // Verify empty object can be retrieved (IV must be stored even for empty objects) getResp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), }) require.NoError(t, err, "Failed to download empty SSE-S3 object") downloadedData, err := io.ReadAll(getResp.Body) require.NoError(t, err, "Failed to read empty decrypted data") getResp.Body.Close() assert.Equal(t, []byte{}, downloadedData, "Empty object content mismatch") assert.Equal(t, types.ServerSideEncryptionAes256, getResp.ServerSideEncryption, "Empty object should be SSE-S3 encrypted") }) t.Run("Large Object SSE-S3", func(t *testing.T) { // Test large objects to ensure IV storage works for chunked uploads largeData := generateTestData(1024 * 1024) // 1MB objectKey := "large-sse-s3-object" _, err := client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), Body: bytes.NewReader(largeData), ServerSideEncryption: types.ServerSideEncryptionAes256, }) require.NoError(t, err, "Failed to upload large SSE-S3 object") // Verify large object can be decrypted (IV must be stored properly) getResp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), }) require.NoError(t, err, "Failed to download large SSE-S3 object") downloadedData, err := io.ReadAll(getResp.Body) require.NoError(t, err, "Failed to read large decrypted data") getResp.Body.Close() assertDataEqual(t, largeData, downloadedData, "Large object decryption failed - IV storage issue") assert.Equal(t, types.ServerSideEncryptionAes256, getResp.ServerSideEncryption, "Large object should be SSE-S3 encrypted") }) } // TestSSES3ErrorHandlingRegression tests error handling improvements that were added func TestSSES3ErrorHandlingRegression(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, "sse-s3-error-regression") require.NoError(t, err, "Failed to create test bucket") defer cleanupTestBucket(ctx, client, bucketName) t.Run("SSE-S3 With Other Valid Operations", func(t *testing.T) { // Ensure SSE-S3 works with other S3 operations (metadata, tagging, etc.) testData := []byte("Testing SSE-S3 with metadata and other operations") objectKey := "sse-s3-with-metadata" // Upload with SSE-S3 and metadata _, err := client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), Body: bytes.NewReader(testData), ServerSideEncryption: types.ServerSideEncryptionAes256, Metadata: map[string]string{ "test-key": "test-value", "purpose": "regression-test", }, }) require.NoError(t, err, "Failed to upload SSE-S3 object with metadata") // HEAD request to verify metadata and encryption headResp, err := client.HeadObject(ctx, &s3.HeadObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), }) require.NoError(t, err, "Failed to HEAD SSE-S3 object") assert.Equal(t, types.ServerSideEncryptionAes256, headResp.ServerSideEncryption, "HEAD should show SSE-S3") assert.Equal(t, "test-value", headResp.Metadata["test-key"], "Metadata should be preserved") assert.Equal(t, "regression-test", headResp.Metadata["purpose"], "Metadata should be preserved") // GET to verify decryption still works with metadata getResp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), }) require.NoError(t, err, "Failed to GET SSE-S3 object") downloadedData, err := io.ReadAll(getResp.Body) require.NoError(t, err, "Failed to read decrypted data") getResp.Body.Close() assertDataEqual(t, testData, downloadedData, "SSE-S3 with metadata decryption failed") assert.Equal(t, types.ServerSideEncryptionAes256, getResp.ServerSideEncryption, "GET should show SSE-S3") assert.Equal(t, "test-value", getResp.Metadata["test-key"], "GET metadata should be preserved") }) } // TestSSES3FunctionalityCompletion tests that SSE-S3 feature is now fully functional func TestSSES3FunctionalityCompletion(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, "sse-s3-completion") require.NoError(t, err, "Failed to create test bucket") defer cleanupTestBucket(ctx, client, bucketName) t.Run("All SSE-S3 Scenarios Work", func(t *testing.T) { scenarios := []struct { name string setupBucket func() error encryption *types.ServerSideEncryption expectSSES3 bool }{ { name: "Explicit SSE-S3 Header", setupBucket: func() error { return nil }, encryption: &[]types.ServerSideEncryption{types.ServerSideEncryptionAes256}[0], expectSSES3: true, }, { name: "Bucket Default SSE-S3", setupBucket: func() error { _, err := client.PutBucketEncryption(ctx, &s3.PutBucketEncryptionInput{ Bucket: aws.String(bucketName), ServerSideEncryptionConfiguration: &types.ServerSideEncryptionConfiguration{ Rules: []types.ServerSideEncryptionRule{ { ApplyServerSideEncryptionByDefault: &types.ServerSideEncryptionByDefault{ SSEAlgorithm: types.ServerSideEncryptionAes256, }, }, }, }, }) return err }, encryption: nil, expectSSES3: true, }, } for i, scenario := range scenarios { t.Run(scenario.name, func(t *testing.T) { // Setup bucket if needed err := scenario.setupBucket() require.NoError(t, err, "Failed to setup bucket for scenario %s", scenario.name) testData := []byte(fmt.Sprintf("Test data for scenario: %s", scenario.name)) objectKey := fmt.Sprintf("completion-test-%d", i) // Upload object putInput := &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), Body: bytes.NewReader(testData), } if scenario.encryption != nil { putInput.ServerSideEncryption = *scenario.encryption } putResp, err := client.PutObject(ctx, putInput) require.NoError(t, err, "Failed to upload object for scenario %s", scenario.name) if scenario.expectSSES3 { assert.Equal(t, types.ServerSideEncryptionAes256, putResp.ServerSideEncryption, "Should use SSE-S3 for %s", scenario.name) } // Download and verify getResp, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), }) require.NoError(t, err, "Failed to download object for scenario %s", scenario.name) if scenario.expectSSES3 { assert.Equal(t, types.ServerSideEncryptionAes256, getResp.ServerSideEncryption, "Should return SSE-S3 for %s", scenario.name) } downloadedData, err := io.ReadAll(getResp.Body) require.NoError(t, err, "Failed to read data for scenario %s", scenario.name) getResp.Body.Close() // This is the ultimate test - decryption must work assertDataEqual(t, testData, downloadedData, "Decryption failed for scenario %s", scenario.name) // Clean up bucket encryption for next scenario client.DeleteBucketEncryption(ctx, &s3.DeleteBucketEncryptionInput{ Bucket: aws.String(bucketName), }) }) } }) }