@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.
@@ -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
@@ -1,5 +1,4 @@
1
- // SPDX-FileCopyrightText: 2025-present Sideband
2
- // SPDX-License-Identifier: AGPL-3.0-or-later
1
+ // SPDX-License-Identifier: Apache-2.0
3
2
 
4
3
  /**
5
4
  * Bitmap-based replay protection for Sideband Relay Protocol (SBRP).