@libp2p/noise 1.0.0-049bfa0fa
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/README.md +95 -0
- package/dist/index.min.js +23 -0
- package/dist/index.min.js.map +7 -0
- package/dist/src/constants.d.ts +5 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +5 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/crypto/index.browser.d.ts +2 -0
- package/dist/src/crypto/index.browser.d.ts.map +1 -0
- package/dist/src/crypto/index.browser.js +3 -0
- package/dist/src/crypto/index.browser.js.map +1 -0
- package/dist/src/crypto/index.d.ts +3 -0
- package/dist/src/crypto/index.d.ts.map +1 -0
- package/dist/src/crypto/index.js +188 -0
- package/dist/src/crypto/index.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 +43 -0
- package/dist/src/crypto/js.js.map +1 -0
- package/dist/src/crypto.d.ts +14 -0
- package/dist/src/crypto.d.ts.map +1 -0
- package/dist/src/crypto.js +11 -0
- package/dist/src/crypto.js.map +1 -0
- package/dist/src/encoder.d.ts +10 -0
- package/dist/src/encoder.d.ts.map +1 -0
- package/dist/src/encoder.js +22 -0
- package/dist/src/encoder.js.map +1 -0
- package/dist/src/errors.d.ts +6 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +9 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/index.d.ts +52 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +43 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/logger.d.ts +10 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +56 -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 +58 -0
- package/dist/src/noise.d.ts.map +1 -0
- package/dist/src/noise.js +174 -0
- package/dist/src/noise.js.map +1 -0
- package/dist/src/nonce.d.ts +17 -0
- package/dist/src/nonce.d.ts.map +1 -0
- package/dist/src/nonce.js +42 -0
- package/dist/src/nonce.js.map +1 -0
- package/dist/src/performHandshake.d.ts +5 -0
- package/dist/src/performHandshake.d.ts.map +1 -0
- package/dist/src/performHandshake.js +69 -0
- package/dist/src/performHandshake.js.map +1 -0
- package/dist/src/proto/payload.d.ts +22 -0
- package/dist/src/proto/payload.d.ts.map +1 -0
- package/dist/src/proto/payload.js +133 -0
- package/dist/src/proto/payload.js.map +1 -0
- package/dist/src/protocol.d.ts +71 -0
- package/dist/src/protocol.d.ts.map +1 -0
- package/dist/src/protocol.js +257 -0
- package/dist/src/protocol.js.map +1 -0
- package/dist/src/types.d.ts +98 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils.d.ts +11 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/utils.js +176 -0
- package/dist/src/utils.js.map +1 -0
- package/package.json +94 -0
- package/src/constants.ts +5 -0
- package/src/crypto/index.browser.ts +3 -0
- package/src/crypto/index.ts +219 -0
- package/src/crypto/js.ts +56 -0
- package/src/crypto.ts +27 -0
- package/src/encoder.ts +24 -0
- package/src/errors.ts +10 -0
- package/src/index.ts +58 -0
- package/src/logger.ts +65 -0
- package/src/metrics.ts +32 -0
- package/src/noise.ts +237 -0
- package/src/nonce.ts +49 -0
- package/src/performHandshake.ts +91 -0
- package/src/proto/payload.proto +12 -0
- package/src/proto/payload.ts +170 -0
- package/src/protocol.ts +313 -0
- package/src/types.ts +105 -0
- package/src/utils.ts +221 -0
package/src/noise.ts
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { publicKeyFromProtobuf } from '@libp2p/crypto/keys'
|
|
2
|
+
import { InvalidCryptoExchangeError, serviceCapabilities } from '@libp2p/interface'
|
|
3
|
+
import { peerIdFromPublicKey } from '@libp2p/peer-id'
|
|
4
|
+
import { lpStream } from '@libp2p/utils'
|
|
5
|
+
import { alloc as uint8ArrayAlloc } from 'uint8arrays/alloc'
|
|
6
|
+
import { NOISE_MSG_MAX_LENGTH_BYTES } from './constants.js'
|
|
7
|
+
import { defaultCrypto } from './crypto/index.js'
|
|
8
|
+
import { wrapCrypto } from './crypto.js'
|
|
9
|
+
import { uint16BEDecode, uint16BEEncode } from './encoder.js'
|
|
10
|
+
import { registerMetrics } from './metrics.js'
|
|
11
|
+
import { performHandshakeInitiator, performHandshakeResponder } from './performHandshake.js'
|
|
12
|
+
import { toMessageStream } from './utils.ts'
|
|
13
|
+
import type { ICryptoInterface } from './crypto.js'
|
|
14
|
+
import type { NoiseComponents } from './index.js'
|
|
15
|
+
import type { MetricsRegistry } from './metrics.js'
|
|
16
|
+
import type { HandshakeResult, ICrypto, INoiseConnection, INoiseExtensions, KeyPair } from './types.js'
|
|
17
|
+
import type { SecuredConnection, PrivateKey, PublicKey, StreamMuxerFactory, SecureConnectionOptions, Logger, MessageStream } from '@libp2p/interface'
|
|
18
|
+
import type { LengthPrefixedStream } from '@libp2p/utils'
|
|
19
|
+
|
|
20
|
+
export interface NoiseExtensions {
|
|
21
|
+
webtransportCerthashes: Uint8Array[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface NoiseInit {
|
|
25
|
+
/**
|
|
26
|
+
* x25519 private key, reuse for faster handshakes
|
|
27
|
+
*/
|
|
28
|
+
staticNoiseKey?: Uint8Array
|
|
29
|
+
extensions?: Partial<NoiseExtensions>
|
|
30
|
+
crypto?: ICryptoInterface
|
|
31
|
+
prologueBytes?: Uint8Array
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class Noise implements INoiseConnection {
|
|
35
|
+
public protocol = '/noise'
|
|
36
|
+
public crypto: ICrypto
|
|
37
|
+
|
|
38
|
+
private readonly prologue: Uint8Array
|
|
39
|
+
private readonly staticKey: KeyPair
|
|
40
|
+
private readonly extensions?: NoiseExtensions
|
|
41
|
+
private readonly metrics?: MetricsRegistry
|
|
42
|
+
private readonly components: NoiseComponents
|
|
43
|
+
private readonly log: Logger
|
|
44
|
+
|
|
45
|
+
constructor (components: NoiseComponents, init: NoiseInit = {}) {
|
|
46
|
+
const { staticNoiseKey, extensions, crypto, prologueBytes } = init
|
|
47
|
+
const { metrics } = components
|
|
48
|
+
|
|
49
|
+
this.components = components
|
|
50
|
+
this.log = components.logger.forComponent('libp2p:noise')
|
|
51
|
+
const _crypto = crypto ?? defaultCrypto
|
|
52
|
+
this.crypto = wrapCrypto(_crypto)
|
|
53
|
+
this.extensions = {
|
|
54
|
+
webtransportCerthashes: [],
|
|
55
|
+
...extensions
|
|
56
|
+
}
|
|
57
|
+
this.metrics = metrics ? registerMetrics(metrics) : undefined
|
|
58
|
+
|
|
59
|
+
if (staticNoiseKey) {
|
|
60
|
+
// accepts x25519 private key of length 32
|
|
61
|
+
this.staticKey = _crypto.generateX25519KeyPairFromSeed(staticNoiseKey)
|
|
62
|
+
} else {
|
|
63
|
+
this.staticKey = _crypto.generateX25519KeyPair()
|
|
64
|
+
}
|
|
65
|
+
this.prologue = prologueBytes ?? uint8ArrayAlloc(0)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
readonly [Symbol.toStringTag] = '@libp2p/noise'
|
|
69
|
+
|
|
70
|
+
readonly [serviceCapabilities]: string[] = [
|
|
71
|
+
'@libp2p/connection-encryption',
|
|
72
|
+
'@libp2p/noise'
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Encrypt outgoing data to the remote party (handshake as initiator)
|
|
77
|
+
*
|
|
78
|
+
* @param connection - streaming iterable duplex that will be encrypted
|
|
79
|
+
* @param options
|
|
80
|
+
* @param options.remotePeer - PeerId of the remote peer. Used to validate the integrity of the remote peer
|
|
81
|
+
* @param options.signal - Used to abort the operation
|
|
82
|
+
*/
|
|
83
|
+
async secureOutbound (connection: MessageStream, options?: SecureConnectionOptions): Promise<SecuredConnection<INoiseExtensions>> {
|
|
84
|
+
const log = connection.log?.newScope('noise') ?? this.log
|
|
85
|
+
const wrappedConnection = lpStream(connection, {
|
|
86
|
+
lengthEncoder: uint16BEEncode,
|
|
87
|
+
lengthDecoder: uint16BEDecode,
|
|
88
|
+
maxDataLength: NOISE_MSG_MAX_LENGTH_BYTES
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const handshake = await this.performHandshakeInitiator(
|
|
92
|
+
wrappedConnection,
|
|
93
|
+
this.components.privateKey,
|
|
94
|
+
log,
|
|
95
|
+
options?.remotePeer?.publicKey,
|
|
96
|
+
options
|
|
97
|
+
)
|
|
98
|
+
const publicKey = publicKeyFromProtobuf(handshake.payload.identityKey)
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
connection: toMessageStream(wrappedConnection.unwrap(), handshake, this.metrics),
|
|
102
|
+
remoteExtensions: handshake.payload.extensions,
|
|
103
|
+
remotePeer: peerIdFromPublicKey(publicKey),
|
|
104
|
+
streamMuxer: options?.skipStreamMuxerNegotiation === true ? undefined : this.getStreamMuxer(handshake.payload.extensions?.streamMuxers)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private getStreamMuxer (protocols?: string[]): StreamMuxerFactory | undefined {
|
|
109
|
+
if (protocols == null || protocols.length === 0) {
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const streamMuxers = this.components.upgrader.getStreamMuxers()
|
|
114
|
+
|
|
115
|
+
if (streamMuxers != null) {
|
|
116
|
+
for (const protocol of protocols) {
|
|
117
|
+
const streamMuxer = streamMuxers.get(protocol)
|
|
118
|
+
|
|
119
|
+
if (streamMuxer != null) {
|
|
120
|
+
return streamMuxer
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (protocols.length) {
|
|
126
|
+
throw new InvalidCryptoExchangeError('Early muxer negotiation was requested but the initiator and responder had no common muxers')
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Decrypt incoming data (handshake as responder).
|
|
132
|
+
*
|
|
133
|
+
* @param connection - streaming iterable duplex that will be encrypted
|
|
134
|
+
* @param options
|
|
135
|
+
* @param options.remotePeer - PeerId of the remote peer. Used to validate the integrity of the remote peer
|
|
136
|
+
* @param options.signal - Used to abort the operation
|
|
137
|
+
*/
|
|
138
|
+
async secureInbound (connection: MessageStream, options?: SecureConnectionOptions): Promise<SecuredConnection<INoiseExtensions>> {
|
|
139
|
+
const log = connection.log?.newScope('noise') ?? this.log
|
|
140
|
+
const wrappedConnection = lpStream(connection, {
|
|
141
|
+
lengthEncoder: uint16BEEncode,
|
|
142
|
+
lengthDecoder: uint16BEDecode,
|
|
143
|
+
maxDataLength: NOISE_MSG_MAX_LENGTH_BYTES
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const handshake = await this.performHandshakeResponder(
|
|
147
|
+
wrappedConnection,
|
|
148
|
+
this.components.privateKey,
|
|
149
|
+
log,
|
|
150
|
+
options?.remotePeer?.publicKey,
|
|
151
|
+
options
|
|
152
|
+
)
|
|
153
|
+
const publicKey = publicKeyFromProtobuf(handshake.payload.identityKey)
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
connection: toMessageStream(wrappedConnection.unwrap(), handshake, this.metrics),
|
|
157
|
+
remoteExtensions: handshake.payload.extensions,
|
|
158
|
+
remotePeer: peerIdFromPublicKey(publicKey),
|
|
159
|
+
streamMuxer: options?.skipStreamMuxerNegotiation === true ? undefined : this.getStreamMuxer(handshake.payload.extensions?.streamMuxers)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Perform XX handshake as initiator.
|
|
165
|
+
*/
|
|
166
|
+
private async performHandshakeInitiator (
|
|
167
|
+
connection: LengthPrefixedStream,
|
|
168
|
+
// TODO: pass private key in noise constructor via Components
|
|
169
|
+
privateKey: PrivateKey,
|
|
170
|
+
log: Logger,
|
|
171
|
+
remoteIdentityKey?: PublicKey,
|
|
172
|
+
options?: SecureConnectionOptions
|
|
173
|
+
): Promise<HandshakeResult> {
|
|
174
|
+
let result: HandshakeResult
|
|
175
|
+
const streamMuxers = options?.skipStreamMuxerNegotiation === true ? [] : [...this.components.upgrader.getStreamMuxers().keys()]
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
result = await performHandshakeInitiator({
|
|
179
|
+
connection,
|
|
180
|
+
privateKey,
|
|
181
|
+
remoteIdentityKey,
|
|
182
|
+
log: log.newScope('xxhandshake'),
|
|
183
|
+
crypto: this.crypto,
|
|
184
|
+
prologue: this.prologue,
|
|
185
|
+
s: this.staticKey,
|
|
186
|
+
extensions: {
|
|
187
|
+
streamMuxers,
|
|
188
|
+
webtransportCerthashes: [],
|
|
189
|
+
...this.extensions
|
|
190
|
+
}
|
|
191
|
+
}, options)
|
|
192
|
+
this.metrics?.xxHandshakeSuccesses.increment()
|
|
193
|
+
} catch (e: unknown) {
|
|
194
|
+
this.metrics?.xxHandshakeErrors.increment()
|
|
195
|
+
throw e
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return result
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Perform XX handshake as responder.
|
|
203
|
+
*/
|
|
204
|
+
private async performHandshakeResponder (
|
|
205
|
+
connection: LengthPrefixedStream,
|
|
206
|
+
privateKey: PrivateKey,
|
|
207
|
+
log: Logger,
|
|
208
|
+
remoteIdentityKey?: PublicKey,
|
|
209
|
+
options?: SecureConnectionOptions
|
|
210
|
+
): Promise<HandshakeResult> {
|
|
211
|
+
let result: HandshakeResult
|
|
212
|
+
const streamMuxers = options?.skipStreamMuxerNegotiation === true ? [] : [...this.components.upgrader.getStreamMuxers().keys()]
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
result = await performHandshakeResponder({
|
|
216
|
+
connection,
|
|
217
|
+
privateKey,
|
|
218
|
+
remoteIdentityKey,
|
|
219
|
+
log: log.newScope('xxhandshake'),
|
|
220
|
+
crypto: this.crypto,
|
|
221
|
+
prologue: this.prologue,
|
|
222
|
+
s: this.staticKey,
|
|
223
|
+
extensions: {
|
|
224
|
+
streamMuxers,
|
|
225
|
+
webtransportCerthashes: [],
|
|
226
|
+
...this.extensions
|
|
227
|
+
}
|
|
228
|
+
}, options)
|
|
229
|
+
this.metrics?.xxHandshakeSuccesses.increment()
|
|
230
|
+
} catch (e: unknown) {
|
|
231
|
+
this.metrics?.xxHandshakeErrors.increment()
|
|
232
|
+
throw e
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return result
|
|
236
|
+
}
|
|
237
|
+
}
|
package/src/nonce.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { alloc as uint8ArrayAlloc } from 'uint8arrays/alloc'
|
|
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 = 'Cipher state 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: number
|
|
20
|
+
private readonly bytes: Uint8Array
|
|
21
|
+
private readonly view: DataView
|
|
22
|
+
|
|
23
|
+
constructor (n = MIN_NONCE) {
|
|
24
|
+
this.n = n
|
|
25
|
+
this.bytes = uint8ArrayAlloc(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 (): Uint8Array {
|
|
37
|
+
return this.bytes
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getUint64 (): number {
|
|
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,91 @@
|
|
|
1
|
+
import {
|
|
2
|
+
logLocalStaticKeys,
|
|
3
|
+
logLocalEphemeralKeys,
|
|
4
|
+
logRemoteEphemeralKey,
|
|
5
|
+
logRemoteStaticKey,
|
|
6
|
+
logCipherState
|
|
7
|
+
} from './logger.js'
|
|
8
|
+
import { ZEROLEN, XXHandshakeState } from './protocol.js'
|
|
9
|
+
import { createHandshakePayload, decodeHandshakePayload } from './utils.js'
|
|
10
|
+
import type { HandshakeResult, HandshakeParams } from './types.js'
|
|
11
|
+
import type { AbortOptions } from '@libp2p/interface'
|
|
12
|
+
|
|
13
|
+
export async function performHandshakeInitiator (init: HandshakeParams, options?: AbortOptions): Promise<HandshakeResult> {
|
|
14
|
+
const { log, connection, crypto, privateKey, prologue, s, remoteIdentityKey, extensions } = init
|
|
15
|
+
|
|
16
|
+
const payload = await createHandshakePayload(privateKey, s.publicKey, extensions)
|
|
17
|
+
const xx = new XXHandshakeState({
|
|
18
|
+
crypto,
|
|
19
|
+
protocolName: 'Noise_XX_25519_ChaChaPoly_SHA256',
|
|
20
|
+
initiator: true,
|
|
21
|
+
prologue,
|
|
22
|
+
s
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
logLocalStaticKeys(xx.s, log)
|
|
26
|
+
log.trace('Stage 0 - Initiator starting to send first message.')
|
|
27
|
+
await connection.write(xx.writeMessageA(ZEROLEN), options)
|
|
28
|
+
log.trace('Stage 0 - Initiator finished sending first message.')
|
|
29
|
+
logLocalEphemeralKeys(xx.e, log)
|
|
30
|
+
|
|
31
|
+
log.trace('Stage 1 - Initiator waiting to receive first message from responder...')
|
|
32
|
+
const plaintext = xx.readMessageB(await connection.read(options))
|
|
33
|
+
log.trace('Stage 1 - Initiator received the message.')
|
|
34
|
+
logRemoteEphemeralKey(xx.re, log)
|
|
35
|
+
logRemoteStaticKey(xx.rs, log)
|
|
36
|
+
|
|
37
|
+
log.trace("Initiator going to check remote's signature...")
|
|
38
|
+
const receivedPayload = await decodeHandshakePayload(plaintext, xx.rs, remoteIdentityKey)
|
|
39
|
+
log.trace('All good with the signature!')
|
|
40
|
+
|
|
41
|
+
log.trace('Stage 2 - Initiator sending third handshake message.')
|
|
42
|
+
await connection.write(xx.writeMessageC(payload), options)
|
|
43
|
+
log.trace('Stage 2 - Initiator sent message with signed payload.')
|
|
44
|
+
|
|
45
|
+
const [cs1, cs2] = xx.ss.split()
|
|
46
|
+
logCipherState(cs1, cs2, log)
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
payload: receivedPayload,
|
|
50
|
+
encrypt: (plaintext) => cs1.encryptWithAd(ZEROLEN, plaintext),
|
|
51
|
+
decrypt: (ciphertext, dst) => cs2.decryptWithAd(ZEROLEN, ciphertext, dst)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function performHandshakeResponder (init: HandshakeParams, options?: AbortOptions): Promise<HandshakeResult> {
|
|
56
|
+
const { log, connection, crypto, privateKey, prologue, s, remoteIdentityKey, extensions } = init
|
|
57
|
+
|
|
58
|
+
const payload = await createHandshakePayload(privateKey, s.publicKey, extensions)
|
|
59
|
+
const xx = new XXHandshakeState({
|
|
60
|
+
crypto,
|
|
61
|
+
protocolName: 'Noise_XX_25519_ChaChaPoly_SHA256',
|
|
62
|
+
initiator: false,
|
|
63
|
+
prologue,
|
|
64
|
+
s
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
logLocalStaticKeys(xx.s, log)
|
|
68
|
+
log.trace('Stage 0 - Responder waiting to receive first message.')
|
|
69
|
+
xx.readMessageA(await connection.read(options))
|
|
70
|
+
log.trace('Stage 0 - Responder received first message.')
|
|
71
|
+
logRemoteEphemeralKey(xx.re, log)
|
|
72
|
+
|
|
73
|
+
log.trace('Stage 1 - Responder sending out first message with signed payload and static key.')
|
|
74
|
+
await connection.write(xx.writeMessageB(payload), options)
|
|
75
|
+
log.trace('Stage 1 - Responder sent the second handshake message with signed payload.')
|
|
76
|
+
logLocalEphemeralKeys(xx.e, log)
|
|
77
|
+
|
|
78
|
+
log.trace('Stage 2 - Responder waiting for third handshake message...')
|
|
79
|
+
const plaintext = xx.readMessageC(await connection.read(options))
|
|
80
|
+
log.trace('Stage 2 - Responder received the message, finished handshake.')
|
|
81
|
+
const receivedPayload = await decodeHandshakePayload(plaintext, xx.rs, remoteIdentityKey)
|
|
82
|
+
|
|
83
|
+
const [cs1, cs2] = xx.ss.split()
|
|
84
|
+
logCipherState(cs1, cs2, log)
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
payload: receivedPayload,
|
|
88
|
+
encrypt: (plaintext) => cs2.encryptWithAd(ZEROLEN, plaintext),
|
|
89
|
+
decrypt: (ciphertext, dst) => cs1.decryptWithAd(ZEROLEN, ciphertext, dst)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
|
|
3
|
+
message NoiseExtensions {
|
|
4
|
+
repeated bytes webtransport_certhashes = 1;
|
|
5
|
+
repeated string stream_muxers = 2;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
message NoiseHandshakePayload {
|
|
9
|
+
bytes identity_key = 1;
|
|
10
|
+
bytes identity_sig = 2;
|
|
11
|
+
optional NoiseExtensions extensions = 4;
|
|
12
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { decodeMessage, encodeMessage, MaxLengthError, message } from 'protons-runtime'
|
|
2
|
+
import { alloc as uint8ArrayAlloc } from 'uint8arrays/alloc'
|
|
3
|
+
import type { Codec, DecodeOptions } from 'protons-runtime'
|
|
4
|
+
import type { Uint8ArrayList } from 'uint8arraylist'
|
|
5
|
+
|
|
6
|
+
export interface NoiseExtensions {
|
|
7
|
+
webtransportCerthashes: Uint8Array[]
|
|
8
|
+
streamMuxers: string[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export namespace NoiseExtensions {
|
|
12
|
+
let _codec: Codec<NoiseExtensions>
|
|
13
|
+
|
|
14
|
+
export const codec = (): Codec<NoiseExtensions> => {
|
|
15
|
+
if (_codec == null) {
|
|
16
|
+
_codec = message<NoiseExtensions>((obj, w, opts = {}) => {
|
|
17
|
+
if (opts.lengthDelimited !== false) {
|
|
18
|
+
w.fork()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (obj.webtransportCerthashes != null) {
|
|
22
|
+
for (const value of obj.webtransportCerthashes) {
|
|
23
|
+
w.uint32(10)
|
|
24
|
+
w.bytes(value)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (obj.streamMuxers != null) {
|
|
29
|
+
for (const value of obj.streamMuxers) {
|
|
30
|
+
w.uint32(18)
|
|
31
|
+
w.string(value)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (opts.lengthDelimited !== false) {
|
|
36
|
+
w.ldelim()
|
|
37
|
+
}
|
|
38
|
+
}, (reader, length, opts = {}) => {
|
|
39
|
+
const obj: any = {
|
|
40
|
+
webtransportCerthashes: [],
|
|
41
|
+
streamMuxers: []
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const end = length == null ? reader.len : reader.pos + length
|
|
45
|
+
|
|
46
|
+
while (reader.pos < end) {
|
|
47
|
+
const tag = reader.uint32()
|
|
48
|
+
|
|
49
|
+
switch (tag >>> 3) {
|
|
50
|
+
case 1: {
|
|
51
|
+
if (opts.limits?.webtransportCerthashes != null && obj.webtransportCerthashes.length === opts.limits.webtransportCerthashes) {
|
|
52
|
+
throw new MaxLengthError('Decode error - map field "webtransportCerthashes" had too many elements')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
obj.webtransportCerthashes.push(reader.bytes())
|
|
56
|
+
break
|
|
57
|
+
}
|
|
58
|
+
case 2: {
|
|
59
|
+
if (opts.limits?.streamMuxers != null && obj.streamMuxers.length === opts.limits.streamMuxers) {
|
|
60
|
+
throw new MaxLengthError('Decode error - map field "streamMuxers" had too many elements')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
obj.streamMuxers.push(reader.string())
|
|
64
|
+
break
|
|
65
|
+
}
|
|
66
|
+
default: {
|
|
67
|
+
reader.skipType(tag & 7)
|
|
68
|
+
break
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return obj
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return _codec
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const encode = (obj: Partial<NoiseExtensions>): Uint8Array => {
|
|
81
|
+
return encodeMessage(obj, NoiseExtensions.codec())
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const decode = (buf: Uint8Array | Uint8ArrayList, opts?: DecodeOptions<NoiseExtensions>): NoiseExtensions => {
|
|
85
|
+
return decodeMessage(buf, NoiseExtensions.codec(), opts)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface NoiseHandshakePayload {
|
|
90
|
+
identityKey: Uint8Array
|
|
91
|
+
identitySig: Uint8Array
|
|
92
|
+
extensions?: NoiseExtensions
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export namespace NoiseHandshakePayload {
|
|
96
|
+
let _codec: Codec<NoiseHandshakePayload>
|
|
97
|
+
|
|
98
|
+
export const codec = (): Codec<NoiseHandshakePayload> => {
|
|
99
|
+
if (_codec == null) {
|
|
100
|
+
_codec = message<NoiseHandshakePayload>((obj, w, opts = {}) => {
|
|
101
|
+
if (opts.lengthDelimited !== false) {
|
|
102
|
+
w.fork()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if ((obj.identityKey != null && obj.identityKey.byteLength > 0)) {
|
|
106
|
+
w.uint32(10)
|
|
107
|
+
w.bytes(obj.identityKey)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if ((obj.identitySig != null && obj.identitySig.byteLength > 0)) {
|
|
111
|
+
w.uint32(18)
|
|
112
|
+
w.bytes(obj.identitySig)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (obj.extensions != null) {
|
|
116
|
+
w.uint32(34)
|
|
117
|
+
NoiseExtensions.codec().encode(obj.extensions, w)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (opts.lengthDelimited !== false) {
|
|
121
|
+
w.ldelim()
|
|
122
|
+
}
|
|
123
|
+
}, (reader, length, opts = {}) => {
|
|
124
|
+
const obj: any = {
|
|
125
|
+
identityKey: uint8ArrayAlloc(0),
|
|
126
|
+
identitySig: uint8ArrayAlloc(0)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const end = length == null ? reader.len : reader.pos + length
|
|
130
|
+
|
|
131
|
+
while (reader.pos < end) {
|
|
132
|
+
const tag = reader.uint32()
|
|
133
|
+
|
|
134
|
+
switch (tag >>> 3) {
|
|
135
|
+
case 1: {
|
|
136
|
+
obj.identityKey = reader.bytes()
|
|
137
|
+
break
|
|
138
|
+
}
|
|
139
|
+
case 2: {
|
|
140
|
+
obj.identitySig = reader.bytes()
|
|
141
|
+
break
|
|
142
|
+
}
|
|
143
|
+
case 4: {
|
|
144
|
+
obj.extensions = NoiseExtensions.codec().decode(reader, reader.uint32(), {
|
|
145
|
+
limits: opts.limits?.extensions
|
|
146
|
+
})
|
|
147
|
+
break
|
|
148
|
+
}
|
|
149
|
+
default: {
|
|
150
|
+
reader.skipType(tag & 7)
|
|
151
|
+
break
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return obj
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return _codec
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const encode = (obj: Partial<NoiseHandshakePayload>): Uint8Array => {
|
|
164
|
+
return encodeMessage(obj, NoiseHandshakePayload.codec())
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export const decode = (buf: Uint8Array | Uint8ArrayList, opts?: DecodeOptions<NoiseHandshakePayload>): NoiseHandshakePayload => {
|
|
168
|
+
return decodeMessage(buf, NoiseHandshakePayload.codec(), opts)
|
|
169
|
+
}
|
|
170
|
+
}
|