Add node:test suite for vendored C-APDU parser and SCP80 decryption

frontend/tests/ (otaman-style function extraction from index.html):
- apdu_parse.test.js: PARSE_INS registry, parseOneApdu (SELECT by FID,
  READ BINARY Le-form, UPDATE BINARY, GET DATA, lowercase input),
  parseCompactApdus (concatenated implied-CLA stream, graceful
  unrecognized-byte degradation).
- scp80_decrypt.test.js: decryptSecured known-answer tests with
  node-crypto generated ciphertexts — 3DES-CBC 2/3-key (real CNTR=200
  revealed), AES-CBC 128, single DES, wrong-key padding guard, key
  length validation, unsupported-algorithm rejection.
- helpers.js: brace-matched function/const/section extraction from
  the single-file frontend; package.json ("test": "node --test").

Fix AES-CBC decryption path found by the new tests: SubtleCrypto CBC
decrypt always validates PKCS#7, which SCP80 PCNTR-style padding
violates. Workaround: append a synthetic block C* = E(C_last XOR
padBlock) so the validator sees a full 0x10 padding block, decrypt,
then discard the synthetic plaintext. Validated: CNTR=200 case
decrypts correctly.

Version 1.20.1, SW cache simtrace-analyser-v29.
This commit is contained in:
catarrh committed 2026-08-24 22:34:23 +03:00
1 parent 6da5acca21
commit 6d7e87b52a
9 files changed
+291 -8

No files matched your search

+57
View File
@@ -0,0 +1,57 @@
// Shared helpers to extract functions/constants from the single-file
// frontend (index.html) so they can be unit-tested with node:test.
const fs = require('node:fs');
const path = require('node:path');
const INDEX_HTML = path.join(__dirname, '..', 'index.html');
function loadSrc() {
return fs.readFileSync(INDEX_HTML, 'utf8');
}
// Extract a `function name(...) { ... }` by brace matching (includes a
// preceding `async` modifier when present).
function extractFunc(src, name) {
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
let start = m.index;
if (src.slice(Math.max(0, start - 6), start) === 'async ') start -= 6;
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return src.slice(start, i + 1);
}
// Extract a `const NAME = { ... };` block by brace matching.
function extractConst(src, name) {
const re = new RegExp('const\\s+' + name + '\\s*=\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('const ' + name + ' not found');
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return src.slice(m.index, i + 1) + ';';
}
// Extract a `// marker ... ` section up to the next marker comment.
function extractSection(src, startMarker, endMarker) {
const i = src.indexOf(startMarker);
if (i < 0) throw new Error('section not found: ' + startMarker);
const j = endMarker ? src.indexOf(endMarker, i) : -1;
return src.slice(i, j > i ? j : undefined);
}
module.exports = { loadSrc, extractFunc, extractConst, extractSection, INDEX_HTML };