Skip to content

Commit

Permalink
fix: use node.js crypto for x25519 keys
Browse files Browse the repository at this point in the history
Using node crypto to do X25519 key operations instead of `@noble/curves`
yields a nice little performance bump which translates to slightly lower
latencies when opening connections.

Running the `benchmark.js` file:

Before:

```console
% node ./benchmark.js
Initializing handshake benchmark
Init complete, running benchmark
handshake x 124 ops/sec ±0.47% (84 runs sampled)
```

After:

```console
% node ./benchmark.js
Initializing handshake benchmark
Init complete, running benchmark
handshake x 314 ops/sec ±0.99% (87 runs sampled)
```
  • Loading branch information
achingbrain committed Nov 23, 2023
1 parent e59d9a8 commit a020ec9
Show file tree
Hide file tree
Showing 10 changed files with 165 additions and 82 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@
"clean": "aegir clean",
"dep-check": "aegir dep-check",
"build": "aegir build",
"prebuild": "mkdirp dist/src && cp -R src/proto dist/src",
"lint": "aegir lint",
"lint:fix": "aegir lint --fix",
"test": "aegir test",
Expand Down
66 changes: 66 additions & 0 deletions src/crypto/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import crypto from 'node:crypto'
import { newInstance, ChaCha20Poly1305 } from '@chainsafe/as-chacha20poly1305'
import { digest } from '@chainsafe/as-sha256'
import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
import { isElectronMain } from 'wherearewe'
import { pureJsCrypto } from './js.js'
import type { KeyPair } from '../@types/libp2p.js'
import type { ICryptoInterface } from '../crypto.js'

const ctx = newInstance()
const asImpl = new ChaCha20Poly1305(ctx)
const CHACHA_POLY1305 = 'chacha20-poly1305'
const PKCS8_PREFIX = Buffer.from([0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x04, 0x22, 0x04, 0x20])
const X25519_PREFIX = Buffer.from([0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x03, 0x21, 0x00])
const nodeCrypto: Pick<ICryptoInterface, 'hashSHA256' | 'chaCha20Poly1305Encrypt' | 'chaCha20Poly1305Decrypt'> = {
hashSHA256 (data) {
return crypto.createHash('sha256').update(data).digest()
Expand Down Expand Up @@ -76,6 +80,68 @@ export const defaultCrypto: ICryptoInterface = {
return asCrypto.chaCha20Poly1305Decrypt(ciphertext, nonce, ad, k, dst)
}
return nodeCrypto.chaCha20Poly1305Decrypt(ciphertext, nonce, ad, k, dst)
},
generateX25519KeyPair (): KeyPair {
const { publicKey, privateKey } = crypto.generateKeyPairSync('x25519', {
publicKeyEncoding: {
type: 'spki',
format: 'der'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'der'
}
})

return {
publicKey: publicKey.subarray(12),
privateKey: privateKey.subarray(16)
}
},
generateX25519KeyPairFromSeed (seed: Uint8Array): KeyPair {
const privateKey = crypto.createPrivateKey({
key: Buffer.concat([
PKCS8_PREFIX,
seed
], PKCS8_PREFIX.byteLength + seed.byteLength),
type: 'pkcs8',
format: 'der'
})

const publicKey = crypto.createPublicKey(privateKey)
.export({
type: 'spki',
format: 'der'
}).subarray(12)

return {
publicKey,
privateKey: seed
}
},
generateX25519SharedKey (privateKey: Uint8Array, publicKey: Uint8Array): Uint8Array {
publicKey = uint8ArrayConcat([
X25519_PREFIX,
publicKey
], X25519_PREFIX.byteLength + publicKey.byteLength)

privateKey = uint8ArrayConcat([
PKCS8_PREFIX,
privateKey
], PKCS8_PREFIX.byteLength + privateKey.byteLength)

return crypto.diffieHellman({
publicKey: crypto.createPublicKey({
key: Buffer.from(publicKey, publicKey.byteOffset, publicKey.byteLength),
type: 'spki',
format: 'der'
}),
privateKey: crypto.createPrivateKey({
key: Buffer.from(privateKey, privateKey.byteOffset, privateKey.byteLength),
type: 'pkcs8',
format: 'der'
})
})
}
}

Expand Down
15 changes: 4 additions & 11 deletions src/encoder.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
import { alloc as uint8ArrayAlloc, allocUnsafe as uint8ArrayAllocUnsafe } from 'uint8arrays/alloc'
import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
import type { bytes } from './@types/basic.js'
import type { MessageBuffer } from './@types/handshake.js'
import type { LengthDecoderFunction } from 'it-length-prefixed'
import type { Uint8ArrayList } from 'uint8arraylist'

