1054 lines · 33.3 KB · Go
RawDownload
// Copyright (c) 2015-2021 MinIO, Inc.//// This file is part of MinIO Object Storage stack//// This program is free software: you can redistribute it and/or modify// it under the terms of the GNU Affero General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// This program is distributed in the hope that it will be useful// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the// GNU Affero General Public License for more details.//// You should have received a copy of the GNU Affero General Public License// along with this program.  If not, see <http://www.gnu.org/licenses/>. package cmd import (	"bufio"	"context"	"crypto/hmac"	"crypto/rand"	"crypto/subtle"	"encoding/binary"	"encoding/hex"	"errors"	"fmt"	"io"	"net/http"	"path"	"strconv"	"strings" 	"github.com/minio/kes"	"github.com/minio/minio/internal/crypto"	"github.com/minio/minio/internal/etag"	"github.com/minio/minio/internal/fips"	"github.com/minio/minio/internal/hash/sha256"	xhttp "github.com/minio/minio/internal/http"	"github.com/minio/minio/internal/kms"	"github.com/minio/minio/internal/logger"	"github.com/minio/sio") var (	// AWS errors for invalid SSE-C requests.	errEncryptedObject      = errors.New("The object was stored using a form of SSE")	errInvalidSSEParameters = errors.New("The SSE-C key for key-rotation is not correct") // special access denied	errKMSNotConfigured     = errors.New("KMS not configured for a server side encrypted object")	errKMSKeyNotFound       = errors.New("Invalid KMS keyId")	// Additional MinIO errors for SSE-C requests.	errObjectTampered = errors.New("The requested object was modified and may be compromised")	// error returned when invalid encryption parameters are specified	errInvalidEncryptionParameters = errors.New("The encryption parameters are not applicable to this object")) const (	// SSECustomerKeySize is the size of valid client provided encryption keys in bytes.	// Currently AWS supports only AES256. So the SSE-C key size is fixed to 32 bytes.	SSECustomerKeySize = 32 	// SSEIVSize is the size of the IV data	SSEIVSize = 32 // 32 bytes 	// SSEDAREPackageBlockSize - SSE dare package block size.	SSEDAREPackageBlockSize = 64 * 1024 // 64KiB bytes 	// SSEDAREPackageMetaSize - SSE dare package meta padding bytes.	SSEDAREPackageMetaSize = 32 // 32 bytes ) // KMSKeyID returns in AWS compatible KMS KeyID() format.func (o *ObjectInfo) KMSKeyID() string { return kmsKeyIDFromMetadata(o.UserDefined) } // KMSKeyID returns in AWS compatible KMS KeyID() format.func (o *MultipartInfo) KMSKeyID() string { return kmsKeyIDFromMetadata(o.UserDefined) } // kmsKeyIDFromMetadata returns any AWS S3 KMS key ID in the// metadata, if any. It returns an empty ID if no key ID is// present.func kmsKeyIDFromMetadata(metadata map[string]string) string {	const ARNPrefix = "arn:aws:kms:"	if len(metadata) == 0 {		return ""	}	kmsID, ok := metadata[crypto.MetaKeyID]	if !ok {		return ""	}	if strings.HasPrefix(kmsID, ARNPrefix) {		return kmsID	}	return ARNPrefix + kmsID} // DecryptETags decryptes the ETag of all ObjectInfos using the KMS.//// It adjusts the size of all encrypted objects since encrypted// objects are slightly larger due to encryption overhead.// Further, it decrypts all single-part SSE-S3 encrypted objects// and formats ETags of SSE-C / SSE-KMS encrypted objects to// be AWS S3 compliant.//// DecryptETags uses a KMS bulk decryption API, if available, which// is more efficient than decrypting ETags sequentually.func DecryptETags(ctx context.Context, KMS kms.KMS, objects []ObjectInfo) error {	const BatchSize = 250 // We process the objects in batches - 250 is a reasonable default.	var (		metadata = make([]map[string]string, 0, BatchSize)		buckets  = make([]string, 0, BatchSize)		names    = make([]string, 0, BatchSize)	)	for len(objects) > 0 {		N := BatchSize		if len(objects) < BatchSize {			N = len(objects)		}		batch := objects[:N] 		// We have to decrypt only ETags of SSE-S3 single-part		// objects.		// Therefore, we remember which objects (there index)		// in the current batch are single-part SSE-S3 objects.		metadata = metadata[:0:N]		buckets = buckets[:0:N]		names = names[:0:N]		SSES3SinglePartObjects := make(map[int]bool)		for i, object := range batch {			if kind, ok := crypto.IsEncrypted(object.UserDefined); ok && kind == crypto.S3 && !crypto.IsMultiPart(object.UserDefined) {				SSES3SinglePartObjects[i] = true 				metadata = append(metadata, object.UserDefined)				buckets = append(buckets, object.Bucket)				names = append(names, object.Name)			}		} 		// If there are no SSE-S3 single-part objects		// we can skip the decryption process. However,		// we still have to adjust the size and ETag		// of SSE-C and SSE-KMS objects.		if len(SSES3SinglePartObjects) == 0 {			for i := range batch {				size, err := batch[i].GetActualSize()				if err != nil {					return err				}				batch[i].Size = size 				if _, ok := crypto.IsEncrypted(batch[i].UserDefined); ok {					ETag, err := etag.Parse(batch[i].ETag)					if err != nil {						return err					}					batch[i].ETag = ETag.Format().String()				}			}			objects = objects[N:]			continue		} 		// There is at least one SSE-S3 single-part object.		// For all SSE-S3 single-part objects we have to		// fetch their decryption keys. We do this using		// a Bulk-Decryption API call, if available.		keys, err := crypto.S3.UnsealObjectKeys(ctx, KMS, metadata, buckets, names)		if err != nil {			return err		} 		// Now, we have to decrypt the ETags of SSE-S3 single-part		// objects and adjust the size and ETags of all encrypted		// objects.		for i := range batch {			size, err := batch[i].GetActualSize()			if err != nil {				return err			}			batch[i].Size = size 			if _, ok := crypto.IsEncrypted(batch[i].UserDefined); ok {				ETag, err := etag.Parse(batch[i].ETag)				if err != nil {					return err				}				if SSES3SinglePartObjects[i] && ETag.IsEncrypted() {					ETag, err = etag.Decrypt(keys[0][:], ETag)					if err != nil {						return err					}					keys = keys[1:]				}				batch[i].ETag = ETag.Format().String()			}		}		objects = objects[N:]	}	return nil} // isMultipart returns true if the current object is// uploaded by the user using multipart mechanism:// initiate new multipart, upload part, complete uploadfunc (o *ObjectInfo) isMultipart() bool {	if len(o.Parts) == 0 {		return false	}	_, encrypted := crypto.IsEncrypted(o.UserDefined)	if encrypted {		if !crypto.IsMultiPart(o.UserDefined) {			return false		}		for _, part := range o.Parts {			_, err := sio.DecryptedSize(uint64(part.Size))			if err != nil {				return false			}		}	} 	// Further check if this object is uploaded using multipart mechanism	// by the user and it is not about Erasure internally splitting the	// object into parts in PutObject()	return !(o.backendType == BackendErasure && len(o.ETag) == 32)} // ParseSSECopyCustomerRequest parses the SSE-C header fields of the provided request.// It returns the client provided key on success.func ParseSSECopyCustomerRequest(h http.Header, metadata map[string]string) (key []byte, err error) {	if crypto.S3.IsEncrypted(metadata) && crypto.SSECopy.IsRequested(h) {		return nil, crypto.ErrIncompatibleEncryptionMethod	}	k, err := crypto.SSECopy.ParseHTTP(h)	return k[:], err} // ParseSSECustomerRequest parses the SSE-C header fields of the provided request.// It returns the client provided key on success.func ParseSSECustomerRequest(r *http.Request) (key []byte, err error) {	return ParseSSECustomerHeader(r.Header)} // ParseSSECustomerHeader parses the SSE-C header fields and returns// the client provided key on success.func ParseSSECustomerHeader(header http.Header) (key []byte, err error) {	if crypto.S3.IsRequested(header) && crypto.SSEC.IsRequested(header) {		return key, crypto.ErrIncompatibleEncryptionMethod	} 	k, err := crypto.SSEC.ParseHTTP(header)	return k[:], err} // This function rotates old to new key.func rotateKey(ctx context.Context, oldKey []byte, newKeyID string, newKey []byte, bucket, object string, metadata map[string]string, cryptoCtx kms.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(ctx, "", 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(cryptoCtx) == 0 {			_, _, _, cryptoCtx, 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 create a copy		// of the client provided context and add the bucket		// key, if not present.		kmsCtx := kms.Context{}		for k, v := range cryptoCtx {			kmsCtx[k] = v		}		if _, ok := kmsCtx[bucket]; !ok {			kmsCtx[bucket] = path.Join(bucket, object)		}		newKey, err := GlobalKMS.GenerateKey(ctx, newKeyID, kmsCtx)		if err != nil {			return err		} 		sealedKey := objectKey.Seal(newKey.Plaintext, crypto.GenerateIV(rand.Reader), crypto.S3KMS.String(), bucket, object)		crypto.S3KMS.CreateMetadata(metadata, newKey.KeyID, newKey.Ciphertext, sealedKey, cryptoCtx)		return nil	case crypto.SSEC:		sealedKey, err := crypto.SSEC.ParseMetadata(metadata)		if err != nil {			return err		} 		var objectKey crypto.ObjectKey		if err = objectKey.Unseal(oldKey, sealedKey, crypto.SSEC.String(), bucket, object); err != nil {			if subtle.ConstantTimeCompare(oldKey, newKey) == 1 {				return errInvalidSSEParameters // AWS returns special error for equal but invalid keys.			}			return crypto.ErrInvalidCustomerKey // To provide strict AWS S3 compatibility we return: access denied. 		}		if subtle.ConstantTimeCompare(oldKey, newKey) == 1 && sealedKey.Algorithm == crypto.SealAlgorithm {			return nil // don't rotate on equal keys if seal algorithm is latest		}		sealedKey = objectKey.Seal(newKey, sealedKey.IV, crypto.SSEC.String(), bucket, object)		crypto.SSEC.CreateMetadata(metadata, sealedKey)		return nil	default:		return errObjectTampered	}} func newEncryptMetadata(ctx context.Context, kind crypto.Type, keyID string, key []byte, bucket, object string, metadata map[string]string, cryptoCtx kms.Context) (crypto.ObjectKey, error) {	var sealedKey crypto.SealedKey	switch kind {	case crypto.S3:		if GlobalKMS == nil {			return crypto.ObjectKey{}, errKMSNotConfigured		}		key, err := GlobalKMS.GenerateKey(ctx, "", kms.Context{bucket: path.Join(bucket, object)})		if err != nil {			return crypto.ObjectKey{}, err		} 		objectKey := crypto.GenerateKey(key.Plaintext, rand.Reader)		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 create a copy		// of the client provided context and add the bucket		// key, if not present.		kmsCtx := kms.Context{}		for k, v := range cryptoCtx {			kmsCtx[k] = v		}		if _, ok := kmsCtx[bucket]; !ok {			kmsCtx[bucket] = path.Join(bucket, object)		}		key, err := GlobalKMS.GenerateKey(ctx, keyID, kmsCtx)		if err != nil {			if errors.Is(err, kes.ErrKeyNotFound) {				return crypto.ObjectKey{}, errKMSKeyNotFound			}			return crypto.ObjectKey{}, err		} 		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, cryptoCtx)		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)	}} func newEncryptReader(ctx context.Context, content io.Reader, kind crypto.Type, keyID string, key []byte, bucket, object string, metadata map[string]string, cryptoCtx kms.Context) (io.Reader, crypto.ObjectKey, error) {	objectEncryptionKey, err := newEncryptMetadata(ctx, kind, keyID, key, bucket, object, metadata, cryptoCtx)	if err != nil {		return nil, crypto.ObjectKey{}, err	} 	reader, err := sio.EncryptReader(content, sio.Config{Key: objectEncryptionKey[:], MinVersion: sio.Version20, CipherSuites: fips.DARECiphers()})	if err != nil {		return nil, crypto.ObjectKey{}, crypto.ErrInvalidCustomerKey	} 	return reader, objectEncryptionKey, nil} // set new encryption metadata from http request headers for SSE-C and generated key from KMS in the case of// SSE-S3func setEncryptionMetadata(r *http.Request, bucket, object string, metadata map[string]string) (err error) {	var (		key    []byte		keyID  string		kmsCtx kms.Context	)	kind, _ := crypto.IsRequested(r.Header)	switch kind {	case crypto.SSEC:		key, err = ParseSSECustomerRequest(r)		if err != nil {			return err		}	case crypto.S3KMS:		keyID, kmsCtx, err = crypto.S3KMS.ParseHTTP(r.Header)		if err != nil {			return err		}	}	_, err = newEncryptMetadata(r.Context(), kind, keyID, key, bucket, object, metadata, kmsCtx)	return} // EncryptRequest takes the client provided content and encrypts the data// 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 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		keyID string		ctx   kms.Context		err   error	)	kind, _ := crypto.IsRequested(r.Header)	if kind == crypto.SSEC {		key, err = ParseSSECustomerRequest(r)		if err != nil {			return nil, crypto.ObjectKey{}, err		}	}	if kind == crypto.S3KMS {		keyID, ctx, err = crypto.S3KMS.ParseHTTP(r.Header)		if err != nil {			return nil, crypto.ObjectKey{}, err		}	}	return newEncryptReader(r.Context(), content, kind, keyID, key, bucket, object, metadata, ctx)} func decryptObjectInfo(key []byte, bucket, object string, metadata map[string]string) ([]byte, error) {	switch kind, _ := crypto.IsEncrypted(metadata); kind {	case crypto.S3:		var KMS kms.KMS = GlobalKMS		if isCacheEncrypted(metadata) {			KMS = globalCacheKMS		}		if KMS == nil {			return nil, errKMSNotConfigured		}		objectKey, err := crypto.S3.UnsealObjectKey(KMS, metadata, bucket, object)		if err != nil {			return nil, err		}		return objectKey[:], nil	case crypto.S3KMS:		if GlobalKMS == nil {			return nil, errKMSNotConfigured		}		objectKey, err := crypto.S3KMS.UnsealObjectKey(GlobalKMS, metadata, bucket, object)		if err != nil {			return nil, err		}		return objectKey[:], nil	case crypto.SSEC:		sealedKey, err := crypto.SSEC.ParseMetadata(metadata)		if err != nil {

Lines 1–500 of 1054.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t