* S3 API: Fix SSE-S3 decryption on object download Fixes #7363 This commit adds missing SSE-S3 decryption support when downloading objects from SSE-S3 encrypted buckets. Previously, SSE-S3 encrypted objects were returned in their encrypted form, causing data corruption and hash mismatches. Changes: - Updated detectPrimarySSEType() to detect SSE-S3 encrypted objects by examining chunk metadata and distinguishing SSE-S3 from SSE-KMS - Added SSE-S3 handling in handleSSEResponse() to route to new handler - Implemented handleSSES3Response() for both single-part and multipart SSE-S3 encrypted objects with proper decryption - Implemented createMultipartSSES3DecryptedReader() for multipart objects with per-chunk decryption using stored IVs - Updated addSSEHeadersToResponse() to include SSE-S3 response headers The fix follows the existing SSE-C and SSE-KMS patterns, using the envelope encryption architecture where each object's DEK is encrypted with the KEK stored in the filer. * Add comprehensive tests for SSE-S3 decryption - TestSSES3EncryptionDecryption: basic encryption/decryption - TestSSES3IsRequestInternal: request detection - TestSSES3MetadataSerialization: metadata serialization/deserialization - TestDetectPrimarySSETypeS3: SSE type detection for various scenarios - TestAddSSES3HeadersToResponse: response header validation - TestSSES3EncryptionWithBaseIV: multipart encryption with base IV - TestSSES3WrongKeyDecryption: wrong key error handling - TestSSES3KeyGeneration: key generation and uniqueness - TestSSES3VariousSizes: encryption/decryption with various data sizes - TestSSES3ResponseHeaders: response header correctness - TestSSES3IsEncryptedInternal: metadata-based encryption detection - TestSSES3InvalidMetadataDeserialization: error handling for invalid metadata - TestGetSSES3Headers: header generation - TestProcessSSES3Request: request processing - TestGetSSES3KeyFromMetadata: key extraction from metadata - TestSSES3EnvelopeEncryption: envelope encryption correctness - TestValidateSSES3Key: key validation All tests pass successfully, providing comprehensive coverage for the SSE-S3 decryption fix. * Address PR review comments 1. Fix resource leak in createMultipartSSES3DecryptedReader: - Wrap decrypted reader with closer to properly release resources - Ensure underlying chunkReader is closed when done 2. Handle mixed-encryption objects correctly: - Check chunk encryption type before attempting decryption - Pass through non-SSE-S3 chunks unmodified - Log encryption type for debugging 3. Improve SSE type detection logic: - Add explicit case for aws:kms algorithm - Handle unknown algorithms gracefully - Better documentation for tie-breaking precedence 4. Document tie-breaking behavior: - Clarify that mixed encryption indicates potential corruption - Explicit precedence order: SSE-C > SSE-KMS > SSE-S3 These changes address high-severity resource management issues and improve robustness when handling edge cases and mixed-encryption scenarios. * Fix IV retrieval for small/inline SSE-S3 encrypted files Critical bug fix: The previous implementation only looked for the IV in chunk metadata, which would fail for small files stored inline (without chunks). Changes: - Check object-level metadata (sseS3Key.IV) first for inline files - Fallback to first chunk metadata only if object-level IV not found - Improved error message to indicate both locations were checked This ensures small SSE-S3 encrypted files (stored inline in entry.Content) can be properly decrypted, as their IV is stored in the object-level SeaweedFSSSES3Key metadata rather than in chunk metadata. Fixes the high-severity issue identified in PR review. * Clean up unused SSE metadata helper functions Remove legacy SSE metadata helper functions that were never fully implemented or used: Removed unused functions: - StoreSSECMetadata() / GetSSECMetadata() - StoreSSEKMSMetadata() / GetSSEKMSMetadata() - StoreSSES3Metadata() / GetSSES3Metadata() - IsSSEEncrypted() - GetSSEAlgorithm() Removed unused constants: - MetaSSEAlgorithm - MetaSSECKeyMD5 - MetaSSEKMSKeyID - MetaSSEKMSEncryptedKey - MetaSSEKMSContext - MetaSSES3KeyID These functions were from an earlier design where IV and other metadata would be stored in common entry.Extended keys. The actual implementations use type-specific serialization: - SSE-C: Uses StoreIVInMetadata()/GetIVFromMetadata() directly for IV - SSE-KMS: Serializes entire SSEKMSKey structure as JSON (includes IV) - SSE-S3: Serializes entire SSES3Key structure as JSON (includes IV) This follows Option A: SSE-S3 uses envelope encryption pattern like SSE-KMS, where IV is stored within the serialized key metadata rather than in a separate metadata field. Kept functions still in use: - StoreIVInMetadata() - Used by SSE-C - GetIVFromMetadata() - Used by SSE-C and streaming copy - MetaSSEIV constant - Used by SSE-C All tests pass after cleanup. * Rename SSE metadata functions to clarify SSE-C specific usage Renamed functions and constants to explicitly indicate they are SSE-C specific, improving code clarity: Renamed: - MetaSSEIV → MetaSSECIV - StoreIVInMetadata() → StoreSSECIVInMetadata() - GetIVFromMetadata() → GetSSECIVFromMetadata() Updated all usages across: - s3api_key_rotation.go - s3api_streaming_copy.go - s3api_object_handlers_copy.go - s3_sse_copy_test.go - s3_sse_test_utils_test.go Rationale: These functions are exclusively used by SSE-C for storing/retrieving the IV in entry.Extended metadata. SSE-KMS and SSE-S3 use different approaches (IV stored in serialized key structures), so the generic names were misleading. The new names make it clear these are part of the SSE-C implementation. All tests pass. * Add integration tests for SSE-S3 end-to-end encryption/decryption These integration tests cover the complete encrypt->store->decrypt cycle that was missing from the original test suite. They would have caught the IV retrieval bug for inline files. Tests added: - TestSSES3EndToEndSmallFile: Tests inline files (10, 50, 256 bytes) * Specifically tests the critical IV retrieval path for inline files * This test explicitly checks the bug we fixed where inline files couldn't retrieve their IV from object-level metadata - TestSSES3EndToEndChunkedFile: Tests multipart encrypted files * Verifies per-chunk metadata serialization/deserialization * Tests that each chunk can be independently decrypted with its own IV - TestSSES3EndToEndWithDetectPrimaryType: Tests type detection * Verifies inline vs chunked SSE-S3 detection * Ensures SSE-S3 is distinguished from SSE-KMS Note: Full HTTP handler tests (PUT -> GET through actual handlers) would require a complete mock server with filer connections, which is complex. These tests focus on the critical decrypt path and data flow. Why these tests are important: - Unit tests alone don't catch integration issues - The IV retrieval bug existed because there was no end-to-end test - These tests simulate the actual storage/retrieval flow - They verify the complete encryption architecture works correctly All tests pass. * Fix TestValidateSSES3Key expectations to match actual implementation The ValidateSSES3Key function only validates that the key struct is not nil, but doesn't validate the Key field contents or size. The test was expecting validation that doesn't exist. Updated test cases: - Nil key struct → should error (correct) - Valid key → should not error (correct) - Invalid key size → should not error (validation doesn't check this) - Nil key bytes → should not error (validation doesn't check this) Added comments to clarify what the current validation actually checks. This matches the behavior of ValidateSSEKMSKey and ValidateSSECKey which also only check for nil struct, not field contents. All SSE tests now pass. * Improve ValidateSSES3Key to properly validate key contents Enhanced the validation function from only checking nil struct to comprehensive validation of all key fields: Validations added: 1. Key bytes not nil 2. Key size exactly 32 bytes (SSES3KeySize) 3. Algorithm must be "AES256" (SSES3Algorithm) 4. Key ID must not be empty 5. IV length must be 16 bytes if set (optional - set during encryption) Test improvements (10 test cases): - Nil key struct - Valid key without IV - Valid key with IV - Invalid key size (too small) - Invalid key size (too large) - Nil key bytes - Empty key ID - Invalid algorithm - Invalid IV length - Empty IV (allowed - set during encryption) This matches the robustness of SSE-C and SSE-KMS validation and will catch configuration errors early rather than failing during encryption/decryption. All SSE tests pass. * Replace custom string helper functions with strings.Contains Address Gemini Code Assist review feedback: - Remove custom contains() and findSubstring() helper functions - Use standard library strings.Contains() instead - Add strings import This makes the code more idiomatic and easier to maintain by using the standard library instead of reimplementing functionality. Changes: - Added "strings" to imports - Replaced contains(err.Error(), tc.errorMsg) with strings.Contains(err.Error(), tc.errorMsg) - Removed 15 lines of custom helper code All tests pass. * filer fix reading and writing SSE-S3 headers * filter out seaweedfs internal headers * Update weed/s3api/s3api_object_handlers.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update weed/s3api/s3_validation_utils.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update s3api_streaming_copy.go * remove fallback * remove redundant check * refactor * remove extra object fetching * in case object is not found * Correct Version Entry for SSE Routing * Proper Error Handling for SSE Entry Fetching * Eliminated All Redundant Lookups * Removed brittle “exactly 5 successes/failures” assertions. Added invariant checks total recorded attempts equals request count, successes never exceed capacity, failures cover remaining attempts, final AvailableSpace matches capacity - successes. * refactor * fix test * Fixed Broken Fallback Logic * refactor * Better Error for Encryption Type Mismatch * refactor --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
540 lines
17 KiB
Go
540 lines
17 KiB
Go
package s3api
|
|
|
|
import (
|
|
"context"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
mathrand "math/rand"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
// SSE-S3 uses AES-256 encryption with server-managed keys
|
|
const (
|
|
SSES3Algorithm = s3_constants.SSEAlgorithmAES256
|
|
SSES3KeySize = 32 // 256 bits
|
|
)
|
|
|
|
// SSES3Key represents a server-managed encryption key for SSE-S3
|
|
type SSES3Key struct {
|
|
Key []byte
|
|
KeyID string
|
|
Algorithm string
|
|
IV []byte // Initialization Vector for this key
|
|
}
|
|
|
|
// IsSSES3RequestInternal checks if the request specifies SSE-S3 encryption
|
|
func IsSSES3RequestInternal(r *http.Request) bool {
|
|
sseHeader := r.Header.Get(s3_constants.AmzServerSideEncryption)
|
|
result := sseHeader == SSES3Algorithm
|
|
|
|
// Debug: log header detection for SSE-S3 requests
|
|
if result {
|
|
glog.V(4).Infof("SSE-S3 detection: method=%s, header=%q, expected=%q, result=%t, copySource=%q", r.Method, sseHeader, SSES3Algorithm, result, r.Header.Get("X-Amz-Copy-Source"))
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// IsSSES3EncryptedInternal checks if the object metadata indicates SSE-S3 encryption
|
|
func IsSSES3EncryptedInternal(metadata map[string][]byte) bool {
|
|
if sseAlgorithm, exists := metadata[s3_constants.AmzServerSideEncryption]; exists {
|
|
return string(sseAlgorithm) == SSES3Algorithm
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GenerateSSES3Key generates a new SSE-S3 encryption key
|
|
func GenerateSSES3Key() (*SSES3Key, error) {
|
|
key := make([]byte, SSES3KeySize)
|
|
if _, err := io.ReadFull(rand.Reader, key); err != nil {
|
|
return nil, fmt.Errorf("failed to generate SSE-S3 key: %w", err)
|
|
}
|
|
|
|
// Generate a key ID for tracking
|
|
keyID := fmt.Sprintf("sse-s3-key-%d", mathrand.Int63())
|
|
|
|
return &SSES3Key{
|
|
Key: key,
|
|
KeyID: keyID,
|
|
Algorithm: SSES3Algorithm,
|
|
}, nil
|
|
}
|
|
|
|
// CreateSSES3EncryptedReader creates an encrypted reader for SSE-S3
|
|
// Returns the encrypted reader and the IV for metadata storage
|
|
func CreateSSES3EncryptedReader(reader io.Reader, key *SSES3Key) (io.Reader, []byte, error) {
|
|
// Create AES cipher
|
|
block, err := aes.NewCipher(key.Key)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("create AES cipher: %w", err)
|
|
}
|
|
|
|
// Generate random IV
|
|
iv := make([]byte, aes.BlockSize)
|
|
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
|
return nil, nil, fmt.Errorf("generate IV: %w", err)
|
|
}
|
|
|
|
// Create CTR mode cipher
|
|
stream := cipher.NewCTR(block, iv)
|
|
|
|
// Return encrypted reader and IV separately for metadata storage
|
|
encryptedReader := &cipher.StreamReader{S: stream, R: reader}
|
|
|
|
return encryptedReader, iv, nil
|
|
}
|
|
|
|
// CreateSSES3DecryptedReader creates a decrypted reader for SSE-S3 using IV from metadata
|
|
func CreateSSES3DecryptedReader(reader io.Reader, key *SSES3Key, iv []byte) (io.Reader, error) {
|
|
// Create AES cipher
|
|
block, err := aes.NewCipher(key.Key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create AES cipher: %w", err)
|
|
}
|
|
|
|
// Create CTR mode cipher with the provided IV
|
|
stream := cipher.NewCTR(block, iv)
|
|
|
|
return &cipher.StreamReader{S: stream, R: reader}, nil
|
|
}
|
|
|
|
// GetSSES3Headers returns the headers for SSE-S3 encrypted objects
|
|
func GetSSES3Headers() map[string]string {
|
|
return map[string]string{
|
|
s3_constants.AmzServerSideEncryption: SSES3Algorithm,
|
|
}
|
|
}
|
|
|
|
// SerializeSSES3Metadata serializes SSE-S3 metadata for storage using envelope encryption
|
|
func SerializeSSES3Metadata(key *SSES3Key) ([]byte, error) {
|
|
if err := ValidateSSES3Key(key); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Encrypt the DEK using the global key manager's super key
|
|
keyManager := GetSSES3KeyManager()
|
|
encryptedDEK, nonce, err := keyManager.encryptKeyWithSuperKey(key.Key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to encrypt DEK: %w", err)
|
|
}
|
|
|
|
metadata := map[string]string{
|
|
"algorithm": key.Algorithm,
|
|
"keyId": key.KeyID,
|
|
"encryptedDEK": base64.StdEncoding.EncodeToString(encryptedDEK),
|
|
"nonce": base64.StdEncoding.EncodeToString(nonce),
|
|
}
|
|
|
|
// Include IV if present (needed for chunk-level decryption)
|
|
if key.IV != nil {
|
|
metadata["iv"] = base64.StdEncoding.EncodeToString(key.IV)
|
|
}
|
|
|
|
// Use JSON for proper serialization
|
|
data, err := json.Marshal(metadata)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal SSE-S3 metadata: %w", err)
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
// DeserializeSSES3Metadata deserializes SSE-S3 metadata from storage and decrypts the DEK
|
|
func DeserializeSSES3Metadata(data []byte, keyManager *SSES3KeyManager) (*SSES3Key, error) {
|
|
if len(data) == 0 {
|
|
return nil, fmt.Errorf("empty SSE-S3 metadata")
|
|
}
|
|
|
|
// Parse the JSON metadata
|
|
var metadata map[string]string
|
|
if err := json.Unmarshal(data, &metadata); err != nil {
|
|
return nil, fmt.Errorf("failed to parse SSE-S3 metadata: %w", err)
|
|
}
|
|
|
|
keyID, exists := metadata["keyId"]
|
|
if !exists {
|
|
return nil, fmt.Errorf("keyId not found in SSE-S3 metadata")
|
|
}
|
|
|
|
algorithm, exists := metadata["algorithm"]
|
|
if !exists {
|
|
algorithm = s3_constants.SSEAlgorithmAES256 // Default algorithm
|
|
}
|
|
|
|
// Decode the encrypted DEK and nonce
|
|
encryptedDEKStr, exists := metadata["encryptedDEK"]
|
|
if !exists {
|
|
return nil, fmt.Errorf("encryptedDEK not found in SSE-S3 metadata")
|
|
}
|
|
encryptedDEK, err := base64.StdEncoding.DecodeString(encryptedDEKStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode encrypted DEK: %w", err)
|
|
}
|
|
|
|
nonceStr, exists := metadata["nonce"]
|
|
if !exists {
|
|
return nil, fmt.Errorf("nonce not found in SSE-S3 metadata")
|
|
}
|
|
nonce, err := base64.StdEncoding.DecodeString(nonceStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode nonce: %w", err)
|
|
}
|
|
|
|
// Decrypt the DEK using the key manager
|
|
if keyManager == nil {
|
|
return nil, fmt.Errorf("key manager is required for SSE-S3 key retrieval")
|
|
}
|
|
|
|
dekBytes, err := keyManager.decryptKeyWithSuperKey(encryptedDEK, nonce)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decrypt DEK: %w", err)
|
|
}
|
|
|
|
// Reconstruct the key
|
|
key := &SSES3Key{
|
|
Key: dekBytes,
|
|
KeyID: keyID,
|
|
Algorithm: algorithm,
|
|
}
|
|
|
|
// Restore IV if present in metadata (for chunk-level decryption)
|
|
if ivStr, exists := metadata["iv"]; exists {
|
|
iv, err := base64.StdEncoding.DecodeString(ivStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode IV: %w", err)
|
|
}
|
|
key.IV = iv
|
|
}
|
|
|
|
return key, nil
|
|
}
|
|
|
|
// SSES3KeyManager manages SSE-S3 encryption keys using envelope encryption
|
|
// Instead of storing keys in memory, it uses a super key (KEK) to encrypt/decrypt DEKs
|
|
type SSES3KeyManager struct {
|
|
mu sync.RWMutex
|
|
superKey []byte // 256-bit master key (KEK - Key Encryption Key)
|
|
filerClient filer_pb.FilerClient // Filer client for KEK persistence
|
|
kekPath string // Path in filer where KEK is stored (e.g., /etc/s3/sse_kek)
|
|
}
|
|
|
|
const (
|
|
// KEK storage directory and file name in filer
|
|
SSES3KEKDirectory = "/etc/s3"
|
|
SSES3KEKParentDir = "/etc"
|
|
SSES3KEKDirName = "s3"
|
|
SSES3KEKFileName = "sse_kek"
|
|
|
|
// Full KEK path in filer
|
|
defaultKEKPath = SSES3KEKDirectory + "/" + SSES3KEKFileName
|
|
)
|
|
|
|
// NewSSES3KeyManager creates a new SSE-S3 key manager with envelope encryption
|
|
func NewSSES3KeyManager() *SSES3KeyManager {
|
|
// This will be initialized properly when attached to an S3ApiServer
|
|
return &SSES3KeyManager{
|
|
kekPath: defaultKEKPath,
|
|
}
|
|
}
|
|
|
|
// InitializeWithFiler initializes the key manager with a filer client
|
|
func (km *SSES3KeyManager) InitializeWithFiler(filerClient filer_pb.FilerClient) error {
|
|
km.mu.Lock()
|
|
defer km.mu.Unlock()
|
|
|
|
km.filerClient = filerClient
|
|
|
|
// Try to load existing KEK from filer
|
|
if err := km.loadSuperKeyFromFiler(); err != nil {
|
|
// Only generate a new key if it does not exist.
|
|
// For other errors (e.g. connectivity), we should fail fast to prevent creating a new key
|
|
// and making existing data undecryptable.
|
|
if errors.Is(err, filer_pb.ErrNotFound) {
|
|
glog.V(1).Infof("SSE-S3 KeyManager: KEK not found, generating new KEK (load from filer %s: %v)", km.kekPath, err)
|
|
if genErr := km.generateAndSaveSuperKeyToFiler(); genErr != nil {
|
|
return fmt.Errorf("failed to generate and save SSE-S3 super key: %w", genErr)
|
|
}
|
|
} else {
|
|
// A different error occurred (e.g., network issue, permission denied).
|
|
// Return the error to prevent starting with a broken state.
|
|
return fmt.Errorf("failed to load SSE-S3 super key from %s: %w", km.kekPath, err)
|
|
}
|
|
} else {
|
|
glog.V(1).Infof("SSE-S3 KeyManager: Loaded KEK from filer %s", km.kekPath)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// loadSuperKeyFromFiler loads the KEK from the filer
|
|
func (km *SSES3KeyManager) loadSuperKeyFromFiler() error {
|
|
if km.filerClient == nil {
|
|
return fmt.Errorf("filer client not initialized")
|
|
}
|
|
|
|
// Get the entry from filer
|
|
entry, err := filer_pb.GetEntry(context.Background(), km.filerClient, util.FullPath(km.kekPath))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get KEK entry from filer: %w", err)
|
|
}
|
|
|
|
// Read the content
|
|
if len(entry.Content) == 0 {
|
|
return fmt.Errorf("KEK entry is empty")
|
|
}
|
|
|
|
// Decode hex-encoded key
|
|
key, err := hex.DecodeString(string(entry.Content))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decode KEK: %w", err)
|
|
}
|
|
|
|
if len(key) != SSES3KeySize {
|
|
return fmt.Errorf("invalid KEK size: expected %d bytes, got %d", SSES3KeySize, len(key))
|
|
}
|
|
|
|
km.superKey = key
|
|
return nil
|
|
}
|
|
|
|
// generateAndSaveSuperKeyToFiler generates a new KEK and saves it to the filer
|
|
func (km *SSES3KeyManager) generateAndSaveSuperKeyToFiler() error {
|
|
if km.filerClient == nil {
|
|
return fmt.Errorf("filer client not initialized")
|
|
}
|
|
|
|
// Generate a random 256-bit super key (KEK)
|
|
superKey := make([]byte, SSES3KeySize)
|
|
if _, err := io.ReadFull(rand.Reader, superKey); err != nil {
|
|
return fmt.Errorf("failed to generate KEK: %w", err)
|
|
}
|
|
|
|
// Encode as hex for storage
|
|
encodedKey := []byte(hex.EncodeToString(superKey))
|
|
|
|
// Create the entry in filer
|
|
// First ensure the parent directory exists
|
|
if err := filer_pb.Mkdir(context.Background(), km.filerClient, SSES3KEKParentDir, SSES3KEKDirName, func(entry *filer_pb.Entry) {
|
|
// Set appropriate permissions for the directory
|
|
entry.Attributes.FileMode = uint32(0700 | os.ModeDir)
|
|
}); err != nil {
|
|
// Only ignore "file exists" errors.
|
|
if !strings.Contains(err.Error(), "file exists") {
|
|
return fmt.Errorf("failed to create KEK directory %s: %w", SSES3KEKDirectory, err)
|
|
}
|
|
glog.V(3).Infof("Parent directory %s already exists, continuing.", SSES3KEKDirectory)
|
|
}
|
|
|
|
// Create the KEK file
|
|
if err := filer_pb.MkFile(context.Background(), km.filerClient, SSES3KEKDirectory, SSES3KEKFileName, nil, func(entry *filer_pb.Entry) {
|
|
entry.Content = encodedKey
|
|
entry.Attributes.FileMode = 0600 // Read/write for owner only
|
|
entry.Attributes.FileSize = uint64(len(encodedKey))
|
|
}); err != nil {
|
|
return fmt.Errorf("failed to create KEK file in filer: %w", err)
|
|
}
|
|
|
|
km.superKey = superKey
|
|
glog.Infof("SSE-S3 KeyManager: Generated and saved new KEK to filer %s", km.kekPath)
|
|
return nil
|
|
}
|
|
|
|
// GetOrCreateKey gets an existing key or creates a new one
|
|
// With envelope encryption, we always generate a new DEK since we don't store them
|
|
func (km *SSES3KeyManager) GetOrCreateKey(keyID string) (*SSES3Key, error) {
|
|
// Always generate a new key - we use envelope encryption so no need to cache DEKs
|
|
return GenerateSSES3Key()
|
|
}
|
|
|
|
// encryptKeyWithSuperKey encrypts a DEK using the super key (KEK) with AES-GCM
|
|
func (km *SSES3KeyManager) encryptKeyWithSuperKey(dek []byte) ([]byte, []byte, error) {
|
|
km.mu.RLock()
|
|
defer km.mu.RUnlock()
|
|
|
|
block, err := aes.NewCipher(km.superKey)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to create cipher: %w", err)
|
|
}
|
|
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to create GCM: %w", err)
|
|
}
|
|
|
|
// Generate random nonce
|
|
nonce := make([]byte, gcm.NonceSize())
|
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
|
return nil, nil, fmt.Errorf("failed to generate nonce: %w", err)
|
|
}
|
|
|
|
// Encrypt the DEK
|
|
encryptedDEK := gcm.Seal(nil, nonce, dek, nil)
|
|
|
|
return encryptedDEK, nonce, nil
|
|
}
|
|
|
|
// decryptKeyWithSuperKey decrypts a DEK using the super key (KEK) with AES-GCM
|
|
func (km *SSES3KeyManager) decryptKeyWithSuperKey(encryptedDEK, nonce []byte) ([]byte, error) {
|
|
km.mu.RLock()
|
|
defer km.mu.RUnlock()
|
|
|
|
block, err := aes.NewCipher(km.superKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create cipher: %w", err)
|
|
}
|
|
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create GCM: %w", err)
|
|
}
|
|
|
|
if len(nonce) != gcm.NonceSize() {
|
|
return nil, fmt.Errorf("invalid nonce size: expected %d, got %d", gcm.NonceSize(), len(nonce))
|
|
}
|
|
|
|
// Decrypt the DEK
|
|
dek, err := gcm.Open(nil, nonce, encryptedDEK, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decrypt DEK: %w", err)
|
|
}
|
|
|
|
return dek, nil
|
|
}
|
|
|
|
// StoreKey is now a no-op since we use envelope encryption and don't cache DEKs
|
|
// The encrypted DEK is stored in the object metadata, not in the key manager
|
|
func (km *SSES3KeyManager) StoreKey(key *SSES3Key) {
|
|
// No-op: With envelope encryption, we don't need to store keys in memory
|
|
// The DEK is encrypted with the super key and stored in object metadata
|
|
}
|
|
|
|
// GetKey is now a no-op since we don't cache keys
|
|
// Keys are retrieved by decrypting the encrypted DEK from object metadata
|
|
func (km *SSES3KeyManager) GetKey(keyID string) (*SSES3Key, bool) {
|
|
// No-op: With envelope encryption, keys are not cached
|
|
// Each object's metadata contains the encrypted DEK
|
|
return nil, false
|
|
}
|
|
|
|
// Global SSE-S3 key manager instance
|
|
var globalSSES3KeyManager = NewSSES3KeyManager()
|
|
|
|
// GetSSES3KeyManager returns the global SSE-S3 key manager
|
|
func GetSSES3KeyManager() *SSES3KeyManager {
|
|
return globalSSES3KeyManager
|
|
}
|
|
|
|
// InitializeGlobalSSES3KeyManager initializes the global key manager with filer access
|
|
func InitializeGlobalSSES3KeyManager(s3ApiServer *S3ApiServer) error {
|
|
return globalSSES3KeyManager.InitializeWithFiler(s3ApiServer)
|
|
}
|
|
|
|
// ProcessSSES3Request processes an SSE-S3 request and returns encryption metadata
|
|
func ProcessSSES3Request(r *http.Request) (map[string][]byte, error) {
|
|
if !IsSSES3RequestInternal(r) {
|
|
return nil, nil
|
|
}
|
|
|
|
// Generate or retrieve encryption key
|
|
keyManager := GetSSES3KeyManager()
|
|
key, err := keyManager.GetOrCreateKey("")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get SSE-S3 key: %w", err)
|
|
}
|
|
|
|
// Serialize key metadata
|
|
keyData, err := SerializeSSES3Metadata(key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("serialize SSE-S3 metadata: %w", err)
|
|
}
|
|
|
|
// Store key in manager
|
|
keyManager.StoreKey(key)
|
|
|
|
// Return metadata
|
|
metadata := map[string][]byte{
|
|
s3_constants.AmzServerSideEncryption: []byte(SSES3Algorithm),
|
|
s3_constants.SeaweedFSSSES3Key: keyData,
|
|
}
|
|
|
|
return metadata, nil
|
|
}
|
|
|
|
// GetSSES3KeyFromMetadata extracts SSE-S3 key from object metadata
|
|
func GetSSES3KeyFromMetadata(metadata map[string][]byte, keyManager *SSES3KeyManager) (*SSES3Key, error) {
|
|
keyData, exists := metadata[s3_constants.SeaweedFSSSES3Key]
|
|
if !exists {
|
|
return nil, fmt.Errorf("SSE-S3 key not found in metadata")
|
|
}
|
|
|
|
return DeserializeSSES3Metadata(keyData, keyManager)
|
|
}
|
|
|
|
// GetSSES3IV extracts the IV for single-part SSE-S3 objects
|
|
// Priority: 1) object-level metadata (for inline/small files), 2) first chunk metadata
|
|
func GetSSES3IV(entry *filer_pb.Entry, sseS3Key *SSES3Key, keyManager *SSES3KeyManager) ([]byte, error) {
|
|
// First check if IV is in the object-level key (for small/inline files)
|
|
if len(sseS3Key.IV) > 0 {
|
|
return sseS3Key.IV, nil
|
|
}
|
|
|
|
// Fallback: Get IV from first chunk's metadata (for chunked files)
|
|
if len(entry.GetChunks()) > 0 {
|
|
chunk := entry.GetChunks()[0]
|
|
if len(chunk.GetSseMetadata()) > 0 {
|
|
chunkKey, err := DeserializeSSES3Metadata(chunk.GetSseMetadata(), keyManager)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize chunk SSE-S3 metadata: %w", err)
|
|
}
|
|
if len(chunkKey.IV) > 0 {
|
|
return chunkKey.IV, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("SSE-S3 IV not found in object or chunk metadata")
|
|
}
|
|
|
|
// CreateSSES3EncryptedReaderWithBaseIV creates an encrypted reader using a base IV for multipart upload consistency.
|
|
// The returned IV is the offset-derived IV, calculated from the input baseIV and offset.
|
|
func CreateSSES3EncryptedReaderWithBaseIV(reader io.Reader, key *SSES3Key, baseIV []byte, offset int64) (io.Reader, []byte /* derivedIV */, error) {
|
|
// Validate key to prevent panics and security issues
|
|
if key == nil {
|
|
return nil, nil, fmt.Errorf("SSES3Key is nil")
|
|
}
|
|
if key.Key == nil || len(key.Key) != SSES3KeySize {
|
|
return nil, nil, fmt.Errorf("invalid SSES3Key: must be %d bytes, got %d", SSES3KeySize, len(key.Key))
|
|
}
|
|
if err := ValidateSSES3Key(key); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
block, err := aes.NewCipher(key.Key)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("create AES cipher: %w", err)
|
|
}
|
|
|
|
// Calculate the proper IV with offset to ensure unique IV per chunk/part
|
|
// This prevents the severe security vulnerability of IV reuse in CTR mode
|
|
iv := calculateIVWithOffset(baseIV, offset)
|
|
|
|
stream := cipher.NewCTR(block, iv)
|
|
encryptedReader := &cipher.StreamReader{S: stream, R: reader}
|
|
return encryptedReader, iv, nil
|
|
}
|