mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-09 12:17:34 +00:00
Moves 290 root test-*.js into tests/unit (177, listed in test-all.sh) and tests/e2e (113, classified in scripts/non-unit-tests.json), per #1981 and PR-D of #1385. Root goes from 348 entries to 48. test-all.sh and test-fixtures/ stay put. The inventory guard now fails if a test reappears in the root or sits in the wrong folder.
Verified independently of the diff: the invoked sets are unchanged (test-all.sh 177 before and after, deploy.yml 96 before and after, both identical as sets), and a full local run of test-all.sh on master and on the branch produced 4702 output lines each whose only differences are absolute paths, stack-trace line numbers shifted by the REPO_ROOT line, the inventory wording and two perf ratios. The guard was mutation-checked: a test back in the root, a unit suite in tests/e2e, and a suite dropped from test-all.sh each make it exit 1. CI run 35246304316 ran 97 suites from tests/e2e and is green.
Follow-up 9335c51d finished the instruction files: no bare root test command is left in AGENTS.md, the squad charters, .github or docs, and every tests/ path they name resolves.
Merged by the interim maintainer without a second human reviewer: CI and the local runs above are the independent checks.
Known and deliberately out of scope: 18 of the 113 files in tests/e2e are invoked by no runner at all, and one of them cannot run anywhere because it requires jsdom, which is not a declared dependency. Tracked separately.
183 lines
7.3 KiB
JavaScript
183 lines
7.3 KiB
JavaScript
/**
|
|
* Tests that channel decryption works in an "insecure context" — i.e. when
|
|
* `window.crypto.subtle` is undefined.
|
|
*
|
|
* Why: when CoreScope is served over plain HTTP (or accessed via a non-https
|
|
* origin like `http://<lan-ip>:8080`), browsers refuse to expose
|
|
* `crypto.subtle` (it requires a secure context). The original
|
|
* `channel-decrypt.js` used `crypto.subtle.digest('SHA-256', …)` for
|
|
* `computeChannelHash` and `crypto.subtle.importKey(…)` +
|
|
* `crypto.subtle.sign('HMAC', …)` for `verifyMAC`. PR #1021 fixed only the
|
|
* AES-ECB path with a pure-JS vendor module, but left SHA-256 and HMAC paths
|
|
* pinned to `crypto.subtle`. Result on HTTP origins:
|
|
*
|
|
* addUserChannel("372a9c93260507adcbf36a84bec0f33d")
|
|
* -> computeChannelHash(key) throws "Cannot read properties of undefined
|
|
* (reading 'digest')"
|
|
* -> caught silently by addUserChannel's try/catch
|
|
* -> user sees "Failed to decrypt"
|
|
*
|
|
* This test sandboxes channel-decrypt.js with `crypto.subtle === undefined`
|
|
* and asserts both `computeChannelHash` and `verifyMAC` still work, using
|
|
* a pure-JS SHA-256 / HMAC-SHA256 fallback.
|
|
*
|
|
* Reference vectors:
|
|
* key bytes = 0x37,0x2a,0x9c,0x93,0x26,0x05,0x07,0xad,0xcb,0xf3,0x6a,0x84,0xbe,0xc0,0xf3,0x3d
|
|
* SHA256(key) = b7ce04f7d9019788b69e709ffb796a36d00225818b444ad4f8979bc1d1445f47
|
|
* -> first byte (channel hash) = 0xb7 = 183
|
|
*
|
|
* HMAC-SHA256 KAT (RFC 4231 Test Case 1):
|
|
* key = 0x0b * 20
|
|
* data = "Hi There"
|
|
* mac = b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7
|
|
*/
|
|
'use strict';
|
|
const REPO_ROOT = require('path').resolve(__dirname, '..', '..');
|
|
|
|
const vm = require('vm');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
function assert(cond, msg) {
|
|
if (cond) { passed++; console.log(' ✓ ' + msg); }
|
|
else { failed++; console.error(' ✗ ' + msg); }
|
|
}
|
|
|
|
function loadChannelDecryptInsecureContext() {
|
|
const storage = {};
|
|
const localStorage = {
|
|
getItem: (k) => storage[k] !== undefined ? storage[k] : null,
|
|
setItem: (k, v) => { storage[k] = String(v); },
|
|
removeItem: (k) => { delete storage[k]; },
|
|
};
|
|
// CRITICAL: crypto present, but no .subtle. Mirrors browser HTTP context.
|
|
const insecureCrypto = {};
|
|
const sandbox = {
|
|
window: {}, crypto: insecureCrypto, TextEncoder, TextDecoder, Uint8Array,
|
|
localStorage, console, Date, JSON, parseInt, Math, String, Number,
|
|
Object, Array, RegExp, Error, Promise, setTimeout,
|
|
};
|
|
sandbox.window = sandbox; sandbox.self = sandbox;
|
|
vm.createContext(sandbox);
|
|
|
|
// Vendored AES (must load before channel-decrypt.js — same as index.html).
|
|
const vendorAesPath = path.join(REPO_ROOT, 'public/vendor/aes-ecb.js');
|
|
if (fs.existsSync(vendorAesPath)) {
|
|
vm.runInContext(fs.readFileSync(vendorAesPath, 'utf8'), sandbox);
|
|
}
|
|
// Optional vendored SHA-256 / HMAC (the fix). Load if present so the test
|
|
// works whether the fix vendors it as a separate file OR inlines it into
|
|
// channel-decrypt.js.
|
|
const vendorShaPath = path.join(REPO_ROOT, 'public/vendor/sha256-hmac.js');
|
|
if (fs.existsSync(vendorShaPath)) {
|
|
vm.runInContext(fs.readFileSync(vendorShaPath, 'utf8'), sandbox);
|
|
}
|
|
|
|
vm.runInContext(
|
|
fs.readFileSync(path.join(REPO_ROOT, 'public/channel-decrypt.js'), 'utf8'),
|
|
sandbox
|
|
);
|
|
return sandbox.window.ChannelDecrypt;
|
|
}
|
|
|
|
async function runTests() {
|
|
console.log('\n=== channel-decrypt.js works without crypto.subtle (HTTP-context) ===');
|
|
const CD = loadChannelDecryptInsecureContext();
|
|
|
|
// 1) computeChannelHash() — pure SHA-256 of 16-byte key, take byte 0.
|
|
const KEY_HEX = '372a9c93260507adcbf36a84bec0f33d';
|
|
const keyBytes = CD.hexToBytes(KEY_HEX);
|
|
|
|
let hashByte, threwHash = null;
|
|
try {
|
|
hashByte = await CD.computeChannelHash(keyBytes);
|
|
} catch (e) {
|
|
threwHash = e;
|
|
}
|
|
assert(threwHash === null,
|
|
'computeChannelHash does not throw without crypto.subtle (got: ' +
|
|
(threwHash && threwHash.message) + ')');
|
|
assert(hashByte === 0xb7,
|
|
'computeChannelHash returns 0xb7 for known PSK key (got: ' + hashByte + ')');
|
|
|
|
// 2) verifyMAC() — RFC 4231 HMAC-SHA256 Test Case 1.
|
|
// We feed a hand-built scenario:
|
|
// verifyMAC's HMAC key is `aesKey ++ 16 zero bytes` (32 bytes).
|
|
// To exercise RFC 4231 TC1 we set aesKey = 16 * 0x0b and pad another 4
|
|
// bytes of 0x0b in the second half (since verifyMAC zero-fills bytes
|
|
// 16..31, we instead use the channel-decrypt API directly here only to
|
|
// prove HMAC-SHA256 is computed correctly with the standard secret).
|
|
//
|
|
// We construct the secret manually and call verifyMAC on a synthetic
|
|
// ciphertext whose HMAC-SHA256 first 2 bytes we precompute with Node's
|
|
// crypto module (independent oracle).
|
|
const nodeCrypto = require('crypto');
|
|
const aesKey = new Uint8Array(16); for (let i = 0; i < 16; i++) aesKey[i] = 0xab;
|
|
const ct = new Uint8Array(16); for (let i = 0; i < 16; i++) ct[i] = i;
|
|
const secret = Buffer.alloc(32); Buffer.from(aesKey).copy(secret, 0);
|
|
const fullMac = nodeCrypto.createHmac('sha256', secret).update(Buffer.from(ct)).digest();
|
|
const expectedMacHex = fullMac.slice(0, 2).toString('hex');
|
|
|
|
let macOk, threwMac = null;
|
|
try {
|
|
macOk = await CD.verifyMAC(aesKey, ct, expectedMacHex);
|
|
} catch (e) {
|
|
threwMac = e;
|
|
}
|
|
assert(threwMac === null,
|
|
'verifyMAC does not throw without crypto.subtle (got: ' +
|
|
(threwMac && threwMac.message) + ')');
|
|
assert(macOk === true,
|
|
'verifyMAC returns true for valid 2-byte MAC (got: ' + macOk + ')');
|
|
|
|
// 3) verifyMAC must still REJECT a wrong MAC.
|
|
let macBad, threwMacBad = null;
|
|
try {
|
|
macBad = await CD.verifyMAC(aesKey, ct, '0000');
|
|
} catch (e) {
|
|
threwMacBad = e;
|
|
}
|
|
assert(threwMacBad === null,
|
|
'verifyMAC does not throw on wrong MAC (got: ' + (threwMacBad && threwMacBad.message) + ')');
|
|
assert(macBad === false,
|
|
'verifyMAC returns false for wrong 2-byte MAC (got: ' + macBad + ')');
|
|
|
|
// 4) End-to-end: decrypt() must work with subtle absent — exercises
|
|
// SHA-256 (key derivation already done) + HMAC + AES-ECB together.
|
|
// Build a synthetic encrypted packet from a known plaintext.
|
|
const aesKey2 = nodeCrypto.randomBytes(16);
|
|
const plaintext = Buffer.alloc(16);
|
|
// timestamp(4 LE) + flags(1) + "alice: hi\0" then padded
|
|
plaintext.writeUInt32LE(0x12345678, 0);
|
|
plaintext[4] = 0x00;
|
|
Buffer.from('alice: hi\0', 'utf8').copy(plaintext, 5);
|
|
|
|
const cipher = nodeCrypto.createCipheriv('aes-128-ecb', aesKey2, null);
|
|
cipher.setAutoPadding(false);
|
|
const ct2 = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
const secret2 = Buffer.alloc(32); aesKey2.copy(secret2, 0);
|
|
const macHex2 = nodeCrypto.createHmac('sha256', secret2).update(ct2).digest().slice(0, 2).toString('hex');
|
|
|
|
let decResult = null, threwDec = null;
|
|
try {
|
|
decResult = await CD.decrypt(new Uint8Array(aesKey2), macHex2, ct2.toString('hex'));
|
|
} catch (e) {
|
|
threwDec = e;
|
|
}
|
|
assert(threwDec === null,
|
|
'decrypt() does not throw without crypto.subtle (got: ' +
|
|
(threwDec && threwDec.message) + ')');
|
|
assert(decResult && decResult.sender === 'alice' && decResult.message === 'hi',
|
|
'decrypt() recovers sender + message in HTTP context (got: ' +
|
|
JSON.stringify(decResult) + ')');
|
|
|
|
console.log('\n=== Results ===');
|
|
console.log('Passed: ' + passed + ', Failed: ' + failed);
|
|
process.exit(failed > 0 ? 1 : 0);
|
|
}
|
|
|
|
runTests().catch(e => { console.error(e); process.exit(1); });
|