Skip to content

[Storage] Widen CSE Encoder Counter for Region Nonce Index - #50205

Open
Isabelle (ibrandes) wants to merge 2 commits into
Azure:mainfrom
ibrandes:bugfix/storage/encoderCounterFix
Open

[Storage] Widen CSE Encoder Counter for Region Nonce Index#50205
Isabelle (ibrandes) wants to merge 2 commits into
Azure:mainfrom
ibrandes:bugfix/storage/encoderCounterFix

Conversation

@ibrandes

@ibrandes Isabelle (ibrandes) commented Aug 20, 2026

Copy link
Copy Markdown
Member

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-bit int before 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.getCipher accepted an int index, and encrypt() passed tuple.getT1().intValue(), truncating the 64-bit region index. ByteBuffer.putLong(index) then wrote the sign-extended/truncated value, so:

  • region 2³² collided with region 0,
  • region 2³¹ onward were sign-extended (nonce prefixed with 0xFFFFFFFF).

Changes

  • EncryptorV2.getCipher(int)getCipher(long); encrypt() now passes the full Long index (no .intValue()).
  • Extracted a small, testable helper 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.
  • Updated the explanatory comment.
  • 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) and putLong((long) i) produce identical bytes (a positive int widens to the same long), 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

  • The related reorder-detection work (which validates each region's nonce against its expected position) is a separate PR. That PR's Java-scheme nonce reconstruction currently mirrors the old truncating encoding; once both merge, it can be widened to the full long as well so that a swap of two regions exactly 2³² apart is also detectable (unreachable today, but tidy). No functional impact for reachable blobs.

@ibrandes
Isabelle (ibrandes) requested a balanced review from Copilot August 20, 2026 21:36
@azure-pipelines

Copy link
Copy Markdown
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.

@github-actions github-actions Bot added the Storage Storage Service (Queues, Blobs, Files) label Aug 20, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread sdk/storage/azure-storage-blob-cryptography/CHANGELOG.md Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@ibrandes
Isabelle (ibrandes) requested a balanced review from Copilot August 20, 2026 21:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@azure-pipelines

Copy link
Copy Markdown
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.

@browndav-msft browndav-msft left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are two things we could explain here:

  1. it would cause nonces to repeat and
  2. 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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just something to check:

  1. 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.
  2. 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 nonce
  • 1 // smallest non-zero; catches wrong offset or endianness immediately
  • 2 // confirms the counter increments by one, not by region size
  • 255 // byte boundary; catches a signed-byte or nibble-order slip
  • (1L << 31) - 1 // last correct index under the old code
  • 1L << 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 code
  • 1L << 32 // wraparound origin; old code collided this with region 0
  • (1L << 32) + 5 // wraparound, non-zero; old code collided this with region 5
  • 1L << 40 // sets a byte the old code could never reach; guards the high half

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Storage Storage Service (Queues, Blobs, Files)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants