package s3api import ( "bytes" "io" "net/http" "strings" "testing" "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" ) // TestSSECObjectCopy tests copying SSE-C encrypted objects with different keys func TestSSECObjectCopy(t *testing.T) { // Original key for source object sourceKey := GenerateTestSSECKey(1) sourceCustomerKey := &SSECustomerKey{ Algorithm: "AES256", Key: sourceKey.Key, KeyMD5: sourceKey.KeyMD5, } // Destination key for target object destKey := GenerateTestSSECKey(2) destCustomerKey := &SSECustomerKey{ Algorithm: "AES256", Key: destKey.Key, KeyMD5: destKey.KeyMD5, } testData := "Hello, SSE-C copy world!" // Encrypt with source key encryptedReader, iv, err := CreateSSECEncryptedReader(strings.NewReader(testData), sourceCustomerKey) if err != nil { t.Fatalf("Failed to create encrypted reader: %v", err) } encryptedData, err := io.ReadAll(encryptedReader) if err != nil { t.Fatalf("Failed to read encrypted data: %v", err) } // Test copy strategy determination sourceMetadata := make(map[string][]byte) StoreIVInMetadata(sourceMetadata, iv) sourceMetadata[s3_constants.AmzServerSideEncryptionCustomerAlgorithm] = []byte("AES256") sourceMetadata[s3_constants.AmzServerSideEncryptionCustomerKeyMD5] = []byte(sourceKey.KeyMD5) t.Run("Same key copy (direct copy)", func(t *testing.T) { strategy, err := DetermineSSECCopyStrategy(sourceMetadata, sourceCustomerKey, sourceCustomerKey) if err != nil { t.Fatalf("Failed to determine copy strategy: %v", err) } if strategy != SSECCopyStrategyDirect { t.Errorf("Expected direct copy strategy for same key, got %v", strategy) } }) t.Run("Different key copy (decrypt-encrypt)", func(t *testing.T) { strategy, err := DetermineSSECCopyStrategy(sourceMetadata, sourceCustomerKey, destCustomerKey) if err != nil { t.Fatalf("Failed to determine copy strategy: %v", err) } if strategy != SSECCopyStrategyDecryptEncrypt { t.Errorf("Expected decrypt-encrypt copy strategy for different keys, got %v", strategy) } }) t.Run("Can direct copy check", func(t *testing.T) { // Same key should allow direct copy canDirect := CanDirectCopySSEC(sourceMetadata, sourceCustomerKey, sourceCustomerKey) if !canDirect { t.Error("Should allow direct copy with same key") } // Different key should not allow direct copy canDirect = CanDirectCopySSEC(sourceMetadata, sourceCustomerKey, destCustomerKey) if canDirect { t.Error("Should not allow direct copy with different keys") } }) // Test actual copy operation (decrypt with source key, encrypt with dest key) t.Run("Full copy operation", func(t *testing.T) { // Decrypt with source key decryptedReader, err := CreateSSECDecryptedReader(bytes.NewReader(encryptedData), sourceCustomerKey, iv) if err != nil { t.Fatalf("Failed to create decrypted reader: %v", err) } // Re-encrypt with destination key reEncryptedReader, destIV, err := CreateSSECEncryptedReader(decryptedReader, destCustomerKey) if err != nil { t.Fatalf("Failed to create re-encrypted reader: %v", err) } reEncryptedData, err := io.ReadAll(reEncryptedReader) if err != nil { t.Fatalf("Failed to read re-encrypted data: %v", err) } // Verify we can decrypt with destination key finalDecryptedReader, err := CreateSSECDecryptedReader(bytes.NewReader(reEncryptedData), destCustomerKey, destIV) if err != nil { t.Fatalf("Failed to create final decrypted reader: %v", err) } finalData, err := io.ReadAll(finalDecryptedReader) if err != nil { t.Fatalf("Failed to read final decrypted data: %v", err) } if string(finalData) != testData { t.Errorf("Expected %s, got %s", testData, string(finalData)) } }) } // TestSSEKMSObjectCopy tests copying SSE-KMS encrypted objects func TestSSEKMSObjectCopy(t *testing.T) { kmsKey := SetupTestKMS(t) defer kmsKey.Cleanup() testData := "Hello, SSE-KMS copy world!" encryptionContext := BuildEncryptionContext("test-bucket", "test-object", false) // Encrypt with SSE-KMS encryptedReader, sseKey, err := CreateSSEKMSEncryptedReader(strings.NewReader(testData), kmsKey.KeyID, encryptionContext) if err != nil { t.Fatalf("Failed to create encrypted reader: %v", err) } encryptedData, err := io.ReadAll(encryptedReader) if err != nil { t.Fatalf("Failed to read encrypted data: %v", err) } t.Run("Same KMS key copy", func(t *testing.T) { // Decrypt with original key decryptedReader, err := CreateSSEKMSDecryptedReader(bytes.NewReader(encryptedData), sseKey) if err != nil { t.Fatalf("Failed to create decrypted reader: %v", err) } // Re-encrypt with same KMS key reEncryptedReader, newSseKey, err := CreateSSEKMSEncryptedReader(decryptedReader, kmsKey.KeyID, encryptionContext) if err != nil { t.Fatalf("Failed to create re-encrypted reader: %v", err) } reEncryptedData, err := io.ReadAll(reEncryptedReader) if err != nil { t.Fatalf("Failed to read re-encrypted data: %v", err) } // Verify we can decrypt with new key finalDecryptedReader, err := CreateSSEKMSDecryptedReader(bytes.NewReader(reEncryptedData), newSseKey) if err != nil { t.Fatalf("Failed to create final decrypted reader: %v", err) } finalData, err := io.ReadAll(finalDecryptedReader) if err != nil { t.Fatalf("Failed to read final decrypted data: %v", err) } if string(finalData) != testData { t.Errorf("Expected %s, got %s", testData, string(finalData)) } }) } // TestSSECToSSEKMSCopy tests cross-encryption copy (SSE-C to SSE-KMS) func TestSSECToSSEKMSCopy(t *testing.T) { // Setup SSE-C key ssecKey := GenerateTestSSECKey(1) ssecCustomerKey := &SSECustomerKey{ Algorithm: "AES256", Key: ssecKey.Key, KeyMD5: ssecKey.KeyMD5, } // Setup SSE-KMS kmsKey := SetupTestKMS(t) defer kmsKey.Cleanup() testData := "Hello, cross-encryption copy world!" // Encrypt with SSE-C encryptedReader, ssecIV, err := CreateSSECEncryptedReader(strings.NewReader(testData), ssecCustomerKey) if err != nil { t.Fatalf("Failed to create SSE-C encrypted reader: %v", err) } encryptedData, err := io.ReadAll(encryptedReader) if err != nil { t.Fatalf("Failed to read SSE-C encrypted data: %v", err) } // Decrypt SSE-C data decryptedReader, err := CreateSSECDecryptedReader(bytes.NewReader(encryptedData), ssecCustomerKey, ssecIV) if err != nil { t.Fatalf("Failed to create SSE-C decrypted reader: %v", err) } // Re-encrypt with SSE-KMS encryptionContext := BuildEncryptionContext("test-bucket", "test-object", false) reEncryptedReader, sseKmsKey, err := CreateSSEKMSEncryptedReader(decryptedReader, kmsKey.KeyID, encryptionContext) if err != nil { t.Fatalf("Failed to create SSE-KMS encrypted reader: %v", err) } reEncryptedData, err := io.ReadAll(reEncryptedReader) if err != nil { t.Fatalf("Failed to read SSE-KMS encrypted data: %v", err) } // Decrypt with SSE-KMS finalDecryptedReader, err := CreateSSEKMSDecryptedReader(bytes.NewReader(reEncryptedData), sseKmsKey) if err != nil { t.Fatalf("Failed to create SSE-KMS decrypted reader: %v", err) } finalData, err := io.ReadAll(finalDecryptedReader) if err != nil { t.Fatalf("Failed to read final decrypted data: %v", err) } if string(finalData) != testData { t.Errorf("Expected %s, got %s", testData, string(finalData)) } } // TestSSEKMSToSSECCopy tests cross-encryption copy (SSE-KMS to SSE-C) func TestSSEKMSToSSECCopy(t *testing.T) { // Setup SSE-KMS kmsKey := SetupTestKMS(t) defer kmsKey.Cleanup() // Setup SSE-C key ssecKey := GenerateTestSSECKey(1) ssecCustomerKey := &SSECustomerKey{ Algorithm: "AES256", Key: ssecKey.Key, KeyMD5: ssecKey.KeyMD5, } testData := "Hello, reverse cross-encryption copy world!" encryptionContext := BuildEncryptionContext("test-bucket", "test-object", false) // Encrypt with SSE-KMS encryptedReader, sseKmsKey, err := CreateSSEKMSEncryptedReader(strings.NewReader(testData), kmsKey.KeyID, encryptionContext) if err != nil { t.Fatalf("Failed to create SSE-KMS encrypted reader: %v", err) } encryptedData, err := io.ReadAll(encryptedReader) if err != nil { t.Fatalf("Failed to read SSE-KMS encrypted data: %v", err) } // Decrypt SSE-KMS data decryptedReader, err := CreateSSEKMSDecryptedReader(bytes.NewReader(encryptedData), sseKmsKey) if err != nil { t.Fatalf("Failed to create SSE-KMS decrypted reader: %v", err) } // Re-encrypt with SSE-C reEncryptedReader, reEncryptedIV, err := CreateSSECEncryptedReader(decryptedReader, ssecCustomerKey) if err != nil { t.Fatalf("Failed to create SSE-C encrypted reader: %v", err) } reEncryptedData, err := io.ReadAll(reEncryptedReader) if err != nil { t.Fatalf("Failed to read SSE-C encrypted data: %v", err) } // Decrypt with SSE-C finalDecryptedReader, err := CreateSSECDecryptedReader(bytes.NewReader(reEncryptedData), ssecCustomerKey, reEncryptedIV) if err != nil { t.Fatalf("Failed to create SSE-C decrypted reader: %v", err) } finalData, err := io.ReadAll(finalDecryptedReader) if err != nil { t.Fatalf("Failed to read final decrypted data: %v", err) } if string(finalData) != testData { t.Errorf("Expected %s, got %s", testData, string(finalData)) } } // TestSSECopyWithCorruptedSource tests copy operations with corrupted source data func TestSSECopyWithCorruptedSource(t *testing.T) { ssecKey := GenerateTestSSECKey(1) ssecCustomerKey := &SSECustomerKey{ Algorithm: "AES256", Key: ssecKey.Key, KeyMD5: ssecKey.KeyMD5, } testData := "Hello, corruption test!" // Encrypt data encryptedReader, iv, err := CreateSSECEncryptedReader(strings.NewReader(testData), ssecCustomerKey) if err != nil { t.Fatalf("Failed to create encrypted reader: %v", err) } encryptedData, err := io.ReadAll(encryptedReader) if err != nil { t.Fatalf("Failed to read encrypted data: %v", err) } // Corrupt the encrypted data corruptedData := make([]byte, len(encryptedData)) copy(corruptedData, encryptedData) if len(corruptedData) > s3_constants.AESBlockSize { // Corrupt a byte after the IV corruptedData[s3_constants.AESBlockSize] ^= 0xFF } // Try to decrypt corrupted data decryptedReader, err := CreateSSECDecryptedReader(bytes.NewReader(corruptedData), ssecCustomerKey, iv) if err != nil { t.Fatalf("Failed to create decrypted reader for corrupted data: %v", err) } decryptedData, err := io.ReadAll(decryptedReader) if err != nil { // This is okay - corrupted data might cause read errors t.Logf("Read error for corrupted data (expected): %v", err) return } // If we can read it, the data should be different from original if string(decryptedData) == testData { t.Error("Decrypted corrupted data should not match original") } } // TestSSEKMSCopyStrategy tests SSE-KMS copy strategy determination func TestSSEKMSCopyStrategy(t *testing.T) { tests := []struct { name string srcMetadata map[string][]byte destKeyID string expectedStrategy SSEKMSCopyStrategy }{ { name: "Unencrypted to unencrypted", srcMetadata: map[string][]byte{}, destKeyID: "", expectedStrategy: SSEKMSCopyStrategyDirect, }, { name: "Same KMS key", srcMetadata: map[string][]byte{ s3_constants.AmzServerSideEncryption: []byte("aws:kms"), s3_constants.AmzServerSideEncryptionAwsKmsKeyId: []byte("test-key-123"), }, destKeyID: "test-key-123", expectedStrategy: SSEKMSCopyStrategyDirect, }, { name: "Different KMS keys", srcMetadata: map[string][]byte{ s3_constants.AmzServerSideEncryption: []byte("aws:kms"), s3_constants.AmzServerSideEncryptionAwsKmsKeyId: []byte("test-key-123"), }, destKeyID: "test-key-456", expectedStrategy: SSEKMSCopyStrategyDecryptEncrypt, }, { name: "Encrypted to unencrypted", srcMetadata: map[string][]byte{ s3_constants.AmzServerSideEncryption: []byte("aws:kms"), s3_constants.AmzServerSideEncryptionAwsKmsKeyId: []byte("test-key-123"), }, destKeyID: "", expectedStrategy: SSEKMSCopyStrategyDecryptEncrypt, }, { name: "Unencrypted to encrypted", srcMetadata: map[string][]byte{}, destKeyID: "test-key-123", expectedStrategy: SSEKMSCopyStrategyDecryptEncrypt, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { strategy, err := DetermineSSEKMSCopyStrategy(tt.srcMetadata, tt.destKeyID) if err != nil { t.Fatalf("DetermineSSEKMSCopyStrategy failed: %v", err) } if strategy != tt.expectedStrategy { t.Errorf("Expected strategy %v, got %v", tt.expectedStrategy, strategy) } }) } } // TestSSEKMSCopyHeaders tests SSE-KMS copy header parsing func TestSSEKMSCopyHeaders(t *testing.T) { tests := []struct { name string headers map[string]string expectedKeyID string expectedContext map[string]string expectedBucketKey bool expectError bool }{ { name: "No SSE-KMS headers", headers: map[string]string{}, expectedKeyID: "", expectedContext: nil, expectedBucketKey: false, expectError: false, }, { name: "SSE-KMS with key ID", headers: map[string]string{ s3_constants.AmzServerSideEncryption: "aws:kms", s3_constants.AmzServerSideEncryptionAwsKmsKeyId: "test-key-123", }, expectedKeyID: "test-key-123", expectedContext: nil, expectedBucketKey: false, expectError: false, }, { name: "SSE-KMS with all options", headers: map[string]string{ s3_constants.AmzServerSideEncryption: "aws:kms", s3_constants.AmzServerSideEncryptionAwsKmsKeyId: "test-key-123", s3_constants.AmzServerSideEncryptionContext: "eyJ0ZXN0IjoidmFsdWUifQ==", // base64 of {"test":"value"} s3_constants.AmzServerSideEncryptionBucketKeyEnabled: "true", }, expectedKeyID: "test-key-123", expectedContext: map[string]string{"test": "value"}, expectedBucketKey: true, expectError: false, }, { name: "Invalid key ID", headers: map[string]string{ s3_constants.AmzServerSideEncryption: "aws:kms", s3_constants.AmzServerSideEncryptionAwsKmsKeyId: "invalid key id", }, expectError: true, }, { name: "Invalid encryption context", headers: map[string]string{ s3_constants.AmzServerSideEncryption: "aws:kms", s3_constants.AmzServerSideEncryptionContext: "invalid-base64!", }, expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req, _ := http.NewRequest("PUT", "/test", nil) for k, v := range tt.headers { req.Header.Set(k, v) } keyID, context, bucketKey, err := ParseSSEKMSCopyHeaders(req) if tt.expectError { if err == nil { t.Error("Expected error but got none") } return } if err != nil { t.Fatalf("Unexpected error: %v", err) } if keyID != tt.expectedKeyID { t.Errorf("Expected keyID %s, got %s", tt.expectedKeyID, keyID) } if !mapsEqual(context, tt.expectedContext) { t.Errorf("Expected context %v, got %v", tt.expectedContext, context) } if bucketKey != tt.expectedBucketKey { t.Errorf("Expected bucketKey %v, got %v", tt.expectedBucketKey, bucketKey) } }) } } // TestSSEKMSDirectCopy tests direct copy scenarios func TestSSEKMSDirectCopy(t *testing.T) { tests := []struct { name string srcMetadata map[string][]byte destKeyID string canDirect bool }{ { name: "Both unencrypted", srcMetadata: map[string][]byte{}, destKeyID: "", canDirect: true, }, { name: "Same key ID", srcMetadata: map[string][]byte{ s3_constants.AmzServerSideEncryption: []byte("aws:kms"), s3_constants.AmzServerSideEncryptionAwsKmsKeyId: []byte("test-key-123"), }, destKeyID: "test-key-123", canDirect: true, }, { name: "Different key IDs", srcMetadata: map[string][]byte{ s3_constants.AmzServerSideEncryption: []byte("aws:kms"), s3_constants.AmzServerSideEncryptionAwsKmsKeyId: []byte("test-key-123"), }, destKeyID: "test-key-456", canDirect: false, }, { name: "Source encrypted, dest unencrypted", srcMetadata: map[string][]byte{ s3_constants.AmzServerSideEncryption: []byte("aws:kms"), s3_constants.AmzServerSideEncryptionAwsKmsKeyId: []byte("test-key-123"), }, destKeyID: "", canDirect: false, }, { name: "Source unencrypted, dest encrypted", srcMetadata: map[string][]byte{}, destKeyID: "test-key-123", canDirect: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { canDirect := CanDirectCopySSEKMS(tt.srcMetadata, tt.destKeyID) if canDirect != tt.canDirect { t.Errorf("Expected canDirect %v, got %v", tt.canDirect, canDirect) } }) } } // TestGetSourceSSEKMSInfo tests extraction of SSE-KMS info from metadata func TestGetSourceSSEKMSInfo(t *testing.T) { tests := []struct { name string metadata map[string][]byte expectedKeyID string expectedEncrypted bool }{ { name: "No encryption", metadata: map[string][]byte{}, expectedKeyID: "", expectedEncrypted: false, }, { name: "SSE-KMS with key ID", metadata: map[string][]byte{ s3_constants.AmzServerSideEncryption: []byte("aws:kms"), s3_constants.AmzServerSideEncryptionAwsKmsKeyId: []byte("test-key-123"), }, expectedKeyID: "test-key-123", expectedEncrypted: true, }, { name: "SSE-KMS without key ID (default key)", metadata: map[string][]byte{ s3_constants.AmzServerSideEncryption: []byte("aws:kms"), }, expectedKeyID: "", expectedEncrypted: true, }, { name: "Non-KMS encryption", metadata: map[string][]byte{ s3_constants.AmzServerSideEncryption: []byte("AES256"), }, expectedKeyID: "", expectedEncrypted: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { keyID, encrypted := GetSourceSSEKMSInfo(tt.metadata) if keyID != tt.expectedKeyID { t.Errorf("Expected keyID %s, got %s", tt.expectedKeyID, keyID) } if encrypted != tt.expectedEncrypted { t.Errorf("Expected encrypted %v, got %v", tt.expectedEncrypted, encrypted) } }) } } // Helper function to compare maps func mapsEqual(a, b map[string]string) bool { if len(a) != len(b) { return false } for k, v := range a { if b[k] != v { return false } } return true }