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