From baec331e843507cb4da97ffad13ce133bf38437b Mon Sep 17 00:00:00 2001 From: Andreas Auernhammer Date: Tue, 16 Oct 2018 19:02:19 +0200 Subject: [PATCH] crypto: add functions for sealing/unsealing the etag for SSE (#6618) This commit adds two functions for sealing/unsealing the etag (a.k.a. content MD5) in case of SSE single-part upload. Sealing the ETag is neccessary in case of SSE-S3 to preserve the security guarantees. In case of SSE-S3 AWS returns the content-MD5 of the plaintext object as ETag. However, we must not store the MD5 of the plaintext for encrypted objects. Otherwise it becomes possible for an attacker to detect equal/non-equal encrypted objects. Therefore we encrypt the ETag before storing on the backend. But we only need to encrypt the ETag (content-MD5) if the client send it - otherwise the client cannot verify it anyway. --- cmd/crypto/key.go | 34 ++++++++++++++++++++++++++++++++++ cmd/crypto/key_test.go | 28 ++++++++++++++++++++++++++++ cmd/crypto/metadata.go | 3 +++ cmd/crypto/metadata_test.go | 23 +++++++++++++++++++++++ 4 files changed, 88 insertions(+) diff --git a/cmd/crypto/key.go b/cmd/crypto/key.go index 0812aa5de..46dcf9111 100644 --- a/cmd/crypto/key.go +++ b/cmd/crypto/key.go @@ -140,3 +140,37 @@ func (key ObjectKey) DerivePartKey(id uint32) (partKey [32]byte) { mac.Sum(partKey[:0]) return partKey } + +// SealETag seals the etag using the object key. +// It does not encrypt empty ETags because such ETags indicate +// that the S3 client hasn't sent an ETag = MD5(object) and +// the backend can pick an ETag value. +func (key ObjectKey) SealETag(etag []byte) []byte { + if len(etag) == 0 { // don't encrypt empty ETag - only if client sent ETag = MD5(object) + return etag + } + var buffer bytes.Buffer + mac := hmac.New(sha256.New, key[:]) + mac.Write([]byte("SSE-etag")) + if _, err := sio.Encrypt(&buffer, bytes.NewReader(etag), sio.Config{Key: mac.Sum(nil)}); err != nil { + logger.CriticalIf(context.Background(), errors.New("Unable to encrypt ETag using object key")) + } + return buffer.Bytes() +} + +// UnsealETag unseals the etag using the provided object key. +// It does not try to decrypt the ETag if len(etag) == 16 +// because such ETags indicate that the S3 client hasn't sent +// an ETag = MD5(object) and the backend has picked an ETag value. +func (key ObjectKey) UnsealETag(etag []byte) ([]byte, error) { + if !IsETagSealed(etag) { + return etag, nil + } + var buffer bytes.Buffer + mac := hmac.New(sha256.New, key[:]) + mac.Write([]byte("SSE-etag")) + if _, err := sio.Decrypt(&buffer, bytes.NewReader(etag), sio.Config{Key: mac.Sum(nil)}); err != nil { + return nil, err + } + return buffer.Bytes(), nil +} diff --git a/cmd/crypto/key_test.go b/cmd/crypto/key_test.go index ff5f38081..6f7378c88 100644 --- a/cmd/crypto/key_test.go +++ b/cmd/crypto/key_test.go @@ -166,3 +166,31 @@ func TestDerivePartKey(t *testing.T) { } } } + +var sealUnsealETagTests = []string{ + "", + "90682b8e8cc7609c", + "90682b8e8cc7609c4671e1d64c73fc30", + "90682b8e8cc7609c4671e1d64c73fc307fb3104f", +} + +func TestSealETag(t *testing.T) { + var key ObjectKey + for i := range key { + key[i] = byte(i) + } + for i, etag := range sealUnsealETagTests { + tag, err := hex.DecodeString(etag) + if err != nil { + t.Errorf("Test %d: failed to decode etag: %s", i, err) + } + sealedETag := key.SealETag(tag) + unsealedETag, err := key.UnsealETag(sealedETag) + if err != nil { + t.Errorf("Test %d: failed to decrypt etag: %s", i, err) + } + if !bytes.Equal(unsealedETag, tag) { + t.Errorf("Test %d: unsealed etag does not match: got %s - want %s", i, hex.EncodeToString(unsealedETag), etag) + } + } +} diff --git a/cmd/crypto/metadata.go b/cmd/crypto/metadata.go index f833da454..a85e598c5 100644 --- a/cmd/crypto/metadata.go +++ b/cmd/crypto/metadata.go @@ -219,3 +219,6 @@ func (ssec) ParseMetadata(metadata map[string]string) (sealedKey SealedKey, err copy(sealedKey.Key[:], encryptedKey) return sealedKey, nil } + +// IsETagSealed returns true if the etag seems to be encrypted. +func IsETagSealed(etag []byte) bool { return len(etag) > 16 } diff --git a/cmd/crypto/metadata_test.go b/cmd/crypto/metadata_test.go index e357ed5bc..3fc448eca 100644 --- a/cmd/crypto/metadata_test.go +++ b/cmd/crypto/metadata_test.go @@ -17,6 +17,7 @@ package crypto import ( "bytes" "encoding/base64" + "encoding/hex" "testing" "github.com/minio/minio/cmd/logger" @@ -364,3 +365,25 @@ func TestSSECCreateMetadata(t *testing.T) { }() _ = SSEC.CreateMetadata(nil, SealedKey{Algorithm: InsecureSealAlgorithm}) } + +var isETagSealedTests = []struct { + ETag string + IsSealed bool +}{ + {ETag: "", IsSealed: false}, // 0 + {ETag: "90682b8e8cc7609c4671e1d64c73fc30", IsSealed: false}, // 1 + {ETag: "f201040c9dc593e39ea004dc1323699bcd", IsSealed: true}, // 2 not valid ciphertext but looks like sealed ETag + {ETag: "20000f00fba2ee2ae4845f725964eeb9e092edfabc7ab9f9239e8344341f769a51ce99b4801b0699b92b16a72fa94972", IsSealed: true}, // 3 +} + +func TestIsETagSealed(t *testing.T) { + for i, test := range isETagSealedTests { + etag, err := hex.DecodeString(test.ETag) + if err != nil { + t.Errorf("Test %d: failed to decode etag: %s", i, err) + } + if sealed := IsETagSealed(etag); sealed != test.IsSealed { + t.Errorf("Test %d: got %v - want %v", i, sealed, test.IsSealed) + } + } +}