const allocUnsafe = (len: number): Uint8Array => {
if (globalThis.Buffer) {
return globalThis.Buffer.allocUnsafe(len)
}

return new Uint8Array(len)
}

export const uint16BEEncode = (value: number): Uint8Array => {
const target = allocUnsafe(2)
const target = uint8ArrayAllocUnsafe(2)
new DataView(target.buffer, target.byteOffset, target.byteLength).setUint16(0, value, false)
return target
}
Expand Down Expand Up @@ -52,7 +45,7 @@ export function decode0 (input: bytes): MessageBuffer {
return {
ne: input.subarray(0, 32),
ciphertext: input.subarray(32, input.length),
ns: new Uint8Array(0)
ns: uint8ArrayAlloc(0)
}
}

Expand All @@ -74,7 +67,7 @@ export function decode2 (input: bytes): MessageBuffer {
}

return {
ne: new Uint8Array(0),
ne: uint8ArrayAlloc(0),
ns: input.subarray(0, 48),
ciphertext: input.subarray(48, input.length)
}
Expand Down
7 changes: 4 additions & 3 deletions src/handshake-xx.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { InvalidCryptoExchangeError, UnexpectedPeerError } from '@libp2p/interface/errors'
import { alloc as uint8ArrayAlloc } from 'uint8arrays/alloc'
import { decode0, decode1, decode2, encode0, encode1, encode2 } from './encoder.js'
import { XX } from './handshakes/xx.js'
import {
Expand Down Expand Up @@ -63,7 +64,7 @@ export class XXHandshake implements IHandshake {
logLocalStaticKeys(this.session.hs.s)
if (this.isInitiator) {
logger.trace('Stage 0 - Initiator starting to send first message.')
const messageBuffer = this.xx.sendMessage(this.session, new Uint8Array(0))
const messageBuffer = this.xx.sendMessage(this.session, uint8ArrayAlloc(0))
await this.connection.write(encode0(messageBuffer))
logger.trace('Stage 0 - Initiator finished sending first message.')
logLocalEphemeralKeys(this.session.hs.e)
Expand Down Expand Up @@ -144,13 +145,13 @@ export class XXHandshake implements IHandshake {
public encrypt (plaintext: Uint8Array, session: NoiseSession): bytes {
const cs = this.getCS(session)

return this.xx.encryptWithAd(cs, new Uint8Array(0), plaintext)
return this.xx.encryptWithAd(cs, uint8ArrayAlloc(0), plaintext)
}

public decrypt (ciphertext: Uint8Array, session: NoiseSession, dst?: Uint8Array): { plaintext: bytes, valid: boolean } {
const cs = this.getCS(session, false)

return this.xx.decryptWithAd(cs, new Uint8Array(0), ciphertext, dst)
return this.xx.decryptWithAd(cs, uint8ArrayAlloc(0), ciphertext, dst)
}

public getRemoteStaticKey (): bytes {
Expand Down
19 changes: 10 additions & 9 deletions src/handshakes/abstract-handshake.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { fromString as uint8ArrayFromString } from 'uint8arrays'
import { alloc as uint8ArrayAlloc } from 'uint8arrays/alloc'
import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
import { logger } from '../logger.js'
Expand Down Expand Up @@ -44,7 +45,7 @@ export abstract class AbstractHandshake {
}

protected createEmptyKey (): bytes32 {
return new Uint8Array(32)
return uint8ArrayAlloc(32)
}

protected isEmptyKey (k: bytes32): boolean {
Expand Down Expand Up @@ -82,7 +83,7 @@ export abstract class AbstractHandshake {
}
} else {
return {
plaintext: new Uint8Array(0),
plaintext: uint8ArrayAlloc(0),
valid: false
}
}
Expand Down Expand Up @@ -112,7 +113,7 @@ export abstract class AbstractHandshake {
} catch (e) {
const err = e as Error
logger.error(err)
return new Uint8Array(32)
return uint8ArrayAlloc(32)
}
}

Expand Down Expand Up @@ -150,31 +151,31 @@ export abstract class AbstractHandshake {

protected hashProtocolName (protocolName: Uint8Array): bytes32 {
if (protocolName.length <= 32) {
const h = new Uint8Array(32)
const h = uint8ArrayAlloc(32)
h.set(protocolName)
return h
} else {
return this.getHash(protocolName, new Uint8Array(0))
return this.getHash(protocolName, uint8ArrayAlloc(0))
}
}

protected split (ss: SymmetricState): SplitState {
const [tempk1, tempk2] = this.crypto.getHKDF(ss.ck, new Uint8Array(0))
const [tempk1, tempk2] = this.crypto.getHKDF(ss.ck, uint8ArrayAlloc(0))
const cs1 = this.initializeKey(tempk1)
const cs2 = this.initializeKey(tempk2)

return { cs1, cs2 }
}

protected writeMessageRegular (cs: CipherState, payload: bytes): MessageBuffer {
const ciphertext = this.encryptWithAd(cs, new Uint8Array(0), payload)
const ciphertext = this.encryptWithAd(cs, uint8ArrayAlloc(0), payload)
const ne = this.createEmptyKey()
const ns = new Uint8Array(0)
const ns = uint8ArrayAlloc(0)

return { ne, ns, ciphertext }
}

protected readMessageRegular (cs: CipherState, message: MessageBuffer): DecryptedResult {
return this.decryptWithAd(cs, new Uint8Array(0), message.ciphertext)
return this.decryptWithAd(cs, uint8ArrayAlloc(0), message.ciphertext)
}
}
11 changes: 6 additions & 5 deletions src/handshakes/xx.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { alloc as uint8ArrayAlloc } from 'uint8arrays/alloc'
import { isValidPublicKey } from '../utils.js'
import { AbstractHandshake, type DecryptedResult } from './abstract-handshake.js'
import type { bytes32, bytes } from '../@types/basic.js'
Expand All @@ -9,7 +10,7 @@ export class XX extends AbstractHandshake {
const name = 'Noise_XX_25519_ChaChaPoly_SHA256'
const ss = this.initializeSymmetric(name)
this.mixHash(ss, prologue)
const re = new Uint8Array(32)
const re = uint8ArrayAlloc(32)

return { ss, s, rs, psk, re }
}
Expand All @@ -18,13 +19,13 @@ export class XX extends AbstractHandshake {
const name = 'Noise_XX_25519_ChaChaPoly_SHA256'
const ss = this.initializeSymmetric(name)
this.mixHash(ss, prologue)
const re = new Uint8Array(32)
const re = uint8ArrayAlloc(32)

return { ss, s, rs, psk, re }
}

private writeMessageA (hs: HandshakeState, payload: bytes, e?: KeyPair): MessageBuffer {
const ns = new Uint8Array(0)
const ns = uint8ArrayAlloc(0)

if (e !== undefined) {
hs.e = e
Expand Down Expand Up @@ -113,7 +114,7 @@ export class XX extends AbstractHandshake {

public initSession (initiator: boolean, prologue: bytes32, s: KeyPair): NoiseSession {
const psk = this.createEmptyKey()
const rs = new Uint8Array(32) // no static key yet
const rs = uint8ArrayAlloc(32) // no static key yet
let hs

if (initiator) {
Expand Down Expand Up @@ -164,7 +165,7 @@ export class XX extends AbstractHandshake {
}

public recvMessage (session: NoiseSession, message: MessageBuffer): DecryptedResult {
let plaintext: bytes = new Uint8Array(0)
let plaintext: bytes = uint8ArrayAlloc(0)
let valid = false
if (session.mc === 0) {
({ plaintext, valid } = this.readMessageA(session.hs, message))
Expand Down
3 changes: 2 additions & 1 deletion src/noise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { decode } from 'it-length-prefixed'
import { lpStream, type LengthPrefixedStream } from 'it-length-prefixed-stream'
import { duplexPair } from 'it-pair/duplex'
import { pipe } from 'it-pipe'
import { alloc as uint8ArrayAlloc } from 'uint8arrays/alloc'
import { NOISE_MSG_MAX_LENGTH_BYTES } from './constants.js'
import { defaultCrypto } from './crypto/index.js'
import { decryptStream, encryptStream } from './crypto/streaming.js'
Expand Down Expand Up @@ -59,7 +60,7 @@ export class Noise implements INoiseConnection {
} else {
this.staticKeys = this.crypto.generateX25519KeyPair()
}
this.prologue = prologueBytes ?? new Uint8Array(0)
this.prologue = prologueBytes ?? uint8ArrayAlloc(0)
}

/**
Expand Down
3 changes: 2 additions & 1 deletion src/nonce.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { alloc as uint8ArrayAlloc } from 'uint8arrays/alloc'
import type { bytes, uint64 } from './@types/basic.js'

export const MIN_NONCE = 0
Expand All @@ -22,7 +23,7 @@ export class Nonce {

constructor (n = MIN_NONCE) {
this.n = n
this.bytes = new Uint8Array(12)
this.bytes = uint8ArrayAlloc(12)
this.view = new DataView(this.bytes.buffer, this.bytes.byteOffset, this.bytes.byteLength)
this.view.setUint32(4, n, true)
}
Expand Down
Loading

0 comments on commit a020ec9

Please sign in to comment.