mirror of
https://github.com/minio/minio.git
synced 2024-12-24 22:25:54 -05:00
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:
parent
557f382477
commit
baec331e84
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 }
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user