v3.29.1: the per-row keysets in the outfile preview

The import applied one global default keyset to every TAR entry of every
imported card.  The preview's TAR table (TAR + MSL, the three role rows +
"+ TAR") now has a keyset select per row, mirroring the preset editor: an
empty "— none —" plus the keysets found in the file with their KIc/KID,
the first preselected, and the editor's amber warning when a non-zero MSL
has no keyset.  Each TAR's packets then use exactly the chosen keyset.

- PWA: the global "Default keyset for the TARs" select (and the request's
  tar_keyset) is gone; cardsOutfileTarsFromDom/FromForm carry each row's
  keyset; cardsOutfileKeysets() became cardsOutfileKeysetOptions() (the
  union with the KIc/KID bytes for the labels).
- Server: a `tars` entry's own `keyset` (1-15; an unusable value is a 400)
  wins, then the optional global `tar_keyset` (the API shorthand), then the
  card's first keyset - parse_outfile no longer clobbers per-entry keysets
  with the card-level fallback.  presets.normalise_keyset_ref() is
  module-level (one rule for the store and the parser).
- Docs: help EN/RU, api.md (the tars entry keyset + the global fallback),
  the repo AGENTS.  Tests: the per-entry/global/card fallbacks, the
  bad-keyset 400, the options helper, the row select and round-trip, the
  import body.  925 frontend + 944 Python; sw cache simple-v448.
