From c7e22583f1efb3a864f678b06d74e466835ee7b1 Mon Sep 17 00:00:00 2001 From: Otto Bittner Date: Wed, 27 Sep 2023 11:43:09 +0200 Subject: [PATCH] s3proxy: add encryption/decryption This implementation currently uses a static key. Do not use with sensitive data; testing only. --- s3proxy/internal/crypto/BUILD.bazel | 8 +++ s3proxy/internal/crypto/crypto.go | 79 +++++++++++++++++++++++++++++ s3proxy/internal/router/BUILD.bazel | 1 + s3proxy/internal/router/object.go | 35 ++++++++++++- 4 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 s3proxy/internal/crypto/BUILD.bazel create mode 100644 s3proxy/internal/crypto/crypto.go diff --git a/s3proxy/internal/crypto/BUILD.bazel b/s3proxy/internal/crypto/BUILD.bazel new file mode 100644 index 0000000000..193aef9531 --- /dev/null +++ b/s3proxy/internal/crypto/BUILD.bazel @@ -0,0 +1,8 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "crypto", + srcs = ["crypto.go"], + importpath = "github.com/edgelesssys/constellation/v2/s3proxy/internal/crypto", + visibility = ["//s3proxy:__subpackages__"], +) diff --git a/s3proxy/internal/crypto/crypto.go b/s3proxy/internal/crypto/crypto.go new file mode 100644 index 0000000000..c9c8b2aa13 --- /dev/null +++ b/s3proxy/internal/crypto/crypto.go @@ -0,0 +1,79 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +/* +Package crypto provides encryption and decryption functions for the s3proxy. +It uses AES-256-GCM to encrypt and decrypt data. +A new nonce is generated for each encryption operation. +*/ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "io" +) + +// Encrypt takes a 32 byte key and encrypts a plaintext using AES-256-GCM. +// Output format is 12 byte nonce + ciphertext. +func Encrypt(plaintext, key []byte) ([]byte, error) { + // Enforce AES-256 + if len(key) != 32 { + return nil, aes.KeySizeError(len(key)) + } + + // None should not be reused more often that 2^32 times: + // https://pkg.go.dev/crypto/cipher#NewGCM + // Assuming n encryption operations per second, the key has to be rotated every: + // n=1: 2^32 / (60*60*24*365*10) = 135 years. + // n=10: 2^32 / (60*60*24*365*10) = 13.5 years. + // n=100: 2^32 / (60*60*24*365*10) = 1.3 years. + // n=1000: 2^32 / (60*60*24*365*10) = 50 days. + nonce := make([]byte, 12) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil) + + // Prepend the nonce to the ciphertext. + ciphertext = append(nonce, ciphertext...) + + return ciphertext, nil +} + +// Decrypt takes a 32 byte key and decrypts a ciphertext using AES-256-GCM. +// ciphertext is formatted as 12 byte nonce + ciphertext. +func Decrypt(ciphertext []byte, key []byte) ([]byte, error) { + // Enforce AES-256 + if len(key) != 32 { + return nil, aes.KeySizeError(len(key)) + } + + // Extract the nonce from the ciphertext. + nonce := ciphertext[:12] + ciphertext = ciphertext[12:] + + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + return aesgcm.Open(nil, nonce, ciphertext, nil) +} diff --git a/s3proxy/internal/router/BUILD.bazel b/s3proxy/internal/router/BUILD.bazel index d2202ab6f9..74930522ad 100644 --- a/s3proxy/internal/router/BUILD.bazel +++ b/s3proxy/internal/router/BUILD.bazel @@ -11,6 +11,7 @@ go_library( visibility = ["//s3proxy:__subpackages__"], deps = [ "//internal/logger", + "//s3proxy/internal/crypto", "//s3proxy/internal/s3", "@com_github_aws_aws_sdk_go_v2_service_s3//:s3", ], diff --git a/s3proxy/internal/router/object.go b/s3proxy/internal/router/object.go index f7bf9ee2c9..a9a09d423a 100644 --- a/s3proxy/internal/router/object.go +++ b/s3proxy/internal/router/object.go @@ -18,8 +18,16 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/edgelesssys/constellation/v2/internal/logger" + "github.com/edgelesssys/constellation/v2/s3proxy/internal/crypto" ) +const ( + // testingKey is a temporary encryption key used for testing. + // TODO (derpsteb): This key needs to be fetched from Constellation's keyservice. + testingKey = "01234567890123456789012345678901" + // encryptionTag is the key used to tag objects that are encrypted with this proxy. Presence of the key implies the object needs to be decrypted. + encryptionTag = "constellation-encryption" +) // object bundles data to implement http.Handler methods that use data from incoming requests. type object struct { @@ -73,14 +81,37 @@ func (o object) get(w http.ResponseWriter, r *http.Request) { return } + plaintext := body + decrypt, ok := data.Metadata[encryptionTag] + + if ok && decrypt == "true" { + plaintext, err = crypto.Decrypt(body, []byte(testingKey)) + if err != nil { + o.log.Errorf("GetObject decrypting response", "error", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + w.WriteHeader(http.StatusOK) - if _, err := w.Write(body); err != nil { + if _, err := w.Write(plaintext); err != nil { o.log.Errorf("GetObject sending response", "error", err) } } func (o object) put(w http.ResponseWriter, r *http.Request) { - output, err := o.client.PutObject(r.Context(), o.bucket, o.key, o.tags, o.contentType, o.objectLockLegalHoldStatus, o.objectLockMode, o.objectLockRetainUntilDate, o.metadata, o.data) + ciphertext, err := crypto.Encrypt(o.data, []byte(testingKey)) + if err != nil { + o.log.Errorf("PutObject", "error", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + // We need to tag objects that are encrypted with this proxy, + // because there might be objects in a bucket that are not encrypted. + // 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) if err != nil { o.log.Errorf("PutObject sending request to S3", "error", err)