Skip to content

feat(crypto): add ChaCha20-Poly1305 cipher support - #25837

Closed
jtdowney wants to merge 1 commit into
oven-sh:mainfrom
jtdowney:claude/chacha20-poly1305-support
Closed

jtdowney wants to merge 1 commit into
oven-sh:mainfrom
jtdowney:claude/chacha20-poly1305-support

Conversation

@jtdowney

@jtdowney jtdowney commented Jan 5, 2026

Copy link
Copy Markdown

Summary

Add EVP_CIPHER wrapper for ChaCha20-Poly1305 AEAD cipher to enable createCipheriv('chacha20-poly1305', ...) in node:crypto module.

  • Patch BoringSSL to expose EVP_chacha20_poly1305() function
  • Add EVP_CIPHER implementation wrapping BoringSSL primitives
  • Support streaming encryption with proper keystream buffering
  • Handle AEAD operations (AAD, auth tags)

This is needed for HomeKit/hap-nodejs compatibility and other applications that require this cipher.

Fixes #8072

Test plan

  • bun bd test test/regression/issue/8072.test.ts - 12 tests pass
  • Verified tests fail with system bun (Unknown cipher: chacha20-poly1305)
  • RFC 7539 test vectors pass
  • Streaming encryption matches single-call encryption

Add EVP_CIPHER wrapper for ChaCha20-Poly1305 AEAD cipher to enable
`createCipheriv('chacha20-poly1305', ...)` in node:crypto module.

This is needed for HomeKit/hap-nodejs compatibility and other applications
that require this cipher.

Implementation:
- Patch BoringSSL to expose EVP_chacha20_poly1305() function
- Add EVP_CIPHER implementation wrapping BoringSSL primitives
- Support streaming encryption with proper keystream buffering
- Handle AEAD operations (AAD, auth tags)

Fixes oven-sh#8072
@coderabbitai

coderabbitai Bot commented Jan 5, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This pull request introduces ChaCha20-Poly1305 cipher support to the crypto module. It adds a new EVP cipher implementation with complete AEAD functionality, integrates it into the cipher enumeration and lookup system, updates build configuration to include the new source file, and provides comprehensive regression tests covering encryption, decryption, authentication, and edge cases.

Changes

Cohort / File(s) Summary
ChaCha20-Poly1305 EVP Cipher Implementation
patches/boringssl/expose_chacha20_poly1305.patch
Introduces EVP_chacha20_poly1305() public function returning a static AEAD cipher. Implements init, cipher, cleanup, and control handlers. Wires cipher into enumeration/lookup by NID and name ("chacha20-poly1305"). Handles key/IV setup, tag management, streaming encryption/decryption, and control operations. Updates build configuration to include source file.
Regression Test Suite
test/regression/issue/8072.test.ts
Comprehensive test coverage for ChaCha20-Poly1305 including cipher discovery, metadata validation, encryption/decryption round-trips with and without AAD, authentication tag verification, key length validation, variable IV lengths, RFC 7539 test vectors, empty plaintext handling, large data (1 MB) operations, and incremental vs single-call encryption consistency.

Suggested reviewers

  • alii
  • dylan-conway

Pre-merge checks

✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: adding ChaCha20-Poly1305 cipher support to the crypto module.
Description check ✅ Passed The description covers both required template sections with clear details about what the PR does and how it was verified, exceeding minimum template requirements.
Linked Issues check ✅ Passed The PR fully addresses issue #8072 by implementing ChaCha20-Poly1305 cipher support in node:crypto, enabling hap-nodejs compatibility as requested.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing ChaCha20-Poly1305 cipher support with a BoringSSL patch, EVP_CIPHER implementation, and comprehensive test coverage—no extraneous modifications detected.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

Fix all issues with AI Agents 🤖
In @patches/boringssl/expose_chacha20_poly1305.patch:
- Around line 11-20: EVP_CIPHER_do_all_sorted must call ciphers in alphabetical
order; the added callback for EVP_chacha20_poly1305 ("chacha20-poly1305") is
placed after EVP_bf_cfb and breaks sorting. Move the
callback(EVP_chacha20_poly1305(), "chacha20-poly1305", NULL, arg) so it appears
before the entries for the "bf-*" ciphers (i.e., before callback(EVP_bf_cbc(),
...)) to restore correct alphabetical order.

