mirror of
				https://github.com/edgelesssys/constellation.git
				synced 2025-10-30 19:28:59 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			219 lines
		
	
	
	
		
			7.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			219 lines
		
	
	
	
		
			7.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| /*
 | |
| Copyright (c) Edgeless Systems GmbH
 | |
| 
 | |
| SPDX-License-Identifier: AGPL-3.0-only
 | |
| */
 | |
| 
 | |
| package router
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"encoding/hex"
 | |
| 	"io"
 | |
| 	"log/slog"
 | |
| 	"net/http"
 | |
| 	"net/url"
 | |
| 	"regexp"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/aws/aws-sdk-go-v2/service/s3"
 | |
| 	"github.com/edgelesssys/constellation/v2/s3proxy/internal/crypto"
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	// dekTag is the name of the header that holds the encrypted data encryption key for the attached object. Presence of the key implies the object needs to be decrypted.
 | |
| 	// Use lowercase only, as AWS automatically lowercases all metadata keys.
 | |
| 	dekTag = "constellation-dek"
 | |
| )
 | |
| 
 | |
| // object bundles data to implement http.Handler methods that use data from incoming requests.
 | |
| type object struct {
 | |
| 	kek                       [32]byte
 | |
| 	client                    s3Client
 | |
| 	key                       string
 | |
| 	bucket                    string
 | |
| 	data                      []byte
 | |
| 	query                     url.Values
 | |
| 	tags                      string
 | |
| 	contentType               string
 | |
| 	metadata                  map[string]string
 | |
| 	objectLockLegalHoldStatus string
 | |
| 	objectLockMode            string
 | |
| 	objectLockRetainUntilDate time.Time
 | |
| 	sseCustomerAlgorithm      string
 | |
| 	sseCustomerKey            string
 | |
| 	sseCustomerKeyMD5         string
 | |
| 	log                       *slog.Logger
 | |
| }
 | |
| 
 | |
| // get is a http.HandlerFunc that implements the GET method for objects.
 | |
