mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-09 06: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.
114 lines
4.2 KiB
JavaScript
114 lines
4.2 KiB
JavaScript
/**
|
|
* Tests for AES-128-ECB decryption in public/channel-decrypt.js.
|
|
*
|
|
* Background: the original implementation simulated ECB via Web Crypto
|
|
* AES-CBC with a zero IV and a dummy PKCS7 padding block. Web Crypto
|
|
* validates PKCS7 padding on the decrypted output and throws an
|
|
* `OperationError` whenever the last 16 bytes of the (CBC-decrypted)
|
|
* output don't form a valid PKCS7 padding sequence — which is the
|
|
* common case here, since the input is real ciphertext, not a padded
|
|
* second block. This test pins decryptECB() to the FIPS-197 NIST
|
|
* AES-128-ECB known-answer vector (Appendix B / C.1) so that the
|
|
* implementation cannot regress to any Web Crypto + ECB hack.
|
|
*
|
|
* Vector (FIPS-197 Appendix C.1, single-block AES-128 ECB):
|
|
* key = 000102030405060708090a0b0c0d0e0f
|
|
* plaintext = 00112233445566778899aabbccddeeff
|
|
* ciphertext = 69c4e0d86a7b0430d8cdb78070b4c55a
|
|
*/
|
|
'use strict';
|
|
const REPO_ROOT = require('path').resolve(__dirname, '..', '..');
|
|
|
|
const vm = require('vm');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { subtle } = require('crypto').webcrypto;
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
function assert(cond, msg) {
|
|
if (cond) { passed++; console.log(' ✓ ' + msg); }
|
|
else { failed++; console.error(' ✗ ' + msg); }
|
|
}
|
|
|
|
function loadChannelDecrypt() {
|
|
const storage = {};
|
|
const localStorage = {
|
|
getItem: (k) => storage[k] !== undefined ? storage[k] : null,
|
|
setItem: (k, v) => { storage[k] = String(v); },
|
|
removeItem: (k) => { delete storage[k]; },
|
|
};
|
|
const sandbox = {
|
|
window: {}, crypto: { subtle }, 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);
|
|
|
|
// Load vendored AES (if present) before channel-decrypt.js.
|
|
const vendorPath = path.join(REPO_ROOT, 'public/vendor/aes-ecb.js');
|
|
if (fs.existsSync(vendorPath)) {
|
|
vm.runInContext(fs.readFileSync(vendorPath, '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=== AES-128-ECB known-answer vector (FIPS-197 C.1) ===');
|
|
|
|
const CD = loadChannelDecrypt();
|
|
|
|
const key = CD.hexToBytes('000102030405060708090a0b0c0d0e0f');
|
|
const ct = CD.hexToBytes('69c4e0d86a7b0430d8cdb78070b4c55a');
|
|
const expectedPlaintextHex = '00112233445566778899aabbccddeeff';
|
|
|
|
let result, threw = null;
|
|
try {
|
|
result = await CD.decryptECB(key, ct);
|
|
} catch (e) {
|
|
threw = e;
|
|
}
|
|
|
|
assert(threw === null, 'decryptECB does not throw on valid ciphertext (got: ' + (threw && threw.message) + ')');
|
|
assert(result instanceof Uint8Array, 'decryptECB returns a Uint8Array');
|
|
assert(
|
|
result && CD.bytesToHex(result) === expectedPlaintextHex,
|
|
'decryptECB matches FIPS-197 vector (got ' + (result ? CD.bytesToHex(result) : 'null') + ')'
|
|
);
|
|
|
|
// Multi-block: two copies of the same block must produce two copies
|
|
// of the same plaintext (true ECB property — no chaining).
|
|
console.log('\n=== AES-128-ECB multi-block (no chaining) ===');
|
|
const ct2 = new Uint8Array(32);
|
|
ct2.set(ct, 0); ct2.set(ct, 16);
|
|
let result2, threw2 = null;
|
|
try { result2 = await CD.decryptECB(key, ct2); }
|
|
catch (e) { threw2 = e; }
|
|
assert(threw2 === null, 'decryptECB does not throw on 2-block ciphertext');
|
|
assert(
|
|
result2 &&
|
|
CD.bytesToHex(result2.slice(0, 16)) === expectedPlaintextHex &&
|
|
CD.bytesToHex(result2.slice(16, 32)) === expectedPlaintextHex,
|
|
'decryptECB on duplicated block yields duplicated plaintext (ECB, no chaining)'
|
|
);
|
|
|
|
// Empty / misaligned input must return null (existing contract).
|
|
console.log('\n=== Edge cases ===');
|
|
const empty = await CD.decryptECB(key, new Uint8Array(0));
|
|
assert(empty === null, 'empty ciphertext returns null');
|
|
const misaligned = await CD.decryptECB(key, new Uint8Array(15));
|
|
assert(misaligned === null, 'misaligned ciphertext returns null');
|
|
|
|
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); });
|