2023-09-27 05:40:32 -04:00
/ *
Copyright ( c ) Edgeless Systems GmbH
SPDX - License - Identifier : AGPL - 3.0 - only
* /
package router
import (
"context"
2023-10-02 03:00:38 -04:00
"encoding/hex"
2023-09-27 05:40:32 -04:00
"io"
2024-02-08 09:20:01 -05:00
"log/slog"
2023-09-27 05:40:32 -04:00
"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 (
2023-10-02 03:00:38 -04:00
// 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"
2023-09-27 05:40:32 -04:00
)
// object bundles data to implement http.Handler methods that use data from incoming requests.
type object struct {
2023-10-02 03:00:38 -04:00
kek [ 32 ] byte
2023-09-27 05:40:32 -04:00
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
2024-02-08 09:20:01 -05:00
log * slog . Logger
2023-09-27 05:40:32 -04:00
}
// get is a http.HandlerFunc that implements the GET method for objects.
func ( o object ) get ( w http . ResponseWriter , r * http . Request ) {
2024-02-08 09:20:01 -05:00
o . log . With ( slog . String ( "key" , o . key ) , slog . String ( "host" , o . bucket ) ) . Debug ( "getObject" )
2023-09-27 05:40:32 -04:00
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).
2024-02-08 09:20:01 -05:00
o . log . With ( slog . Any ( "error" , err ) ) . Error ( "GetObject sending request to S3" )
2023-09-27 05:40:32 -04:00
// 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 {
2024-02-08 09:20:01 -05:00
o . log . With ( slog . Any ( "error" , err ) ) . Error ( "GetObject reading S3 response" )
2023-09-27 05:40:32 -04:00
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
plaintext := body
2023-10-02 03:00:38 -04:00
rawEncryptedDEK , ok := output . Metadata [ dekTag ]
if ok {
encryptedDEK , err := hex . DecodeString ( rawEncryptedDEK )
if err != nil {
2024-02-08 09:20:01 -05:00
o . log . Error ( "GetObject decoding DEK" , "error" , err )
2023-10-02 03:00:38 -04:00
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
2023-09-27 05:40:32 -04:00
2023-10-02 03:00:38 -04:00
plaintext , err = crypto . Decrypt ( body , encryptedDEK , o . kek )
2023-09-27 05:40:32 -04:00
if err != nil {
2024-02-08 09:20:01 -05:00
o . log . With ( slog . Any ( "error" , err ) ) . Error ( "GetObject decrypting response" )
2023-09-27 05:40:32 -04:00
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
}
w . WriteHeader ( http . StatusOK )
if _ , err := w . Write ( plaintext ) ; err != nil {
2024-02-08 09:20:01 -05:00
o . log . With ( slog . Any ( "error" , err ) ) . Error ( "GetObject sending response" )
2023-09-27 05:40:32 -04:00
}
}
// put is a http.HandlerFunc that implements the PUT method for objects.
func ( o object ) put ( w http . ResponseWriter , r * http . Request ) {
2023-10-02 03:00:38 -04:00
ciphertext , encryptedDEK , err := crypto . Encrypt ( o . data , o . kek )
2023-09-27 05:40:32 -04:00
if err != nil {
2024-02-08 09:20:01 -05:00
o . log . With ( slog . Any ( "error" , err ) ) . Error ( "PutObject" )
2023-09-27 05:40:32 -04:00
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
2023-10-02 03:00:38 -04:00
o . metadata [ dekTag ] = hex . EncodeToString ( encryptedDEK )
2023-09-27 05:40:32 -04:00
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 {
2024-02-08 09:20:01 -05:00
o . log . With ( slog . Any ( "error" , err ) ) . Error ( "PutObject sending request to S3" )
2023-09-27 05:40:32 -04:00
// 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 {
2024-02-08 09:20:01 -05:00
o . log . With ( slog . Any ( "error" , err ) ) . Error ( "PutObject sending response" )
2023-09-27 05:40:32 -04:00
}
}
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 )
}