Files
catarrh 6d7e87b52a 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.
2026-08-24 22:34:23 +03:00

93 lines
3.4 KiB
JavaScript

const { test } = require('node:test');
const assert = require('node:assert');
const { loadSrc, extractFunc, extractConst } = require('./helpers.js');
const src = loadSrc();
// Single direct eval with var substitutions: function/var declarations
// hoist into this module's scope (const would collide with itself).
eval(
extractConst(src, 'PARSE_INS').replace(/^const /m, 'var ') + '\n' +
extractFunc(src, 'describeP1P2') + '\n' +
extractFunc(src, 'describeInstallDataLv') + '\n' +
extractFunc(src, 'parseOneApdu') + '\n' +
extractFunc(src, 'parseCompactApdus')
);
function child(tree, label) {
return (tree.children || []).find(k => k.label === label);
}
test('PARSE_INS registry: SIM/USIM + GP commands', () => {
assert.equal(PARSE_INS.A4.name, 'SELECT');
assert.equal(PARSE_INS.B0.name, 'READ BINARY');
assert.equal(PARSE_INS.B0.data, 'le');
assert.equal(PARSE_INS.D6.name, 'UPDATE BINARY');
assert.equal(PARSE_INS.D6.data, 'lc');
assert.equal(PARSE_INS.E6.name, 'INSTALL');
});
test('parseOneApdu: SELECT by FID (case 3)', () => {
const t = parseOneApdu('00A40000023F00');
assert.equal(child(t, 'INS').desc, 'SELECT');
assert.equal(child(t, 'P1').desc, 'by FID');
const data = child(t, 'Data');
assert.equal(data.hex, '3F00');
const lc = child(t, 'Lc');
assert.equal(lc.desc, 'Data length: 2');
});
test('parseOneApdu: READ BINARY (case 2, Le)', () => {
const t = parseOneApdu('00B0000009');
assert.equal(child(t, 'INS').desc, 'READ BINARY');
const le = child(t, 'Le');
assert.equal(le.hex, '09');
assert.match(le.desc, /Expected length: 9/);
assert.equal(child(t, 'Data'), undefined);
});
test('parseOneApdu: UPDATE BINARY (case 3)', () => {
const t = parseOneApdu('00D600000CAABBCCDD1122334455667788');
const data = child(t, 'Data');
assert.equal(data.hex, 'AABBCCDD1122334455667788');
const lc = child(t, 'Lc');
assert.equal(lc.desc, 'Data length: 12');
});
test('parseOneApdu: GET DATA (P1P2 = tag, P3 = Le)', () => {
const t = parseOneApdu('80CA9F1700');
assert.equal(child(t, 'INS').desc, 'GET DATA');
const le = child(t, 'Le');
assert.equal(le.hex, '00');
});
test('parseOneApdu: lowercase input normalized', () => {
const t = parseOneApdu('00a40000023f00');
assert.equal(child(t, 'INS').desc, 'SELECT');
assert.equal(child(t, 'Data').hex, '3F00');
});
test('parseCompactApdus: concatenated stream with implied CLA', () => {
// SELECT 3F00 | SELECT 2FE2 (implied CLA) | UPDATE BINARY 12B (implied CLA) | trailing Le
const stream = '00A40000023F00' + 'A40000022FE2' +
'D600000CAABBCCDD1122334455667788' + '99';
const apdus = parseCompactApdus(stream);
const named = apdus.filter(a => a.label !== 'Unknown');
assert.equal(named.length, 4);
assert.equal(child(named[0], 'INS').desc, 'SELECT');
assert.equal(named[1].label, 'APDU (implied CLA)');
assert.equal(child(named[1], 'INS').desc, 'SELECT');
assert.equal(named[2].label, 'APDU (implied CLA)');
assert.equal(child(named[2], 'INS').desc, 'UPDATE BINARY');
assert.equal(named[3].label, 'Le');
});
test('parseCompactApdus: unrecognized bytes degrade gracefully', () => {
// Trailing 2 hex chars are interpreted as Le by design — no crash,
// and the leading unrecognized bytes come back as Unknown entries.
const apdus = parseCompactApdus('FFFFFF');
assert.equal(apdus.length, 3);
assert.equal(apdus[0].label, 'Unknown');
assert.equal(apdus[1].label, 'Unknown');
assert.equal(apdus[2].label, 'Le');
});