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.
399 lines
10 KiB
399 lines
10 KiB
package s3api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/kms"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
|
)
|
|
|
|
func TestSSEKMSEncryptionDecryption(t *testing.T) {
|
|
kmsKey := SetupTestKMS(t)
|
|
defer kmsKey.Cleanup()
|
|
|
|
// Test data
|
|
testData := "Hello, SSE-KMS world! This is a test of envelope encryption."
|
|
testReader := strings.NewReader(testData)
|
|
|
|
// Create encryption context
|
|
encryptionContext := BuildEncryptionContext("test-bucket", "test-object", false)
|
|
|
|
// Encrypt the data
|
|
encryptedReader, sseKey, err := CreateSSEKMSEncryptedReader(testReader, kmsKey.KeyID, encryptionContext)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create encrypted reader: %v", err)
|
|
}
|
|
|
|
// Verify SSE key metadata
|
|
if sseKey.KeyID != kmsKey.KeyID {
|
|
t.Errorf("Expected key ID %s, got %s", kmsKey.KeyID, sseKey.KeyID)
|
|
}
|
|
|
|
if len(sseKey.EncryptedDataKey) == 0 {
|
|
t.Error("Encrypted data key should not be empty")
|
|
}
|
|
|
|
if sseKey.EncryptionContext == nil {
|
|
t.Error("Encryption context should not be nil")
|
|
}
|
|
|
|
// Read the encrypted data
|
|
encryptedData, err := io.ReadAll(encryptedReader)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read encrypted data: %v", err)
|
|
}
|
|
|
|
// Verify the encrypted data is different from original
|
|
if string(encryptedData) == testData {
|
|
t.Error("Encrypted data should be different from original data")
|
|
}
|
|
|
|
// The encrypted data should be same size as original (IV is stored in metadata, not in stream)
|
|
if len(encryptedData) != len(testData) {
|
|
t.Errorf("Encrypted data should be same size as original: expected %d, got %d", len(testData), len(encryptedData))
|
|
}
|
|
|
|
// Decrypt the data
|
|
decryptedReader, err := CreateSSEKMSDecryptedReader(bytes.NewReader(encryptedData), sseKey)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create decrypted reader: %v", err)
|
|
}
|
|
|
|
// Read the decrypted data
|
|
decryptedData, err := io.ReadAll(decryptedReader)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read decrypted data: %v", err)
|
|
}
|
|
|
|
// Verify the decrypted data matches the original
|
|
if string(decryptedData) != testData {
|
|
t.Errorf("Decrypted data does not match original.\nExpected: %s\nGot: %s", testData, string(decryptedData))
|
|
}
|
|
}
|
|
|
|
func TestSSEKMSKeyValidation(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
keyID string
|
|
wantValid bool
|
|
}{
|
|
{
|
|
name: "Valid UUID key ID",
|
|
keyID: "12345678-1234-1234-1234-123456789012",
|
|
wantValid: true,
|
|
},
|
|
{
|
|
name: "Valid alias",
|
|
keyID: "alias/my-test-key",
|
|
wantValid: true,
|
|
},
|
|
{
|
|
name: "Valid ARN",
|
|
keyID: "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012",
|
|
wantValid: true,
|
|
},
|
|
{
|
|
name: "Valid alias ARN",
|
|
keyID: "arn:aws:kms:us-east-1:123456789012:alias/my-test-key",
|
|
wantValid: true,
|
|
},
|
|
|
|
{
|
|
name: "Valid test key format",
|
|
keyID: "invalid-key-format",
|
|
wantValid: true, // Now valid - following Minio's permissive approach
|
|
},
|
|
{
|
|
name: "Valid short key",
|
|
keyID: "12345678-1234",
|
|
wantValid: true, // Now valid - following Minio's permissive approach
|
|
},
|
|
{
|
|
name: "Invalid - leading space",
|
|
keyID: " leading-space",
|
|
wantValid: false,
|
|
},
|
|
{
|
|
name: "Invalid - trailing space",
|
|
keyID: "trailing-space ",
|
|
wantValid: false,
|
|
},
|
|
{
|
|
name: "Invalid - empty",
|
|
keyID: "",
|
|
wantValid: false,
|
|
},
|
|
{
|
|
name: "Invalid - internal spaces",
|
|
keyID: "invalid key id",
|
|
wantValid: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
valid := isValidKMSKeyID(tt.keyID)
|
|
if valid != tt.wantValid {
|
|
t.Errorf("isValidKMSKeyID(%s) = %v, want %v", tt.keyID, valid, tt.wantValid)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSSEKMSMetadataSerialization(t *testing.T) {
|
|
// Create test SSE key
|
|
sseKey := &SSEKMSKey{
|
|
KeyID: "test-key-id",
|
|
EncryptedDataKey: []byte("encrypted-data-key"),
|
|
EncryptionContext: map[string]string{
|
|
"aws:s3:arn": "arn:aws:s3:::test-bucket/test-object",
|
|
},
|
|
BucketKeyEnabled: true,
|
|
}
|
|
|
|
// Serialize metadata
|
|
serialized, err := SerializeSSEKMSMetadata(sseKey)
|
|
if err != nil {
|
|
t.Fatalf("Failed to serialize SSE-KMS metadata: %v", err)
|
|
}
|
|
|
|
// Verify it's valid JSON
|
|
var jsonData map[string]interface{}
|
|
if err := json.Unmarshal(serialized, &jsonData); err != nil {
|
|
t.Fatalf("Serialized data is not valid JSON: %v", err)
|
|
}
|
|
|
|
// Deserialize metadata
|
|
deserializedKey, err := DeserializeSSEKMSMetadata(serialized)
|
|
if err != nil {
|
|
t.Fatalf("Failed to deserialize SSE-KMS metadata: %v", err)
|
|
}
|
|
|
|
// Verify the deserialized data matches original
|
|
if deserializedKey.KeyID != sseKey.KeyID {
|
|
t.Errorf("KeyID mismatch: expected %s, got %s", sseKey.KeyID, deserializedKey.KeyID)
|
|
}
|
|
|
|
if !bytes.Equal(deserializedKey.EncryptedDataKey, sseKey.EncryptedDataKey) {
|
|
t.Error("EncryptedDataKey mismatch")
|
|
}
|
|
|
|
if len(deserializedKey.EncryptionContext) != len(sseKey.EncryptionContext) {
|
|
t.Error("EncryptionContext length mismatch")
|
|
}
|
|
|
|
for k, v := range sseKey.EncryptionContext {
|
|
if deserializedKey.EncryptionContext[k] != v {
|
|
t.Errorf("EncryptionContext mismatch for key %s: expected %s, got %s", k, v, deserializedKey.EncryptionContext[k])
|
|
}
|
|
}
|
|
|
|
if deserializedKey.BucketKeyEnabled != sseKey.BucketKeyEnabled {
|
|
t.Errorf("BucketKeyEnabled mismatch: expected %v, got %v", sseKey.BucketKeyEnabled, deserializedKey.BucketKeyEnabled)
|
|
}
|
|
}
|
|
|
|
func TestBuildEncryptionContext(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
bucket string
|
|
object string
|
|
useBucketKey bool
|
|
expectedARN string
|
|
}{
|
|
{
|
|
name: "Object-level encryption",
|
|
bucket: "test-bucket",
|
|
object: "test-object",
|
|
useBucketKey: false,
|
|
expectedARN: "arn:aws:s3:::test-bucket/test-object",
|
|
},
|
|
{
|
|
name: "Bucket-level encryption",
|
|
bucket: "test-bucket",
|
|
object: "test-object",
|
|
useBucketKey: true,
|
|
expectedARN: "arn:aws:s3:::test-bucket",
|
|
},
|
|
{
|
|
name: "Nested object path",
|
|
bucket: "my-bucket",
|
|
object: "folder/subfolder/file.txt",
|
|
useBucketKey: false,
|
|
expectedARN: "arn:aws:s3:::my-bucket/folder/subfolder/file.txt",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
context := BuildEncryptionContext(tt.bucket, tt.object, tt.useBucketKey)
|
|
|
|
if context == nil {
|
|
t.Fatal("Encryption context should not be nil")
|
|
}
|
|
|
|
arn, exists := context[kms.EncryptionContextS3ARN]
|
|
if !exists {
|
|
t.Error("Encryption context should contain S3 ARN")
|
|
}
|
|
|
|
if arn != tt.expectedARN {
|
|
t.Errorf("Expected ARN %s, got %s", tt.expectedARN, arn)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestKMSErrorMapping(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
kmsError *kms.KMSError
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "Key not found",
|
|
kmsError: &kms.KMSError{
|
|
Code: kms.ErrCodeNotFoundException,
|
|
Message: "Key not found",
|
|
},
|
|
expectedErr: "KMSKeyNotFoundException",
|
|
},
|
|
{
|
|
name: "Access denied",
|
|
kmsError: &kms.KMSError{
|
|
Code: kms.ErrCodeAccessDenied,
|
|
Message: "Access denied",
|
|
},
|
|
expectedErr: "KMSAccessDeniedException",
|
|
},
|
|
{
|
|
name: "Key unavailable",
|
|
kmsError: &kms.KMSError{
|
|
Code: kms.ErrCodeKeyUnavailable,
|
|
Message: "Key is disabled",
|
|
},
|
|
expectedErr: "KMSKeyDisabledException",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
errorCode := MapKMSErrorToS3Error(tt.kmsError)
|
|
|
|
// Get the actual error description
|
|
apiError := s3err.GetAPIError(errorCode)
|
|
if apiError.Code != tt.expectedErr {
|
|
t.Errorf("Expected error code %s, got %s", tt.expectedErr, apiError.Code)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestLargeDataEncryption tests encryption/decryption of larger data streams
|
|
func TestSSEKMSLargeDataEncryption(t *testing.T) {
|
|
kmsKey := SetupTestKMS(t)
|
|
defer kmsKey.Cleanup()
|
|
|
|
// Create a larger test dataset (1MB)
|
|
testData := strings.Repeat("This is a test of SSE-KMS with larger data streams. ", 20000)
|
|
testReader := strings.NewReader(testData)
|
|
|
|
// Create encryption context
|
|
encryptionContext := BuildEncryptionContext("large-bucket", "large-object", false)
|
|
|
|
// Encrypt the data
|
|
encryptedReader, sseKey, err := CreateSSEKMSEncryptedReader(testReader, kmsKey.KeyID, encryptionContext)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create encrypted reader: %v", err)
|
|
}
|
|
|
|
// Read the encrypted data
|
|
encryptedData, err := io.ReadAll(encryptedReader)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read encrypted data: %v", err)
|
|
}
|
|
|
|
// Decrypt the data
|
|
decryptedReader, err := CreateSSEKMSDecryptedReader(bytes.NewReader(encryptedData), sseKey)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create decrypted reader: %v", err)
|
|
}
|
|
|
|
// Read the decrypted data
|
|
decryptedData, err := io.ReadAll(decryptedReader)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read decrypted data: %v", err)
|
|
}
|
|
|
|
// Verify the decrypted data matches the original
|
|
if string(decryptedData) != testData {
|
|
t.Errorf("Decrypted data length: %d, original data length: %d", len(decryptedData), len(testData))
|
|
t.Error("Decrypted large data does not match original")
|
|
}
|
|
|
|
t.Logf("Successfully encrypted/decrypted %d bytes of data", len(testData))
|
|
}
|
|
|
|
// TestValidateSSEKMSKey tests the ValidateSSEKMSKey function, which correctly handles empty key IDs
|
|
func TestValidateSSEKMSKey(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
sseKey *SSEKMSKey
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "nil SSE-KMS key",
|
|
sseKey: nil,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "empty key ID (valid - represents default KMS key)",
|
|
sseKey: &SSEKMSKey{
|
|
KeyID: "",
|
|
EncryptionContext: map[string]string{"test": "value"},
|
|
BucketKeyEnabled: false,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid UUID key ID",
|
|
sseKey: &SSEKMSKey{
|
|
KeyID: "12345678-1234-1234-1234-123456789012",
|
|
EncryptionContext: map[string]string{"test": "value"},
|
|
BucketKeyEnabled: true,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid alias",
|
|
sseKey: &SSEKMSKey{
|
|
KeyID: "alias/my-test-key",
|
|
EncryptionContext: map[string]string{},
|
|
BucketKeyEnabled: false,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid flexible key ID format",
|
|
sseKey: &SSEKMSKey{
|
|
KeyID: "invalid-format",
|
|
EncryptionContext: map[string]string{},
|
|
BucketKeyEnabled: false,
|
|
},
|
|
wantErr: false, // Now valid - following Minio's permissive approach
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := ValidateSSEKMSKey(tt.sseKey)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("ValidateSSEKMSKey() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|