Stratum V2 initial commit

This commit is contained in:
Ben
2026-06-05 13:14:09 -04:00
parent f2d2283b47
commit 2a37cb1f06
35 changed files with 6969 additions and 31 deletions
+243
View File
@@ -0,0 +1,243 @@
import {
encodeFrameHeader,
decodeFrameHeader,
isChannelMessage,
plaintextToCiphertextLength,
ciphertextToPlaintextLength,
Sv2FrameReader,
Sv2FrameWriter,
Sv2FrameHeader,
} from './sv2-frame';
import { SV2_CHANNEL_MSG_FLAG, SV2_MAC_SIZE, SV2_MAX_PLAINTEXT_CHUNK } from './sv2-constants';
describe('SV2 Frame', () => {
// ── Header Encode / Decode ──────────────────────────────────────────
describe('frame header', () => {
it('should encode and decode a basic header', () => {
const header: Sv2FrameHeader = {
extensionType: 0x0000,
msgType: 0x10,
msgLength: 42,
};
const buf = encodeFrameHeader(header);
expect(buf.length).toBe(6);
const decoded = decodeFrameHeader(buf);
expect(decoded).toEqual(header);
});
it('should handle max msgLength (24-bit)', () => {
const header: Sv2FrameHeader = {
extensionType: 0,
msgType: 0xff,
msgLength: 0xffffff,
};
const buf = encodeFrameHeader(header);
const decoded = decodeFrameHeader(buf);
expect(decoded.msgLength).toBe(0xffffff);
});
it('should preserve channel message flag', () => {
const header: Sv2FrameHeader = {
extensionType: SV2_CHANNEL_MSG_FLAG | 0x01,
msgType: 0x1a,
msgLength: 24,
};
const buf = encodeFrameHeader(header);
const decoded = decodeFrameHeader(buf);
expect(decoded.extensionType).toBe(SV2_CHANNEL_MSG_FLAG | 0x01);
expect(isChannelMessage(decoded)).toBe(true);
});
it('should detect non-channel messages', () => {
const header: Sv2FrameHeader = {
extensionType: 0x0000,
msgType: 0x00,
msgLength: 10,
};
expect(isChannelMessage(header)).toBe(false);
});
it('should throw on buffer too short', () => {
expect(() => decodeFrameHeader(Buffer.alloc(5))).toThrow(RangeError);
});
});
// ── Ciphertext Length Calculations ──────────────────────────────────
describe('ciphertext length calculations', () => {
it('should return 0 for 0-length plaintext', () => {
expect(plaintextToCiphertextLength(0)).toBe(0);
expect(ciphertextToPlaintextLength(0)).toBe(0);
});
it('should add one MAC for small payload', () => {
expect(plaintextToCiphertextLength(100)).toBe(100 + SV2_MAC_SIZE);
});
it('should handle exactly one full chunk', () => {
const ct = plaintextToCiphertextLength(SV2_MAX_PLAINTEXT_CHUNK);
expect(ct).toBe(SV2_MAX_PLAINTEXT_CHUNK + SV2_MAC_SIZE);
});
it('should handle one full chunk + 1 byte remainder', () => {
const ct = plaintextToCiphertextLength(SV2_MAX_PLAINTEXT_CHUNK + 1);
expect(ct).toBe(SV2_MAX_PLAINTEXT_CHUNK + SV2_MAC_SIZE + 1 + SV2_MAC_SIZE);
});
it('plaintext -> ciphertext -> plaintext round-trips', () => {
const sizes = [0, 1, 100, SV2_MAX_PLAINTEXT_CHUNK, SV2_MAX_PLAINTEXT_CHUNK + 1, SV2_MAX_PLAINTEXT_CHUNK * 2 + 500];
for (const pt of sizes) {
const ct = plaintextToCiphertextLength(pt);
expect(ciphertextToPlaintextLength(ct)).toBe(pt);
}
});
it('should throw for invalid ciphertext length (remainder too small)', () => {
// A remainder of exactly SV2_MAC_SIZE would mean 0 plaintext bytes but still a chunk
// Actually that's valid (0 plaintext + 16 MAC). Let's test remainder < MAC
expect(() => ciphertextToPlaintextLength(5)).toThrow(RangeError);
});
});
// ── Plaintext Frame Reader / Writer ─────────────────────────────────
describe('plaintext framing', () => {
it('should write and read a complete plaintext frame', () => {
const writer = new Sv2FrameWriter(null);
const header: Sv2FrameHeader = {
extensionType: 0,
msgType: 0x00,
msgLength: 5,
};
const payload = Buffer.from('hello');
const wire = writer.writeFrame(header, payload);
const reader = new Sv2FrameReader(null);
const frames = reader.feed(wire);
expect(frames.length).toBe(1);
expect(frames[0].header.msgType).toBe(0x00);
expect(frames[0].payload.toString()).toBe('hello');
});
it('should handle fragmented TCP delivery', () => {
const writer = new Sv2FrameWriter(null);
const header: Sv2FrameHeader = {
extensionType: 0,
msgType: 0x10,
msgLength: 10,
};
const payload = Buffer.alloc(10, 0xab);
const wire = writer.writeFrame(header, payload);
const reader = new Sv2FrameReader(null);
// Feed byte by byte
let frames: any[] = [];
for (let i = 0; i < wire.length; i++) {
const chunk = wire.subarray(i, i + 1);
frames = frames.concat(reader.feed(Buffer.from(chunk)));
}
expect(frames.length).toBe(1);
expect(frames[0].header.msgType).toBe(0x10);
expect(frames[0].payload).toEqual(payload);
});
it('should parse multiple frames in one feed', () => {
const writer = new Sv2FrameWriter(null);
const frames: Buffer[] = [];
for (let i = 0; i < 3; i++) {
const header: Sv2FrameHeader = {
extensionType: 0,
msgType: i,
msgLength: 4,
};
frames.push(writer.writeFrame(header, Buffer.alloc(4, i)));
}
const reader = new Sv2FrameReader(null);
const result = reader.feed(Buffer.concat(frames));
expect(result.length).toBe(3);
for (let i = 0; i < 3; i++) {
expect(result[i].header.msgType).toBe(i);
expect(result[i].payload).toEqual(Buffer.alloc(4, i));
}
});
it('should handle zero-length payload', () => {
const writer = new Sv2FrameWriter(null);
const header: Sv2FrameHeader = {
extensionType: 0,
msgType: 0x01,
msgLength: 0,
};
const wire = writer.writeFrame(header, Buffer.alloc(0));
const reader = new Sv2FrameReader(null);
const frames = reader.feed(wire);
expect(frames.length).toBe(1);
expect(frames[0].payload.length).toBe(0);
});
});
// ── Encrypted Frame Reader / Writer ─────────────────────────────────
describe('encrypted framing', () => {
it('should write and read encrypted frames with mock cipher', () => {
// Simple XOR "encryption" for testing frame logic
const xorKey = 0x42;
const encrypt = (pt: Buffer) => {
const ct = Buffer.alloc(pt.length + SV2_MAC_SIZE);
for (let i = 0; i < pt.length; i++) ct[i] = pt[i] ^ xorKey;
// Fake MAC: zeros
return ct;
};
const decrypt = (ct: Buffer) => {
const pt = Buffer.alloc(ct.length - SV2_MAC_SIZE);
for (let i = 0; i < pt.length; i++) pt[i] = ct[i] ^ xorKey;
return pt;
};
const writer = new Sv2FrameWriter(encrypt);
const header: Sv2FrameHeader = {
extensionType: 0,
msgType: 0x10,
msgLength: 8,
};
const payload = Buffer.from('testdata');
const wire = writer.writeFrame(header, payload);
const reader = new Sv2FrameReader(decrypt);
const frames = reader.feed(wire);
expect(frames.length).toBe(1);
expect(frames[0].header.msgType).toBe(0x10);
expect(frames[0].payload.toString()).toBe('testdata');
});
it('should switch from plaintext to encrypted mode', () => {
const encrypt = (pt: Buffer) => Buffer.concat([pt, Buffer.alloc(SV2_MAC_SIZE)]);
const decrypt = (ct: Buffer) => ct.subarray(0, ct.length - SV2_MAC_SIZE);
// Start in plaintext mode
const writer = new Sv2FrameWriter(null);
const reader = new Sv2FrameReader(null);
const plainHeader: Sv2FrameHeader = { extensionType: 0, msgType: 0x00, msgLength: 3 };
const plainWire = writer.writeFrame(plainHeader, Buffer.from('abc'));
let frames = reader.feed(plainWire);
expect(frames.length).toBe(1);
expect(frames[0].payload.toString()).toBe('abc');
// Switch to encrypted
writer.setEncryptFn(encrypt);
reader.setDecryptFn(decrypt);
const encHeader: Sv2FrameHeader = { extensionType: 0, msgType: 0x01, msgLength: 3 };
const encWire = writer.writeFrame(encHeader, Buffer.from('xyz'));
frames = reader.feed(encWire);
expect(frames.length).toBe(1);
expect(frames[0].payload.toString()).toBe('xyz');
});
});
});