mirror of
https://github.com/minio/minio.git
synced 2025-11-20 18:06:10 -05:00
add SSE-KMS support and use SSE-KMS for auto encryption (#11767)
This commit adds basic SSE-KMS support. Now, a client can specify the SSE-KMS headers (algorithm, optional key-id, optional context) such that the object gets encrypted using the SSE-KMS method. Further, auto-encryption now defaults to SSE-KMS. This commit does not try to do any refactoring and instead tries to implement SSE-KMS as a minimal change to the code base. However, refactoring the entire crypto-related code is planned - but needs a separate effort. Signed-off-by: Andreas Auernhammer <aead@mail.de> Co-authored-by: Klaus Post <klauspost@gmail.com>
This commit is contained in:
committed by
GitHub
parent
3a0e7347ca
commit
26f1fcab7d
@@ -26,6 +26,7 @@ import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
@@ -36,6 +37,7 @@ import (
|
||||
xhttp "github.com/minio/minio/cmd/http"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/fips"
|
||||
"github.com/minio/minio/pkg/kms"
|
||||
"github.com/minio/sio"
|
||||
)
|
||||
|
||||
@@ -116,11 +118,71 @@ func ParseSSECustomerHeader(header http.Header) (key []byte, err error) {
|
||||
}
|
||||
|
||||
// This function rotates old to new key.
|
||||
func rotateKey(oldKey []byte, newKey []byte, bucket, object string, metadata map[string]string) error {
|
||||
switch {
|
||||
default:
|
||||
return errObjectTampered
|
||||
case crypto.SSEC.IsEncrypted(metadata):
|
||||
func rotateKey(oldKey []byte, newKeyID string, newKey []byte, bucket, object string, metadata map[string]string, ctx crypto.Context) error {
|
||||
kind, _ := crypto.IsEncrypted(metadata)
|
||||
switch kind {
|
||||
case crypto.S3:
|
||||
if GlobalKMS == nil {
|
||||
return errKMSNotConfigured
|
||||
}
|
||||
keyID, kmsKey, sealedKey, err := crypto.S3.ParseMetadata(metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
oldKey, err := GlobalKMS.DecryptKey(keyID, kmsKey, kms.Context{bucket: path.Join(bucket, object)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var objectKey crypto.ObjectKey
|
||||
if err = objectKey.Unseal(oldKey, sealedKey, crypto.S3.String(), bucket, object); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newKey, err := GlobalKMS.GenerateKey("", kms.Context{bucket: path.Join(bucket, object)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sealedKey = objectKey.Seal(newKey.Plaintext, crypto.GenerateIV(rand.Reader), crypto.S3.String(), bucket, object)
|
||||
crypto.S3.CreateMetadata(metadata, newKey.KeyID, newKey.Ciphertext, sealedKey)
|
||||
return nil
|
||||
case crypto.S3KMS:
|
||||
if GlobalKMS == nil {
|
||||
return errKMSNotConfigured
|
||||
}
|
||||
objectKey, err := crypto.S3KMS.UnsealObjectKey(GlobalKMS, metadata, bucket, object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(ctx) == 0 {
|
||||
_, _, _, ctx, err = crypto.S3KMS.ParseMetadata(metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// If the context does not contain the bucket key
|
||||
// we must add it for key generation. However,
|
||||
// the context must be stored exactly like the
|
||||
// client provided it. Therefore, we delete the
|
||||
// bucket key, if added by us, after generating
|
||||
// the key.
|
||||
_, ctxContainsBucket := ctx[bucket]
|
||||
if !ctxContainsBucket {
|
||||
ctx[bucket] = path.Join(bucket, object)
|
||||
}
|
||||
newKey, err := GlobalKMS.GenerateKey(newKeyID, ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ctxContainsBucket {
|
||||
delete(ctx, bucket)
|
||||
}
|
||||
|
||||
sealedKey := objectKey.Seal(newKey.Plaintext, crypto.GenerateIV(rand.Reader), crypto.S3KMS.String(), bucket, object)
|
||||
crypto.S3KMS.CreateMetadata(metadata, newKey.KeyID, newKey.Ciphertext, sealedKey, ctx)
|
||||
return nil
|
||||
case crypto.SSEC:
|
||||
sealedKey, err := crypto.SSEC.ParseMetadata(metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -140,40 +202,19 @@ func rotateKey(oldKey []byte, newKey []byte, bucket, object string, metadata map
|
||||
sealedKey = objectKey.Seal(newKey, sealedKey.IV, crypto.SSEC.String(), bucket, object)
|
||||
crypto.SSEC.CreateMetadata(metadata, sealedKey)
|
||||
return nil
|
||||
case crypto.S3.IsEncrypted(metadata):
|
||||
if GlobalKMS == nil {
|
||||
return errKMSNotConfigured
|
||||
}
|
||||
keyID, kmsKey, sealedKey, err := crypto.S3.ParseMetadata(metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
oldKey, err := GlobalKMS.DecryptKey(keyID, kmsKey, crypto.Context{bucket: path.Join(bucket, object)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var objectKey crypto.ObjectKey
|
||||
if err = objectKey.Unseal(oldKey, sealedKey, crypto.S3.String(), bucket, object); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newKey, err := GlobalKMS.GenerateKey("", crypto.Context{bucket: path.Join(bucket, object)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sealedKey = objectKey.Seal(newKey.Plaintext, crypto.GenerateIV(rand.Reader), crypto.S3.String(), bucket, object)
|
||||
crypto.S3.CreateMetadata(metadata, newKey.KeyID, newKey.Ciphertext, sealedKey)
|
||||
return nil
|
||||
default:
|
||||
return errObjectTampered
|
||||
}
|
||||
}
|
||||
|
||||
func newEncryptMetadata(key []byte, bucket, object string, metadata map[string]string, sseS3 bool) (crypto.ObjectKey, error) {
|
||||
func newEncryptMetadata(kind crypto.Type, keyID string, key []byte, bucket, object string, metadata map[string]string, ctx kms.Context) (crypto.ObjectKey, error) {
|
||||
var sealedKey crypto.SealedKey
|
||||
if sseS3 {
|
||||
switch kind {
|
||||
case crypto.S3:
|
||||
if GlobalKMS == nil {
|
||||
return crypto.ObjectKey{}, errKMSNotConfigured
|
||||
}
|
||||
key, err := GlobalKMS.GenerateKey("", crypto.Context{bucket: path.Join(bucket, object)})
|
||||
key, err := GlobalKMS.GenerateKey("", kms.Context{bucket: path.Join(bucket, object)})
|
||||
if err != nil {
|
||||
return crypto.ObjectKey{}, err
|
||||
}
|
||||
@@ -182,15 +223,45 @@ func newEncryptMetadata(key []byte, bucket, object string, metadata map[string]s
|
||||
sealedKey = objectKey.Seal(key.Plaintext, crypto.GenerateIV(rand.Reader), crypto.S3.String(), bucket, object)
|
||||
crypto.S3.CreateMetadata(metadata, key.KeyID, key.Ciphertext, sealedKey)
|
||||
return objectKey, nil
|
||||
case crypto.S3KMS:
|
||||
if GlobalKMS == nil {
|
||||
return crypto.ObjectKey{}, errKMSNotConfigured
|
||||
}
|
||||
|
||||
// If the context does not contain the bucket key
|
||||
// we must add it for key generation. However,
|
||||
// the context must be stored exactly like the
|
||||
// client provided it. Therefore, we delete the
|
||||
// bucket key, if added by us, after generating
|
||||
// the key.
|
||||
_, ctxContainsBucket := ctx[bucket]
|
||||
if !ctxContainsBucket {
|
||||
ctx[bucket] = path.Join(bucket, object)
|
||||
}
|
||||
key, err := GlobalKMS.GenerateKey(keyID, ctx)
|
||||
if err != nil {
|
||||
return crypto.ObjectKey{}, err
|
||||
}
|
||||
if !ctxContainsBucket {
|
||||
delete(ctx, bucket)
|
||||
}
|
||||
|
||||
objectKey := crypto.GenerateKey(key.Plaintext, rand.Reader)
|
||||
sealedKey = objectKey.Seal(key.Plaintext, crypto.GenerateIV(rand.Reader), crypto.S3KMS.String(), bucket, object)
|
||||
crypto.S3KMS.CreateMetadata(metadata, key.KeyID, key.Ciphertext, sealedKey, ctx)
|
||||
return objectKey, nil
|
||||
case crypto.SSEC:
|
||||
objectKey := crypto.GenerateKey(key, rand.Reader)
|
||||
sealedKey = objectKey.Seal(key, crypto.GenerateIV(rand.Reader), crypto.SSEC.String(), bucket, object)
|
||||
crypto.SSEC.CreateMetadata(metadata, sealedKey)
|
||||
return objectKey, nil
|
||||
default:
|
||||
return crypto.ObjectKey{}, fmt.Errorf("encryption type '%v' not supported", kind)
|
||||
}
|
||||
objectKey := crypto.GenerateKey(key, rand.Reader)
|
||||
sealedKey = objectKey.Seal(key, crypto.GenerateIV(rand.Reader), crypto.SSEC.String(), bucket, object)
|
||||
crypto.SSEC.CreateMetadata(metadata, sealedKey)
|
||||
return objectKey, nil
|
||||
}
|
||||
|
||||
func newEncryptReader(content io.Reader, key []byte, bucket, object string, metadata map[string]string, sseS3 bool) (io.Reader, crypto.ObjectKey, error) {
|
||||
objectEncryptionKey, err := newEncryptMetadata(key, bucket, object, metadata, sseS3)
|
||||
func newEncryptReader(content io.Reader, kind crypto.Type, keyID string, key []byte, bucket, object string, metadata map[string]string, ctx crypto.Context) (io.Reader, crypto.ObjectKey, error) {
|
||||
objectEncryptionKey, err := newEncryptMetadata(kind, keyID, key, bucket, object, metadata, ctx)
|
||||
if err != nil {
|
||||
return nil, crypto.ObjectKey{}, err
|
||||
}
|
||||
@@ -207,15 +278,24 @@ func newEncryptReader(content io.Reader, key []byte, bucket, object string, meta
|
||||
// SSE-S3
|
||||
func setEncryptionMetadata(r *http.Request, bucket, object string, metadata map[string]string) (err error) {
|
||||
var (
|
||||
key []byte
|
||||
key []byte
|
||||
keyID string
|
||||
ctx crypto.Context
|
||||
)
|
||||
if crypto.SSEC.IsRequested(r.Header) {
|
||||
kind, _ := crypto.IsRequested(r.Header)
|
||||
switch kind {
|
||||
case crypto.SSEC:
|
||||
key, err = ParseSSECustomerRequest(r)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
case crypto.S3KMS:
|
||||
keyID, ctx, err = crypto.S3KMS.ParseHTTP(r.Header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = newEncryptMetadata(key, bucket, object, metadata, crypto.S3.IsRequested(r.Header))
|
||||
_, err = newEncryptMetadata(kind, keyID, key, bucket, object, metadata, ctx)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -223,24 +303,32 @@ func setEncryptionMetadata(r *http.Request, bucket, object string, metadata map[
|
||||
// with the client provided key. It also marks the object as client-side-encrypted
|
||||
// and sets the correct headers.
|
||||
func EncryptRequest(content io.Reader, r *http.Request, bucket, object string, metadata map[string]string) (io.Reader, crypto.ObjectKey, error) {
|
||||
if crypto.S3.IsRequested(r.Header) && crypto.SSEC.IsRequested(r.Header) {
|
||||
return nil, crypto.ObjectKey{}, crypto.ErrIncompatibleEncryptionMethod
|
||||
}
|
||||
if r.ContentLength > encryptBufferThreshold {
|
||||
// The encryption reads in blocks of 64KB.
|
||||
// We add a buffer on bigger files to reduce the number of syscalls upstream.
|
||||
content = bufio.NewReaderSize(content, encryptBufferSize)
|
||||
}
|
||||
|
||||
var key []byte
|
||||
if crypto.SSEC.IsRequested(r.Header) {
|
||||
var err error
|
||||
var (
|
||||
key []byte
|
||||
keyID string
|
||||
ctx crypto.Context
|
||||
err error
|
||||
)
|
||||
kind, _ := crypto.IsRequested(r.Header)
|
||||
if kind == crypto.SSEC {
|
||||
key, err = ParseSSECustomerRequest(r)
|
||||
if err != nil {
|
||||
return nil, crypto.ObjectKey{}, err
|
||||
}
|
||||
}
|
||||
return newEncryptReader(content, key, bucket, object, metadata, crypto.S3.IsRequested(r.Header))
|
||||
if kind == crypto.S3KMS {
|
||||
keyID, ctx, err = crypto.S3KMS.ParseHTTP(r.Header)
|
||||
if err != nil {
|
||||
return nil, crypto.ObjectKey{}, err
|
||||
}
|
||||
}
|
||||
return newEncryptReader(content, kind, keyID, key, bucket, object, metadata, ctx)
|
||||
}
|
||||
|
||||
func decryptObjectInfo(key []byte, bucket, object string, metadata map[string]string) ([]byte, error) {
|
||||
@@ -590,7 +678,7 @@ func getDecryptedETag(headers http.Header, objInfo ObjectInfo, copySource bool)
|
||||
// Since server side copy with same source and dest just replaces the ETag, we save
|
||||
// encrypted content MD5Sum as ETag for both SSE-C and SSE-S3, we standardize the ETag
|
||||
// encryption across SSE-C and SSE-S3, and only return last 32 bytes for SSE-C
|
||||
if crypto.SSEC.IsEncrypted(objInfo.UserDefined) && !copySource {
|
||||
if (crypto.SSEC.IsEncrypted(objInfo.UserDefined) || crypto.S3KMS.IsEncrypted(objInfo.UserDefined)) && !copySource {
|
||||
return objInfo.ETag[len(objInfo.ETag)-32:]
|
||||
}
|
||||
|
||||
@@ -774,7 +862,7 @@ func DecryptObjectInfo(info *ObjectInfo, r *http.Request) (encrypted bool, err e
|
||||
// disallow X-Amz-Server-Side-Encryption header on HEAD and GET
|
||||
switch r.Method {
|
||||
case http.MethodGet, http.MethodHead:
|
||||
if crypto.S3.IsRequested(headers) {
|
||||
if crypto.S3.IsRequested(headers) || crypto.S3KMS.IsRequested(headers) {
|
||||
return false, errInvalidEncryptionParameters
|
||||
}
|
||||
}
|
||||
@@ -797,6 +885,12 @@ func DecryptObjectInfo(info *ObjectInfo, r *http.Request) (encrypted bool, err e
|
||||
}
|
||||
}
|
||||
|
||||
if crypto.S3KMS.IsEncrypted(info.UserDefined) && r.Header.Get(xhttp.AmzCopySource) == "" {
|
||||
if crypto.SSEC.IsRequested(headers) || crypto.SSECopy.IsRequested(headers) {
|
||||
return encrypted, errEncryptedObject
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = info.DecryptedSize(); err != nil {
|
||||
return encrypted, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user