Skip to content

Commit

Permalink
s3proxy: handle server side encryption (sse)
Browse files Browse the repository at this point in the history
Also handle unsigned payloads.
And correctly return ETags.
  • Loading branch information
derpsteb committed Sep 29, 2023
1 parent 3984b5a commit f6eb31e
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 21 deletions.
48 changes: 39 additions & 9 deletions s3proxy/internal/router/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ type object struct {
objectLockLegalHoldStatus string
objectLockMode string
objectLockRetainUntilDate time.Time
sseCustomerAlgorithm string
sseCustomerKey string
sseCustomerKeyMD5 string
log *slog.Logger
}

Expand All @@ -54,7 +57,7 @@ func (o object) get(w http.ResponseWriter, r *http.Request) {
versionID = []string{""}
}

data, err := o.client.GetObject(r.Context(), o.bucket, o.key, versionID[0])
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.Error("GetObject sending request to S3", "error", err)
Expand All @@ -70,19 +73,46 @@ func (o object) get(w http.ResponseWriter, r *http.Request) {
return
}

if data.ETag != nil {
w.Header().Set("ETag", strings.Trim(*data.ETag, "\""))
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(data.Body)
body, err := io.ReadAll(output.Body)
if err != nil {
o.log.Error("GetObject reading S3 response", "error", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

plaintext := body
decrypt, ok := data.Metadata[encryptionTag]
decrypt, ok := output.Metadata[encryptionTag]

if ok && decrypt == "true" {
plaintext, err = crypto.Decrypt(body, []byte(testingKey))
Expand Down Expand Up @@ -111,7 +141,7 @@ func (o object) put(w http.ResponseWriter, r *http.Request) {
// GetObject needs to be able to recognize these objects and skip decryption.
o.metadata[encryptionTag] = "true"

output, err := o.client.PutObject(r.Context(), o.bucket, o.key, o.tags, o.contentType, o.objectLockLegalHoldStatus, o.objectLockMode, o.objectLockRetainUntilDate, o.metadata, ciphertext)
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.Error("PutObject sending request to S3", "error", err)

Expand All @@ -132,7 +162,7 @@ func (o object) put(w http.ResponseWriter, r *http.Request) {
w.Header().Set("x-amz-version-id", *output.VersionId)
}
if output.ETag != nil {
w.Header().Set("ETag", *output.ETag)
w.Header().Set("ETag", strings.Trim(*output.ETag, "\""))
}
if output.Expiration != nil {
w.Header().Set("x-amz-expiration", *output.Expiration)
Expand Down Expand Up @@ -180,6 +210,6 @@ func parseErrorCode(err error) int {
}

type s3Client interface {
GetObject(ctx context.Context, bucket, key, versionID string) (*s3.GetObjectOutput, error)
PutObject(ctx context.Context, bucket, key, tags, contentType, objectLockLegalHoldStatus, objectLockMode string, objectLockRetainUntilDate time.Time, metadata map[string]string, body []byte) (*s3.PutObjectOutput, error)
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)
}
38 changes: 26 additions & 12 deletions s3proxy/internal/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,14 @@ func (r Router) Serve(w http.ResponseWriter, req *http.Request) {
}

obj := object{
client: client,
key: key,
bucket: bucket,
query: req.URL.Query(),
log: r.log,
client: client,
key: key,
bucket: bucket,
query: req.URL.Query(),
sseCustomerAlgorithm: req.Header.Get("x-amz-server-side-encryption-customer-algorithm"),
sseCustomerKey: req.Header.Get("x-amz-server-side-encryption-customer-key"),
sseCustomerKeyMD5: req.Header.Get("x-amz-server-side-encryption-customer-key-MD5"),
log: r.log,
}
h = get(obj.get)
case !containsBucket(req.Host) && match(path, "/([^/?]+)/(.+)", &bucket, &key) && req.Method == "GET" && !isGetObjectX(req.URL.Query()):
Expand All @@ -93,11 +96,14 @@ func (r Router) Serve(w http.ResponseWriter, req *http.Request) {
}

obj := object{
client: client,
key: key,
bucket: bucket,
query: req.URL.Query(),
log: r.log,
client: client,
key: key,
bucket: bucket,
query: req.URL.Query(),
sseCustomerAlgorithm: req.Header.Get("x-amz-server-side-encryption-customer-algorithm"),
sseCustomerKey: req.Header.Get("x-amz-server-side-encryption-customer-key"),
sseCustomerKeyMD5: req.Header.Get("x-amz-server-side-encryption-customer-key-MD5"),
log: r.log,
}
h = get(obj.get)

Expand All @@ -122,7 +128,8 @@ func (r Router) Serve(w http.ResponseWriter, req *http.Request) {
// If the client intentionally sends a mismatching content digest, we would take the client request, rewrap it,
// calculate the correct digest for the new body and NOT get an error.
// Thus we have to check incoming requets for matching content digests.
if clientDigest != "" && clientDigest != serverDigest {
// UNSIGNED-PAYLOAD can be used to disabled payload signing. In that case we don't check the content digest.
if clientDigest != "" && clientDigest != "UNSIGNED-PAYLOAD" && clientDigest != serverDigest {
r.log.Debug("PutObject", "error", "x-amz-content-sha256 mismatch")
// The S3 API responds with an XML formatted error message.
mismatchErr := NewContentSHA256MismatchError(clientDigest, serverDigest)
Expand Down Expand Up @@ -166,6 +173,9 @@ func (r Router) Serve(w http.ResponseWriter, req *http.Request) {
objectLockLegalHoldStatus: req.Header.Get("x-amz-object-lock-legal-hold"),
objectLockMode: req.Header.Get("x-amz-object-lock-mode"),
objectLockRetainUntilDate: retentionTime,
sseCustomerAlgorithm: req.Header.Get("x-amz-server-side-encryption-customer-algorithm"),
sseCustomerKey: req.Header.Get("x-amz-server-side-encryption-customer-key"),
sseCustomerKeyMD5: req.Header.Get("x-amz-server-side-encryption-customer-key-MD5"),
log: r.log,
}

Expand All @@ -188,7 +198,8 @@ func (r Router) Serve(w http.ResponseWriter, req *http.Request) {
// If the client intentionally sends a mismatching content digest, we would take the client request, rewrap it,
// calculate the correct digest for the new body and NOT get an error.
// Thus we have to check incoming requets for matching content digests.
if clientDigest != "" && clientDigest != serverDigest {
// UNSIGNED-PAYLOAD can be used to disabled payload signing. In that case we don't check the content digest.
if clientDigest != "" && clientDigest != "UNSIGNED-PAYLOAD" && clientDigest != serverDigest {
r.log.Debug("PutObject", "error", "x-amz-content-sha256 mismatch")
// The S3 API responds with an XML formatted error message.
mismatchErr := NewContentSHA256MismatchError(clientDigest, serverDigest)
Expand Down Expand Up @@ -232,6 +243,9 @@ func (r Router) Serve(w http.ResponseWriter, req *http.Request) {
objectLockLegalHoldStatus: req.Header.Get("x-amz-object-lock-legal-hold"),
objectLockMode: req.Header.Get("x-amz-object-lock-mode"),
objectLockRetainUntilDate: retentionTime,
sseCustomerAlgorithm: req.Header.Get("x-amz-server-side-encryption-customer-algorithm"),
sseCustomerKey: req.Header.Get("x-amz-server-side-encryption-customer-key"),
sseCustomerKeyMD5: req.Header.Get("x-amz-server-side-encryption-customer-key-MD5"),
log: r.log,
}

Expand Down

0 comments on commit f6eb31e

Please sign in to comment.