@acpantenna/relay-protocol 0.0.0-stage → 0.3.0

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 (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +54 -2
  3. package/dist/bip39-english.d.ts +28 -0
  4. package/dist/bip39-english.d.ts.map +1 -0
  5. package/dist/bip39-english.js +28 -0
  6. package/dist/bip39-english.js.map +1 -0
  7. package/dist/bytes.d.ts +21 -0
  8. package/dist/bytes.d.ts.map +1 -0
  9. package/dist/bytes.js +114 -0
  10. package/dist/bytes.js.map +1 -0
  11. package/dist/frames.d.ts +90 -0
  12. package/dist/frames.d.ts.map +1 -0
  13. package/dist/frames.js +234 -0
  14. package/dist/frames.js.map +1 -0
  15. package/dist/index.d.ts +14 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +14 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/link.d.ts +44 -0
  20. package/dist/link.d.ts.map +1 -0
  21. package/dist/link.js +120 -0
  22. package/dist/link.js.map +1 -0
  23. package/dist/noise.d.ts +181 -0
  24. package/dist/noise.d.ts.map +1 -0
  25. package/dist/noise.js +490 -0
  26. package/dist/noise.js.map +1 -0
  27. package/dist/relay.d.ts +37 -0
  28. package/dist/relay.d.ts.map +1 -0
  29. package/dist/relay.js +70 -0
  30. package/dist/relay.js.map +1 -0
  31. package/dist/sas.d.ts +11 -0
  32. package/dist/sas.d.ts.map +1 -0
  33. package/dist/sas.js +26 -0
  34. package/dist/sas.js.map +1 -0
  35. package/dist/session.d.ts +298 -0
  36. package/dist/session.d.ts.map +1 -0
  37. package/dist/session.js +608 -0
  38. package/dist/session.js.map +1 -0
  39. package/package.json +56 -5
  40. package/src/bip39-english.ts +30 -0
  41. package/src/bytes.ts +125 -0
  42. package/src/frames.ts +278 -0
  43. package/src/index.ts +14 -0
  44. package/src/link.ts +160 -0
  45. package/src/noise.ts +655 -0
  46. package/src/relay.ts +106 -0
  47. package/src/sas.ts +28 -0
  48. package/src/session.ts +848 -0
  49. package/test-vectors/frames.json +353 -0
  50. package/test-vectors/noise-kk.json +43 -0
  51. package/test-vectors/noise-nkpsk0.json +47 -0
  52. package/test-vectors/sas.json +21 -0
  53. package/test-vectors/session.json +58 -0
package/src/noise.ts ADDED
@@ -0,0 +1,655 @@
1
+ /**
2
+ * `Noise_NKpsk0_25519_ChaChaPoly_BLAKE2s` for pairing and
3
+ * `Noise_KK_25519_ChaChaPoly_BLAKE2s` for reconnecting, following the Noise
4
+ * Protocol Framework, revision 34 (https://noiseprotocol.org/noise.html): the
5
+ * CipherState, SymmetricState and HandshakeState objects of §5, with the
6
+ * `psk` modifier of §9 and `Rekey()` of §11.3.
7
+ *
8
+ * NKpsk0:
9
+ *
10
+ * <- s
11
+ * ...
12
+ * -> psk, e, es
13
+ * <- e, ee
14
+ *
15
+ * KK:
16
+ *
17
+ * -> s
18
+ * <- s
19
+ * ...
20
+ * -> e, es, ss
21
+ * <- e, ee, se
22
+ */
23
+
24
+ import { chacha20poly1305 } from "@noble/ciphers/chacha.js";
25
+ import { x25519 } from "@noble/curves/ed25519.js";
26
+ import { blake2s } from "@noble/hashes/blake2.js";
27
+ import { hmac } from "@noble/hashes/hmac.js";
28
+ import { concatBytes, randomBytes, utf8Encode } from "./bytes.ts";
29
+
30
+ export const PROTOCOL_NAME = "Noise_NKpsk0_25519_ChaChaPoly_BLAKE2s";
31
+ export const KK_PROTOCOL_NAME = "Noise_KK_25519_ChaChaPoly_BLAKE2s";
32
+ export const DHLEN = 32;
33
+ export const HASHLEN = 32;
34
+ export const KEYLEN = 32;
35
+ export const TAGLEN = 16;
36
+ /** The largest Noise message, in bytes (§3). */
37
+ export const MAX_MESSAGE_LENGTH = 65535;
38
+
39
+ /** A decryption failed, or a message could not be processed. Always fatal. */
40
+ export class NoiseError extends Error {
41
+ constructor(message: string) {
42
+ super(message);
43
+ this.name = "NoiseError";
44
+ }
45
+ }
46
+
47
+ export interface KeyPair {
48
+ publicKey: Uint8Array;
49
+ privateKey: Uint8Array;
50
+ }
51
+
52
+ /** Makes an X25519 key pair, from `privateKey` if given. */
53
+ export function generateKeyPair(privateKey?: Uint8Array): KeyPair {
54
+ const secret = privateKey ?? randomBytes(DHLEN);
55
+ if (secret.length !== DHLEN) {
56
+ throw new NoiseError("An X25519 private key is 32 bytes");
57
+ }
58
+ return { privateKey: secret, publicKey: x25519.getPublicKey(secret) };
59
+ }
60
+
61
+ /**
62
+ * A static key whose private half this code may never see: a browser keeps
63
+ * it as a non-extractable WebCrypto key, so its DH is asynchronous.
64
+ */
65
+ export interface StaticKey {
66
+ publicKey: Uint8Array;
67
+ /** X25519 with `publicKey`; rejects (any error) for an invalid one. */
68
+ dh(publicKey: Uint8Array): Promise<Uint8Array>;
69
+ }
70
+
71
+ /** A {@link StaticKey} over a key pair held in memory. */
72
+ export function staticKey(keyPair: KeyPair): StaticKey {
73
+ return {
74
+ publicKey: keyPair.publicKey,
75
+ dh: async (publicKey) => dh(keyPair, publicKey),
76
+ };
77
+ }
78
+
79
+ async function staticDh(
80
+ key: StaticKey,
81
+ publicKey: Uint8Array,
82
+ ): Promise<Uint8Array> {
83
+ let shared: Uint8Array;
84
+ try {
85
+ shared = await key.dh(publicKey);
86
+ } catch {
87
+ throw new NoiseError("Invalid X25519 public key");
88
+ }
89
+ if (shared.length !== DHLEN || shared.every((byte) => byte === 0)) {
90
+ throw new NoiseError("Invalid X25519 public key");
91
+ }
92
+ return shared;
93
+ }
94
+
95
+ function dh(keyPair: KeyPair, publicKey: Uint8Array): Uint8Array {
96
+ try {
97
+ return x25519.getSharedSecret(keyPair.privateKey, publicKey);
98
+ } catch {
99
+ // A low-order public key gives an all-zero result, which noble refuses.
100
+ throw new NoiseError("Invalid X25519 public key");
101
+ }
102
+ }
103
+
104
+ function hash(data: Uint8Array): Uint8Array {
105
+ return blake2s(data);
106
+ }
107
+
108
+ /** HKDF from §4.3, with HMAC-BLAKE2s. */
109
+ function hkdf(
110
+ chainingKey: Uint8Array,
111
+ inputKeyMaterial: Uint8Array,
112
+ outputs: 2 | 3,
113
+ ): Uint8Array[] {
114
+ const tempKey = hmac(blake2s, chainingKey, inputKeyMaterial);
115
+ const output1 = hmac(blake2s, tempKey, Uint8Array.of(1));
116
+ const output2 = hmac(
117
+ blake2s,
118
+ tempKey,
119
+ concatBytes(output1, Uint8Array.of(2)),
120
+ );
121
+ if (outputs === 2) {
122
+ return [output1, output2];
123
+ }
124
+ const output3 = hmac(
125
+ blake2s,
126
+ tempKey,
127
+ concatBytes(output2, Uint8Array.of(3)),
128
+ );
129
+ return [output1, output2, output3];
130
+ }
131
+
132
+ /** ChaChaPoly's nonce: 32 bits of zeros, then the 64-bit counter, little-endian. */
133
+ function chachaNonce(n: number): Uint8Array {
134
+ const nonce = new Uint8Array(12);
135
+ const view = new DataView(nonce.buffer);
136
+ view.setUint32(4, n >>> 0, true);
137
+ view.setUint32(8, Math.floor(n / 2 ** 32), true);
138
+ return nonce;
139
+ }
140
+
141
+ /** 2^64 − 1, the nonce `Rekey()` uses and no message may. */
142
+ const REKEY_NONCE = Uint8Array.of(
143
+ 0,
144
+ 0,
145
+ 0,
146
+ 0,
147
+ 255,
148
+ 255,
149
+ 255,
150
+ 255,
151
+ 255,
152
+ 255,
153
+ 255,
154
+ 255,
155
+ );
156
+
157
+ /**
158
+ * A key and a nonce counter (§5.1). The counter is a JavaScript number, so it
159
+ * stops at 2^53 rather than 2^64 − 1; a session never gets near either.
160
+ */
161
+ export class CipherState {
162
+ private key: Uint8Array | undefined;
163
+ private nonce = 0;
164
+
165
+ constructor(key?: Uint8Array) {
166
+ if (key) {
167
+ this.initializeKey(key);
168
+ }
169
+ }
170
+
171
+ initializeKey(key: Uint8Array): void {
172
+ this.key = key;
173
+ this.nonce = 0;
174
+ }
175
+
176
+ hasKey(): boolean {
177
+ return this.key !== undefined;
178
+ }
179
+
180
+ /** How many messages this state has encrypted or decrypted. */
181
+ get messages(): number {
182
+ return this.nonce;
183
+ }
184
+
185
+ encryptWithAd(ad: Uint8Array, plaintext: Uint8Array): Uint8Array {
186
+ if (!this.key) {
187
+ return plaintext;
188
+ }
189
+ this.checkNonce();
190
+ const ciphertext = chacha20poly1305(
191
+ this.key,
192
+ chachaNonce(this.nonce),
193
+ ad,
194
+ ).encrypt(plaintext);
195
+ this.nonce++;
196
+ return ciphertext;
197
+ }
198
+
199
+ /** Throws a {@link NoiseError} if the tag does not verify; the nonce is then unchanged. */
200
+ decryptWithAd(ad: Uint8Array, ciphertext: Uint8Array): Uint8Array {
201
+ if (!this.key) {
202
+ return ciphertext;
203
+ }
204
+ this.checkNonce();
205
+ let plaintext: Uint8Array;
206
+ try {
207
+ plaintext = chacha20poly1305(
208
+ this.key,
209
+ chachaNonce(this.nonce),
210
+ ad,
211
+ ).decrypt(ciphertext);
212
+ } catch {
213
+ throw new NoiseError("Decryption failed");
214
+ }
215
+ this.nonce++;
216
+ return plaintext;
217
+ }
218
+
219
+ /** §11.3: replaces the key with the first 32 bytes of ENCRYPT(k, 2^64 − 1, ε, zeros). */
220
+ rekey(): void {
221
+ if (!this.key) {
222
+ throw new NoiseError("Cannot rekey a CipherState without a key");
223
+ }
224
+ this.key = chacha20poly1305(this.key, REKEY_NONCE, new Uint8Array(0))
225
+ .encrypt(new Uint8Array(KEYLEN))
226
+ .subarray(0, KEYLEN);
227
+ }
228
+
229
+ private checkNonce(): void {
230
+ if (this.nonce >= Number.MAX_SAFE_INTEGER) {
231
+ throw new NoiseError("Nonce exhausted");
232
+ }
233
+ }
234
+ }
235
+
236
+ /** The chaining key, the handshake hash and a CipherState (§5.2). */
237
+ export class SymmetricState {
238
+ readonly cipher = new CipherState();
239
+ private chainingKey: Uint8Array;
240
+ private h: Uint8Array;
241
+
242
+ constructor(protocolName: string) {
243
+ const name = utf8Encode(protocolName);
244
+ if (name.length <= HASHLEN) {
245
+ this.h = new Uint8Array(HASHLEN);
246
+ this.h.set(name);
247
+ } else {
248
+ this.h = hash(name);
249
+ }
250
+ this.chainingKey = this.h;
251
+ }
252
+
253
+ get handshakeHash(): Uint8Array {
254
+ return this.h;
255
+ }
256
+
257
+ mixKey(inputKeyMaterial: Uint8Array): void {
258
+ const [chainingKey, tempKey] = hkdf(this.chainingKey, inputKeyMaterial, 2);
259
+ this.chainingKey = chainingKey as Uint8Array;
260
+ this.cipher.initializeKey(tempKey as Uint8Array);
261
+ }
262
+
263
+ mixHash(data: Uint8Array): void {
264
+ this.h = hash(concatBytes(this.h, data));
265
+ }
266
+
267
+ mixKeyAndHash(inputKeyMaterial: Uint8Array): void {
268
+ const [chainingKey, tempHash, tempKey] = hkdf(
269
+ this.chainingKey,
270
+ inputKeyMaterial,
271
+ 3,
272
+ );
273
+ this.chainingKey = chainingKey as Uint8Array;
274
+ this.mixHash(tempHash as Uint8Array);
275
+ this.cipher.initializeKey(tempKey as Uint8Array);
276
+ }
277
+
278
+ encryptAndHash(plaintext: Uint8Array): Uint8Array {
279
+ const ciphertext = this.cipher.encryptWithAd(this.h, plaintext);
280
+ this.mixHash(ciphertext);
281
+ return ciphertext;
282
+ }
283
+
284
+ decryptAndHash(ciphertext: Uint8Array): Uint8Array {
285
+ const plaintext = this.cipher.decryptWithAd(this.h, ciphertext);
286
+ this.mixHash(ciphertext);
287
+ return plaintext;
288
+ }
289
+
290
+ /** Returns the initiator's sending and receiving CipherStates, in that order. */
291
+ split(): [CipherState, CipherState] {
292
+ const [key1, key2] = hkdf(this.chainingKey, new Uint8Array(0), 2);
293
+ return [
294
+ new CipherState(key1 as Uint8Array),
295
+ new CipherState(key2 as Uint8Array),
296
+ ];
297
+ }
298
+ }
299
+
300
+ export interface HandshakeOptions {
301
+ prologue: Uint8Array;
302
+ psk: Uint8Array;
303
+ /** Fixes the ephemeral key, for test vectors only. */
304
+ ephemeralPrivateKey?: Uint8Array;
305
+ }
306
+
307
+ export interface InitiatorHandshakeOptions extends HandshakeOptions {
308
+ /** The responder's static public key, known in advance (`<- s`). */
309
+ remoteStaticPublicKey: Uint8Array;
310
+ }
311
+
312
+ export interface ResponderHandshakeOptions extends HandshakeOptions {
313
+ staticKeyPair: KeyPair;
314
+ }
315
+
316
+ /** The result of a finished handshake. */
317
+ export interface HandshakeResult {
318
+ /** Encrypts what this side sends. */
319
+ send: CipherState;
320
+ /** Decrypts what this side receives. */
321
+ receive: CipherState;
322
+ /** The final handshake hash `h`. */
323
+ handshakeHash: Uint8Array;
324
+ /** The other side's ephemeral public key. */
325
+ remoteEphemeralPublicKey: Uint8Array;
326
+ }
327
+
328
+ /**
329
+ * The NKpsk0 HandshakeState (§5.3), for one side. Call `writeMessage` and
330
+ * `readMessage` in turn: the initiator writes message 1 and reads message 2,
331
+ * the responder the other way round. After message 2 `result` is set.
332
+ */
333
+ export class HandshakeState {
334
+ readonly initiator: boolean;
335
+ private readonly symmetric = new SymmetricState(PROTOCOL_NAME);
336
+ private readonly psk: Uint8Array;
337
+ private readonly staticKeyPair: KeyPair | undefined;
338
+ private readonly remoteStatic: Uint8Array;
339
+ private readonly fixedEphemeral: Uint8Array | undefined;
340
+ private ephemeral: KeyPair | undefined;
341
+ private remoteEphemeral: Uint8Array | undefined;
342
+ private step = 0;
343
+ result: HandshakeResult | undefined;
344
+
345
+ private constructor(
346
+ initiator: boolean,
347
+ options: HandshakeOptions & {
348
+ staticKeyPair?: KeyPair;
349
+ remoteStaticPublicKey: Uint8Array;
350
+ },
351
+ ) {
352
+ if (options.psk.length !== 32) {
353
+ throw new NoiseError("The psk is 32 bytes");
354
+ }
355
+ if (options.remoteStaticPublicKey.length !== DHLEN) {
356
+ throw new NoiseError("The responder's static key is 32 bytes");
357
+ }
358
+ this.initiator = initiator;
359
+ this.psk = options.psk;
360
+ this.staticKeyPair = options.staticKeyPair;
361
+ this.remoteStatic = options.remoteStaticPublicKey;
362
+ this.fixedEphemeral = options.ephemeralPrivateKey;
363
+ this.symmetric.mixHash(options.prologue);
364
+ // The pre-message `<- s`: both sides hash the responder's static key.
365
+ this.symmetric.mixHash(this.remoteStatic);
366
+ }
367
+
368
+ static initiator(options: InitiatorHandshakeOptions): HandshakeState {
369
+ return new HandshakeState(true, options);
370
+ }
371
+
372
+ static responder(options: ResponderHandshakeOptions): HandshakeState {
373
+ return new HandshakeState(false, {
374
+ ...options,
375
+ remoteStaticPublicKey: options.staticKeyPair.publicKey,
376
+ });
377
+ }
378
+
379
+ get handshakeHash(): Uint8Array {
380
+ return this.symmetric.handshakeHash;
381
+ }
382
+
383
+ /** Writes the next handshake message, carrying `payload`. */
384
+ writeMessage(payload: Uint8Array = new Uint8Array(0)): Uint8Array {
385
+ if (this.step === 0 && this.initiator) {
386
+ // -> psk, e, es
387
+ this.symmetric.mixKeyAndHash(this.psk);
388
+ const e = this.writeEphemeral();
389
+ this.symmetric.mixKey(dh(this.ephemeralKeyPair(), this.remoteStatic));
390
+ const message = concatBytes(e, this.symmetric.encryptAndHash(payload));
391
+ this.step = 1;
392
+ return this.checkLength(message);
393
+ }
394
+ if (this.step === 1 && !this.initiator) {
395
+ // <- e, ee
396
+ const e = this.writeEphemeral();
397
+ this.symmetric.mixKey(
398
+ dh(this.ephemeralKeyPair(), this.remoteEphemeralKey()),
399
+ );
400
+ const message = concatBytes(e, this.symmetric.encryptAndHash(payload));
401
+ this.finish();
402
+ return this.checkLength(message);
403
+ }
404
+ throw new NoiseError("Not this side's turn to write");
405
+ }
406
+
407
+ /** Reads the next handshake message and returns its payload. */
408
+ readMessage(message: Uint8Array): Uint8Array {
409
+ if (message.length > MAX_MESSAGE_LENGTH) {
410
+ throw new NoiseError("Handshake message too long");
411
+ }
412
+ if (message.length < DHLEN + TAGLEN) {
413
+ throw new NoiseError("Handshake message too short");
414
+ }
415
+ const e = message.subarray(0, DHLEN);
416
+ const rest = message.subarray(DHLEN);
417
+ if (this.step === 0 && !this.initiator) {
418
+ // -> psk, e, es
419
+ this.symmetric.mixKeyAndHash(this.psk);
420
+ this.readEphemeral(e);
421
+ this.symmetric.mixKey(dh(this.staticKeys(), this.remoteEphemeralKey()));
422
+ const payload = this.symmetric.decryptAndHash(rest);
423
+ this.step = 1;
424
+ return payload;
425
+ }
426
+ if (this.step === 1 && this.initiator) {
427
+ // <- e, ee
428
+ this.readEphemeral(e);
429
+ this.symmetric.mixKey(
430
+ dh(this.ephemeralKeyPair(), this.remoteEphemeralKey()),
431
+ );
432
+ const payload = this.symmetric.decryptAndHash(rest);
433
+ this.finish();
434
+ return payload;
435
+ }
436
+ throw new NoiseError("Not this side's turn to read");
437
+ }
438
+
439
+ private writeEphemeral(): Uint8Array {
440
+ this.ephemeral = generateKeyPair(this.fixedEphemeral);
441
+ this.symmetric.mixHash(this.ephemeral.publicKey);
442
+ // The psk modifier (§9.2): `e` also calls MixKey.
443
+ this.symmetric.mixKey(this.ephemeral.publicKey);
444
+ return this.ephemeral.publicKey;
445
+ }
446
+
447
+ private readEphemeral(e: Uint8Array): void {
448
+ this.remoteEphemeral = e.slice();
449
+ this.symmetric.mixHash(this.remoteEphemeral);
450
+ this.symmetric.mixKey(this.remoteEphemeral);
451
+ }
452
+
453
+ private ephemeralKeyPair(): KeyPair {
454
+ if (!this.ephemeral) {
455
+ throw new NoiseError("No ephemeral key");
456
+ }
457
+ return this.ephemeral;
458
+ }
459
+
460
+ private remoteEphemeralKey(): Uint8Array {
461
+ if (!this.remoteEphemeral) {
462
+ throw new NoiseError("No remote ephemeral key");
463
+ }
464
+ return this.remoteEphemeral;
465
+ }
466
+
467
+ private staticKeys(): KeyPair {
468
+ if (!this.staticKeyPair) {
469
+ throw new NoiseError("No static key");
470
+ }
471
+ return this.staticKeyPair;
472
+ }
473
+
474
+ private finish(): void {
475
+ const [initiatorSend, responderSend] = this.symmetric.split();
476
+ this.result = {
477
+ send: this.initiator ? initiatorSend : responderSend,
478
+ receive: this.initiator ? responderSend : initiatorSend,
479
+ handshakeHash: this.symmetric.handshakeHash,
480
+ remoteEphemeralPublicKey: this.remoteEphemeralKey(),
481
+ };
482
+ this.step = 2;
483
+ }
484
+
485
+ private checkLength(message: Uint8Array): Uint8Array {
486
+ if (message.length > MAX_MESSAGE_LENGTH) {
487
+ throw new NoiseError("Handshake message too long");
488
+ }
489
+ return message;
490
+ }
491
+ }
492
+
493
+ export interface KKHandshakeOptions {
494
+ prologue: Uint8Array;
495
+ /** This side's static key. */
496
+ staticKey: StaticKey;
497
+ /** The other side's static public key, known in advance. */
498
+ remoteStaticPublicKey: Uint8Array;
499
+ /** Fixes the ephemeral key, for test vectors only. */
500
+ ephemeralPrivateKey?: Uint8Array;
501
+ }
502
+
503
+ /**
504
+ * The KK HandshakeState (§5.3), for one side: both static keys are known in
505
+ * advance, so it needs no psk. As with {@link HandshakeState}, the initiator
506
+ * writes message 1 and reads message 2. Its steps are asynchronous because a
507
+ * {@link StaticKey}'s DH is.
508
+ */
509
+ export class KKHandshakeState {
510
+ readonly initiator: boolean;
511
+ private readonly symmetric = new SymmetricState(KK_PROTOCOL_NAME);
512
+ private readonly staticKey: StaticKey;
513
+ private readonly remoteStatic: Uint8Array;
514
+ private readonly fixedEphemeral: Uint8Array | undefined;
515
+ private ephemeral: KeyPair | undefined;
516
+ private remoteEphemeral: Uint8Array | undefined;
517
+ private step = 0;
518
+ result: HandshakeResult | undefined;
519
+
520
+ private constructor(initiator: boolean, options: KKHandshakeOptions) {
521
+ if (
522
+ options.remoteStaticPublicKey.length !== DHLEN ||
523
+ options.staticKey.publicKey.length !== DHLEN
524
+ ) {
525
+ throw new NoiseError("A static key is 32 bytes");
526
+ }
527
+ this.initiator = initiator;
528
+ this.staticKey = options.staticKey;
529
+ this.remoteStatic = options.remoteStaticPublicKey;
530
+ this.fixedEphemeral = options.ephemeralPrivateKey;
531
+ this.symmetric.mixHash(options.prologue);
532
+ // The pre-messages `-> s` and `<- s`: the initiator's key is hashed first.
533
+ const [initiatorStatic, responderStatic] = initiator
534
+ ? [this.staticKey.publicKey, this.remoteStatic]
535
+ : [this.remoteStatic, this.staticKey.publicKey];
536
+ this.symmetric.mixHash(initiatorStatic);
537
+ this.symmetric.mixHash(responderStatic);
538
+ }
539
+
540
+ static initiator(options: KKHandshakeOptions): KKHandshakeState {
541
+ return new KKHandshakeState(true, options);
542
+ }
543
+
544
+ static responder(options: KKHandshakeOptions): KKHandshakeState {
545
+ return new KKHandshakeState(false, options);
546
+ }
547
+
548
+ get handshakeHash(): Uint8Array {
549
+ return this.symmetric.handshakeHash;
550
+ }
551
+
552
+ /** Writes the next handshake message, carrying `payload`. */
553
+ async writeMessage(
554
+ payload: Uint8Array = new Uint8Array(0),
555
+ ): Promise<Uint8Array> {
556
+ if (this.step === 0 && this.initiator) {
557
+ // -> e, es, ss
558
+ this.step = -1;
559
+ const e = this.writeEphemeral();
560
+ this.symmetric.mixKey(dh(this.ephemeralKeyPair(), this.remoteStatic));
561
+ this.symmetric.mixKey(await staticDh(this.staticKey, this.remoteStatic));
562
+ const message = concatBytes(e, this.symmetric.encryptAndHash(payload));
563
+ this.step = 1;
564
+ return message;
565
+ }
566
+ if (this.step === 1 && !this.initiator) {
567
+ // <- e, ee, se
568
+ const e = this.writeEphemeral();
569
+ this.symmetric.mixKey(
570
+ dh(this.ephemeralKeyPair(), this.remoteEphemeralKey()),
571
+ );
572
+ this.symmetric.mixKey(dh(this.ephemeralKeyPair(), this.remoteStatic));
573
+ const message = concatBytes(e, this.symmetric.encryptAndHash(payload));
574
+ this.finish();
575
+ return message;
576
+ }
577
+ throw new NoiseError("Not this side's turn to write");
578
+ }
579
+
580
+ /** Reads the next handshake message and returns its payload. */
581
+ async readMessage(message: Uint8Array): Promise<Uint8Array> {
582
+ if (message.length > MAX_MESSAGE_LENGTH) {
583
+ throw new NoiseError("Handshake message too long");
584
+ }
585
+ if (message.length < DHLEN + TAGLEN) {
586
+ throw new NoiseError("Handshake message too short");
587
+ }
588
+ const e = message.subarray(0, DHLEN);
589
+ const rest = message.subarray(DHLEN);
590
+ if (this.step === 0 && !this.initiator) {
591
+ // -> e, es, ss
592
+ this.step = -1;
593
+ this.readEphemeral(e);
594
+ this.symmetric.mixKey(
595
+ await staticDh(this.staticKey, this.remoteEphemeralKey()),
596
+ );
597
+ this.symmetric.mixKey(await staticDh(this.staticKey, this.remoteStatic));
598
+ const payload = this.symmetric.decryptAndHash(rest);
599
+ this.step = 1;
600
+ return payload;
601
+ }
602
+ if (this.step === 1 && this.initiator) {
603
+ // <- e, ee, se
604
+ this.step = -1;
605
+ this.readEphemeral(e);
606
+ this.symmetric.mixKey(
607
+ dh(this.ephemeralKeyPair(), this.remoteEphemeralKey()),
608
+ );
609
+ this.symmetric.mixKey(
610
+ await staticDh(this.staticKey, this.remoteEphemeralKey()),
611
+ );
612
+ const payload = this.symmetric.decryptAndHash(rest);
613
+ this.finish();
614
+ return payload;
615
+ }
616
+ throw new NoiseError("Not this side's turn to read");
617
+ }
618
+
619
+ private writeEphemeral(): Uint8Array {
620
+ this.ephemeral = generateKeyPair(this.fixedEphemeral);
621
+ // No psk, so `e` is only hashed.
622
+ this.symmetric.mixHash(this.ephemeral.publicKey);
623
+ return this.ephemeral.publicKey;
624
+ }
625
+
626
+ private readEphemeral(e: Uint8Array): void {
627
+ this.remoteEphemeral = e.slice();
628
+ this.symmetric.mixHash(this.remoteEphemeral);
629
+ }
630
+
631
+ private ephemeralKeyPair(): KeyPair {
632
+ if (!this.ephemeral) {
633
+ throw new NoiseError("No ephemeral key");
634
+ }
635
+ return this.ephemeral;
636
+ }
637
+
638
+ private remoteEphemeralKey(): Uint8Array {
639
+ if (!this.remoteEphemeral) {
640
+ throw new NoiseError("No remote ephemeral key");
641
+ }
642
+ return this.remoteEphemeral;
643
+ }
644
+
645
+ private finish(): void {
646
+ const [initiatorSend, responderSend] = this.symmetric.split();
647
+ this.result = {
648
+ send: this.initiator ? initiatorSend : responderSend,
649
+ receive: this.initiator ? responderSend : initiatorSend,
650
+ handshakeHash: this.symmetric.handshakeHash,
651
+ remoteEphemeralPublicKey: this.remoteEphemeralKey(),
652
+ };
653
+ this.step = 2;
654
+ }
655
+ }