This commit is contained in:
2026-10-15 00:40:16 +03:00
parent cf7ee77a9e
commit 8a66ea5a0a
12 changed files with 168 additions and 95 deletions
+1 -1
View File
@@ -371,7 +371,7 @@
</tbody>
</table>
<p class="text-sm mb-3">Столбец <strong>SCP81</strong> показывает, задана ли в пресете рабочая пара PSK: <strong>&#10003;</strong> (зелёный) — идентификатор и ключ заданы; <strong>&#9888;</strong> (жёлтый) — задано только одно из двух, и слушатель такой пресет игнорирует; <strong>&mdash;</strong> — PSK нет. Идентификатор и ключ PSK задаются вместе. Кнопка <strong>Добавить пресет</strong> открывает редактор с новым пресетом; <strong>Редактировать</strong> загружает пресет строки в редактор, поэтому поля можно менять без повторного ввода карты; <strong>Сохранить</strong> сохраняет и возвращает к списку, <strong>Отмена</strong> отменяет правки; <strong>Удалить</strong> удаляет строку (только в списке). Обмен пресетами: <strong>Экспорт в файл</strong> для выгрузки и <strong>Импорт из файла</strong> для загрузки. Выбранный пресет автоматически заполняет форму Secured Packet и устанавливает TAR для операции (RAM/GP &rarr; ISD TAR, SIM RFM &rarr; UICC RFM TAR, USIM RFM &rarr; ADF RFM TAR); TAR, введённый в форму Secured Packet вручную, сохраняется при отправке - счётчик, ключи и MSL этого TAR по-прежнему берутся из пресета; SPI1 берётся из MSL, а меньшее значение даёт предупреждение (сервер тоже возвращает его как <code class="font-mono text-sm">warning</code>); изменения сразу передаются работающему слушателю SCP81.</p>
<p class="text-sm mb-3"><strong>Импорт outfile</strong> загружает сразу много карт из <em>outfile</em> поставщика карт &mdash; текстового дампа инструмента персонализации: шапка с баннером и параметрами, затем разделённая пробелами <strong>строка колонок</strong> (среди токенов есть <code class="font-mono text-sm">ICCID</code>; первый токен может нести префикс вида <code class="font-mono text-sm">var_out:</code>) и по одной строке на карту. Всё до строки колонок и все неиспользуемые колонки игнорируются. Разбираются <code class="font-mono text-sm">ICCID</code> (цифры, цифры с добивочным <code class="font-mono text-sm">F</code> до чётного числа нибблов, цифры с разделителями или сырой hex EF &mdash; всё приводится к одной форме), <code class="font-mono text-sm">PIN1</code>/<code class="font-mono text-sm">PIN2</code>/<code class="font-mono text-sm">PUK1</code>/<code class="font-mono text-sm">PUK2</code>, <code class="font-mono text-sm">ADM</code> (hex-значение ключа, 4&ndash;16 hex-цифр (чётное число) &mdash; например <code class="font-mono text-sm">3632333239463337</code>), <code class="font-mono text-sm">PSK</code> (16 байт; необязательная колонка <code class="font-mono text-sm">PSKID</code>/<code class="font-mono text-sm">PSK_ID</code> задаёт её идентификатор, по умолчанию берётся ICCID карты) и нумерованные пары ключей <code class="font-mono text-sm">KICn</code>/<code class="font-mono text-sm">KIDn</code> &mdash; по одному keyset на пару, до 15. <strong>Байтов KIc/KID в файле нет</strong>: номер keyset &mdash; это номер колонки (<code class="font-mono text-sm">KIC2</code> &rarr; keyset 2), а ниббл алгоритма определяется размером ключа &mdash; 8 байт DES, 24 байта 3DES3, а для <strong>16 байт</strong> &mdash; выбором в предпросмотре: <strong>3DES2</strong> (по умолчанию) или <strong>AES</strong>; любой другой размер пропускает этот keyset с пометкой. TAR также не берутся из файла: предпросмотр показывает таблицу TAR, которую получат импортируемые карты (по умолчанию три ролевые записи с TAR по спецификации и MSL 16; их можно править, кнопка <strong>+ TAR</strong> добавляет записи), и один <strong>keyset по умолчанию</strong>, применяемый ко всем записям TAR &mdash; он обязателен для карты с несколькими keysets (ненулевой MSL должен называть keyset). <strong>Общее имя</strong> применяется ко всем импортируемым картам (пустое &mdash; берётся ICCID); <strong>режим импорта</strong> выбирает <strong>Дополнить</strong> (по умолчанию &mdash; существующие пресеты сохраняются, дубли ICCID пропускаются и перечисляются) или <strong>Заменить</strong> (хранилище сначала очищается). Предпросмотр перечисляет разобранные карты (ICCID, keysets, найденные коды) и каждую пропущенную строку/ключ с причиной &mdash; до того, как что-либо сохранено.</p>
<p class="text-sm mb-3"><strong>Импорт outfile</strong> загружает сразу много карт из <em>outfile</em> поставщика карт &mdash; текстового дампа инструмента персонализации: шапка с баннером и параметрами, затем разделённая пробелами <strong>строка колонок</strong> (среди токенов есть <code class="font-mono text-sm">ICCID</code>; первый токен может нести префикс вида <code class="font-mono text-sm">var_out:</code>) и по одной строке на карту. Всё до строки колонок и все неиспользуемые колонки игнорируются. Разбираются <code class="font-mono text-sm">ICCID</code> (цифры, цифры с добивочным <code class="font-mono text-sm">F</code> до чётного числа нибблов, цифры с разделителями или сырой hex EF &mdash; всё приводится к одной форме), <code class="font-mono text-sm">PIN1</code>/<code class="font-mono text-sm">PIN2</code>/<code class="font-mono text-sm">PUK1</code>/<code class="font-mono text-sm">PUK2</code>, <code class="font-mono text-sm">ADM</code> (hex-значение ключа, 4&ndash;16 hex-цифр (чётное число) &mdash; например <code class="font-mono text-sm">3632333239463337</code>), <code class="font-mono text-sm">PSK</code> (16 байт; необязательная колонка <code class="font-mono text-sm">PSKID</code>/<code class="font-mono text-sm">PSK_ID</code> задаёт её идентификатор, по умолчанию берётся ICCID карты) и нумерованные пары ключей <code class="font-mono text-sm">KICn</code>/<code class="font-mono text-sm">KIDn</code> &mdash; по одному keyset на пару, до 15. <strong>Байтов KIc/KID в файле нет</strong>: номер keyset &mdash; это номер колонки (<code class="font-mono text-sm">KIC2</code> &rarr; keyset 2), а ниббл алгоритма определяется размером ключа &mdash; 8 байт DES, 24 байта 3DES3, а для <strong>16 байт</strong> &mdash; выбором в предпросмотре: <strong>3DES2</strong> (по умолчанию) или <strong>AES</strong>; любой другой размер пропускает этот keyset с пометкой. TAR также не берутся из файла: предпросмотр показывает таблицу TAR, которую получат импортируемые карты (по умолчанию три ролевые записи с TAR по спецификации и MSL 16; их можно править, кнопка <strong>+ TAR</strong> добавляет записи), и <strong>у каждой строки свой keyset</strong> &mdash; список из найденных в файле keysets (первый выбран заранее; при ненулевом MSL keyset обязателен и помечается жёлтым), поэтому пакеты к каждому TAR используют именно выбранный keyset. <strong>Общее имя</strong> применяется ко всем импортируемым картам (пустое &mdash; берётся ICCID); <strong>режим импорта</strong> выбирает <strong>Дополнить</strong> (по умолчанию &mdash; существующие пресеты сохраняются, дубли ICCID пропускаются и перечисляются) или <strong>Заменить</strong> (хранилище сначала очищается). Предпросмотр перечисляет разобранные карты (ICCID, keysets, найденные коды) и каждую пропущенную строку/ключ с причиной &mdash; до того, как что-либо сохранено.</p>
</section>
<section class="mb-10">
+1 -1
View File
@@ -370,7 +370,7 @@
</tbody>
</table>
<p class="text-sm mb-3">The <strong>SCP81</strong> column shows whether the preset supplies a usable PSK pair: <strong>&#10003;</strong> (green) — identity and key set; <strong>&#9888;</strong> (amber) — only one of the two, so the listener ignores the preset; <strong>&mdash;</strong> — no PSK. PSK identity and key must be set together. <strong>Add preset</strong> opens the editor with a fresh preset; <strong>Edit</strong> loads the row's preset into it so fields can be changed without re-entering the card; <strong>Save</strong> stores it and returns to the list, <strong>Cancel</strong> discards the edits; <strong>Remove</strong> deletes the row (list only). Presets can be shared with <strong>Export to file</strong> and restored with <strong>Import from file</strong>. The selected card preset auto-fills the Secured Packet form and sets the TAR for the operation (RAM/GP &rarr; ISD TAR, SIM RFM &rarr; UICC RFM TAR, USIM RFM &rarr; ADF RFM TAR); a TAR typed into the Secured Packet form by hand is kept when sending - the counter, keys and the TAR's MSL are still re-read from the preset; the SPI1 is seeded from the TAR's MSL and a lower value warns (the server returns it as a <code class="font-mono text-sm">warning</code> too); edits are pushed into a running SCP81 listener automatically.</p>
<p class="text-sm mb-3"><strong>Import outfile</strong> imports many cards at once from a card vendor's <em>outfile</em> — the personalization tool's plain-text dump: a banner and a configuration header, then a whitespace-separated <strong>column line</strong> (the tokens include <code class="font-mono text-sm">ICCID</code>; the first token may carry a <code class="font-mono text-sm">var_out:</code>-style prefix) and one row per card. Everything before the column line and every unused column is ignored. The parser reads <code class="font-mono text-sm">ICCID</code> (digits, the digits with an odd-nibble <code class="font-mono text-sm">F</code> pad, spaced digits or the raw EF hex — all normalised to one form), <code class="font-mono text-sm">PIN1</code>/<code class="font-mono text-sm">PIN2</code>/<code class="font-mono text-sm">PUK1</code>/<code class="font-mono text-sm">PUK2</code>, <code class="font-mono text-sm">ADM</code> (the key's hex value, 4&ndash;16 hex digits in byte pairs — e.g. <code class="font-mono text-sm">3632333239463337</code>), <code class="font-mono text-sm">PSK</code> (16 bytes; an optional <code class="font-mono text-sm">PSKID</code>/<code class="font-mono text-sm">PSK_ID</code> names its identity, defaulting to the card's ICCID) and the numbered key pairs <code class="font-mono text-sm">KICn</code>/<code class="font-mono text-sm">KIDn</code> — one keyset per pair, up to 15. The <strong>KIc/KID bytes are never in the file</strong>: the keyset number is the column number (<code class="font-mono text-sm">KIC2</code> &rarr; keyset 2) and the algorithm nibble follows the key size — 8 bytes DES, 24 bytes 3DES3, and for <strong>16 bytes</strong> the preview's choice, <strong>3DES2</strong> (default) or <strong>AES</strong>; any other size skips that keyset with a note. The TARs never come from the file either: the preview shows the TAR table the imported cards get (the three role entries with their spec TARs and MSL 16 by default, editable, with <strong>+ TAR</strong> for extra entries) and one <strong>default keyset</strong> applied to every TAR entry — required for a card with several keysets (a non-zero MSL must name a keyset). A <strong>common name</strong> is applied to every imported card (empty falls back to the ICCID); <strong>Import mode</strong> chooses <strong>Merge</strong> (default — existing presets are kept, duplicate ICCIDs are skipped and reported) or <strong>Replace</strong> (the store is wiped first). The preview lists the parsed cards (ICCID, keysets, the codes found) and every skipped row/key with its reason before anything is stored.</p>
<p class="text-sm mb-3"><strong>Import outfile</strong> imports many cards at once from a card vendor's <em>outfile</em> — the personalization tool's plain-text dump: a banner and a configuration header, then a whitespace-separated <strong>column line</strong> (the tokens include <code class="font-mono text-sm">ICCID</code>; the first token may carry a <code class="font-mono text-sm">var_out:</code>-style prefix) and one row per card. Everything before the column line and every unused column is ignored. The parser reads <code class="font-mono text-sm">ICCID</code> (digits, the digits with an odd-nibble <code class="font-mono text-sm">F</code> pad, spaced digits or the raw EF hex — all normalised to one form), <code class="font-mono text-sm">PIN1</code>/<code class="font-mono text-sm">PIN2</code>/<code class="font-mono text-sm">PUK1</code>/<code class="font-mono text-sm">PUK2</code>, <code class="font-mono text-sm">ADM</code> (the key's hex value, 4&ndash;16 hex digits in byte pairs — e.g. <code class="font-mono text-sm">3632333239463337</code>), <code class="font-mono text-sm">PSK</code> (16 bytes; an optional <code class="font-mono text-sm">PSKID</code>/<code class="font-mono text-sm">PSK_ID</code> names its identity, defaulting to the card's ICCID) and the numbered key pairs <code class="font-mono text-sm">KICn</code>/<code class="font-mono text-sm">KIDn</code> — one keyset per pair, up to 15. The <strong>KIc/KID bytes are never in the file</strong>: the keyset number is the column number (<code class="font-mono text-sm">KIC2</code> &rarr; keyset 2) and the algorithm nibble follows the key size — 8 bytes DES, 24 bytes 3DES3, and for <strong>16 bytes</strong> the preview's choice, <strong>3DES2</strong> (default) or <strong>AES</strong>; any other size skips that keyset with a note. The TARs never come from the file either: the preview shows the TAR table the imported cards get (the three role entries with their spec TARs and MSL 16 by default, editable, with <strong>+ TAR</strong> for extra entries) and <strong>every row carries its own keyset</strong> — a select over the keysets found in the file (the first one is preselected; a non-zero MSL needs one, marked amber), so each TAR's packets use exactly the keyset you choose. A <strong>common name</strong> is applied to every imported card (empty falls back to the ICCID); <strong>Import mode</strong> chooses <strong>Merge</strong> (default — existing presets are kept, duplicate ICCIDs are skipped and reported) or <strong>Replace</strong> (the store is wiped first). The preview lists the parsed cards (ICCID, keysets, the codes found) and every skipped row/key with its reason before anything is stored.</p>
</section>
<section class="mb-10">
+34 -29
View File
@@ -1928,7 +1928,7 @@
// ===== Version =====
// Single source of truth for the PWA version: shown in the header and used
// by the server version check in pysimConnect().
const SIMPLE_VERSION = '3.29.0';
const SIMPLE_VERSION = '3.29.1';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== view history (the browser Back/Forward buttons) =====
@@ -14856,7 +14856,7 @@ function cardsImportFile(input) {
// shows the preview plus the controls: the common name, the 16-byte key
// algorithm, the TAR table and its one global default keyset - the final call
// imports with a mode.
let _cardsOutfile = null; // {text, file, name, algorithm16, tarKeyset, mode, preview}
let _cardsOutfile = null; // {text, file, name, algorithm16, mode, preview}
let _cardsOutfileTars = 0; // the free TAR rows of the panel
function cardsOutfilePick() {
@@ -14879,7 +14879,7 @@ async function cardsOutfileFile(input) {
}
_cardsOutfile = { text: text, file: String(file.name || ''),
name: String(file.name || '').replace(/\.[^.]+$/, ''),
algorithm16: '3des2', tarKeyset: null, mode: 'merge', preview: null };
algorithm16: '3des2', mode: 'merge', preview: null };
_cardsOutfileTars = 0;
cardsOutfileRender();
await cardsOutfilePreview();
@@ -14905,8 +14905,6 @@ function cardsOutfileControls() {
if (document.getElementById('cards-of-mode')) {
o.mode = val('cards-of-mode') === 'replace' ? 'replace' : 'merge';
}
const ks = parseInt(val('cards-of-keyset'), 10);
if (Number.isInteger(ks) && ks > 0) o.tarKeyset = ks;
}
// The TAR table as the panel shows it: the three role rows (their defaults
@@ -14917,17 +14915,21 @@ function cardsOutfileTarsFromDom() {
const el = document.getElementById(id);
return el ? String(el.value).trim() : '';
};
const ks = i => {
const v = parseInt(get('cards-of-tar-' + i + '-keyset'), 10);
return Number.isInteger(v) && v > 0 ? v : null;
};
const out = CARDS_TAR_ROLES.map((role, i) => ({
role: role,
tar: get('cards-of-tar-' + i + '-tar').replace(/\s/g, '').toUpperCase(),
msl: get('cards-of-tar-' + i + '-msl').replace(/\s/g, '').toUpperCase(),
desc: '',
desc: '', keyset: ks(i),
}));
for (let i = 0; i < _cardsOutfileTars; i++) {
const idx = CARDS_TAR_ROLES.length + i;
out.push({ tar: get('cards-of-tar-' + idx + '-tar').replace(/\s/g, '').toUpperCase(),
msl: get('cards-of-tar-' + idx + '-msl').replace(/\s/g, '').toUpperCase(),
desc: get('cards-of-tar-' + idx + '-desc') });
desc: get('cards-of-tar-' + idx + '-desc'), keyset: ks(idx) });
}
return out;
}
@@ -14942,7 +14944,7 @@ function cardsOutfileTarsFromForm() {
const e = rows[i];
if (e.role) {
out.push({ role: e.role, tar: e.tar || cardsRoleDefault(e.role),
msl: e.msl || CARDS_MSL_DEFAULT, desc: '' });
msl: e.msl || CARDS_MSL_DEFAULT, desc: '', keyset: e.keyset || null });
} else if (e.tar || e.msl || e.desc) {
out.push(e);
}
@@ -14953,9 +14955,24 @@ function cardsOutfileTarsFromForm() {
function cardsOutfileTarRowHtml(e, i) {
const cls = 'border border-gray-300 dark:border-slate-600 text-xs rounded px-1.5 py-0.5 dark:bg-slate-800 font-mono';
const role = CARDS_TAR_ROLES[i] || '';
// The keyset this TAR's packets use: one select per row, like the preset
// editor (v3.29.1); it defaults to the first parsed keyset, and a non-zero
// MSL without one gets the amber marker.
const opts = cardsOutfileKeysetOptions();
const sel = e.keyset ? Number(e.keyset) : (opts.length ? opts[0].v : null);
const msl = String(e.msl || '').toUpperCase();
let html = '<div class="flex flex-wrap items-center gap-1.5 mb-1">';
html += '<input id="cards-of-tar-' + i + '-tar" class="w-24 ' + cls + '" maxlength="6" placeholder="TAR" value="' + escHtml(e.tar || '') + '">';
html += '<input id="cards-of-tar-' + i + '-msl" class="w-14 ' + cls + '" maxlength="2" placeholder="MSL" value="' + escHtml(e.msl || '') + '">';
html += '<select id="cards-of-tar-' + i + '-keyset" class="' + cls + '" title="' + esc(t('The keyset packets to this TAR use - a non-zero MSL requires one')) + '">' +
'<option value=""' + (sel ? '' : ' selected') + '>' + esc(t('— none —')) + '</option>' +
opts.map(o => '<option value="' + o.v + '"' + (o.v === sel ? ' selected' : '') + '>' +
esc(t('Keyset') + ' ' + o.v + ' \u2014 ' + o.kic + '/' + o.kid) + '</option>').join('') +
'</select>';
if (!sel && msl && msl !== '00') {
html += '<span class="text-amber-500" title="' +
esc(t('This TAR needs a keyset (its MSL requires security)')) + '">\u26a0</span>';
}
if (role) {
html += '<span class="text-xs text-gray-500 dark:text-slate-400">' + esc(CARDS_TAR_ROLE_DESC[role] || role) + '</span>';
} else {
@@ -15006,15 +15023,19 @@ function cardsOutfileTarRender(rows) {
}
// The keyset numbers the parsed cards carry (the global default's options).
function cardsOutfileKeysets() {
const out = [];
// The parsed keysets as the TAR rows' options: the union across the parsed
// cards, with their KIc/KID bytes for the labels (v3.29.1).
function cardsOutfileKeysetOptions() {
const seen = {};
(((_cardsOutfile || {}).preview || {}).presets || []).forEach(c => {
(c.keysets || []).forEach(ks => {
const v = spKeysetKvnOf(ks.kic);
if (v !== null && !out.includes(v)) out.push(v);
if (v !== null && !seen[v]) {
seen[v] = { v: v, kic: ks.kic || '??', kid: ks.kid || '??' };
}
});
});
return out.sort((a, b) => a - b);
return Object.keys(seen).map(k => seen[k]).sort((a, b) => a.v - b.v);
}
function cardsOutfileResultHtml() {
@@ -15059,15 +15080,6 @@ function cardsOutfileRender() {
const panel = document.getElementById('cards-outfile-panel');
const o = _cardsOutfile;
if (!panel || !o) return;
const keysets = cardsOutfileKeysets();
// An empty or failed preview must not drop a chosen keyset: the value is
// only re-selected against a list that has keysets, and the option list is
// the preview's keysets plus a remembered value (so it stays visible).
if (keysets.length && !keysets.includes(o.tarKeyset)) o.tarKeyset = keysets[0];
const options = keysets.slice();
if (o.tarKeyset && !options.includes(o.tarKeyset)) options.push(o.tarKeyset);
options.sort((a, b) => a - b);
const selected = o.tarKeyset;
let html = '<div class="flex items-center justify-between mb-2">';
html += '<span class="text-sm font-semibold">' + esc(t('outfile import')) +
': <span class="font-mono text-xs">' + esc(o.file || '') + '</span></span>';
@@ -15080,11 +15092,6 @@ function cardsOutfileRender() {
'<select id="cards-of-algo" onchange="cardsOutfilePreview()" class="block mt-0.5 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1 dark:bg-slate-800">' +
'<option value="3des2"' + (o.algorithm16 === 'aes' ? '' : ' selected') + '>3DES2</option>' +
'<option value="aes"' + (o.algorithm16 === 'aes' ? ' selected' : '') + '>AES</option></select></label>';
html += '<label class="text-xs text-gray-600 dark:text-slate-400">' + esc(t('Default keyset for the TARs')) +
'<select id="cards-of-keyset" onchange="cardsOutfilePreview()" class="block mt-0.5 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1 dark:bg-slate-800">' +
(options.length ? options.map(v => '<option value="' + v + '"' + (v === selected ? ' selected' : '') + '>' + esc(t('Keyset') + ' ' + v) + '</option>').join('')
: '<option value="">\u2014</option>') +
'</select></label>';
html += '<label class="text-xs text-gray-600 dark:text-slate-400">' + esc(t('Import mode')) +
'<select id="cards-of-mode" class="block mt-0.5 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1 dark:bg-slate-800">' +
'<option value="merge"' + (o.mode === 'replace' ? '' : ' selected') + '>' + esc(t('Merge')) + '</option>' +
@@ -15110,7 +15117,6 @@ async function cardsOutfilePreview() {
text: o.text, name: o.name, algorithm16: o.algorithm16,
tars: cardsOutfileTarsFromForm(),
};
if (o.tarKeyset) body.tar_keyset = o.tarKeyset;
try {
o.preview = await pysimFetchOk('/api/presets/import-outfile', body);
} catch (e) {
@@ -15131,7 +15137,6 @@ async function cardsOutfileImport() {
text: o.text, name: o.name, algorithm16: o.algorithm16,
tars: cardsOutfileTarsFromForm(), mode: o.mode,
};
if (o.tarKeyset) body.tar_keyset = o.tarKeyset;
try {
const res = await pysimFetchOk('/api/presets/import-outfile', body);
await cardsFetch();
@@ -23632,7 +23637,7 @@ const LANG_RU = {
'outfile import': 'Импорт outfile',
'Common name': 'Общее имя',
'16-byte keys': '16-байтные ключи',
'Default keyset for the TARs': 'Keyset по умолчанию для TAR',
'This TAR needs a keyset (its MSL requires security)': 'Этому TAR нужен keyset (его MSL требует защиты)',
'Import mode': 'Режим импорта',
'Merge': 'Дополнить',
'Replace': 'Заменить',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v447';
const CACHE = 'simple-v448';
const URLS = [
'index.html',
'help.html',
+53 -35
View File
@@ -31,7 +31,7 @@ const ASYNC_FNS = ['cardsOutfileFile', 'cardsOutfilePreview', 'cardsOutfileImpor
let code = '';
for (const fn of ['cardsOutfileTarsFromDom', 'cardsOutfileTarsFromForm',
'cardsOutfileTarRowHtml', 'cardsOutfileTarRowsHtml', 'cardsOutfileTarAdd',
'cardsOutfileTarRemove', 'cardsOutfileTarRender', 'cardsOutfileKeysets',
'cardsOutfileTarRemove', 'cardsOutfileTarRender', 'cardsOutfileKeysetOptions',
'cardsOutfileResultHtml', 'cardsOutfileControls', 'cardsOutfileClose',
'cardsOutfileRender', 'cardsOutfileFile', 'cardsOutfilePreview',
'cardsOutfileImport', 'esc', 'escHtml', 'cardsRoleDefault', 'spKeysetKvnOf']) {
@@ -71,7 +71,7 @@ const app = new Function('els', `
sandbox.cardsOutfileTarAdd = cardsOutfileTarAdd;
sandbox.cardsOutfileTarRemove = cardsOutfileTarRemove;
sandbox.cardsOutfileTarRender = cardsOutfileTarRender;
sandbox.cardsOutfileKeysets = cardsOutfileKeysets;
sandbox.cardsOutfileKeysetOptions = cardsOutfileKeysetOptions;
sandbox.cardsOutfileResultHtml = cardsOutfileResultHtml;
sandbox.cardsOutfileControls = cardsOutfileControls;
sandbox.cardsOutfileClose = cardsOutfileClose;
@@ -107,9 +107,11 @@ test('the preview and the import both post the outfile endpoint', () => {
assert.match(preview, /'\/api\/presets\/import-outfile'/);
assert.match(imp, /'\/api\/presets\/import-outfile'/);
assert.match(imp, /mode: o\.mode/);
// the keyset is only sent when one was resolved
assert.match(preview, /if \(o\.tarKeyset\) body\.tar_keyset = o\.tarKeyset;/);
assert.match(imp, /if \(o\.tarKeyset\) body\.tar_keyset = o\.tarKeyset;/);
// the keysets travel per TAR row (v3.29.1): no global tar_keyset
assert.ok(!preview.includes('tar_keyset'), 'the preview must not send a global keyset');
assert.ok(!imp.includes('tar_keyset'), 'the import must not send a global keyset');
assert.match(preview, /tars: cardsOutfileTarsFromForm\(\)/);
assert.match(imp, /tars: cardsOutfileTarsFromForm\(\), mode: o\.mode/);
});
test('the 16-byte algorithm values match the server map', () => {
@@ -159,20 +161,33 @@ test('the TAR rows render the role ids and the free rows with a remove button',
assert.match(rows2, /id="cards-of-tar-3-tar"/);
assert.match(rows2, /id="cards-of-tar-3-desc"/);
assert.match(rows2, /onclick="cardsOutfileTarRemove\(3\)"/);
// every row has a keyset select, fed by the parsed keysets (v3.29.1)
assert.match(rows2, /id="cards-of-tar-0-keyset"/);
assert.match(rows2, /id="cards-of-tar-3-keyset"/);
app.state.preview = { presets: [{ keysets: [{ kic: '15', kid: '15' }, { kic: '25', kid: '25' }] }] };
const rows3 = app.cardsOutfileTarRowsHtml();
assert.match(rows3, /<option value="1" selected>Keyset 1 — 15\/15<\/option>/);
assert.match(rows3, /<option value="2">Keyset 2 — 25\/25<\/option>/);
// a non-zero MSL without a keyset gets the amber marker
app.state.preview = { presets: [] };
const rows4 = app.cardsOutfileTarRowsHtml();
assert.match(rows4, /This TAR needs a keyset/);
});
test('cardsOutfileKeysets collects the parsed keyset numbers', () => {
test('cardsOutfileKeysetOptions collects the parsed keysets with their bytes', () => {
clearEls();
app.state = {
text: TEXT, name: 'x', algorithm16: '3des2', tarKeyset: null, mode: 'merge',
text: TEXT, name: 'x', algorithm16: '3des2', mode: 'merge',
preview: { presets: [
{ keysets: [{ kic: '25' }, { kic: '15' }] },
{ keysets: [{ kic: '15' }, { kic: '31' }] },
{ keysets: [{ kic: '25', kid: '25' }, { kic: '15', kid: '15' }] },
{ keysets: [{ kic: '15', kid: '15' }, { kic: '31', kid: '31' }] },
] },
};
assert.deepStrictEqual(app.cardsOutfileKeysets(), [1, 2, 3]);
assert.deepStrictEqual(app.cardsOutfileKeysetOptions(),
[{ v: 1, kic: '15', kid: '15' }, { v: 2, kic: '25', kid: '25' },
{ v: 3, kic: '31', kid: '31' }]);
app.state.preview = { presets: [] };
assert.deepStrictEqual(app.cardsOutfileKeysets(), []);
assert.deepStrictEqual(app.cardsOutfileKeysetOptions(), []);
});
test('cardsOutfileResultHtml renders the report and the parsed cards', () => {
@@ -203,16 +218,14 @@ test('cardsOutfileResultHtml renders the report and the parsed cards', () => {
test('cardsOutfileControls reads the panel controls back into the state', () => {
clearEls();
app.state = { text: TEXT, name: 'old', algorithm16: '3des2', tarKeyset: null, mode: 'merge', preview: null };
app.state = { text: TEXT, name: 'old', algorithm16: '3des2', mode: 'merge', preview: null };
setEl('cards-of-name', ' Batch 7 ');
setEl('cards-of-algo', 'aes');
setEl('cards-of-mode', 'replace');
setEl('cards-of-keyset', '2');
app.cardsOutfileControls();
assert.strictEqual(app.state.name, 'Batch 7');
assert.strictEqual(app.state.algorithm16, 'aes');
assert.strictEqual(app.state.mode, 'replace');
assert.strictEqual(app.state.tarKeyset, 2);
});
test('picking a file previews it with the file name and the TAR defaults', async () => {
@@ -231,9 +244,12 @@ test('picking a file previews it with the file name and the TAR defaults', async
assert.strictEqual(body.name, 'batch'); // the file base name
assert.strictEqual(body.algorithm16, '3des2');
assert.strictEqual(body.mode, undefined); // the preview is a dry run
assert.strictEqual(body.tar_keyset, undefined); // none resolved yet
assert.strictEqual(body.tar_keyset, undefined); // the keysets are per row now
assert.strictEqual(body.tars.length, 3);
assert.strictEqual(app.state.tarKeyset, 1); // the first parsed keyset
// the rows' keyset selects are filled from the parsed keysets (the first one
// is the default)
assert.match(els['cards-outfile-panel'].innerHTML, /id="cards-of-tar-0-keyset"/);
assert.match(els['cards-outfile-panel'].innerHTML, /<option value="1" selected>Keyset 1 — 15\/15<\/option>/);
assert.match(els['cards-outfile-panel'].innerHTML, /8970119000004600098/);
});
@@ -259,11 +275,13 @@ test('the import posts the chosen mode and refreshes the list', async () => {
setEl('cards-of-mode', 'replace');
setEl('cards-of-name', 'Batch 7');
app.state = { text: TEXT, file: 'batch.out', name: 'Batch 7', algorithm16: '3des2',
tarKeyset: 2, mode: 'merge', preview: { presets: [], report: {} } };
mode: 'merge', preview: { presets: [], report: {} } };
setEl('cards-of-tar-0-keyset', '2'); // the ISD row's chosen keyset
await app.cardsOutfileImport();
assert.strictEqual(app.calls.length, 1);
assert.strictEqual(app.calls[0].body.mode, 'replace');
assert.strictEqual(app.calls[0].body.tar_keyset, 2);
assert.strictEqual(app.calls[0].body.tar_keyset, undefined); // per row, not global
assert.strictEqual(app.calls[0].body.tars[0].keyset, 2);
assert.strictEqual(app.calls[0].body.name, 'Batch 7');
assert.strictEqual(app.fetched, 1);
assert.deepStrictEqual(app.io, [['cards-io', 'Imported 2 cards (1 skipped).']]);
@@ -292,22 +310,22 @@ test('a declined replace confirmation aborts the import', async () => {
assert.strictEqual(app.calls.length, 1);
});
test('a chosen keyset survives an empty (failed) preview', () => {
test('a row keeps its chosen keyset through the DOM round-trip', () => {
clearEls();
setEl('cards-outfile-panel', '');
app.state = { text: TEXT, file: 'a.out', name: 'x', algorithm16: '3des2',
tarKeyset: 2, mode: 'merge',
preview: { presets: [{ iccid: '8970119000004600098', keysets: [{ kic: '15' }, { kic: '25' }] }], report: {} } };
app.cardsOutfileRender();
assert.strictEqual(app.state.tarKeyset, 2);
assert.match(els['cards-outfile-panel'].innerHTML, /<option value="2" selected>/);
// a failed re-preview has no keysets - the choice must not be dropped
app.state.preview = { report: { error: 'TAR must be three hex bytes: ZZ' } };
app.cardsOutfileRender();
assert.strictEqual(app.state.tarKeyset, 2);
assert.match(els['cards-outfile-panel'].innerHTML, /<option value="2" selected>/);
// a keyset the new preview does not define falls back to its first one
app.state.preview = { presets: [{ iccid: '8970119000004600098', keysets: [{ kic: '15' }] }], report: {} };
app.cardsOutfileRender();
assert.strictEqual(app.state.tarKeyset, 1);
app.state = { text: TEXT, name: 'x', algorithm16: '3des2', mode: 'merge',
preview: { presets: [{ keysets: [{ kic: '15', kid: '15' }, { kic: '25', kid: '25' }] }] } };
app.tars = 0;
setEl('cards-of-tar-0-keyset', '2'); // the user picked keyset 2 for the ISD row
setEl('cards-of-tar-1-keyset', '');
const tars = app.cardsOutfileTarsFromForm();
assert.strictEqual(tars[0].keyset, 2);
assert.strictEqual(tars[1].keyset, null);
// a free row carries its own keyset too
app.tars = 1;
setEl('cards-of-tar-3-tar', 'b00011');
setEl('cards-of-tar-3-msl', '1a');
setEl('cards-of-tar-3-keyset', '1');
const tars2 = app.cardsOutfileTarsFromForm();
assert.strictEqual(tars2.length, 4);
assert.strictEqual(tars2[3].keyset, 1);
});