Skip to content

Introduce page-level ChaCha20-Poly1305 helpers to enable fused implementations - #259

Merged
utelle merged 1 commit into
utelle:mainfrom
matbech:chacha20-poly1305-api
Sep 6, 2026
Merged

Introduce page-level ChaCha20-Poly1305 helpers to enable fused implementations#259
utelle merged 1 commit into
utelle:mainfrom
matbech:chacha20-poly1305-api

Conversation

@matbech

@matbech matbech commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Description

Introduce two internal page-level functions for the ChaCha20-Poly1305 cipher:

chacha20_poly1305_page_encrypt(...)
chacha20_poly1305_page_decrypt(...)

Initially, these functions would use the existing scalar chacha20_xor() and poly1305() implementations. This is primarily an internal refactoring with no database-format or external API change.
The abstraction would allow architecture-specific implementations-particularly a fused WebAssembly SIMD implementation without duplicating or restructuring cipher_chacha20.c.

Type of Change

Please select the type of change:

  • Refactor (no behavior change)

Motivation

For ordinary authenticated database pages, the current implementation performs ChaCha20 and Poly1305 as separate operations.
This scalar organization is straightforward, but it prevents optimized implementations from combining the two operations.
ChaCha20 and Poly1305 contain largely independent arithmetic. A fused implementation can interleave Poly1305 work with otherwise latency-bound ChaCha20 rounds, reducing separate memory traversal and improving instruction-level parallelism.
This is particularly useful for WebAssembly SIMD, where ChaCha20 can process four blocks in parallel while scalar Poly1305 work is scheduled between the vectorized ChaCha20 rounds.

Benchmarks

For complete authenticated page decryption:

Runtime 4 KiB pages 64 KiB pages
Node.js 26.7.0 23.6% faster 28.7% faster
Chromium 153 21.8% faster 23.9% faster
Firefox 155 34.7% faster 40.2% faster

Each sample decrypted 128 MiB.

Current raw fused throughput

The resulting combined ChaCha20-Poly1305 implementation - including encryption and authentication or decryption and tag verification measured:

Runtime Page size Encrypt Decrypt
Node.js 26.7.0 4 KiB 1.26 GB/s 1.29 GB/s
Node.js 26.7.0 64 KiB 1.33 GB/s 1.34 GB/s
Chrome 152 4 KiB 1.28 GB/s 1.27 GB/s
Chrome 152 64 KiB 1.35 GB/s 1.34 GB/s
Firefox 155 4 KiB 1.39 GB/s 1.42 GB/s
Firefox 155 64 KiB 1.56 GB/s 1.52 GB/s

Checklist

  • I have independently verified the issue
  • I am not submitting unverified or speculative changes
  • I understand that AI-assisted changes must be reviewed by a human before submission

… cipher:

chacha20_poly1305_page_encrypt(...)
chacha20_poly1305_page_decrypt(...)

Initially, these functions would use the existing scalar chacha20_xor() and
poly1305() implementations. This is primarily an internal refactoring with no
database-format or external API change.
The abstraction would allow architecture-specific implementations-particularly
a fused WebAssembly SIMD implementation without duplicating or restructuring
cipher_chacha20.c.
@utelle

utelle commented Sep 3, 2026

Copy link
Copy Markdown
Owner

In principle, I have nothing against this restructuring. However, I have some questions.

  1. Where did you get the performance data from? Do you already have a fused implementation?
  2. For the page decryption a fused implementation will decrypt page data, before the tag was determined and checked. That's exactly what you changed in PR chacha20: Reorder MAC verification and decryption process #254 to guarantee that tag verification is done before decrypting the page data. I guess you use an additional local buffer, which has to be copied to SQLite's page buffer, after the tag was verified. Is this assumption correct?

I will check the PR in more detail after the weekend.

@matbech

matbech commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

1 Where did you get the performance data from? Do you already have a fused implementation?

