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:
1 parent
6da5acca21
commit
6d7e87b52a
9 files changed
+291
-8
No files matched your search
@@ -0,0 +1,92 @@
|
||||
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');
|
||||
});
|
||||
Reference in new issue
Block a user