Skip to content

Fix AES-GCM counter carry corrupting the keystream on dart2wasm - #29

Merged
dipu-bd merged 2 commits into
mainfrom
dipu-bd/issue28
Jul 27, 2026
Merged

Fix AES-GCM counter carry corrupting the keystream on dart2wasm#29
dipu-bd merged 2 commits into
mainfrom
dipu-bd/issue28

Conversation

@dipu-bd

@dipu-bd dipu-bd commented Jul 27, 2026

Copy link
Copy Markdown
Member

The GCM block counter lives in a Uint8List view over a Uint32List buffer. dart2wasm does not apply the modulo-256 truncation when such a view is assigned an out-of-range value, so counter[15]++ let the carry bleed into the adjacent byte: 0000FFFF incremented to 00010001 instead of 00010000. Past that first 32-bit carry, at ~1 MiB, the keystream desynchronised from every other platform, silently corrupting cross-platform ciphertext.

Mask every stored counter byte explicitly, pin inc32 against raw AES-ECB at each byte-carry boundary, add an end-to-end check across the 1 MiB boundary, and run the dart2wasm suite on every push rather than only at release time.

Fixes #28

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

Files with missing lines Coverage Δ
lib/src/algorithms/aes_modes/gcm.dart 100.00% <100.00%> (ø)

dipu-bd added 2 commits July 27, 2026 18:49
The GCM block counter lives in a Uint8List view over a Uint32List buffer.
dart2wasm does not apply the modulo-256 truncation when such a view is
assigned an out-of-range value, so `counter[15]++` let the carry bleed into
the adjacent byte: 0000FFFF incremented to 00010001 instead of 00010000.
Past that first 32-bit carry, at ~1 MiB, the keystream desynchronised from
every other platform, silently corrupting cross-platform ciphertext.

Mask every stored counter byte explicitly, pin inc32 against raw AES-ECB at
each byte-carry boundary, add an end-to-end check across the 1 MiB boundary,
and run the dart2wasm suite on every push rather than only at release time.

Fixes #28
A mis-carrying block counter still round-trips perfectly, so neither the
size sweeps in correctness_test.dart nor a decrypt-of-encrypt check can see
it. Assert instead that block j of a run started at counter C equals block 0
of a run started at C + j. The starting counter is an input, so this reaches
the 32- and 64-bit carries that no achievable message length could walk to.

Covers AES/Twofish/Blowfish CTR, ChaCha20 with both counter widths,
XChaCha20, Salsa20 and XSalsa20, plus a 1 MiB run per cipher for large-index
output addressing. Verified by mutation: breaking the carry in ctr.dart or
chacha20.dart fails exactly the affected subjects.
@dipu-bd
dipu-bd merged commit 527f152 into main Jul 27, 2026
3 checks passed
@dipu-bd
dipu-bd deleted the dipu-bd/issue28 branch July 27, 2026 15:13
@gmpassos

Copy link
Copy Markdown

@dipu-bd do you think that the benchmark table needs any update after that?

@dipu-bd

dipu-bd commented Jul 28, 2026

Copy link
Copy Markdown
Member Author

Benchmark will not be affected in a major way, ±5 Mbps at most. I'll do it later.

@gmpassos

Copy link
Copy Markdown

@dipu-bd, just confirming that it's working in production in a WASM-based PWA.

Thanks for the fix.

Best regards.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AES-GCM produces wrong output on dart2wasm for inputs ≥ ~1 MiB (silent cross-platform mismatch)

2 participants