@sideband/secure-relay 0.2.2 → 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 (46) hide show
  1. package/README.md +6 -4
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/constants.d.ts +49 -0
  4. package/dist/constants.d.ts.map +1 -0
  5. package/dist/constants.js +51 -0
  6. package/dist/constants.js.map +1 -0
  7. package/dist/crypto.d.ts +70 -0
  8. package/dist/crypto.d.ts.map +1 -0
  9. package/dist/crypto.js +144 -0
  10. package/dist/crypto.js.map +1 -0
  11. package/dist/frame.d.ts +219 -0
  12. package/dist/frame.d.ts.map +1 -0
  13. package/dist/frame.js +554 -0
  14. package/dist/frame.js.map +1 -0
  15. package/dist/handshake.d.ts +39 -0
  16. package/dist/handshake.d.ts.map +1 -0
  17. package/dist/handshake.js +93 -0
  18. package/dist/handshake.js.map +1 -0
  19. package/dist/index.d.ts +46 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +12 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/replay.d.ts +32 -0
  24. package/dist/replay.d.ts.map +1 -0
  25. package/dist/replay.js +88 -0
  26. package/dist/replay.js.map +1 -0
  27. package/dist/session.d.ts +67 -0
  28. package/dist/session.d.ts.map +1 -0
  29. package/dist/session.js +122 -0
  30. package/dist/session.js.map +1 -0
  31. package/dist/types.d.ts +120 -0
  32. package/dist/types.d.ts.map +1 -0
  33. package/dist/types.js +81 -0
  34. package/dist/types.js.map +1 -0
  35. package/package.json +1 -1
  36. package/src/constants.ts +3 -3
  37. package/src/crypto.test.ts +5 -5
  38. package/src/frame.test.ts +113 -47
  39. package/src/frame.ts +119 -86
  40. package/src/handshake.test.ts +29 -41
  41. package/src/handshake.ts +25 -27
  42. package/src/index.ts +4 -10
  43. package/src/integration.test.ts +97 -138
  44. package/src/session.test.ts +12 -10
  45. package/src/types.ts +4 -14
  46. /package/{dist/LICENSE → LICENSE} +0 -0
package/src/frame.ts CHANGED
@@ -14,6 +14,8 @@
14
14
  */
15
15
 
