From 21d7f58f6169524d7bdfaec84c48d4e6f5e04437 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Wed, 14 Oct 2026 13:50:30 +0300 Subject: [PATCH] v3.27.0 review: the outfile import hardening - The endpoint validates algorithm16 (3des2/aes, case-insensitive) and mode (merge/replace or omitted): a misspelled value answers 400 instead of silently picking 3DES2 or turning an import into a dry run. The choices come from outfile.ALGORITHM16_CHOICES, derived from the algorithm map, so the validation and the parser cannot drift apart. - A present but unusable PIN/PUK value is reported with its raw text: a value that strips to nothing (e.g. 'abcd') used to vanish without a report line, contradicting the "nothing is silently dropped" contract. A column absent from a short row stays silent - it is not present. - The column-line detection drops the arbitrary 8-character column-name cap: a candidate header must carry ICCID plus at least one column the import consumes (a fixed name or a numbered KIC/KID), so a long unknown column name next to a real one parses while 'ICCID ' and 'ICCID CUSTOMER_NAME' config lines are still rejected. Both real samples are unaffected. - PWA: a chosen default keyset survives an empty or failed preview (it used to reset to null, and the import silently fell back to the card's first keyset); the select renders the preview's keysets plus a remembered value. - PWA: the destructive Replace mode asks for confirmation first, matching the other store-wiping actions. - api.md documents both validations. Tests: the endpoint's 400s and the replace import, the raw-value report, the header guards, the keyset preservation and the declined confirmation. 906 frontend + 904 Python; sw cache simple-v437. --- docs/api.md | 8 +++-- frontend/index.html | 15 +++++++-- frontend/sw.js | 2 +- frontend/tests/cards_outfile.test.js | 46 +++++++++++++++++++++++++- pysim_simple_server/outfile.py | 49 ++++++++++++++++++++-------- pysim_simple_server/server.py | 24 ++++++++++++-- tests/test_fetch_routes.py | 15 +++++++++ tests/test_outfile.py | 27 +++++++++++++++ 8 files changed, 163 insertions(+), 23 deletions(-) diff --git a/docs/api.md b/docs/api.md index e91d628..9bc34ff 100644 --- a/docs/api.md +++ b/docs/api.md @@ -307,7 +307,9 @@ ignored. Without a `mode` the call is a **dry run**: the parsed presets and the report come back for the preview (nothing is stored). With `mode: "merge"` / `"replace"` the same parse runs and the presets are imported through the -store (`replace` wipes the store first, like `/api/presets/import`). +store (`replace` wipes the store first, like `/api/presets/import`); any other +`mode` value is refused with `400` — a typo must not silently turn an import +into a preview. ```json {"text": "…", "name": "Batch 0002", "algorithm16": "3des2", @@ -322,7 +324,9 @@ store (`replace` wipes the store first, like `/api/presets/import`). - `name` — one common name applied to every imported card; empty falls back to the card's ICCID. - `algorithm16` — the algorithm nibble for a **16-byte** key: `"3des2"` - (default, KIc/KID nibble `5`) or `"aes"` (nibble `2`). The key size alone + (default, KIc/KID nibble `5`) or `"aes"` (nibble `2`); any other value is + refused with `400` (a misspelled algorithm must never silently pick one — + the nibble decides whether the card accepts the packet). The key size alone decides the others: 8 bytes → DES (`1`), 24 bytes → 3DES3 (`9`); any other size skips that keyset with a report line. - `tars` — the TAR table every imported card gets (defaults to the three role diff --git a/frontend/index.html b/frontend/index.html index d93d63b..cc080d0 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -14731,8 +14731,14 @@ function cardsOutfileRender() { const o = _cardsOutfile; if (!panel || !o) return; const keysets = cardsOutfileKeysets(); - const selected = keysets.includes(o.tarKeyset) ? o.tarKeyset : (keysets[0] || null); - o.tarKeyset = selected; + // 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 = '
'; html += '' + esc(t('outfile import')) + ': ' + esc(o.file || '') + ''; @@ -14747,7 +14753,7 @@ function cardsOutfileRender() { ''; html += ''; html += '