aboutsummaryrefslogtreecommitdiff
path: root/weed/s3api/s3_sse_s3_integration_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'weed/s3api/s3_sse_s3_integration_test.go')
-rw-r--r--weed/s3api/s3_sse_s3_integration_test.go325
1 files changed, 325 insertions, 0 deletions
diff --git a/weed/s3api/s3_sse_s3_integration_test.go b/weed/s3api/s3_sse_s3_integration_test.go
new file mode 100644
index 000000000..8232aea7f
--- /dev/null
+++ b/weed/s3api/s3_sse_s3_integration_test.go
@@ -0,0 +1,325 @@
+package s3api
+
+import (
+ "bytes"
+ "io"
+ "testing"
+
+ "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
+ "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
+)
+
+// NOTE: These are integration tests that test the end-to-end encryption/decryption flow.
+// Full HTTP handler tests (PUT -> GET) would require a complete mock server with filer,
+// which is complex to set up. These tests focus on the critical decrypt path.
+
+// TestSSES3EndToEndSmallFile tests the complete encryption->storage->decryption cycle for small inline files
+// This test would have caught the IV retrieval bug for inline files
+func TestSSES3EndToEndSmallFile(t *testing.T) {
+ // Initialize global SSE-S3 key manager
+ globalSSES3KeyManager = NewSSES3KeyManager()
+ defer func() {
+ globalSSES3KeyManager = NewSSES3KeyManager()
+ }()
+
+ // Set up the key manager with a super key for testing
+ keyManager := GetSSES3KeyManager()
+ keyManager.superKey = make([]byte, 32)
+ for i := range keyManager.superKey {
+ keyManager.superKey[i] = byte(i)
+ }
+
+ testCases := []struct {
+ name string
+ data []byte
+ }{
+ {"tiny file (10 bytes)", []byte("test12345")},
+ {"small file (50 bytes)", []byte("This is a small test file for SSE-S3 encryption")},
+ {"medium file (256 bytes)", bytes.Repeat([]byte("a"), 256)},
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ // Step 1: Encrypt (simulates what happens during PUT)
+ sseS3Key, err := GenerateSSES3Key()
+ if err != nil {
+ t.Fatalf("Failed to generate SSE-S3 key: %v", err)
+ }
+
+ encryptedReader, iv, err := CreateSSES3EncryptedReader(bytes.NewReader(tc.data), sseS3Key)
+ 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)
+ }
+
+ // Store IV in the key (this is critical for inline files!)
+ sseS3Key.IV = iv
+
+ // Serialize the metadata (this is stored in entry.Extended)
+ serializedMetadata, err := SerializeSSES3Metadata(sseS3Key)
+ if err != nil {
+ t.Fatalf("Failed to serialize SSE-S3 metadata: %v", err)
+ }
+
+ // Step 2: Simulate storage (inline file - no chunks)
+ // For inline files, data is in Content, metadata in Extended
+ mockEntry := &filer_pb.Entry{
+ Extended: map[string][]byte{
+ s3_constants.SeaweedFSSSES3Key: serializedMetadata,
+ s3_constants.AmzServerSideEncryption: []byte("AES256"),
+ },
+ Content: encryptedData,
+ Chunks: []*filer_pb.FileChunk{}, // Critical: inline files have NO chunks
+ }
+
+ // Step 3: Decrypt (simulates what happens during GET)
+ // This tests the IV retrieval path for inline files
+
+ // First, deserialize metadata from storage
+ retrievedKeyData := mockEntry.Extended[s3_constants.SeaweedFSSSES3Key]
+ retrievedKey, err := DeserializeSSES3Metadata(retrievedKeyData, keyManager)
+ if err != nil {
+ t.Fatalf("Failed to deserialize SSE-S3 metadata: %v", err)
+ }
+
+ // CRITICAL TEST: For inline files, IV must be in object-level metadata
+ var retrievedIV []byte
+ if len(retrievedKey.IV) > 0 {
+ // Success path: IV found in object-level key
+ retrievedIV = retrievedKey.IV
+ } else if len(mockEntry.GetChunks()) > 0 {
+ // Fallback path: would check chunks (but inline files have no chunks)
+ t.Fatal("Inline file should have IV in object-level metadata, not chunks")
+ }
+
+ if len(retrievedIV) == 0 {
+ // THIS IS THE BUG WE FIXED: inline files had no way to get IV!
+ t.Fatal("Failed to retrieve IV for inline file - this is the bug we fixed!")
+ }
+
+ // Now decrypt with the retrieved IV
+ decryptedReader, err := CreateSSES3DecryptedReader(bytes.NewReader(encryptedData), retrievedKey, retrievedIV)
+ if err != nil {
+ t.Fatalf("Failed to create decrypted reader: %v", err)
+ }
+
+ decryptedData, err := io.ReadAll(decryptedReader)
+ if err != nil {
+ t.Fatalf("Failed to read decrypted data: %v", err)
+ }
+
+ // Verify decrypted data matches original
+ if !bytes.Equal(decryptedData, tc.data) {
+ t.Errorf("Decrypted data doesn't match original.\nExpected: %q\nGot: %q", tc.data, decryptedData)
+ }
+ })
+ }
+}
+
+// TestSSES3EndToEndChunkedFile tests the complete flow for chunked files
+func TestSSES3EndToEndChunkedFile(t *testing.T) {
+ // Initialize global SSE-S3 key manager
+ globalSSES3KeyManager = NewSSES3KeyManager()
+ defer func() {
+ globalSSES3KeyManager = NewSSES3KeyManager()
+ }()
+
+ keyManager := GetSSES3KeyManager()
+ keyManager.superKey = make([]byte, 32)
+ for i := range keyManager.superKey {
+ keyManager.superKey[i] = byte(i)
+ }
+
+ // Generate SSE-S3 key
+ sseS3Key, err := GenerateSSES3Key()
+ if err != nil {
+ t.Fatalf("Failed to generate SSE-S3 key: %v", err)
+ }
+
+ // Create test data for two chunks
+ chunk1Data := []byte("This is chunk 1 data for SSE-S3 encryption test")
+ chunk2Data := []byte("This is chunk 2 data for SSE-S3 encryption test")
+
+ // Encrypt chunk 1
+ encryptedReader1, iv1, err := CreateSSES3EncryptedReader(bytes.NewReader(chunk1Data), sseS3Key)
+ if err != nil {
+ t.Fatalf("Failed to create encrypted reader for chunk 1: %v", err)
+ }
+ encryptedChunk1, _ := io.ReadAll(encryptedReader1)
+
+ // Encrypt chunk 2
+ encryptedReader2, iv2, err := CreateSSES3EncryptedReader(bytes.NewReader(chunk2Data), sseS3Key)
+ if err != nil {
+ t.Fatalf("Failed to create encrypted reader for chunk 2: %v", err)
+ }
+ encryptedChunk2, _ := io.ReadAll(encryptedReader2)
+
+ // Create metadata for each chunk
+ chunk1Key := &SSES3Key{
+ Key: sseS3Key.Key,
+ IV: iv1,
+ Algorithm: sseS3Key.Algorithm,
+ KeyID: sseS3Key.KeyID,
+ }
+ chunk2Key := &SSES3Key{
+ Key: sseS3Key.Key,
+ IV: iv2,
+ Algorithm: sseS3Key.Algorithm,
+ KeyID: sseS3Key.KeyID,
+ }
+
+ serializedChunk1Meta, _ := SerializeSSES3Metadata(chunk1Key)
+ serializedChunk2Meta, _ := SerializeSSES3Metadata(chunk2Key)
+ serializedObjMeta, _ := SerializeSSES3Metadata(sseS3Key)
+
+ // Create mock entry with chunks
+ mockEntry := &filer_pb.Entry{
+ Extended: map[string][]byte{
+ s3_constants.SeaweedFSSSES3Key: serializedObjMeta,
+ s3_constants.AmzServerSideEncryption: []byte("AES256"),
+ },
+ Chunks: []*filer_pb.FileChunk{
+ {
+ FileId: "chunk1,123",
+ Offset: 0,
+ Size: uint64(len(encryptedChunk1)),
+ SseType: filer_pb.SSEType_SSE_S3,
+ SseMetadata: serializedChunk1Meta,
+ },
+ {
+ FileId: "chunk2,456",
+ Offset: int64(len(chunk1Data)),
+ Size: uint64(len(encryptedChunk2)),
+ SseType: filer_pb.SSEType_SSE_S3,
+ SseMetadata: serializedChunk2Meta,
+ },
+ },
+ }
+
+ // Verify multipart detection
+ sses3Chunks := 0
+ for _, chunk := range mockEntry.GetChunks() {
+ if chunk.GetSseType() == filer_pb.SSEType_SSE_S3 && len(chunk.GetSseMetadata()) > 0 {
+ sses3Chunks++
+ }
+ }
+
+ isMultipart := sses3Chunks > 1
+ if !isMultipart {
+ t.Error("Expected multipart SSE-S3 object detection")
+ }
+
+ if sses3Chunks != 2 {
+ t.Errorf("Expected 2 SSE-S3 chunks, got %d", sses3Chunks)
+ }
+
+ // Verify each chunk has valid metadata with IV
+ for i, chunk := range mockEntry.GetChunks() {
+ deserializedKey, err := DeserializeSSES3Metadata(chunk.GetSseMetadata(), keyManager)
+ if err != nil {
+ t.Errorf("Failed to deserialize chunk %d metadata: %v", i, err)
+ }
+ if len(deserializedKey.IV) == 0 {
+ t.Errorf("Chunk %d has no IV", i)
+ }
+
+ // Decrypt this chunk to verify it works
+ var chunkData []byte
+ if i == 0 {
+ chunkData = encryptedChunk1
+ } else {
+ chunkData = encryptedChunk2
+ }
+
+ decryptedReader, err := CreateSSES3DecryptedReader(bytes.NewReader(chunkData), deserializedKey, deserializedKey.IV)
+ if err != nil {
+ t.Errorf("Failed to decrypt chunk %d: %v", i, err)
+ continue
+ }
+
+ decrypted, _ := io.ReadAll(decryptedReader)
+ var expectedData []byte
+ if i == 0 {
+ expectedData = chunk1Data
+ } else {
+ expectedData = chunk2Data
+ }
+
+ if !bytes.Equal(decrypted, expectedData) {
+ t.Errorf("Chunk %d decryption failed", i)
+ }
+ }
+}
+
+// TestSSES3EndToEndWithDetectPrimaryType tests that type detection works correctly for different scenarios
+func TestSSES3EndToEndWithDetectPrimaryType(t *testing.T) {
+ s3a := &S3ApiServer{}
+
+ testCases := []struct {
+ name string
+ entry *filer_pb.Entry
+ expectedType string
+ shouldBeSSES3 bool
+ }{
+ {
+ name: "Inline SSE-S3 file (no chunks)",
+ entry: &filer_pb.Entry{
+ Extended: map[string][]byte{
+ s3_constants.AmzServerSideEncryption: []byte("AES256"),
+ },
+ Attributes: &filer_pb.FuseAttributes{},
+ Content: []byte("encrypted data"),
+ Chunks: []*filer_pb.FileChunk{},
+ },
+ expectedType: s3_constants.SSETypeS3,
+ shouldBeSSES3: true,
+ },
+ {
+ name: "Single chunk SSE-S3 file",
+ entry: &filer_pb.Entry{
+ Extended: map[string][]byte{
+ s3_constants.AmzServerSideEncryption: []byte("AES256"),
+ },
+ Attributes: &filer_pb.FuseAttributes{},
+ Chunks: []*filer_pb.FileChunk{
+ {
+ FileId: "1,123",
+ SseType: filer_pb.SSEType_SSE_S3,
+ SseMetadata: []byte("metadata"),
+ },
+ },
+ },
+ expectedType: s3_constants.SSETypeS3,
+ shouldBeSSES3: true,
+ },
+ {
+ name: "SSE-KMS file (has KMS key ID)",
+ entry: &filer_pb.Entry{
+ Extended: map[string][]byte{
+ s3_constants.AmzServerSideEncryption: []byte("AES256"),
+ s3_constants.AmzServerSideEncryptionAwsKmsKeyId: []byte("kms-key-123"),
+ },
+ Attributes: &filer_pb.FuseAttributes{},
+ Chunks: []*filer_pb.FileChunk{},
+ },
+ expectedType: s3_constants.SSETypeKMS,
+ shouldBeSSES3: false,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ detectedType := s3a.detectPrimarySSEType(tc.entry)
+ if detectedType != tc.expectedType {
+ t.Errorf("Expected type %s, got %s", tc.expectedType, detectedType)
+ }
+ if (detectedType == s3_constants.SSETypeS3) != tc.shouldBeSSES3 {
+ t.Errorf("SSE-S3 detection mismatch: expected %v, got %v", tc.shouldBeSSES3, detectedType == s3_constants.SSETypeS3)
+ }
+ })
+ }
+}