From 065978618c61ba5606a941160f6db97dcb54ad88 Mon Sep 17 00:00:00 2001 From: chrislu Date: Sun, 16 Nov 2025 18:00:36 -0800 Subject: [PATCH] IV relative to offset in each part, not the whole file --- weed/s3api/s3_sse_c.go | 25 +++++++++++++- weed/s3api/s3_sse_kms.go | 11 +++++- weed/s3api/s3_sse_s3.go | 11 +++++- weed/s3api/s3api_object_handlers.go | 45 ++++++++++++++++--------- weed/s3api/s3api_object_handlers_put.go | 4 +-- 5 files changed, 75 insertions(+), 21 deletions(-) diff --git a/weed/s3api/s3_sse_c.go b/weed/s3api/s3_sse_c.go index 733ae764e..3394a3ba6 100644 --- a/weed/s3api/s3_sse_c.go +++ b/weed/s3api/s3_sse_c.go @@ -16,6 +16,20 @@ import ( "github.com/seaweedfs/seaweedfs/weed/s3api/s3err" ) +// decryptReaderCloser wraps a cipher.StreamReader with proper Close() support +// This ensures the underlying io.ReadCloser (like http.Response.Body) is properly closed +type decryptReaderCloser struct { + io.Reader + underlyingCloser io.Closer +} + +func (d *decryptReaderCloser) Close() error { + if d.underlyingCloser != nil { + return d.underlyingCloser.Close() + } + return nil +} + // SSECCopyStrategy represents different strategies for copying SSE-C objects type SSECCopyStrategy int @@ -197,8 +211,17 @@ func CreateSSECDecryptedReader(r io.Reader, customerKey *SSECustomerKey, iv []by // Create CTR mode cipher using the IV from metadata stream := cipher.NewCTR(block, iv) + decryptReader := &cipher.StreamReader{S: stream, R: r} + + // Wrap with closer if the underlying reader implements io.Closer + if closer, ok := r.(io.Closer); ok { + return &decryptReaderCloser{ + Reader: decryptReader, + underlyingCloser: closer, + }, nil + } - return &cipher.StreamReader{S: stream, R: r}, nil + return decryptReader, nil } // CreateSSECEncryptedReaderWithOffset creates an encrypted reader with a specific counter offset diff --git a/weed/s3api/s3_sse_kms.go b/weed/s3api/s3_sse_kms.go index 3b721aa26..ee21c129c 100644 --- a/weed/s3api/s3_sse_kms.go +++ b/weed/s3api/s3_sse_kms.go @@ -436,9 +436,18 @@ func CreateSSEKMSDecryptedReader(r io.Reader, sseKey *SSEKMSKey) (io.Reader, err // Create CTR mode cipher stream for decryption // Note: AES-CTR is used for object data decryption to match the encryption mode stream := cipher.NewCTR(block, iv) + decryptReader := &cipher.StreamReader{S: stream, R: r} + + // Wrap with closer if the underlying reader implements io.Closer + if closer, ok := r.(io.Closer); ok { + return &decryptReaderCloser{ + Reader: decryptReader, + underlyingCloser: closer, + }, nil + } // Return the decrypted reader - return &cipher.StreamReader{S: stream, R: r}, nil + return decryptReader, nil } // ParseSSEKMSHeaders parses SSE-KMS headers from an HTTP request diff --git a/weed/s3api/s3_sse_s3.go b/weed/s3api/s3_sse_s3.go index bc648205e..f02149088 100644 --- a/weed/s3api/s3_sse_s3.go +++ b/weed/s3api/s3_sse_s3.go @@ -109,8 +109,17 @@ func CreateSSES3DecryptedReader(reader io.Reader, key *SSES3Key, iv []byte) (io. // Create CTR mode cipher with the provided IV stream := cipher.NewCTR(block, iv) + decryptReader := &cipher.StreamReader{S: stream, R: reader} - return &cipher.StreamReader{S: stream, R: reader}, nil + // Wrap with closer if the underlying reader implements io.Closer + if closer, ok := reader.(io.Closer); ok { + return &decryptReaderCloser{ + Reader: decryptReader, + underlyingCloser: closer, + }, nil + } + + return decryptReader, nil } // GetSSES3Headers returns the headers for SSE-S3 encrypted objects diff --git a/weed/s3api/s3api_object_handlers.go b/weed/s3api/s3api_object_handlers.go index 0c510210f..0ce251f28 100644 --- a/weed/s3api/s3api_object_handlers.go +++ b/weed/s3api/s3api_object_handlers.go @@ -677,7 +677,7 @@ func (s3a *S3ApiServer) streamFromVolumeServers(w http.ResponseWriter, r *http.R // Get chunks chunks := entry.GetChunks() - glog.V(3).Infof("streamFromVolumeServers: entry has %d chunks, totalSize=%d, isRange=%v, offset=%d, size=%d", + glog.Infof("streamFromVolumeServers: entry has %d chunks, totalSize=%d, isRange=%v, offset=%d, size=%d", len(chunks), totalSize, isRangeRequest, offset, size) if len(chunks) == 0 { @@ -696,7 +696,7 @@ func (s3a *S3ApiServer) streamFromVolumeServers(w http.ResponseWriter, r *http.R // Log chunk details for i, chunk := range chunks { - glog.V(3).Infof(" Chunk[%d]: fid=%s, offset=%d, size=%d", i, chunk.GetFileIdString(), chunk.Offset, chunk.Size) + glog.Infof(" GET Chunk[%d]: fid=%s, offset=%d, size=%d", i, chunk.GetFileIdString(), chunk.Offset, chunk.Size) } // Create lookup function via filer client (reuse shared helper) @@ -737,8 +737,14 @@ func (s3a *S3ApiServer) streamFromVolumeServers(w http.ResponseWriter, r *http.R // Stream directly to response tStreamExec := time.Now() + glog.Infof("streamFromVolumeServers: starting streamFn, offset=%d, size=%d", offset, size) err = streamFn(w) streamExecTime = time.Since(tStreamExec) + if err != nil { + glog.Errorf("streamFromVolumeServers: streamFn failed: %v", err) + } else { + glog.Infof("streamFromVolumeServers: streamFn completed successfully") + } return err } @@ -1150,7 +1156,10 @@ func (s3a *S3ApiServer) streamDecryptedRangeFromChunks(ctx context.Context, w io glog.V(3).Infof("streamDecryptedRangeFromChunks: about to copy decrypted chunk %s, expected ViewSize=%d", chunkView.FileId, chunkView.ViewSize) written, copyErr := io.Copy(w, decryptedChunkReader) if closer, ok := decryptedChunkReader.(io.Closer); ok { - closer.Close() + closeErr := closer.Close() + if closeErr != nil { + glog.Warningf("streamDecryptedRangeFromChunks: failed to close decrypted chunk reader: %v", closeErr) + } } if copyErr != nil { glog.Errorf("streamDecryptedRangeFromChunks: copy error after writing %d bytes (expected %d): %v", written, chunkView.ViewSize, copyErr) @@ -1159,6 +1168,7 @@ func (s3a *S3ApiServer) streamDecryptedRangeFromChunks(ctx context.Context, w io if written != int64(chunkView.ViewSize) { glog.Errorf("streamDecryptedRangeFromChunks: size mismatch - wrote %d bytes but expected %d", written, chunkView.ViewSize) + return fmt.Errorf("size mismatch: wrote %d bytes but expected %d for chunk %s", written, chunkView.ViewSize, chunkView.FileId) } totalWritten += written @@ -1212,14 +1222,14 @@ func (s3a *S3ApiServer) decryptSSECChunkView(ctx context.Context, fileChunk *fil return nil, fmt.Errorf("failed to fetch chunk data: %w", err) } - // Decrypt with CTR IV offset adjustment for the ABSOLUTE offset in the file + // Decrypt with CTR IV offset adjustment // CTR mode: IV for block N = base_IV + (N / 16) - // The IV must be adjusted based on the absolute position from the start of the encrypted stream - // Use chunkView.ViewOffset which represents the absolute position in the file - absoluteOffset := chunkView.ViewOffset - glog.V(3).Infof("decryptSSECChunkView: chunk=%s, fileChunk.Offset=%d, chunkView.OffsetInChunk=%d, chunkView.ViewOffset=%d, chunkView.ViewSize=%d, metadata.PartOffset=%d, using absoluteOffset=%d", - chunkView.FileId, fileChunk.Offset, chunkView.OffsetInChunk, chunkView.ViewOffset, chunkView.ViewSize, ssecMetadata.PartOffset, absoluteOffset) - adjustedIV := adjustCTRIV(chunkIV, absoluteOffset) + // For multipart objects, each part is encrypted independently with offset=0 + // So we need to adjust IV based on position WITHIN THE PART, not absolute file position + offsetWithinPart := chunkView.ViewOffset - ssecMetadata.PartOffset + glog.V(3).Infof("decryptSSECChunkView: chunk=%s, fileChunk.Offset=%d, chunkView.OffsetInChunk=%d, chunkView.ViewOffset=%d, chunkView.ViewSize=%d, metadata.PartOffset=%d, offsetWithinPart=%d", + chunkView.FileId, fileChunk.Offset, chunkView.OffsetInChunk, chunkView.ViewOffset, chunkView.ViewSize, ssecMetadata.PartOffset, offsetWithinPart) + adjustedIV := adjustCTRIV(chunkIV, offsetWithinPart) return CreateSSECDecryptedReader(encryptedReader, customerKey, adjustedIV) } @@ -1246,19 +1256,22 @@ func (s3a *S3ApiServer) decryptSSEKMSChunkView(ctx context.Context, fileChunk *f return nil, fmt.Errorf("failed to fetch chunk data: %w", err) } - // Decrypt with CTR IV offset adjustment for the ABSOLUTE offset in the file - // The IV must be adjusted based on the absolute position from the start of the encrypted stream - // Use chunkView.ViewOffset which represents the absolute position in the file - absoluteOffset := chunkView.ViewOffset - adjustedIV := adjustCTRIV(sseKMSKey.IV, absoluteOffset) + // Decrypt with CTR IV offset adjustment + // For multipart objects, each part is encrypted independently with offset=0 + // So we need to adjust IV based on position WITHIN THE PART, not absolute file position + // sseKMSKey.ChunkOffset contains the part offset (where this part starts in the file) + offsetWithinPart := chunkView.ViewOffset - sseKMSKey.ChunkOffset + adjustedIV := adjustCTRIV(sseKMSKey.IV, offsetWithinPart) adjustedKey := &SSEKMSKey{ KeyID: sseKMSKey.KeyID, EncryptedDataKey: sseKMSKey.EncryptedDataKey, EncryptionContext: sseKMSKey.EncryptionContext, BucketKeyEnabled: sseKMSKey.BucketKeyEnabled, IV: adjustedIV, - ChunkOffset: absoluteOffset, + ChunkOffset: offsetWithinPart, } + glog.V(3).Infof("decryptSSEKMSChunkView: chunk=%s, ViewOffset=%d, PartOffset=%d, offsetWithinPart=%d", + chunkView.FileId, chunkView.ViewOffset, sseKMSKey.ChunkOffset, offsetWithinPart) return CreateSSEKMSDecryptedReader(encryptedReader, adjustedKey) } diff --git a/weed/s3api/s3api_object_handlers_put.go b/weed/s3api/s3api_object_handlers_put.go index 87a16a664..28b5ab687 100644 --- a/weed/s3api/s3api_object_handlers_put.go +++ b/weed/s3api/s3api_object_handlers_put.go @@ -376,12 +376,12 @@ func (s3a *S3ApiServer) putToFiler(r *http.Request, uploadUrl string, dataReader md5Sum := chunkResult.Md5Hash.Sum(nil) etag = fmt.Sprintf("%x", md5Sum) - glog.V(3).Infof("putToFiler: Chunked upload SUCCESS - path=%s, chunks=%d, size=%d, etag=%s", + glog.Infof("putToFiler: Chunked upload SUCCESS - path=%s, chunks=%d, size=%d, etag=%s", filePath, len(chunkResult.FileChunks), chunkResult.TotalSize, etag) // Log chunk details for debugging for i, chunk := range chunkResult.FileChunks { - glog.V(3).Infof(" Chunk[%d]: fid=%s, offset=%d, size=%d", i, chunk.GetFileIdString(), chunk.Offset, chunk.Size) + glog.Infof(" PUT Chunk[%d]: fid=%s, offset=%d, size=%d", i, chunk.GetFileIdString(), chunk.Offset, chunk.Size) } // Add SSE metadata to all chunks if present