From 8e4a45ec41720d1669bc82a0f41ca38ce78a5c71 Mon Sep 17 00:00:00 2001 From: Klaus Post Date: Wed, 31 Aug 2022 17:13:23 +0200 Subject: [PATCH] fix: encrypt checksums in metadata (#15620) --- cmd/api-response.go | 9 ++- cmd/encryption-v1.go | 59 +++++++++++++++ cmd/erasure-multipart.go | 7 +- cmd/erasure-object.go | 5 +- cmd/object-api-datatypes.go | 5 +- cmd/object-api-interface.go | 3 + cmd/object-api-utils.go | 32 ++------ cmd/object-handlers-common.go | 2 +- cmd/object-handlers.go | 5 +- cmd/object-multipart-handlers.go | 10 ++- cmd/storage-datatypes.go | 3 +- cmd/storage-datatypes_gen.go | 123 ++++--------------------------- cmd/xl-storage-format-v2.go | 12 +-- internal/hash/checksum.go | 5 +- 14 files changed, 118 insertions(+), 162 deletions(-) diff --git a/cmd/api-response.go b/cmd/api-response.go index 60405d619..99666a07a 100644 --- a/cmd/api-response.go +++ b/cmd/api-response.go @@ -704,16 +704,17 @@ func generateInitiateMultipartUploadResponse(bucket, key, uploadID string) Initi // generates CompleteMultipartUploadResponse for given bucket, key, location and ETag. func generateCompleteMultpartUploadResponse(bucket, key, location string, oi ObjectInfo) CompleteMultipartUploadResponse { + cs := oi.decryptChecksums() c := CompleteMultipartUploadResponse{ Location: location, Bucket: bucket, Key: key, // AWS S3 quotes the ETag in XML, make sure we are compatible here. ETag: "\"" + oi.ETag + "\"", - ChecksumSHA1: oi.Checksum[hash.ChecksumSHA1.String()], - ChecksumSHA256: oi.Checksum[hash.ChecksumSHA256.String()], - ChecksumCRC32: oi.Checksum[hash.ChecksumCRC32.String()], - ChecksumCRC32C: oi.Checksum[hash.ChecksumCRC32C.String()], + ChecksumSHA1: cs[hash.ChecksumSHA1.String()], + ChecksumSHA256: cs[hash.ChecksumSHA256.String()], + ChecksumCRC32: cs[hash.ChecksumCRC32.String()], + ChecksumCRC32C: cs[hash.ChecksumCRC32C.String()], } return c } diff --git a/cmd/encryption-v1.go b/cmd/encryption-v1.go index 9a947905a..18a71489a 100644 --- a/cmd/encryption-v1.go +++ b/cmd/encryption-v1.go @@ -19,6 +19,7 @@ package cmd import ( "bufio" + "bytes" "context" "crypto/hmac" "crypto/rand" @@ -37,6 +38,7 @@ import ( "github.com/minio/minio/internal/crypto" "github.com/minio/minio/internal/etag" "github.com/minio/minio/internal/fips" + "github.com/minio/minio/internal/hash" "github.com/minio/minio/internal/hash/sha256" xhttp "github.com/minio/minio/internal/http" "github.com/minio/minio/internal/kms" @@ -1052,3 +1054,60 @@ func deriveClientKey(clientKey [32]byte, bucket, object string) [32]byte { mac.Sum(key[:0]) return key } + +type ( + objectMetaEncryptFn func(baseKey string, data []byte) []byte + objectMetaDecryptFn func(baseKey string, data []byte) ([]byte, error) +) + +// metadataEncrypter returns a function that will read data from input, +// encrypt it using the provided key and return the result. +// 0 sized inputs are passed through. +func metadataEncrypter(key crypto.ObjectKey) objectMetaEncryptFn { + return func(baseKey string, data []byte) []byte { + if len(data) == 0 { + return data + } + var buffer bytes.Buffer + mac := hmac.New(sha256.New, key[:]) + mac.Write([]byte(baseKey)) + if _, err := sio.Encrypt(&buffer, bytes.NewReader(data), sio.Config{Key: mac.Sum(nil), CipherSuites: fips.DARECiphers()}); err != nil { + logger.CriticalIf(context.Background(), errors.New("unable to encrypt using object key")) + } + return buffer.Bytes() + } +} + +// metadataDecrypter reverses metadataEncrypter. +func (o *ObjectInfo) metadataDecrypter() objectMetaDecryptFn { + return func(baseKey string, input []byte) ([]byte, error) { + if len(input) == 0 { + return input, nil + } + + key, err := decryptObjectInfo(nil, o.Bucket, o.Name, o.UserDefined) + if err != nil { + return nil, err + } + mac := hmac.New(sha256.New, key) + mac.Write([]byte(baseKey)) + return sio.DecryptBuffer(nil, input, sio.Config{Key: mac.Sum(nil), CipherSuites: fips.DARECiphers()}) + } +} + +// decryptChecksums will attempt to decode checksums and return it/them if set. +func (o *ObjectInfo) decryptChecksums() map[string]string { + data := o.Checksum + if len(data) == 0 { + return nil + } + if _, encrypted := crypto.IsEncrypted(o.UserDefined); encrypted { + decrypted, err := o.metadataDecrypter()("object-checksum", data) + if err != nil { + logger.LogIf(GlobalContext, err) + return nil + } + data = decrypted + } + return hash.ReadCheckSums(data) +} diff --git a/cmd/erasure-multipart.go b/cmd/erasure-multipart.go index d2ffd7e4e..632248311 100644 --- a/cmd/erasure-multipart.go +++ b/cmd/erasure-multipart.go @@ -603,7 +603,7 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo return pi, InvalidArgument{ Bucket: bucket, Object: fi.Name, - Err: fmt.Errorf("checksum missing"), + Err: fmt.Errorf("checksum missing, want %s, got %s", cs, r.ContentCRCType().String()), } } } @@ -1170,7 +1170,10 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str } if checksumType.IsSet() { cs := hash.NewChecksumFromData(checksumType, checksumCombined) - fi.Checksum = map[string]string{cs.Type.String(): cs.Encoded} + fi.Checksum = cs.AppendTo(nil) + if opts.EncryptFn != nil { + fi.Checksum = opts.EncryptFn("object-checksum", fi.Checksum) + } } delete(fi.Metadata, hash.MinIOMultipartChecksum) // Not needed in final object. diff --git a/cmd/erasure-object.go b/cmd/erasure-object.go index dcd9c8b78..facc14fbb 100644 --- a/cmd/erasure-object.go +++ b/cmd/erasure-object.go @@ -962,7 +962,10 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st } fi.DataDir = mustGetUUID() - fi.Checksum = opts.WantChecksum.AsMap() + fi.Checksum = opts.WantChecksum.AppendTo(nil) + if opts.EncryptFn != nil { + fi.Checksum = opts.EncryptFn("object-checksum", fi.Checksum) + } uniqueID := mustGetUUID() tempObj := uniqueID diff --git a/cmd/object-api-datatypes.go b/cmd/object-api-datatypes.go index c09c4c122..94b9731f0 100644 --- a/cmd/object-api-datatypes.go +++ b/cmd/object-api-datatypes.go @@ -179,8 +179,9 @@ type ObjectInfo struct { // The modtime of the successor object version if any SuccessorModTime time.Time - // User-Defined object tags - Checksum map[string]string + // Checksums added on upload. + // Encoded, maybe encrypted. + Checksum []byte } // ArchiveInfo returns any saved zip archive meta information diff --git a/cmd/object-api-interface.go b/cmd/object-api-interface.go index 6f81c8f0a..ef4e36230 100644 --- a/cmd/object-api-interface.go +++ b/cmd/object-api-interface.go @@ -78,6 +78,9 @@ type ObjectOptions struct { // Use the maximum parity (N/2), used when saving server configuration files MaxParity bool + // Provides a per object encryption function, allowing metadata encryption. + EncryptFn objectMetaEncryptFn + // SkipDecommissioned set to 'true' if the call requires skipping the pool being decommissioned. // mainly set for certain WRITE operations. SkipDecommissioned bool diff --git a/cmd/object-api-utils.go b/cmd/object-api-utils.go index 81f805bd8..ede145d4e 100644 --- a/cmd/object-api-utils.go +++ b/cmd/object-api-utils.go @@ -20,7 +20,6 @@ package cmd import ( "bytes" "context" - "crypto/hmac" "encoding/hex" "errors" "fmt" @@ -44,15 +43,12 @@ import ( "github.com/minio/minio/internal/config/dns" "github.com/minio/minio/internal/config/storageclass" "github.com/minio/minio/internal/crypto" - "github.com/minio/minio/internal/fips" "github.com/minio/minio/internal/hash" - "github.com/minio/minio/internal/hash/sha256" xhttp "github.com/minio/minio/internal/http" "github.com/minio/minio/internal/ioutil" "github.com/minio/minio/internal/logger" "github.com/minio/pkg/trie" "github.com/minio/pkg/wildcard" - "github.com/minio/sio" ) const ( @@ -720,7 +716,7 @@ func NewGetObjectReader(rs *HTTPRangeSpec, oi ObjectInfo, opts ObjectOptions) ( } // Decompression reader. var dopts []s2.ReaderOption - if off > 0 { + if off > 0 || decOff > 0 { // We are not starting at the beginning, so ignore stream identifiers. dopts = append(dopts, s2.ReaderIgnoreStreamIdentifier()) } @@ -845,6 +841,8 @@ func (g *GetObjectReader) Close() error { return nil } +// compressionIndexEncrypter returns a function that will read data from input, +// encrypt it using the provided key and return the result. func compressionIndexEncrypter(key crypto.ObjectKey, input func() []byte) func() []byte { var data []byte var fetched bool @@ -853,31 +851,13 @@ func compressionIndexEncrypter(key crypto.ObjectKey, input func() []byte) func() data = input() fetched = true } - if len(data) == 0 { - return data - } - var buffer bytes.Buffer - mac := hmac.New(sha256.New, key[:]) - mac.Write([]byte("compression-index")) - if _, err := sio.Encrypt(&buffer, bytes.NewReader(data), sio.Config{Key: mac.Sum(nil), CipherSuites: fips.DARECiphers()}); err != nil { - logger.CriticalIf(context.Background(), errors.New("unable to encrypt compression index using object key")) - } - return buffer.Bytes() + return metadataEncrypter(key)("compression-index", data) } } +// compressionIndexDecrypt reverses compressionIndexEncrypter. func (o *ObjectInfo) compressionIndexDecrypt(input []byte) ([]byte, error) { - if len(input) == 0 { - return input, nil - } - - key, err := decryptObjectInfo(nil, o.Bucket, o.Name, o.UserDefined) - if err != nil { - return nil, err - } - mac := hmac.New(sha256.New, key) - mac.Write([]byte("compression-index")) - return sio.DecryptBuffer(nil, input, sio.Config{Key: mac.Sum(nil), CipherSuites: fips.DARECiphers()}) + return o.metadataDecrypter()("compression-index", input) } // SealMD5CurrFn seals md5sum with object encryption key and returns sealed diff --git a/cmd/object-handlers-common.go b/cmd/object-handlers-common.go index 7921d5cb2..30e932715 100644 --- a/cmd/object-handlers-common.go +++ b/cmd/object-handlers-common.go @@ -267,7 +267,7 @@ func setPutObjHeaders(w http.ResponseWriter, objInfo ObjectInfo, delete bool) { lc.SetPredictionHeaders(w, objInfo.ToLifecycleOpts()) } } - hash.AddChecksumHeader(w, objInfo.Checksum) + hash.AddChecksumHeader(w, objInfo.decryptChecksums()) } func deleteObjectVersions(ctx context.Context, o ObjectLayer, bucket string, toDel []ObjectToDelete) { diff --git a/cmd/object-handlers.go b/cmd/object-handlers.go index 74162e36d..da9d1ea3d 100644 --- a/cmd/object-handlers.go +++ b/cmd/object-handlers.go @@ -520,7 +520,7 @@ func (api objectAPIHandlers) getObjectHandler(ctx context.Context, objectAPI Obj } if r.Header.Get(xhttp.AmzChecksumMode) == "ENABLED" { - hash.AddChecksumHeader(w, objInfo.Checksum) + hash.AddChecksumHeader(w, objInfo.decryptChecksums()) } if err = setObjectHeaders(w, objInfo, rs, opts); err != nil { @@ -788,7 +788,7 @@ func (api objectAPIHandlers) headObjectHandler(ctx context.Context, objectAPI Ob } if r.Header.Get(xhttp.AmzChecksumMode) == "ENABLED" { - hash.AddChecksumHeader(w, objInfo.Checksum) + hash.AddChecksumHeader(w, objInfo.decryptChecksums()) } // Set standard object headers. @@ -1850,6 +1850,7 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req if opts.IndexCB != nil { opts.IndexCB = compressionIndexEncrypter(objectEncryptionKey, opts.IndexCB) } + opts.EncryptFn = metadataEncrypter(objectEncryptionKey) } } diff --git a/cmd/object-multipart-handlers.go b/cmd/object-multipart-handlers.go index a29e49dad..207e2c8ef 100644 --- a/cmd/object-multipart-handlers.go +++ b/cmd/object-multipart-handlers.go @@ -389,8 +389,7 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http return } - rawReader := hashReader - pReader := NewPutObjReader(rawReader) + pReader := NewPutObjReader(hashReader) _, isEncrypted := crypto.IsEncrypted(mi.UserDefined) var objectEncryptionKey crypto.ObjectKey @@ -446,14 +445,21 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) return } + if err := hashReader.AddChecksum(r, true); err != nil { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidChecksum), r.URL) + return + } + pReader, err = pReader.WithEncryption(hashReader, &objectEncryptionKey) if err != nil { writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) return } + if idxCb != nil { idxCb = compressionIndexEncrypter(objectEncryptionKey, idxCb) } + opts.EncryptFn = metadataEncrypter(objectEncryptionKey) } opts.IndexCB = idxCb diff --git a/cmd/storage-datatypes.go b/cmd/storage-datatypes.go index e318b9223..fe3a7b8ce 100644 --- a/cmd/storage-datatypes.go +++ b/cmd/storage-datatypes.go @@ -231,8 +231,7 @@ type FileInfo struct { DiskMTime time.Time `msg:"dmt"` // Combined checksum when object was uploaded. - // Format is type:base64(checksum). - Checksum map[string]string `msg:"cs,allownil"` + Checksum []byte `msg:"cs,allownil"` } // Equals checks if fi(FileInfo) matches ofi(FileInfo) diff --git a/cmd/storage-datatypes_gen.go b/cmd/storage-datatypes_gen.go index 2bcdd4fb8..ee4da0f85 100644 --- a/cmd/storage-datatypes_gen.go +++ b/cmd/storage-datatypes_gen.go @@ -778,43 +778,10 @@ func (z *FileInfo) DecodeMsg(dc *msgp.Reader) (err error) { err = msgp.WrapError(err, "DiskMTime") return } - if dc.IsNil() { - err = dc.ReadNil() - if err != nil { - err = msgp.WrapError(err) - return - } - z.Checksum = nil - } else { - var zb0004 uint32 - zb0004, err = dc.ReadMapHeader() - if err != nil { - err = msgp.WrapError(err, "Checksum") - return - } - if z.Checksum == nil { - z.Checksum = make(map[string]string, zb0004) - } else if len(z.Checksum) > 0 { - for key := range z.Checksum { - delete(z.Checksum, key) - } - } - for zb0004 > 0 { - zb0004-- - var za0004 string - var za0005 string - za0004, err = dc.ReadString() - if err != nil { - err = msgp.WrapError(err, "Checksum") - return - } - za0005, err = dc.ReadString() - if err != nil { - err = msgp.WrapError(err, "Checksum", za0004) - return - } - z.Checksum[za0004] = za0005 - } + z.Checksum, err = dc.ReadBytes(z.Checksum) + if err != nil { + err = msgp.WrapError(err, "Checksum") + return } return } @@ -980,29 +947,10 @@ func (z *FileInfo) EncodeMsg(en *msgp.Writer) (err error) { err = msgp.WrapError(err, "DiskMTime") return } - if z.Checksum == nil { // allownil: if nil - err = en.WriteNil() - if err != nil { - return - } - } else { - err = en.WriteMapHeader(uint32(len(z.Checksum))) - if err != nil { - err = msgp.WrapError(err, "Checksum") - return - } - for za0004, za0005 := range z.Checksum { - err = en.WriteString(za0004) - if err != nil { - err = msgp.WrapError(err, "Checksum") - return - } - err = en.WriteString(za0005) - if err != nil { - err = msgp.WrapError(err, "Checksum", za0004) - return - } - } + err = en.WriteBytes(z.Checksum) + if err != nil { + err = msgp.WrapError(err, "Checksum") + return } return } @@ -1058,15 +1006,7 @@ func (z *FileInfo) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.AppendBool(o, z.Fresh) o = msgp.AppendInt(o, z.Idx) o = msgp.AppendTime(o, z.DiskMTime) - if z.Checksum == nil { // allownil: if nil - o = msgp.AppendNil(o) - } else { - o = msgp.AppendMapHeader(o, uint32(len(z.Checksum))) - for za0004, za0005 := range z.Checksum { - o = msgp.AppendString(o, za0004) - o = msgp.AppendString(o, za0005) - } - } + o = msgp.AppendBytes(o, z.Checksum) return } @@ -1254,39 +1194,10 @@ func (z *FileInfo) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "DiskMTime") return } - if msgp.IsNil(bts) { - bts = bts[1:] - z.Checksum = nil - } else { - var zb0004 uint32 - zb0004, bts, err = msgp.ReadMapHeaderBytes(bts) - if err != nil { - err = msgp.WrapError(err, "Checksum") - return - } - if z.Checksum == nil { - z.Checksum = make(map[string]string, zb0004) - } else if len(z.Checksum) > 0 { - for key := range z.Checksum { - delete(z.Checksum, key) - } - } - for zb0004 > 0 { - var za0004 string - var za0005 string - zb0004-- - za0004, bts, err = msgp.ReadStringBytes(bts) - if err != nil { - err = msgp.WrapError(err, "Checksum") - return - } - za0005, bts, err = msgp.ReadStringBytes(bts) - if err != nil { - err = msgp.WrapError(err, "Checksum", za0004) - return - } - z.Checksum[za0004] = za0005 - } + z.Checksum, bts, err = msgp.ReadBytesBytes(bts, z.Checksum) + if err != nil { + err = msgp.WrapError(err, "Checksum") + return } o = bts return @@ -1305,13 +1216,7 @@ func (z *FileInfo) Msgsize() (s int) { for za0003 := range z.Parts { s += z.Parts[za0003].Msgsize() } - s += z.Erasure.Msgsize() + msgp.BoolSize + z.ReplicationState.Msgsize() + msgp.BytesPrefixSize + len(z.Data) + msgp.IntSize + msgp.TimeSize + msgp.BoolSize + msgp.IntSize + msgp.TimeSize + msgp.MapHeaderSize - if z.Checksum != nil { - for za0004, za0005 := range z.Checksum { - _ = za0005 - s += msgp.StringPrefixSize + len(za0004) + msgp.StringPrefixSize + len(za0005) - } - } + s += z.Erasure.Msgsize() + msgp.BoolSize + z.ReplicationState.Msgsize() + msgp.BytesPrefixSize + len(z.Data) + msgp.IntSize + msgp.TimeSize + msgp.BoolSize + msgp.IntSize + msgp.TimeSize + msgp.BytesPrefixSize + len(z.Checksum) return } diff --git a/cmd/xl-storage-format-v2.go b/cmd/xl-storage-format-v2.go index d7e4b2ea3..14a25ffc3 100644 --- a/cmd/xl-storage-format-v2.go +++ b/cmd/xl-storage-format-v2.go @@ -36,7 +36,6 @@ import ( jsoniter "github.com/json-iterator/go" "github.com/minio/minio/internal/bucket/lifecycle" "github.com/minio/minio/internal/bucket/replication" - "github.com/minio/minio/internal/hash" xhttp "github.com/minio/minio/internal/http" "github.com/minio/minio/internal/logger" "github.com/tinylib/msgp/msgp" @@ -640,7 +639,7 @@ func (j xlMetaV2Object) ToFileInfo(volume, path string) (FileInfo, error) { fi.TransitionTier = string(sc) } if crcs := j.MetaSys[ReservedMetadataPrefixLower+"crc"]; len(crcs) > 0 { - fi.Checksum = hash.ReadCheckSums(crcs) + fi.Checksum = crcs } return fi, nil } @@ -1541,14 +1540,7 @@ func (x *xlMetaV2) AddVersion(fi FileInfo) error { ventry.ObjectV2.MetaSys[ReservedMetadataPrefixLower+TransitionTier] = []byte(fi.TransitionTier) } if len(fi.Checksum) > 0 { - res := make([]byte, 0, len(fi.Checksum)*40) - for k, v := range fi.Checksum { - crc := hash.NewChecksumString(k, v) - if crc.Valid() { - res = crc.AppendTo(res) - } - } - ventry.ObjectV2.MetaSys[ReservedMetadataPrefixLower+"crc"] = res + ventry.ObjectV2.MetaSys[ReservedMetadataPrefixLower+"crc"] = fi.Checksum } } diff --git a/internal/hash/checksum.go b/internal/hash/checksum.go index 56b8f7a27..0efdae62a 100644 --- a/internal/hash/checksum.go +++ b/internal/hash/checksum.go @@ -215,7 +215,10 @@ func NewChecksumString(alg, value string) *Checksum { // AppendTo will append the checksum to b. // ReadCheckSums reads the values back. -func (c Checksum) AppendTo(b []byte) []byte { +func (c *Checksum) AppendTo(b []byte) []byte { + if c == nil { + return nil + } var tmp [binary.MaxVarintLen32]byte n := binary.PutUvarint(tmp[:], uint64(c.Type)) crc := c.Raw()