From 92506e72b9e764bd4e517d760070afc7cd4e99c7 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: Sun, 4 Oct 2026 20:57:38 +0300 Subject: [PATCH] v3.19.0: the applet-agent script batch - por objects, alpha/sms checks, scp80 format The test scripts get the pieces an applet test needed: - scp80 check.por accepts an object {status?, sw?, data?} asserting the decoded PoR of this very exchange (inline or via SEND SHORT MESSAGE); the evaluation is shared with the expect-step por check (_por_check_results). The editor's PoR select gains 'custom' with the three fields (an empty object refuses on save). - a new alpha content check reads the command's Alpha identifier (05/85, Annex A coded) - the text check stays bound to the 8D Text string, so a command carrying both keeps them distinct. - a new sms content check asserts a SEND SHORT MESSAGE's submit TPDU: DA / PID / DCS / UDL / UD (the parsed fields are logged and reported; an applet's own SMS, not the OTA PoR the por check decodes). - scp80.params.format wraps the C-APDU in the TS 102 226 5.2.1 Command Scripting template (expanded = AA..., expanded-ae = AE 80...00 00); the log shows WRAPPED=, the report C-APDU= format=. - docs: Worksheet 1 addresses an RFM TAR (B00000/B00001/B00010 compact, B00120/B00140/B00130 expanded) with the own-TAR and MSL/spi1 notes, and file-write is documented as a direct APDU that does not fire EVENT_REMOTE_FILE_UPDATE. Tests: 819 frontend + 724 Python; sw cache simple-v387. --- docs/api.md | 20 ++- docs/test-scripts.md | 53 +++++-- frontend/help-ru.html | 4 +- frontend/help.html | 4 +- frontend/index.html | 101 ++++++++++++-- frontend/sw.js | 2 +- frontend/tests/testscripts.test.js | 92 ++++++++++++ pyproject.toml | 2 +- pysim_simple_server/server.py | 164 +++++++++++++++++----- pysim_simple_server/testscript.py | 73 ++++++++-- tests/test_testscript.py | 215 +++++++++++++++++++++++++++++ 11 files changed, 650 insertions(+), 80 deletions(-) diff --git a/docs/api.md b/docs/api.md index 3b8daaa..5bdb4a0 100644 --- a/docs/api.md +++ b/docs/api.md @@ -677,25 +677,33 @@ SET UP MENU, which is refreshed whenever the card sends the menu), `file-write` verifies `check.data`), `apdu` (raw transport, no auto-handler), `scp80` (`apdu` or `sp`, selected by the optional `source` field when both are present; optional `kvn` (keyset -number 1-15), `tar`/`spi1`/`spi2` overrides - KIc/KID and the counter always +number 1-15), `tar`/`spi1`/`spi2` overrides and `format` (`compact` default / +`expanded` / `expanded-ae` - the TS 102 226 5.2.1 Command Scripting template +around the C-APDU) - KIc/KID and the counter always come from the resolved preset, which must match the equipped card and be complete) or `status` (`attempts`, `interval_ms` - when `attempts > 1` the default SW check is the mask `91??`, i.e. poll until the card announces a command). `check` is `{"sw": ..., "data": ...}` (exact or `{"mode": "mask", "value": -"91??"}`, `?` = per-nibble wildcard) plus `"por": "none"|"ok"|"any"` for -SCP80. `on_fail` is `error` (terminates the script) or `warning` (continues). +"91??"}`, `?` = per-nibble wildcard) plus `"por"` for SCP80 - +`"none"`/`"ok"`/`"any"` or an object `{"status"?, "sw"?, "data"?}` asserting +the decoded PoR of this exchange (inline or SEND SHORT MESSAGE; at least one +field). `on_fail` is `error` (terminates the script) or `warning` (continues). **Expectation steps** (`type: "expect"`) require a command pending from the previous step (`91XX`); they never poll - a `9000` response means no command and is an error (TS 102 221 7.4.2.1 / TS 102 223 6.3; add a `status` action if the card delivers on poll). `command` is a name or type code; `checks` -may be `text` (contains/exact), `item` (`id`/`text` for SELECT ITEM / SET UP -MENU), `raw` (mask), `por` (a SEND SHORT MESSAGE carrying the PoR of the +may be `text` (contains/exact), `alpha` (the command's Alpha identifier +`05`/`85`, contains/exact - distinct from the `8D` text string), `item` +(`id`/`text` for SELECT ITEM / SET UP MENU), `raw` (mask), `por` (a SEND +SHORT MESSAGE carrying the PoR of the preceding `scp80` step - PoR-in-submit, SPI2 bit `20`: `status` (the response status name, `ok` = `por_ok`), `sw` (the R-APDU status word, exact/mask) and -`data` (the R-APDU response data, exact/mask)) or `files` (the File List of a +`data` (the R-APDU response data, exact/mask)), `sms` (the SEND SHORT MESSAGE +TPDU: `da` digits, `pid`/`dcs`/`ud` hex exact/mask, `udl` decimal; SMS-SUBMIT +only) or `files` (the File List of a REFRESH, TS 102 223 8.18; `files` is a list of path hex strings, matched order-insensitively); `respond` is the TERMINAL RESPONSE in the TS 102 223 6.8.0 object order (`result` name or value; `text`+`dcs` for GET INKEY/GET diff --git a/docs/test-scripts.md b/docs/test-scripts.md index 3ab092e..4bd7b6d 100644 --- a/docs/test-scripts.md +++ b/docs/test-scripts.md @@ -14,8 +14,9 @@ localStorage export imports as-is), and any API client can create or run them **Format**: server ≥ 3.15.0 (`kvn`, `por` and `files` checks), scripts stored server-side since 3.16.0, `menu-select` by text and the spec-order TERMINAL -RESPONSE codings since 3.17.0, the semantic `event` action since 3.18.0 and -the `envelope` action's `src` override since 3.18.1. +RESPONSE codings since 3.17.0, the semantic `event` action since 3.18.0, the +`envelope` action's `src` override since 3.18.1, the inline `por` object, the +`alpha`/`sms` checks and `scp80.params.format` since 3.19.0. ## Writing a test for an applet @@ -24,9 +25,9 @@ and the card's preset): | What | Where it goes | Notes | |---|---|---| -| Applet TAR (3 bytes) + MSL, or an explicit SPI1 | `scp80.params.tar` / `spi1` | the card preset must carry the TAR with its MSL, or the step passes `spi1` | +| TAR to address (the RFM application or the applet's own) + MSL, or an explicit SPI1 | `scp80.params.tar` / `spi1` | the card preset must carry the TAR with its MSL, or the step passes `spi1`; RFM TARs: `B00000`/`B00001`/`B00010` compact, `B00120`/`B00140`/`B00130` expanded (see Worksheet 1) | | Keyset number | `scp80.params.kvn` (1–15) | only when the applet's keys are not the preset's first keyset | -| Trigger C-APDUs | `scp80.params.apdu` | one secured packet per `scp80` step; the preset provides keys and counter | +| Trigger C-APDUs (+ their format) | `scp80.params.apdu` / `format` | one secured packet per `scp80` step; the preset provides keys and counter, `format` wraps the APDU for an expanded-format application | | Expected file paths + contents | `file-read.params.path` + `check.data` | a mask must have **exactly** the actual value's length | | Expected proactive command(s) | `expect.command` + `qualifier`/`checks` | REFRESH `qualifier: "00"` = NAA init + full FCN, `files` asserts the changed EFs | | PoR transport + expected PoR | `scp80.check.por`/`check.data` (inline) or `expect SEND SHORT MESSAGE` + `por` check | pick per the SPI2 the applet expects | @@ -38,7 +39,7 @@ and the card's preset): {"name": "applet RFM update", "steps": [ {"type": "action", "kind": "scp80", - "params": {"apdu": "", "tar": "", "kvn": 1, "spi2": "01"}, + "params": {"apdu": "", "tar": "B00001", "kvn": 1, "spi2": "01"}, "check": {"sw": {"mode": "mask", "value": "91??"}, "por": "none"}, "label": "RFM update"}, {"type": "expect", "command": "REFRESH", "qualifier": "00", @@ -49,6 +50,17 @@ and the card's preset): ]} ``` +`tar` addresses the **RFM application that owns the file system**, not the +applet-under-test: `B00000` = UICC shared FS, `B00001` = ADF RFM, `B00010` = +SIM FS (compact), and `B00120`/`B00140`/`B00130` the expanded-format twins +(TS 101 220 Annex D; the probe list in the SCP80 tab carries the same names). +An applet that accepts secured packets under its **own** TAR - the TAR given +in its INSTALL [for install] parameters (TS 102 226 §8.2.1.3.2.7) - uses that +TAR instead. The preset must carry a TAR entry **with its MSL** or the step +passes an explicit `spi1`; an expanded-format RFM application also expects the +wrapped command template (`"format": "expanded"`/`"expanded-ae"`, see the +actions table). + ### Worksheet 2 - incoming data → particular PoR, no other actions Inline PoR: @@ -113,6 +125,9 @@ TEST-RUN step 4: MENU-SELECT ENVELOPE=80c2000009d30702020181900101 item=1 ('One' - A PoR delivered as SEND SHORT MESSAGE is logged on the step that fetches it: `PoR[sms-submit] …` followed by the same `R-APDU` line. +- A SEND SHORT MESSAGE expectation also logs its parsed TPDU + (`SMS DA=… PID=… DCS=… UDL=… UD=…`) after the `CMD` line, so an applet's own + SMS can be read even without an `sms` check. - Other actions log their send/response pair too (`APDU TX=… -> SW=…`, `ENVELOPE(Event Download) … -> SW=…`, `READ MF/… -> SW=…`). - The shared secured-packet sender logs each segment's answer @@ -166,12 +181,17 @@ continue correctly. | `file-write` | `path`, `data`, `mode` `auto`/`binary`/`record`, `record` 1–255 | UPDATE BINARY/RECORD | | `file-read` | `path`, `mode`, `record` | READ BINARY/RECORD; verify with `check.data` | | `apdu` | `apdu` hex | raw transport, no auto-handler | -| `scp80` | `apdu` **xor** `sp` (optional `source`), `kvn`, `tar`, `spi1`, `spi2` | secured packet to the TAR | +| `scp80` | `apdu` **xor** `sp` (optional `source`), `kvn`, `tar`, `spi1`, `spi2`, `format` | secured packet to the TAR; `format`: `compact` (default - the C-APDU verbatim), `expanded` (the `AA`-prefixed Command Scripting template) or `expanded-ae` (the `AE 80 … 00 00` form), TS 102 226 §5.2.1 | | `status` | `attempts` 1–1000, `interval_ms` 0–10000 | with `attempts > 1` the default SW check is `91??` (poll until a command) | `path` is `/`-separated: `MF` (or `3F00`) or an ADF name/AID first, then FIDs or file names - e.g. `MF/7F20/6F07`, `ADF.USIM/EF.TEST`. +`file-write` sends a direct terminal-side UPDATE BINARY/RECORD - it does **not** +fire the card's EVENT_REMOTE_FILE_UPDATE (that event reports an update done by +a remote entity through OTA/RFM, TS 102 223 §7.5.9): assert the change with +`file-read` or the REFRESH `files` check. + ### Semantic events (`kind: "event"`) The server builds the event objects for the events a test needs most; an @@ -208,7 +228,11 @@ the default is the terminal, matching the Phone tab's forms. `attempts > 1` defaults to the `91??` mask). - `data` - the response data (for SCP80: the inline PoR packet). - `por` (SCP80 actions only) - `"any"` (default, no check), `"ok"` - (`response_status == por_ok`), `"none"` (no PoR expected). + (`response_status == por_ok`), `"none"` (no PoR expected), or an **object** + `{"status"?, "sw"?, "data"?}` asserting the decoded PoR of this very + exchange (at least one field; the same fields as the expectation's `por` + check) - it works for both the inline and the SEND SHORT MESSAGE transport. + Example: `"por": {"status": "por_ok", "sw": "9000"}`. - A plain hex string is exact; a value containing `?` is a mask. **A mask must have exactly the same length as the actual value** (`?` is a per-nibble wildcard) - e.g. `91??` matches `9102`, `027100????` matches a 4-byte @@ -232,10 +256,12 @@ command-details byte 3 (one byte, exact/mask). | kind | fields | checks | |---|---|---| -| `text` | `mode` contains/exact, `value`, `case_sensitive` | alpha identifier / text | +| `text` | `mode` contains/exact, `value`, `case_sensitive` | the **Text string** (`8D`, DCS coded) of DISPLAY TEXT / GET INKEY / GET INPUT | +| `alpha` | `mode` contains/exact, `value`, `case_sensitive` | the command's **Alpha identifier** (tag `05`/`85`, TS 102 223 §8.2, Annex A coded) - distinct from `text` when a command carries both | | `item` | `id`, `text`, `mode`, `case_sensitive` | items of SELECT ITEM / SET UP MENU | | `raw` | `mode` exact/mask, `value` | the whole fetched command (hex, exact length needed) | | `por` | `status`, `sw`, `data` | the PoR carried by a **SEND SHORT MESSAGE** (see below) | +| `sms` | `da`, `pid`, `dcs`, `udl`, `ud` | the TPDU of a SEND SHORT MESSAGE (see below) | | `files` | `files` (list of path hex strings) or a comma/space separated string | the **File List** of a REFRESH, TS 102 223 §8.18; matched order-insensitively | `por` needs an SCP80 step **before** the expectation: the TPDU of the SEND @@ -248,6 +274,14 @@ several SMS parts is accumulated across SEND SHORT MESSAGE expectations: put the `por` check on the step that completes the sequence (intermediate steps should use `raw` or no PoR check). +`sms` parses the fetched SEND SHORT MESSAGE's TPDU (SMS-SUBMIT only - any other +MTI fails the check): `da` the destination digits (exact, whitespace ignored), +`pid`/`dcs` single bytes and `ud` the raw user data (a UDH included) as hex +exact/mask, `udl` the user-data length in decimal (octets, or septets for the +7-bit alphabet - whatever the TPDU carries). Use it for an applet's own SMS; +the OTA PoR of the preceding scp80 step is what `por` decodes. At least one +field is required. + ### `respond` The TERMINAL RESPONSE for the fetched command, in the TS 102 223 §6.8.0 @@ -275,7 +309,8 @@ UCS2 answer to a GET INPUT: 1. **Inline** - the card answers the ENVELOPE `9000` with the PoR in the response data. Verify on the action: `"por": "ok"` and/or - `"data": {"mode": "mask", "value": "…"}`. + `"data": {"mode": "mask", "value": "…"}`, or assert the decoded PoR with + `"por": {"status": "por_ok", "sw": "9000"}`. 2. **PoR-in-submit** (SPI2 bit `20`, e.g. `spi2: "21"`) - the card announces a **SEND SHORT MESSAGE** (`91XX`); the PoR is inside its SMS TPDU: diff --git a/frontend/help-ru.html b/frontend/help-ru.html index 81e39f1..21cd1a3 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -563,8 +563,8 @@