16
16
  import {
17
+ ED25519_PUBLIC_KEY_LENGTH,
18
+ ED25519_SIGNATURE_LENGTH,
17
19
  FRAME_HEADER_SIZE,
18
20
  HANDSHAKE_ACCEPT_PAYLOAD_SIZE,
19
21
  HANDSHAKE_INIT_PAYLOAD_SIZE,
@@ -22,6 +24,7 @@ import {
22
24
  MIN_CONTROL_PAYLOAD_SIZE,
23
25
  MIN_ENCRYPTED_PAYLOAD_SIZE,
24
26
  SIGNAL_PAYLOAD_SIZE,
27
+ X25519_PUBLIC_KEY_LENGTH,
25
28
  } from "./constants.js";
26
29
  import { extractSequence } from "./crypto.js";
27
30
  import type {
@@ -29,10 +32,11 @@ import type {
29
32
  HandshakeAccept,
30
33
  HandshakeInit,
31
34
  SessionId,
32
- SignalCode,
33
- SignalReason,
34
35
  } from "./types.js";
35
- import { SbrpError, SbrpErrorCode } from "./types.js";
36
+ import { SbrpError, SbrpErrorCode, SignalCode, SignalReason } from "./types.js";
37
+
38
+ const textEncoder = new TextEncoder();
39
+ const textDecoder = new TextDecoder("utf-8", { fatal: false });
36
40
 
37
41
  /**
38
42
  * Frame type discriminant (wire byte).
@@ -67,10 +71,10 @@ export type FrameType = (typeof FrameType)[keyof typeof FrameType];
67
71
  *
68
72
  * Codes use ranges for categorization:
69
73
  * - 0x01xx: Authentication (terminal)
70
- * - 0x02xx: Routing (varies)
74
+ * - 0x02xx: Routing (terminal)
71
75
  * - 0x03xx: Session (terminal)
72
76
  * - 0x04xx: Wire format (terminal)
73
- * - 0x09xx: Rate limiting (non-terminal)
77
+ * - 0x09xx: Throttling (varies: rate_limited=N, backpressure=T)
74
78
  * - 0x10xx: Session state (non-terminal)
75
79
  */
76
80
  export const WireControlCode = {
@@ -78,9 +82,9 @@ export const WireControlCode = {
78
82
  Unauthorized: 0x0101,
79
83
  Forbidden: 0x0102,
80
84
 
81
- // Routing (0x02xx) - Varies
85
+ // Routing (0x02xx) - Terminal
82
86
  DaemonNotFound: 0x0201,
83
- DaemonOffline: 0x0202, // Non-terminal
87
+ DaemonOffline: 0x0202, // Terminal
84
88
 
85
89
  // Session (0x03xx) - Terminal
86
90
  SessionNotFound: 0x0301,
@@ -96,8 +100,9 @@ export const WireControlCode = {
96
100
  // Internal (0x06xx) - Terminal
97
101
  InternalError: 0x0601,
98
102
 
99
- // Rate Limiting (0x09xx) - Non-terminal
103
+ // Throttling (0x09xx) - Varies (RateLimited=N, Backpressure=T)
100
104
  RateLimited: 0x0901,
105
+ Backpressure: 0x0902,
101
106
 
102
107
  // Session State (0x10xx) - Non-terminal
103
108
  SessionPaused: 0x1001,
@@ -109,14 +114,23 @@ export const WireControlCode = {
109
114
  export type WireControlCode =
110
115
  (typeof WireControlCode)[keyof typeof WireControlCode];
111
116
 
112
- /** Check if a control code is terminal (closes connection) */
117
+ /**
118
+ * Check if a control code is terminal (relay closes WebSocket after sending).
119
+ *
120
+ * Fail-safe pattern: only enumerate non-terminal exceptions; unknown/new codes
121
+ * default to terminal so they never silently keep a session alive.
122
+ */
113
123
  export function isTerminalCode(code: WireControlCode): boolean {
114
- // Non-terminal codes: daemon_offline (0x0202), rate_limited (0x09xx), session state (0x10xx)
115
- return (
116
- code !== WireControlCode.DaemonOffline &&
117
- code !== WireControlCode.RateLimited &&
118
- code < 0x1000
119
- );
124
+ switch (code) {
125
+ case WireControlCode.RateLimited:
126
+ case WireControlCode.SessionPaused:
127
+ case WireControlCode.SessionResumed:
128
+ case WireControlCode.SessionEnded:
129
+ case WireControlCode.SessionPending:
130
+ return false;
131
+ default:
132
+ return true;
133
+ }
120
134
  }
121
135
 
122
136
  /** Decoded frame header */
@@ -170,8 +184,9 @@ const sbrpToWire: Record<string, WireControlCode> = {
170
184
  // Internal
171
185
  [SbrpErrorCode.InternalError]: WireControlCode.InternalError,
172
186
 
173
- // Rate Limiting
187
+ // Rate Limiting / Backpressure
174
188
  [SbrpErrorCode.RateLimited]: WireControlCode.RateLimited,
189
+ [SbrpErrorCode.Backpressure]: WireControlCode.Backpressure,
175
190
 
176
191
  // Session State
177
192
  [SbrpErrorCode.SessionPaused]: WireControlCode.SessionPaused,
@@ -203,8 +218,9 @@ const wireToSbrp: Record<number, SbrpErrorCode> = {
203
218
  // Internal
204
219
  [WireControlCode.InternalError]: SbrpErrorCode.InternalError,
205
220
 
206
- // Rate Limiting
221
+ // Rate Limiting / Backpressure
207
222
  [WireControlCode.RateLimited]: SbrpErrorCode.RateLimited,
223
+ [WireControlCode.Backpressure]: SbrpErrorCode.Backpressure,
208
224
 
209
225
  // Session State
210
226
  [WireControlCode.SessionPaused]: SbrpErrorCode.SessionPaused,
@@ -226,7 +242,9 @@ export function toWireControlCode(code: SbrpErrorCode): WireControlCode {
226
242
  export function fromWireControlCode(code: WireControlCode): SbrpErrorCode {
227
243
  const sbrp = wireToSbrp[code];
228
244
  if (sbrp === undefined) {
229
- throw new Error(`Unknown WireControlCode: 0x${code.toString(16)}`);
245
+ throw new Error(
246
+ `Unknown WireControlCode: 0x${code.toString(16).padStart(4, "0")}`,
247
+ );
230
248
  }
231
249
  return sbrp;
232
250
  }
@@ -332,6 +350,23 @@ export function readFrameHeader(data: Uint8Array): FrameHeader {
332
350
 
333
351
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
334
352
  const type = data[0] as FrameType;
353
+
354
+ switch (type) {
355
+ case FrameType.HandshakeInit:
356
+ case FrameType.HandshakeAccept:
357
+ case FrameType.Data:
358
+ case FrameType.Signal:
359
+ case FrameType.Ping:
360
+ case FrameType.Pong:
361
+ case FrameType.Control:
362
+ break;
363
+ default:
364
+ throw new SbrpError(
365
+ SbrpErrorCode.InvalidFrameType,
366
+ `Unknown frame type: 0x${(type as number).toString(16).padStart(2, "0")}`,
367
+ );
368
+ }
369
+
335
370
  const length = view.getUint32(1, false);
336
371
  const sessionId = view.getBigUint64(5, false);
337
372
 
@@ -391,28 +426,6 @@ export function decodeFrame(data: Uint8Array): Frame {
391
426
  return { ...header, payload };
392
427
  }
393
428
 
394
- /**
395
- * Decode frame allowing trailing bytes (for streaming use).
396
- * Returns the frame and number of bytes consumed.
397
- */
398
- function decodeFrameFromBuffer(data: Uint8Array): {
399
- frame: Frame;
400
- bytesConsumed: number;
401
- } {
402
- const header = readFrameHeader(data);
403
- const frameSize = FRAME_HEADER_SIZE + header.length;
404
-
405
- if (data.length < frameSize) {
406
- throw new SbrpError(
407
- SbrpErrorCode.MalformedFrame,
408
- `Frame truncated: got ${data.length}, expected ${frameSize}`,
409
- );
410
- }
411
-
412
- const payload = data.subarray(FRAME_HEADER_SIZE, frameSize);
413
- return { frame: { ...header, payload }, bytesConsumed: frameSize };
414
- }
415
-
416
429
  // ============================================================================
417
430
  // High-level frame encoding (typed message → binary)
418
431
  // ============================================================================
@@ -438,27 +451,39 @@ export function encodeHandshakeInit(
438
451
  /**
439
452
  * Encode HandshakeAccept to wire frame.
440
453
  *
441
- * @throws {SbrpError} if acceptPublicKey/signature have wrong sizes or sessionId is invalid
454
+ * Wire layout (128 bytes): identityPublicKey(32) + acceptPublicKey(32) + signature(64)
455
+ *
456
+ * @throws {SbrpError} if field sizes are wrong or sessionId is invalid
442
457
  */
443
458
  export function encodeHandshakeAccept(
444
459
  sessionId: SessionId,
445
460
  accept: HandshakeAccept,
446
461
  ): Uint8Array {
447
- if (accept.acceptPublicKey.length !== 32) {
462
+ if (accept.identityPublicKey.length !== ED25519_PUBLIC_KEY_LENGTH) {
448
463
  throw new SbrpError(
449
464
  SbrpErrorCode.MalformedFrame,
450
- `acceptPublicKey must be 32 bytes, got ${accept.acceptPublicKey.length}`,
465
+ `identityPublicKey must be ${ED25519_PUBLIC_KEY_LENGTH} bytes, got ${accept.identityPublicKey.length}`,
451
466
  );
452
467
  }
453
- if (accept.signature.length !== 64) {
468
+ if (accept.acceptPublicKey.length !== X25519_PUBLIC_KEY_LENGTH) {
454
469
  throw new SbrpError(
455
470
  SbrpErrorCode.MalformedFrame,
456
- `signature must be 64 bytes, got ${accept.signature.length}`,
471
+ `acceptPublicKey must be ${X25519_PUBLIC_KEY_LENGTH} bytes, got ${accept.acceptPublicKey.length}`,
472
+ );
473
+ }
474
+ if (accept.signature.length !== ED25519_SIGNATURE_LENGTH) {
475
+ throw new SbrpError(
476
+ SbrpErrorCode.MalformedFrame,
477
+ `signature must be ${ED25519_SIGNATURE_LENGTH} bytes, got ${accept.signature.length}`,
457
478
  );
458
479
  }
459
480
  const payload = new Uint8Array(HANDSHAKE_ACCEPT_PAYLOAD_SIZE);
460
- payload.set(accept.acceptPublicKey, 0);
461
- payload.set(accept.signature, 32);
481
+ payload.set(accept.identityPublicKey, 0);
482
+ payload.set(accept.acceptPublicKey, ED25519_PUBLIC_KEY_LENGTH);
483
+ payload.set(
484
+ accept.signature,
485
+ ED25519_PUBLIC_KEY_LENGTH + X25519_PUBLIC_KEY_LENGTH,
486
+ );
462
487
  return encodeFrame(FrameType.HandshakeAccept, sessionId, payload);
463
488
  }
464
489
 
@@ -490,7 +515,7 @@ export function encodeData(
490
515
  export function encodeSignal(
491
516
  sessionId: SessionId,
492
517
  signal: SignalCode,
493
- reason: SignalReason = 0x00,
518
+ reason: SignalReason = SignalReason.None,
494
519
  ): Uint8Array {
495
520
  const payload = new Uint8Array(SIGNAL_PAYLOAD_SIZE);
496
521
  payload[0] = signal;
@@ -544,7 +569,7 @@ export function encodeControl(
544
569
  code: WireControlCode,
545
570
  message?: string,
546
571
  ): Uint8Array {
547
- const msgBytes = message ? new TextEncoder().encode(message) : null;
572
+ const msgBytes = message ? textEncoder.encode(message) : null;
548
573
  const payload = new Uint8Array(2 + (msgBytes?.length ?? 0));
549
574
  new DataView(payload.buffer).setUint16(0, code, false);
550
575
  if (msgBytes) payload.set(msgBytes, 2);
@@ -555,26 +580,10 @@ export function encodeControl(
555
580
  // High-level frame decoding (Frame → typed message)
556
581
  // ============================================================================
557
582
 
558
- /** Validate sessionId for session-bound frames on decode path */
559
- function validateSessionIdOnDecode(frame: Frame): void {
560
- if (isSessionBound(frame.type) && frame.sessionId === 0n) {
561
- throw new SbrpError(
562
- SbrpErrorCode.MalformedFrame,
563
- `Session-bound frame type 0x${frame.type.toString(16).padStart(2, "0")} requires non-zero sessionId`,
564
- );
565
- }
566
- if (isConnectionScoped(frame.type) && frame.sessionId !== 0n) {
567
- throw new SbrpError(
568
- SbrpErrorCode.MalformedFrame,
569
- `Connection-scoped frame type 0x${frame.type.toString(16).padStart(2, "0")} requires sessionId = 0`,
570
- );
571
- }
572
- }
573
-
574
583
  /**
575
584
  * Decode HandshakeInit from frame.
576
585
  *
577
- * @throws {SbrpError} if frame type, payload size, or sessionId is invalid
586
+ * @throws {SbrpError} if frame type or payload size is invalid
578
587
  */
579
588
  export function decodeHandshakeInit(frame: Frame): HandshakeInit {
580
589
  if (frame.type !== FrameType.HandshakeInit) {
@@ -583,7 +592,6 @@ export function decodeHandshakeInit(frame: Frame): HandshakeInit {
583
592
  `Expected HandshakeInit (0x01), got 0x${frame.type.toString(16).padStart(2, "0")}`,
584
593
  );
585
594
  }
586
- validateSessionIdOnDecode(frame);
587
595
  if (frame.payload.length !== HANDSHAKE_INIT_PAYLOAD_SIZE) {
588
596
  throw new SbrpError(
589
597
  SbrpErrorCode.MalformedFrame,
@@ -599,7 +607,9 @@ export function decodeHandshakeInit(frame: Frame): HandshakeInit {
599
607
  /**
600
608
  * Decode HandshakeAccept from frame.
601
609
  *
602
- * @throws {SbrpError} if frame type, payload size, or sessionId is invalid
610
+ * Wire layout (128 bytes): identityPublicKey(32) + acceptPublicKey(32) + signature(64)
611
+ *
612
+ * @throws {SbrpError} if frame type or payload size is invalid
603
613
  */
604
614
  export function decodeHandshakeAccept(frame: Frame): HandshakeAccept {
605
615
  if (frame.type !== FrameType.HandshakeAccept) {
@@ -608,24 +618,25 @@ export function decodeHandshakeAccept(frame: Frame): HandshakeAccept {
608
618
  `Expected HandshakeAccept (0x02), got 0x${frame.type.toString(16).padStart(2, "0")}`,
609
619
  );
610
620
  }
611
- validateSessionIdOnDecode(frame);
612
621
  if (frame.payload.length !== HANDSHAKE_ACCEPT_PAYLOAD_SIZE) {
613
622
  throw new SbrpError(
614
623
  SbrpErrorCode.MalformedFrame,
615
624
  `HandshakeAccept payload must be ${HANDSHAKE_ACCEPT_PAYLOAD_SIZE} bytes, got ${frame.payload.length}`,
616
625
  );
617
626
  }
627
+ const keyEnd = ED25519_PUBLIC_KEY_LENGTH + X25519_PUBLIC_KEY_LENGTH;
618
628
  return {
619
629
  type: "handshake.accept",
620
- acceptPublicKey: frame.payload.slice(0, 32),
621
- signature: frame.payload.slice(32, 96),
630
+ identityPublicKey: frame.payload.slice(0, ED25519_PUBLIC_KEY_LENGTH),
631
+ acceptPublicKey: frame.payload.slice(ED25519_PUBLIC_KEY_LENGTH, keyEnd),
632
+ signature: frame.payload.slice(keyEnd, keyEnd + ED25519_SIGNATURE_LENGTH),
622
633
  };
623
634
  }
624
635
 
625
636
  /**
626
637
  * Decode Data frame (encrypted message).
627
638
  *
628
- * @throws {SbrpError} if frame type, payload, or sessionId is invalid
639
+ * @throws {SbrpError} if frame type or payload is invalid
629
640
  */
630
641
  export function decodeData(frame: Frame): EncryptedMessage {
631
642
  if (frame.type !== FrameType.Data) {
@@ -634,7 +645,6 @@ export function decodeData(frame: Frame): EncryptedMessage {
634
645
  `Expected Data (0x03), got 0x${frame.type.toString(16).padStart(2, "0")}`,
635
646
  );
636
647
  }
637
- validateSessionIdOnDecode(frame);
638
648
  if (frame.payload.length < MIN_ENCRYPTED_PAYLOAD_SIZE) {
639
649
  throw new SbrpError(
640
650
  SbrpErrorCode.MalformedFrame,
@@ -652,7 +662,7 @@ export function decodeData(frame: Frame): EncryptedMessage {
652
662
  /**
653
663
  * Decode Signal frame (daemon → relay).
654
664
  *
655
- * @throws {SbrpError} if frame type, payload size, or sessionId is invalid
665
+ * @throws {SbrpError} if frame type, payload size, or signal values are invalid
656
666
  */
657
667
  export function decodeSignal(frame: Frame): SignalPayload {
658
668
  if (frame.type !== FrameType.Signal) {
@@ -661,17 +671,35 @@ export function decodeSignal(frame: Frame): SignalPayload {
661
671
  `Expected Signal (0x04), got 0x${frame.type.toString(16).padStart(2, "0")}`,
662
672
  );
663
673
  }
664
- validateSessionIdOnDecode(frame);
665
674
  if (frame.payload.length !== SIGNAL_PAYLOAD_SIZE) {
666
675
  throw new SbrpError(
667
676
  SbrpErrorCode.MalformedFrame,
668
677
  `Signal payload must be ${SIGNAL_PAYLOAD_SIZE} bytes, got ${frame.payload.length}`,
669
678
  );
670
679
  }
671
- return {
672
- signal: frame.payload[0] as SignalCode,
673
- reason: frame.payload[1] as SignalReason,
674
- };
680
+ // Length validated above (exactly SIGNAL_PAYLOAD_SIZE = 2 bytes)
681
+ const signal = frame.payload[0]!;
682
+ const reason = frame.payload[1]!;
683
+ if (signal !== SignalCode.Ready && signal !== SignalCode.Close) {
684
+ throw new SbrpError(
685
+ SbrpErrorCode.MalformedFrame,
686
+ `Unknown signal code: 0x${signal.toString(16).padStart(2, "0")}`,
687
+ );
688
+ }
689
+ switch (reason) {
690
+ case SignalReason.None:
691
+ case SignalReason.StateLost:
692
+ case SignalReason.Shutdown:
693
+ case SignalReason.Policy:
694
+ case SignalReason.Error:
695
+ break;
696
+ default:
697
+ throw new SbrpError(
698
+ SbrpErrorCode.MalformedFrame,
699
+ `Unknown signal reason: 0x${reason.toString(16).padStart(2, "0")}`,
700
+ );
701
+ }
702
+ return { signal: signal as SignalCode, reason: reason as SignalReason };
675
703
  }
676
704
 
677
705
  /**
@@ -699,11 +727,16 @@ export function decodeControl(frame: Frame): ControlPayload {
699
727
  frame.payload.byteOffset,
700
728
  frame.payload.byteLength,
701
729
  );
702
- const code = view.getUint16(0, false) as WireControlCode;
730
+ const rawCode = view.getUint16(0, false);
731
+ if (wireToSbrp[rawCode] === undefined) {
732
+ throw new SbrpError(
733
+ SbrpErrorCode.MalformedFrame,
734
+ `Unknown control code: 0x${rawCode.toString(16).padStart(4, "0")}`,
735
+ );
736
+ }
737
+ const code = rawCode as WireControlCode;
703
738
  // TextDecoder with fatal:false replaces invalid UTF-8 with U+FFFD
704
- const message = new TextDecoder("utf-8", { fatal: false }).decode(
705
- frame.payload.subarray(2),
706
- );
739
+ const message = textDecoder.decode(frame.payload.subarray(2));
707
740
  return { code, message };
708
741
  }
709
742
 
@@ -753,9 +786,9 @@ export class FrameDecoder {
753
786
  break; // Incomplete frame, wait for more data
754
787
  }
755
788
 
756
- const { frame, bytesConsumed } = decodeFrameFromBuffer(this.buffer);
757
- yield frame;
758
- this.buffer = this.buffer.subarray(bytesConsumed);
789
+ const payload = this.buffer.subarray(FRAME_HEADER_SIZE, frameSize);
790
+ yield { ...header, payload };
791
+ this.buffer = this.buffer.subarray(frameSize);
759
792
  }
760
793
  }
761
794
 
@@ -21,14 +21,14 @@ describe("handshake", () => {
21
21
  createHandshakeInit();
22
22
 
23
23
  // Daemon processes init and creates accept
24
- const { message: accept, result: daemonResult } = processHandshakeInit(
24
+ const { message: accept, sessionKeys: daemonKeys } = processHandshakeInit(
25
25
  init,
26
26
  daemonId,
27
27
  daemonIdentity,
28
28
  );
29
29
 
30
30
  // Client processes accept
31
- const clientResult = processHandshakeAccept(
31
+ const clientKeys = processHandshakeAccept(
32
32
  accept,
33
33
  daemonId,
34
34
  daemonIdentity.publicKey,
@@ -36,8 +36,8 @@ describe("handshake", () => {
36
36
  );
37
37
 
38
38
  // Both should have valid session keys
39
- expect(clientResult.sessionKeys).toBeDefined();
40
- expect(daemonResult.sessionKeys).toBeDefined();
39
+ expect(clientKeys).toBeDefined();
40
+ expect(daemonKeys).toBeDefined();
41
41
  });
42
42
  });
43
43
 
@@ -47,12 +47,12 @@ describe("handshake", () => {
47
47
 
48
48
  const { message: init, ephemeralKeyPair: clientEphemeral } =
49
49
  createHandshakeInit();
50
- const { message: accept, result: daemonResult } = processHandshakeInit(
50
+ const { message: accept, sessionKeys: daemonKeys } = processHandshakeInit(
51
51
  init,
52
52
  daemonId,
53
53
  daemonIdentity,
54
54
  );
55
- const clientResult = processHandshakeAccept(
55
+ const clientKeys = processHandshakeAccept(
56
56
  accept,
57
57
  daemonId,
58
58
  daemonIdentity.publicKey,
@@ -60,12 +60,8 @@ describe("handshake", () => {
60
60
  );
61
61
 
62
62
  // Keys should be byte-for-byte identical
63
- expect(clientResult.sessionKeys.clientToDaemon).toEqual(
64
- daemonResult.sessionKeys.clientToDaemon,
65
- );
66
- expect(clientResult.sessionKeys.daemonToClient).toEqual(
67
- daemonResult.sessionKeys.daemonToClient,
68
- );
63
+ expect(clientKeys.clientToDaemon).toEqual(daemonKeys.clientToDaemon);
64
+ expect(clientKeys.daemonToClient).toEqual(daemonKeys.daemonToClient);
69
65
  });
70
66
 
71
67
  it("derives directional keys (clientToDaemon != daemonToClient)", () => {
@@ -73,12 +69,12 @@ describe("handshake", () => {
73
69
 
74
70
  const { message: init, ephemeralKeyPair: clientEphemeral } =
75
71
  createHandshakeInit();
76
- const { message: accept, result: daemonResult } = processHandshakeInit(
72
+ const { message: accept, sessionKeys: daemonKeys } = processHandshakeInit(
77
73
  init,
78
74
  daemonId,
79
75
  daemonIdentity,
80
76
  );
81
- const clientResult = processHandshakeAccept(
77
+ const clientKeys = processHandshakeAccept(
82
78
  accept,
83
79
  daemonId,
84
80
  daemonIdentity.publicKey,
@@ -86,12 +82,8 @@ describe("handshake", () => {
86
82
  );
87
83
 
88
84
  // Directional keys must differ to prevent reflection attacks
89
- expect(clientResult.sessionKeys.clientToDaemon).not.toEqual(
90
- clientResult.sessionKeys.daemonToClient,
91
- );
92
- expect(daemonResult.sessionKeys.clientToDaemon).not.toEqual(
93
- daemonResult.sessionKeys.daemonToClient,
94
- );
85
+ expect(clientKeys.clientToDaemon).not.toEqual(clientKeys.daemonToClient);
86
+ expect(daemonKeys.clientToDaemon).not.toEqual(daemonKeys.daemonToClient);
95
87
  });
96
88
 
97
89
  it("produces different keys for different handshakes (ephemeral randomness)", () => {
@@ -105,7 +97,7 @@ describe("handshake", () => {
105
97
  daemonId,
106
98
  daemonIdentity,
107
99
  );
108
- const result1 = processHandshakeAccept(
100
+ const keys1 = processHandshakeAccept(
109
101
  accept1,
110
102
  daemonId,
111
103
  daemonIdentity.publicKey,
@@ -120,7 +112,7 @@ describe("handshake", () => {
120
112
  daemonId,
121
113
  daemonIdentity,
122
114
  );
123
- const result2 = processHandshakeAccept(
115
+ const keys2 = processHandshakeAccept(
124
116
  accept2,
125
117
  daemonId,
126
118
  daemonIdentity.publicKey,
@@ -128,17 +120,13 @@ describe("handshake", () => {
128
120
  );
129
121
 
130
122
  // Keys from different handshakes should differ
131
- expect(result1.sessionKeys.clientToDaemon).not.toEqual(
132
- result2.sessionKeys.clientToDaemon,
133
- );
134
- expect(result1.sessionKeys.daemonToClient).not.toEqual(
135
- result2.sessionKeys.daemonToClient,
136
- );
123
+ expect(keys1.clientToDaemon).not.toEqual(keys2.clientToDaemon);
124
+ expect(keys1.daemonToClient).not.toEqual(keys2.daemonToClient);
137
125
  });
138
126
  });
139
127
 
140
128
  describe("signature verification", () => {
141
- it("fails with wrong identity key (throws SbrpError with HandshakeFailed)", () => {
129
+ it("fails with wrong identity key (IdentityKeyChanged)", () => {
142
130
  const daemonIdentity = generateIdentityKeyPair();
143
131
  const wrongIdentity = generateIdentityKeyPair();
144
132
 
@@ -169,7 +157,7 @@ describe("handshake", () => {
169
157
  );
170
158
  } catch (err) {
171
159
  expect(err).toBeInstanceOf(SbrpError);
172
- expect((err as SbrpError).code).toBe(SbrpErrorCode.HandshakeFailed);
160
+ expect((err as SbrpError).code).toBe(SbrpErrorCode.IdentityKeyChanged);
173
161
  }
174
162
  });
175
163
 
@@ -189,7 +177,7 @@ describe("handshake", () => {
189
177
  ...accept,
190
178
  signature: new Uint8Array(accept.signature),
191
179
  };
192
- tamperedAccept.signature[0] ^= 0xff; // flip bits
180
+ tamperedAccept.signature[0] = tamperedAccept.signature[0]! ^ 0xff; // flip bits
193
181
 
194
182
  expect(() =>
195
183
  processHandshakeAccept(
@@ -262,11 +250,11 @@ describe("handshake", () => {
262
250
  });
263
251
 
264
252
  describe("processHandshakeInit", () => {
265
- it("returns 64-byte signature and 32-byte ephemeral key", () => {
253
+ it("returns accept message with correct field sizes", () => {
266
254
  const daemonIdentity = generateIdentityKeyPair();
267
255
  const { message: init } = createHandshakeInit();
268
256
 
269
- const { message: accept, result } = processHandshakeInit(
257
+ const { message: accept, sessionKeys } = processHandshakeInit(
270
258
  init,
271
259
  daemonId,
272
260
  daemonIdentity,
@@ -277,8 +265,8 @@ describe("handshake", () => {
277
265
  expect(accept.acceptPublicKey.length).toBe(32);
278
266
  expect(accept.signature).toBeInstanceOf(Uint8Array);
279
267
  expect(accept.signature.length).toBe(64);
280
- expect(result.signature).toEqual(accept.signature);
281
- expect(result.ephemeralKeyPair.publicKey).toEqual(accept.acceptPublicKey);
268
+ expect(sessionKeys.clientToDaemon.length).toBe(32);
269
+ expect(sessionKeys.daemonToClient.length).toBe(32);
282
270
  });
283
271
 
284
272
  it("generates different ephemeral keys and signatures each time", () => {
@@ -304,22 +292,22 @@ describe("handshake", () => {
304
292
 
305
293
  const { message: init, ephemeralKeyPair: clientEphemeral } =
306
294
  createHandshakeInit();
307
- const { message: accept, result: daemonResult } = processHandshakeInit(
295
+ const { message: accept, sessionKeys: daemonKeys } = processHandshakeInit(
308
296
  init,
309
297
  daemonId,
310
298
  daemonIdentity,
311
299
  );
312
- const clientResult = processHandshakeAccept(
300
+ const clientKeys = processHandshakeAccept(
313
301
  accept,
314
302
  daemonId,
315
303
  daemonIdentity.publicKey,
316
304
  clientEphemeral,
317
305
  );
318
306
 
319
- expect(clientResult.sessionKeys.clientToDaemon.length).toBe(32);
320
- expect(clientResult.sessionKeys.daemonToClient.length).toBe(32);
321
- expect(daemonResult.sessionKeys.clientToDaemon.length).toBe(32);
322
- expect(daemonResult.sessionKeys.daemonToClient.length).toBe(32);
307
+ expect(clientKeys.clientToDaemon.length).toBe(32);
308
+ expect(clientKeys.daemonToClient.length).toBe(32);
309
+ expect(daemonKeys.clientToDaemon.length).toBe(32);
310
+ expect(daemonKeys.daemonToClient.length).toBe(32);
323
311
  });
324
312
  });
325
313
  });