@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.
Files changed (98) hide show
  1. package/dist/index.min.js +27 -0
  2. package/dist/src/@types/basic.d.ts +5 -0
  3. package/dist/src/@types/basic.d.ts.map +1 -0
  4. package/dist/src/@types/basic.js +2 -0
  5. package/dist/src/@types/basic.js.map +1 -0
  6. package/dist/src/@types/handshake-interface.d.ts +15 -0
  7. package/dist/src/@types/handshake-interface.d.ts.map +1 -0
  8. package/dist/src/@types/handshake-interface.js +2 -0
  9. package/dist/src/@types/handshake-interface.js.map +1 -0
  10. package/dist/src/@types/handshake.d.ts +40 -0
  11. package/dist/src/@types/handshake.d.ts.map +1 -0
  12. package/dist/src/@types/handshake.js +2 -0
  13. package/dist/src/@types/handshake.js.map +1 -0
  14. package/dist/src/@types/libp2p.d.ts +10 -0
  15. package/dist/src/@types/libp2p.d.ts.map +1 -0
  16. package/dist/src/@types/libp2p.js +2 -0
  17. package/dist/src/@types/libp2p.js.map +1 -0
  18. package/dist/src/constants.d.ts +4 -0
  19. package/dist/src/constants.d.ts.map +1 -0
  20. package/dist/src/constants.js +4 -0
  21. package/dist/src/constants.js.map +1 -0
  22. package/dist/src/crypto/js.d.ts +3 -0
  23. package/dist/src/crypto/js.d.ts.map +1 -0
  24. package/dist/src/crypto/js.js +42 -0
  25. package/dist/src/crypto/js.js.map +1 -0
  26. package/dist/src/crypto/streaming.d.ts +7 -0
  27. package/dist/src/crypto/streaming.d.ts.map +1 -0
  28. package/dist/src/crypto/streaming.js +50 -0
  29. package/dist/src/crypto/streaming.js.map +1 -0
  30. package/dist/src/crypto.d.ts +13 -0
  31. package/dist/src/crypto.d.ts.map +1 -0
  32. package/dist/src/crypto.js +2 -0
  33. package/dist/src/crypto.js.map +1 -0
  34. package/dist/src/encoder.d.ts +15 -0
  35. package/dist/src/encoder.d.ts.map +1 -0
  36. package/dist/src/encoder.js +63 -0
  37. package/dist/src/encoder.js.map +1 -0
  38. package/dist/src/handshake-xx.d.ts +33 -0
  39. package/dist/src/handshake-xx.d.ts.map +1 -0
  40. package/dist/src/handshake-xx.js +139 -0
  41. package/dist/src/handshake-xx.js.map +1 -0
  42. package/dist/src/handshakes/abstract-handshake.d.ts +36 -0
  43. package/dist/src/handshakes/abstract-handshake.d.ts.map +1 -0
  44. package/dist/src/handshakes/abstract-handshake.js +140 -0
  45. package/dist/src/handshakes/abstract-handshake.js.map +1 -0
  46. package/dist/src/handshakes/xx.d.ts +18 -0
  47. package/dist/src/handshakes/xx.d.ts.map +1 -0
  48. package/dist/src/handshakes/xx.js +161 -0
  49. package/dist/src/handshakes/xx.js.map +1 -0
  50. package/dist/src/index.d.ts +7 -0
  51. package/dist/src/index.d.ts.map +1 -0
  52. package/dist/src/index.js +6 -0
  53. package/dist/src/index.js.map +1 -0
  54. package/dist/src/logger.d.ts +11 -0
  55. package/dist/src/logger.d.ts.map +1 -0
  56. package/dist/src/logger.js +45 -0
  57. package/dist/src/logger.js.map +1 -0
  58. package/dist/src/metrics.d.ts +4 -0
  59. package/dist/src/metrics.d.ts.map +1 -0
  60. package/dist/src/metrics.js +20 -0
  61. package/dist/src/metrics.js.map +1 -0
  62. package/dist/src/noise.d.ts +55 -0
  63. package/dist/src/noise.d.ts.map +1 -0
  64. package/dist/src/noise.js +130 -0
  65. package/dist/src/noise.js.map +1 -0
  66. package/dist/src/nonce.d.ts +18 -0
  67. package/dist/src/nonce.d.ts.map +1 -0
  68. package/dist/src/nonce.js +41 -0
  69. package/dist/src/nonce.js.map +1 -0
  70. package/dist/src/proto/payload.d.ts +21 -0
  71. package/dist/src/proto/payload.d.ts.map +1 -0
  72. package/dist/src/proto/payload.js +114 -0
  73. package/dist/src/proto/payload.js.map +1 -0
  74. package/dist/src/utils.d.ts +20 -0
  75. package/dist/src/utils.d.ts.map +1 -0
  76. package/dist/src/utils.js +74 -0
  77. package/dist/src/utils.js.map +1 -0
  78. package/package.json +99 -0
  79. package/src/@types/basic.ts +5 -0
  80. package/src/@types/handshake-interface.ts +12 -0
  81. package/src/@types/handshake.ts +48 -0
  82. package/src/@types/libp2p.ts +10 -0
  83. package/src/constants.ts +4 -0
  84. package/src/crypto/js.ts +58 -0
  85. package/src/crypto/streaming.ts +58 -0
  86. package/src/crypto.ts +16 -0
  87. package/src/encoder.ts +81 -0
  88. package/src/handshake-xx.ts +177 -0
  89. package/src/handshakes/abstract-handshake.ts +180 -0
  90. package/src/handshakes/xx.ts +184 -0
  91. package/src/index.ts +10 -0
  92. package/src/logger.ts +51 -0
  93. package/src/metrics.ts +32 -0
  94. package/src/noise.ts +192 -0
  95. package/src/nonce.ts +49 -0
  96. package/src/proto/payload.proto +11 -0
  97. package/src/proto/payload.ts +148 -0
  98. 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,11 @@
1
+ syntax = "proto3";
2
+
3
+ message NoiseExtensions {
4
+ repeated bytes webtransport_certhashes = 1;
5
+ }
6
+
7
+ message NoiseHandshakePayload {
8
+ bytes identity_key = 1;
9
+ bytes identity_sig = 2;
10
+ optional NoiseExtensions extensions = 4;
11
+ }
@@ -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
+ }