CopyObject must preserve checksums and encrypt them if required (#21399)

This commit is contained in:
Mark Theunissen
2025-06-25 17:08:54 +02:00
committed by GitHub
parent a65292cab1
commit 2718d9a430
12 changed files with 396 additions and 21 deletions

View File

@@ -1074,8 +1074,16 @@ func (o *ObjectInfo) metadataDecrypter(h http.Header) objectMetaDecryptFn {
return input, nil
}
var key []byte
if k, err := crypto.SSEC.ParseHTTP(h); err == nil {
key = k[:]
if crypto.SSECopy.IsRequested(h) {
sseCopyKey, err := crypto.SSECopy.ParseHTTP(h)
if err != nil {
return nil, err
}
key = sseCopyKey[:]
} else {
if k, err := crypto.SSEC.ParseHTTP(h); err == nil {
key = k[:]
}
}
key, err := decryptObjectMeta(key, o.Bucket, o.Name, o.UserDefined)
if err != nil {
@@ -1087,7 +1095,8 @@ func (o *ObjectInfo) metadataDecrypter(h http.Header) objectMetaDecryptFn {
}
}
// decryptPartsChecksums will attempt to decode checksums and return it/them if set.
// decryptPartsChecksums will attempt to decrypt and decode part checksums, and save
// only the decrypted part checksum values on ObjectInfo directly.
// if part > 0, and we have the checksum for the part that will be returned.
func (o *ObjectInfo) decryptPartsChecksums(h http.Header) {
data := o.Checksum
@@ -1112,6 +1121,23 @@ func (o *ObjectInfo) decryptPartsChecksums(h http.Header) {
}
}
// decryptChecksum will attempt to decrypt the ObjectInfo.Checksum, returns the decrypted value
// An error is only returned if it was encrypted and the decryption failed.
func (o *ObjectInfo) decryptChecksum(h http.Header) ([]byte, error) {
data := o.Checksum
if len(data) == 0 {
return data, nil
}
if _, encrypted := crypto.IsEncrypted(o.UserDefined); encrypted {
decrypted, err := o.metadataDecrypter(h)("object-checksum", data)
if err != nil {
return nil, err
}
data = decrypted
}
return data, nil
}
// metadataEncryptFn provides an encryption function for metadata.
// Will return nil, nil if unencrypted.
func (o *ObjectInfo) metadataEncryptFn(headers http.Header) (objectMetaEncryptFn, error) {

View File

@@ -1470,7 +1470,17 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
actualSize = n
}
}
if fi.Checksum == nil {
// If ServerSideChecksum is wanted for this object, it takes precedence
// over opts.WantChecksum.
if opts.WantServerSideChecksumType.IsSet() {
serverSideChecksum := r.RawServerSideChecksumResult()
if serverSideChecksum != nil {
fi.Checksum = serverSideChecksum.AppendTo(nil, nil)
if opts.EncryptFn != nil {
fi.Checksum = opts.EncryptFn("object-checksum", fi.Checksum)
}
}
} else if fi.Checksum == nil && opts.WantChecksum != nil {
// Trailing headers checksums should now be filled.
fi.Checksum = opts.WantChecksum.AppendTo(nil, nil)
if opts.EncryptFn != nil {

View File

@@ -1340,12 +1340,15 @@ func (z *erasureServerPools) CopyObject(ctx context.Context, srcBucket, srcObjec
}
putOpts := ObjectOptions{
ServerSideEncryption: dstOpts.ServerSideEncryption,
UserDefined: srcInfo.UserDefined,
Versioned: dstOpts.Versioned,
VersionID: dstOpts.VersionID,
MTime: dstOpts.MTime,
NoLock: true,
ServerSideEncryption: dstOpts.ServerSideEncryption,
UserDefined: srcInfo.UserDefined,
Versioned: dstOpts.Versioned,
VersionID: dstOpts.VersionID,
MTime: dstOpts.MTime,
NoLock: true,
EncryptFn: dstOpts.EncryptFn,
WantChecksum: dstOpts.WantChecksum,
WantServerSideChecksumType: dstOpts.WantServerSideChecksumType,
}
return z.serverPools[poolIdx].PutObject(ctx, dstBucket, dstObject, srcInfo.PutObjReader, putOpts)

View File

@@ -868,11 +868,14 @@ func (s *erasureSets) CopyObject(ctx context.Context, srcBucket, srcObject, dstB
}
putOpts := ObjectOptions{
ServerSideEncryption: dstOpts.ServerSideEncryption,
UserDefined: srcInfo.UserDefined,
Versioned: dstOpts.Versioned,
VersionID: dstOpts.VersionID,
MTime: dstOpts.MTime,
ServerSideEncryption: dstOpts.ServerSideEncryption,
UserDefined: srcInfo.UserDefined,
Versioned: dstOpts.Versioned,
VersionID: dstOpts.VersionID,
MTime: dstOpts.MTime,
EncryptFn: dstOpts.EncryptFn,
WantChecksum: dstOpts.WantChecksum,
WantServerSideChecksumType: dstOpts.WantServerSideChecksumType,
}
return dstSet.putObject(ctx, dstBucket, dstObject, srcInfo.PutObjReader, putOpts)

View File

@@ -152,6 +152,10 @@ func encLogIf(ctx context.Context, err error, errKind ...interface{}) {
logger.LogIf(ctx, "encryption", err, errKind...)
}
func encLogOnceIf(ctx context.Context, err error, id string, errKind ...interface{}) {
logger.LogOnceIf(ctx, "encryption", err, id, errKind...)
}
func storageLogIf(ctx context.Context, err error, errKind ...interface{}) {
logger.LogIf(ctx, "storage", err, errKind...)
}

View File

@@ -654,6 +654,7 @@ type objectAttributesChecksum struct {
ChecksumSHA1 string `xml:",omitempty"`
ChecksumSHA256 string `xml:",omitempty"`
ChecksumCRC64NVME string `xml:",omitempty"`
ChecksumType string `xml:",omitempty"`
}
type objectAttributesParts struct {

View File

@@ -86,6 +86,8 @@ type ObjectOptions struct {
WantChecksum *hash.Checksum // x-amz-checksum-XXX checksum sent to PutObject/ CompleteMultipartUpload.
WantServerSideChecksumType hash.ChecksumType // if set, we compute a server-side checksum of this type
NoDecryption bool // indicates if the stream must be decrypted.
PreserveETag string // preserves this etag during a PUT call.
NoLock bool // indicates to lower layers if the caller is expecting to hold locks.

View File

@@ -1096,6 +1096,16 @@ func NewPutObjReader(rawReader *hash.Reader) *PutObjReader {
return &PutObjReader{Reader: rawReader, rawReader: rawReader}
}
// RawServerSideChecksumResult returns the ServerSideChecksumResult from the
// underlying rawReader, since the PutObjReader might be encrypted data and
// thus any checksum from that would be incorrect.
func (p *PutObjReader) RawServerSideChecksumResult() *hash.Checksum {
if p.rawReader != nil {
return p.rawReader.ServerSideChecksumResult
}
return nil
}
func sealETag(encKey crypto.ObjectKey, md5CurrSum []byte) []byte {
var emptyKey [32]byte
if bytes.Equal(encKey[:], emptyKey[:]) {

View File

@@ -641,6 +641,7 @@ func (api objectAPIHandlers) getObjectAttributesHandler(ctx context.Context, obj
ChecksumSHA1: strings.Split(chkSums["SHA1"], "-")[0],
ChecksumSHA256: strings.Split(chkSums["SHA256"], "-")[0],
ChecksumCRC64NVME: strings.Split(chkSums["CRC64NVME"], "-")[0],
ChecksumType: chkSums[xhttp.AmzChecksumType],
}
}
}
@@ -1465,6 +1466,46 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
targetSize, _ = srcInfo.DecryptedSize()
}
// Client can request that a different type of checksum is computed server-side for the
// destination object using the x-amz-checksum-algorithm header.
headerChecksumType := hash.NewChecksumHeader(r.Header)
if headerChecksumType.IsSet() {
dstOpts.WantServerSideChecksumType = headerChecksumType.Base()
srcInfo.Reader.AddServerSideChecksumHasher(headerChecksumType)
dstOpts.WantChecksum = nil
} else {
// Check the source object for checksum.
// If Checksum is not encrypted, decryptChecksum will be a no-op and return
// the already unencrypted value.
srcChecksumDecrypted, err := srcInfo.decryptChecksum(r.Header)
if err != nil {
encLogOnceIf(GlobalContext,
fmt.Errorf("Unable to decryptChecksum for object: %s/%s, error: %w", srcBucket, srcObject, err),
"copy-object-decrypt-checksums-"+srcBucket+srcObject)
}
// The source object has a checksum set, we need the destination to have one too.
if srcChecksumDecrypted != nil {
dstOpts.WantChecksum = hash.ChecksumFromBytes(srcChecksumDecrypted)
// When an object is being copied from a source that is multipart, the destination will
// no longer be multipart, and thus the checksum becomes full-object instead. Since
// the CopyObject API does not require that the caller send us this final checksum, we need
// to compute it server-side, with the same type as the source object.
if dstOpts.WantChecksum != nil && dstOpts.WantChecksum.Type.IsMultipartComposite() {
dstOpts.WantServerSideChecksumType = dstOpts.WantChecksum.Type.Base()
srcInfo.Reader.AddServerSideChecksumHasher(dstOpts.WantServerSideChecksumType)
dstOpts.WantChecksum = nil
}
} else {
// S3: All copied objects without checksums and specified destination checksum algorithms
// automatically gain a CRC-64NVME checksum algorithm.
dstOpts.WantServerSideChecksumType = hash.ChecksumCRC64NVME
srcInfo.Reader.AddServerSideChecksumHasher(dstOpts.WantServerSideChecksumType)
dstOpts.WantChecksum = nil
}
}
if isTargetEncrypted {
var encReader io.Reader
kind, _ := crypto.IsRequested(r.Header)
@@ -1498,6 +1539,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
if dstOpts.IndexCB != nil {
dstOpts.IndexCB = compressionIndexEncrypter(objEncKey, dstOpts.IndexCB)
}
dstOpts.EncryptFn = metadataEncrypter(objEncKey)
}
}
@@ -1633,6 +1675,13 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
return
}
// After we've checked for an invalid copy (above), if a server-side checksum type
// is requested, we need to read the source to recompute the checksum.
if dstOpts.WantServerSideChecksumType.IsSet() {
srcInfo.metadataOnly = false
}
// Federation only.
remoteCallRequired := isRemoteCopyRequired(ctx, srcBucket, dstBucket, objectAPI)
var objInfo ObjectInfo