In @test/regression/issue/8072.test.ts:
- Around line 93-99: The test title and content mismatch: the test named
"supports different IV lengths up to 12 bytes" only checks a 12-byte IV. Either
rename the test to "supports standard 12-byte IV" (update the test(...) string)
or extend the test to actually exercise other IV lengths by creating additional
ciphers (e.g., const cipher8 = createCipheriv("chacha20-poly1305", key,
Buffer.alloc(8, 0x11)); cipher8.update(plaintext); cipher8.final();
expect(cipher8.getAuthTag().length).toBe(16);) so the name matches behavior;
adjust or add assertions accordingly.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ce97887 and 34ce95f.

📒 Files selected for processing (2)
  • patches/boringssl/expose_chacha20_poly1305.patch
  • test/regression/issue/8072.test.ts
🧰 Additional context used
📓 Path-based instructions (5)
test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}: Use bun:test with files that end in *.test.{ts,js,jsx,tsx,mjs,cjs}
Do not write flaky tests. Never wait for time to pass in tests; always wait for the condition to be met instead of using an arbitrary amount of time
Never use hardcoded port numbers in tests. Always use port: 0 to get a random port
Prefer concurrent tests over sequential tests using test.concurrent or describe.concurrent when multiple tests spawn processes or write files, unless it's very difficult to make them concurrent
When spawning Bun processes in tests, use bunExe and bunEnv from harness to ensure the same build of Bun is used and debug logging is silenced
Use -e flag for single-file tests when spawning Bun processes
Use tempDir() from harness to create temporary directories with files for multi-file tests instead of creating files manually
Prefer async/await over callbacks in tests
When callbacks must be used and it's just a single callback, use Promise.withResolvers to create a promise that can be resolved or rejected from a callback
Do not set a timeout on tests. Bun already has timeouts
Use Buffer.alloc(count, fill).toString() instead of 'A'.repeat(count) to create repetitive strings in tests, as ''.repeat is very slow in debug JavaScriptCore builds
Use describe blocks for grouping related tests
Always use await using or using to ensure proper resource cleanup in tests for APIs like Bun.listen, Bun.connect, Bun.spawn, Bun.serve, etc
Always check exit codes and test error scenarios in error tests
Use describe.each() for parameterized tests
Use toMatchSnapshot() for snapshot testing
Use beforeAll(), afterEach(), beforeEach() for setup/teardown in tests
Track resources (servers, clients) in arrays for cleanup in afterEach()

Files:

  • test/regression/issue/8072.test.ts
test/regression/issue/**/*.test.ts

📄 CodeRabbit inference engine (test/CLAUDE.md)

Regression tests for specific issues go in /test/regression/issue/${issueNumber}.test.ts. Do not put tests without issue numbers in the regression directory

Files:

  • test/regression/issue/8072.test.ts
**/*.test.ts?(x)

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.test.ts?(x): Never use bun test directly - always use bun bd test to run tests with debug build changes
For single-file tests, prefer -e flag over tempDir
For multi-file tests, prefer tempDir and Bun.spawn over single-file tests
Use normalizeBunSnapshot to normalize snapshot output of tests
Never write tests that check for 'panic', 'uncaught exception', or similar strings in test output
Use tempDir from harness to create temporary directories - do not use tmpdirSync or fs.mkdtempSync
When spawning processes in tests, expect stdout before expecting exit code for more useful error messages on test failure
Do not write flaky tests - do not use setTimeout in tests; instead await the condition to be met
Verify tests fail with USE_SYSTEM_BUN=1 bun test <file> and pass with bun bd test <file> - tests are invalid if they pass with USE_SYSTEM_BUN=1
Test files must end with .test.ts or .test.tsx
Avoid shell commands like find or grep in tests - use Bun's Glob and built-in tools instead

Files:

  • test/regression/issue/8072.test.ts
test/regression/issue/*.test.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Place regression tests for specific GitHub issues in test/regression/issue/${issueNumber}.test.ts with real issue numbers only

Files:

  • test/regression/issue/8072.test.ts
test/**/*.test.ts?(x)

📄 CodeRabbit inference engine (CLAUDE.md)

Always use port: 0 in tests - do not hardcode ports or use custom random port number functions

Files:

  • test/regression/issue/8072.test.ts
🧠 Learnings (2)
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/test/v8/v8.test.ts : Add corresponding test cases to test/v8/v8.test.ts using checkSameOutput() function to compare Node.js and Bun output

Applied to files:

  • test/regression/issue/8072.test.ts
📚 Learning: 2025-11-24T18:37:30.259Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: test/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:37:30.259Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Use `Buffer.alloc(count, fill).toString()` instead of `'A'.repeat(count)` to create repetitive strings in tests, as ''.repeat is very slow in debug JavaScriptCore builds