8.9 Тест-скрипт

Тест-скрипт — упорядоченный список действий и ожиданий проактивных команд, выполняемый на подключённой карте на стороне сервера. Пока запуск активен, карта принадлежит скрипту: остальные операции с картой отклоняются (HTTP 409), фоновый опрос STATUS приостанавливается; отчёт о запуске хранится в памяти (в том числе после перезагрузки страницы) до нажатия Очистить отчёт. Скрипты хранятся в хранилище на сервере (~/.pysim-simple-server/test_scripts.json, --test-scripts), поэтому все браузеры работают с одним набором; изменения записываются в хранилище по мере правки (кнопки «Сохранить» нет — черновик без валидного шага остаётся локальным); экспорт/импорт JSON остаётся для резервных копий и переноса скриптов между стендами (старый экспорт из localStorage импортируется как есть).

    -
  • Действия — Envelope (Event Download: сырой тип события + данные, с опциональным переопределением источника — 82 терминал / 83 сеть) или Event (семантические формы: Location status, Access technology change, Network rejection, Data connection status change — поля редактируются как объект JSON в редакторе шагов, а для остальных событий форма события на вкладке «Телефон» показывает готовый hex), Menu Selection (item id 1–255 или текст пункта с точным/частичным совпадением — текст подбирается по меню карты, которое следует за параметрами установки апплета), UPDATE/READ файла (путь + режим; чтение проверяет данные файла), сырой APDU (без авто-обработчика), SCP80 (C-APDU или готовый защищённый пакет, опциональные переопределения номера keyset (kvn) / TAR / SPI1 / SPI2; ключи и счётчик берутся из пресета карты, подобранного по ICCID подключённой карты — запуск называет пресет в хранилище, поэтому всегда используются его keysets, TAR и счётчик — а счётчик записывается обратно после запуска) и STATUS (при попытках > 1 опрашивает, пока карта не объявит команду). У каждого действия есть проверка ответа: ожидаемые SW и данные (точно или маска с подстановкой ? — 91??) и проверка PoR для SCP80 (любой / без PoR / por_ok).
  • -
  • Ожидания — карта должна объявить проактивную команду на предыдущем шаге (SW 91XX, TS 102 221 §7.4.2.1 / TS 102 223 §6.3); 9000 означает «команды нет» и считается ошибкой — добавьте действие STATUS, если апплет выдаёт команду при опросе. Проверяются тип команды, квалификатор и содержимое: текст / пункт / raw, проверка PoR (SEND SHORT MESSAGE несёт PoR, когда пакет запросил PoR-in-submit, бит SPI2 20: статус ответа, SW R-APDU и данные ответа; перед этим должен выполниться шаг SCP80) и проверка Files (File List команды REFRESH, например квалификатор 00 = NAA initialization with full file change notification). TERMINAL RESPONSE (результат, идентификатор пункта, текст, доп. TLV) является частью инструкции.
  • +
  • Действия — Envelope (Event Download: сырой тип события + данные, с опциональным переопределением источника — 82 терминал / 83 сеть) или Event (семантические формы: Location status, Access technology change, Network rejection, Data connection status change — поля редактируются как объект JSON в редакторе шагов, а для остальных событий форма события на вкладке «Телефон» показывает готовый hex), Menu Selection (item id 1–255 или текст пункта с точным/частичным совпадением — текст подбирается по меню карты, которое следует за параметрами установки апплета), UPDATE/READ файла (путь + режим; чтение проверяет данные файла), сырой APDU (без авто-обработчика), SCP80 (C-APDU или готовый защищённый пакет, опциональные переопределения номера keyset (kvn) / TAR / SPI1 / SPI2 и формат команды — компактный или обёрнутый в шаблон Command Scripting TS 102 226 §5.2.1 expanded (AA) / expanded AE 80; ключи и счётчик берутся из пресета карты, подобранного по ICCID подключённой карты — запуск называет пресет в хранилище, поэтому всегда используются его keysets, TAR и счётчик — а счётчик записывается обратно после запуска) и STATUS (при попытках > 1 опрашивает, пока карта не объявит команду). У каждого действия есть проверка ответа: ожидаемые SW и данные (точно или маска с подстановкой ? — 91??) и проверка PoR для SCP80 (любой / без PoR / por_ok или своя проверка декодированного PoR — статус / SW / данные; транспорт через SEND SHORT MESSAGE работает так же).
  • +
  • Ожидания — карта должна объявить проактивную команду на предыдущем шаге (SW 91XX, TS 102 221 §7.4.2.1 / TS 102 223 §6.3); 9000 означает «команды нет» и считается ошибкой — добавьте действие STATUS, если апплет выдаёт команду при опросе. Проверяются тип команды, квалификатор и содержимое: текст / пункт / raw, проверка Alpha (идентификатор Alpha команды, тег 05/85 — отличается от текстовой строки 8D), проверка SMS TPDU (submit-TPDU команды SEND SHORT MESSAGE: получатель / PID / DCS / UDL / данные — для собственного SMS апплета), проверка PoR (SEND SHORT MESSAGE несёт PoR, когда пакет запросил PoR-in-submit, бит SPI2 20: статус ответа, SW R-APDU и данные ответа; перед этим должен выполниться шаг SCP80) и проверка Files (File List команды REFRESH, например квалификатор 00 = NAA initialization with full file change notification). TERMINAL RESPONSE (результат, идентификатор пункта, текст, доп. TLV) является частью инструкции.
  • Уровни ошибок — несовпадение помечает шаг как Ошибка (скрипт останавливается) или Предупреждение (продолжает); каждая проверка показывается с ожидаемым и фактическим значением.
  • Шаблоны — Empty, Обзор меню STK, Апплет через SCP80; редактор шагов — формный (поля действия Event вводятся объектом JSON), отдельные скрипты экспортируются/импортируются как JSON.
