From cb75fedcd6d675012fa790029d180a3239d74f55 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Fri, 10 Nov 2023 06:53:31 +0000 Subject: [PATCH] fix: use native crypto if available Adds the lodestar [crypto implementation](https://github.com/ChainSafe/lodestar/blob/6f27ac6e5f439577906cbfe5f9e01c59586c2af3/packages/beacon-node/src/network/libp2p/noise.ts#L3) to this module for use in node. I'm trying to get js-libp2p back onto the [libp2p performance dashboard](https://observablehq.com/@libp2p-workspace/performance-dashboard) and in testing this change increases streaming throughput from around 60 MB/s to almost 300 MB/s. --- package.json | 6 ++- src/crypto/index.browser.ts | 3 ++ src/crypto/index.ts | 79 +++++++++++++++++++++++++++++++++++++ src/noise.ts | 4 +- 4 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 src/crypto/index.browser.ts create mode 100644 src/crypto/index.ts diff --git a/package.json b/package.json index adccecc..bbd67b3 100644 --- a/package.json +++ b/package.json @@ -68,13 +68,15 @@ "prepublish": "npm run build" }, "dependencies": { + "@chainsafe/as-chacha20poly1305": "^0.1.0", + "@chainsafe/as-sha256": "^0.4.1", "@libp2p/crypto": "^2.0.0", "@libp2p/interface": "^0.1.0", "@libp2p/logger": "^3.0.0", "@libp2p/peer-id": "^3.0.0", + "@noble/ciphers": "^0.4.0", "@noble/curves": "^1.1.0", "@noble/hashes": "^1.3.1", - "@noble/ciphers": "^0.4.0", "it-byte-stream": "^1.0.0", "it-length-prefixed": "^9.0.1", "it-length-prefixed-stream": "^1.0.0", @@ -108,7 +110,7 @@ "sinon": "^16.1.3" }, "browser": { - "./dist/src/alloc-unsafe.js": "./dist/src/alloc-unsafe-browser.js", + "./dist/src/crypto/index.js": "./dist/src/crypto/index.browser.js", "util": false } } diff --git a/src/crypto/index.browser.ts b/src/crypto/index.browser.ts new file mode 100644 index 0000000..f691a85 --- /dev/null +++ b/src/crypto/index.browser.ts @@ -0,0 +1,3 @@ +import { pureJsCrypto } from './js' + +export const defaultCrypto = pureJsCrypto diff --git a/src/crypto/index.ts b/src/crypto/index.ts new file mode 100644 index 0000000..a00634d --- /dev/null +++ b/src/crypto/index.ts @@ -0,0 +1,79 @@ +import crypto from 'node:crypto' +import { newInstance, ChaCha20Poly1305 } from '@chainsafe/as-chacha20poly1305' +import { digest } from '@chainsafe/as-sha256' +import { pureJsCrypto } from './js.js' +import type { ICryptoInterface } from '../crypto.js' + +const ctx = newInstance() +const asImpl = new ChaCha20Poly1305(ctx) +const CHACHA_POLY1305 = 'chacha20-poly1305' +const nodeCrypto: Pick = { + hashSHA256 (data) { + return crypto.createHash('sha256').update(data).digest() + }, + + chaCha20Poly1305Encrypt (plaintext, nonce, ad, k) { + const cipher = crypto.createCipheriv(CHACHA_POLY1305, k, nonce, { + authTagLength: 16 + }) + cipher.setAAD(ad, { plaintextLength: plaintext.byteLength }) + const updated = cipher.update(plaintext) + const final = cipher.final() + const tag = cipher.getAuthTag() + + const encrypted = Buffer.concat([updated, tag, final]) + return encrypted + }, + + chaCha20Poly1305Decrypt (ciphertext, nonce, ad, k, _dst) { + const authTag = ciphertext.slice(ciphertext.length - 16) + const text = ciphertext.slice(0, ciphertext.length - 16) + const decipher = crypto.createDecipheriv(CHACHA_POLY1305, k, nonce, { + authTagLength: 16 + }) + decipher.setAAD(ad, { + plaintextLength: text.byteLength + }) + decipher.setAuthTag(authTag) + const updated = decipher.update(text) + const final = decipher.final() + if (final.byteLength > 0) { + return Buffer.concat([updated, final]) + } + return updated + } +} + +const asCrypto: Pick = { + hashSHA256 (data) { + return digest(data) + }, + chaCha20Poly1305Encrypt (plaintext, nonce, ad, k) { + return asImpl.seal(k, nonce, plaintext, ad) + }, + chaCha20Poly1305Decrypt (ciphertext, nonce, ad, k, dst) { + return asImpl.open(k, nonce, ciphertext, ad, dst) + } +} + +// benchmarks show that for chacha20poly1305 +// the as implementation is faster for smaller payloads(<1200) +// and the node implementation is faster for larger payloads +export const defaultCrypto: ICryptoInterface = { + ...pureJsCrypto, + hashSHA256 (data) { + return nodeCrypto.hashSHA256(data) + }, + chaCha20Poly1305Encrypt (plaintext, nonce, ad, k) { + if (plaintext.length < 1200) { + return asCrypto.chaCha20Poly1305Encrypt(plaintext, nonce, ad, k) + } + return nodeCrypto.chaCha20Poly1305Encrypt(plaintext, nonce, ad, k) + }, + chaCha20Poly1305Decrypt (ciphertext, nonce, ad, k, dst) { + if (ciphertext.length < 1200) { + return asCrypto.chaCha20Poly1305Decrypt(ciphertext, nonce, ad, k, dst) + } + return nodeCrypto.chaCha20Poly1305Decrypt(ciphertext, nonce, ad, k, dst) + } +} diff --git a/src/noise.ts b/src/noise.ts index 2277a75..9a57813 100644 --- a/src/noise.ts +++ b/src/noise.ts @@ -3,7 +3,7 @@ import { lpStream, type LengthPrefixedStream } from 'it-length-prefixed-stream' import { duplexPair } from 'it-pair/duplex' import { pipe } from 'it-pipe' import { NOISE_MSG_MAX_LENGTH_BYTES } from './constants.js' -import { pureJsCrypto } from './crypto/js.js' +import { defaultCrypto } from './crypto/index.js' import { decryptStream, encryptStream } from './crypto/streaming.js' import { uint16BEDecode, uint16BEEncode } from './encoder.js' import { XXHandshake } from './handshake-xx.js' @@ -49,7 +49,7 @@ export class Noise implements INoiseConnection { constructor (init: NoiseInit = {}) { const { staticNoiseKey, extensions, crypto, prologueBytes, metrics } = init - this.crypto = crypto ?? pureJsCrypto + this.crypto = crypto ?? defaultCrypto this.extensions = extensions this.metrics = metrics ? registerMetrics(metrics) : undefined