@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,58 @@
1
+ import { hkdf } from '@noble/hashes/hkdf'
2
+ import { sha256 } from '@noble/hashes/sha256'
3
+ import { ChaCha20Poly1305 } from '@stablelib/chacha20poly1305'
4
+ import * as x25519 from '@stablelib/x25519'
5
+ import type { bytes, bytes32 } from '../@types/basic.js'
6
+ import type { Hkdf } from '../@types/handshake.js'
7
+ import type { KeyPair } from '../@types/libp2p.js'
8
+ import type { ICryptoInterface } from '../crypto.js'
9
+
10
+ export const pureJsCrypto: ICryptoInterface = {
11
+ hashSHA256 (data: Uint8Array): Uint8Array {
12
+ return sha256(data)
13
+ },
14
+
15
+ getHKDF (ck: bytes32, ikm: Uint8Array): Hkdf {
16
+ const okm = hkdf(sha256, ikm, ck, undefined, 96)
17
+
18
+ const k1 = okm.subarray(0, 32)
19
+ const k2 = okm.subarray(32, 64)
20
+ const k3 = okm.subarray(64, 96)
21
+
22
+ return [k1, k2, k3]
23
+ },
24
+
25
+ generateX25519KeyPair (): KeyPair {
26
+ const keypair = x25519.generateKeyPair()
27
+
28
+ return {
29
+ publicKey: keypair.publicKey,
30
+ privateKey: keypair.secretKey
31
+ }
32
+ },
33
+
34
+ generateX25519KeyPairFromSeed (seed: Uint8Array): KeyPair {
35
+ const keypair = x25519.generateKeyPairFromSeed(seed)
36
+
37
+ return {
38
+ publicKey: keypair.publicKey,
39
+ privateKey: keypair.secretKey
40
+ }
41
+ },
42
+
43
+ generateX25519SharedKey (privateKey: Uint8Array, publicKey: Uint8Array): Uint8Array {
44
+ return x25519.sharedKey(privateKey, publicKey)
45
+ },
46
+
47
+ chaCha20Poly1305Encrypt (plaintext: Uint8Array, nonce: Uint8Array, ad: Uint8Array, k: bytes32): bytes {
48
+ const ctx = new ChaCha20Poly1305(k)
49
+
50
+ return ctx.seal(nonce, plaintext, ad)
51
+ },
52
+
53
+ chaCha20Poly1305Decrypt (ciphertext: Uint8Array, nonce: Uint8Array, ad: Uint8Array, k: bytes32, dst?: Uint8Array): bytes | null {
54
+ const ctx = new ChaCha20Poly1305(k)
55
+
56
+ return ctx.open(nonce, ciphertext, ad, dst)
57
+ }
58
+ }
@@ -0,0 +1,58 @@
1
+ import { TAG_LENGTH } from '@stablelib/chacha20poly1305'
2
+ import { NOISE_MSG_MAX_LENGTH_BYTES, NOISE_MSG_MAX_LENGTH_BYTES_WITHOUT_TAG } from '../constants.js'
3
+ import { uint16BEEncode } from '../encoder.js'
4
+ import type { IHandshake } from '../@types/handshake-interface.js'
5
+ import type { MetricsRegistry } from '../metrics.js'
6
+ import type { Transform } from 'it-stream-types'
7
+ import type { Uint8ArrayList } from 'uint8arraylist'
8
+
9
+ // Returns generator that encrypts payload from the user
10
+ export function encryptStream (handshake: IHandshake, metrics?: MetricsRegistry): Transform<AsyncIterable<Uint8Array>> {
11
+ return async function * (source) {
12
+ for await (const chunk of source) {
13
+ for (let i = 0; i < chunk.length; i += NOISE_MSG_MAX_LENGTH_BYTES_WITHOUT_TAG) {
14
+ let end = i + NOISE_MSG_MAX_LENGTH_BYTES_WITHOUT_TAG
15
+ if (end > chunk.length) {
16
+ end = chunk.length
17
+ }
18
+
19
+ const data = handshake.encrypt(chunk.subarray(i, end), handshake.session)
20
+ metrics?.encryptedPackets.increment()
21
+
22
+ yield uint16BEEncode(data.byteLength)
23
+ yield data
24
+ }
25
+ }
26
+ }
27
+ }
28
+
29
+ // Decrypt received payload to the user
30
+ export function decryptStream (handshake: IHandshake, metrics?: MetricsRegistry): Transform<AsyncIterable<Uint8ArrayList>, AsyncIterable<Uint8Array>> {
31
+ return async function * (source) {
32
+ for await (const chunk of source) {
33
+ for (let i = 0; i < chunk.length; i += NOISE_MSG_MAX_LENGTH_BYTES) {
34
+ let end = i + NOISE_MSG_MAX_LENGTH_BYTES
35
+ if (end > chunk.length) {
36
+ end = chunk.length
37
+ }
38
+
39
+ if (end - TAG_LENGTH < i) {
40
+ throw new Error('Invalid chunk')
41
+ }
42
+ const encrypted = chunk.subarray(i, end)
43
+ // memory allocation is not cheap so reuse the encrypted Uint8Array
44
+ // see https://github.com/ChainSafe/js-libp2p-noise/pull/242#issue-1422126164
45
+ // this is ok because chacha20 reads bytes one by one and don't reread after that
46
+ // it's also tested in https://github.com/ChainSafe/as-chacha20poly1305/pull/1/files#diff-25252846b58979dcaf4e41d47b3eadd7e4f335e7fb98da6c049b1f9cd011f381R48
47
+ const dst = chunk.subarray(i, end - TAG_LENGTH)
48
+ const { plaintext: decrypted, valid } = handshake.decrypt(encrypted, handshake.session, dst)
49
+ if (!valid) {
50
+ metrics?.decryptErrors.increment()
51
+ throw new Error('Failed to validate decrypted chunk')
52
+ }
53
+ metrics?.decryptedPackets.increment()
54
+ yield decrypted
55
+ }
56
+ }
57
+ }
58
+ }
package/src/crypto.ts ADDED
@@ -0,0 +1,16 @@
1
+ import type { bytes32, bytes } from './@types/basic.js'
2
+ import type { Hkdf } from './@types/handshake.js'
3
+ import type { KeyPair } from './@types/libp2p.js'
4
+
5
+ export interface ICryptoInterface {
6
+ hashSHA256: (data: Uint8Array) => Uint8Array
7
+
8
+ getHKDF: (ck: bytes32, ikm: Uint8Array) => Hkdf
9
+
10
+ generateX25519KeyPair: () => KeyPair
11
+ generateX25519KeyPairFromSeed: (seed: Uint8Array) => KeyPair
12
+ generateX25519SharedKey: (privateKey: Uint8Array, publicKey: Uint8Array) => Uint8Array
13
+
14
+ chaCha20Poly1305Encrypt: (plaintext: Uint8Array, nonce: Uint8Array, ad: Uint8Array, k: bytes32) => bytes
15
+ chaCha20Poly1305Decrypt: (ciphertext: Uint8Array, nonce: Uint8Array, ad: Uint8Array, k: bytes32, dst?: Uint8Array) => bytes | null
16
+ }
package/src/encoder.ts ADDED
@@ -0,0 +1,81 @@
1
+ import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
2
+ import type { bytes } from './@types/basic.js'
3
+ import type { MessageBuffer } from './@types/handshake.js'
4
+ import type { LengthDecoderFunction } from 'it-length-prefixed'
5
+ import type { Uint8ArrayList } from 'uint8arraylist'
6
+
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
+
15
+ export const uint16BEEncode = (value: number): Uint8Array => {
16
+ const target = allocUnsafe(2)
17
+ new DataView(target.buffer, target.byteOffset, target.byteLength).setUint16(0, value, false)
18
+ return target
19
+ }
20
+ uint16BEEncode.bytes = 2
21
+
22
+ export const uint16BEDecode: LengthDecoderFunction = (data: Uint8Array | Uint8ArrayList): number => {
23
+ if (data.length < 2) throw RangeError('Could not decode int16BE')
24
+
25
+ if (data instanceof Uint8Array) {
26
+ return new DataView(data.buffer, data.byteOffset, data.byteLength).getUint16(0, false)
27
+ }
28
+
29
+ return data.getUint16(0)
30
+ }
31
+ uint16BEDecode.bytes = 2
32
+
33
+ // Note: IK and XX encoder usage is opposite (XX uses in stages encode0 where IK uses encode1)
34
+
35
+ export function encode0 (message: MessageBuffer): bytes {
36
+ return uint8ArrayConcat([message.ne, message.ciphertext], message.ne.length + message.ciphertext.length)
37
+ }
38
+
39
+ export function encode1 (message: MessageBuffer): bytes {
40
+ return uint8ArrayConcat([message.ne, message.ns, message.ciphertext], message.ne.length + message.ns.length + message.ciphertext.length)
41
+ }
42
+
43
+ export function encode2 (message: MessageBuffer): bytes {
44
+ return uint8ArrayConcat([message.ns, message.ciphertext], message.ns.length + message.ciphertext.length)
45
+ }
46
+
47
+ export function decode0 (input: bytes): MessageBuffer {
48
+ if (input.length < 32) {
49
+ throw new Error('Cannot decode stage 0 MessageBuffer: length less than 32 bytes.')
50
+ }
51
+
52
+ return {
53
+ ne: input.subarray(0, 32),
54
+ ciphertext: input.subarray(32, input.length),
55
+ ns: new Uint8Array(0)
56
+ }
57
+ }
58
+
59
+ export function decode1 (input: bytes): MessageBuffer {
60
+ if (input.length < 80) {
61
+ throw new Error('Cannot decode stage 1 MessageBuffer: length less than 80 bytes.')
62
+ }
63
+
64
+ return {
65
+ ne: input.subarray(0, 32),
66
+ ns: input.subarray(32, 80),
67
+ ciphertext: input.subarray(80, input.length)
68
+ }
69
+ }
70
+
71
+ export function decode2 (input: bytes): MessageBuffer {
72
+ if (input.length < 48) {
73
+ throw new Error('Cannot decode stage 2 MessageBuffer: length less than 48 bytes.')
74
+ }
75
+
76
+ return {
77
+ ne: new Uint8Array(0),
78
+ ns: input.subarray(0, 48),
79
+ ciphertext: input.subarray(48, input.length)
80
+ }
81
+ }
@@ -0,0 +1,177 @@
1
+ import { InvalidCryptoExchangeError, UnexpectedPeerError } from '@libp2p/interface/errors'
2
+ import { decode0, decode1, decode2, encode0, encode1, encode2 } from './encoder.js'
3
+ import { XX } from './handshakes/xx.js'
4
+ import {
5
+ logger,
6
+ logLocalStaticKeys,
7
+ logLocalEphemeralKeys,
8
+ logRemoteEphemeralKey,
9
+ logRemoteStaticKey,
10
+ logCipherState
11
+ } from './logger.js'
12
+ import {
13
+ decodePayload,
14
+ getPeerIdFromPayload,
15
+ verifySignedPayload
16
+ } from './utils.js'
17
+ import type { bytes, bytes32 } from './@types/basic.js'
18
+ import type { IHandshake } from './@types/handshake-interface.js'
19
+ import type { CipherState, NoiseSession } from './@types/handshake.js'
20
+ import type { KeyPair } from './@types/libp2p.js'
21
+ import type { ICryptoInterface } from './crypto.js'
22
+ import type { NoiseExtensions } from './proto/payload.js'
23
+ import type { PeerId } from '@libp2p/interface/peer-id'
24
+ import type { LengthPrefixedStream } from 'it-length-prefixed-stream'
25
+
26
+ export class XXHandshake implements IHandshake {
27
+ public isInitiator: boolean
28
+ public session: NoiseSession
29
+ public remotePeer!: PeerId
30
+ public remoteExtensions: NoiseExtensions = { webtransportCerthashes: [] }
31
+
32
+ protected payload: bytes
33
+ protected connection: LengthPrefixedStream
34
+ protected xx: XX
35
+ protected staticKeypair: KeyPair
36
+
37
+ private readonly prologue: bytes32
38
+
39
+ constructor (
40
+ isInitiator: boolean,
41
+ payload: bytes,
42
+ prologue: bytes32,
43
+ crypto: ICryptoInterface,
44
+ staticKeypair: KeyPair,
45
+ connection: LengthPrefixedStream,
46
+ remotePeer?: PeerId,
47
+ handshake?: XX
48
+ ) {
49
+ this.isInitiator = isInitiator
50
+ this.payload = payload
51
+ this.prologue = prologue
52
+ this.staticKeypair = staticKeypair
53
+ this.connection = connection
54
+ if (remotePeer) {
55
+ this.remotePeer = remotePeer
56
+ }
57
+ this.xx = handshake ?? new XX(crypto)
58
+ this.session = this.xx.initSession(this.isInitiator, this.prologue, this.staticKeypair)
59
+ }
60
+
61
+ // stage 0
62
+ public async propose (): Promise<void> {
63
+ logLocalStaticKeys(this.session.hs.s)
64
+ if (this.isInitiator) {
65
+ logger.trace('Stage 0 - Initiator starting to send first message.')
66
+ const messageBuffer = this.xx.sendMessage(this.session, new Uint8Array(0))
67
+ await this.connection.write(encode0(messageBuffer))
68
+ logger.trace('Stage 0 - Initiator finished sending first message.')
69
+ logLocalEphemeralKeys(this.session.hs.e)
70
+ } else {
71
+ logger.trace('Stage 0 - Responder waiting to receive first message...')
72
+ const receivedMessageBuffer = decode0((await this.connection.read()).subarray())
73
+ const { valid } = this.xx.recvMessage(this.session, receivedMessageBuffer)
74
+ if (!valid) {
75
+ throw new InvalidCryptoExchangeError('xx handshake stage 0 validation fail')
76
+ }
77
+ logger.trace('Stage 0 - Responder received first message.')
78
+ logRemoteEphemeralKey(this.session.hs.re)
79
+ }
80
+ }
81
+
82
+ // stage 1
83
+ public async exchange (): Promise<void> {
84
+ if (this.isInitiator) {
85
+ logger.trace('Stage 1 - Initiator waiting to receive first message from responder...')
86
+ const receivedMessageBuffer = decode1((await this.connection.read()).subarray())
87
+ const { plaintext, valid } = this.xx.recvMessage(this.session, receivedMessageBuffer)
88
+ if (!valid) {
89
+ throw new InvalidCryptoExchangeError('xx handshake stage 1 validation fail')
90
+ }
91
+ logger.trace('Stage 1 - Initiator received the message.')
92
+ logRemoteEphemeralKey(this.session.hs.re)
93
+ logRemoteStaticKey(this.session.hs.rs)
94
+
95
+ logger.trace("Initiator going to check remote's signature...")
96
+ try {
97
+ const decodedPayload = decodePayload(plaintext)
98
+ this.remotePeer = this.remotePeer || await getPeerIdFromPayload(decodedPayload)
99
+ await verifySignedPayload(this.session.hs.rs, decodedPayload, this.remotePeer)
100
+ this.setRemoteNoiseExtension(decodedPayload.extensions)
101
+ } catch (e) {
102
+ const err = e as Error
103
+ throw new UnexpectedPeerError(`Error occurred while verifying signed payload: ${err.message}`)
104
+ }
105
+ logger.trace('All good with the signature!')
106
+ } else {
107
+ logger.trace('Stage 1 - Responder sending out first message with signed payload and static key.')
108
+ const messageBuffer = this.xx.sendMessage(this.session, this.payload)
109
+ await this.connection.write(encode1(messageBuffer))
110
+ logger.trace('Stage 1 - Responder sent the second handshake message with signed payload.')
111
+ logLocalEphemeralKeys(this.session.hs.e)
112
+ }
113
+ }
114
+
115
+ // stage 2
116
+ public async finish (): Promise<void> {
117
+ if (this.isInitiator) {
118
+ logger.trace('Stage 2 - Initiator sending third handshake message.')
119
+ const messageBuffer = this.xx.sendMessage(this.session, this.payload)
120
+ await this.connection.write(encode2(messageBuffer))
121
+ logger.trace('Stage 2 - Initiator sent message with signed payload.')
122
+ } else {
123
+ logger.trace('Stage 2 - Responder waiting for third handshake message...')
124
+ const receivedMessageBuffer = decode2((await this.connection.read()).subarray())
125
+ const { plaintext, valid } = this.xx.recvMessage(this.session, receivedMessageBuffer)
126
+ if (!valid) {
127
+ throw new InvalidCryptoExchangeError('xx handshake stage 2 validation fail')
128
+ }
129
+ logger.trace('Stage 2 - Responder received the message, finished handshake.')
130
+
131
+ try {
132
+ const decodedPayload = decodePayload(plaintext)
133
+ this.remotePeer = this.remotePeer || await getPeerIdFromPayload(decodedPayload)
134
+ await verifySignedPayload(this.session.hs.rs, decodedPayload, this.remotePeer)
135
+ this.setRemoteNoiseExtension(decodedPayload.extensions)
136
+ } catch (e) {
137
+ const err = e as Error
138
+ throw new UnexpectedPeerError(`Error occurred while verifying signed payload: ${err.message}`)
139
+ }
140
+ }
141
+ logCipherState(this.session)
142
+ }
143
+
144
+ public encrypt (plaintext: Uint8Array, session: NoiseSession): bytes {
145
+ const cs = this.getCS(session)
146
+
147
+ return this.xx.encryptWithAd(cs, new Uint8Array(0), plaintext)
148
+ }
149
+
150
+ public decrypt (ciphertext: Uint8Array, session: NoiseSession, dst?: Uint8Array): { plaintext: bytes, valid: boolean } {
151
+ const cs = this.getCS(session, false)
152
+
153
+ return this.xx.decryptWithAd(cs, new Uint8Array(0), ciphertext, dst)
154
+ }
155
+
156
+ public getRemoteStaticKey (): bytes {
157
+ return this.session.hs.rs
158
+ }
159
+
160
+ private getCS (session: NoiseSession, encryption = true): CipherState {
161
+ if (!session.cs1 || !session.cs2) {
162
+ throw new InvalidCryptoExchangeError('Handshake not completed properly, cipher state does not exist.')
163
+ }
164
+
165
+ if (this.isInitiator) {
166
+ return encryption ? session.cs1 : session.cs2
167
+ } else {
168
+ return encryption ? session.cs2 : session.cs1
169
+ }
170
+ }
171
+
172
+ protected setRemoteNoiseExtension (e: NoiseExtensions | null | undefined): void {
173
+ if (e) {
174
+ this.remoteExtensions = e
175
+ }
176
+ }
177
+ }
@@ -0,0 +1,180 @@
1
+ import { fromString as uint8ArrayFromString } from 'uint8arrays'
2
+ import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
3
+ import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
4
+ import { logger } from '../logger.js'
5
+ import { Nonce } from '../nonce.js'
6
+ import type { bytes, bytes32 } from '../@types/basic.js'
7
+ import type { CipherState, MessageBuffer, SymmetricState } from '../@types/handshake.js'
8
+ import type { ICryptoInterface } from '../crypto.js'
9
+
10
+ export interface DecryptedResult {
11
+ plaintext: bytes
12
+ valid: boolean
13
+ }
14
+
15
+ export interface SplitState {
16
+ cs1: CipherState
17
+ cs2: CipherState
18
+ }
19
+
20
+ export abstract class AbstractHandshake {
21
+ public crypto: ICryptoInterface
22
+
23
+ constructor (crypto: ICryptoInterface) {
24
+ this.crypto = crypto
25
+ }
26
+
27
+ public encryptWithAd (cs: CipherState, ad: Uint8Array, plaintext: Uint8Array): bytes {
28
+ const e = this.encrypt(cs.k, cs.n, ad, plaintext)
29
+ cs.n.increment()
30
+
31
+ return e
32
+ }
33
+
34
+ public decryptWithAd (cs: CipherState, ad: Uint8Array, ciphertext: Uint8Array, dst?: Uint8Array): DecryptedResult {
35
+ const { plaintext, valid } = this.decrypt(cs.k, cs.n, ad, ciphertext, dst)
36
+ if (valid) cs.n.increment()
37
+
38
+ return { plaintext, valid }
39
+ }
40
+
41
+ // Cipher state related
42
+ protected hasKey (cs: CipherState): boolean {
43
+ return !this.isEmptyKey(cs.k)
44
+ }
45
+
46
+ protected createEmptyKey (): bytes32 {
47
+ return new Uint8Array(32)
48
+ }
49
+
50
+ protected isEmptyKey (k: bytes32): boolean {
51
+ const emptyKey = this.createEmptyKey()
52
+ return uint8ArrayEquals(emptyKey, k)
53
+ }
54
+
55
+ protected encrypt (k: bytes32, n: Nonce, ad: Uint8Array, plaintext: Uint8Array): bytes {
56
+ n.assertValue()
57
+
58
+ return this.crypto.chaCha20Poly1305Encrypt(plaintext, n.getBytes(), ad, k)
59
+ }
60
+
61
+ protected encryptAndHash (ss: SymmetricState, plaintext: bytes): bytes {
62
+ let ciphertext
63
+ if (this.hasKey(ss.cs)) {
64
+ ciphertext = this.encryptWithAd(ss.cs, ss.h, plaintext)
65
+ } else {
66
+ ciphertext = plaintext
67
+ }
68
+
69
+ this.mixHash(ss, ciphertext)
70
+ return ciphertext
71
+ }
72
+
73
+ protected decrypt (k: bytes32, n: Nonce, ad: bytes, ciphertext: bytes, dst?: Uint8Array): DecryptedResult {
74
+ n.assertValue()
75
+
76
+ const encryptedMessage = this.crypto.chaCha20Poly1305Decrypt(ciphertext, n.getBytes(), ad, k, dst)
77
+
78
+ if (encryptedMessage) {
79
+ return {
80
+ plaintext: encryptedMessage,
81
+ valid: true
82
+ }
83
+ } else {
84
+ return {
85
+ plaintext: new Uint8Array(0),
86
+ valid: false
87
+ }
88
+ }
89
+ }
90
+
91
+ protected decryptAndHash (ss: SymmetricState, ciphertext: bytes): DecryptedResult {
92
+ let plaintext: bytes; let valid = true
93
+ if (this.hasKey(ss.cs)) {
94
+ ({ plaintext, valid } = this.decryptWithAd(ss.cs, ss.h, ciphertext))
95
+ } else {
96
+ plaintext = ciphertext
97
+ }
98
+
99
+ this.mixHash(ss, ciphertext)
100
+ return { plaintext, valid }
101
+ }
102
+
103
+ protected dh (privateKey: bytes32, publicKey: bytes32): bytes32 {
104
+ try {
105
+ const derivedU8 = this.crypto.generateX25519SharedKey(privateKey, publicKey)
106
+
107
+ if (derivedU8.length === 32) {
108
+ return derivedU8
109
+ }
110
+
111
+ return derivedU8.subarray(0, 32)
112
+ } catch (e) {
113
+ const err = e as Error
114
+ logger.error(err)
115
+ return new Uint8Array(32)
116
+ }
117
+ }
118
+
119
+ protected mixHash (ss: SymmetricState, data: bytes): void {
120
+ ss.h = this.getHash(ss.h, data)
121
+ }
122
+
123
+ protected getHash (a: Uint8Array, b: Uint8Array): bytes32 {
124
+ const u = this.crypto.hashSHA256(uint8ArrayConcat([a, b], a.length + b.length))
125
+ return u
126
+ }
127
+
128
+ protected mixKey (ss: SymmetricState, ikm: bytes32): void {
129
+ const [ck, tempK] = this.crypto.getHKDF(ss.ck, ikm)
130
+ ss.cs = this.initializeKey(tempK)
131
+ ss.ck = ck
132
+ }
133
+
134
+ protected initializeKey (k: bytes32): CipherState {
135
+ return { k, n: new Nonce() }
136
+ }
137
+
138
+ // Symmetric state related
139
+
140
+ protected initializeSymmetric (protocolName: string): SymmetricState {
141
+ const protocolNameBytes = uint8ArrayFromString(protocolName, 'utf-8')
142
+ const h = this.hashProtocolName(protocolNameBytes)
143
+
144
+ const ck = h
145
+ const key = this.createEmptyKey()
146
+ const cs: CipherState = this.initializeKey(key)
147
+
148
+ return { cs, ck, h }
149
+ }
150
+
151
+ protected hashProtocolName (protocolName: Uint8Array): bytes32 {
152
+ if (protocolName.length <= 32) {
153
+ const h = new Uint8Array(32)
154
+ h.set(protocolName)
155
+ return h
156
+ } else {
157
+ return this.getHash(protocolName, new Uint8Array(0))
158
+ }
159
+ }
160
+
161
+ protected split (ss: SymmetricState): SplitState {
162
+ const [tempk1, tempk2] = this.crypto.getHKDF(ss.ck, new Uint8Array(0))
163
+ const cs1 = this.initializeKey(tempk1)
164
+ const cs2 = this.initializeKey(tempk2)
165
+
166
+ return { cs1, cs2 }
167
+ }
168
+
169
+ protected writeMessageRegular (cs: CipherState, payload: bytes): MessageBuffer {
170
+ const ciphertext = this.encryptWithAd(cs, new Uint8Array(0), payload)
171
+ const ne = this.createEmptyKey()
172
+ const ns = new Uint8Array(0)
173
+
174
+ return { ne, ns, ciphertext }
175
+ }
176
+
177
+ protected readMessageRegular (cs: CipherState, message: MessageBuffer): DecryptedResult {
178
+ return this.decryptWithAd(cs, new Uint8Array(0), message.ciphertext)
179
+ }
180
+ }