@libp2p/noise 12.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.min.js +27 -0
- package/dist/src/@types/basic.d.ts +5 -0
- package/dist/src/@types/basic.d.ts.map +1 -0
- package/dist/src/@types/basic.js +2 -0
- package/dist/src/@types/basic.js.map +1 -0
- package/dist/src/@types/handshake-interface.d.ts +15 -0
- package/dist/src/@types/handshake-interface.d.ts.map +1 -0
- package/dist/src/@types/handshake-interface.js +2 -0
- package/dist/src/@types/handshake-interface.js.map +1 -0
- package/dist/src/@types/handshake.d.ts +40 -0
- package/dist/src/@types/handshake.d.ts.map +1 -0
- package/dist/src/@types/handshake.js +2 -0
- package/dist/src/@types/handshake.js.map +1 -0
- package/dist/src/@types/libp2p.d.ts +10 -0
- package/dist/src/@types/libp2p.d.ts.map +1 -0
- package/dist/src/@types/libp2p.js +2 -0
- package/dist/src/@types/libp2p.js.map +1 -0
- package/dist/src/constants.d.ts +4 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +4 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/crypto/js.d.ts +3 -0
- package/dist/src/crypto/js.d.ts.map +1 -0
- package/dist/src/crypto/js.js +42 -0
- package/dist/src/crypto/js.js.map +1 -0
- package/dist/src/crypto/streaming.d.ts +7 -0
- package/dist/src/crypto/streaming.d.ts.map +1 -0
- package/dist/src/crypto/streaming.js +50 -0
- package/dist/src/crypto/streaming.js.map +1 -0
- package/dist/src/crypto.d.ts +13 -0
- package/dist/src/crypto.d.ts.map +1 -0
- package/dist/src/crypto.js +2 -0
- package/dist/src/crypto.js.map +1 -0
- package/dist/src/encoder.d.ts +15 -0
- package/dist/src/encoder.d.ts.map +1 -0
- package/dist/src/encoder.js +63 -0
- package/dist/src/encoder.js.map +1 -0
- package/dist/src/handshake-xx.d.ts +33 -0
- package/dist/src/handshake-xx.d.ts.map +1 -0
- package/dist/src/handshake-xx.js +139 -0
- package/dist/src/handshake-xx.js.map +1 -0
- package/dist/src/handshakes/abstract-handshake.d.ts +36 -0
- package/dist/src/handshakes/abstract-handshake.d.ts.map +1 -0
- package/dist/src/handshakes/abstract-handshake.js +140 -0
- package/dist/src/handshakes/abstract-handshake.js.map +1 -0
- package/dist/src/handshakes/xx.d.ts +18 -0
- package/dist/src/handshakes/xx.d.ts.map +1 -0
- package/dist/src/handshakes/xx.js +161 -0
- package/dist/src/handshakes/xx.js.map +1 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +6 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/logger.d.ts +11 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +45 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/metrics.d.ts +4 -0
- package/dist/src/metrics.d.ts.map +1 -0
- package/dist/src/metrics.js +20 -0
- package/dist/src/metrics.js.map +1 -0
- package/dist/src/noise.d.ts +55 -0
- package/dist/src/noise.d.ts.map +1 -0
- package/dist/src/noise.js +130 -0
- package/dist/src/noise.js.map +1 -0
- package/dist/src/nonce.d.ts +18 -0
- package/dist/src/nonce.d.ts.map +1 -0
- package/dist/src/nonce.js +41 -0
- package/dist/src/nonce.js.map +1 -0
- package/dist/src/proto/payload.d.ts +21 -0
- package/dist/src/proto/payload.d.ts.map +1 -0
- package/dist/src/proto/payload.js +114 -0
- package/dist/src/proto/payload.js.map +1 -0
- package/dist/src/utils.d.ts +20 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/utils.js +74 -0
- package/dist/src/utils.js.map +1 -0
- package/package.json +99 -0
- package/src/@types/basic.ts +5 -0
- package/src/@types/handshake-interface.ts +12 -0
- package/src/@types/handshake.ts +48 -0
- package/src/@types/libp2p.ts +10 -0
- package/src/constants.ts +4 -0
- package/src/crypto/js.ts +58 -0
- package/src/crypto/streaming.ts +58 -0
- package/src/crypto.ts +16 -0
- package/src/encoder.ts +81 -0
- package/src/handshake-xx.ts +177 -0
- package/src/handshakes/abstract-handshake.ts +180 -0
- package/src/handshakes/xx.ts +184 -0
- package/src/index.ts +10 -0
- package/src/logger.ts +51 -0
- package/src/metrics.ts +32 -0
- package/src/noise.ts +192 -0
- package/src/nonce.ts +49 -0
- package/src/proto/payload.proto +11 -0
- package/src/proto/payload.ts +148 -0
- package/src/utils.ts +111 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { isValidPublicKey } from '../utils.js'
|
|
2
|
+
import { AbstractHandshake, type DecryptedResult } from './abstract-handshake.js'
|
|
3
|
+
import type { bytes32, bytes } from '../@types/basic.js'
|
|
4
|
+
import type { CipherState, HandshakeState, MessageBuffer, NoiseSession } from '../@types/handshake.js'
|
|
5
|
+
import type { KeyPair } from '../@types/libp2p.js'
|
|
6
|
+
|
|
7
|
+
export class XX extends AbstractHandshake {
|
|
8
|
+
private initializeInitiator (prologue: bytes32, s: KeyPair, rs: bytes32, psk: bytes32): HandshakeState {
|
|
9
|
+
const name = 'Noise_XX_25519_ChaChaPoly_SHA256'
|
|
10
|
+
const ss = this.initializeSymmetric(name)
|
|
11
|
+
this.mixHash(ss, prologue)
|
|
12
|
+
const re = new Uint8Array(32)
|
|
13
|
+
|
|
14
|
+
return { ss, s, rs, psk, re }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private initializeResponder (prologue: bytes32, s: KeyPair, rs: bytes32, psk: bytes32): HandshakeState {
|
|
18
|
+
const name = 'Noise_XX_25519_ChaChaPoly_SHA256'
|
|
19
|
+
const ss = this.initializeSymmetric(name)
|
|
20
|
+
this.mixHash(ss, prologue)
|
|
21
|
+
const re = new Uint8Array(32)
|
|
22
|
+
|
|
23
|
+
return { ss, s, rs, psk, re }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private writeMessageA (hs: HandshakeState, payload: bytes, e?: KeyPair): MessageBuffer {
|
|
27
|
+
const ns = new Uint8Array(0)
|
|
28
|
+
|
|
29
|
+
if (e !== undefined) {
|
|
30
|
+
hs.e = e
|
|
31
|
+
} else {
|
|
32
|
+
hs.e = this.crypto.generateX25519KeyPair()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ne = hs.e.publicKey
|
|
36
|
+
|
|
37
|
+
this.mixHash(hs.ss, ne)
|
|
38
|
+
const ciphertext = this.encryptAndHash(hs.ss, payload)
|
|
39
|
+
|
|
40
|
+
return { ne, ns, ciphertext }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private writeMessageB (hs: HandshakeState, payload: bytes): MessageBuffer {
|
|
44
|
+
hs.e = this.crypto.generateX25519KeyPair()
|
|
45
|
+
const ne = hs.e.publicKey
|
|
46
|
+
this.mixHash(hs.ss, ne)
|
|
47
|
+
|
|
48
|
+
this.mixKey(hs.ss, this.dh(hs.e.privateKey, hs.re))
|
|
49
|
+
const spk = hs.s.publicKey
|
|
50
|
+
const ns = this.encryptAndHash(hs.ss, spk)
|
|
51
|
+
|
|
52
|
+
this.mixKey(hs.ss, this.dh(hs.s.privateKey, hs.re))
|
|
53
|
+
const ciphertext = this.encryptAndHash(hs.ss, payload)
|
|
54
|
+
|
|
55
|
+
return { ne, ns, ciphertext }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private writeMessageC (hs: HandshakeState, payload: bytes): { messageBuffer: MessageBuffer, cs1: CipherState, cs2: CipherState, h: bytes } {
|
|
59
|
+
const spk = hs.s.publicKey
|
|
60
|
+
const ns = this.encryptAndHash(hs.ss, spk)
|
|
61
|
+
this.mixKey(hs.ss, this.dh(hs.s.privateKey, hs.re))
|
|
62
|
+
const ciphertext = this.encryptAndHash(hs.ss, payload)
|
|
63
|
+
const ne = this.createEmptyKey()
|
|
64
|
+
const messageBuffer: MessageBuffer = { ne, ns, ciphertext }
|
|
65
|
+
const { cs1, cs2 } = this.split(hs.ss)
|
|
66
|
+
|
|
67
|
+
return { h: hs.ss.h, messageBuffer, cs1, cs2 }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private readMessageA (hs: HandshakeState, message: MessageBuffer): DecryptedResult {
|
|
71
|
+
if (isValidPublicKey(message.ne)) {
|
|
72
|
+
hs.re = message.ne
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.mixHash(hs.ss, hs.re)
|
|
76
|
+
return this.decryptAndHash(hs.ss, message.ciphertext)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private readMessageB (hs: HandshakeState, message: MessageBuffer): DecryptedResult {
|
|
80
|
+
if (isValidPublicKey(message.ne)) {
|
|
81
|
+
hs.re = message.ne
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.mixHash(hs.ss, hs.re)
|
|
85
|
+
if (!hs.e) {
|
|
86
|
+
throw new Error('Handshake state `e` param is missing.')
|
|
87
|
+
}
|
|
88
|
+
this.mixKey(hs.ss, this.dh(hs.e.privateKey, hs.re))
|
|
89
|
+
const { plaintext: ns, valid: valid1 } = this.decryptAndHash(hs.ss, message.ns)
|
|
90
|
+
if (valid1 && isValidPublicKey(ns)) {
|
|
91
|
+
hs.rs = ns
|
|
92
|
+
}
|
|
93
|
+
this.mixKey(hs.ss, this.dh(hs.e.privateKey, hs.rs))
|
|
94
|
+
const { plaintext, valid: valid2 } = this.decryptAndHash(hs.ss, message.ciphertext)
|
|
95
|
+
return { plaintext, valid: (valid1 && valid2) }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private readMessageC (hs: HandshakeState, message: MessageBuffer): { h: bytes, plaintext: bytes, valid: boolean, cs1: CipherState, cs2: CipherState } {
|
|
99
|
+
const { plaintext: ns, valid: valid1 } = this.decryptAndHash(hs.ss, message.ns)
|
|
100
|
+
if (valid1 && isValidPublicKey(ns)) {
|
|
101
|
+
hs.rs = ns
|
|
102
|
+
}
|
|
103
|
+
if (!hs.e) {
|
|
104
|
+
throw new Error('Handshake state `e` param is missing.')
|
|
105
|
+
}
|
|
106
|
+
this.mixKey(hs.ss, this.dh(hs.e.privateKey, hs.rs))
|
|
107
|
+
|
|
108
|
+
const { plaintext, valid: valid2 } = this.decryptAndHash(hs.ss, message.ciphertext)
|
|
109
|
+
const { cs1, cs2 } = this.split(hs.ss)
|
|
110
|
+
|
|
111
|
+
return { h: hs.ss.h, plaintext, valid: (valid1 && valid2), cs1, cs2 }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public initSession (initiator: boolean, prologue: bytes32, s: KeyPair): NoiseSession {
|
|
115
|
+
const psk = this.createEmptyKey()
|
|
116
|
+
const rs = new Uint8Array(32) // no static key yet
|
|
117
|
+
let hs
|
|
118
|
+
|
|
119
|
+
if (initiator) {
|
|
120
|
+
hs = this.initializeInitiator(prologue, s, rs, psk)
|
|
121
|
+
} else {
|
|
122
|
+
hs = this.initializeResponder(prologue, s, rs, psk)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
hs,
|
|
127
|
+
i: initiator,
|
|
128
|
+
mc: 0
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public sendMessage (session: NoiseSession, message: bytes, ephemeral?: KeyPair): MessageBuffer {
|
|
133
|
+
let messageBuffer: MessageBuffer
|
|
134
|
+
if (session.mc === 0) {
|
|
135
|
+
messageBuffer = this.writeMessageA(session.hs, message, ephemeral)
|
|
136
|
+
} else if (session.mc === 1) {
|
|
137
|
+
messageBuffer = this.writeMessageB(session.hs, message)
|
|
138
|
+
} else if (session.mc === 2) {
|
|
139
|
+
const { h, messageBuffer: resultingBuffer, cs1, cs2 } = this.writeMessageC(session.hs, message)
|
|
140
|
+
messageBuffer = resultingBuffer
|
|
141
|
+
session.h = h
|
|
142
|
+
session.cs1 = cs1
|
|
143
|
+
session.cs2 = cs2
|
|
144
|
+
} else if (session.mc > 2) {
|
|
145
|
+
if (session.i) {
|
|
146
|
+
if (!session.cs1) {
|
|
147
|
+
throw new Error('CS1 (cipher state) is not defined')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
messageBuffer = this.writeMessageRegular(session.cs1, message)
|
|
151
|
+
} else {
|
|
152
|
+
if (!session.cs2) {
|
|
153
|
+
throw new Error('CS2 (cipher state) is not defined')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
messageBuffer = this.writeMessageRegular(session.cs2, message)
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
throw new Error('Session invalid.')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
session.mc++
|
|
163
|
+
return messageBuffer
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
public recvMessage (session: NoiseSession, message: MessageBuffer): DecryptedResult {
|
|
167
|
+
let plaintext: bytes = new Uint8Array(0)
|
|
168
|
+
let valid = false
|
|
169
|
+
if (session.mc === 0) {
|
|
170
|
+
({ plaintext, valid } = this.readMessageA(session.hs, message))
|
|
171
|
+
} else if (session.mc === 1) {
|
|
172
|
+
({ plaintext, valid } = this.readMessageB(session.hs, message))
|
|
173
|
+
} else if (session.mc === 2) {
|
|
174
|
+
const { h, plaintext: resultingPlaintext, valid: resultingValid, cs1, cs2 } = this.readMessageC(session.hs, message)
|
|
175
|
+
plaintext = resultingPlaintext
|
|
176
|
+
valid = resultingValid
|
|
177
|
+
session.h = h
|
|
178
|
+
session.cs1 = cs1
|
|
179
|
+
session.cs2 = cs2
|
|
180
|
+
}
|
|
181
|
+
session.mc++
|
|
182
|
+
return { plaintext, valid }
|
|
183
|
+
}
|
|
184
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Noise } from './noise.js'
|
|
2
|
+
import type { NoiseInit } from './noise.js'
|
|
3
|
+
import type { NoiseExtensions } from './proto/payload.js'
|
|
4
|
+
import type { ConnectionEncrypter } from '@libp2p/interface/connection-encrypter'
|
|
5
|
+
export type { ICryptoInterface } from './crypto.js'
|
|
6
|
+
export { pureJsCrypto } from './crypto/js.js'
|
|
7
|
+
|
|
8
|
+
export function noise (init: NoiseInit = {}): () => ConnectionEncrypter<NoiseExtensions> {
|
|
9
|
+
return () => new Noise(init)
|
|
10
|
+
}
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type Logger, logger } from '@libp2p/logger'
|
|
2
|
+
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
|
|
3
|
+
import { DUMP_SESSION_KEYS } from './constants.js'
|
|
4
|
+
import type { NoiseSession } from './@types/handshake.js'
|
|
5
|
+
import type { KeyPair } from './@types/libp2p.js'
|
|
6
|
+
|
|
7
|
+
const log = logger('libp2p:noise')
|
|
8
|
+
|
|
9
|
+
export { log as logger }
|
|
10
|
+
|
|
11
|
+
let keyLogger: Logger
|
|
12
|
+
if (DUMP_SESSION_KEYS) {
|
|
13
|
+
keyLogger = log
|
|
14
|
+
} else {
|
|
15
|
+
keyLogger = Object.assign(() => { /* do nothing */ }, {
|
|
16
|
+
enabled: false,
|
|
17
|
+
trace: () => {},
|
|
18
|
+
error: () => {}
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function logLocalStaticKeys (s: KeyPair): void {
|
|
23
|
+
keyLogger(`LOCAL_STATIC_PUBLIC_KEY ${uint8ArrayToString(s.publicKey, 'hex')}`)
|
|
24
|
+
keyLogger(`LOCAL_STATIC_PRIVATE_KEY ${uint8ArrayToString(s.privateKey, 'hex')}`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function logLocalEphemeralKeys (e: KeyPair | undefined): void {
|
|
28
|
+
if (e) {
|
|
29
|
+
keyLogger(`LOCAL_PUBLIC_EPHEMERAL_KEY ${uint8ArrayToString(e.publicKey, 'hex')}`)
|
|
30
|
+
keyLogger(`LOCAL_PRIVATE_EPHEMERAL_KEY ${uint8ArrayToString(e.privateKey, 'hex')}`)
|
|
31
|
+
} else {
|
|
32
|
+
keyLogger('Missing local ephemeral keys.')
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function logRemoteStaticKey (rs: Uint8Array): void {
|
|
37
|
+
keyLogger(`REMOTE_STATIC_PUBLIC_KEY ${uint8ArrayToString(rs, 'hex')}`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function logRemoteEphemeralKey (re: Uint8Array): void {
|
|
41
|
+
keyLogger(`REMOTE_EPHEMERAL_PUBLIC_KEY ${uint8ArrayToString(re, 'hex')}`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function logCipherState (session: NoiseSession): void {
|
|
45
|
+
if (session.cs1 && session.cs2) {
|
|
46
|
+
keyLogger(`CIPHER_STATE_1 ${session.cs1.n.getUint64()} ${uint8ArrayToString(session.cs1.k, 'hex')}`)
|
|
47
|
+
keyLogger(`CIPHER_STATE_2 ${session.cs2.n.getUint64()} ${uint8ArrayToString(session.cs2.k, 'hex')}`)
|
|
48
|
+
} else {
|
|
49
|
+
keyLogger('Missing cipher state.')
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/metrics.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Counter, Metrics } from '@libp2p/interface/metrics'
|
|
2
|
+
|
|
3
|
+
export type MetricsRegistry = Record<string, Counter>
|
|
4
|
+
|
|
5
|
+
export function registerMetrics (metrics: Metrics): MetricsRegistry {
|
|
6
|
+
return {
|
|
7
|
+
xxHandshakeSuccesses: metrics.registerCounter(
|
|
8
|
+
'libp2p_noise_xxhandshake_successes_total', {
|
|
9
|
+
help: 'Total count of noise xxHandshakes successes_'
|
|
10
|
+
}),
|
|
11
|
+
|
|
12
|
+
xxHandshakeErrors: metrics.registerCounter(
|
|
13
|
+
'libp2p_noise_xxhandshake_error_total', {
|
|
14
|
+
help: 'Total count of noise xxHandshakes errors'
|
|
15
|
+
}),
|
|
16
|
+
|
|
17
|
+
encryptedPackets: metrics.registerCounter(
|
|
18
|
+
'libp2p_noise_encrypted_packets_total', {
|
|
19
|
+
help: 'Total count of noise encrypted packets successfully'
|
|
20
|
+
}),
|
|
21
|
+
|
|
22
|
+
decryptedPackets: metrics.registerCounter(
|
|
23
|
+
'libp2p_noise_decrypted_packets_total', {
|
|
24
|
+
help: 'Total count of noise decrypted packets'
|
|
25
|
+
}),
|
|
26
|
+
|
|
27
|
+
decryptErrors: metrics.registerCounter(
|
|
28
|
+
'libp2p_noise_decrypt_errors_total', {
|
|
29
|
+
help: 'Total count of noise decrypt errors'
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/noise.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { decode } from 'it-length-prefixed'
|
|
2
|
+
import { lpStream, type LengthPrefixedStream } from 'it-length-prefixed-stream'
|
|
3
|
+
import { duplexPair } from 'it-pair/duplex'
|
|
4
|
+
import { pipe } from 'it-pipe'
|
|
5
|
+
import { NOISE_MSG_MAX_LENGTH_BYTES } from './constants.js'
|
|
6
|
+
import { pureJsCrypto } from './crypto/js.js'
|
|
7
|
+
import { decryptStream, encryptStream } from './crypto/streaming.js'
|
|
8
|
+
import { uint16BEDecode, uint16BEEncode } from './encoder.js'
|
|
9
|
+
import { XXHandshake } from './handshake-xx.js'
|
|
10
|
+
import { type MetricsRegistry, registerMetrics } from './metrics.js'
|
|
11
|
+
import { getPayload } from './utils.js'
|
|
12
|
+
import type { bytes } from './@types/basic.js'
|
|
13
|
+
import type { IHandshake } from './@types/handshake-interface.js'
|
|
14
|
+
import type { INoiseConnection, KeyPair } from './@types/libp2p.js'
|
|
15
|
+
import type { ICryptoInterface } from './crypto.js'
|
|
16
|
+
import type { NoiseExtensions } from './proto/payload.js'
|
|
17
|
+
import type { SecuredConnection } from '@libp2p/interface/connection-encrypter'
|
|
18
|
+
import type { Metrics } from '@libp2p/interface/metrics'
|
|
19
|
+
import type { PeerId } from '@libp2p/interface/peer-id'
|
|
20
|
+
import type { Duplex, Source } from 'it-stream-types'
|
|
21
|
+
|
|
22
|
+
interface HandshakeParams {
|
|
23
|
+
connection: LengthPrefixedStream
|
|
24
|
+
isInitiator: boolean
|
|
25
|
+
localPeer: PeerId
|
|
26
|
+
remotePeer?: PeerId
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface NoiseInit {
|
|
30
|
+
/**
|
|
31
|
+
* x25519 private key, reuse for faster handshakes
|
|
32
|
+
*/
|
|
33
|
+
staticNoiseKey?: bytes
|
|
34
|
+
extensions?: NoiseExtensions
|
|
35
|
+
crypto?: ICryptoInterface
|
|
36
|
+
prologueBytes?: Uint8Array
|
|
37
|
+
metrics?: Metrics
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class Noise implements INoiseConnection {
|
|
41
|
+
public protocol = '/noise'
|
|
42
|
+
public crypto: ICryptoInterface
|
|
43
|
+
|
|
44
|
+
private readonly prologue: Uint8Array
|
|
45
|
+
private readonly staticKeys: KeyPair
|
|
46
|
+
private readonly extensions?: NoiseExtensions
|
|
47
|
+
private readonly metrics?: MetricsRegistry
|
|
48
|
+
|
|
49
|
+
constructor (init: NoiseInit = {}) {
|
|
50
|
+
const { staticNoiseKey, extensions, crypto, prologueBytes, metrics } = init
|
|
51
|
+
|
|
52
|
+
this.crypto = crypto ?? pureJsCrypto
|
|
53
|
+
this.extensions = extensions
|
|
54
|
+
this.metrics = metrics ? registerMetrics(metrics) : undefined
|
|
55
|
+
|
|
56
|
+
if (staticNoiseKey) {
|
|
57
|
+
// accepts x25519 private key of length 32
|
|
58
|
+
this.staticKeys = this.crypto.generateX25519KeyPairFromSeed(staticNoiseKey)
|
|
59
|
+
} else {
|
|
60
|
+
this.staticKeys = this.crypto.generateX25519KeyPair()
|
|
61
|
+
}
|
|
62
|
+
this.prologue = prologueBytes ?? new Uint8Array(0)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Encrypt outgoing data to the remote party (handshake as initiator)
|
|
67
|
+
*
|
|
68
|
+
* @param {PeerId} localPeer - PeerId of the receiving peer
|
|
69
|
+
* @param {Duplex<AsyncGenerator<Uint8Array>, AsyncIterable<Uint8Array>, Promise<void>>} connection - streaming iterable duplex that will be encrypted
|
|
70
|
+
* @param {PeerId} remotePeer - PeerId of the remote peer. Used to validate the integrity of the remote peer.
|
|
71
|
+
* @returns {Promise<SecuredConnection>}
|
|
72
|
+
*/
|
|
73
|
+
public async secureOutbound (localPeer: PeerId, connection: Duplex<AsyncGenerator<Uint8Array>, AsyncIterable<Uint8Array>, Promise<void>>, remotePeer?: PeerId): Promise<SecuredConnection<NoiseExtensions>> {
|
|
74
|
+
const wrappedConnection = lpStream(
|
|
75
|
+
connection,
|
|
76
|
+
{
|
|
77
|
+
lengthEncoder: uint16BEEncode,
|
|
78
|
+
lengthDecoder: uint16BEDecode,
|
|
79
|
+
maxDataLength: NOISE_MSG_MAX_LENGTH_BYTES
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
const handshake = await this.performHandshake({
|
|
83
|
+
connection: wrappedConnection,
|
|
84
|
+
isInitiator: true,
|
|
85
|
+
localPeer,
|
|
86
|
+
remotePeer
|
|
87
|
+
})
|
|
88
|
+
const conn = await this.createSecureConnection(wrappedConnection, handshake)
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
conn,
|
|
92
|
+
remoteExtensions: handshake.remoteExtensions,
|
|
93
|
+
remotePeer: handshake.remotePeer
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Decrypt incoming data (handshake as responder).
|
|
99
|
+
*
|
|
100
|
+
* @param {PeerId} localPeer - PeerId of the receiving peer.
|
|
101
|
+
* @param {Duplex<AsyncGenerator<Uint8Array>, AsyncIterable<Uint8Array>, Promise<void>>} connection - streaming iterable duplex that will be encryption.
|
|
102
|
+
* @param {PeerId} remotePeer - optional PeerId of the initiating peer, if known. This may only exist during transport upgrades.
|
|
103
|
+
* @returns {Promise<SecuredConnection>}
|
|
104
|
+
*/
|
|
105
|
+
public async secureInbound (localPeer: PeerId, connection: Duplex<AsyncGenerator<Uint8Array>, AsyncIterable<Uint8Array>, Promise<void>>, remotePeer?: PeerId): Promise<SecuredConnection<NoiseExtensions>> {
|
|
106
|
+
const wrappedConnection = lpStream(
|
|
107
|
+
connection,
|
|
108
|
+
{
|
|
109
|
+
lengthEncoder: uint16BEEncode,
|
|
110
|
+
lengthDecoder: uint16BEDecode,
|
|
111
|
+
maxDataLength: NOISE_MSG_MAX_LENGTH_BYTES
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
const handshake = await this.performHandshake({
|
|
115
|
+
connection: wrappedConnection,
|
|
116
|
+
isInitiator: false,
|
|
117
|
+
localPeer,
|
|
118
|
+
remotePeer
|
|
119
|
+
})
|
|
120
|
+
const conn = await this.createSecureConnection(wrappedConnection, handshake)
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
conn,
|
|
124
|
+
remotePeer: handshake.remotePeer,
|
|
125
|
+
remoteExtensions: handshake.remoteExtensions
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* If Noise pipes supported, tries IK handshake first with XX as fallback if it fails.
|
|
131
|
+
* If noise pipes disabled or remote peer static key is unknown, use XX.
|
|
132
|
+
*
|
|
133
|
+
* @param {HandshakeParams} params
|
|
134
|
+
*/
|
|
135
|
+
private async performHandshake (params: HandshakeParams): Promise<IHandshake> {
|
|
136
|
+
const payload = await getPayload(params.localPeer, this.staticKeys.publicKey, this.extensions)
|
|
137
|
+
|
|
138
|
+
// run XX handshake
|
|
139
|
+
return this.performXXHandshake(params, payload)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private async performXXHandshake (
|
|
143
|
+
params: HandshakeParams,
|
|
144
|
+
payload: bytes
|
|
145
|
+
): Promise<XXHandshake> {
|
|
146
|
+
const { isInitiator, remotePeer, connection } = params
|
|
147
|
+
const handshake = new XXHandshake(
|
|
148
|
+
isInitiator,
|
|
149
|
+
payload,
|
|
150
|
+
this.prologue,
|
|
151
|
+
this.crypto,
|
|
152
|
+
this.staticKeys,
|
|
153
|
+
connection,
|
|
154
|
+
remotePeer
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
await handshake.propose()
|
|
159
|
+
await handshake.exchange()
|
|
160
|
+
await handshake.finish()
|
|
161
|
+
this.metrics?.xxHandshakeSuccesses.increment()
|
|
162
|
+
} catch (e: unknown) {
|
|
163
|
+
this.metrics?.xxHandshakeErrors.increment()
|
|
164
|
+
if (e instanceof Error) {
|
|
165
|
+
e.message = `Error occurred during XX handshake: ${e.message}`
|
|
166
|
+
throw e
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return handshake
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private async createSecureConnection (
|
|
174
|
+
connection: LengthPrefixedStream<Duplex<AsyncGenerator<Uint8Array>, AsyncIterable<Uint8Array>, Promise<void>>>,
|
|
175
|
+
handshake: IHandshake
|
|
176
|
+
): Promise<Duplex<AsyncGenerator<Uint8Array>, Source<Uint8Array>, Promise<void>>> {
|
|
177
|
+
// Create encryption box/unbox wrapper
|
|
178
|
+
const [secure, user] = duplexPair<Uint8Array>()
|
|
179
|
+
const network = connection.unwrap()
|
|
180
|
+
|
|
181
|
+
await pipe(
|
|
182
|
+
secure, // write to wrapper
|
|
183
|
+
encryptStream(handshake, this.metrics), // encrypt data + prefix with message length
|
|
184
|
+
network, // send to the remote peer
|
|
185
|
+
(source) => decode(source, { lengthDecoder: uint16BEDecode }), // read message length prefix
|
|
186
|
+
decryptStream(handshake, this.metrics), // decrypt the incoming data
|
|
187
|
+
secure // pipe to the wrapper
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return user
|
|
191
|
+
}
|
|
192
|
+
}
|
package/src/nonce.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { bytes, uint64 } from './@types/basic.js'
|
|
2
|
+
|
|
3
|
+
export const MIN_NONCE = 0
|
|
4
|
+
// For performance reasons, the nonce is represented as a JS `number`
|
|
5
|
+
// Although JS `number` can safely represent integers up to 2 ** 53 - 1, we choose to only use
|
|
6
|
+
// 4 bytes to store the data for performance reason.
|
|
7
|
+
// This is a slight deviation from the noise spec, which describes the max nonce as 2 ** 64 - 2
|
|
8
|
+
// The effect is that this implementation will need a new handshake to be performed after fewer messages are exchanged than other implementations with full uint64 nonces.
|
|
9
|
+
// this MAX_NONCE is still a large number of messages, so the practical effect of this is negligible.
|
|
10
|
+
export const MAX_NONCE = 0xffffffff
|
|
11
|
+
|
|
12
|
+
const ERR_MAX_NONCE = 'Cipherstate has reached maximum n, a new handshake must be performed'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The nonce is an uint that's increased over time.
|
|
16
|
+
* Maintaining different representations help improve performance.
|
|
17
|
+
*/
|
|
18
|
+
export class Nonce {
|
|
19
|
+
private n: uint64
|
|
20
|
+
private readonly bytes: bytes
|
|
21
|
+
private readonly view: DataView
|
|
22
|
+
|
|
23
|
+
constructor (n = MIN_NONCE) {
|
|
24
|
+
this.n = n
|
|
25
|
+
this.bytes = new Uint8Array(12)
|
|
26
|
+
this.view = new DataView(this.bytes.buffer, this.bytes.byteOffset, this.bytes.byteLength)
|
|
27
|
+
this.view.setUint32(4, n, true)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
increment (): void {
|
|
31
|
+
this.n++
|
|
32
|
+
// Even though we're treating the nonce as 8 bytes, RFC7539 specifies 12 bytes for a nonce.
|
|
33
|
+
this.view.setUint32(4, this.n, true)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
getBytes (): bytes {
|
|
37
|
+
return this.bytes
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getUint64 (): uint64 {
|
|
41
|
+
return this.n
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
assertValue (): void {
|
|
45
|
+
if (this.n > MAX_NONCE) {
|
|
46
|
+
throw new Error(ERR_MAX_NONCE)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/* eslint-disable import/export */
|
|
2
|
+
/* eslint-disable complexity */
|
|
3
|
+
/* eslint-disable @typescript-eslint/no-namespace */
|
|
4
|
+
/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */
|
|
5
|
+
/* eslint-disable @typescript-eslint/no-empty-interface */
|
|
6
|
+
|
|
7
|
+
import { encodeMessage, decodeMessage, message } from 'protons-runtime'
|
|
8
|
+
import type { Codec } from 'protons-runtime'
|
|
9
|
+
import type { Uint8ArrayList } from 'uint8arraylist'
|
|
10
|
+
|
|
11
|
+
export interface NoiseExtensions {
|
|
12
|
+
webtransportCerthashes: Uint8Array[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export namespace NoiseExtensions {
|
|
16
|
+
let _codec: Codec<NoiseExtensions>
|
|
17
|
+
|
|
18
|
+
export const codec = (): Codec<NoiseExtensions> => {
|
|
19
|
+
if (_codec == null) {
|
|
20
|
+
_codec = message<NoiseExtensions>((obj, w, opts = {}) => {
|
|
21
|
+
if (opts.lengthDelimited !== false) {
|
|
22
|
+
w.fork()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (obj.webtransportCerthashes != null) {
|
|
26
|
+
for (const value of obj.webtransportCerthashes) {
|
|
27
|
+
w.uint32(10)
|
|
28
|
+
w.bytes(value)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (opts.lengthDelimited !== false) {
|
|
33
|
+
w.ldelim()
|
|
34
|
+
}
|
|
35
|
+
}, (reader, length) => {
|
|
36
|
+
const obj: any = {
|
|
37
|
+
webtransportCerthashes: []
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const end = length == null ? reader.len : reader.pos + length
|
|
41
|
+
|
|
42
|
+
while (reader.pos < end) {
|
|
43
|
+
const tag = reader.uint32()
|
|
44
|
+
|
|
45
|
+
switch (tag >>> 3) {
|
|
46
|
+
case 1:
|
|
47
|
+
obj.webtransportCerthashes.push(reader.bytes())
|
|
48
|
+
break
|
|
49
|
+
default:
|
|
50
|
+
reader.skipType(tag & 7)
|
|
51
|
+
break
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return obj
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return _codec
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const encode = (obj: Partial<NoiseExtensions>): Uint8Array => {
|
|
63
|
+
return encodeMessage(obj, NoiseExtensions.codec())
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const decode = (buf: Uint8Array | Uint8ArrayList): NoiseExtensions => {
|
|
67
|
+
return decodeMessage(buf, NoiseExtensions.codec())
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface NoiseHandshakePayload {
|
|
72
|
+
identityKey: Uint8Array
|
|
73
|
+
identitySig: Uint8Array
|
|
74
|
+
extensions?: NoiseExtensions
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export namespace NoiseHandshakePayload {
|
|
78
|
+
let _codec: Codec<NoiseHandshakePayload>
|
|
79
|
+
|
|
80
|
+
export const codec = (): Codec<NoiseHandshakePayload> => {
|
|
81
|
+
if (_codec == null) {
|
|
82
|
+
_codec = message<NoiseHandshakePayload>((obj, w, opts = {}) => {
|
|
83
|
+
if (opts.lengthDelimited !== false) {
|
|
84
|
+
w.fork()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (opts.writeDefaults === true || (obj.identityKey != null && obj.identityKey.byteLength > 0)) {
|
|
88
|
+
w.uint32(10)
|
|
89
|
+
w.bytes(obj.identityKey ?? new Uint8Array(0))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (opts.writeDefaults === true || (obj.identitySig != null && obj.identitySig.byteLength > 0)) {
|
|
93
|
+
w.uint32(18)
|
|
94
|
+
w.bytes(obj.identitySig ?? new Uint8Array(0))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (obj.extensions != null) {
|
|
98
|
+
w.uint32(34)
|
|
99
|
+
NoiseExtensions.codec().encode(obj.extensions, w, {
|
|
100
|
+
writeDefaults: false
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (opts.lengthDelimited !== false) {
|
|
105
|
+
w.ldelim()
|
|
106
|
+
}
|
|
107
|
+
}, (reader, length) => {
|
|
108
|
+
const obj: any = {
|
|
109
|
+
identityKey: new Uint8Array(0),
|
|
110
|
+
identitySig: new Uint8Array(0)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const end = length == null ? reader.len : reader.pos + length
|
|
114
|
+
|
|
115
|
+
while (reader.pos < end) {
|
|
116
|
+
const tag = reader.uint32()
|
|
117
|
+
|
|
118
|
+
switch (tag >>> 3) {
|
|
119
|
+
case 1:
|
|
120
|
+
obj.identityKey = reader.bytes()
|
|
121
|
+
break
|
|
122
|
+
case 2:
|
|
123
|
+
obj.identitySig = reader.bytes()
|
|
124
|
+
break
|
|
125
|
+
case 4:
|
|
126
|
+
obj.extensions = NoiseExtensions.codec().decode(reader, reader.uint32())
|
|
127
|
+
break
|
|
128
|
+
default:
|
|
129
|
+
reader.skipType(tag & 7)
|
|
130
|
+
break
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return obj
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return _codec
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export const encode = (obj: Partial<NoiseHandshakePayload>): Uint8Array => {
|
|
142
|
+
return encodeMessage(obj, NoiseHandshakePayload.codec())
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const decode = (buf: Uint8Array | Uint8ArrayList): NoiseHandshakePayload => {
|
|
146
|
+
return decodeMessage(buf, NoiseHandshakePayload.codec())
|
|
147
|
+
}
|
|
148
|
+
}
|