fee/aesstream: range decryption API (FIL-472)#8
Open
Peeja wants to merge 1 commit into
Open
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a range-decryption API to fee/aesstream so consumers can read arbitrary plaintext byte ranges from a chunked AES-256-GCM STREAM ciphertext using only the overlapping ciphertext chunks.
Changes:
- Introduces
RangeReader,OpenRange, andDecryptedSizeplus shared chunk-geometry math (chunkLayout). - Adds comprehensive tests covering correctness, chunk-selective fetch behavior, tamper detection, and size/range validation.
- Promotes
github.com/stretchr/testifyto a direct dependency to support the new test suites.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| go.mod | Promotes testify to a direct dependency for new tests. |
| fee/aesstream/rangereader.go | Implements range decryption reader + size/geometry helpers. |
| fee/aesstream/rangereader_test.go | Black-box tests for range correctness, selective fetch, and tamper/validation behavior. |
| fee/aesstream/rangereader_internal_test.go | Internal tests pinning chunkLayout boundary math and MaxChunks guard. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Peeja
pushed a commit
that referenced
this pull request
Jun 25, 2026
OpenRange buffers the whole requested range. On 64-bit platforms r.Len() (bounded by the plaintext length) always fits an int, but on a 32-bit build a multi-GiB range would overflow the int make wants and panic with "makeslice: len out of range". Return a clear error in that case and point callers at the streaming NewRangeReader instead. Addresses a review note on #8. (The same note's claim that make([]byte, int64) fails to compile is incorrect — the Go spec allows any integer type for make's size; this only guards the 32-bit overflow.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7GkuUdrMgiQhmpoY9FjKN
f5cbf0f to
22e8271
Compare
ae29d47 to
2a55a29
Compare
Range decryption on top of the chunked AES-256-GCM STREAM body cipher (FIL-470). No new crypto — chunk-index arithmetic plus a single contiguous fetch of the chunks overlapping a request. The engine is sequential: a caller turns a plaintext range into one contiguous ciphertext fetch and streams the result through the decryptor, so the read path is one request per segment with O(chunkSize) memory. - CiphertextRange(ciphertextSize, chunkSize, off, length) → the single contiguous ciphertext byte span covering a plaintext range. Overlapping chunks are adjacent, so the bytes needed are one span — the consumer (FIL-487) fetches a segment's range in one request. Offsets are stream-relative; add any envelope-header length. Chunk-aligned, so the edge over-read is <2*chunkSize (GCM authenticates whole chunks). - SpanReader / OpenSpan: decrypt that span — delivered as a sequential io.Reader (e.g. a range-request body) — into exactly the requested plaintext. The total ciphertext length pins each chunk's index, final-chunk flag and length up front, so there is no last-chunk retry and the first/last chunks are trimmed to the range. OpenSpan guards its int64->int buffer with math.MaxInt (steering huge ranges to the streaming reader instead of panicking in make on 32-bit builds). - DecryptedSize: inverse of EncryptedSize; derives and validates the plaintext length from a ciphertext length without fetching any ciphertext. - chunkLayout: the shared geometry behind all three. No io.ReaderAt form: range reads are inherently sequential (each chunk read once, in order), so the random-access engine wasn't load-bearing. A random-access source can still feed SpanReader via io.NewSectionReader(src, start, n) using CiphertextRange's offsets — noted in the CiphertextRange doc. Range semantics are HTTP-range-like: a length past the end clamps; a negative offset/length or offset past the end is ErrRange; a structurally invalid declared ciphertext length is ErrCiphertextSize; a tampered/reordered/misframed chunk is ErrCorrupted; a span shorter than the geometry requires is ErrTruncated. Tests use testify require. TestSpanReader/Ranges is a table of hand-computed literal expectations (span start/length and decrypted length) — an independent oracle rather than a re-derivation of the range math — that also checks the span is consumed in full and no further. TestSpanReader/RoundTrip reads each sealed object back in full through adjacent range windows of varying widths (chunk-aligned and not), via both OpenSpan and SpanReader, and confirms the reassembly equals the original plaintext (length recovered through DecryptedSize). Alongside: tamper detection, short-span truncation, wrong-size detection, range/size validation, default 256 KiB chunks, CiphertextRange input validation, the internal chunkLayout geometry + MaxChunks guard, and EncryptedSize/DecryptedSize inversion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7GkuUdrMgiQhmpoY9FjKN
2a55a29 to
23704c9
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This will support range requests for objects. A higher layer will translate an object range request into plaintext segment (piece/shard) range requests, and those into ciphertext blob range requests which can go to Piri. This PR provides everything that layer needs to convert from a plaintext range to a ciphertext range (
CiphertextRange()), and then turn the ciphertext result of those range requests into the plaintext we're after (SpanReader).Implements FIL-472 — the range-decryption API for the FilOne File Encryption Envelope (FEE).
Pure STREAM mechanics on top of the existing body cipher — no new crypto, just chunk-index arithmetic and a single contiguous fetch of the chunks overlapping a request.
Design: sequential, one request per segment
A range read turns a plaintext range into one contiguous ciphertext fetch, then streams the result through the decryptor — so the read path is one request per segment with O(chunkSize) memory, no per-chunk fetching.
CiphertextRange(ciphertextSize, chunkSize, off, length) (start, n, err)— the single contiguous ciphertext byte span covering a plaintext range. Because the chunks overlapping a range are adjacent, the bytes needed are one span; the consumer (FIL-487) fetches it in one range request. Offsets are stream-relative (add any envelope-header length). Chunk-aligned, so the edge over-read is <2·chunkSize (GCM authenticates whole chunks).SpanReader(io.Reader) /OpenSpan— decrypt that span, delivered as a sequentialio.Reader(e.g. a range-request body), into exactly the requested plaintext. The total ciphertext length pins each chunk's index, final-chunk flag and length up front, so there's no last-chunk retry; the first/last chunks are trimmed to the range.OpenSpanguards itsint64→intbuffer withmath.MaxInt(steers huge ranges to the streaming reader instead of panicking inmakeon 32-bit builds).DecryptedSize— inverse ofEncryptedSize; derives and validates the plaintext length from a ciphertext length without fetching any ciphertext.Semantics
HTTP-range-like: a length past the end clamps to the bytes available from the offset; a negative offset/length or an offset past the end is
ErrRange. A structurally invalid declared ciphertext length isErrCiphertextSize; a tampered/reordered/misframed chunk isErrCorrupted; a span shorter than the geometry requires isErrTruncated.What the tests cover (testify
require)TestSpanReader/Ranges— a table of hand-computed literal expectations (span start/length and decrypted length): an independent oracle that pins the exact spanCiphertextRangereports, thatOpenSpan/SpanReaderreproduce the right plaintext from exactly that span, and that the span is consumed in full and no further.TestSpanReader/RoundTrip— reads each sealed object back in full through adjacent range windows of varying widths (chunk-aligned and not), via bothOpenSpanandSpanReader, and confirms the reassembly equals the original plaintext (length recovered throughDecryptedSize).CiphertextRangeinput validation, the internalchunkLayoutgeometry +MaxChunksguard, andEncryptedSize↔DecryptedSizeinversion.go test(incl.-race),go vet,gofmtclean.🤖 Generated with Claude Code