@sideband/secure-relay 0.1.0 → 0.2.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.
- package/README.md +70 -11
- package/dist/LICENSE +190 -694
- package/package.json +1 -4
- package/src/constants.ts +29 -4
- package/src/crypto.test.ts +644 -0
- package/src/crypto.ts +1 -2
- package/src/frame.test.ts +820 -0
- package/src/frame.ts +771 -0
- package/src/handshake.test.ts +325 -0
- package/src/handshake.ts +7 -2
- package/src/index.ts +44 -2
- package/src/integration.test.ts +1025 -0
- package/src/replay.test.ts +306 -0
- package/src/replay.ts +1 -2
- package/src/session.test.ts +767 -0
- package/src/session.ts +1 -2
- package/src/types.ts +85 -18
- package/LICENSE +0 -190
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "bun:test";
|
|
4
|
+
import { DEFAULT_REPLAY_WINDOW_SIZE } from "./constants.js";
|
|
5
|
+
import {
|
|
6
|
+
checkAndUpdateReplay,
|
|
7
|
+
createReplayWindow,
|
|
8
|
+
isValidSequence,
|
|
9
|
+
resetReplayWindow,
|
|
10
|
+
} from "./replay.js";
|
|
11
|
+
|
|
12
|
+
describe("replay protection", () => {
|
|
13
|
+
describe("createReplayWindow", () => {
|
|
14
|
+
it("creates window with default size", () => {
|
|
15
|
+
const window = createReplayWindow();
|
|
16
|
+
expect(window.maxSeen).toBe(-1n);
|
|
17
|
+
expect(window.bitmap).toBe(0n);
|
|
18
|
+
expect(window.windowSize).toBe(DEFAULT_REPLAY_WINDOW_SIZE);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("creates window with custom size", () => {
|
|
22
|
+
const window = createReplayWindow(128n);
|
|
23
|
+
expect(window.windowSize).toBe(128n);
|
|
24
|
+
expect(window.maxSeen).toBe(-1n);
|
|
25
|
+
expect(window.bitmap).toBe(0n);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("checkAndUpdateReplay", () => {
|
|
30
|
+
it("accepts first message at seq=0", () => {
|
|
31
|
+
const window = createReplayWindow();
|
|
32
|
+
expect(checkAndUpdateReplay(0n, window)).toBe(true);
|
|
33
|
+
expect(window.maxSeen).toBe(0n);
|
|
34
|
+
expect(window.bitmap).toBe(1n);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("accepts sequential messages 0,1,2,3", () => {
|
|
38
|
+
const window = createReplayWindow();
|
|
39
|
+
expect(checkAndUpdateReplay(0n, window)).toBe(true);
|
|
40
|
+
expect(checkAndUpdateReplay(1n, window)).toBe(true);
|
|
41
|
+
expect(checkAndUpdateReplay(2n, window)).toBe(true);
|
|
42
|
+
expect(checkAndUpdateReplay(3n, window)).toBe(true);
|
|
43
|
+
|
|
44
|
+
expect(window.maxSeen).toBe(3n);
|
|
45
|
+
// Bitmap should have bits 0,1,2,3 set (counting from maxSeen backwards)
|
|
46
|
+
// bit 0 = seq 3, bit 1 = seq 2, bit 2 = seq 1, bit 3 = seq 0
|
|
47
|
+
expect(window.bitmap).toBe(0b1111n);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("rejects duplicate sequence (replay attack)", () => {
|
|
51
|
+
const window = createReplayWindow();
|
|
52
|
+
expect(checkAndUpdateReplay(5n, window)).toBe(true);
|
|
53
|
+
expect(checkAndUpdateReplay(5n, window)).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("rejects multiple replays of same sequence", () => {
|
|
57
|
+
const window = createReplayWindow();
|
|
58
|
+
expect(checkAndUpdateReplay(10n, window)).toBe(true);
|
|
59
|
+
expect(checkAndUpdateReplay(10n, window)).toBe(false);
|
|
60
|
+
expect(checkAndUpdateReplay(10n, window)).toBe(false);
|
|
61
|
+
expect(checkAndUpdateReplay(10n, window)).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("accepts out-of-order within window", () => {
|
|
65
|
+
const window = createReplayWindow();
|
|
66
|
+
expect(checkAndUpdateReplay(5n, window)).toBe(true);
|
|
67
|
+
expect(checkAndUpdateReplay(3n, window)).toBe(true); // Out of order
|
|
68
|
+
expect(checkAndUpdateReplay(4n, window)).toBe(true); // Out of order
|
|
69
|
+
expect(checkAndUpdateReplay(2n, window)).toBe(true); // Out of order
|
|
70
|
+
|
|
71
|
+
expect(window.maxSeen).toBe(5n);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("rejects sequence too old (outside window)", () => {
|
|
75
|
+
const window = createReplayWindow(64n);
|
|
76
|
+
expect(checkAndUpdateReplay(100n, window)).toBe(true);
|
|
77
|
+
// Sequence 36 is exactly at windowSize boundary (100 - 64 = 36)
|
|
78
|
+
expect(checkAndUpdateReplay(36n, window)).toBe(false);
|
|
79
|
+
// Sequence 35 is outside window
|
|
80
|
+
expect(checkAndUpdateReplay(35n, window)).toBe(false);
|
|
81
|
+
expect(checkAndUpdateReplay(0n, window)).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("accepts sequence at exact boundary (maxSeen - windowSize + 1)", () => {
|
|
85
|
+
const window = createReplayWindow(64n);
|
|
86
|
+
expect(checkAndUpdateReplay(100n, window)).toBe(true);
|
|
87
|
+
// Sequence 37 is exactly at boundary (100 - 64 + 1 = 37)
|
|
88
|
+
expect(checkAndUpdateReplay(37n, window)).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("resets bitmap on large jump ahead", () => {
|
|
92
|
+
const window = createReplayWindow(64n);
|
|
93
|
+
expect(checkAndUpdateReplay(0n, window)).toBe(true);
|
|
94
|
+
expect(checkAndUpdateReplay(1n, window)).toBe(true);
|
|
95
|
+
|
|
96
|
+
// Jump far ahead (beyond window size)
|
|
97
|
+
expect(checkAndUpdateReplay(1000n, window)).toBe(true);
|
|
98
|
+
expect(window.maxSeen).toBe(1000n);
|
|
99
|
+
expect(window.bitmap).toBe(1n); // Reset to just the new sequence
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("rejects old sequences after large jump", () => {
|
|
103
|
+
const window = createReplayWindow(64n);
|
|
104
|
+
expect(checkAndUpdateReplay(0n, window)).toBe(true);
|
|
105
|
+
expect(checkAndUpdateReplay(1000n, window)).toBe(true);
|
|
106
|
+
|
|
107
|
+
// Old sequences should be rejected
|
|
108
|
+
expect(checkAndUpdateReplay(0n, window)).toBe(false);
|
|
109
|
+
expect(checkAndUpdateReplay(1n, window)).toBe(false);
|
|
110
|
+
expect(checkAndUpdateReplay(935n, window)).toBe(false); // 1000 - 65
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("tracks bitmap correctly for interleaved sequences", () => {
|
|
114
|
+
const window = createReplayWindow();
|
|
115
|
+
// Receive: 0, 2, 4, 6
|
|
116
|
+
expect(checkAndUpdateReplay(0n, window)).toBe(true);
|
|
117
|
+
expect(checkAndUpdateReplay(2n, window)).toBe(true);
|
|
118
|
+
expect(checkAndUpdateReplay(4n, window)).toBe(true);
|
|
119
|
+
expect(checkAndUpdateReplay(6n, window)).toBe(true);
|
|
120
|
+
|
|
121
|
+
// Now fill in gaps: 1, 3, 5
|
|
122
|
+
expect(checkAndUpdateReplay(1n, window)).toBe(true);
|
|
123
|
+
expect(checkAndUpdateReplay(3n, window)).toBe(true);
|
|
124
|
+
expect(checkAndUpdateReplay(5n, window)).toBe(true);
|
|
125
|
+
|
|
126
|
+
// All should be marked as seen now
|
|
127
|
+
expect(window.maxSeen).toBe(6n);
|
|
128
|
+
// Bitmap: bits 0-6 set = 0b1111111 = 127
|
|
129
|
+
expect(window.bitmap).toBe(0b1111111n);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("handles first message with high sequence number", () => {
|
|
133
|
+
const window = createReplayWindow();
|
|
134
|
+
expect(checkAndUpdateReplay(1000000n, window)).toBe(true);
|
|
135
|
+
expect(window.maxSeen).toBe(1000000n);
|
|
136
|
+
expect(window.bitmap).toBe(1n);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("isValidSequence", () => {
|
|
141
|
+
it("does not modify window state", () => {
|
|
142
|
+
const window = createReplayWindow();
|
|
143
|
+
checkAndUpdateReplay(10n, window);
|
|
144
|
+
|
|
145
|
+
const originalMaxSeen = window.maxSeen;
|
|
146
|
+
const originalBitmap = window.bitmap;
|
|
147
|
+
|
|
148
|
+
// Check validity without updating
|
|
149
|
+
expect(isValidSequence(5n, window)).toBe(true);
|
|
150
|
+
expect(isValidSequence(15n, window)).toBe(true);
|
|
151
|
+
expect(isValidSequence(10n, window)).toBe(false); // Already seen
|
|
152
|
+
|
|
153
|
+
// State should be unchanged
|
|
154
|
+
expect(window.maxSeen).toBe(originalMaxSeen);
|
|
155
|
+
expect(window.bitmap).toBe(originalBitmap);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("returns true for empty window", () => {
|
|
159
|
+
const window = createReplayWindow();
|
|
160
|
+
expect(isValidSequence(0n, window)).toBe(true);
|
|
161
|
+
expect(isValidSequence(100n, window)).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("returns true for sequence ahead of maxSeen", () => {
|
|
165
|
+
const window = createReplayWindow();
|
|
166
|
+
checkAndUpdateReplay(10n, window);
|
|
167
|
+
expect(isValidSequence(11n, window)).toBe(true);
|
|
168
|
+
expect(isValidSequence(100n, window)).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("returns false for sequence outside window", () => {
|
|
172
|
+
const window = createReplayWindow(64n);
|
|
173
|
+
checkAndUpdateReplay(100n, window);
|
|
174
|
+
expect(isValidSequence(35n, window)).toBe(false);
|
|
175
|
+
expect(isValidSequence(0n, window)).toBe(false);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("returns false for already seen sequence", () => {
|
|
179
|
+
const window = createReplayWindow();
|
|
180
|
+
checkAndUpdateReplay(5n, window);
|
|
181
|
+
checkAndUpdateReplay(3n, window);
|
|
182
|
+
|
|
183
|
+
expect(isValidSequence(5n, window)).toBe(false);
|
|
184
|
+
expect(isValidSequence(3n, window)).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("returns true for unseen sequence within window", () => {
|
|
188
|
+
const window = createReplayWindow();
|
|
189
|
+
checkAndUpdateReplay(10n, window);
|
|
190
|
+
|
|
191
|
+
// Sequences 0-9 are within window and unseen
|
|
192
|
+
expect(isValidSequence(0n, window)).toBe(true);
|
|
193
|
+
expect(isValidSequence(5n, window)).toBe(true);
|
|
194
|
+
expect(isValidSequence(9n, window)).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("resetReplayWindow", () => {
|
|
199
|
+
it("clears window state", () => {
|
|
200
|
+
const window = createReplayWindow();
|
|
201
|
+
checkAndUpdateReplay(100n, window);
|
|
202
|
+
checkAndUpdateReplay(50n, window);
|
|
203
|
+
|
|
204
|
+
resetReplayWindow(window);
|
|
205
|
+
|
|
206
|
+
expect(window.maxSeen).toBe(-1n);
|
|
207
|
+
expect(window.bitmap).toBe(0n);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("preserves window size", () => {
|
|
211
|
+
const window = createReplayWindow(128n);
|
|
212
|
+
checkAndUpdateReplay(100n, window);
|
|
213
|
+
|
|
214
|
+
resetReplayWindow(window);
|
|
215
|
+
|
|
216
|
+
expect(window.windowSize).toBe(128n);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("allows accepting same sequences after reset", () => {
|
|
220
|
+
const window = createReplayWindow();
|
|
221
|
+
checkAndUpdateReplay(5n, window);
|
|
222
|
+
expect(checkAndUpdateReplay(5n, window)).toBe(false);
|
|
223
|
+
|
|
224
|
+
resetReplayWindow(window);
|
|
225
|
+
|
|
226
|
+
expect(checkAndUpdateReplay(5n, window)).toBe(true);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("window size variations", () => {
|
|
231
|
+
it("works with small window (8 bits)", () => {
|
|
232
|
+
const window = createReplayWindow(8n);
|
|
233
|
+
expect(checkAndUpdateReplay(0n, window)).toBe(true);
|
|
234
|
+
expect(checkAndUpdateReplay(10n, window)).toBe(true);
|
|
235
|
+
|
|
236
|
+
// Sequence 2 is within window (10 - 8 + 1 = 3, so 2 is outside)
|
|
237
|
+
expect(checkAndUpdateReplay(2n, window)).toBe(false);
|
|
238
|
+
// Sequence 3 is at boundary
|
|
239
|
+
expect(checkAndUpdateReplay(3n, window)).toBe(true);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("works with large window (256 bits)", () => {
|
|
243
|
+
const window = createReplayWindow(256n);
|
|
244
|
+
expect(checkAndUpdateReplay(300n, window)).toBe(true);
|
|
245
|
+
|
|
246
|
+
// Sequence 45 is within window (300 - 256 + 1 = 45)
|
|
247
|
+
expect(checkAndUpdateReplay(45n, window)).toBe(true);
|
|
248
|
+
// Sequence 44 is outside
|
|
249
|
+
expect(checkAndUpdateReplay(44n, window)).toBe(false);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe("edge cases", () => {
|
|
254
|
+
it("handles maxSeen=-1n correctly (first message ever)", () => {
|
|
255
|
+
const window = createReplayWindow();
|
|
256
|
+
expect(window.maxSeen).toBe(-1n);
|
|
257
|
+
|
|
258
|
+
// Any first sequence should be accepted
|
|
259
|
+
expect(isValidSequence(0n, window)).toBe(true);
|
|
260
|
+
expect(isValidSequence(1000n, window)).toBe(true);
|
|
261
|
+
|
|
262
|
+
expect(checkAndUpdateReplay(42n, window)).toBe(true);
|
|
263
|
+
expect(window.maxSeen).toBe(42n);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("handles shift exactly equal to window size", () => {
|
|
267
|
+
const window = createReplayWindow(64n);
|
|
268
|
+
expect(checkAndUpdateReplay(0n, window)).toBe(true);
|
|
269
|
+
// Jump exactly windowSize ahead
|
|
270
|
+
expect(checkAndUpdateReplay(64n, window)).toBe(true);
|
|
271
|
+
expect(window.bitmap).toBe(1n); // Reset
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("handles shift one less than window size", () => {
|
|
275
|
+
const window = createReplayWindow(64n);
|
|
276
|
+
expect(checkAndUpdateReplay(0n, window)).toBe(true);
|
|
277
|
+
// Jump windowSize - 1 ahead (shift within range)
|
|
278
|
+
expect(checkAndUpdateReplay(63n, window)).toBe(true);
|
|
279
|
+
// Bitmap should have both bits set
|
|
280
|
+
// bit 0 = seq 63 (current), bit 63 = seq 0
|
|
281
|
+
expect(window.bitmap & 1n).toBe(1n); // Sequence 63
|
|
282
|
+
expect(window.bitmap & (1n << 63n)).toBe(1n << 63n); // Sequence 0
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("bitmap correctly handles sequences near boundaries", () => {
|
|
286
|
+
const window = createReplayWindow(64n);
|
|
287
|
+
|
|
288
|
+
// Set up window at maxSeen=63
|
|
289
|
+
expect(checkAndUpdateReplay(63n, window)).toBe(true);
|
|
290
|
+
|
|
291
|
+
// Sequences 0-63 should all be within window
|
|
292
|
+
expect(isValidSequence(0n, window)).toBe(true);
|
|
293
|
+
expect(isValidSequence(1n, window)).toBe(true);
|
|
294
|
+
expect(isValidSequence(62n, window)).toBe(true);
|
|
295
|
+
|
|
296
|
+
// Accept some in-window sequences
|
|
297
|
+
expect(checkAndUpdateReplay(0n, window)).toBe(true);
|
|
298
|
+
expect(checkAndUpdateReplay(62n, window)).toBe(true);
|
|
299
|
+
|
|
300
|
+
// Verify they're now marked as seen
|
|
301
|
+
expect(isValidSequence(0n, window)).toBe(false);
|
|
302
|
+
expect(isValidSequence(62n, window)).toBe(false);
|
|
303
|
+
expect(isValidSequence(63n, window)).toBe(false);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
});
|
package/src/replay.ts
CHANGED