streamingaead: add NewDecryptingReaderAt for random-access decryption - #44
Open
iainmcgin wants to merge 3 commits into
Open
streamingaead: add NewDecryptingReaderAt for random-access decryption#44iainmcgin wants to merge 3 commits into
iainmcgin wants to merge 3 commits into
Conversation
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
iainmcgin
force-pushed
the
streamingaead-readerat-noncebased
branch
from
July 20, 2026 20:52
7fc890c to
87ff222
Compare
ReaderAt provides io.ReaderAt-style random access to the plaintext of a nonce-based streaming AEAD ciphertext, given the total ciphertext size. The segment containing a requested plaintext offset is computed arithmetically, fetched via the underlying io.ReaderAt, decrypted, and cached. A CiphertextRange helper exposes the underlying byte range that will be touched for a given plaintext range, allowing callers backed by remote storage to prefetch. This mirrors the segment math used by Tink-Java's StreamingAeadSeekableDecryptingChannel.
…ES-CTR-HMAC Each primitive reads the stream header at offset 0 of the supplied io.ReaderAt, derives the per-stream key, and constructs a noncebased.ReaderAt over the segment ciphertext that follows the header. The returned reader supports random access to plaintext bytes.
NewDecryptingReaderAt accepts a keyset handle, a SizedReaderAt over the full ciphertext, and associated data, and returns a DecryptingReaderAt that exposes io.ReaderAt, Size, and CiphertextRange over the plaintext. For keysets with multiple keys the correct key is selected lazily on the first ReadAt: each key whose header parameters match the stream header is tried in keyset order until one authenticates the requested segment. This is the Go counterpart to Tink-Java's StreamingAead.newSeekableDecryptingChannel.
iainmcgin
force-pushed
the
streamingaead-readerat-noncebased
branch
from
July 20, 2026 21:16
87ff222 to
67c5eea
Compare
iainmcgin
marked this pull request as ready for review
July 20, 2026 21:16
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.
Summary
Adds
streamingaead.NewDecryptingReaderAt, the Go counterpart to Java'sStreamingAead.newSeekableDecryptingChannel. This enables random-access decryption of streaming AEAD ciphertexts — for example, reading an arbitrary byte range from a large encrypted blob in object storage without downloading and decrypting the whole stream.API
CiphertextRangereturns the underlying ciphertext byte range thatReadAtwill request for a given plaintext range, so callers backed by remote storage can fetch one contiguous range up front instead of one round-trip per segment.Implementation notes
StreamingAeadSeekableDecryptingChannelin tink-java.ReadAtis safe for concurrent use and executes in parallel: all per-stream state (derived key, nonce prefix, segment geometry) is immutable after construction, the most-recently-decrypted segment is cached behind anatomic.Pointer, and segment decryption allocates fresh working buffers per call.hmac.Newper segment instead of reusing a singlehash.Hash. This also affects the existingNewDecryptingReaderpath; the per-segment cost is negligible relative to MAC computation over the segment.ReadAt, so reading at a large offset does not force a fetch of segment 0.Testing
(offset, len)grid, random-access fuzz, both AES-GCM-HKDF and AES-CTR-HMAC, multiple key templates and segment sizes, multi-key keysets.TestReaderAtModifiedCiphertext, mirroring the existingTestAESCTRHMACModifiedCiphertext): truncation at every 8 bytes (each asserted to fail a full-plaintext read), appended garbage, every-byte bit flip, segment deletion, segment duplication, AAD modification — each verified at many offsets to assert no read ever returns corrupted plaintext.SizedReaderAtreporting an incorrect size (TestDecryptingReaderAtWrongSize).ReadAtfrom multiple goroutines under-race, plus aRunParallelbenchmark.Truncation
Random access authenticates what it reads, so a read of a leading segment succeeds whether or not later segments are present, and
Size()is derived from the ciphertext length rather than from anything authenticated. What must not happen is for a caller who reads the whole plaintext to accept a truncated stream, and that is what the last-segment nonce flag establishes.A read that reaches the end of the plaintext therefore decrypts the final segment even when that segment carries no plaintext bytes, which is the case whenever the plaintext length is a multiple of the segment size. Without this, a stream cut to whole segments plus a tag would report a smaller
Size()and return that many correct bytes with no error, while the sequentialReaderrejects the same input. Reading the whole plaintext, or a single byte atSize()-1, is thus sufficient to establish that the size is genuine. An empty plaintext has no read that can reach its final segment, so that one is decrypted when the reader is constructed.TestReaderAtModifiedCiphertext/truncateasserts this over every truncation length: for each, reading the whole plaintext must fail.Commits
This PR is structured as three reviewable commits (noncebased core → subtle primitive wiring → keyset wrapper); each builds and passes tests independently.