Applied to files:

  • test/regression/issue/8072.test.ts
🧬 Code graph analysis (1)
test/regression/issue/8072.test.ts (1)
test/js/node/test/parallel/test-crypto-getcipherinfo.js (1)
  • ciphers (14-14)
🔇 Additional comments (16)
test/regression/issue/8072.test.ts (6)

1-12: LGTM!

Good use of bun:test imports and proper test setup with Buffer.alloc. The sequential initialization of key/IV values (lines 11-12) is correctly done with a loop since you need incrementing values.


14-53: Well-structured basic tests.

Good coverage of cipher listing, info retrieval, and round-trip encryption with and without AAD. The auth tag length assertion (line 32) is a nice touch.


55-83: Good error scenario coverage.

Properly tests that decryption fails with corrupted auth tag and incorrect AAD. The XOR corruption technique (line 62) is a good approach.


101-137: Excellent use of RFC 7539 test vectors.

Using the official test vector from Section 2.8.2 provides strong validation that the implementation is correct. The bidirectional verification (encrypt then decrypt) is thorough.


139-168: Good edge case coverage.

Empty plaintext and large data (1MB) tests properly exercise boundary conditions. Using Buffer.alloc(1024 * 1024, 0x42) follows coding guidelines.


170-193: Streaming consistency test is well-designed.

This test validates an important property - that incremental encryption produces the same result as single-call encryption. Critical for streaming use cases.

patches/boringssl/expose_chacha20_poly1305.patch (10)

65-82: Well-structured context for streaming AEAD.

The context properly tracks all state needed for streaming encryption: keystream buffer, usage counter, block counter, and Poly1305 state.


84-91: LGTM - correct little-endian length encoding.

The implicit truncation to uint8_t correctly extracts each byte. This matches RFC 7539's Poly1305 length field format.


93-102: Correct Poly1305 key derivation.

Per RFC 7539, the Poly1305 key is derived from the first ChaCha20 block (counter=0). The implementation correctly encrypts zeros to get the keystream as the Poly1305 key.


104-132: LGTM - proper key/IV initialization.

State reset is correct: keystream_used = 64 forces fresh keystream generation, and counter = 1 reserves counter=0 for Poly1305 key derivation per RFC 7539.


134-138: Good security practice.

Using OPENSSL_cleanse ensures sensitive key material is securely erased and won't be optimized away by the compiler.


177-184: Good constant-time tag comparison.

Using CRYPTO_memcmp for authentication tag verification prevents timing attacks. This is a critical security requirement for AEAD ciphers.


200-266: Streaming implementation is correct.

The three-step approach (use remaining keystream → process full blocks → buffer partial block) correctly handles streaming encryption while maintaining keystream state across calls. The Poly1305 update order (ciphertext) is correct for both encrypt and decrypt paths.


287-291: Verify IV length handling consistency.

EVP_CTRL_AEAD_SET_IVLEN accepts IVs from 1-12 bytes, but chacha20_poly1305_init_key (line 119) always copies exactly 12 bytes from the IV. If a caller sets a shorter IV length, they must still provide a 12-byte buffer. This is consistent with BoringSSL's behavior but could be documented.


325-333: LGTM - context copy implementation.

Properly handles EVP_CTRL_COPY for cipher context duplication. The null checks ensure safety.


340-358: LGTM - correct cipher object definition.

The static EVP_CIPHER is properly configured:

  • block_size=1 for stream cipher
  • Correct key/IV lengths (32/12)
  • Appropriate flags for AEAD operation with custom IV and streaming support

