* S3: Implement IAM defaults and STS signing key fallback logic * S3: Refactor startup order to init SSE-S3 key manager before IAM * S3: Derive STS signing key from KEK using HKDF for security isolation * S3: Document STS signing key fallback in security.toml * fix(s3api): refine anonymous access logic and secure-by-default behavior - Initialize anonymous identity by default in `NewIdentityAccessManagement` to prevent nil pointer exceptions. - Ensure `ReplaceS3ApiConfiguration` preserves the anonymous identity if not present in the new configuration. - Update `NewIdentityAccessManagement` signature to accept `filerClient`. - In legacy mode (no policy engine), anonymous defaults to Deny (no actions), preserving secure-by-default behavior. - Use specific `LookupAnonymous` method instead of generic map lookup. - Update tests to accommodate signature changes and verify improved anonymous handling. * feat(s3api): make IAM configuration optional - Start S3 API server without a configuration file if `EnableIam` option is set. - Default to `Allow` effect for policy engine when no configuration is provided (Zero-Config mode). - Handle empty configuration path gracefully in `loadIAMManagerFromConfig`. - Add integration test `iam_optional_test.go` to verify empty config behavior. * fix(iamapi): fix signature mismatch in NewIdentityAccessManagementWithStore * fix(iamapi): properly initialize FilerClient instead of passing nil * fix(iamapi): properly initialize filer client for IAM management - Instead of passing `nil`, construct a `wdclient.FilerClient` using the provided `Filers` addresses. - Ensure `NewIdentityAccessManagementWithStore` receives a valid `filerClient` to avoid potential nil pointer dereferences or limited functionality. * clean: remove dead code in s3api_server.go * refactor(s3api): improve IAM initialization, safety and anonymous access security * fix(s3api): ensure IAM config loads from filer after client init * fix(s3): resolve test failures in integration, CORS, and tagging tests - Fix CORS tests by providing explicit anonymous permissions config - Fix S3 integration tests by setting admin credentials in init - Align tagging test credentials in CI with IAM defaults - Added goroutine to retry IAM config load in iamapi server * fix(s3): allow anonymous access to health targets and S3 Tables when identities are present * fix(ci): use /healthz for Caddy health check in awscli tests * iam, s3api: expose DefaultAllow from IAM and Policy Engine This allows checking the global "Open by Default" configuration from other components like S3 Tables. * s3api/s3tables: support DefaultAllow in permission logic and handler Updated CheckPermissionWithContext to respect the DefaultAllow flag in PolicyContext. This enables "Open by Default" behavior for unauthenticated access in zero-config environments. Added a targeted unit test to verify the logic. * s3api/s3tables: propagate DefaultAllow through handlers Propagated the DefaultAllow flag to individual handlers for namespaces, buckets, tables, policies, and tagging. This ensures consistent "Open by Default" behavior across all S3 Tables API endpoints. * s3api: wire up DefaultAllow for S3 Tables API initialization Updated registerS3TablesRoutes to query the global IAM configuration and set the DefaultAllow flag on the S3 Tables API server. This completes the end-to-end propagation required for anonymous access in zero-config environments. Added a SetDefaultAllow method to S3TablesApiServer to facilitate this. * s3api: fix tests by adding DefaultAllow to mock IAM integrations The IAMIntegration interface was updated to include DefaultAllow(), breaking several mock implementations in tests. This commit fixes the build errors by adding the missing method to the mocks. * env * ensure ports * env * env * fix default allow * add one more test using non-anonymous user * debug * add more debug * less logs
610 lines
19 KiB
Go
610 lines
19 KiB
Go
package s3api
|
|
|
|
import (
|
|
"context"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
mathrand "math/rand"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
"github.com/seaweedfs/seaweedfs/weed/wdclient"
|
|
"golang.org/x/crypto/hkdf"
|
|
"google.golang.org/grpc"
|
|
)
|
|
|
|
// 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
|
|
// An object is considered SSE-S3 encrypted only if it has BOTH the encryption header
|
|
// AND the actual encryption key metadata. This prevents false positives when an object
|
|
// has leftover headers from a previous encryption state (e.g., after being decrypted
|
|
// during a copy operation). Fixes GitHub issue #7562.
|
|
func IsSSES3EncryptedInternal(metadata map[string][]byte) bool {
|
|
// Check for SSE-S3 algorithm header
|
|
sseAlgorithm, hasHeader := metadata[s3_constants.AmzServerSideEncryption]
|
|
if !hasHeader || string(sseAlgorithm) != SSES3Algorithm {
|
|
return false
|
|
}
|
|
|
|
// Must also have the actual encryption key to be considered encrypted
|
|
// Without the key, the object cannot be decrypted and should be treated as unencrypted
|
|
_, hasKey := metadata[s3_constants.SeaweedFSSSES3Key]
|
|
return hasKey
|
|
}
|
|
|
|
// 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)
|
|
decryptReader := &cipher.StreamReader{S: stream, R: reader}
|
|
|
|
// Wrap with closer if the underlying reader implements io.Closer
|
|
if closer, ok := reader.(io.Closer); ok {
|
|
return &decryptReaderCloser{
|
|
Reader: decryptReader,
|
|
underlyingCloser: closer,
|
|
}, nil
|
|
}
|
|
|
|
return decryptReader, 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 with retries to handle transient connectivity issues during startup
|
|
var err error
|
|
for i := 0; i < 10; i++ {
|
|
err = km.loadSuperKeyFromFiler()
|
|
if err == nil {
|
|
glog.V(1).Infof("SSE-S3 KeyManager: Loaded KEK from filer %s", km.kekPath)
|
|
return nil
|
|
}
|
|
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)
|
|
}
|
|
return nil
|
|
}
|
|
glog.Warningf("SSE-S3 KeyManager: failed to load KEK (attempt %d/10): %v", i+1, err)
|
|
time.Sleep(2 * time.Second)
|
|
}
|
|
|
|
// If we're here, all retries failed
|
|
return fmt.Errorf("failed to load SSE-S3 super key from %s after 10 attempts: %w", km.kekPath, err)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// GetMasterKey returns a derived key from the master KEK for STS signing
|
|
// This uses HKDF to isolate the STS security domain from the SSE-S3 domain
|
|
func (km *SSES3KeyManager) GetMasterKey() []byte {
|
|
km.mu.RLock()
|
|
defer km.mu.RUnlock()
|
|
|
|
if len(km.superKey) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Derive a separate key for STS to isolate security domains
|
|
// We use the KEK as the secret, and "seaweedfs-sts-signing-key" as the info
|
|
hkdfReader := hkdf.New(sha256.New, km.superKey, nil, []byte("seaweedfs-sts-signing-key"))
|
|
derived := make([]byte, 32) // 256-bit derived key
|
|
if _, err := io.ReadFull(hkdfReader, derived); err != nil {
|
|
glog.Errorf("Failed to derive STS key: %v", err)
|
|
return nil
|
|
}
|
|
return derived
|
|
}
|
|
|
|
// Global SSE-S3 key manager instance
|
|
var globalSSES3KeyManager = NewSSES3KeyManager()
|
|
|
|
// GetSSES3KeyManager returns the global SSE-S3 key manager
|
|
func GetSSES3KeyManager() *SSES3KeyManager {
|
|
return globalSSES3KeyManager
|
|
}
|
|
|
|
// KeyManagerFilerClient wraps wdclient.FilerClient to satisfy filer_pb.FilerClient interface
|
|
type KeyManagerFilerClient struct {
|
|
*wdclient.FilerClient
|
|
grpcDialOption grpc.DialOption
|
|
}
|
|
|
|
func (k *KeyManagerFilerClient) AdjustedUrl(location *filer_pb.Location) string {
|
|
return location.Url
|
|
}
|
|
|
|
func (k *KeyManagerFilerClient) WithFilerClient(streamingMode bool, fn func(filer_pb.SeaweedFilerClient) error) error {
|
|
filerAddress := k.GetCurrentFiler()
|
|
if filerAddress == "" {
|
|
return fmt.Errorf("no filer available")
|
|
}
|
|
return pb.WithGrpcFilerClient(streamingMode, 0, filerAddress, k.grpcDialOption, fn)
|
|
}
|
|
|
|
// InitializeGlobalSSES3KeyManager initializes the global key manager with filer access
|
|
func InitializeGlobalSSES3KeyManager(filerClient *wdclient.FilerClient, grpcDialOption grpc.DialOption) error {
|
|
wrapper := &KeyManagerFilerClient{
|
|
FilerClient: filerClient,
|
|
grpcDialOption: grpcDialOption,
|
|
}
|
|
return globalSSES3KeyManager.InitializeWithFiler(wrapper)
|
|
}
|
|
|
|
// 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
|
|
// Skip is not used here because we're encrypting from the start (not reading a range)
|
|
iv, _ := calculateIVWithOffset(baseIV, offset)
|
|
|
|
stream := cipher.NewCTR(block, iv)
|
|
encryptedReader := &cipher.StreamReader{S: stream, R: reader}
|
|
return encryptedReader, iv, nil
|
|
}
|