ui: verify the ADM PIN from the matched card preset (v2.7.8)
The preset ADM key was stored but never used: the header badge showed whether a key exists and whether the card was verified, yet the only way to verify was the pySim command line. - POST /api/verify-adm builds the TS 102 221 VERIFY itself (CHV number from the card model, short keys padded to 8 bytes with 'f') so the raw SW is reported: 63Cx -> attempts_left, 6983/9804 -> blocked, 6982 -> security error. The key is never stored and is redacted from request logs. - PWA: the header ADM badge is clickable when the matched preset has a key; a failed file-manager read/write (6982/9804) shows a Verify ADM button next to the error. Every retry after a failure asks for confirmation and shows the remaining attempts (stronger text on the last attempt); a blocked ADM disables both entry points until the card session changes. No automatic retries. - tests: tests/test_adm_verify.py (fake scc, APDU/SW mapping, redaction) and frontend/tests/adm_verify.test.js (retry prompt, SW classifier, wiring) + card_state indicator expectations - docs/api.md, help EN/RU, AGENTS; version trio 2.7.8; sw cache v211
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
|
||||
|
||||
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 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);
|
||||
}
|
||||
|
||||
let code = '';
|
||||
for (const fn of ['pysimAdmRetryPrompt', 'pysimAdmSecuritySw']) {
|
||||
code += extractFunc(html, fn) + '\n';
|
||||
}
|
||||
code += 'globalThis.t = s => s;\n';
|
||||
eval(code);
|
||||
|
||||
test('pysimAdmRetryPrompt is silent on the first try', () => {
|
||||
assert.strictEqual(pysimAdmRetryPrompt(null), null);
|
||||
assert.strictEqual(pysimAdmRetryPrompt(undefined), null);
|
||||
});
|
||||
|
||||
test('pysimAdmRetryPrompt warns with the remaining attempts', () => {
|
||||
const msg = pysimAdmRetryPrompt(3);
|
||||
assert.ok(msg.includes('3'), msg);
|
||||
assert.ok(msg.includes('attempt(s) left'), msg);
|
||||
assert.ok(msg.includes('block'), msg);
|
||||
assert.ok(msg.includes('Try again?'), msg);
|
||||
});
|
||||
|
||||
test('pysimAdmRetryPrompt uses the stronger last-attempt text', () => {
|
||||
for (const left of [1, 0]) {
|
||||
const msg = pysimAdmRetryPrompt(left);
|
||||
assert.ok(msg.includes('last attempt'), left + ': ' + msg);
|
||||
assert.ok(msg.includes('Try again?'), msg);
|
||||
}
|
||||
});
|
||||
|
||||
test('pysimAdmSecuritySw flags only the access-condition SWs', () => {
|
||||
for (const sw of ['6982', '9804']) assert.ok(pysimAdmSecuritySw(sw), sw);
|
||||
for (const sw of ['9000', '6985', '63C2', '', null, undefined]) {
|
||||
assert.ok(!pysimAdmSecuritySw(sw), String(sw));
|
||||
}
|
||||
});
|
||||
|
||||
test('file-manager security failures route through the ADM hint', () => {
|
||||
// no raw SW error rendering is left (all three sites use the hint helper)
|
||||
assert.ok(!/statusEl\.textContent = 'SW: ' \+ \(data\.sw/.test(html), 'raw SW error rendering is gone');
|
||||
assert.strictEqual((html.match(/pysimFsShowError\(statusEl, data\.sw/g) || []).length, 3);
|
||||
assert.ok(html.includes("btn.textContent = t('Verify ADM')"));
|
||||
assert.ok(html.includes("el.setAttribute('onclick', 'pysimVerifyAdm()')"));
|
||||
});
|
||||
Reference in New Issue
Block a user