[Storage] Widen CSE Encoder Counter for Region Nonce Index - #50205
Open
Isabelle (ibrandes) wants to merge 2 commits into
Open
[Storage] Widen CSE Encoder Counter for Region Nonce Index#50205Isabelle (ibrandes) wants to merge 2 commits into
Isabelle (ibrandes) wants to merge 2 commits into
Conversation
|
Azure Pipelines: Successfully started running 1 pipeline(s). 34 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
Contributor
There was a problem hiding this comment.
Pull request overview
Widens CSEv2 region nonce indexing from 32 to 64 bits to prevent AES-GCM nonce reuse.
Changes:
- Encodes region indices as 64-bit nonce values.
- Adds boundary and end-to-end regression tests.
- Documents the security fix in the changelog.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
EncryptorV2.java |
Uses full 64-bit region indices for nonces. |
EncryptorV2NonceTests.java |
Adds nonce encoding and uniqueness tests. |
CHANGELOG.md |
Records the nonce-counter fix. |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Isabelle (ibrandes)
marked this pull request as ready for review
August 20, 2026 22:03
Isabelle (ibrandes)
requested review from
a team,
Alan Zimmer (alzimmermsft),
browndav-msft,
gunjansingh-msft,
Kyle Knapp (kyleknap) and
Sean McCullough (seanmcc-msft)
as code owners
August 20, 2026 22:03
|
Azure Pipelines: Successfully started running 1 pipeline(s). 34 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
Isabelle (ibrandes)
requested review from
Jacob Lauzon (jalauzon-msft) and
Jocelyn (jaschrep-msft)
August 20, 2026 22:03
Jocelyn (jaschrep-msft)
approved these changes
Aug 21, 2026
Jacob Lauzon (jalauzon-msft)
approved these changes
Aug 21, 2026
browndav-msft
approved these changes
Aug 24, 2026
browndav-msft
left a comment
Member
There was a problem hiding this comment.
I think we just need to add a few tests to prevent an accidental regression.
Comment on lines
+111
to
+113
| // once with a given key. Truncating the index to 32 bits would cause nonces to repeat - and | ||
| // AES-GCM security to break - once a blob exceeds 2^32 authenticated regions. | ||
| gcmCipher = getCipher(tuple.getT1()); |
Member
There was a problem hiding this comment.
I think there are two things we could explain here:
- it would cause
nonces to repeat and - Half of the region indices would narrow to negative — every other 2^31-sized band, alternating from 2^31 up to 2^63 — yielding nonces with an FFFFFFFF prefix rather than 00000000 when widened from 32-bit to 64-bit in
computeRegionNonce()
| * nonce reuse (a security failure). These tests exercise the encoding directly and end-to-end through | ||
| * {@link EncryptorV2#encrypt(Flux)}. | ||
| */ | ||
| public class EncryptorV2NonceTests { |
Member
There was a problem hiding this comment.
nit: I think we should name this EncryptorV2Tests because I imagine we'll test more than the nonce at some point 🤷♂️
| * nonce reuse (a security failure). These tests exercise the encoding directly and end-to-end through | ||
| * {@link EncryptorV2#encrypt(Flux)}. | ||
| */ | ||
| public class EncryptorV2NonceTests { |
Member
There was a problem hiding this comment.
Just something to check:
- I think we might be missing some tests to ensure that the bug isn't reintroduced. The tests all call computeRegionNonce(long) directly, but the defect was at the call site
getCipher(tuple.getT1().intValue()). Readd.intValue()and the suite still passes. - Also, nonceEncodesRegionIndexAsBigEndianLongWithTrailingZeros builds its expected value with ByteBuffer.putLong, which I think is the same as the implementation, so it mirrors the code . Could we add assertions against hardcoded expected bytes at these boundaries?
0// all-zero nonce1// smallest non-zero; catches wrong offset or endianness immediately2// confirms the counter increments by one, not by region size255// byte boundary; catches a signed-byte or nibble-order slip(1L << 31) - 1// last correct index under the old code1L << 31// sign-extension boundary; old code emitted a leading FFFFFFFF here(1L << 32) - 1// last index of the negative band, decoded as -1 by the old code1L << 32// wraparound origin; old code collided this with region 0(1L << 32) + 5// wraparound, non-zero; old code collided this with region 51L << 40// sets a byte the old code could never reach; guards the high half
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.
Fix client-side encryption (v2) region nonce counter truncation
merge this before region detection pr
Summary
The CSE v2 encryptor (
EncryptorV2) derived each authenticated region's AES-GCM nonce from the region's sequential index, but truncated that index to a 32-bitintbefore encoding it. As a result, region nonces repeat every 2³² regions — reusing region N's nonce for region N + 2³² under the same content-encryption key. AES-GCM security depends on nonces never repeating for a given key, so this is a latent nonce-reuse bug. It also undermines any position-based reorder detection built on the nonce.This PR widens the counter to the full 64-bit
long, guaranteeing a unique nonce for every region.Reason for the change
EncryptorV2.getCipheraccepted anint index, andencrypt()passedtuple.getT1().intValue(), truncating the 64-bit region index.ByteBuffer.putLong(index)then wrote the sign-extended/truncated value, so:2³²collided with region0,2³¹onward were sign-extended (nonce prefixed with0xFFFFFFFF).Changes
EncryptorV2.getCipher(int)→getCipher(long);encrypt()now passes the fullLongindex (no.intValue()).static byte[] computeRegionNonce(long index)that encodes the index as an 8-byte big-endian value in the leading bytes of the 12-byte nonce.CHANGELOG.md— added a Bugs Fixed entry.Compatibility
No wire-format change for any blob reachable in practice. For every region index
< 2³¹,putLong((int) i)andputLong((long) i)produce identical bytes (a positiveintwidens to the samelong), so existing blobs remain byte-for-byte compatible and continue to decrypt unchanged. The old and new encodings only diverge at region index≥ 2³¹, i.e. ≈ 8 EiB at the default 4 MiB region size — not reachable by any real blob. Decryption is unaffected regardless, since the decryptor reads the nonce inline from the ciphertext rather than reconstructing it.Notes
longas well so that a swap of two regions exactly 2³² apart is also detectable (unreachable today, but tidy). No functional impact for reachable blobs.