| func (o object) get(w http.ResponseWriter, r *http.Request) {
 | |
| 	o.log.With(slog.String("key", o.key), slog.String("host", o.bucket)).Debug("getObject")
 | |
| 
 | |
| 	versionID, ok := o.query["versionId"]
 | |
| 	if !ok {
 | |
| 		versionID = []string{""}
 | |
| 	}
 | |
| 
 | |
| 	output, err := o.client.GetObject(r.Context(), o.bucket, o.key, versionID[0], o.sseCustomerAlgorithm, o.sseCustomerKey, o.sseCustomerKeyMD5)
 | |
| 	if err != nil {
 | |
| 		// log with Info as it might be expected behavior (e.g. object not found).
 | |
| 		o.log.With(slog.Any("error", err)).Error("GetObject sending request to S3")
 | |
| 
 | |
| 		// We want to forward error codes from the s3 API to clients as much as possible.
 | |
| 		code := parseErrorCode(err)
 | |
| 		if code != 0 {
 | |
| 			http.Error(w, err.Error(), code)
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		http.Error(w, err.Error(), http.StatusInternalServerError)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if output.ETag != nil {
 | |
| 		w.Header().Set("ETag", strings.Trim(*output.ETag, "\""))
 | |
| 	}
 | |
| 	if output.Expiration != nil {
 | |
| 		w.Header().Set("x-amz-expiration", *output.Expiration)
 | |
| 	}
 | |
| 	if output.ChecksumCRC32 != nil {
 | |
| 		w.Header().Set("x-amz-checksum-crc32", *output.ChecksumCRC32)
 | |
| 	}
 | |
| 	if output.ChecksumCRC32C != nil {
 | |
| 		w.Header().Set("x-amz-checksum-crc32c", *output.ChecksumCRC32C)
 | |
| 	}
 | |
| 	if output.ChecksumSHA1 != nil {
 | |
| 		w.Header().Set("x-amz-checksum-sha1", *output.ChecksumSHA1)
 | |
| 	}
 | |
| 	if output.ChecksumSHA256 != nil {
 | |
| 		w.Header().Set("x-amz-checksum-sha256", *output.ChecksumSHA256)
 | |
| 	}
 | |
| 	if output.SSECustomerAlgorithm != nil {
 | |
| 		w.Header().Set("x-amz-server-side-encryption-customer-algorithm", *output.SSECustomerAlgorithm)
 | |
| 	}
 | |
| 	if output.SSECustomerKeyMD5 != nil {
 | |
| 		w.Header().Set("x-amz-server-side-encryption-customer-key-MD5", *output.SSECustomerKeyMD5)
 | |
| 	}
 | |
| 	if output.SSEKMSKeyId != nil {
 | |
| 		w.Header().Set("x-amz-server-side-encryption-aws-kms-key-id", *output.SSEKMSKeyId)
 | |
| 	}
 | |
| 	if output.ServerSideEncryption != "" {
 | |
| 		w.Header().Set("x-amz-server-side-encryption-context", string(output.ServerSideEncryption))
 | |
| 	}
 | |
| 
 | |
| 	body, err := io.ReadAll(output.Body)
 | |
| 	if err != nil {
 | |
| 		o.log.With(slog.Any("error", err)).Error("GetObject reading S3 response")
 | |
| 		http.Error(w, err.Error(), http.StatusInternalServerError)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	plaintext := body
 | |
| 	rawEncryptedDEK, ok := output.Metadata[dekTag]
 | |
| 	if ok {
 | |
| 		encryptedDEK, err := hex.DecodeString(rawEncryptedDEK)
 | |
| 		if err != nil {
 | |
| 			o.log.Error("GetObject decoding DEK", "error", err)
 | |
| 			http.Error(w, err.Error(), http.StatusInternalServerError)
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		plaintext, err = crypto.Decrypt(body, encryptedDEK, o.kek)
 | |
| 		if err != nil {
 | |
| 			o.log.With(slog.Any("error", err)).Error("GetObject decrypting response")
 | |
| 			http.Error(w, err.Error(), http.StatusInternalServerError)
 | |
| 			return
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	w.WriteHeader(http.StatusOK)
 | |
| 	if _, err := w.Write(plaintext); err != nil {
 | |
| 		o.log.With(slog.Any("error", err)).Error("GetObject sending response")
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // put is a http.HandlerFunc that implements the PUT method for objects.
 | |
| func (o object) put(w http.ResponseWriter, r *http.Request) {
 | |
| 	ciphertext, encryptedDEK, err := crypto.Encrypt(o.data, o.kek)
 | |
| 	if err != nil {
 | |
| 		o.log.With(slog.Any("error", err)).Error("PutObject")
 | |
| 		http.Error(w, err.Error(), http.StatusInternalServerError)
 | |
| 		return
 | |
| 	}
 | |
| 	o.metadata[dekTag] = hex.EncodeToString(encryptedDEK)
 | |
| 
 | |
| 	output, err := o.client.PutObject(r.Context(), o.bucket, o.key, o.tags, o.contentType, o.objectLockLegalHoldStatus, o.objectLockMode, o.sseCustomerAlgorithm, o.sseCustomerKey, o.sseCustomerKeyMD5, o.objectLockRetainUntilDate, o.metadata, ciphertext)
 | |
| 	if err != nil {
 | |
| 		o.log.With(slog.Any("error", err)).Error("PutObject sending request to S3")
 | |
| 
 | |
| 		// We want to forward error codes from the s3 API to clients whenever possible.
 | |
| 		code := parseErrorCode(err)
 | |
| 		if code != 0 {
 | |
| 			http.Error(w, err.Error(), code)
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		http.Error(w, err.Error(), http.StatusInternalServerError)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	w.Header().Set("x-amz-server-side-encryption", string(output.ServerSideEncryption))
 | |
| 
 | |
| 	if output.VersionId != nil {
 | |
| 		w.Header().Set("x-amz-version-id", *output.VersionId)
 | |
| 	}
 | |
| 	if output.ETag != nil {
 | |
| 		w.Header().Set("ETag", strings.Trim(*output.ETag, "\""))
 | |
| 	}
 | |
| 	if output.Expiration != nil {
 | |
| 		w.Header().Set("x-amz-expiration", *output.Expiration)
 | |
| 	}
 | |
| 	if output.ChecksumCRC32 != nil {
 | |
| 		w.Header().Set("x-amz-checksum-crc32", *output.ChecksumCRC32)
 | |
| 	}
 | |
| 	if output.ChecksumCRC32C != nil {
 | |
| 		w.Header().Set("x-amz-checksum-crc32c", *output.ChecksumCRC32C)
 | |
| 	}
 | |
| 	if output.ChecksumSHA1 != nil {
 | |
| 		w.Header().Set("x-amz-checksum-sha1", *output.ChecksumSHA1)
 | |
| 	}
 | |
| 	if output.ChecksumSHA256 != nil {
 | |
| 		w.Header().Set("x-amz-checksum-sha256", *output.ChecksumSHA256)
 | |
| 	}
 | |
| 	if output.SSECustomerAlgorithm != nil {
 | |
| 		w.Header().Set("x-amz-server-side-encryption-customer-algorithm", *output.SSECustomerAlgorithm)
 | |
| 	}
 | |
| 	if output.SSECustomerKeyMD5 != nil {
 | |
| 		w.Header().Set("x-amz-server-side-encryption-customer-key-MD5", *output.SSECustomerKeyMD5)
 | |
| 	}
 | |
| 	if output.SSEKMSKeyId != nil {
 | |
| 		w.Header().Set("x-amz-server-side-encryption-aws-kms-key-id", *output.SSEKMSKeyId)
 | |
| 	}
 | |
| 	if output.SSEKMSEncryptionContext != nil {
 | |
| 		w.Header().Set("x-amz-server-side-encryption-context", *output.SSEKMSEncryptionContext)
 | |
| 	}
 | |
| 
 | |
| 	w.WriteHeader(http.StatusOK)
 | |
| 	if _, err := w.Write(nil); err != nil {
 | |
| 		o.log.With(slog.Any("error", err)).Error("PutObject sending response")
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func parseErrorCode(err error) int {
 | |
| 	regex := regexp.MustCompile(`https response error StatusCode: (\d+)`)
 | |
| 	matches := regex.FindStringSubmatch(err.Error())
 | |
| 	if len(matches) > 1 {
 | |
| 		code, _ := strconv.Atoi(matches[1])
 | |
| 		return code
 | |
| 	}
 | |
| 
 | |
| 	return 0
 | |
| }
 | |
| 
 | |
| type s3Client interface {
 | |
| 	GetObject(ctx context.Context, bucket, key, versionID, sseCustomerAlgorithm, sseCustomerKey, sseCustomerKeyMD5 string) (*s3.GetObjectOutput, error)
 | |
| 	PutObject(ctx context.Context, bucket, key, tags, contentType, objectLockLegalHoldStatus, objectLockMode, sseCustomerAlgorithm, sseCustomerKey, sseCustomerKeyMD5 string, objectLockRetainUntilDate time.Time, metadata map[string]string, body []byte) (*s3.PutObjectOutput, error)
 | |
| }
 | 