Yes. I have implemented and benchmarked a fused ChaCha20-Poly1305 path using WebAssembly SIMD. The reported results come from that downstream prototype. On the same hardware, openssl speed with AVX disabled (set OPENSSL_ia32cap=:0) achieves approximately 1.8 GB/s. This suggests that the current fused WASM SIMD implementation, at roughly 1.6 GB/s depending on the runtime and page size, is already reasonably close to the practical performance ceiling for 128-bit SIMD.

2 I guess you use an additional local buffer

No. The fused implementation does not use an additional page-sized buffer.

It feeds each ciphertext group into the Poly1305 calculation before overwriting that group with plaintext, interleaving the Poly1305 arithmetic with the ChaCha20 rounds. The final tag comparison can naturally only occur after the entire page has been processed.

If the comparison fails, the complete page buffer is securely cleared with sqlite3mcSecureZeroMemory() and SQLITE_CORRUPT is returned. Consequently, unauthenticated plaintext is never returned successfully to the SQLite pager, although plaintext temporarily exists in the page buffer before the final tag verification.

I considered this an acceptable compromise because the pager cannot consume the page successfully before the codec callback returns. However, it does not preserve the stronger invariant that no decryption may occur before verification.
If that invariant is required, the fused implementation would need a page-sized temporary buffer, and the published performance results would need to be remeasured with the additional copy.

The scalar implementation in this PR still verifies the tag before decrypting.

@utelle

utelle commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Thanks for the detailed information.

For the database itself the approach with zeroing the buffer, before returning should work as expected. Although I don't know for sure that sqlite3mcSecureZeroMemory() works correctly in WASM builds (Binaryen's wasm-opt could possibly remove the memset call).

If the database is in WAL journal mode things are a bit different. That is, I have to verify that your approach would work there, too.

Using an extra local buffer would guarantee to be on the safe side, but it comes at the price of extra memory and of copying the buffer.

@matbech

matbech commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

I benchmarked our fused WASM SIMD implementation against the prebuilt libsodium.js WASM distribution (libsodium-sumo 0.8.4, reporting libsodium 1.0.22).

Both implementations perform IETF ChaCha20-Poly1305 encryption and authentication, or decryption and tag verification, using identical inputs. Ciphertext and tags were checked for equality.

Median throughput on an AMD Ryzen 7 7840HS, in decimal GB/s, for 64 KiB-sized records (65,504-byte payloads):

Browser Our encrypt libsodium encrypt Our decrypt libsodium decrypt
Chrome 152 1.314 0.581 1.338 0.587
Firefox 155 1.566 0.327 1.529 0.327

Measurements exclude allocations, input/output copies, random-number generation, and I/O. Our implementation uses a benchmark-only adapter to match libsodium’s IETF framing, rather than SQLite3MC’s page format.

Even before fusion, our WASM SIMD implementation achieved approximately 1 GB/s for combined encryption and authentication in earlier 4 KiB-page benchmarks. Consequently, the entire advantage over libsodium cannot be attributed to fusion: these results compare different implementations and builds, not a controlled fused-versus-unfused experiment.

@utelle

utelle commented Sep 6, 2026

Copy link
Copy Markdown
Owner

I checked the WAL journal mode, and AFAICT it should not impose any additional problem. So, I'm going to merge the PR.

Nevertheless, we should discuss whether to use an extra local buffer for decrypting with a fused implementation. A local buffer we would have fully under control, while that is not true for SQLite's page buffer, although the current SQLite implementation behaves correctly.

@utelle
utelle merged commit 2188fd8 into utelle:main Sep 6, 2026
9 checks passed
@matbech
matbech deleted the chacha20-poly1305-api branch September 6, 2026 10:41
@utelle

utelle commented Sep 8, 2026

Copy link
Copy Markdown
Owner

The implementations of combined chacha20-poly1305 with hardware acceleration for example in OpenSSL are mostly based on assembler code. For SQLite3MC I'd like to avoid separate or embedded assembler code. libsodium's variant for poly1305 can't be compiled with MSVC, because MSVC lacks support for uint128_t. And the combined algorithm for chacha20-poly1305 in libsodium is not really fused, just interleaved.

I'd really like to see your fused implementation, so that I can check how much effort it would be to adjust it to architectures x86_64 and aarch64.

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.

2 participants