| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060 |
- package s3api
- import (
- "context"
- "crypto/aes"
- "crypto/cipher"
- "crypto/rand"
- "crypto/sha256"
- "encoding/base64"
- "encoding/hex"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "regexp"
- "sort"
- "strings"
- "time"
- "github.com/seaweedfs/seaweedfs/weed/glog"
- "github.com/seaweedfs/seaweedfs/weed/kms"
- "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
- "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
- "github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
- )
- // Compiled regex patterns for KMS key validation
- var (
- uuidRegex = regexp.MustCompile(`^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$`)
- arnRegex = regexp.MustCompile(`^arn:aws:kms:[a-z0-9-]+:\d{12}:(key|alias)/.+$`)
- )
- // SSEKMSKey contains the metadata for an SSE-KMS encrypted object
- type SSEKMSKey struct {
- KeyID string // The KMS key ID used
- EncryptedDataKey []byte // The encrypted data encryption key
- EncryptionContext map[string]string // The encryption context used
- BucketKeyEnabled bool // Whether S3 Bucket Keys are enabled
- IV []byte // The initialization vector for encryption
- ChunkOffset int64 // Offset of this chunk within the original part (for IV calculation)
- }
- // SSEKMSMetadata represents the metadata stored with SSE-KMS objects
- type SSEKMSMetadata struct {
- Algorithm string `json:"algorithm"` // "aws:kms"
- KeyID string `json:"keyId"` // KMS key identifier
- EncryptedDataKey string `json:"encryptedDataKey"` // Base64-encoded encrypted data key
- EncryptionContext map[string]string `json:"encryptionContext"` // Encryption context
- BucketKeyEnabled bool `json:"bucketKeyEnabled"` // S3 Bucket Key optimization
- IV string `json:"iv"` // Base64-encoded initialization vector
- PartOffset int64 `json:"partOffset"` // Offset within original multipart part (for IV calculation)
- }
- const (
- // Default data key size (256 bits)
- DataKeySize = 32
- )
- // Bucket key cache TTL (moved to be used with per-bucket cache)
- const BucketKeyCacheTTL = time.Hour
- // CreateSSEKMSEncryptedReader creates an encrypted reader using KMS envelope encryption
- func CreateSSEKMSEncryptedReader(r io.Reader, keyID string, encryptionContext map[string]string) (io.Reader, *SSEKMSKey, error) {
- return CreateSSEKMSEncryptedReaderWithBucketKey(r, keyID, encryptionContext, false)
- }
- // CreateSSEKMSEncryptedReaderWithBucketKey creates an encrypted reader with optional S3 Bucket Keys optimization
- func CreateSSEKMSEncryptedReaderWithBucketKey(r io.Reader, keyID string, encryptionContext map[string]string, bucketKeyEnabled bool) (io.Reader, *SSEKMSKey, error) {
- if bucketKeyEnabled {
- // Use S3 Bucket Keys optimization - try to get or create a bucket-level data key
- // Note: This is a simplified implementation. In practice, this would need
- // access to the bucket name and S3ApiServer instance for proper per-bucket caching.
- // For now, generate per-object keys (bucket key optimization disabled)
- glog.V(2).Infof("Bucket key optimization requested but not fully implemented yet - using per-object keys")
- bucketKeyEnabled = false
- }
- // Generate data key using common utility
- dataKeyResult, err := generateKMSDataKey(keyID, encryptionContext)
- if err != nil {
- return nil, nil, err
- }
- // Ensure we clear the plaintext data key from memory when done
- defer clearKMSDataKey(dataKeyResult)
- // Generate a random IV for CTR mode
- // Note: AES-CTR is used for object data encryption (not AES-GCM) because:
- // 1. CTR mode supports streaming encryption for large objects
- // 2. CTR mode supports range requests (seek to arbitrary positions)
- // 3. This matches AWS S3 and other S3-compatible implementations
- // The KMS data key encryption (separate layer) uses AES-GCM for authentication
- iv := make([]byte, s3_constants.AESBlockSize)
- if _, err := io.ReadFull(rand.Reader, iv); err != nil {
- return nil, nil, fmt.Errorf("failed to generate IV: %v", err)
- }
- // Create CTR mode cipher stream
- stream := cipher.NewCTR(dataKeyResult.Block, iv)
- // Create the SSE-KMS metadata using utility function
- sseKey := createSSEKMSKey(dataKeyResult, encryptionContext, bucketKeyEnabled, iv, 0)
- // The IV is stored in SSE key metadata, so the encrypted stream does not need to prepend the IV
- // This ensures correct Content-Length for clients
- encryptedReader := &cipher.StreamReader{S: stream, R: r}
- // Store IV in the SSE key for metadata storage
- sseKey.IV = iv
- return encryptedReader, sseKey, nil
- }
- // CreateSSEKMSEncryptedReaderWithBaseIV creates an SSE-KMS encrypted reader using a provided base IV
- // This is used for multipart uploads where all chunks need to use the same base IV
- func CreateSSEKMSEncryptedReaderWithBaseIV(r io.Reader, keyID string, encryptionContext map[string]string, bucketKeyEnabled bool, baseIV []byte) (io.Reader, *SSEKMSKey, error) {
- if err := ValidateIV(baseIV, "base IV"); err != nil {
- return nil, nil, err
- }
- // Generate data key using common utility
- dataKeyResult, err := generateKMSDataKey(keyID, encryptionContext)
- if err != nil {
- return nil, nil, err
- }
- // Ensure we clear the plaintext data key from memory when done
- defer clearKMSDataKey(dataKeyResult)
- // Use the provided base IV instead of generating a new one
- iv := make([]byte, s3_constants.AESBlockSize)
- copy(iv, baseIV)
- // Create CTR mode cipher stream
- stream := cipher.NewCTR(dataKeyResult.Block, iv)
- // Create the SSE-KMS metadata using utility function
- sseKey := createSSEKMSKey(dataKeyResult, encryptionContext, bucketKeyEnabled, iv, 0)
- // The IV is stored in SSE key metadata, so the encrypted stream does not need to prepend the IV
- // This ensures correct Content-Length for clients
- encryptedReader := &cipher.StreamReader{S: stream, R: r}
- // Store the base IV in the SSE key for metadata storage
- sseKey.IV = iv
- return encryptedReader, sseKey, nil
- }
- // CreateSSEKMSEncryptedReaderWithBaseIVAndOffset creates an SSE-KMS encrypted reader using a provided base IV and offset
- // This is used for multipart uploads where all chunks need unique IVs to prevent IV reuse vulnerabilities
- func CreateSSEKMSEncryptedReaderWithBaseIVAndOffset(r io.Reader, keyID string, encryptionContext map[string]string, bucketKeyEnabled bool, baseIV []byte, offset int64) (io.Reader, *SSEKMSKey, error) {
- if err := ValidateIV(baseIV, "base IV"); err != nil {
- return nil, nil, err
- }
- // Generate data key using common utility
- dataKeyResult, err := generateKMSDataKey(keyID, encryptionContext)
- if err != nil {
- return nil, nil, err
- }
- // Ensure we clear the plaintext data key from memory when done
- defer clearKMSDataKey(dataKeyResult)
- // Calculate unique IV using base IV and offset to prevent IV reuse in multipart uploads
- iv := calculateIVWithOffset(baseIV, offset)
- // Create CTR mode cipher stream
- stream := cipher.NewCTR(dataKeyResult.Block, iv)
- // Create the SSE-KMS metadata using utility function
- sseKey := createSSEKMSKey(dataKeyResult, encryptionContext, bucketKeyEnabled, iv, offset)
- // The IV is stored in SSE key metadata, so the encrypted stream does not need to prepend the IV
- // This ensures correct Content-Length for clients
- encryptedReader := &cipher.StreamReader{S: stream, R: r}
- return encryptedReader, sseKey, nil
- }
- // hashEncryptionContext creates a deterministic hash of the encryption context
- func hashEncryptionContext(encryptionContext map[string]string) string {
- if len(encryptionContext) == 0 {
- return "empty"
- }
- // Create a deterministic representation of the context
- hash := sha256.New()
- // Sort keys to ensure deterministic hash
- keys := make([]string, 0, len(encryptionContext))
- for k := range encryptionContext {
- keys = append(keys, k)
- }
- sort.Strings(keys)
- // Hash the sorted key-value pairs
- for _, k := range keys {
- hash.Write([]byte(k))
- hash.Write([]byte("="))
- hash.Write([]byte(encryptionContext[k]))
- hash.Write([]byte(";"))
- }
- return hex.EncodeToString(hash.Sum(nil))[:16] // Use first 16 chars for brevity
- }
- // getBucketDataKey retrieves or creates a cached bucket-level data key for SSE-KMS
- // This is a simplified implementation that demonstrates the per-bucket caching concept
- // In a full implementation, this would integrate with the actual bucket configuration system
- func getBucketDataKey(bucketName, keyID string, encryptionContext map[string]string, bucketCache *BucketKMSCache) (*kms.GenerateDataKeyResponse, error) {
- // Create context hash for cache key
- contextHash := hashEncryptionContext(encryptionContext)
- cacheKey := fmt.Sprintf("%s:%s", keyID, contextHash)
- // Try to get from cache first if cache is available
- if bucketCache != nil {
- if cacheEntry, found := bucketCache.Get(cacheKey); found {
- if dataKey, ok := cacheEntry.DataKey.(*kms.GenerateDataKeyResponse); ok {
- glog.V(3).Infof("Using cached bucket key for bucket %s, keyID %s", bucketName, keyID)
- return dataKey, nil
- }
- }
- }
- // Cache miss - generate new data key
- kmsProvider := kms.GetGlobalKMS()
- if kmsProvider == nil {
- return nil, fmt.Errorf("KMS is not configured")
- }
- dataKeyReq := &kms.GenerateDataKeyRequest{
- KeyID: keyID,
- KeySpec: kms.KeySpecAES256,
- EncryptionContext: encryptionContext,
- }
- ctx := context.Background()
- dataKeyResp, err := kmsProvider.GenerateDataKey(ctx, dataKeyReq)
- if err != nil {
- return nil, fmt.Errorf("failed to generate bucket data key: %v", err)
- }
- // Cache the data key for future use if cache is available
- if bucketCache != nil {
- bucketCache.Set(cacheKey, keyID, dataKeyResp, BucketKeyCacheTTL)
- glog.V(2).Infof("Generated and cached new bucket key for bucket %s, keyID %s", bucketName, keyID)
- } else {
- glog.V(2).Infof("Generated new bucket key for bucket %s, keyID %s (caching disabled)", bucketName, keyID)
- }
- return dataKeyResp, nil
- }
- // CreateSSEKMSEncryptedReaderForBucket creates an encrypted reader with bucket-specific caching
- // This method is part of S3ApiServer to access bucket configuration and caching
- func (s3a *S3ApiServer) CreateSSEKMSEncryptedReaderForBucket(r io.Reader, bucketName, keyID string, encryptionContext map[string]string, bucketKeyEnabled bool) (io.Reader, *SSEKMSKey, error) {
- var dataKeyResp *kms.GenerateDataKeyResponse
- var err error
- if bucketKeyEnabled {
- // Use S3 Bucket Keys optimization with persistent per-bucket caching
- bucketCache, err := s3a.getBucketKMSCache(bucketName)
- if err != nil {
- glog.V(2).Infof("Failed to get bucket KMS cache for %s, falling back to per-object key: %v", bucketName, err)
- bucketKeyEnabled = false
- } else {
- dataKeyResp, err = getBucketDataKey(bucketName, keyID, encryptionContext, bucketCache)
- if err != nil {
- // Fall back to per-object key generation if bucket key fails
- glog.V(2).Infof("Bucket key generation failed for bucket %s, falling back to per-object key: %v", bucketName, err)
- bucketKeyEnabled = false
- }
- }
- }
- if !bucketKeyEnabled {
- // Generate a per-object data encryption key using KMS
- kmsProvider := kms.GetGlobalKMS()
- if kmsProvider == nil {
- return nil, nil, fmt.Errorf("KMS is not configured")
- }
- dataKeyReq := &kms.GenerateDataKeyRequest{
- KeyID: keyID,
- KeySpec: kms.KeySpecAES256,
- EncryptionContext: encryptionContext,
- }
- ctx := context.Background()
- dataKeyResp, err = kmsProvider.GenerateDataKey(ctx, dataKeyReq)
- if err != nil {
- return nil, nil, fmt.Errorf("failed to generate data key: %v", err)
- }
- }
- // Ensure we clear the plaintext data key from memory when done
- defer kms.ClearSensitiveData(dataKeyResp.Plaintext)
- // Create AES cipher with the data key
- block, err := aes.NewCipher(dataKeyResp.Plaintext)
- if err != nil {
- return nil, nil, fmt.Errorf("failed to create AES cipher: %v", err)
- }
- // Generate a random IV for CTR mode
- iv := make([]byte, 16) // AES block size
- if _, err := io.ReadFull(rand.Reader, iv); err != nil {
- return nil, nil, fmt.Errorf("failed to generate IV: %v", err)
- }
- // Create CTR mode cipher stream
- stream := cipher.NewCTR(block, iv)
- // Create the encrypting reader
- sseKey := &SSEKMSKey{
- KeyID: keyID,
- EncryptedDataKey: dataKeyResp.CiphertextBlob,
- EncryptionContext: encryptionContext,
- BucketKeyEnabled: bucketKeyEnabled,
- IV: iv,
- }
- return &cipher.StreamReader{S: stream, R: r}, sseKey, nil
- }
- // getBucketKMSCache gets or creates the persistent KMS cache for a bucket
- func (s3a *S3ApiServer) getBucketKMSCache(bucketName string) (*BucketKMSCache, error) {
- // Get bucket configuration
- bucketConfig, errCode := s3a.getBucketConfig(bucketName)
- if errCode != s3err.ErrNone {
- if errCode == s3err.ErrNoSuchBucket {
- return nil, fmt.Errorf("bucket %s does not exist", bucketName)
- }
- return nil, fmt.Errorf("failed to get bucket config: %v", errCode)
- }
- // Initialize KMS cache if it doesn't exist
- if bucketConfig.KMSKeyCache == nil {
- bucketConfig.KMSKeyCache = NewBucketKMSCache(bucketName, BucketKeyCacheTTL)
- glog.V(3).Infof("Initialized new KMS cache for bucket %s", bucketName)
- }
- return bucketConfig.KMSKeyCache, nil
- }
- // CleanupBucketKMSCache performs cleanup of expired KMS keys for a specific bucket
- func (s3a *S3ApiServer) CleanupBucketKMSCache(bucketName string) int {
- bucketCache, err := s3a.getBucketKMSCache(bucketName)
- if err != nil {
- glog.V(3).Infof("Could not get KMS cache for bucket %s: %v", bucketName, err)
- return 0
- }
- cleaned := bucketCache.CleanupExpired()
- if cleaned > 0 {
- glog.V(2).Infof("Cleaned up %d expired KMS keys for bucket %s", cleaned, bucketName)
- }
- return cleaned
- }
- // CleanupAllBucketKMSCaches performs cleanup of expired KMS keys for all buckets
- func (s3a *S3ApiServer) CleanupAllBucketKMSCaches() int {
- totalCleaned := 0
- // Access the bucket config cache safely
- if s3a.bucketConfigCache != nil {
- s3a.bucketConfigCache.mutex.RLock()
- bucketNames := make([]string, 0, len(s3a.bucketConfigCache.cache))
- for bucketName := range s3a.bucketConfigCache.cache {
- bucketNames = append(bucketNames, bucketName)
- }
- s3a.bucketConfigCache.mutex.RUnlock()
- // Clean up each bucket's KMS cache
- for _, bucketName := range bucketNames {
- cleaned := s3a.CleanupBucketKMSCache(bucketName)
- totalCleaned += cleaned
- }
- }
- if totalCleaned > 0 {
- glog.V(2).Infof("Cleaned up %d expired KMS keys across %d bucket caches", totalCleaned, len(s3a.bucketConfigCache.cache))
- }
- return totalCleaned
- }
- // CreateSSEKMSDecryptedReader creates a decrypted reader using KMS envelope encryption
- func CreateSSEKMSDecryptedReader(r io.Reader, sseKey *SSEKMSKey) (io.Reader, error) {
- kmsProvider := kms.GetGlobalKMS()
- if kmsProvider == nil {
- return nil, fmt.Errorf("KMS is not configured")
- }
- // Decrypt the data encryption key using KMS
- decryptReq := &kms.DecryptRequest{
- CiphertextBlob: sseKey.EncryptedDataKey,
- EncryptionContext: sseKey.EncryptionContext,
- }
- ctx := context.Background()
- decryptResp, err := kmsProvider.Decrypt(ctx, decryptReq)
- if err != nil {
- return nil, fmt.Errorf("failed to decrypt data key: %v", err)
- }
- // Ensure we clear the plaintext data key from memory when done
- defer kms.ClearSensitiveData(decryptResp.Plaintext)
- // Verify the key ID matches (security check)
- if decryptResp.KeyID != sseKey.KeyID {
- return nil, fmt.Errorf("KMS key ID mismatch: expected %s, got %s", sseKey.KeyID, decryptResp.KeyID)
- }
- // Use the IV from the SSE key metadata, calculating offset if this is a chunked part
- if err := ValidateIV(sseKey.IV, "SSE key IV"); err != nil {
- return nil, fmt.Errorf("invalid IV in SSE key: %w", err)
- }
- // Calculate the correct IV for this chunk's offset within the original part
- var iv []byte
- if sseKey.ChunkOffset > 0 {
- iv = calculateIVWithOffset(sseKey.IV, sseKey.ChunkOffset)
- glog.Infof("Using calculated IV with offset %d for chunk decryption", sseKey.ChunkOffset)
- } else {
- iv = sseKey.IV
- // glog.Infof("Using base IV for chunk decryption (offset=0)")
- }
- // Create AES cipher with the decrypted data key
- block, err := aes.NewCipher(decryptResp.Plaintext)
- if err != nil {
- return nil, fmt.Errorf("failed to create AES cipher: %v", err)
- }
- // Create CTR mode cipher stream for decryption
- // Note: AES-CTR is used for object data decryption to match the encryption mode
- stream := cipher.NewCTR(block, iv)
- // Return the decrypted reader
- return &cipher.StreamReader{S: stream, R: r}, nil
- }
- // ParseSSEKMSHeaders parses SSE-KMS headers from an HTTP request
- func ParseSSEKMSHeaders(r *http.Request) (*SSEKMSKey, error) {
- sseAlgorithm := r.Header.Get(s3_constants.AmzServerSideEncryption)
- // Check if SSE-KMS is requested
- if sseAlgorithm == "" {
- return nil, nil // No SSE headers present
- }
- if sseAlgorithm != s3_constants.SSEAlgorithmKMS {
- return nil, fmt.Errorf("invalid SSE algorithm: %s", sseAlgorithm)
- }
- keyID := r.Header.Get(s3_constants.AmzServerSideEncryptionAwsKmsKeyId)
- encryptionContextHeader := r.Header.Get(s3_constants.AmzServerSideEncryptionContext)
- bucketKeyEnabledHeader := r.Header.Get(s3_constants.AmzServerSideEncryptionBucketKeyEnabled)
- // Parse encryption context if provided
- var encryptionContext map[string]string
- if encryptionContextHeader != "" {
- // Decode base64-encoded JSON encryption context
- contextBytes, err := base64.StdEncoding.DecodeString(encryptionContextHeader)
- if err != nil {
- return nil, fmt.Errorf("invalid encryption context format: %v", err)
- }
- if err := json.Unmarshal(contextBytes, &encryptionContext); err != nil {
- return nil, fmt.Errorf("invalid encryption context JSON: %v", err)
- }
- }
- // Parse bucket key enabled flag
- bucketKeyEnabled := strings.ToLower(bucketKeyEnabledHeader) == "true"
- sseKey := &SSEKMSKey{
- KeyID: keyID,
- EncryptionContext: encryptionContext,
- BucketKeyEnabled: bucketKeyEnabled,
- }
- // Validate the parsed key including key ID format
- if err := ValidateSSEKMSKeyInternal(sseKey); err != nil {
- return nil, err
- }
- return sseKey, nil
- }
- // ValidateSSEKMSKey validates an SSE-KMS key configuration
- func ValidateSSEKMSKeyInternal(sseKey *SSEKMSKey) error {
- if err := ValidateSSEKMSKey(sseKey); err != nil {
- return err
- }
- // An empty key ID is valid and means the default KMS key should be used.
- if sseKey.KeyID != "" && !isValidKMSKeyID(sseKey.KeyID) {
- return fmt.Errorf("invalid KMS key ID format: %s", sseKey.KeyID)
- }
- return nil
- }
- // BuildEncryptionContext creates the encryption context for S3 objects
- func BuildEncryptionContext(bucketName, objectKey string, useBucketKey bool) map[string]string {
- return kms.BuildS3EncryptionContext(bucketName, objectKey, useBucketKey)
- }
- // parseEncryptionContext parses the user-provided encryption context from base64 JSON
- func parseEncryptionContext(contextHeader string) (map[string]string, error) {
- if contextHeader == "" {
- return nil, nil
- }
- // Decode base64
- contextBytes, err := base64.StdEncoding.DecodeString(contextHeader)
- if err != nil {
- return nil, fmt.Errorf("invalid base64 encoding in encryption context: %w", err)
- }
- // Parse JSON
- var context map[string]string
- if err := json.Unmarshal(contextBytes, &context); err != nil {
- return nil, fmt.Errorf("invalid JSON in encryption context: %w", err)
- }
- // Validate context keys and values
- for k, v := range context {
- if k == "" || v == "" {
- return nil, fmt.Errorf("encryption context keys and values cannot be empty")
- }
- // AWS KMS has limits on context key/value length (256 chars each)
- if len(k) > 256 || len(v) > 256 {
- return nil, fmt.Errorf("encryption context key or value too long (max 256 characters)")
- }
- }
- return context, nil
- }
- // SerializeSSEKMSMetadata serializes SSE-KMS metadata for storage in object metadata
- func SerializeSSEKMSMetadata(sseKey *SSEKMSKey) ([]byte, error) {
- if err := ValidateSSEKMSKey(sseKey); err != nil {
- return nil, err
- }
- metadata := &SSEKMSMetadata{
- Algorithm: s3_constants.SSEAlgorithmKMS,
- KeyID: sseKey.KeyID,
- EncryptedDataKey: base64.StdEncoding.EncodeToString(sseKey.EncryptedDataKey),
- EncryptionContext: sseKey.EncryptionContext,
- BucketKeyEnabled: sseKey.BucketKeyEnabled,
- IV: base64.StdEncoding.EncodeToString(sseKey.IV), // Store IV for decryption
- PartOffset: sseKey.ChunkOffset, // Store within-part offset
- }
- data, err := json.Marshal(metadata)
- if err != nil {
- return nil, fmt.Errorf("failed to marshal SSE-KMS metadata: %w", err)
- }
- glog.V(4).Infof("Serialized SSE-KMS metadata: keyID=%s, bucketKey=%t", sseKey.KeyID, sseKey.BucketKeyEnabled)
- return data, nil
- }
- // DeserializeSSEKMSMetadata deserializes SSE-KMS metadata from storage and reconstructs the SSE-KMS key
- func DeserializeSSEKMSMetadata(data []byte) (*SSEKMSKey, error) {
- if len(data) == 0 {
- return nil, fmt.Errorf("empty SSE-KMS metadata")
- }
- var metadata SSEKMSMetadata
- if err := json.Unmarshal(data, &metadata); err != nil {
- return nil, fmt.Errorf("failed to unmarshal SSE-KMS metadata: %w", err)
- }
- // Validate algorithm - be lenient with missing/empty algorithm for backward compatibility
- if metadata.Algorithm != "" && metadata.Algorithm != s3_constants.SSEAlgorithmKMS {
- return nil, fmt.Errorf("invalid SSE-KMS algorithm: %s", metadata.Algorithm)
- }
- // Set default algorithm if empty
- if metadata.Algorithm == "" {
- metadata.Algorithm = s3_constants.SSEAlgorithmKMS
- }
- // Decode the encrypted data key
- encryptedDataKey, err := base64.StdEncoding.DecodeString(metadata.EncryptedDataKey)
- if err != nil {
- return nil, fmt.Errorf("failed to decode encrypted data key: %w", err)
- }
- // Decode the IV
- var iv []byte
- if metadata.IV != "" {
- iv, err = base64.StdEncoding.DecodeString(metadata.IV)
- if err != nil {
- return nil, fmt.Errorf("failed to decode IV: %w", err)
- }
- }
- sseKey := &SSEKMSKey{
- KeyID: metadata.KeyID,
- EncryptedDataKey: encryptedDataKey,
- EncryptionContext: metadata.EncryptionContext,
- BucketKeyEnabled: metadata.BucketKeyEnabled,
- IV: iv, // Restore IV for decryption
- ChunkOffset: metadata.PartOffset, // Use stored within-part offset
- }
- glog.V(4).Infof("Deserialized SSE-KMS metadata: keyID=%s, bucketKey=%t", sseKey.KeyID, sseKey.BucketKeyEnabled)
- return sseKey, nil
- }
- // SSECMetadata represents SSE-C metadata for per-chunk storage (unified with SSE-KMS approach)
- type SSECMetadata struct {
- Algorithm string `json:"algorithm"` // SSE-C algorithm (always "AES256")
- IV string `json:"iv"` // Base64-encoded initialization vector for this chunk
- KeyMD5 string `json:"keyMD5"` // MD5 of the customer-provided key
- PartOffset int64 `json:"partOffset"` // Offset within original multipart part (for IV calculation)
- }
- // SerializeSSECMetadata serializes SSE-C metadata for storage in chunk metadata
- func SerializeSSECMetadata(iv []byte, keyMD5 string, partOffset int64) ([]byte, error) {
- if err := ValidateIV(iv, "IV"); err != nil {
- return nil, err
- }
- metadata := &SSECMetadata{
- Algorithm: s3_constants.SSEAlgorithmAES256,
- IV: base64.StdEncoding.EncodeToString(iv),
- KeyMD5: keyMD5,
- PartOffset: partOffset,
- }
- data, err := json.Marshal(metadata)
- if err != nil {
- return nil, fmt.Errorf("failed to marshal SSE-C metadata: %w", err)
- }
- glog.V(4).Infof("Serialized SSE-C metadata: keyMD5=%s, partOffset=%d", keyMD5, partOffset)
- return data, nil
- }
- // DeserializeSSECMetadata deserializes SSE-C metadata from chunk storage
- func DeserializeSSECMetadata(data []byte) (*SSECMetadata, error) {
- if len(data) == 0 {
- return nil, fmt.Errorf("empty SSE-C metadata")
- }
- var metadata SSECMetadata
- if err := json.Unmarshal(data, &metadata); err != nil {
- return nil, fmt.Errorf("failed to unmarshal SSE-C metadata: %w", err)
- }
- // Validate algorithm
- if metadata.Algorithm != s3_constants.SSEAlgorithmAES256 {
- return nil, fmt.Errorf("invalid SSE-C algorithm: %s", metadata.Algorithm)
- }
- // Validate IV
- if metadata.IV == "" {
- return nil, fmt.Errorf("missing IV in SSE-C metadata")
- }
- if _, err := base64.StdEncoding.DecodeString(metadata.IV); err != nil {
- return nil, fmt.Errorf("invalid base64 IV in SSE-C metadata: %w", err)
- }
- glog.V(4).Infof("Deserialized SSE-C metadata: keyMD5=%s, partOffset=%d", metadata.KeyMD5, metadata.PartOffset)
- return &metadata, nil
- }
- // AddSSEKMSResponseHeaders adds SSE-KMS response headers to an HTTP response
- func AddSSEKMSResponseHeaders(w http.ResponseWriter, sseKey *SSEKMSKey) {
- w.Header().Set(s3_constants.AmzServerSideEncryption, s3_constants.SSEAlgorithmKMS)
- w.Header().Set(s3_constants.AmzServerSideEncryptionAwsKmsKeyId, sseKey.KeyID)
- if len(sseKey.EncryptionContext) > 0 {
- // Encode encryption context as base64 JSON
- contextBytes, err := json.Marshal(sseKey.EncryptionContext)
- if err == nil {
- contextB64 := base64.StdEncoding.EncodeToString(contextBytes)
- w.Header().Set(s3_constants.AmzServerSideEncryptionContext, contextB64)
- } else {
- glog.Errorf("Failed to encode encryption context: %v", err)
- }
- }
- if sseKey.BucketKeyEnabled {
- w.Header().Set(s3_constants.AmzServerSideEncryptionBucketKeyEnabled, "true")
- }
- }
- // IsSSEKMSRequest checks if the request contains SSE-KMS headers
- func IsSSEKMSRequest(r *http.Request) bool {
- // If SSE-C headers are present, this is not an SSE-KMS request (they are mutually exclusive)
- if r.Header.Get(s3_constants.AmzServerSideEncryptionCustomerAlgorithm) != "" {
- return false
- }
- // According to AWS S3 specification, SSE-KMS is only valid when the encryption header
- // is explicitly set to "aws:kms". The KMS key ID header alone is not sufficient.
- sseAlgorithm := r.Header.Get(s3_constants.AmzServerSideEncryption)
- return sseAlgorithm == s3_constants.SSEAlgorithmKMS
- }
- // IsSSEKMSEncrypted checks if the metadata indicates SSE-KMS encryption
- func IsSSEKMSEncrypted(metadata map[string][]byte) bool {
- if metadata == nil {
- return false
- }
- // The canonical way to identify an SSE-KMS encrypted object is by this header.
- if sseAlgorithm, exists := metadata[s3_constants.AmzServerSideEncryption]; exists {
- return string(sseAlgorithm) == s3_constants.SSEAlgorithmKMS
- }
- return false
- }
- // IsAnySSEEncrypted checks if metadata indicates any type of SSE encryption
- func IsAnySSEEncrypted(metadata map[string][]byte) bool {
- if metadata == nil {
- return false
- }
- // Check for any SSE type
- if IsSSECEncrypted(metadata) {
- return true
- }
- if IsSSEKMSEncrypted(metadata) {
- return true
- }
- // Check for SSE-S3
- if sseAlgorithm, exists := metadata[s3_constants.AmzServerSideEncryption]; exists {
- return string(sseAlgorithm) == s3_constants.SSEAlgorithmAES256
- }
- return false
- }
- // MapKMSErrorToS3Error maps KMS errors to appropriate S3 error codes
- func MapKMSErrorToS3Error(err error) s3err.ErrorCode {
- if err == nil {
- return s3err.ErrNone
- }
- // Check if it's a KMS error
- kmsErr, ok := err.(*kms.KMSError)
- if !ok {
- return s3err.ErrInternalError
- }
- switch kmsErr.Code {
- case kms.ErrCodeNotFoundException:
- return s3err.ErrKMSKeyNotFound
- case kms.ErrCodeAccessDenied:
- return s3err.ErrKMSAccessDenied
- case kms.ErrCodeKeyUnavailable:
- return s3err.ErrKMSDisabled
- case kms.ErrCodeInvalidKeyUsage:
- return s3err.ErrKMSAccessDenied
- case kms.ErrCodeInvalidCiphertext:
- return s3err.ErrKMSInvalidCiphertext
- default:
- glog.Errorf("Unmapped KMS error: %s - %s", kmsErr.Code, kmsErr.Message)
- return s3err.ErrInternalError
- }
- }
- // SSEKMSCopyStrategy represents different strategies for copying SSE-KMS encrypted objects
- type SSEKMSCopyStrategy int
- const (
- // SSEKMSCopyStrategyDirect - Direct chunk copy (same key, no re-encryption needed)
- SSEKMSCopyStrategyDirect SSEKMSCopyStrategy = iota
- // SSEKMSCopyStrategyDecryptEncrypt - Decrypt source and re-encrypt for destination
- SSEKMSCopyStrategyDecryptEncrypt
- )
- // String returns string representation of the strategy
- func (s SSEKMSCopyStrategy) String() string {
- switch s {
- case SSEKMSCopyStrategyDirect:
- return "Direct"
- case SSEKMSCopyStrategyDecryptEncrypt:
- return "DecryptEncrypt"
- default:
- return "Unknown"
- }
- }
- // GetSourceSSEKMSInfo extracts SSE-KMS information from source object metadata
- func GetSourceSSEKMSInfo(metadata map[string][]byte) (keyID string, isEncrypted bool) {
- if sseAlgorithm, exists := metadata[s3_constants.AmzServerSideEncryption]; exists && string(sseAlgorithm) == s3_constants.SSEAlgorithmKMS {
- if kmsKeyID, exists := metadata[s3_constants.AmzServerSideEncryptionAwsKmsKeyId]; exists {
- return string(kmsKeyID), true
- }
- return "", true // SSE-KMS with default key
- }
- return "", false
- }
- // CanDirectCopySSEKMS determines if we can directly copy chunks without decrypt/re-encrypt
- func CanDirectCopySSEKMS(srcMetadata map[string][]byte, destKeyID string) bool {
- srcKeyID, srcEncrypted := GetSourceSSEKMSInfo(srcMetadata)
- // Case 1: Source unencrypted, destination unencrypted -> Direct copy
- if !srcEncrypted && destKeyID == "" {
- return true
- }
- // Case 2: Source encrypted with same KMS key as destination -> Direct copy
- if srcEncrypted && destKeyID != "" {
- // Same key if key IDs match (empty means default key)
- return srcKeyID == destKeyID
- }
- // All other cases require decrypt/re-encrypt
- return false
- }
- // DetermineSSEKMSCopyStrategy determines the optimal copy strategy for SSE-KMS
- func DetermineSSEKMSCopyStrategy(srcMetadata map[string][]byte, destKeyID string) (SSEKMSCopyStrategy, error) {
- if CanDirectCopySSEKMS(srcMetadata, destKeyID) {
- return SSEKMSCopyStrategyDirect, nil
- }
- return SSEKMSCopyStrategyDecryptEncrypt, nil
- }
- // ParseSSEKMSCopyHeaders parses SSE-KMS headers from copy request
- func ParseSSEKMSCopyHeaders(r *http.Request) (destKeyID string, encryptionContext map[string]string, bucketKeyEnabled bool, err error) {
- // Check if this is an SSE-KMS request
- if !IsSSEKMSRequest(r) {
- return "", nil, false, nil
- }
- // Get destination KMS key ID
- destKeyID = r.Header.Get(s3_constants.AmzServerSideEncryptionAwsKmsKeyId)
- // Validate key ID if provided
- if destKeyID != "" && !isValidKMSKeyID(destKeyID) {
- return "", nil, false, fmt.Errorf("invalid KMS key ID: %s", destKeyID)
- }
- // Parse encryption context if provided
- if contextHeader := r.Header.Get(s3_constants.AmzServerSideEncryptionContext); contextHeader != "" {
- contextBytes, decodeErr := base64.StdEncoding.DecodeString(contextHeader)
- if decodeErr != nil {
- return "", nil, false, fmt.Errorf("invalid encryption context encoding: %v", decodeErr)
- }
- if unmarshalErr := json.Unmarshal(contextBytes, &encryptionContext); unmarshalErr != nil {
- return "", nil, false, fmt.Errorf("invalid encryption context JSON: %v", unmarshalErr)
- }
- }
- // Parse bucket key enabled flag
- if bucketKeyHeader := r.Header.Get(s3_constants.AmzServerSideEncryptionBucketKeyEnabled); bucketKeyHeader != "" {
- bucketKeyEnabled = strings.ToLower(bucketKeyHeader) == "true"
- }
- return destKeyID, encryptionContext, bucketKeyEnabled, nil
- }
- // UnifiedCopyStrategy represents all possible copy strategies across encryption types
- type UnifiedCopyStrategy int
- const (
- // CopyStrategyDirect - Direct chunk copy (no encryption changes)
- CopyStrategyDirect UnifiedCopyStrategy = iota
- // CopyStrategyEncrypt - Encrypt during copy (plain → encrypted)
- CopyStrategyEncrypt
- // CopyStrategyDecrypt - Decrypt during copy (encrypted → plain)
- CopyStrategyDecrypt
- // CopyStrategyReencrypt - Decrypt and re-encrypt (different keys/methods)
- CopyStrategyReencrypt
- // CopyStrategyKeyRotation - Same object, different key (metadata-only update)
- CopyStrategyKeyRotation
- )
- // String returns string representation of the unified strategy
- func (s UnifiedCopyStrategy) String() string {
- switch s {
- case CopyStrategyDirect:
- return "Direct"
- case CopyStrategyEncrypt:
- return "Encrypt"
- case CopyStrategyDecrypt:
- return "Decrypt"
- case CopyStrategyReencrypt:
- return "Reencrypt"
- case CopyStrategyKeyRotation:
- return "KeyRotation"
- default:
- return "Unknown"
- }
- }
- // EncryptionState represents the encryption state of source and destination
- type EncryptionState struct {
- SrcSSEC bool
- SrcSSEKMS bool
- SrcSSES3 bool
- DstSSEC bool
- DstSSEKMS bool
- DstSSES3 bool
- SameObject bool
- }
- // IsSourceEncrypted returns true if source has any encryption
- func (e *EncryptionState) IsSourceEncrypted() bool {
- return e.SrcSSEC || e.SrcSSEKMS || e.SrcSSES3
- }
- // IsTargetEncrypted returns true if target should be encrypted
- func (e *EncryptionState) IsTargetEncrypted() bool {
- return e.DstSSEC || e.DstSSEKMS || e.DstSSES3
- }
- // DetermineUnifiedCopyStrategy determines the optimal copy strategy for all encryption types
- func DetermineUnifiedCopyStrategy(state *EncryptionState, srcMetadata map[string][]byte, r *http.Request) (UnifiedCopyStrategy, error) {
- // Key rotation: same object with different encryption
- if state.SameObject && state.IsSourceEncrypted() && state.IsTargetEncrypted() {
- // Check if it's actually a key change
- if state.SrcSSEC && state.DstSSEC {
- // SSE-C key rotation - need to compare keys
- return CopyStrategyKeyRotation, nil
- }
- if state.SrcSSEKMS && state.DstSSEKMS {
- // SSE-KMS key rotation - need to compare key IDs
- srcKeyID, _ := GetSourceSSEKMSInfo(srcMetadata)
- dstKeyID := r.Header.Get(s3_constants.AmzServerSideEncryptionAwsKmsKeyId)
- if srcKeyID != dstKeyID {
- return CopyStrategyKeyRotation, nil
- }
- }
- }
- // Direct copy: no encryption changes
- if !state.IsSourceEncrypted() && !state.IsTargetEncrypted() {
- return CopyStrategyDirect, nil
- }
- // Same encryption type and key
- if state.SrcSSEKMS && state.DstSSEKMS {
- srcKeyID, _ := GetSourceSSEKMSInfo(srcMetadata)
- dstKeyID := r.Header.Get(s3_constants.AmzServerSideEncryptionAwsKmsKeyId)
- if srcKeyID == dstKeyID {
- return CopyStrategyDirect, nil
- }
- }
- if state.SrcSSEC && state.DstSSEC {
- // For SSE-C, we'd need to compare the actual keys, but we can't do that securely
- // So we assume different keys and use reencrypt strategy
- return CopyStrategyReencrypt, nil
- }
- // Encrypt: plain → encrypted
- if !state.IsSourceEncrypted() && state.IsTargetEncrypted() {
- return CopyStrategyEncrypt, nil
- }
- // Decrypt: encrypted → plain
- if state.IsSourceEncrypted() && !state.IsTargetEncrypted() {
- return CopyStrategyDecrypt, nil
- }
- // Reencrypt: different encryption types or keys
- if state.IsSourceEncrypted() && state.IsTargetEncrypted() {
- return CopyStrategyReencrypt, nil
- }
- return CopyStrategyDirect, nil
- }
- // DetectEncryptionState analyzes the source metadata and request headers to determine encryption state
- func DetectEncryptionState(srcMetadata map[string][]byte, r *http.Request, srcPath, dstPath string) *EncryptionState {
- state := &EncryptionState{
- SrcSSEC: IsSSECEncrypted(srcMetadata),
- SrcSSEKMS: IsSSEKMSEncrypted(srcMetadata),
- SrcSSES3: IsSSES3EncryptedInternal(srcMetadata),
- DstSSEC: IsSSECRequest(r),
- DstSSEKMS: IsSSEKMSRequest(r),
- DstSSES3: IsSSES3RequestInternal(r),
- SameObject: srcPath == dstPath,
- }
- return state
- }
- // DetectEncryptionStateWithEntry analyzes the source entry and request headers to determine encryption state
- // This version can detect multipart encrypted objects by examining chunks
- func DetectEncryptionStateWithEntry(entry *filer_pb.Entry, r *http.Request, srcPath, dstPath string) *EncryptionState {
- state := &EncryptionState{
- SrcSSEC: IsSSECEncryptedWithEntry(entry),
- SrcSSEKMS: IsSSEKMSEncryptedWithEntry(entry),
- SrcSSES3: IsSSES3EncryptedInternal(entry.Extended),
- DstSSEC: IsSSECRequest(r),
- DstSSEKMS: IsSSEKMSRequest(r),
- DstSSES3: IsSSES3RequestInternal(r),
- SameObject: srcPath == dstPath,
- }
- return state
- }
- // IsSSEKMSEncryptedWithEntry detects SSE-KMS encryption from entry (including multipart objects)
- func IsSSEKMSEncryptedWithEntry(entry *filer_pb.Entry) bool {
- if entry == nil {
- return false
- }
- // Check object-level metadata first
- if IsSSEKMSEncrypted(entry.Extended) {
- return true
- }
- // Check for multipart SSE-KMS by examining chunks
- if len(entry.GetChunks()) > 0 {
- for _, chunk := range entry.GetChunks() {
- if chunk.GetSseType() == filer_pb.SSEType_SSE_KMS {
- return true
- }
- }
- }
- return false
- }
- // IsSSECEncryptedWithEntry detects SSE-C encryption from entry (including multipart objects)
- func IsSSECEncryptedWithEntry(entry *filer_pb.Entry) bool {
- if entry == nil {
- return false
- }
- // Check object-level metadata first
- if IsSSECEncrypted(entry.Extended) {
- return true
- }
- // Check for multipart SSE-C by examining chunks
- if len(entry.GetChunks()) > 0 {
- for _, chunk := range entry.GetChunks() {
- if chunk.GetSseType() == filer_pb.SSEType_SSE_C {
- return true
- }
- }
- }
- return false
- }
- // Helper functions for SSE-C detection are in s3_sse_c.go
|