Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
575 lines
18 KiB
Go
575 lines
18 KiB
Go
package s3api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// initSSES3KeyManagerForTest resets the global SSE-S3 key manager and seeds it
|
|
// with a deterministic super key suitable for envelope-encrypt/decrypt cycles
|
|
// inside tests. Returns the freshly initialized manager.
|
|
func initSSES3KeyManagerForTest(t *testing.T) *SSES3KeyManager {
|
|
t.Helper()
|
|
globalSSES3KeyManager = NewSSES3KeyManager()
|
|
t.Cleanup(func() { globalSSES3KeyManager = NewSSES3KeyManager() })
|
|
km := GetSSES3KeyManager()
|
|
km.superKey = make([]byte, 32)
|
|
for i := range km.superKey {
|
|
km.superKey[i] = byte(i)
|
|
}
|
|
return km
|
|
}
|
|
|
|
// encryptSSES3Part encrypts data with a freshly generated DEK and returns the
|
|
// ciphertext plus serialized per-chunk metadata (DEK + IV). Mirrors what the
|
|
// multipart upload path writes into each part's chunk metadata.
|
|
func encryptSSES3Part(t *testing.T, data []byte) (ciphertext, metadata []byte) {
|
|
t.Helper()
|
|
key, err := GenerateSSES3Key()
|
|
if err != nil {
|
|
t.Fatalf("GenerateSSES3Key: %v", err)
|
|
}
|
|
encReader, iv, err := CreateSSES3EncryptedReader(bytes.NewReader(data), key)
|
|
if err != nil {
|
|
t.Fatalf("CreateSSES3EncryptedReader: %v", err)
|
|
}
|
|
ciphertext, err = io.ReadAll(encReader)
|
|
if err != nil {
|
|
t.Fatalf("ReadAll ciphertext: %v", err)
|
|
}
|
|
key.IV = iv
|
|
metadata, err = SerializeSSES3Metadata(key)
|
|
if err != nil {
|
|
t.Fatalf("SerializeSSES3Metadata: %v", err)
|
|
}
|
|
return ciphertext, metadata
|
|
}
|
|
|
|
// TestBuildMultipartSSES3Reader_PerChunkKeys locks in the fix from PR #9211:
|
|
// each multipart part has its own DEK and IV, and the direct multipart reader
|
|
// must decrypt each chunk with its own per-chunk metadata (not the entry-level
|
|
// key). Before the fix, using a shared entry key produced garbled output.
|
|
func TestBuildMultipartSSES3Reader_PerChunkKeys(t *testing.T) {
|
|
keyManager := initSSES3KeyManagerForTest(t)
|
|
|
|
// Two parts with distinct sizes (including a short final part) and
|
|
// independent DEKs/IVs to exercise the per-chunk key plumbing.
|
|
part1Plaintext := bytes.Repeat([]byte("ABCDEFGHIJKLMNOP"), 16) // 256 bytes
|
|
part2Plaintext := []byte("short tail part") // 15 bytes
|
|
|
|
cipher1, meta1 := encryptSSES3Part(t, part1Plaintext)
|
|
cipher2, meta2 := encryptSSES3Part(t, part2Plaintext)
|
|
|
|
chunks := []*filer_pb.FileChunk{
|
|
{
|
|
FileId: "1,aaa",
|
|
Offset: 0,
|
|
Size: uint64(len(cipher1)),
|
|
SseType: filer_pb.SSEType_SSE_S3,
|
|
SseMetadata: meta1,
|
|
},
|
|
{
|
|
FileId: "2,bbb",
|
|
Offset: int64(len(part1Plaintext)),
|
|
Size: uint64(len(cipher2)),
|
|
SseType: filer_pb.SSEType_SSE_S3,
|
|
SseMetadata: meta2,
|
|
},
|
|
}
|
|
// Pass chunks out of order to verify offset-based sort.
|
|
shuffled := []*filer_pb.FileChunk{chunks[1], chunks[0]}
|
|
// Snapshot the input ordering; the helper must not mutate the caller's
|
|
// slice, which is backed by entry.Chunks and relied on elsewhere (e.g.
|
|
// ETag computation).
|
|
shuffledOrderBefore := []*filer_pb.FileChunk{shuffled[0], shuffled[1]}
|
|
|
|
fetched := map[string]int{}
|
|
chunkData := map[string][]byte{
|
|
"1,aaa": cipher1,
|
|
"2,bbb": cipher2,
|
|
}
|
|
fetch := func(c *filer_pb.FileChunk) (io.ReadCloser, error) {
|
|
fetched[c.GetFileIdString()]++
|
|
data, ok := chunkData[c.GetFileIdString()]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unexpected chunk %s", c.GetFileIdString())
|
|
}
|
|
return io.NopCloser(bytes.NewReader(data)), nil
|
|
}
|
|
|
|
reader, err := buildMultipartSSES3Reader(shuffled, keyManager, fetch)
|
|
if err != nil {
|
|
t.Fatalf("buildMultipartSSES3Reader: %v", err)
|
|
}
|
|
|
|
got, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
t.Fatalf("ReadAll reader: %v", err)
|
|
}
|
|
|
|
want := append(append([]byte{}, part1Plaintext...), part2Plaintext...)
|
|
if !bytes.Equal(got, want) {
|
|
t.Fatalf("decrypted output mismatch\n want (len=%d): %q\n got (len=%d): %q",
|
|
len(want), want, len(got), got)
|
|
}
|
|
if fetched["1,aaa"] != 1 || fetched["2,bbb"] != 1 {
|
|
t.Errorf("expected each chunk fetched once, got %v", fetched)
|
|
}
|
|
for i := range shuffledOrderBefore {
|
|
if shuffled[i] != shuffledOrderBefore[i] {
|
|
t.Errorf("caller's chunk slice was reordered at index %d: before=%s after=%s",
|
|
i, shuffledOrderBefore[i].GetFileIdString(), shuffled[i].GetFileIdString())
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestBuildMultipartSSES3Reader_InvalidIVLength verifies that per-chunk metadata
|
|
// with a missing or short IV is rejected with a clear error instead of
|
|
// panicking inside cipher.NewCTR. The short-IV case is crafted by encoding the
|
|
// metadata JSON directly, since SerializeSSES3Metadata refuses to emit a bad
|
|
// IV; this simulates corrupted or legacy on-disk metadata.
|
|
func TestBuildMultipartSSES3Reader_InvalidIVLength(t *testing.T) {
|
|
keyManager := initSSES3KeyManagerForTest(t)
|
|
|
|
// Pre-encrypt the DEK with the current super key so Deserialize can unwrap
|
|
// it successfully and we reach the IV-length check.
|
|
dek := bytes.Repeat([]byte{0x42}, s3_constants.AESKeySize)
|
|
encryptedDEK, nonce, err := keyManager.encryptKeyWithSuperKey(dek)
|
|
if err != nil {
|
|
t.Fatalf("encryptKeyWithSuperKey: %v", err)
|
|
}
|
|
|
|
makeMetadata := func(iv []byte) []byte {
|
|
meta := map[string]string{
|
|
"algorithm": s3_constants.SSEAlgorithmAES256,
|
|
"keyId": "test-key",
|
|
"encryptedDEK": base64.StdEncoding.EncodeToString(encryptedDEK),
|
|
"nonce": base64.StdEncoding.EncodeToString(nonce),
|
|
}
|
|
if iv != nil {
|
|
meta["iv"] = base64.StdEncoding.EncodeToString(iv)
|
|
}
|
|
out, err := json.Marshal(meta)
|
|
if err != nil {
|
|
t.Fatalf("marshal metadata: %v", err)
|
|
}
|
|
return out
|
|
}
|
|
|
|
cases := []struct {
|
|
name string
|
|
iv []byte
|
|
}{
|
|
{"missing IV", nil},
|
|
{"short IV", []byte("too-short")}, // 9 bytes, not 16
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
fetchCalled := false
|
|
fetch := func(c *filer_pb.FileChunk) (io.ReadCloser, error) {
|
|
fetchCalled = true
|
|
return io.NopCloser(bytes.NewReader([]byte("whatever"))), nil
|
|
}
|
|
|
|
chunks := []*filer_pb.FileChunk{
|
|
{
|
|
FileId: "1,bad",
|
|
Offset: 0,
|
|
Size: 8,
|
|
SseType: filer_pb.SSEType_SSE_S3,
|
|
SseMetadata: makeMetadata(tc.iv),
|
|
},
|
|
}
|
|
|
|
_, err := buildMultipartSSES3Reader(chunks, keyManager, fetch)
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid IV length, got nil")
|
|
}
|
|
// ValidateIV's error format is "invalid <name> length: ...";
|
|
// match on the part of the message that's stable across the
|
|
// shared helper's wording.
|
|
if !strings.Contains(err.Error(), "IV length") {
|
|
t.Errorf("expected 'IV length' in error, got: %v", err)
|
|
}
|
|
// Validation runs upfront before any chunk fetch, so no volume-server
|
|
// HTTP connection should have been opened on the failure path.
|
|
if fetchCalled {
|
|
t.Error("fetchChunk was called for an invalid-IV chunk; metadata validation should fail before any fetch")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestBuildMultipartSSES3Reader_RejectsBadChunkBeforeAnyFetch verifies that
|
|
// when any chunk's metadata is malformed, the helper returns an error WITHOUT
|
|
// having opened a volume-server HTTP connection for any chunk. Per-chunk
|
|
// metadata is validated upfront precisely so a bad chunk in position N does
|
|
// not leak open HTTP responses for chunks 0..N-1 (the original eager
|
|
// implementation depended on a closeAppendedReaders cleanup path; this test
|
|
// pins the stronger contract: nothing is opened in the first place).
|
|
func TestBuildMultipartSSES3Reader_RejectsBadChunkBeforeAnyFetch(t *testing.T) {
|
|
keyManager := initSSES3KeyManagerForTest(t)
|
|
|
|
// First chunk: valid SSE-S3 chunk.
|
|
cipher1, meta1 := encryptSSES3Part(t, []byte("first chunk plaintext"))
|
|
|
|
// Second chunk: missing per-chunk metadata, triggers error.
|
|
chunks := []*filer_pb.FileChunk{
|
|
{
|
|
FileId: "1,good",
|
|
Offset: 0,
|
|
Size: uint64(len(cipher1)),
|
|
SseType: filer_pb.SSEType_SSE_S3,
|
|
SseMetadata: meta1,
|
|
},
|
|
{
|
|
FileId: "2,bad",
|
|
Offset: int64(len(cipher1)),
|
|
Size: 1,
|
|
SseType: filer_pb.SSEType_SSE_S3,
|
|
SseMetadata: nil, // triggers "missing per-chunk metadata"
|
|
},
|
|
}
|
|
|
|
fetched := map[string]int{}
|
|
fetch := func(c *filer_pb.FileChunk) (io.ReadCloser, error) {
|
|
fetched[c.GetFileIdString()]++
|
|
return io.NopCloser(bytes.NewReader([]byte("x"))), nil
|
|
}
|
|
|
|
_, err := buildMultipartSSES3Reader(chunks, keyManager, fetch)
|
|
if err == nil {
|
|
t.Fatal("expected error from missing chunk metadata, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "missing per-chunk metadata") {
|
|
t.Errorf("expected 'missing per-chunk metadata' in error, got: %v", err)
|
|
}
|
|
if len(fetched) != 0 {
|
|
t.Errorf("expected no chunks fetched on validation failure, got %v", fetched)
|
|
}
|
|
}
|