Skip to content

Commit

Permalink
s3proxy: add encryption/decryption
Browse files Browse the repository at this point in the history
This implementation currently uses a static key.
Do not use with sensitive data; testing only.
  • Loading branch information
derpsteb committed Sep 27, 2023
1 parent 2518ee9 commit 49061ef
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 2 deletions.
8 changes: 8 additions & 0 deletions s3proxy/internal/crypto/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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__"],
)
79 changes: 79 additions & 0 deletions s3proxy/internal/crypto/crypto.go
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 1 addition & 0 deletions s3proxy/internal/router/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
Expand Down
35 changes: 33 additions & 2 deletions s3proxy/internal/router/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)

Expand Down

0 comments on commit 49061ef

Please sign in to comment.