Skip to content

Commit

Permalink
Fix SSEC multipart checksum replication (#19915)
Browse files Browse the repository at this point in the history
* Multipart SSEC checksums were not transferred.
* Remove key mismatch logging. This key is user-controlled with SSEC.
* If the source is SSEC and the destination reports ErrSSEEncryptedObject, 
  assume replication is good.
  • Loading branch information
klauspost committed Jun 13, 2024
1 parent ba9f0f2 commit ad04afe
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 31 deletions.
38 changes: 28 additions & 10 deletions cmd/bucket-replication.go
Original file line number Diff line number Diff line change
Expand Up @@ -1418,7 +1418,8 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
}

// Set the encrypted size for SSE-C objects
if crypto.SSEC.IsEncrypted(objInfo.UserDefined) {
isSSEC := crypto.SSEC.IsEncrypted(objInfo.UserDefined)
if isSSEC {
size = objInfo.Size
}

Expand Down Expand Up @@ -1478,6 +1479,13 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
return
}
} else {
// SSEC objects will refuse HeadObject without the decryption key.
// Ignore the error, since we know the object exists and versioning prevents overwriting existing versions.
if isSSEC && strings.Contains(cerr.Error(), errorCodes[ErrSSEEncryptedObject].Description) {
rinfo.ReplicationStatus = replication.Completed
rinfo.ReplicationAction = replicateNone
goto applyAction
}
// if target returns error other than NoSuchKey, defer replication attempt
if minio.IsNetworkOrHostDown(cerr, true) && !globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
globalBucketTargetSys.markOffline(tgt.EndpointURL())
Expand Down Expand Up @@ -1505,6 +1513,7 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
return
}
}
applyAction:
rinfo.ReplicationStatus = replication.Completed
rinfo.Size = size
rinfo.ReplicationAction = rAction
Expand Down Expand Up @@ -1638,13 +1647,14 @@ func replicateObjectWithMultipart(ctx context.Context, c *minio.Core, bucket, ob
}()

var (
hr *hash.Reader
pInfo minio.ObjectPart
hr *hash.Reader
pInfo minio.ObjectPart
isSSEC = crypto.SSEC.IsEncrypted(objInfo.UserDefined)
)

