Skip to content

perf: use the native crypto of Node.js for Curve25519 and Ed25519 - #1135

Merged
bwp91 merged 5 commits into
homebridge:latestfrom
StefanRiese:perf/native-pair-verify-crypto
Oct 24, 2026
Merged

bwp91 merged 5 commits into
homebridge:latestfrom
StefanRiese:perf/native-pair-verify-crypto

Conversation

@StefanRiese

Copy link
Copy Markdown
Contributor

Problem

Pair verify and pair setup do their X25519 and Ed25519 with tweetnacl, in JavaScript, on the event loop. That is slow on the small machines many bridges run on. Measured on a Raspberry Pi (OpenCCU, Node.js 22):

per operation tweetnacl Node.js crypto
X25519 key pair / shared secret 6.6 / 6.4 ms 0.5 / 0.6 ms
Ed25519 sign / verify 36 / 66 ms 0.4 / 0.8 ms

Meanwhile the reads of the controllers waited, and hap-nodejs logged "didn't respond at all" for them.

The move to tweetnacl (#664) came from the native ed25519/curve25519 add-ons that were hard to build. Node.js has had X25519 and Ed25519 built in for years (generateKeyPairSync since v12, crypto.diffieHellman since v13.9), and the engines field already requires Node.js 22.

Change

  • hapCrypto uses the crypto built into Node.js (OpenSSL) for generateCurve25519KeyPair and generateCurve25519SharedSecKey, and gets generateEd25519KeyPair, ed25519Sign and ed25519Verify. HAPServer (pair setup M5, pair verify M1/M3) and AccessoryInfo.create use them.
  • Ed25519 secret keys keep the 64-byte layout of tweetnacl (seed + public key), so the stored long-term keys of existing accessories work unchanged. Ed25519 is deterministic, so the signatures are the same as before.
  • generateCurve25519SharedSecKey still returns zeros for a public key of small order, as tweetnacl did (OpenSSL refuses it). The controller still has to sign with its long-term key, so it cannot pair with it.
  • ed25519Verify returns false for a signature or public key of the wrong size, or a public key that is no point on the curve, where tweetnacl threw for the wrong sizes. A malformed proof in pair setup M5 or pair verify M3 now gets the authentication error response.
  • tweetnacl is only used by the tests now (the test clients in src/test-utils and the cross-checks), so it moves to the devDependencies. The lockfile change is limited to that.

Tests

  • hapCrypto.spec.ts:
    • the RFC 7748 X25519 test vector, and the RFC 8032 Ed25519 test vectors 1 and 2
    • cross-checks against tweetnacl with random keys: the same shared secrets, the same signatures, verification of tweetnacl's signatures, the same answer for random public keys that are no point on the curve, and key pairs in tweetnacl's layout (keyPair.fromSeed)
    • zeros for a public key of small order, rejected keys and signatures of the wrong size
  • The pair setup and pair verify tests of HAPServer.spec.ts run with the tweetnacl-based test clients against the native server side.
  • npm run lint and npm run build pass. npx jest: 921 passed in 26 suites. In one of two full runs, one test in Accessory.spec.ts failed under load; it passed alone three times and in the other full run.
  • The same native calls (patched into hap-nodejs 2.2.3 at runtime) run on my Raspberry Pi with 7 bridges in homekit-ccu: all controllers reconnect after restarts, CPU at the start went from 24.8 s to 17.7 s in the first 42 s, pair verify from 6.5 s to 0.6 s, and the "didn't respond" warnings stopped (0 in 4 starts, before up to 22 per start).

Pair verify and pair setup did their X25519 and Ed25519 with tweetnacl,
in JavaScript. On a Raspberry Pi one pair verify (key pair, shared
secret, sign, verify) took about 115 ms, with the crypto built into
Node.js (OpenSSL) about 2 ms. After a restart every controller connects
to every bridge at once; on a Raspberry Pi with 7 bridges that kept the
event loop busy for seconds (6.5 s of CPU in a profile of one start),
and reads of the controllers timed out meanwhile.

hapCrypto now has generateEd25519KeyPair, ed25519Sign and ed25519Verify
next to the Curve25519 functions, all native. Ed25519 secret keys keep
the 64-byte layout of tweetnacl (seed and public key), so the stored
long-term keys of accessories work unchanged, and the signatures are the
same (Ed25519 is deterministic). generateCurve25519SharedSecKey still
returns zeros for a public key of small order, as tweetnacl did, where
OpenSSL refuses it. ed25519Verify returns false for a signature or
public key of the wrong size instead of throwing.

tweetnacl is only used by the tests now (the test clients and the
cross-checks), so it moves to the devDependencies. The engines field
already requires Node.js 22, which has X25519 and Ed25519.
generateCurve25519SharedSecKey returned an all-zero secret for any
error of OpenSSL. Only a public key of small order makes OpenSSL fail
the derivation (ERR_OSSL_FAILED_DURING_DERIVATION) once the key sizes
are checked; that one still gives the zeros tweetnacl returned, any
other error is thrown. The test covers four points of small order (0,
1, a point of order 8, p - 1).
@hjdhjd

hjdhjd commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Thanks for the contribution - I’ll review this over the weekend.

@hjdhjd
hjdhjd self-requested a review October 15, 2026 16:26
@coveralls

coveralls commented Oct 19, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 39374458514

Coverage increased (+0.06%) to 67.026%

Details

  • Coverage increased (+0.06%) from the base build.
  • Patch coverage: 2 uncovered changes across 1 file (34 of 36 lines covered, 94.44%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
src/lib/util/hapCrypto.ts 30 28 93.33%
Total (3 files) 36 34 94.44%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 9706
Covered Lines: 6822
Line Coverage: 70.29%
Relevant Branches: 3471
Covered Branches: 2010
Branch Coverage: 57.91%
Branches in Coverage %: Yes
Coverage Strength: 365.76 hits per line

💛 - Coveralls

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.

🔵 Needs a closer look

Authentication cryptography, persisted-key compatibility, and low-order-key handling warrant final human security review.

1 open issue
What changed in this PR

Moves pairing cryptography from TweetNaCl to Node.js native crypto to reduce event-loop blocking while preserving stored accessory keys.

Changes:

  • Adds native X25519 and Ed25519 helpers and updates pairing call sites.
  • Adds standards-based vectors and TweetNaCl compatibility tests.
  • Moves TweetNaCl to development dependencies.
File Description
src/​lib/​util/​hapCrypto.ts Implements native cryptography helpers.
src/​lib/​util/​hapCrypto.spec.ts Adds compatibility and invalid-input tests.
src/​lib/​model/​AccessoryInfo.ts Uses native accessory key generation.
src/​lib/​HAPServer.ts Uses native pairing signatures and verification.
package.json Makes TweetNaCl development-only.
package-lock.json Updates dependency classification.

Review effort: Balanced

Comment thread src/lib/util/hapCrypto.ts Resolved
Comment thread src/lib/util/hapCrypto.ts Resolved
…r key fallback preserving authentication only'

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@hjdhjd

hjdhjd commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

@StefanRiese Code is solid - thanks for the contribution! Can you make the edit to the comments above (removing them) and I'll clear this for commit?

@StefanRiese

Copy link
Copy Markdown
Contributor Author

@StefanRiese Code is solid - thanks for the contribution! Can you make the edit to the comments above (removing them) and I'll clear this for commit?

Done.

@bwp91 bwp91 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.

Thanks!

@bwp91
bwp91 merged commit f1d8ee7 into homebridge:latest Oct 24, 2026
16 checks passed
@bwp91

bwp91 commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

Hi @StefanRiese
This change has been included in the v2.2.4-beta series and will be included in the upcoming v2.2.4 release.

@StefanRiese
StefanRiese deleted the perf/native-pair-verify-crypto branch October 25, 2026 18:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants