From 44483f6976c4c6960475906bf96c4e741ef3156a Mon Sep 17 00:00:00 2001 From: chrislu Date: Mon, 17 Nov 2025 16:26:38 -0800 Subject: [PATCH] offset Fetch FULL encrypted chunk (not just the range) Adjust IV by PartOffset/ChunkOffset only Decrypt full chunk Skip in the DECRYPTED stream to reach OffsetInChunk --- weed/s3api/s3api_object_handlers.go | 112 ++++++++++++++++------------ 1 file changed, 65 insertions(+), 47 deletions(-) diff --git a/weed/s3api/s3api_object_handlers.go b/weed/s3api/s3api_object_handlers.go index ed11517c7..071461ff0 100644 --- a/weed/s3api/s3api_object_handlers.go +++ b/weed/s3api/s3api_object_handlers.go @@ -1282,35 +1282,43 @@ func (s3a *S3ApiServer) decryptSSECChunkView(ctx context.Context, fileChunk *fil return nil, fmt.Errorf("failed to decode IV: %w", err) } - // Fetch only the needed encrypted bytes for this view - encryptedReader, err := s3a.fetchChunkViewData(ctx, chunkView) + // Fetch FULL encrypted chunk + // Note: Fetching full chunk is necessary for proper CTR decryption stream + fullChunkReader, err := s3a.fetchFullChunk(ctx, chunkView.FileId) if err != nil { - return nil, fmt.Errorf("failed to fetch chunk view: %w", err) + return nil, fmt.Errorf("failed to fetch full chunk: %w", err) } - // For multipart SSE-C, each part is encrypted independently starting from offset 0 - // PartOffset = the chunk's offset within the part's encrypted stream - // OffsetInChunk = where we start reading within this chunk - // The IV must be adjusted to: PartOffset + OffsetInChunk - // This ensures we decrypt from the correct position in the encrypted stream - absoluteOffset := ssecMetadata.PartOffset + chunkView.OffsetInChunk + // Calculate IV using PartOffset + // PartOffset is the position of this chunk within its part's encrypted stream var adjustedIV []byte - if absoluteOffset > 0 { - adjustedIV = adjustCTRIV(chunkIV, absoluteOffset) + if ssecMetadata.PartOffset > 0 { + adjustedIV = adjustCTRIV(chunkIV, ssecMetadata.PartOffset) } else { adjustedIV = chunkIV } - // Decrypt from the correct offset - decryptedReader, decryptErr := CreateSSECDecryptedReader(encryptedReader, customerKey, adjustedIV) + // Decrypt the full chunk + decryptedReader, decryptErr := CreateSSECDecryptedReader(fullChunkReader, customerKey, adjustedIV) if decryptErr != nil { - encryptedReader.Close() + fullChunkReader.Close() return nil, fmt.Errorf("failed to create decrypted reader: %w", decryptErr) } - // Return a reader that only reads ViewSize bytes + // Skip to the position we need in the decrypted stream + if chunkView.OffsetInChunk > 0 { + _, err = io.CopyN(io.Discard, decryptedReader, chunkView.OffsetInChunk) + if err != nil { + if closer, ok := decryptedReader.(io.Closer); ok { + closer.Close() + } + return nil, fmt.Errorf("failed to skip to offset %d: %w", chunkView.OffsetInChunk, err) + } + } + + // Return a reader that only reads ViewSize bytes with proper cleanup limitedReader := io.LimitReader(decryptedReader, int64(chunkView.ViewSize)) - return &rc{Reader: limitedReader, Closer: encryptedReader}, nil + return &rc{Reader: limitedReader, Closer: fullChunkReader}, nil } // Single-part SSE-C: use object-level IV (should not hit this in range path, but handle it) @@ -1330,18 +1338,16 @@ func (s3a *S3ApiServer) decryptSSEKMSChunkView(ctx context.Context, fileChunk *f return nil, fmt.Errorf("failed to deserialize SSE-KMS metadata: %w", err) } - // Fetch only the needed encrypted bytes for this view - encryptedReader, err := s3a.fetchChunkViewData(ctx, chunkView) + // Fetch FULL encrypted chunk + fullChunkReader, err := s3a.fetchFullChunk(ctx, chunkView.FileId) if err != nil { - return nil, fmt.Errorf("failed to fetch chunk view: %w", err) + return nil, fmt.Errorf("failed to fetch full chunk: %w", err) } - // Calculate IV using both ChunkOffset and OffsetInChunk for CTR seek - // CTR mode allows seeking by adjusting the IV to any byte offset - absoluteOffset := sseKMSKey.ChunkOffset + chunkView.OffsetInChunk + // Calculate IV using ChunkOffset (same as PartOffset in SSE-C) var adjustedIV []byte - if absoluteOffset > 0 { - adjustedIV = adjustCTRIV(sseKMSKey.IV, absoluteOffset) + if sseKMSKey.ChunkOffset > 0 { + adjustedIV = adjustCTRIV(sseKMSKey.IV, sseKMSKey.ChunkOffset) } else { adjustedIV = sseKMSKey.IV } @@ -1352,18 +1358,28 @@ func (s3a *S3ApiServer) decryptSSEKMSChunkView(ctx context.Context, fileChunk *f EncryptionContext: sseKMSKey.EncryptionContext, BucketKeyEnabled: sseKMSKey.BucketKeyEnabled, IV: adjustedIV, - ChunkOffset: absoluteOffset, + ChunkOffset: sseKMSKey.ChunkOffset, } - decryptedReader, decryptErr := CreateSSEKMSDecryptedReader(encryptedReader, adjustedKey) + decryptedReader, decryptErr := CreateSSEKMSDecryptedReader(fullChunkReader, adjustedKey) if decryptErr != nil { - encryptedReader.Close() + fullChunkReader.Close() return nil, fmt.Errorf("failed to create KMS decrypted reader: %w", decryptErr) } - // Return a reader that only reads ViewSize bytes + // Skip to position and limit to ViewSize + if chunkView.OffsetInChunk > 0 { + _, err = io.CopyN(io.Discard, decryptedReader, chunkView.OffsetInChunk) + if err != nil { + if closer, ok := decryptedReader.(io.Closer); ok { + closer.Close() + } + return nil, fmt.Errorf("failed to skip to offset: %w", err) + } + } + limitedReader := io.LimitReader(decryptedReader, int64(chunkView.ViewSize)) - return &rc{Reader: limitedReader, Closer: encryptedReader}, nil + return &rc{Reader: limitedReader, Closer: fullChunkReader}, nil } // Non-KMS encrypted chunk @@ -1380,36 +1396,38 @@ func (s3a *S3ApiServer) decryptSSES3ChunkView(ctx context.Context, fileChunk *fi return nil, fmt.Errorf("failed to deserialize SSE-S3 metadata: %w", err) } - // Fetch only the needed encrypted bytes for this view - encryptedReader, err := s3a.fetchChunkViewData(ctx, chunkView) + // Fetch FULL encrypted chunk + fullChunkReader, err := s3a.fetchFullChunk(ctx, chunkView.FileId) if err != nil { - return nil, fmt.Errorf("failed to fetch chunk view: %w", err) + return nil, fmt.Errorf("failed to fetch full chunk: %w", err) } - // Get base IV - baseIV, err := GetSSES3IV(entry, sseS3Key, keyManager) + // Get base IV and use it directly (no offset adjustment for full chunk) + iv, err := GetSSES3IV(entry, sseS3Key, keyManager) if err != nil { - encryptedReader.Close() + fullChunkReader.Close() return nil, fmt.Errorf("failed to get SSE-S3 IV: %w", err) } - // Adjust IV for CTR seek if needed - var adjustedIV []byte - if chunkView.OffsetInChunk > 0 { - adjustedIV = adjustCTRIV(baseIV, chunkView.OffsetInChunk) - } else { - adjustedIV = baseIV - } - - decryptedReader, decryptErr := CreateSSES3DecryptedReader(encryptedReader, sseS3Key, adjustedIV) + decryptedReader, decryptErr := CreateSSES3DecryptedReader(fullChunkReader, sseS3Key, iv) if decryptErr != nil { - encryptedReader.Close() + fullChunkReader.Close() return nil, fmt.Errorf("failed to create S3 decrypted reader: %w", decryptErr) } - // Return a reader that only reads ViewSize bytes + // Skip to position and limit to ViewSize + if chunkView.OffsetInChunk > 0 { + _, err = io.CopyN(io.Discard, decryptedReader, chunkView.OffsetInChunk) + if err != nil { + if closer, ok := decryptedReader.(io.Closer); ok { + closer.Close() + } + return nil, fmt.Errorf("failed to skip to offset: %w", err) + } + } + limitedReader := io.LimitReader(decryptedReader, int64(chunkView.ViewSize)) - return &rc{Reader: limitedReader, Closer: encryptedReader}, nil + return &rc{Reader: limitedReader, Closer: fullChunkReader}, nil } // adjustCTRIV adjusts the IV for CTR mode based on byte offset