mirror of https://github.com/minio/minio.git
				
				
				
			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