Files
securebit-chat/tests/inbound-frame-authentication.test.mjs
T

194 lines
8.0 KiB
JavaScript
Raw Normal View History

// Regression tests for inbound frame authentication.
//
// Chat content must reach the UI through exactly one door: an `enhanced_message`
// that has been AES-GCM decrypted, HMAC-verified and replay-checked. Anything
// else arriving on the data channel is unauthenticated peer input and must be
// dropped rather than rendered as if it were a real message.
import assert from 'node:assert/strict';
globalThis.window = globalThis.window || {};
const { EnhancedSecureWebRTCManager } = await import('../src/network/EnhancedSecureWebRTCManager.js');
const P = EnhancedSecureWebRTCManager.prototype;
function createReceiver({ isVerified = true } = {}) {
const delivered = [];
const receiver = {
delivered,
isVerified,
encryptionKey: null,
macKey: null,
metadataKey: null,
securityFeatures: {},
onMessage: (m, t) => delivered.push({ message: m, type: t }),
deliverMessageToUI(msg, type) { this.onMessage(msg, type); },
_secureLog() {},
_checkInboundRateLimit: () => true,
establishConnection: async () => {},
initializeFileTransfer() {},
startHeartbeat() {},
_notifyVerificationReadyIfPossible() {},
initiateVerification() {},
setupDataChannel: P.setupDataChannel,
_processBinaryDataWithoutMutex: P._processBinaryDataWithoutMutex,
_processEnhancedMessageWithoutMutex: P._processEnhancedMessageWithoutMutex
};
const channel = { readyState: 'open', send() {} };
receiver.setupDataChannel(channel);
return { receiver, channel };
}
// ── unencrypted {type:'message'} frames are rejected ─────────────────────────
{
const { receiver, channel } = createReceiver();
await channel.onmessage({
data: JSON.stringify({ type: 'message', data: 'injected without any crypto' })
});
assert.deepEqual(receiver.delivered, [], 'plaintext message frames must not reach the UI');
}
// ── raw non-JSON text is rejected ────────────────────────────────────────────
{
const { receiver, channel } = createReceiver();
await channel.onmessage({ data: 'not even json' });
assert.deepEqual(receiver.delivered, [], 'raw text frames must not reach the UI');
}
// ── binary frames never reach the UI ─────────────────────────────────────────
{
const { receiver, channel } = createReceiver();
const buf = new TextEncoder().encode('binary injection').buffer;
await channel.onmessage({ data: buf });
assert.deepEqual(receiver.delivered, [], 'binary frames must not reach the UI');
}
// ── cover traffic is still discarded silently ────────────────────────────────
{
const { receiver, channel } = createReceiver();
const fake = new TextEncoder().encode(JSON.stringify({ type: 'fake', isFakeTraffic: true })).buffer;
await channel.onmessage({ data: fake });
assert.deepEqual(receiver.delivered, []);
}
// ── the authenticated path still delivers, and enforces anti-replay ──────────
{
let seq = 0;
globalThis.window.EnhancedSecureCryptoUtils = {
decryptMessage: async () => ({
message: JSON.stringify({ type: 'message', data: `msg-${seq}` }),
messageId: `id-${seq}`,
sequenceNumber: seq
})
};
const delivered = [];
const receiver = {
encryptionKey: {}, macKey: {}, metadataKey: {},
_secureLog() {},
_checkInboundRateLimit: () => true,
_sanitizeIncomingChatMessage: (m) => m,
_sanitizeMessageMeta: P._sanitizeMessageMeta,
_validateIncomingSequenceNumber: P._validateIncomingSequenceNumber,
replayProtectionEnabled: true,
expectedSequenceNumber: 0,
replayWindow: new Set(),
replayWindowSize: 64,
maxSequenceGap: 100,
onMessage: (m) => delivered.push(m),
deliverMessageToUI: P.deliverMessageToUI,
_processEnhancedMessageWithoutMutex: P._processEnhancedMessageWithoutMutex
};
seq = 0;
await receiver._processEnhancedMessageWithoutMutex({ type: 'enhanced_message', data: 'enc' });
seq = 1;
await receiver._processEnhancedMessageWithoutMutex({ type: 'enhanced_message', data: 'enc' });
assert.deepEqual(delivered, ['msg-0', 'msg-1'], 'fresh messages must be delivered');
// Replaying sequence number 0 must be refused.
seq = 0;
await receiver._processEnhancedMessageWithoutMutex({ type: 'enhanced_message', data: 'enc' });
assert.deepEqual(delivered, ['msg-0', 'msg-1'], 'replayed message must be dropped');
}
// ── a missing sequence number fails closed ───────────────────────────────────
{
const receiver = {
replayProtectionEnabled: true,
expectedSequenceNumber: 0,
replayWindow: new Set(),
replayWindowSize: 64,
maxSequenceGap: 100,
_secureLog() {},
_validateIncomingSequenceNumber: P._validateIncomingSequenceNumber
};
assert.equal(receiver._validateIncomingSequenceNumber(undefined, 't'), false);
assert.equal(receiver._validateIncomingSequenceNumber(null, 't'), false);
assert.equal(receiver._validateIncomingSequenceNumber('3', 't'), false);
assert.equal(receiver._validateIncomingSequenceNumber(NaN, 't'), false);
assert.equal(receiver.replayWindow.size, 0, 'rejected values must not pollute the window');
assert.equal(receiver._validateIncomingSequenceNumber(0, 't'), true);
}
// ── file control frames are gated on verification ────────────────────────────
{
const handled = [];
const makeReceiver = (isVerified) => {
const receiver = {
isVerified,
securityFeatures: {},
fileTransferSystem: { handleFileMessage: async (m) => handled.push(m.type) },
_secureLog() {},
_checkInboundRateLimit: () => true,
_enforceVerificationGate: P._enforceVerificationGate,
onMessage() {},
deliverMessageToUI() {},
establishConnection: async () => {},
initializeFileTransfer() {},
startHeartbeat() {},
_notifyVerificationReadyIfPossible() {},
initiateVerification() {},
setupDataChannel: P.setupDataChannel
};
const channel = { readyState: 'open', send() {} };
receiver.setupDataChannel(channel);
return channel;
};
// Unverified peer: file frames must be dropped, not acted on.
const unverified = makeReceiver(false);
await unverified.onmessage({
data: JSON.stringify({ type: 'file_transfer_start', fileId: 'f1', fileName: 'x.png' })
});
await unverified.onmessage({ data: JSON.stringify({ type: 'file_chunk', fileId: 'f1' }) });
assert.deepEqual(handled, [], 'file frames must not be processed before verification');
// Verified peer: transfers keep working exactly as before.
const verified = makeReceiver(true);
await verified.onmessage({
data: JSON.stringify({ type: 'file_transfer_start', fileId: 'f1', fileName: 'x.png' })
});
await verified.onmessage({ data: JSON.stringify({ type: 'file_chunk', fileId: 'f1' }) });
assert.deepEqual(handled, ['file_transfer_start', 'file_chunk'], 'verified transfers must still work');
}
// ── anti-replay helpers live on the connection, not on key storage ───────────
{
for (const name of [
'_validateIncomingSequenceNumber',
'_validateMessageAAD',
'getAntiReplayStatus',
'configureAntiReplayProtection'
]) {
assert.equal(
typeof P[name],
'function',
`${name} must be reachable on the WebRTC manager (it reads this.replayWindow)`
);
}
}
console.log('Inbound frame authentication tests passed');