var objectSize int64
for _, partInfo := range objInfo.Parts {
if crypto.SSEC.IsEncrypted(objInfo.UserDefined) {
if isSSEC {
hr, err = hash.NewReader(ctx, io.LimitReader(r, partInfo.Size), partInfo.Size, "", "", partInfo.ActualSize)
} else {
hr, err = hash.NewReader(ctx, io.LimitReader(r, partInfo.ActualSize), partInfo.ActualSize, "", "", partInfo.ActualSize)
Expand All @@ -1655,16 +1665,18 @@ func replicateObjectWithMultipart(ctx context.Context, c *minio.Core, bucket, ob

cHeader := http.Header{}
cHeader.Add(xhttp.MinIOSourceReplicationRequest, "true")
crc := getCRCMeta(objInfo, partInfo.Number, nil) // No SSE-C keys here.
for k, v := range crc {
cHeader.Add(k, v)
if !isSSEC {
crc := getCRCMeta(objInfo, partInfo.Number, nil) // No SSE-C keys here.
for k, v := range crc {
cHeader.Add(k, v)
}
}
popts := minio.PutObjectPartOptions{
SSE: opts.ServerSideEncryption,
CustomHeader: cHeader,
}

if crypto.SSEC.IsEncrypted(objInfo.UserDefined) {
if isSSEC {
objectSize += partInfo.Size
pInfo, err = c.PutObjectPart(ctx, bucket, object, uploadID, partInfo.Number, hr, partInfo.Size, popts)
} else {
Expand All @@ -1674,7 +1686,7 @@ func replicateObjectWithMultipart(ctx context.Context, c *minio.Core, bucket, ob
if err != nil {
return err
}
if !crypto.SSEC.IsEncrypted(objInfo.UserDefined) && pInfo.Size != partInfo.ActualSize {
if !isSSEC && pInfo.Size != partInfo.ActualSize {
return fmt.Errorf("Part size mismatch: got %d, want %d", pInfo.Size, partInfo.ActualSize)
}
uploadedParts = append(uploadedParts, minio.CompletePart{
Expand All @@ -1686,12 +1698,18 @@ func replicateObjectWithMultipart(ctx context.Context, c *minio.Core, bucket, ob
ChecksumSHA256: pInfo.ChecksumSHA256,
})
}
userMeta := map[string]string{
validSSEReplicationHeaders[ReservedMetadataPrefix+"Actual-Object-Size"]: objInfo.UserDefined[ReservedMetadataPrefix+"actual-size"],
}
if isSSEC && objInfo.UserDefined[ReplicationSsecChecksumHeader] != "" {
userMeta[ReplicationSsecChecksumHeader] = objInfo.UserDefined[ReplicationSsecChecksumHeader]
}

// really big value but its okay on heavily loaded systems. This is just tail end timeout.
cctx, ccancel := context.WithTimeout(ctx, 10*time.Minute)
defer ccancel()
_, err = c.CompleteMultipartUpload(cctx, bucket, object, uploadID, uploadedParts, minio.PutObjectOptions{
UserMetadata: map[string]string{validSSEReplicationHeaders[ReservedMetadataPrefix+"Actual-Object-Size"]: objInfo.UserDefined[ReservedMetadataPrefix+"actual-size"]},
UserMetadata: userMeta,
Internal: minio.AdvancedPutOptions{
SourceMTime: objInfo.ModTime,
SourceETag: objInfo.ETag,
Expand Down
8 changes: 6 additions & 2 deletions cmd/encryption-v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -1025,7 +1025,9 @@ func DecryptObjectInfo(info *ObjectInfo, r *http.Request) (encrypted bool, err e
if encrypted {
if crypto.SSEC.IsEncrypted(info.UserDefined) {
if !(crypto.SSEC.IsRequested(headers) || crypto.SSECopy.IsRequested(headers)) {
return encrypted, errEncryptedObject
if r.Header.Get(xhttp.MinIOSourceReplicationRequest) != "true" {
return encrypted, errEncryptedObject
}
}
}

Expand Down Expand Up @@ -1168,7 +1170,9 @@ func (o *ObjectInfo) decryptChecksums(part int, h http.Header) map[string]string
if _, encrypted := crypto.IsEncrypted(o.UserDefined); encrypted {
decrypted, err := o.metadataDecrypter(h)("object-checksum", data)
if err != nil {
encLogIf(GlobalContext, err)
if err != crypto.ErrSecretKeyMismatch {
encLogIf(GlobalContext, err)
}
return nil
}
data = decrypted
Expand Down
21 changes: 14 additions & 7 deletions cmd/erasure-multipart.go
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,11 @@ func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string,
}
fi.DataDir = mustGetUUID()

if userDefined[ReplicationSsecChecksumHeader] != "" {
fi.Checksum, _ = base64.StdEncoding.DecodeString(userDefined[ReplicationSsecChecksumHeader])
delete(userDefined, ReplicationSsecChecksumHeader)
}

// Initialize erasure metadata.
for index := range partsMetadata {
partsMetadata[index] = fi
Expand Down Expand Up @@ -1294,6 +1299,14 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
defer lk.Unlock(lkctx)
}

// Accept encrypted checksum from incoming request.
if opts.UserDefined[ReplicationSsecChecksumHeader] != "" {
if v, err := base64.StdEncoding.DecodeString(opts.UserDefined[ReplicationSsecChecksumHeader]); err == nil {
fi.Checksum = v
}
delete(opts.UserDefined, ReplicationSsecChecksumHeader)
}

if checksumType.IsSet() {
checksumType |= hash.ChecksumMultipart | hash.ChecksumIncludesMultipart
var cs *hash.Checksum
Expand All @@ -1303,13 +1316,7 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
fi.Checksum = opts.EncryptFn("object-checksum", fi.Checksum)
}
}
if fi.Metadata[ReplicationSsecChecksumHeader] != "" {
if v, err := base64.StdEncoding.DecodeString(fi.Metadata[ReplicationSsecChecksumHeader]); err == nil {
fi.Checksum = v
}
}
delete(fi.Metadata, ReplicationSsecChecksumHeader) // Transferred above.
delete(fi.Metadata, hash.MinIOMultipartChecksum) // Not needed in final object.
delete(fi.Metadata, hash.MinIOMultipartChecksum) // Not needed in final object.

// Save the final object size and modtime.
fi.Size = objectSize
Expand Down
21 changes: 13 additions & 8 deletions cmd/object-api-options.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ func getDefaultOpts(header http.Header, copySource bool, metadata map[string]str
var sse encrypt.ServerSide

opts = ObjectOptions{UserDefined: metadata}
if v, ok := header[xhttp.MinIOSourceProxyRequest]; ok {
opts.ProxyHeaderSet = true
opts.ProxyRequest = strings.Join(v, "") == "true"
}
if _, ok := header[xhttp.MinIOSourceReplicationRequest]; ok {
opts.ReplicationRequest = true
}
opts.Speedtest = header.Get(globalObjectPerfUserMetadata) != ""

if copySource {
if crypto.SSECopy.IsRequested(header) {
clientKey, err = crypto.SSECopy.ParseHTTP(header)
Expand Down Expand Up @@ -66,14 +75,7 @@ func getDefaultOpts(header http.Header, copySource bool, metadata map[string]str
if crypto.S3.IsRequested(header) || (metadata != nil && crypto.S3.IsEncrypted(metadata)) {
opts.ServerSideEncryption = encrypt.NewSSE()
}
if v, ok := header[xhttp.MinIOSourceProxyRequest]; ok {
opts.ProxyHeaderSet = true
opts.ProxyRequest = strings.Join(v, "") == "true"
}
if _, ok := header[xhttp.MinIOSourceReplicationRequest]; ok {
opts.ReplicationRequest = true
}
opts.Speedtest = header.Get(globalObjectPerfUserMetadata) != ""

return
}

Expand Down Expand Up @@ -494,5 +496,8 @@ func completeMultipartOpts(ctx context.Context, r *http.Request, bucket, object
if _, ok := r.Header[xhttp.MinIOSourceReplicationRequest]; ok {
opts.ReplicationRequest = true
}
if r.Header.Get(ReplicationSsecChecksumHeader) != "" {
opts.UserDefined[ReplicationSsecChecksumHeader] = r.Header.Get(ReplicationSsecChecksumHeader)
}
return opts, nil
}
9 changes: 5 additions & 4 deletions cmd/object-handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -1482,9 +1482,10 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re

// convert copy src encryption options for GET calls
getOpts := ObjectOptions{
VersionID: srcOpts.VersionID,
Versioned: srcOpts.Versioned,
VersionSuspended: srcOpts.VersionSuspended,
VersionID: srcOpts.VersionID,
Versioned: srcOpts.Versioned,
VersionSuspended: srcOpts.VersionSuspended,
ReplicationRequest: r.Header.Get(xhttp.MinIOSourceReplicationRequest) == "true",
}
getSSE := encrypt.SSE(srcOpts.ServerSideEncryption)
if getSSE != srcOpts.ServerSideEncryption {
Expand Down Expand Up @@ -1616,7 +1617,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
return
}
// Encryption parameters not present for this object.
if crypto.SSEC.IsEncrypted(srcInfo.UserDefined) && !crypto.SSECopy.IsRequested(r.Header) {
if crypto.SSEC.IsEncrypted(srcInfo.UserDefined) && !crypto.SSECopy.IsRequested(r.Header) && r.Header.Get(xhttp.MinIOSourceReplicationRequest) != "true" {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidSSECustomerAlgorithm), r.URL)
return
}
Expand Down

0 comments on commit ad04afe

Please sign in to comment.