Skip to content

Commit 8ba08ea

Browse files
authored
fix: use node.js crypto for x25519 keys (#389)
* fix: use node.js crypto for x25519 keys 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) ``` * chore: prefer Buffer in node
1 parent e59d9a8 commit 8ba08ea

File tree

10 files changed

+164
-82
lines changed

10 files changed

+164
-82
lines changed

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@
5555
"clean": "aegir clean",
5656
"dep-check": "aegir dep-check",
5757
"build": "aegir build",
58-
"prebuild": "mkdirp dist/src && cp -R src/proto dist/src",
5958
"lint": "aegir lint",
6059
"lint:fix": "aegir lint --fix",
6160
"test": "aegir test",

src/crypto/index.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ import { newInstance, ChaCha20Poly1305 } from '@chainsafe/as-chacha20poly1305'
33
import { digest } from '@chainsafe/as-sha256'
44
import { isElectronMain } from 'wherearewe'
55
import { pureJsCrypto } from './js.js'
6+
import type { KeyPair } from '../@types/libp2p.js'
67
import type { ICryptoInterface } from '../crypto.js'
78

89
const ctx = newInstance()
910
const asImpl = new ChaCha20Poly1305(ctx)
1011
const CHACHA_POLY1305 = 'chacha20-poly1305'
12+
const PKCS8_PREFIX = Buffer.from([0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x04, 0x22, 0x04, 0x20])
13+
const X25519_PREFIX = Buffer.from([0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x03, 0x21, 0x00])
1114
const nodeCrypto: Pick<ICryptoInterface, 'hashSHA256' | 'chaCha20Poly1305Encrypt' | 'chaCha20Poly1305Decrypt'> = {
1215
hashSHA256 (data) {
1316
return crypto.createHash('sha256').update(data).digest()
@@ -76,6 +79,68 @@ export const defaultCrypto: ICryptoInterface = {
7679
return asCrypto.chaCha20Poly1305Decrypt(ciphertext, nonce, ad, k, dst)
7780
}
7881
return nodeCrypto.chaCha20Poly1305Decrypt(ciphertext, nonce, ad, k, dst)
82+
},
83+
generateX25519KeyPair (): KeyPair {
84+
const { publicKey, privateKey } = crypto.generateKeyPairSync('x25519', {
85+
publicKeyEncoding: {
86+
type: 'spki',
87+
format: 'der'
88+
},
89+
privateKeyEncoding: {
90+
type: 'pkcs8',
91+
format: 'der'
92+
}
93+
})
94+
95+
return {
96+
publicKey: publicKey.subarray(X25519_PREFIX.length),
97+
privateKey: privateKey.subarray(PKCS8_PREFIX.length)
98+
}
99+
},
100+
generateX25519KeyPairFromSeed (seed: Uint8Array): KeyPair {
101+
const privateKey = crypto.createPrivateKey({
102+
key: Buffer.concat([
103+
PKCS8_PREFIX,
104+
seed
105+
], PKCS8_PREFIX.byteLength + seed.byteLength),
106+
type: 'pkcs8',
107+
format: 'der'
108+
})
109+
110+
const publicKey = crypto.createPublicKey(privateKey)
111+
.export({
112+
type: 'spki',
113+
format: 'der'
114+
}).subarray(X25519_PREFIX.length)
115+
116+
return {
117+
publicKey,
118+
privateKey: seed
119+
}
120+
},
121+
generateX25519SharedKey (privateKey: Uint8Array, publicKey: Uint8Array): Uint8Array {
122+
publicKey = Buffer.concat([
123+
X25519_PREFIX,
124+
publicKey
125+
], X25519_PREFIX.byteLength + publicKey.byteLength)
126+
127+
privateKey = Buffer.concat([
128+
PKCS8_PREFIX,
129+
privateKey
130+
], PKCS8_PREFIX.byteLength + privateKey.byteLength)
131+
132+
return crypto.diffieHellman({
133+
publicKey: crypto.createPublicKey({
134+
key: Buffer.from(publicKey, publicKey.byteOffset, publicKey.byteLength),
135+
type: 'spki',
136+
format: 'der'
137+
}),
138+
privateKey: crypto.createPrivateKey({
139+
key: Buffer.from(privateKey, privateKey.byteOffset, privateKey.byteLength),
140+
type: 'pkcs8',
141+
format: 'der'
142+
})
143+
})
79144
}
80145
}
81146

src/encoder.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,12 @@
1+
import { alloc as uint8ArrayAlloc, allocUnsafe as uint8ArrayAllocUnsafe } from 'uint8arrays/alloc'
12
import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
23
import type { bytes } from './@types/basic.js'
34
import type { MessageBuffer } from './@types/handshake.js'
45
import type { LengthDecoderFunction } from 'it-length-prefixed'
56
import type { Uint8ArrayList } from 'uint8arraylist'
67

7-
const allocUnsafe = (len: number): Uint8Array => {
8-
if (globalThis.Buffer) {
9-
return globalThis.Buffer.allocUnsafe(len)
10-
}
11-
12-
return new Uint8Array(len)
13-
}
14-
158
export const uint16BEEncode = (value: number): Uint8Array => {
16-
const target = allocUnsafe(2)
9+
const target = uint8ArrayAllocUnsafe(2)
1710
new DataView(target.buffer, target.byteOffset, target.byteLength).setUint16(0, value, false)
1811
return target
1912
}
@@ -52,7 +45,7 @@ export function decode0 (input: bytes): MessageBuffer {
5245
return {
5346
ne: input.subarray(0, 32),
5447
ciphertext: input.subarray(32, input.length),
55-
ns: new Uint8Array(0)
48+
ns: uint8ArrayAlloc(0)
5649
}
5750
}
5851

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

7669
return {
77-
ne: new Uint8Array(0),
70+
ne: uint8ArrayAlloc(0),
7871
ns: input.subarray(0, 48),
7972
ciphertext: input.subarray(48, input.length)
8073
}

src/handshake-xx.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { InvalidCryptoExchangeError, UnexpectedPeerError } from '@libp2p/interface/errors'
2+
import { alloc as uint8ArrayAlloc } from 'uint8arrays/alloc'
23
import { decode0, decode1, decode2, encode0, encode1, encode2 } from './encoder.js'
34
import { XX } from './handshakes/xx.js'
45
import {
@@ -63,7 +64,7 @@ export class XXHandshake implements IHandshake {
6364
logLocalStaticKeys(this.session.hs.s)
6465
if (this.isInitiator) {
6566
logger.trace('Stage 0 - Initiator starting to send first message.')
66-
const messageBuffer = this.xx.sendMessage(this.session, new Uint8Array(0))
67+
const messageBuffer = this.xx.sendMessage(this.session, uint8ArrayAlloc(0))
6768
await this.connection.write(encode0(messageBuffer))
6869
logger.trace('Stage 0 - Initiator finished sending first message.')
6970
logLocalEphemeralKeys(this.session.hs.e)
@@ -144,13 +145,13 @@ export class XXHandshake implements IHandshake {
144145
public encrypt (plaintext: Uint8Array, session: NoiseSession): bytes {
145146
const cs = this.getCS(session)
146147

147-
return this.xx.encryptWithAd(cs, new Uint8Array(0), plaintext)
148+
return this.xx.encryptWithAd(cs, uint8ArrayAlloc(0), plaintext)
148149
}
149150

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

153-
return this.xx.decryptWithAd(cs, new Uint8Array(0), ciphertext, dst)
154+
return this.xx.decryptWithAd(cs, uint8ArrayAlloc(0), ciphertext, dst)
154155
}
155156

156157
public getRemoteStaticKey (): bytes {

src/handshakes/abstract-handshake.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { fromString as uint8ArrayFromString } from 'uint8arrays'
2+
import { alloc as uint8ArrayAlloc } from 'uint8arrays/alloc'
23
import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
34
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
45
import { logger } from '../logger.js'
@@ -44,7 +45,7 @@ export abstract class AbstractHandshake {
4445
}
4546

4647
protected createEmptyKey (): bytes32 {
47-
return new Uint8Array(32)
48+
return uint8ArrayAlloc(32)
4849
}
4950

5051
protected isEmptyKey (k: bytes32): boolean {
@@ -82,7 +83,7 @@ export abstract class AbstractHandshake {
8283
}
8384
} else {
8485
return {
85-
plaintext: new Uint8Array(0),
86+
plaintext: uint8ArrayAlloc(0),
8687
valid: false
8788
}
8889
}
@@ -112,7 +113,7 @@ export abstract class AbstractHandshake {
112113
} catch (e) {
113114
const err = e as Error
114115
logger.error(err)
115-
return new Uint8Array(32)
116+
return uint8ArrayAlloc(32)
116117
}
117118
}
118119

@@ -150,31 +151,31 @@ export abstract class AbstractHandshake {
150151

151152
protected hashProtocolName (protocolName: Uint8Array): bytes32 {
152153
if (protocolName.length <= 32) {
153-
const h = new Uint8Array(32)
154+
const h = uint8ArrayAlloc(32)
154155
h.set(protocolName)
155156
return h
156157
} else {
157-
return this.getHash(protocolName, new Uint8Array(0))
158+
return this.getHash(protocolName, uint8ArrayAlloc(0))
158159
}
159160
}
160161

161162
protected split (ss: SymmetricState): SplitState {
162-
const [tempk1, tempk2] = this.crypto.getHKDF(ss.ck, new Uint8Array(0))
163+
const [tempk1, tempk2] = this.crypto.getHKDF(ss.ck, uint8ArrayAlloc(0))
163164
const cs1 = this.initializeKey(tempk1)
164165
const cs2 = this.initializeKey(tempk2)
165166

166167
return { cs1, cs2 }
167168
}
168169

169170
protected writeMessageRegular (cs: CipherState, payload: bytes): MessageBuffer {
170-
const ciphertext = this.encryptWithAd(cs, new Uint8Array(0), payload)
171+
const ciphertext = this.encryptWithAd(cs, uint8ArrayAlloc(0), payload)
171172
const ne = this.createEmptyKey()
172-
const ns = new Uint8Array(0)
173+
const ns = uint8ArrayAlloc(0)
173174

174175
return { ne, ns, ciphertext }
175176
}
176177

177178
protected readMessageRegular (cs: CipherState, message: MessageBuffer): DecryptedResult {
178-
return this.decryptWithAd(cs, new Uint8Array(0), message.ciphertext)
179+
return this.decryptWithAd(cs, uint8ArrayAlloc(0), message.ciphertext)
179180
}
180181
}

src/handshakes/xx.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { alloc as uint8ArrayAlloc } from 'uint8arrays/alloc'
12
import { isValidPublicKey } from '../utils.js'
23
import { AbstractHandshake, type DecryptedResult } from './abstract-handshake.js'
34
import type { bytes32, bytes } from '../@types/basic.js'
@@ -9,7 +10,7 @@ export class XX extends AbstractHandshake {
910
const name = 'Noise_XX_25519_ChaChaPoly_SHA256'
1011
const ss = this.initializeSymmetric(name)
1112
this.mixHash(ss, prologue)
12-
const re = new Uint8Array(32)
13+
const re = uint8ArrayAlloc(32)
1314

1415
return { ss, s, rs, psk, re }
1516
}
@@ -18,13 +19,13 @@ export class XX extends AbstractHandshake {
1819
const name = 'Noise_XX_25519_ChaChaPoly_SHA256'
1920
const ss = this.initializeSymmetric(name)
2021
this.mixHash(ss, prologue)
21-
const re = new Uint8Array(32)
22+
const re = uint8ArrayAlloc(32)
2223

2324
return { ss, s, rs, psk, re }
2425
}
2526

2627
private writeMessageA (hs: HandshakeState, payload: bytes, e?: KeyPair): MessageBuffer {
27-
const ns = new Uint8Array(0)
28+
const ns = uint8ArrayAlloc(0)
2829

2930
if (e !== undefined) {
3031
hs.e = e
@@ -113,7 +114,7 @@ export class XX extends AbstractHandshake {
113114

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

119120
if (initiator) {
@@ -164,7 +165,7 @@ export class XX extends AbstractHandshake {
164165
}
165166

166167
public recvMessage (session: NoiseSession, message: MessageBuffer): DecryptedResult {
167-
let plaintext: bytes = new Uint8Array(0)
168+
let plaintext: bytes = uint8ArrayAlloc(0)
168169
let valid = false
169170
if (session.mc === 0) {
170171
({ plaintext, valid } = this.readMessageA(session.hs, message))

src/noise.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { decode } from 'it-length-prefixed'
22
import { lpStream, type LengthPrefixedStream } from 'it-length-prefixed-stream'
33
import { duplexPair } from 'it-pair/duplex'
44
import { pipe } from 'it-pipe'
5+
import { alloc as uint8ArrayAlloc } from 'uint8arrays/alloc'
56
import { NOISE_MSG_MAX_LENGTH_BYTES } from './constants.js'
67
import { defaultCrypto } from './crypto/index.js'
78
import { decryptStream, encryptStream } from './crypto/streaming.js'
@@ -59,7 +60,7 @@ export class Noise implements INoiseConnection {
5960
} else {
6061
this.staticKeys = this.crypto.generateX25519KeyPair()
6162
}
62-
this.prologue = prologueBytes ?? new Uint8Array(0)
63+
this.prologue = prologueBytes ?? uint8ArrayAlloc(0)
6364
}
6465

6566
/**

src/nonce.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { alloc as uint8ArrayAlloc } from 'uint8arrays/alloc'
12
import type { bytes, uint64 } from './@types/basic.js'
23

34
export const MIN_NONCE = 0
@@ -22,7 +23,7 @@ export class Nonce {
2223

2324
constructor (n = MIN_NONCE) {
2425
this.n = n
25-
this.bytes = new Uint8Array(12)
26+
this.bytes = uint8ArrayAlloc(12)
2627
this.view = new DataView(this.bytes.buffer, this.bytes.byteOffset, this.bytes.byteLength)
2728
this.view.setUint32(4, n, true)
2829
}

0 commit comments

Comments
 (0)