diff --git a/frontend/help.html b/frontend/help.html index d4f17ad..5b663e4 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -562,8 +562,8 @@

8.9 Test script

A test script is an ordered list of actions and proactive-command expectations executed server-side on the equipped card. While a run is active the card belongs to the script: other card actions are refused (HTTP 409) and background STATUS polling is paused; the run report is kept in memory (also after reloading the page) until Clear report. Scripts live in the server-side store (~/.pysim-simple-server/test_scripts.json, --test-scripts), so every browser manages the same set; edits are written to the store as you change them (there is no Save button — a draft without a valid step stays local until it is valid); JSON export/import remains for backup and for moving scripts between benches (the old localStorage export imports as-is).

    -
  • Actions — Envelope (Event Download: raw event type + data, with an optional source override — 82 terminal / 83 network) or Event (the semantic forms: Location status, Access technology change, Network rejection, Data connection status change — the fields are edited as a JSON object in the step editor, and the Phone tab's event form shows the built hex for the other events), Menu Selection (item id 1–255, or the item's text with an exact/contains match — the text is resolved against the card's menu, which follows the applet's install parameters), UPDATE/READ file (path + mode; reads verify the file data), raw APDU (no auto-handler), SCP80 (C-APDU or a pre-built secured packet, optional keyset number (kvn) / TAR / SPI1 / SPI2 overrides; the keys and the counter come from the card preset matched by the equipped card's ICCID — the run names the stored preset, so it always uses its keysets, TARs and counter — and the counter is written back to it after the run) and STATUS (with attempts > 1 it polls until the card announces a command). Every action has a response check: expected SW and data (exact, or a mask with ? nibble wildcards — 91??) plus a PoR check for SCP80 (any / no PoR / por_ok).
  • -
  • Expectations — the card must have announced a proactive command in the previous step (SW 91XX, TS 102 221 §7.4.2.1 / TS 102 223 §6.3); a 9000 means “no command” and is an error — add a STATUS action when the applet delivers on poll. The command type, qualifier and contents are verified: text / item / raw checks, a PoR check (a SEND SHORT MESSAGE carries the PoR when the packet requested PoR-in-submit, SPI2 bit 20: the response status, the R-APDU SW and the response data; make sure an SCP80 step ran first) and a Files check (the File List of a REFRESH, e.g. qualifier 00 = NAA initialization with full file change notification). The scripted TERMINAL RESPONSE (result, item identifier, text, extra TLVs) is part of the instruction.
  • +
  • Actions — Envelope (Event Download: raw event type + data, with an optional source override — 82 terminal / 83 network) or Event (the semantic forms: Location status, Access technology change, Network rejection, Data connection status change — the fields are edited as a JSON object in the step editor, and the Phone tab's event form shows the built hex for the other events), Menu Selection (item id 1–255, or the item's text with an exact/contains match — the text is resolved against the card's menu, which follows the applet's install parameters), UPDATE/READ file (path + mode; reads verify the file data), raw APDU (no auto-handler), SCP80 (C-APDU or a pre-built secured packet, optional keyset number (kvn) / TAR / SPI1 / SPI2 overrides and a command format — compact, or the TS 102 226 §5.2.1 Command Scripting template expanded (AA) / expanded AE 80 around the C-APDU; the keys and the counter come from the card preset matched by the equipped card's ICCID — the run names the stored preset, so it always uses its keysets, TARs and counter — and the counter is written back to it after the run) and STATUS (with attempts > 1 it polls until the card announces a command). Every action has a response check: expected SW and data (exact, or a mask with ? nibble wildcards — 91??) plus a PoR check for SCP80 (any / no PoR / por_ok, or a custom assertion of the decoded PoR — status / SW / data; the SEND SHORT MESSAGE transport works the same).
  • +
  • Expectations — the card must have announced a proactive command in the previous step (SW 91XX, TS 102 221 §7.4.2.1 / TS 102 223 §6.3); a 9000 means “no command” and is an error — add a STATUS action when the applet delivers on poll. The command type, qualifier and contents are verified: text / item / raw checks, an Alpha check (the command's Alpha identifier, tag 05/85 — distinct from the 8D text string), an SMS TPDU check (the submit TPDU of a SEND SHORT MESSAGE: destination / PID / DCS / UDL / user data — for an applet's own SMS), a PoR check (a SEND SHORT MESSAGE carries the PoR when the packet requested PoR-in-submit, SPI2 bit 20: the response status, the R-APDU SW and the response data; make sure an SCP80 step ran first) and a Files check (the File List of a REFRESH, e.g. qualifier 00 = NAA initialization with full file change notification). The scripted TERMINAL RESPONSE (result, item identifier, text, extra TLVs) is part of the instruction.
  • Fail levels — a mismatch flags the step as Error (terminates the script) or Warning (continues); each check is reported with expected vs actual values.
  • Templates — Empty, STK menu browsing, Applet via SCP80; the step editor is form-based (the event action's fields are a JSON object) and individual scripts export/import as JSON.
diff --git a/frontend/index.html b/frontend/index.html index 4748722..5794b6d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1877,7 +1877,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.18.4'; +const SIMPLE_VERSION = '3.19.0'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== @@ -7040,9 +7040,18 @@ function testScriptProblem(script) { return ''; } +function testPorSpecSummary(por) { + const parts = []; + if (por.status) parts.push('status ' + por.status); + if (por.sw) parts.push('SW ' + (por.sw.mode === 'mask' ? '~' : '') + por.sw.value); + if (por.data) parts.push('data ' + (por.data.mode === 'mask' ? '~' : '') + por.data.value); + return parts.join(' '); +} + function testCheckSummary(c) { if (!c || !c.kind) return ''; if (c.kind === 'text') return 'text ' + (c.mode === 'exact' ? '=' : '~') + JSON.stringify(c.value || ''); + if (c.kind === 'alpha') return 'alpha ' + (c.mode === 'exact' ? '=' : '~') + JSON.stringify(c.value || ''); if (c.kind === 'item') { let s = 'item'; if (c.id) s += ' #' + c.id; @@ -7050,12 +7059,15 @@ function testCheckSummary(c) { return s; } if (c.kind === 'raw') return 'raw ' + (c.mode === 'mask' ? '~' : '=') + (c.value || ''); - if (c.kind === 'por') { + if (c.kind === 'por') return 'por ' + testPorSpecSummary(c); + if (c.kind === 'sms') { const parts = []; - if (c.status) parts.push('status ' + c.status); - if (c.sw) parts.push('SW ' + (c.sw.mode === 'mask' ? '~' : '') + c.sw.value); - if (c.data) parts.push('data ' + (c.data.mode === 'mask' ? '~' : '') + c.data.value); - return 'por ' + parts.join(' '); + if (c.da) parts.push('DA ' + c.da); + if (c.pid) parts.push('PID ' + (c.pid.mode === 'mask' ? '~' : '') + c.pid.value); + if (c.dcs) parts.push('DCS ' + (c.dcs.mode === 'mask' ? '~' : '') + c.dcs.value); + if (c.udl !== undefined) parts.push('UDL ' + c.udl); + if (c.ud) parts.push('UD ' + (c.ud.mode === 'mask' ? '~' : '') + c.ud.value); + return 'sms ' + parts.join(' '); } if (c.kind === 'files') return 'files ' + (c.files || []).join(' '); return ''; @@ -7090,7 +7102,7 @@ function testStepSummary(step) { s = 'EVENT ' + (EVENT_NAMES[code] || ('0x' + String(p.event || '?'))); } else if (step.kind === 'apdu') s = 'APDU ' + (p.apdu || '?'); - else if (step.kind === 'scp80') s = 'SCP80 ' + ((p.source || (p.sp ? 'sp' : 'apdu')) === 'sp' ? (p.sp || '(secured packet)') : (p.apdu || '?')) + (p.tar ? ' TAR=' + p.tar : '') + (p.spi1 ? ' SPI1=' + p.spi1 : '') + (p.spi2 ? ' SPI2=' + p.spi2 : ''); + else if (step.kind === 'scp80') s = 'SCP80 ' + ((p.source || (p.sp ? 'sp' : 'apdu')) === 'sp' ? (p.sp || '(secured packet)') : (p.apdu || '?')) + (p.tar ? ' TAR=' + p.tar : '') + (p.spi1 ? ' SPI1=' + p.spi1 : '') + (p.spi2 ? ' SPI2=' + p.spi2 : '') + (p.format && p.format !== 'compact' ? ' fmt=' + p.format : ''); else if (step.kind === 'file-write') s = 'UPDATE ' + (p.path || '?') + (p.mode && p.mode !== 'auto' ? ' (' + p.mode + (p.record ? ' ' + p.record : '') + ')' : '') + ' = ' + (p.data || ''); else if (step.kind === 'file-read') s = 'READ ' + (p.path || '?') + (p.mode && p.mode !== 'auto' ? ' (' + p.mode + (p.record ? ' ' + p.record : '') + ')' : ''); else if (step.kind === 'status') s = 'STATUS' + ((p.attempts || 1) > 1 ? ' x' + p.attempts + ' (poll)' : ''); @@ -7099,7 +7111,9 @@ function testStepSummary(step) { const ck = []; if (c.sw && c.sw.value && !(c.sw.mode !== 'mask' && c.sw.value === '9000')) ck.push('SW ' + (c.sw.mode === 'mask' ? '~' : '') + c.sw.value); if (c.data && c.data.value) ck.push('data ' + (c.data.mode === 'mask' ? '~' : '') + c.data.value); - if (c.por && c.por !== 'any') ck.push('PoR ' + c.por); + if (c.por && c.por !== 'any') { + ck.push('PoR ' + (typeof c.por === 'string' ? c.por : testPorSpecSummary(c.por))); + } if (step.on_fail === 'warning') ck.push('⚠'); return s + (ck.length ? ' · ' + ck.join(', ') : ''); } @@ -7390,7 +7404,13 @@ function testStepRender() { html += testFormRow('Source', testFormSelect('test-f-src', src, [ {v: 'apdu', l: t('C-APDU (built with the preset)')}, {v: 'sp', l: t('Pre-built secured packet')}], 'testStepKindChanged()')); if (src === 'sp') html += testFormRow('Secured packet (hex)', testFormInput('test-f-sp', p.sp, '01 00 ...')); - else html += testFormRow('C-APDU (hex)', testFormInput('test-f-apdu', p.apdu, '80E2900000')); + else { + html += testFormRow('C-APDU (hex)', testFormInput('test-f-apdu', p.apdu, '80E2900000')); + html += testFormRow('Command format', testFormSelect('test-f-fmt', p.format || 'compact', [ + {v: 'compact', l: t('Compact')}, + {v: 'expanded', l: t('Expanded (AA template)')}, + {v: 'expanded-ae', l: t('Expanded (AE 80 template)')}])); + } html += testFormRow('Keyset number (optional)', testFormInput('test-f-kvn', p.kvn === undefined ? '' : p.kvn, t('empty = the preset\'s first keyset'))); html += testFormRow('TAR override (hex, optional)', testFormInput('test-f-tar', p.tar, 'B00000')); @@ -7410,8 +7430,15 @@ function testStepRender() { + testFormSelect('test-f-cdata-mode', cd.mode || 'exact', [{v: 'exact', l: t('Exact')}, {v: 'mask', l: t('Mask ("?" = wildcard)')}]) + testFormInput('test-f-cdata', cd.value, 'AA??') + ''); if (step.kind === 'scp80') { - html += testFormRow('PoR check', testFormSelect('test-f-por', c.por || 'any', [ - {v: 'any', l: t('Any')}, {v: 'none', l: t('No PoR expected')}, {v: 'ok', l: t('PoR = por_ok')}])); + const porObj = c.por && typeof c.por === 'object' ? c.por : null; + html += testFormRow('PoR check', testFormSelect('test-f-por', porObj ? 'custom' : (c.por || 'any'), [ + {v: 'any', l: t('Any')}, {v: 'none', l: t('No PoR expected')}, {v: 'ok', l: t('PoR = por_ok')}, + {v: 'custom', l: t('Custom (status / SW / data)')}], 'testStepKindChanged()')); + if (porObj) { + html += testFormRow('PoR status', testFormInput('test-f-porstatus', porObj.status, 'por_ok')); + html += testFormRow('PoR SW (hex)', testFormInput('test-f-por-sw', porObj.sw && porObj.sw.value, '6A88')); + html += testFormRow('PoR data (hex)', testFormInput('test-f-por-data', porObj.data && porObj.data.value, 'AA??')); + } } html += testFormRow('On mismatch', testFormSelect('test-f-fail', step.on_fail || 'error', [ {v: 'error', l: t('Error (stop the script)')}, {v: 'warning', l: t('Warning (continue)')}])); @@ -7478,7 +7505,8 @@ function testRenderChecks() { html += '
' + testFormSelect('test-check-kind-' + i, c.kind || 'text', [ {v: 'text', l: t('Text')}, {v: 'item', l: t('Item')}, {v: 'raw', l: t('Raw')}, - {v: 'por', l: t('PoR')}, {v: 'files', l: t('Files')}], 'testCheckKindChanged()'); + {v: 'alpha', l: t('Alpha')}, {v: 'por', l: t('PoR')}, {v: 'sms', l: t('SMS TPDU')}, + {v: 'files', l: t('Files')}], 'testCheckKindChanged()'); if (c.kind === 'item') { html += testFormInput('test-check-id-' + i, c.id, t('id')); html += testFormInput('test-check-text-' + i, c.text, t('text')); @@ -7491,6 +7519,12 @@ function testRenderChecks() { html += testFormInput('test-check-porstatus-' + i, c.status, 'por_ok'); html += testFormInput('test-check-por-sw-' + i, c.sw && c.sw.value, '6A88'); html += testFormInput('test-check-por-data-' + i, c.data && c.data.value, 'AA??'); + } else if (c.kind === 'sms') { + html += testFormInput('test-check-smsda-' + i, c.da, '12345'); + html += testFormInput('test-check-smspid-' + i, c.pid && c.pid.value, '7F'); + html += testFormInput('test-check-smsdcs-' + i, c.dcs && c.dcs.value, '00'); + html += testFormInput('test-check-smsudl-' + i, c.udl === undefined ? '' : c.udl, 'UDL'); + html += testFormInput('test-check-smsud-' + i, c.ud && c.ud.value, 'AA??'); } else if (c.kind === 'files') { html += testFormInput('test-check-files-' + i, (c.files || []).join(', '), '3F007F106F3A'); } else { @@ -7544,6 +7578,17 @@ function testChecksCollect() { if (sw) row.sw = {mode: sw.includes('?') ? 'mask' : 'exact', value: sw}; const data = val('test-check-por-data-' + i).replace(/[^0-9a-fA-F?]/g, '').toUpperCase(); if (data) row.data = {mode: data.includes('?') ? 'mask' : 'exact', value: data}; + } else if (kind === 'sms') { + const da = val('test-check-smsda-' + i); + if (da) row.da = da; + const pid = val('test-check-smspid-' + i).replace(/[^0-9a-fA-F?]/g, '').toUpperCase(); + if (pid) row.pid = {mode: pid.includes('?') ? 'mask' : 'exact', value: pid}; + const dcs = val('test-check-smsdcs-' + i).replace(/[^0-9a-fA-F?]/g, '').toUpperCase(); + if (dcs) row.dcs = {mode: dcs.includes('?') ? 'mask' : 'exact', value: dcs}; + const udl = val('test-check-smsudl-' + i); + if (udl !== '' && !isNaN(parseInt(udl, 10))) row.udl = parseInt(udl, 10); + const ud = val('test-check-smsud-' + i).replace(/[^0-9a-fA-F?]/g, '').toUpperCase(); + if (ud) row.ud = {mode: ud.includes('?') ? 'mask' : 'exact', value: ud}; } else if (kind === 'files') { row.files = val('test-check-files-' + i).split(/[,\s]+/) .map(s => s.replace(/[^0-9a-fA-F]/g, '').toUpperCase()).filter(Boolean); @@ -7621,6 +7666,8 @@ function testStepCollect() { ['tar', 'spi1', 'spi2'].forEach(k => { const v = hexNoMask('test-f-' + k); if (v) p[k] = v; }); const kvn = (document.getElementById('test-f-kvn') || {}).value; if (kvn !== undefined && String(kvn).trim() !== '') p.kvn = parseInt(String(kvn).trim(), 10); + const fmt = val('test-f-fmt'); + if (fmt && fmt !== 'compact') p.format = fmt; } else if (kind === 'status') { p.attempts = num('test-f-attempts', 1); p.interval_ms = num('test-f-interval', 200); } step.params = p; @@ -7630,7 +7677,19 @@ function testStepCollect() { const dataVal = hex('test-f-cdata'); if (dataVal) check.data = {mode: val('test-f-cdata-mode') || 'exact', value: dataVal}; const por = val('test-f-por'); - if (kind === 'scp80' && por && por !== 'any') check.por = por; + if (kind === 'scp80' && por === 'custom') { + // the decoded-PoR assertion: status / SW / data, at least one + const spec = {}; + const st = val('test-f-porstatus'); + if (st) spec.status = st; + const psw = hex('test-f-por-sw'); + if (psw) spec.sw = {mode: psw.includes('?') ? 'mask' : 'exact', value: psw}; + const pdata = hex('test-f-por-data'); + if (pdata) spec.data = {mode: pdata.includes('?') ? 'mask' : 'exact', value: pdata}; + check.por = spec; + } else if (kind === 'scp80' && por && por !== 'any') { + check.por = por; + } step.check = check; step.on_fail = val('test-f-fail') || 'error'; const label = val('test-f-label'); @@ -7645,6 +7704,7 @@ function testStepCollect() { if (c.kind === 'text') return !!(c.value || '').trim(); if (c.kind === 'item') return c.id || (c.text || '').trim(); if (c.kind === 'por') return !!(c.status || c.sw || c.data); + if (c.kind === 'sms') return !!(c.da || c.pid || c.dcs || c.udl !== undefined || c.ud); if (c.kind === 'files') return !!(c.files && c.files.length); return !!(c.value || '').trim(); }); @@ -7669,6 +7729,10 @@ function testStepFormError() { if (step.kind === 'scp80') { const src = p.source || (p.sp ? 'sp' : 'apdu'); if (src === 'sp' ? !p.sp : !p.apdu) return t('SCP80 needs an APDU or a secured packet'); + const pc = (step.check || {}).por; + if (pc && typeof pc === 'object' && !pc.status && !pc.sw && !pc.data) { + return t('PoR check: status, SW or data is required'); + } } if (step.kind === 'menu-select') { const problem = testMenuSelectProblem(p); @@ -21116,6 +21180,17 @@ const LANG_RU = { 'default: 9000 (91?? when polling)': 'по умолчанию: 9000 (91?? при опросе)', 'Expected data (hex, optional)': 'Ожидаемые данные (hex, опционально)', 'PoR check': 'Проверка PoR', + 'Custom (status / SW / data)': 'Своя проверка (status / SW / data)', + 'PoR status': 'Статус PoR', + 'PoR SW (hex)': 'SW PoR (hex)', + 'PoR data (hex)': 'Данные PoR (hex)', + 'PoR check: status, SW or data is required': 'Проверка PoR: нужен status, SW или data', + 'Command format': 'Формат команды', + 'Compact': 'Компактный', + 'Expanded (AA template)': 'Расширенный (шаблон AA)', + 'Expanded (AE 80 template)': 'Расширенный (шаблон AE 80)', + 'Alpha': 'Alpha', + 'SMS TPDU': 'TPDU SMS', 'PoR ok': 'PoR: успешно', 'no PoR': 'PoR: нет', 'PoR error': 'Ошибка PoR', diff --git a/frontend/sw.js b/frontend/sw.js index 4fe3d11..245459c 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v386'; +const CACHE = 'simple-v387'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/testscripts.test.js b/frontend/tests/testscripts.test.js index 2414a7f..21d8dab 100644 --- a/frontend/tests/testscripts.test.js +++ b/frontend/tests/testscripts.test.js @@ -34,6 +34,7 @@ eval(extractBlock('const TEST_ACTION_KINDS = [', 'let _testScripts').replace(/^c eval(extractFunc(html, 'testTemplate')); eval(extractFunc(html, 'testMenuSelectProblem')); eval(extractFunc(html, 'testScriptProblem')); +eval(extractFunc(html, 'testPorSpecSummary')); eval(extractFunc(html, 'testCheckSummary')); eval(extractFunc(html, 'testStepSummary')); eval(extractFunc(html, 'testCommandOptions')); @@ -194,6 +195,97 @@ test('the SCP80 source switch sticks and preserves the other value', () => { assert.strictEqual(_testEditStep.params.sp, 'AABBCC'); }); +test('the SCP80 step collects the format and the custom PoR object', () => { + _testEditStep = { type: 'action', kind: 'scp80', params: { apdu: '80E2900000' } }; + _testEditChecks = []; + _testEditStepIndex = 0; + fakeForm({ 'test-step-kind': 'scp80', 'test-f-src': 'apdu', 'test-f-apdu': '80E2900000', + 'test-f-fmt': 'expanded', 'test-f-kvn': '', 'test-f-tar': '', 'test-f-spi1': '', + 'test-f-spi2': '', 'test-f-sw': '', 'test-f-cdata': '', 'test-f-por': 'custom', + 'test-f-porstatus': 'por_ok', 'test-f-por-sw': '6a8?', 'test-f-por-data': 'aa??', + 'test-f-fail': 'error' }); + testStepCollect(); + assert.strictEqual(_testEditStep.params.format, 'expanded'); + assert.deepStrictEqual(_testEditStep.check.por, + { status: 'por_ok', sw: { mode: 'mask', value: '6A8?' }, + data: { mode: 'mask', value: 'AA??' } }); + // a custom PoR check with no fields refuses to save + fakeForm({ 'test-step-kind': 'scp80', 'test-f-src': 'apdu', 'test-f-apdu': '80E2900000', + 'test-f-fmt': 'compact', 'test-f-kvn': '', 'test-f-tar': '', 'test-f-spi1': '', + 'test-f-spi2': '', 'test-f-sw': '', 'test-f-cdata': '', 'test-f-por': 'custom', + 'test-f-fail': 'error' }); + assert.match(testStepFormError(), /PoR check/); + assert.ok(!('format' in _testEditStep.params)); // compact is the default + // the plain string forms stay and their object is dropped on switch + fakeForm({ 'test-step-kind': 'scp80', 'test-f-src': 'apdu', 'test-f-apdu': '80E2900000', + 'test-f-fmt': '', 'test-f-kvn': '', 'test-f-tar': '', 'test-f-spi1': '', + 'test-f-spi2': '', 'test-f-sw': '', 'test-f-cdata': '', 'test-f-por': 'none', + 'test-f-fail': 'error' }); + testStepCollect(); + assert.strictEqual(_testEditStep.check.por, 'none'); + // the summary renders the effective format and the object's fields + const s = testStepSummary({ type: 'action', kind: 'scp80', + params: { apdu: '80E2', format: 'expanded-ae' }, + check: { por: { status: 'por_ok', sw: { mode: 'mask', value: '6A8?' } } } }); + assert.ok(s.includes('fmt=expanded-ae'), s); + assert.ok(s.includes('PoR status por_ok SW ~6A8?'), s); +}); + +test('the SCP80 step form renders the format select and the custom PoR fields', () => { + const els = { + 'test-step-modal': { classList: { add: () => {}, remove: () => {} } }, + 'test-step-title': {}, 'test-step-body': {}, + 'test-step-error': { classList: { add: () => {}, remove: () => {} } }, + }; + globalThis.document = { getElementById: id => els[id] || null }; + _testEditStep = { type: 'action', kind: 'scp80', + params: { apdu: '80E2900000', format: 'expanded' }, + check: { por: { status: 'por_ok' } } }; + _testEditChecks = []; + _testEditStepIndex = 0; + testStepRender(); + const body = els['test-step-body'].innerHTML; + assert.match(body, /id="test-f-fmt"/); + assert.match(body, /