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.
This commit is contained in:
Andreas Auernhammer 2018-10-16 19:02:19 +02:00 committed by kannappanr
parent 557f382477
commit baec331e84
4 changed files with 88 additions and 0 deletions

View file

@ -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
}

View file

@ -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)
}
}
}

View file

@ -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 }

View file

@ -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)
}
}
}