v3.11.0 review fixes: ADM length, TAR re-render hygiene
- the ADM input capped the key at 8 chars, but the server's _verify_adm takes 4-16 hex digits (padded to 8 CHV bytes) and the old input had no limit; maxlength=16 now, with a static guard in html.test.js. - cardsTarAdd/cardsTarRemove re-rendered the table from the save-path reader, so an emptied role field was silently refilled with its default. The renderer no longer invents defaults; a raw reader (cardsTarsFromTable) feeds Add/Remove and the language-switch refresh, while the save path (cardsTarsFromForm) keeps the default-filling for the mandatory roles. - the editor's dynamic TAR/keyset rows now rebuild on a language switch (refreshDynamicI18n) from what is typed - their placeholders and titles carry t() strings. (The title/Save label were already refreshed there.) - dropped the now-unused cardsRoleMsl helper and its test references. - docs: the ADM is documented as 4-16 hex digits (help EN/RU, api.md, AGENTS). sw cache simple-v362. Tests: 665 Python + 784 frontend green; the whole inline PWA script syntax-checked.
This commit is contained in:
+1
-1
@@ -243,7 +243,7 @@ pass `spi1` itself, otherwise it is refused with `400` and a hint.
|
||||
**Card codes.** The optional `pin1`/`puk1`/`pin2`/`puk2`/`adm` fields are
|
||||
stored for later use (nothing consumes them yet). PIN/PUK must be **4–8
|
||||
decimal digits** when set (empty is fine; anything else is refused with `400`
|
||||
naming the field); the ADM key stays free-form (hex or ASCII digits).
|
||||
naming the field); the ADM key is 4–16 hex digits (a decimal PIN is fine).
|
||||
|
||||
**Counters.** The store is the source of truth for the SCP80 counters, one per
|
||||
keyset ("a dedicated counter shall be associated to each key version",
|
||||
|
||||
@@ -352,7 +352,7 @@
|
||||
<tbody>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Name</td><td class="py-1 px-2">Понятная метка (обязательна)</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">ICCID</td><td class="py-1 px-2">Опциональный идентификатор карты. Кнопка <strong>С карты</strong> подставляет EF.ICCID подключённой карты (активна, только когда ICCID читается); при сохранении ICCID, уже присутствующий в списке, отклоняется (сравнение без пробелов и с учётом сырой hex-формы, конфликтующий пресет называется)</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">PIN1 / PUK1 / PIN2 / PUK2 / ADM</td><td class="py-1 px-2">Необязательные коды карты, сохраняются для будущего использования (пока ничем не используются). PIN/PUK — строго 4–8 десятичных цифр (иначе сохранение отклоняется); ADM — администраторский ключ (hex или до 8 ASCII-цифр), используется файловым менеджером. Представлениями SCP80/SCP81 не используются</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">PIN1 / PUK1 / PIN2 / PUK2 / ADM</td><td class="py-1 px-2">Необязательные коды карты, сохраняются для будущего использования (пока ничем не используются). PIN/PUK — строго 4–8 десятичных цифр (иначе сохранение отклоняется); ADM — администраторский ключ (4–16 hex-цифр, например десятичный PIN), используется файловым менеджером. Представлениями SCP80/SCP81 не используются</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">KIc / KID</td><td class="py-1 px-2">Индикаторы ключа и алгоритма (например, 15 = индекс 1, 3DES-CBC2; x2 = AES); задаются вместе с ключами</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">TAR / MSL / Description</td><td class="py-1 px-2">Таблица TAR: одна строка на TAR с его MSL (минимальный SPI1, TS 102 226 §8.2.1.3.2.4). Первые три строки — фиксированные роли (предзаполнены, не удаляются) с фиксированными описаниями; дополнительные TAR добавляются кнопкой <strong>Добавить TAR</strong>, несут необязательное описание и могут быть удалены. SPI1 пакета берётся из MSL его TAR</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">ISD compact format</td><td class="py-1 px-2">TAR Issuer Security Domain (TS 101 220, приложение D); используется для операций RAM/GP (представление SCP80 RAM, цепочки RAM/GP); по умолчанию по спецификации <code class="font-mono text-sm">000000</code>, подставляется при пустом значении</td></tr>
|
||||
|
||||
+1
-1
@@ -351,7 +351,7 @@
|
||||
<tbody>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Name</td><td class="py-1 px-2">Human-readable label (required)</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">ICCID</td><td class="py-1 px-2">Optional card identifier. <strong>From card</strong> fills it with the equipped card's EF.ICCID (enabled only while a readable ICCID is known); saving refuses an ICCID that already exists in the list (compared ignoring spaces and the raw-hex form, and the conflicting preset is named)</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">PIN1 / PUK1 / PIN2 / PUK2 / ADM</td><td class="py-1 px-2">Optional card codes, all stored for later use (nothing consumes them yet). PIN/PUK must be 4–8 decimal digits (saving refuses anything else); ADM is the administrator key (hex, or up to 8 ASCII digits), used by the file manager. None is used by the SCP80/SCP81 views</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">PIN1 / PUK1 / PIN2 / PUK2 / ADM</td><td class="py-1 px-2">Optional card codes, all stored for later use (nothing consumes them yet). PIN/PUK must be 4–8 decimal digits (saving refuses anything else); ADM is the administrator key (4–16 hex digits, e.g. a decimal PIN), used by the file manager. None is used by the SCP80/SCP81 views</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">KIc / KID</td><td class="py-1 px-2">Key and algorithm indicators (e.g. 15 = index 1, 3DES-CBC2; x2 = AES), required together with the keys</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">TAR / MSL / Description</td><td class="py-1 px-2">The TAR table: one uniform row per TAR with its MSL (Minimum SPI1, TS 102 226 §8.2.1.3.2.4). The first three rows are the fixed role entries (pre-settled, not removable) with their fixed descriptions; further TARs are added with <strong>Add TAR</strong>, carry an optional description and can be removed. A packet's SPI1 is derived from its TAR's MSL</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">ISD compact format</td><td class="py-1 px-2">Issuer Security Domain TAR (TS 101 220 Annex D); used for RAM/GP operations (SCP80 RAM view, RAM/GP chains); spec default <code class="font-mono text-sm">000000</code>, prefilled when empty</td></tr>
|
||||
|
||||
+36
-21
@@ -1658,7 +1658,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<label for="cards-adm" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400">ADM</label>
|
||||
<input id="cards-adm" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono text-center" maxlength="8" placeholder="ADM (optional)">
|
||||
<input id="cards-adm" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono text-center" maxlength="16" placeholder="ADM (optional)">
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-slate-400" data-l10n="All codes are optional: PIN/PUK are 4-8 decimal digits (stored for later use by the card operations — not consumed yet); ADM is the administrator key.">All codes are optional: PIN/PUK are 4-8 decimal digits (stored for later use by the card operations — not consumed yet); ADM is the administrator key.</p>
|
||||
@@ -11675,12 +11675,6 @@ function cardsRoleTar(preset, role) {
|
||||
return (e && e.tar) || cardsRoleDefault(role);
|
||||
}
|
||||
|
||||
// The MSL of a role entry, '' when the preset carries none.
|
||||
function cardsRoleMsl(preset, role) {
|
||||
const e = cardsTarList(preset).find(x => x.role === role);
|
||||
return (e && e.msl) || '';
|
||||
}
|
||||
|
||||
// The TAR entry matching a value (case-insensitive), or null.
|
||||
function cardsTarEntry(preset, tarHex) {
|
||||
const want = String(tarHex || '').trim().toUpperCase();
|
||||
@@ -11918,15 +11912,16 @@ function cardsTarRowHtml(e, i) {
|
||||
}
|
||||
|
||||
// Render the whole table from a preset's TAR list (roles first, then free
|
||||
// rows); the role rows are always shown, with the pre-settled defaults when
|
||||
// the list carries none.
|
||||
// rows); the role rows are always shown - with the values given (a fresh form
|
||||
// passes the pre-settled defaults). A re-render must keep what is displayed,
|
||||
// so it never invents defaults here.
|
||||
function cardsTarRowsRender(entries) {
|
||||
const box = document.getElementById('cards-tars');
|
||||
if (!box) return;
|
||||
const list = entries || [];
|
||||
const roles = CARDS_TAR_ROLES.map(role => {
|
||||
const e = list.find(x => x.role === role) || {};
|
||||
return { tar: e.tar || cardsRoleDefault(role), msl: e.msl || CARDS_MSL_DEFAULT };
|
||||
return { tar: e.tar || '', msl: e.msl || '' };
|
||||
});
|
||||
const free = list.filter(e => !e.role);
|
||||
_cardsTarCount = free.length;
|
||||
@@ -11935,13 +11930,13 @@ function cardsTarRowsRender(entries) {
|
||||
}
|
||||
|
||||
function cardsTarAdd() {
|
||||
cardsTarRowsRender(cardsTarsFromForm().concat([{ tar: '', msl: '', desc: '' }]));
|
||||
cardsTarRowsRender(cardsTarsFromTable().concat([{ tar: '', msl: '', desc: '' }]));
|
||||
}
|
||||
|
||||
// `i` is the table row index; the role rows are fixed and never removed.
|
||||
function cardsTarRemove(i) {
|
||||
if (i < CARDS_TAR_ROLES.length) return;
|
||||
const rows = cardsTarsFromForm();
|
||||
const rows = cardsTarsFromTable();
|
||||
rows.splice(i, 1);
|
||||
cardsTarRowsRender(rows);
|
||||
}
|
||||
@@ -11963,26 +11958,37 @@ function cardsFreeTarsFromForm() {
|
||||
return out;
|
||||
}
|
||||
|
||||
// The TAR table as preset entries: the three role entries from the first three
|
||||
// rows (an emptied field keeps the pre-settled default) plus the free rows.
|
||||
function cardsTarsFromForm() {
|
||||
// The TAR table exactly as displayed: the role rows as typed (an emptied field
|
||||
// stays empty) plus the free rows - the re-render source for Add/Remove, so an
|
||||
// emptied field is not silently refilled with its default.
|
||||
function cardsTarsFromTable() {
|
||||
const out = CARDS_TAR_ROLES.map((role, i) => {
|
||||
const get = id => {
|
||||
const el = document.getElementById('cards-tr-' + i + '-' + id);
|
||||
return el ? String(el.value).trim() : '';
|
||||
};
|
||||
const tar = get('tar').replace(/\s/g, '').toUpperCase();
|
||||
const msl = get('msl').replace(/\s/g, '').toUpperCase();
|
||||
return { role: role, tar: tar || cardsRoleDefault(role),
|
||||
msl: msl || CARDS_MSL_DEFAULT, desc: '' };
|
||||
return { role: role,
|
||||
tar: get('tar').replace(/\s/g, '').toUpperCase(),
|
||||
msl: get('msl').replace(/\s/g, '').toUpperCase(), desc: '' };
|
||||
});
|
||||
return out.concat(cardsFreeTarsFromForm());
|
||||
}
|
||||
|
||||
// The TAR table as preset entries (the save path): an emptied role field keeps
|
||||
// the pre-settled default - the three role entries are mandatory with a
|
||||
// non-empty TAR and MSL.
|
||||
function cardsTarsFromForm() {
|
||||
return cardsTarsFromTable().map(e => e.role
|
||||
? { role: e.role, tar: e.tar || cardsRoleDefault(e.role),
|
||||
msl: e.msl || CARDS_MSL_DEFAULT, desc: '' }
|
||||
: e);
|
||||
}
|
||||
|
||||
// The role rows back to their pre-settled defaults (TAR + MSL 16) and no free
|
||||
// rows.
|
||||
function cardsTarsReset() {
|
||||
cardsTarRowsRender([]);
|
||||
cardsTarRowsRender(CARDS_TAR_ROLES.map(role => ({
|
||||
role: role, tar: cardsRoleDefault(role), msl: CARDS_MSL_DEFAULT, desc: '' })));
|
||||
}
|
||||
|
||||
// ===== Keyset selectors (SCP80 / RAM) =====
|
||||
@@ -21141,7 +21147,16 @@ function refreshDynamicI18n() {
|
||||
if (isViewVisible('profiler-scan-modal')) profilerScanRefreshOptions();
|
||||
pysimUpdateCardActionLabels();
|
||||
pysimUpdateStateIndicator();
|
||||
if (isViewVisible('tab-cards')) { cardsRender(); cardsSetFormMode(); }
|
||||
if (isViewVisible('tab-cards')) {
|
||||
cardsRender();
|
||||
cardsSetFormMode();
|
||||
// the editor's dynamic rows carry t() strings (placeholders, titles):
|
||||
// rebuild them from what is typed, not from the defaults
|
||||
if (isViewVisible('cards-editor-view')) {
|
||||
cardsKeysetRowsRender(cardsKeysetsFromForm());
|
||||
cardsTarRowsRender(cardsTarsFromTable());
|
||||
}
|
||||
}
|
||||
if (isViewVisible('scp81-sub-scripts')) { scriptsRender(); scriptsRebuildSelect(); }
|
||||
if (_ramExplorerData && isViewVisible('ram-explorer')) ramRenderExplorer();
|
||||
// the CAP analysis bodies are built with t(); the legends are static
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v361';
|
||||
const CACHE = 'simple-v362';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -28,7 +28,7 @@ function extractFunc(src, name) {
|
||||
}
|
||||
|
||||
let code = '';
|
||||
for (const fn of ['cardsTarList', 'cardsRoleDefault', 'cardsRoleTar', 'cardsRoleMsl',
|
||||
for (const fn of ['cardsTarList', 'cardsRoleDefault', 'cardsRoleTar',
|
||||
'cardsTarEntry', 'cardsTarMsl', 'cardsApplyFields', 'cardsApply',
|
||||
'spRefreshFromPreset', 'spPorAccepted', 'spNextCntr', 'spPresetIdx',
|
||||
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
|
||||
|
||||
@@ -41,10 +41,10 @@ for (const fn of ['cardsFormValues', 'cardsClearForm', 'cardsEdit',
|
||||
'cardsKeysetAdd', 'cardsKeysetRemove', 'cardsKeysetKvnUpdate',
|
||||
'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply', 'spKeysetChanged',
|
||||
'ramKeysetChanged', 'ramPresetIdx', 'tarPresetIdx', 'spKeysetGuard', 'ramKeysetGuard',
|
||||
'cardsTarList', 'cardsRoleDefault', 'cardsRoleTar', 'cardsRoleMsl',
|
||||
'cardsTarList', 'cardsRoleDefault', 'cardsRoleTar',
|
||||
'cardsTarEntry', 'cardsTarMsl', 'cardsTarText', 'spMslWarningText',
|
||||
'cardsTarRowHtml', 'cardsTarRowsRender', 'cardsTarAdd', 'cardsTarRemove',
|
||||
'cardsFreeTarsFromForm', 'cardsTarsFromForm', 'cardsTarsReset',
|
||||
'cardsFreeTarsFromForm', 'cardsTarsFromTable', 'cardsTarsFromForm', 'cardsTarsReset',
|
||||
'cardsShowList', 'cardsShowEditor', 'cardsSetFormMode', 'cardsNew',
|
||||
'cardsCancelEdit']) {
|
||||
code += extractFunc(html, fn, ASYNC_FNS.indexOf(fn) >= 0) + '\n';
|
||||
@@ -181,7 +181,6 @@ test('the preset TAR table helpers read the role entries and the MSLs', () => {
|
||||
{ role: 'usimRfm', tar: 'B00001', msl: '16' },
|
||||
{ tar: 'AF4D02', msl: '0A', desc: 'applet' }] });
|
||||
assert.strictEqual(cardsRoleTar(p, 'isd'), 'AF4D01');
|
||||
assert.strictEqual(cardsRoleMsl(p, 'isd'), '1A');
|
||||
assert.strictEqual(cardsTarMsl(p, 'af4d02'), '0A');
|
||||
assert.strictEqual(cardsTarMsl(p, '123456'), '');
|
||||
assert.strictEqual(cardsTarEntry(p, 'AF4D02').desc, 'applet');
|
||||
@@ -538,7 +537,7 @@ test('the TAR table renders the role rows and adds/removes free rows', () => {
|
||||
cardsTarRowsRender([{ tar: 'AF4D01', msl: '0A', desc: 'one' },
|
||||
{ tar: 'AF4D02', msl: '0A', desc: 'two' }]);
|
||||
assert.strictEqual(_cardsTarCount, 2);
|
||||
assert.match(els['cards-tars'].innerHTML, /cards-tr-0-tar"[^>]*value="000000"/);
|
||||
assert.match(els['cards-tars'].innerHTML, /cards-tr-0-tar"[^>]*value=""/);
|
||||
assert.match(els['cards-tars'].innerHTML, /ISD compact format/);
|
||||
assert.match(els['cards-tars'].innerHTML, /cards-tr-3-tar"[^>]*value="AF4D01"/);
|
||||
assert.match(els['cards-tars'].innerHTML, /cards-tr-4-tar"[^>]*value="AF4D02"/);
|
||||
@@ -560,6 +559,14 @@ test('the TAR table renders the role rows and adds/removes free rows', () => {
|
||||
// the role rows are never removable
|
||||
cardsTarRemove(0);
|
||||
assert.strictEqual(_cardsTarCount, 2);
|
||||
// an emptied role field survives a re-render (no silent default refill),
|
||||
// while the save path still fills the default
|
||||
tarRow(els, 0, { tar: '', msl: '' });
|
||||
cardsTarRemove(3);
|
||||
assert.strictEqual(_cardsTarCount, 0);
|
||||
assert.match(els['cards-tars'].innerHTML, /cards-tr-0-tar"[^>]*value=""/);
|
||||
assert.strictEqual(cardsTarsFromForm()[0].tar, '000000');
|
||||
assert.strictEqual(cardsTarsFromForm()[0].msl, '16');
|
||||
});
|
||||
|
||||
test('the list view opens the editor for a new preset and Cancel returns', () => {
|
||||
|
||||
@@ -32,7 +32,7 @@ for (const fn of ['swapNibbles', 'encIccid', 'decIccid',
|
||||
'cardsNormIccid', 'cardsFindByIccid', 'cardsFindDuplicateIccid',
|
||||
'cardsIccidFromCard', 'cardsAutoSelectByIccid', 'cardsRender',
|
||||
'esc', 'spKeysetList', 'spKeysetKvnOf',
|
||||
'cardsTarList', 'cardsRoleDefault', 'cardsRoleTar', 'cardsRoleMsl',
|
||||
'cardsTarList', 'cardsRoleDefault', 'cardsRoleTar',
|
||||
'cardsTarEntry', 'cardsTarMsl', 'cardsTarText', 'cardsScp81Complete']) {
|
||||
code += extractFunc(html, fn) + '\n';
|
||||
}
|
||||
|
||||
@@ -164,6 +164,16 @@ test('the card editor groups its fields into labelled fieldsets', () => {
|
||||
assert.ok(!fieldsets.some(f => f.includes('id="cards-name"')));
|
||||
});
|
||||
|
||||
test('the ADM input allows the full 4-16 hex digit key', () => {
|
||||
// the server's _verify_adm takes 4-16 hex digits (padded to 8 CHV bytes);
|
||||
// the input must not cap it at the PIN length
|
||||
const m = /<input id="cards-adm"[^>]*>/.exec(html);
|
||||
assert.ok(m, 'the ADM input is missing');
|
||||
const limit = /maxlength="(\d+)"/.exec(m[0]);
|
||||
assert.ok(!limit || parseInt(limit[1], 10) >= 16,
|
||||
'the ADM input must allow 4-16 hex digits (server _verify_adm)');
|
||||
});
|
||||
|
||||
test('the Cards tab splits the preset list and the editor into views', () => {
|
||||
assert.match(html, /id="cards-list-view"/);
|
||||
assert.match(html, /id="cards-editor-view" class="hidden"/);
|
||||
|
||||
Reference in New Issue
Block a user