Comment on lines +11 to +20
--- a/decrepit/evp/evp_do_all.cc
+++ b/decrepit/evp/evp_do_all.cc
@@ -36,6 +36,7 @@ void EVP_CIPHER_do_all_sorted(void (*callback)(const EVP_CIPHER *cipher,
callback(EVP_aes_256_gcm(), "aes-256-gcm", NULL, arg);
callback(EVP_bf_cbc(), "bf-cbc", NULL, arg);
callback(EVP_bf_cfb(), "bf-cfb", NULL, arg);
+ callback(EVP_chacha20_poly1305(), "chacha20-poly1305", NULL, arg);
callback(EVP_bf_ecb(), "bf-ecb", NULL, arg);
callback(EVP_des_cbc(), "des-cbc", NULL, arg);
callback(EVP_des_ecb(), "des-ecb", NULL, arg);

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.

⚠️ Potential issue | 🟡 Minor

Cipher not inserted in alphabetical order.

EVP_CIPHER_do_all_sorted implies alphabetical ordering. "chacha20-poly1305" should come before "bf-cbc" (c < bf), not after "bf-cfb".

🔎 Proposed fix
 void EVP_CIPHER_do_all_sorted(void (*callback)(const EVP_CIPHER *cipher,
   callback(EVP_aes_256_gcm(), "aes-256-gcm", NULL, arg);
+  callback(EVP_chacha20_poly1305(), "chacha20-poly1305", NULL, arg);
   callback(EVP_bf_cbc(), "bf-cbc", NULL, arg);
   callback(EVP_bf_cfb(), "bf-cfb", NULL, arg);
-  callback(EVP_chacha20_poly1305(), "chacha20-poly1305", NULL, arg);
   callback(EVP_bf_ecb(), "bf-ecb", NULL, arg);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @patches/boringssl/expose_chacha20_poly1305.patch around lines 11-20,
EVP_CIPHER_do_all_sorted must call ciphers in alphabetical order; the added
callback for EVP_chacha20_poly1305 ("chacha20-poly1305") is placed after
EVP_bf_cfb and breaks sorting. Move the callback(EVP_chacha20_poly1305(),
"chacha20-poly1305", NULL, arg) so it appears before the entries for the "bf-*"
ciphers (i.e., before callback(EVP_bf_cbc(), ...)) to restore correct
alphabetical order.

Comment on lines +93 to +99
test("supports different IV lengths up to 12 bytes", () => {
// 12-byte IV is standard
const cipher12 = createCipheriv("chacha20-poly1305", key, Buffer.alloc(12, 0x11));
cipher12.update(plaintext);
cipher12.final();
expect(cipher12.getAuthTag().length).toBe(16);
});

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.

🧹 Nitpick | 🔵 Trivial

Test name doesn't match test content.

The test is titled "supports different IV lengths up to 12 bytes" but only verifies the standard 12-byte IV. Consider either:

  1. Renaming to "supports standard 12-byte IV", or
  2. Adding tests for shorter IV lengths (e.g., 8-byte) if they're actually supported
🔎 Option 1: Rename to match actual behavior
-  test("supports different IV lengths up to 12 bytes", () => {
-    // 12-byte IV is standard
+  test("supports standard 12-byte IV", () => {
     const cipher12 = createCipheriv("chacha20-poly1305", key, Buffer.alloc(12, 0x11));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test("supports different IV lengths up to 12 bytes", () => {
// 12-byte IV is standard
const cipher12 = createCipheriv("chacha20-poly1305", key, Buffer.alloc(12, 0x11));
cipher12.update(plaintext);
cipher12.final();
expect(cipher12.getAuthTag().length).toBe(16);
});
test("supports standard 12-byte IV", () => {
const cipher12 = createCipheriv("chacha20-poly1305", key, Buffer.alloc(12, 0x11));
cipher12.update(plaintext);
cipher12.final();
expect(cipher12.getAuthTag().length).toBe(16);
});
🤖 Prompt for AI Agents
In @test/regression/issue/8072.test.ts around lines 93-99, The test title and
content mismatch: the test named "supports different IV lengths up to 12 bytes"
only checks a 12-byte IV. Either rename the test to "supports standard 12-byte
IV" (update the test(...) string) or extend the test to actually exercise other
IV lengths by creating additional ciphers (e.g., const cipher8 =
createCipheriv("chacha20-poly1305", key, Buffer.alloc(8, 0x11));
cipher8.update(plaintext); cipher8.final();
expect(cipher8.getAuthTag().length).toBe(16);) so the name matches behavior;
adjust or add assertions accordingly.

@robobun

robobun commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Picked this up in #33305, which takes the same approach but lands EVP_chacha20_poly1305() in the oven-sh/boringssl fork (oven-sh/boringssl#9) rather than as a patches/ file, at a maintainer's request.

Worth flagging one thing from this PR that no longer holds: BoringSSL has since split evp_cipher_st's single cipher callback into cipher_update, cipher_final and update_aad, so the positional initializer here would bind cipher to cipher_update and leave cipher_final/update_aad pointing at cleanup/ctrl. Also, the new e_chacha20poly1305_cipher.cc was only added to gen/sources.cmake, and Bun's build now lists BoringSSL's sources explicitly in scripts/build/deps/boringssl.ts, so it would never have been compiled.

Thanks for the groundwork on this either way.

@jtdowney

Copy link
Copy Markdown
Author

Superseded by #33305

@jtdowney jtdowney closed this Jul 23, 2026
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.

Support ChaCha20-Poly1305 in node:crypto

2 participants