You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							325 lines
						
					
					
						
							9.8 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							325 lines
						
					
					
						
							9.8 KiB
						
					
					
				
								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)
							 | 
						|
											}
							 | 
						|
										})
							 | 
						|
									}
							 | 
						|
								}
							 |