@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.
- package/README.md +6 -4
- package/dist/.tsbuildinfo +1 -0
- package/dist/constants.d.ts +49 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +51 -0
- package/dist/constants.js.map +1 -0
- package/dist/crypto.d.ts +70 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +144 -0
- package/dist/crypto.js.map +1 -0
- package/dist/frame.d.ts +219 -0
- package/dist/frame.d.ts.map +1 -0
- package/dist/frame.js +554 -0
- package/dist/frame.js.map +1 -0
- package/dist/handshake.d.ts +39 -0
- package/dist/handshake.d.ts.map +1 -0
- package/dist/handshake.js +93 -0
- package/dist/handshake.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/replay.d.ts +32 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +88 -0
- package/dist/replay.js.map +1 -0
- package/dist/session.d.ts +67 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +122 -0
- package/dist/session.js.map +1 -0
- package/dist/types.d.ts +120 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +81 -0
- package/dist/types.js.map +1 -0
- package/package.json +1 -1
- package/src/constants.ts +3 -3
- package/src/crypto.test.ts +5 -5
- package/src/frame.test.ts +113 -47
- package/src/frame.ts +119 -86
- package/src/handshake.test.ts +29 -41
- package/src/handshake.ts +25 -27
- package/src/index.ts +4 -10
- package/src/integration.test.ts +97 -138
- package/src/session.test.ts +12 -10
- package/src/types.ts +4 -14
- /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 (
|
|
74
|
+
* - 0x02xx: Routing (terminal)
|
|
71
75
|
* - 0x03xx: Session (terminal)
|
|
72
76
|
* - 0x04xx: Wire format (terminal)
|
|
73
|
-
* - 0x09xx:
|
|
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) -
|
|
85
|
+
// Routing (0x02xx) - Terminal
|
|
82
86
|
DaemonNotFound: 0x0201,
|
|
83
|
-
DaemonOffline: 0x0202, //
|
|
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
|
-
//
|
|
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
|
-
/**
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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(
|
|
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
|
-
*
|
|
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.
|
|
462
|
+
if (accept.identityPublicKey.length !== ED25519_PUBLIC_KEY_LENGTH) {
|
|
448
463
|
throw new SbrpError(
|
|
449
464
|
SbrpErrorCode.MalformedFrame,
|
|
450
|
-
`
|
|
465
|
+
`identityPublicKey must be ${ED25519_PUBLIC_KEY_LENGTH} bytes, got ${accept.identityPublicKey.length}`,
|
|
451
466
|
);
|
|
452
467
|
}
|
|
453
|
-
if (accept.
|
|
468
|
+
if (accept.acceptPublicKey.length !== X25519_PUBLIC_KEY_LENGTH) {
|
|
454
469
|
throw new SbrpError(
|
|
455
470
|
SbrpErrorCode.MalformedFrame,
|
|
456
|
-
`
|
|
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.
|
|
461
|
-
payload.set(accept.
|
|
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 =
|
|
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 ?
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
621
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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
|
|
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 =
|
|
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
|
|
757
|
-
yield
|
|
758
|
-
this.buffer = this.buffer.subarray(
|
|
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
|
|
package/src/handshake.test.ts
CHANGED
|
@@ -21,14 +21,14 @@ describe("handshake", () => {
|
|
|
21
21
|
createHandshakeInit();
|
|
22
22
|
|
|
23
23
|
// Daemon processes init and creates accept
|
|
24
|
-
const { message: accept,
|
|
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
|
|
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(
|
|
40
|
-
expect(
|
|
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,
|
|
50
|
+
const { message: accept, sessionKeys: daemonKeys } = processHandshakeInit(
|
|
51
51
|
init,
|
|
52
52
|
daemonId,
|
|
53
53
|
daemonIdentity,
|
|
54
54
|
);
|
|
55
|
-
const
|
|
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(
|
|
64
|
-
|
|
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,
|
|
72
|
+
const { message: accept, sessionKeys: daemonKeys } = processHandshakeInit(
|
|
77
73
|
init,
|
|
78
74
|
daemonId,
|
|
79
75
|
daemonIdentity,
|
|
80
76
|
);
|
|
81
|
-
const
|
|
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(
|
|
90
|
-
|
|
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
|
|
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
|
|
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(
|
|
132
|
-
|
|
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 (
|
|
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.
|
|
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]
|
|
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
|
|
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,
|
|
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(
|
|
281
|
-
expect(
|
|
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,
|
|
295
|
+
const { message: accept, sessionKeys: daemonKeys } = processHandshakeInit(
|
|
308
296
|
init,
|
|
309
297
|
daemonId,
|
|
310
298
|
daemonIdentity,
|
|
311
299
|
);
|
|
312
|
-
const
|
|
300
|
+
const clientKeys = processHandshakeAccept(
|
|
313
301
|
accept,
|
|
314
302
|
daemonId,
|
|
315
303
|
daemonIdentity.publicKey,
|
|
316
304
|
clientEphemeral,
|
|
317
305
|
);
|
|
318
306
|
|
|
319
|
-
expect(
|
|
320
|
-
expect(
|
|
321
|
-
expect(
|
|
322
|
-
expect(
|
|
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
|
});
|