v3.11.0: Cards tab redesign (list + editor), preset PIN/PUK codes
The Cards tab now shows the preset list by default; "Add preset" / "Edit" open a separate editor view (Save stores and returns to the list, Cancel discards; ramGoToPreset opens it too). The editor keeps the name/ICCID header (with From card) and groups its fields into bordered blocks: - PIN/ADM: the new optional card codes PIN1/PUK1/PIN2/PUK2 (plain text, 4-8 decimal digits) plus ADM, moved out of the top row; nothing consumes the PIN/PUK codes yet. - TAR: one uniform table (TAR, MSL, description); the first three rows are the fixed role entries with the immutable English descriptions "ISD compact format" / "UICC shared FS RFM compact format" / "ADF FS RFM compact format"; further rows are deletable with an optional description. - SCP80 (keyset editor) and SCP81 (PSK pair) as before. Store: PRESET_FIELDS gains pin1/puk1/pin2/puk2 (optional; whitespace stripped; 4-8 decimal digits validated on save and import), SCHEMA_VERSION 4, docs/help/AGENTS updated. TAR rows use unified ids (cards-tr-N-*), the editor switches via cardsShowList/cardsShowEditor. Tests: 665 Python + 783 frontend green; the whole inline PWA script syntax-checked. sw cache simple-v361.
This commit is contained in:
+7
-1
@@ -204,6 +204,7 @@ the bytes, there is no separate field:
|
||||
|
||||
```json
|
||||
{"id": "…", "name": "Card A", "iccid": "…",
|
||||
"pin1": "", "puk1": "", "pin2": "", "puk2": "",
|
||||
"pskIdentity": "", "pskKey": "", "adm": "",
|
||||
"tars": [
|
||||
{"role": "isd", "tar": "000000", "msl": "16", "desc": ""},
|
||||
@@ -239,6 +240,11 @@ three role entries, the `spi1` as their MSL. Operations **derive a packet's
|
||||
SPI1 from the MSL of its TAR**; a request naming a TAR outside the preset must
|
||||
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).
|
||||
|
||||
**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",
|
||||
Annex A.1 — the spec's wording). The operations below accept a `preset_id` and **persist the
|
||||
@@ -250,7 +256,7 @@ is not defined in preset '…' - add it in the Cards tab"}`), and so is a
|
||||
KIc/KID number mismatch. The Cards tab's export/import uses this API; the
|
||||
import also accepts presets exported by the older localStorage-based builds.
|
||||
|
||||
- `GET /api/presets` — `{"path": "…", "count": 2, "version": 3, "presets": [{…}]}`
|
||||
- `GET /api/presets` — `{"path": "…", "count": 2, "version": 4, "presets": [{…}]}`
|
||||
(card-free: it answers while a long card operation runs).
|
||||
- `POST /api/presets` — body = the preset fields (`keysets` included); returns
|
||||
`{"ok": true, "preset": {…}}`. A duplicate ICCID (digits, spaced or raw EF
|
||||
|
||||
@@ -346,25 +346,25 @@
|
||||
<p class="mb-2">Каждый принятый картой пакет <strong>расходует счётчик</strong>, который сохраняется в keyset пресета — проба отклоняется, если в разрешённом SPI1 нет проверки счётчика или в пресете нет такого номера keyset. На живой карте: <code class="font-mono text-sm">000000</code>, <code class="font-mono text-sm">B00000</code> и <code class="font-mono text-sm">B00001</code> зарегистрированы (приложения компактного формата), расширенный формат, USAT, мультиплексирование и CASD отклонены с <code class="font-mono text-sm">6200</code>.</p>
|
||||
|
||||
<h2 id="cards" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">4. Вкладка «Карты»</h2>
|
||||
<p class="mb-2">Хранит пресеты карт на сервере (<code class="font-mono text-sm">~/.pysim-simple-server/card_presets.json</code>; переопределяется <code class="font-mono text-sm">--card-presets</code>), чтобы представление Secured Packet могло автоматически подставлять ключи и параметры, слушатель SCP81 HTTP OTA — находить PSK-ключи, а счётчики SCP80, израсходованные операцией, сохранялись самой операцией — закрытая вкладка или второе окно браузера больше не могут потерять инкремент. Пресет содержит <strong>несколько keysets</strong> — номер keyset задаётся нибблом <code class="font-mono text-sm">b8..b5</code> его KIc/KID (TS 102 225 §5.1.2/A.2): KIc и KID одного keyset должны нести один и тот же номер (01–0F; A.2 отвергает несовпадение), и у каждого keyset на карте свой счётчик (Annex A.1), поэтому счётчики ведутся по keysets. В форме keysets редактируются строками — по одной на номер, с кнопками <strong>Добавить keyset</strong> / <strong>Удалить keyset</strong>; номер в строке вычисляется из байта KIc/KID, а <code class="font-mono text-sm">00</code> («без защиты») не принимается как keyset. Список загружается при подключении PWA и экспортируется/импортируется как JSON (импорт принимает и пресеты старых сборок — плоские и с keysets). «Карты» — верхнеуровневая вкладка. При подключении карты читается её EF.ICCID, и пресет с тем же ICCID автоматически выбирается во всех трёх представлениях SCP80; если подключённая карта не совпадает <strong>ни с одним</strong> пресетом — или выбранный пресет принадлежит другой карте — все три представления SCP80 показывают жёлтое предупреждение над выбором пресета. В списке подсвечивается пресет подключённой карты. Поля формы сгруппированы в два блока с рамкой: <strong>SCP80 (GSM 03.48, ETSI TS 102 225)</strong> (keysets, таблица TAR — три обязательные роли ISD / UICC RFM / ADF RFM, у каждой свой MSL, плюс дополнительные TAR с необязательным описанием — и счётчики) и <strong>SCP81 (HTTP OTA)</strong> (пара PSK identity/ключ с пояснением о выборе ключа). Необязательное поле <strong>ADM</strong> находится в верхней строке и сохраняется для последующего использования файловым менеджером. В шапке отображаются серые маркеры <strong>SCP80</strong>/<strong>SCP81</strong> и значок ключа на бейдже ADM, когда в пресете с ICCID подключённой карты эти поля заполнены.</p>
|
||||
<p class="mb-2">Хранит пресеты карт на сервере (<code class="font-mono text-sm">~/.pysim-simple-server/card_presets.json</code>; переопределяется <code class="font-mono text-sm">--card-presets</code>), чтобы представление Secured Packet могло автоматически подставлять ключи и параметры, слушатель SCP81 HTTP OTA — находить PSK-ключи, а счётчики SCP80, израсходованные операцией, сохранялись самой операцией — закрытая вкладка или второе окно браузера больше не могут потерять инкремент. Пресет содержит <strong>несколько keysets</strong> — номер keyset задаётся нибблом <code class="font-mono text-sm">b8..b5</code> его KIc/KID (TS 102 225 §5.1.2/A.2): KIc и KID одного keyset должны нести один и тот же номер (01–0F; A.2 отвергает несовпадение), и у каждого keyset на карте свой счётчик (Annex A.1), поэтому счётчики ведутся по keysets. В форме keysets редактируются строками — по одной на номер, с кнопками <strong>Добавить keyset</strong> / <strong>Удалить keyset</strong>; номер в строке вычисляется из байта KIc/KID, а <code class="font-mono text-sm">00</code> («без защиты») не принимается как keyset. Список загружается при подключении PWA и экспортируется/импортируется как JSON (импорт принимает и пресеты старых сборок — плоские и с keysets). «Карты» — верхнеуровневая вкладка: по умолчанию показывается <strong>список пресетов</strong>, а <strong>Добавить пресет</strong> / <strong>Редактировать</strong> открывают <strong>редактор</strong> (отдельное представление) — <strong>Сохранить</strong> сохраняет пресет и возвращает к списку, <strong>Отмена</strong> отменяет правки. При подключении карты читается её EF.ICCID, и пресет с тем же ICCID автоматически выбирается во всех трёх представлениях SCP80; если подключённая карта не совпадает <strong>ни с одним</strong> пресетом — или выбранный пресет принадлежит другой карте — все три представления SCP80 показывают жёлтое предупреждение над выбором пресета. В списке подсвечивается пресет подключённой карты. Поля редактора сгруппированы в блоки с рамкой: <strong>PIN/ADM</strong> (необязательные коды карты — PIN1/PUK1/PIN2/PUK2 по 4–8 десятичных цифр и ключ ADM — сохраняются для будущего использования), <strong>TAR</strong> (таблица TAR), <strong>SCP80 (GSM 03.48, ETSI TS 102 225)</strong> (keysets и их счётчики) и <strong>SCP81 (HTTP OTA)</strong> (пара PSK identity/ключ с пояснением о выборе ключа). В шапке отображаются серые маркеры <strong>SCP80</strong>/<strong>SCP81</strong> и значок ключа на бейдже ADM, когда в пресете с ICCID подключённой карты эти поля заполнены.</p>
|
||||
<table class="w-full text-sm mb-3 border-collapse">
|
||||
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Поле</th><th class="text-left py-1 px-2">Описание</th></tr></thead>
|
||||
<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">ADM</td><td class="py-1 px-2">Необязательный администраторский PIN (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 — администраторский ключ (hex или до 8 ASCII-цифр), используется файловым менеджером. Представлениями 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">TARs / MSL</td><td class="py-1 px-2">Таблица TAR: три обязательные роли (ISD / UICC RFM / ADF RFM — предзаполнены) и дополнительные TAR, у каждого свой MSL (минимальный SPI1, TS 102 226 §8.2.1.3.2.4). SPI1 пакета берётся из MSL его TAR; у свободных записей есть необязательное описание</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">ISD TAR</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>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">UICC RFM TAR</td><td class="py-1 px-2">TAR UICC Shared File System RFM (TS 102 226 §7.2, TS 101 220 приложение D): MF выбирается неявно и доступны только файлы под MF (ADF недоступны); используется представлением SIM RFM; по умолчанию <code class="font-mono text-sm">B00000</code></td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">ADF RFM TAR</td><td class="py-1 px-2">TAR ADF RFM (TS 102 226 §7.3, TS 101 220 приложение D): ADF выбирается неявно, TAR привязан к AID приложения — ADF.USIM в представлении USIM RFM; по умолчанию <code class="font-mono text-sm">B00001</code></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>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">UICC shared FS RFM compact format</td><td class="py-1 px-2">TAR UICC Shared File System RFM (TS 102 226 §7.2, TS 101 220 приложение D): MF выбирается неявно и доступны только файлы под MF (ADF недоступны); используется представлением SIM RFM; по умолчанию <code class="font-mono text-sm">B00000</code></td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">ADF FS RFM compact format</td><td class="py-1 px-2">TAR ADF RFM (TS 102 226 §7.3, TS 101 220 приложение D): ADF выбирается неявно, TAR привязан к AID приложения — ADF.USIM в представлении USIM RFM; по умолчанию <code class="font-mono text-sm">B00001</code></td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Counter</td><td class="py-1 px-2">Счётчик повторов (5 байт); свой у каждого keyset, сервер увеличивает и сохраняет его после каждой операции, которая его расходует</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">KIc key / KID key</td><td class="py-1 px-2">16/24/32 hex-символа (ключи 8/16/24 байта 3DES) или 32/48/64 hex-символа (ключи 16/24/32 байта AES)</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">PSK identity</td><td class="py-1 px-2">SCP81 HTTP OTA: идентификатор, который карта присылает в TLS-рукопожатии (например, <code class="font-mono text-sm">89390100000129506903</code>)</td></tr>
|
||||
<tr><td class="py-1 px-2">PSK key</td><td class="py-1 px-2">SCP81 HTTP OTA: 32 hex-символа (16 байт) для PSK TLS-слушателя; ключ выбирается по идентификатору, который предъявляет карта</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-sm mb-3">Столбец <strong>SCP81</strong> показывает, задана ли в пресете рабочая пара PSK: <strong>✓</strong> (зелёный) — идентификатор и ключ заданы; <strong>⚠</strong> (жёлтый) — задано только одно из двух, и слушатель такой пресет игнорирует; <strong>—</strong> — PSK нет. Идентификатор и ключ PSK задаются вместе. Кнопка <strong>Редактировать</strong> загружает пресет в форму (кнопка Add становится <strong>Сохранить</strong>; <strong>Отмена</strong> очищает форму), поэтому поля можно менять без повторного ввода карты; <strong>Удалить</strong> удаляет строку. Обмен пресетами: <strong>Экспорт в файл</strong> для выгрузки и <strong>Импорт из файла</strong> для загрузки. Выбранный пресет автоматически заполняет форму Secured Packet и устанавливает TAR для операции (RAM/GP → ISD TAR, SIM RFM → UICC RFM TAR, USIM RFM → 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>SCP81</strong> показывает, задана ли в пресете рабочая пара PSK: <strong>✓</strong> (зелёный) — идентификатор и ключ заданы; <strong>⚠</strong> (жёлтый) — задано только одно из двух, и слушатель такой пресет игнорирует; <strong>—</strong> — PSK нет. Идентификатор и ключ PSK задаются вместе. Кнопка <strong>Добавить пресет</strong> открывает редактор с новым пресетом; <strong>Редактировать</strong> загружает пресет строки в редактор, поэтому поля можно менять без повторного ввода карты; <strong>Сохранить</strong> сохраняет и возвращает к списку, <strong>Отмена</strong> отменяет правки; <strong>Удалить</strong> удаляет строку (только в списке). Обмен пресетами: <strong>Экспорт в файл</strong> для выгрузки и <strong>Импорт из файла</strong> для загрузки. Выбранный пресет автоматически заполняет форму Secured Packet и устанавливает TAR для операции (RAM/GP → ISD TAR, SIM RFM → UICC RFM TAR, USIM RFM → ADF RFM TAR); TAR, введённый в форму Secured Packet вручную, сохраняется при отправке - счётчик, ключи и MSL этого TAR по-прежнему берутся из пресета; SPI1 берётся из MSL, а меньшее значение даёт предупреждение (сервер тоже возвращает его как <code class="font-mono text-sm">warning</code>); изменения сразу передаются работающему слушателю SCP81.</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-10">
|
||||
|
||||
+7
-7
@@ -345,25 +345,25 @@
|
||||
<p class="mb-2">Each packet the card accepts <strong>consumes a counter</strong>, which is saved into the preset's keyset — the probe is refused when the resolved SPI1 has no counter check or the named preset does not define the keyset number. Observed on a live card: <code class="font-mono text-sm">000000</code>, <code class="font-mono text-sm">B00000</code> and <code class="font-mono text-sm">B00001</code> registered (compact-format applications), the expanded-format TARs, USAT, multiplexing and CASD refused with <code class="font-mono text-sm">6200</code>.</p>
|
||||
|
||||
<h2 id="cards" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">4. Cards tab</h2>
|
||||
<p class="mb-2">Stores card presets on the server (<code class="font-mono text-sm">~/.pysim-simple-server/card_presets.json</code>; <code class="font-mono text-sm">--card-presets</code> overrides it) so the Secured Packet view can auto-fill keys and parameters, the SCP81 HTTP OTA listener can look up PSK keys, and the SCP80 counters an operation consumed are persisted with the operation itself — a closed tab or a second browser window can no longer lose an increment. A preset carries <strong>several keysets</strong> — a keyset's number is the <code class="font-mono text-sm">b8..b5</code> nibble of its KIc/KID (TS 102 225 §5.1.2/A.2): KIc and KID of a keyset must carry the same number (01–0F; A.2 rejects a mismatch), and each keyset has its own counter on the card (Annex A.1), so counters are tracked per keyset. In the form the keysets are edited as rows — one per number, with <strong>Add keyset</strong> / <strong>Remove keyset</strong>; each row shows the number derived from its KIc/KID byte, and <code class="font-mono text-sm">00</code> (no security) is not accepted as a keyset. The list is loaded when the PWA connects and can be exported/imported as JSON (the import also takes presets exported by older builds, flat or keyset-shaped). Cards is a top-level tab. When the card is equipped its EF.ICCID is read and the preset with the same ICCID is selected automatically in all three SCP80 views; when the equipped card matches <strong>no</strong> preset — or the selected preset belongs to another card — all three SCP80 views show an amber warning above the preset selector. The list highlights the equipped card's preset. The form groups the fields into two bordered blocks: <strong>SCP80 (GSM 03.48, ETSI TS 102 225)</strong> (the keysets, the TAR table — the three mandatory role entries ISD / UICC RFM / ADF RFM, each with its MSL, plus further TARs with an optional description — and the counters) and <strong>SCP81 (HTTP OTA)</strong> (the PSK identity/key pair with the key-selection note). The optional <strong>ADM</strong> field sits in the top row and is stored for later use by the file manager. The header shows gray <strong>SCP80</strong>/<strong>SCP81</strong> markers and a key glyph on the ADM badge when the preset matching the equipped card's ICCID has those settings filled in.</p>
|
||||
<p class="mb-2">Stores card presets on the server (<code class="font-mono text-sm">~/.pysim-simple-server/card_presets.json</code>; <code class="font-mono text-sm">--card-presets</code> overrides it) so the Secured Packet view can auto-fill keys and parameters, the SCP81 HTTP OTA listener can look up PSK keys, and the SCP80 counters an operation consumed are persisted with the operation itself — a closed tab or a second browser window can no longer lose an increment. A preset carries <strong>several keysets</strong> — a keyset's number is the <code class="font-mono text-sm">b8..b5</code> nibble of its KIc/KID (TS 102 225 §5.1.2/A.2): KIc and KID of a keyset must carry the same number (01–0F; A.2 rejects a mismatch), and each keyset has its own counter on the card (Annex A.1), so counters are tracked per keyset. In the form the keysets are edited as rows — one per number, with <strong>Add keyset</strong> / <strong>Remove keyset</strong>; each row shows the number derived from its KIc/KID byte, and <code class="font-mono text-sm">00</code> (no security) is not accepted as a keyset. The list is loaded when the PWA connects and can be exported/imported as JSON (the import also takes presets exported by older builds, flat or keyset-shaped). Cards is a top-level tab: it shows the <strong>preset list</strong> by default, and <strong>Add preset</strong> / <strong>Edit</strong> open the <strong>editor</strong> (a separate view) — <strong>Save</strong> stores the preset and returns to the list, <strong>Cancel</strong> discards the edits. When the card is equipped its EF.ICCID is read and the preset with the same ICCID is selected automatically in all three SCP80 views; when the equipped card matches <strong>no</strong> preset — or the selected preset belongs to another card — all three SCP80 views show an amber warning above the preset selector. The list highlights the equipped card's preset. The editor groups the fields into bordered blocks: <strong>PIN/ADM</strong> (the optional card codes — PIN1/PUK1/PIN2/PUK2 as 4–8 decimal digits and the ADM key — stored for later use), <strong>TAR</strong> (the TAR table), <strong>SCP80 (GSM 03.48, ETSI TS 102 225)</strong> (the keysets and their counters) and <strong>SCP81 (HTTP OTA)</strong> (the PSK identity/key pair with the key-selection note). The header shows gray <strong>SCP80</strong>/<strong>SCP81</strong> markers and a key glyph on the ADM badge when the preset matching the equipped card's ICCID has those settings filled in.</p>
|
||||
<table class="w-full text-sm mb-3 border-collapse">
|
||||
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Field</th><th class="text-left py-1 px-2">Description</th></tr></thead>
|
||||
<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">ADM</td><td class="py-1 px-2">Optional administrator PIN (hex, or up to 8 ASCII digits), stored in the preset for later use by the file manager; not 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 (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">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">TARs / MSL</td><td class="py-1 px-2">The TAR table: the three role entries (ISD / UICC RFM / ADF RFM — mandatory, pre-settled) plus further TARs, each with its MSL (Minimum SPI1, TS 102 226 §8.2.1.3.2.4). A packet's SPI1 is derived from its TAR's MSL; free entries carry an optional description</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">ISD TAR</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>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">UICC RFM TAR</td><td class="py-1 px-2">UICC Shared File System RFM TAR (TS 102 226 §7.2, TS 101 220 Annex D): the MF is implicitly selected and only files under the MF are accessible (no ADFs); used by the SIM RFM view; spec default <code class="font-mono text-sm">B00000</code></td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">ADF RFM TAR</td><td class="py-1 px-2">ADF RFM TAR (TS 102 226 §7.3, TS 101 220 Annex D): the ADF is implicitly selected and the TAR is linked to the application's AID — ADF.USIM in the USIM RFM view; spec default <code class="font-mono text-sm">B00001</code></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>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">UICC shared FS RFM compact format</td><td class="py-1 px-2">UICC Shared File System RFM TAR (TS 102 226 §7.2, TS 101 220 Annex D): the MF is implicitly selected and only files under the MF are accessible (no ADFs); used by the SIM RFM view; spec default <code class="font-mono text-sm">B00000</code></td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">ADF FS RFM compact format</td><td class="py-1 px-2">ADF RFM TAR (TS 102 226 §7.3, TS 101 220 Annex D): the ADF is implicitly selected and the TAR is linked to the application's AID — ADF.USIM in the USIM RFM view; spec default <code class="font-mono text-sm">B00001</code></td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Counter</td><td class="py-1 px-2">Replay counter (5 bytes); one per keyset, advanced and saved by the server after each operation that consumes it</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">KIc key / KID key</td><td class="py-1 px-2">16/24/32 hex chars (8/16/24-byte 3DES) or 32/48/64 hex chars (16/24/32-byte AES) keys</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">PSK identity</td><td class="py-1 px-2">SCP81 HTTP OTA: the identity the card sends in the TLS handshake (e.g. <code class="font-mono text-sm">89390100000129506903</code>)</td></tr>
|
||||
<tr><td class="py-1 px-2">PSK key</td><td class="py-1 px-2">SCP81 HTTP OTA: 32 hex chars (16 bytes) for the PSK TLS listener, picked by the identity the card presents</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-sm mb-3">The <strong>SCP81</strong> column shows whether the preset supplies a usable PSK pair: <strong>✓</strong> (green) — identity and key set; <strong>⚠</strong> (amber) — only one of the two, so the listener ignores the preset; <strong>—</strong> — no PSK. PSK identity and key must be set together. <strong>Edit</strong> loads a preset into the form (the Add button becomes <strong>Save</strong>; <strong>Cancel</strong> clears it) so fields can be changed without re-entering the card; <strong>Remove</strong> deletes the row. 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 → ISD TAR, SIM RFM → UICC RFM TAR, USIM RFM → 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">The <strong>SCP81</strong> column shows whether the preset supplies a usable PSK pair: <strong>✓</strong> (green) — identity and key set; <strong>⚠</strong> (amber) — only one of the two, so the listener ignores the preset; <strong>—</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 → ISD TAR, SIM RFM → UICC RFM TAR, USIM RFM → 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>
|
||||
</section>
|
||||
|
||||
<section class="mb-10">
|
||||
|
||||
+194
-92
@@ -1593,49 +1593,95 @@
|
||||
|
||||
|
||||
<div id="tab-cards" class="tab-content hidden">
|
||||
<p class="text-xs text-gray-500 dark:text-slate-400 mb-3" data-l10n="Card presets are stored on the server. JSON export/import buttons are below the card list; a preset carries several keysets, each with its own number and counter, and a TAR table — every TAR with its MSL (Minimum SPI1).">Card presets are stored on the server. JSON export/import buttons are below the card list; a preset carries several keysets, each with its own number and counter, and a TAR table — every TAR with its MSL (Minimum SPI1).</p>
|
||||
<!-- ===== preset list (default view) ===== -->
|
||||
<div id="cards-list-view">
|
||||
<p class="text-xs text-gray-500 dark:text-slate-400 mb-3" data-l10n="Card presets are stored on the server. A preset carries the card codes (PIN/PUK/ADM), a TAR table (every TAR with its MSL) and several keysets, each with its own number and counter.">Card presets are stored on the server. A preset carries the card codes (PIN/PUK/ADM), a TAR table (every TAR with its MSL) and several keysets, each with its own number and counter.</p>
|
||||
<div class="flex flex-wrap gap-2 mb-3">
|
||||
<button id="cards-new-btn" onclick="cardsNew()" class="px-4 py-2.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700 whitespace-nowrap" data-l10n="Add preset">Add preset</button>
|
||||
<button onclick="cardsExportFile()" class="px-3 py-2 text-sm rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300" data-l10n="Export to file">Export to file</button>
|
||||
<button onclick="document.getElementById('cards-file').click()" class="px-3 py-2 text-sm rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300" data-l10n="Import from file">Import from file</button>
|
||||
</div>
|
||||
<div id="cards-list" class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead><tr>
|
||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="Name">Name</th>
|
||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="ICCID">ICCID</th>
|
||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="Keysets (number: KIc/KID — counter)">Keysets (number: KIc/KID \u2014 counter)</th>
|
||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="TARs (MSL)">TARs (MSL)</th>
|
||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="SCP81">SCP81</th>
|
||||
<th class="border-b border-gray-200 dark:border-slate-700"></th>
|
||||
</tr></thead>
|
||||
<tbody id="cards-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<input type="file" id="cards-file" accept=".json,application/json" class="hidden" onchange="cardsImportFile(this)">
|
||||
<div id="cards-io" class="hidden mt-2 text-xs text-gray-500 dark:text-slate-400"></div>
|
||||
</div>
|
||||
|
||||
<!-- ===== preset editor ===== -->
|
||||
<div id="cards-editor-view" class="hidden">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 id="cards-editor-title" class="text-lg font-semibold text-gray-800 dark:text-slate-200"></h3>
|
||||
<button onclick="cardsCancelEdit()" class="px-3 py-1.5 text-sm rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300" data-l10n="Cancel">Cancel</button>
|
||||
</div>
|
||||
<div class="flex gap-2 mb-3">
|
||||
<input id="cards-name" class="flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="Name (e.g. Foobar SIM)">
|
||||
<input id="cards-iccid" class="flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 font-mono" placeholder="ICCID (optional)">
|
||||
<button id="cards-iccid-from-card" data-needs="card-iccid" onclick="cardsIccidFromCard()" class="px-3 py-1.5 text-sm rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300 whitespace-nowrap disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="From card">From card</button>
|
||||
<input id="cards-adm" class="w-40 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 font-mono" placeholder="ADM (optional)">
|
||||
<button id="cards-add-btn" onclick="cardsAdd()" class="px-4 py-2.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700 whitespace-nowrap" data-l10n="Add">Add</button>
|
||||
<button id="cards-cancel-btn" onclick="cardsCancelEdit()" class="hidden px-4 py-2.5 bg-gray-500 text-white text-sm font-medium rounded hover:bg-gray-600 whitespace-nowrap" data-l10n="Cancel">Cancel</button>
|
||||
<div class="flex-1">
|
||||
<label for="cards-name" class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Name">Name</label>
|
||||
<input id="cards-name" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="Name (e.g. Foobar SIM)">
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<label for="cards-iccid" class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="ICCID">ICCID</label>
|
||||
<div class="flex gap-1">
|
||||
<input id="cards-iccid" class="flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 font-mono" placeholder="ICCID (optional)">
|
||||
<button id="cards-iccid-from-card" data-needs="card-iccid" onclick="cardsIccidFromCard()" class="px-3 py-1.5 text-sm rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300 whitespace-nowrap disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="From card">From card</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<fieldset class="border border-gray-200 dark:border-slate-700 rounded p-3 mb-3">
|
||||
<legend class="px-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="SCP80 (GSM 03.48, ETSI TS 102 225)">SCP80 (GSM 03.48, ETSI TS 102 225)</legend>
|
||||
<div class="flex gap-3 mb-2 flex-wrap items-end">
|
||||
<legend class="px-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="PIN/ADM">PIN/ADM</legend>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-2 mb-2">
|
||||
<div>
|
||||
<label for="cards-tar" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400" data-l10n="ISD TAR">ISD TAR</label>
|
||||
<input id="cards-tar" class="w-20 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono" maxlength="6" value="000000">
|
||||
<label for="cards-pin1" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400">PIN1</label>
|
||||
<input id="cards-pin1" 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" inputmode="numeric" placeholder="4-8 digits">
|
||||
</div>
|
||||
<div>
|
||||
<label for="cards-tar-msl" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400" data-l10n="MSL">MSL</label>
|
||||
<input id="cards-tar-msl" class="w-12 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="2" value="16" data-l10n-title="MSL (Minimum SPI1, one hex byte)">
|
||||
<label for="cards-puk1" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400">PUK1</label>
|
||||
<input id="cards-puk1" 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" inputmode="numeric" placeholder="4-8 digits">
|
||||
</div>
|
||||
<div>
|
||||
<label for="cards-uicc-tar" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400" data-l10n="UICC RFM TAR">UICC RFM TAR</label>
|
||||
<input id="cards-uicc-tar" class="w-20 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono" maxlength="6" value="B00000">
|
||||
<label for="cards-pin2" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400">PIN2</label>
|
||||
<input id="cards-pin2" 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" inputmode="numeric" placeholder="4-8 digits">
|
||||
</div>
|
||||
<div>
|
||||
<label for="cards-uicc-tar-msl" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400" data-l10n="MSL">MSL</label>
|
||||
<input id="cards-uicc-tar-msl" class="w-12 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="2" value="16" data-l10n-title="MSL (Minimum SPI1, one hex byte)">
|
||||
<label for="cards-puk2" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400">PUK2</label>
|
||||
<input id="cards-puk2" 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" inputmode="numeric" placeholder="4-8 digits">
|
||||
</div>
|
||||
<div>
|
||||
<label for="cards-usim-tar" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400" data-l10n="ADF RFM TAR">ADF RFM TAR</label>
|
||||
<input id="cards-usim-tar" class="w-20 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono" maxlength="6" value="B00001">
|
||||
</div>
|
||||
<div>
|
||||
<label for="cards-usim-tar-msl" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400" data-l10n="MSL">MSL</label>
|
||||
<input id="cards-usim-tar-msl" class="w-12 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="2" value="16" data-l10n-title="MSL (Minimum SPI1, one hex byte)">
|
||||
<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)">
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-slate-400 mb-2" data-l10n="Each TAR carries its MSL (Minimum SPI1) — the card checks it before the security processing (TS 102 226 8.2.1.3.2.4); the packet SPI1 is derived from it.">Each TAR carries its MSL (Minimum SPI1) — the card checks it before the security processing (TS 102 226 8.2.1.3.2.4); the packet SPI1 is derived from it.</p>
|
||||
<div class="mb-1 flex items-center justify-between">
|
||||
<span class="text-xs text-gray-600 dark:text-slate-400" data-l10n="More TARs — each with its MSL and an optional description">More TARs — each with its MSL and an optional description</span>
|
||||
<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>
|
||||
</fieldset>
|
||||
<fieldset class="border border-gray-200 dark:border-slate-700 rounded p-3 mb-3">
|
||||
<legend class="px-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="TAR">TAR</legend>
|
||||
<table class="w-full text-sm mb-2">
|
||||
<thead><tr>
|
||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="TAR">TAR</th>
|
||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="MSL">MSL</th>
|
||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="Description">Description</th>
|
||||
<th class="w-8 border-b border-gray-200 dark:border-slate-700"></th>
|
||||
</tr></thead>
|
||||
<tbody id="cards-tars"></tbody>
|
||||
</table>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs text-gray-600 dark:text-slate-400" data-l10n="The first three rows are the fixed role TARs; add further TARs as needed.">The first three rows are the fixed role TARs; add further TARs as needed.</span>
|
||||
<button type="button" onclick="cardsTarAdd()" class="px-2 py-0.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700" data-l10n="Add TAR">Add TAR</button>
|
||||
</div>
|
||||
<div id="cards-tars"></div>
|
||||
<p class="text-xs text-gray-500 dark:text-slate-400" data-l10n="Each TAR carries its MSL (Minimum SPI1) — the card checks it before the security processing (TS 102 226 8.2.1.3.2.4); the packet SPI1 is derived from it.">Each TAR carries its MSL (Minimum SPI1) — the card checks it before the security processing (TS 102 226 8.2.1.3.2.4); the packet SPI1 is derived from it.</p>
|
||||
</fieldset>
|
||||
<fieldset class="border border-gray-200 dark:border-slate-700 rounded p-3 mb-3">
|
||||
<legend class="px-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="SCP80 (GSM 03.48, ETSI TS 102 225)">SCP80 (GSM 03.48, ETSI TS 102 225)</legend>
|
||||
<div class="mb-1 flex items-center justify-between">
|
||||
<span class="text-xs text-gray-600 dark:text-slate-400" data-l10n="OTA keysets — each with its own number and counter">OTA keysets — each with its own number and counter</span>
|
||||
<button type="button" onclick="cardsKeysetAdd()" class="px-2 py-0.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700" data-l10n="Add keyset">Add keyset</button>
|
||||
@@ -1651,25 +1697,11 @@
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-slate-400" data-l10n="SCP81 HTTP OTA: the listener picks the key by the PSK identity the card sends in the TLS handshake. Cards without a PSK are ignored by the listener.">SCP81 HTTP OTA: the listener picks the key by the PSK identity the card sends in the TLS handshake. Cards without a PSK are ignored by the listener.</p>
|
||||
</fieldset>
|
||||
<div id="cards-list" class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead><tr>
|
||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="Name">Name</th>
|
||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="ICCID">ICCID</th>
|
||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="Keysets (number: KIc/KID — counter)">Keysets (number: KIc/KID \u2014 counter)</th>
|
||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="TARs (MSL)">TARs (MSL)</th>
|
||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="SCP81">SCP81</th>
|
||||
<th class="border-b border-gray-200 dark:border-slate-700"></th>
|
||||
</tr></thead>
|
||||
<tbody id="cards-tbody"></tbody>
|
||||
</table>
|
||||
<div class="flex gap-2">
|
||||
<button id="cards-add-btn" onclick="cardsAdd()" class="px-4 py-2.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700" data-l10n="Save">Save</button>
|
||||
<button onclick="cardsCancelEdit()" class="px-4 py-2.5 bg-gray-500 text-white text-sm font-medium rounded hover:bg-gray-600" data-l10n="Cancel">Cancel</button>
|
||||
</div>
|
||||
<input type="file" id="cards-file" accept=".json,application/json" class="hidden" onchange="cardsImportFile(this)">
|
||||
<div class="flex flex-wrap gap-2 mt-2">
|
||||
<button onclick="cardsExportFile()" class="px-2 py-1 text-xs rounded bg-blue-600 text-white hover:bg-blue-700" data-l10n="Export to file">Export to file</button>
|
||||
<button onclick="document.getElementById('cards-file').click()" class="px-2 py-1 text-xs rounded bg-gray-600 text-white hover:bg-gray-700" data-l10n="Import from file">Import from file</button>
|
||||
</div>
|
||||
<div id="cards-io" class="hidden mt-2 text-xs text-gray-500 dark:text-slate-400"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1816,7 +1848,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.10.0';
|
||||
const SIMPLE_VERSION = '3.11.0';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== Tab switching =====
|
||||
@@ -11855,46 +11887,72 @@ function cardsKeysetsFromForm() {
|
||||
return out;
|
||||
}
|
||||
|
||||
// ===== TAR table editor (Cards form) =====
|
||||
// The three role entries have fixed inputs (their labels are the immutable
|
||||
// descriptions); further TARs are free rows with an optional description.
|
||||
// Every entry carries the TAR and its MSL (Minimum SPI1, one hex byte).
|
||||
// ===== TAR table editor (Cards editor) =====
|
||||
// One uniform table: the first three rows are the fixed role entries - their
|
||||
// descriptions are the immutable role labels (English fixed text) and they
|
||||
// carry no remove button; further rows are free entries with an optional
|
||||
// description. Every row carries the TAR and its MSL (Minimum SPI1, one hex
|
||||
// byte).
|
||||
let _cardsTarCount = 0;
|
||||
|
||||
// The fixed role descriptions (spec role names; not translated).
|
||||
const CARDS_TAR_ROLE_DESC = {
|
||||
isd: 'ISD compact format',
|
||||
uiccRfm: 'UICC shared FS RFM compact format',
|
||||
usimRfm: 'ADF FS RFM compact format',
|
||||
};
|
||||
|
||||
function cardsTarRowHtml(e, i) {
|
||||
const cls = 'border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono';
|
||||
return '<div class="flex gap-2 items-center mb-1">' +
|
||||
'<input id="cards-ft-' + i + '-tar" class="w-20 ' + cls + ' text-center" maxlength="6" placeholder="TAR" value="' + esc(e.tar || '') + '">' +
|
||||
'<input id="cards-ft-' + i + '-msl" class="w-12 ' + cls + ' text-center" maxlength="2" placeholder="MSL" value="' + esc(e.msl || '') + '" title="' + esc(t('MSL (Minimum SPI1, one hex byte)')) + '">' +
|
||||
'<input id="cards-ft-' + i + '-desc" class="flex-1 ' + cls + '" placeholder="' + esc(t('Description (optional)')) + '" value="' + esc(e.desc || '') + '">' +
|
||||
'<button type="button" onclick="cardsTarRemove(' + i + ')" class="px-2 py-1 text-xs rounded bg-red-600 text-white hover:bg-red-700" title="' + esc(t('Remove TAR')) + '">\u2715</button>' +
|
||||
'</div>';
|
||||
const role = CARDS_TAR_ROLES[i] || '';
|
||||
let cells = '<td class="py-1 px-1"><input id="cards-tr-' + i + '-tar" class="w-24 ' + cls + ' text-center" maxlength="6" placeholder="TAR" value="' + esc(e.tar || '') + '"></td>' +
|
||||
'<td class="py-1 px-1"><input id="cards-tr-' + i + '-msl" class="w-14 ' + cls + ' text-center" maxlength="2" placeholder="MSL" value="' + esc(e.msl || '') + '" title="' + esc(t('MSL (Minimum SPI1, one hex byte)')) + '"></td>';
|
||||
if (role) {
|
||||
cells += '<td class="py-1 px-2 text-xs text-gray-600 dark:text-slate-400">' + esc(CARDS_TAR_ROLE_DESC[role] || '') + '</td>' +
|
||||
'<td class="py-1 px-1"></td>';
|
||||
} else {
|
||||
cells += '<td class="py-1 px-1"><input id="cards-tr-' + i + '-desc" class="w-full ' + cls + '" placeholder="' + esc(t('Description (optional)')) + '" value="' + esc(e.desc || '') + '"></td>' +
|
||||
'<td class="py-1 px-1"><button type="button" onclick="cardsTarRemove(' + i + ')" class="px-2 py-1 text-xs rounded bg-red-600 text-white hover:bg-red-700" title="' + esc(t('Remove TAR')) + '">\u2715</button></td>';
|
||||
}
|
||||
return '<tr>' + cells + '</tr>';
|
||||
}
|
||||
|
||||
// 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.
|
||||
function cardsTarRowsRender(entries) {
|
||||
const box = document.getElementById('cards-tars');
|
||||
if (!box) return;
|
||||
const list = entries || [];
|
||||
_cardsTarCount = list.length;
|
||||
box.innerHTML = list.map((e, i) => cardsTarRowHtml(e, i)).join('');
|
||||
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 };
|
||||
});
|
||||
const free = list.filter(e => !e.role);
|
||||
_cardsTarCount = free.length;
|
||||
box.innerHTML = roles.map((e, i) => cardsTarRowHtml(e, i)).join('') +
|
||||
free.map((e, i) => cardsTarRowHtml(e, CARDS_TAR_ROLES.length + i)).join('');
|
||||
}
|
||||
|
||||
function cardsTarAdd() {
|
||||
cardsTarRowsRender(cardsFreeTarsFromForm().concat([{ tar: '', msl: '', desc: '' }]));
|
||||
cardsTarRowsRender(cardsTarsFromForm().concat([{ tar: '', msl: '', desc: '' }]));
|
||||
}
|
||||
|
||||
// `i` is the table row index; the role rows are fixed and never removed.
|
||||
function cardsTarRemove(i) {
|
||||
const rows = cardsFreeTarsFromForm();
|
||||
if (i < CARDS_TAR_ROLES.length) return;
|
||||
const rows = cardsTarsFromForm();
|
||||
rows.splice(i, 1);
|
||||
cardsTarRowsRender(rows);
|
||||
}
|
||||
|
||||
// The free editor rows (fully empty rows are dropped).
|
||||
// The free table rows (fully empty rows are dropped).
|
||||
function cardsFreeTarsFromForm() {
|
||||
const out = [];
|
||||
for (let i = 0; i < _cardsTarCount; i++) {
|
||||
const idx = CARDS_TAR_ROLES.length + i;
|
||||
const get = id => {
|
||||
const el = document.getElementById('cards-ft-' + i + '-' + id);
|
||||
const el = document.getElementById('cards-tr-' + idx + '-' + id);
|
||||
return el ? String(el.value).trim() : '';
|
||||
};
|
||||
const e = { tar: get('tar').replace(/\s/g, '').toUpperCase(),
|
||||
@@ -11905,32 +11963,25 @@ function cardsFreeTarsFromForm() {
|
||||
return out;
|
||||
}
|
||||
|
||||
// The TAR table as preset entries: the three role entries from their fixed
|
||||
// inputs (the labels are the role descriptions) plus the free rows.
|
||||
// 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() {
|
||||
const roleInputs = { isd: 'cards-tar', uiccRfm: 'cards-uicc-tar', usimRfm: 'cards-usim-tar' };
|
||||
const out = CARDS_TAR_ROLES.map(role => {
|
||||
const tarEl = document.getElementById(roleInputs[role]);
|
||||
const mslEl = document.getElementById(roleInputs[role] + '-msl');
|
||||
const tar = (tarEl ? String(tarEl.value).trim() : '').replace(/\s/g, '').toUpperCase();
|
||||
const msl = (mslEl ? String(mslEl.value).trim() : '').replace(/\s/g, '').toUpperCase();
|
||||
// An emptied role field keeps the pre-settled default: the three role
|
||||
// entries are mandatory with a non-empty TAR and MSL.
|
||||
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 out.concat(cardsFreeTarsFromForm());
|
||||
}
|
||||
|
||||
// The role inputs back to their pre-settled defaults (TAR + MSL 16).
|
||||
// The role rows back to their pre-settled defaults (TAR + MSL 16) and no free
|
||||
// rows.
|
||||
function cardsTarsReset() {
|
||||
const roleInputs = { isd: 'cards-tar', uiccRfm: 'cards-uicc-tar', usimRfm: 'cards-usim-tar' };
|
||||
CARDS_TAR_ROLES.forEach(role => {
|
||||
const tarEl = document.getElementById(roleInputs[role]);
|
||||
if (tarEl) tarEl.value = cardsRoleDefault(role);
|
||||
const mslEl = document.getElementById(roleInputs[role] + '-msl');
|
||||
if (mslEl) mslEl.value = CARDS_MSL_DEFAULT;
|
||||
});
|
||||
cardsTarRowsRender([]);
|
||||
}
|
||||
|
||||
@@ -12091,6 +12142,10 @@ function cardsFormValues() {
|
||||
return {
|
||||
name: val('name'),
|
||||
iccid: val('iccid'),
|
||||
pin1: val('pin1').replace(/\s/g, ''),
|
||||
puk1: val('puk1').replace(/\s/g, ''),
|
||||
pin2: val('pin2').replace(/\s/g, ''),
|
||||
puk2: val('puk2').replace(/\s/g, ''),
|
||||
adm: val('adm').replace(/\s/g, '').toUpperCase(),
|
||||
tars: cardsTarsFromForm(),
|
||||
pskIdentity: val('psk-id'),
|
||||
@@ -12099,19 +12154,49 @@ function cardsFormValues() {
|
||||
};
|
||||
}
|
||||
|
||||
// Show the preset list (the Cards tab's default view).
|
||||
function cardsShowList() {
|
||||
const list = document.getElementById('cards-list-view');
|
||||
const editor = document.getElementById('cards-editor-view');
|
||||
if (list) list.classList.remove('hidden');
|
||||
if (editor) editor.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Show the preset editor.
|
||||
function cardsShowEditor() {
|
||||
const list = document.getElementById('cards-list-view');
|
||||
const editor = document.getElementById('cards-editor-view');
|
||||
if (list) list.classList.add('hidden');
|
||||
if (editor) editor.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function cardsSetFormMode() {
|
||||
const title = document.getElementById('cards-editor-title');
|
||||
if (title) {
|
||||
title.textContent = _cardsEditIdx === null
|
||||
? t('Add preset')
|
||||
: t('Edit preset') + ': ' + ((cards[_cardsEditIdx] || {}).name || '');
|
||||
}
|
||||
const btn = document.getElementById('cards-add-btn');
|
||||
if (btn) btn.textContent = _cardsEditIdx === null ? t('Add') : t('Save');
|
||||
const cancel = document.getElementById('cards-cancel-btn');
|
||||
if (cancel) cancel.classList.toggle('hidden', _cardsEditIdx === null);
|
||||
}
|
||||
|
||||
// "Add preset" in the list view: reset the form and open the editor.
|
||||
function cardsNew() {
|
||||
_cardsEditIdx = null;
|
||||
cardsClearForm();
|
||||
cardsSetFormMode();
|
||||
cardsShowEditor();
|
||||
const el = document.getElementById('cards-name');
|
||||
if (el) el.focus();
|
||||
}
|
||||
|
||||
function cardsClearForm() {
|
||||
['name','iccid','adm','psk-id','psk-key'].forEach(id => {
|
||||
['name','iccid','pin1','puk1','pin2','puk2','adm','psk-id','psk-key'].forEach(id => {
|
||||
const el = document.getElementById('cards-' + id);
|
||||
if (el) el.value = '';
|
||||
});
|
||||
// The three role entries keep their pre-settled defaults (TAR + MSL 16)
|
||||
// The three role rows keep their pre-settled defaults (TAR + MSL 16)
|
||||
cardsTarsReset();
|
||||
cardsKeysetRowsRender([]);
|
||||
}
|
||||
@@ -12119,6 +12204,14 @@ function cardsClearForm() {
|
||||
async function cardsAdd() {
|
||||
const v = cardsFormValues();
|
||||
if (!v.name) { alert(t('Name is required')); return; }
|
||||
// The optional card codes: PIN/PUK are 4-8 decimal digits; ADM stays
|
||||
// free-form (hex or ASCII digits)
|
||||
for (const k of ['pin1', 'puk1', 'pin2', 'puk2']) {
|
||||
if (v[k] && !/^[0-9]{4,8}$/.test(v[k])) {
|
||||
alert(t('PIN/PUK codes must be 4-8 decimal digits') + ': ' + k.toUpperCase());
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!v.keysets.length) {
|
||||
alert(t('Define at least one keyset (KIc/KID and their keys)'));
|
||||
return;
|
||||
@@ -12191,24 +12284,24 @@ function cardsEdit(i) {
|
||||
const el = document.getElementById('cards-' + id);
|
||||
if (el) el.value = value == null ? '' : value;
|
||||
};
|
||||
set('name', c.name); set('iccid', c.iccid); set('adm', c.adm);
|
||||
set('tar', cardsRoleTar(c, 'isd'));
|
||||
set('tar-msl', cardsRoleMsl(c, 'isd'));
|
||||
set('uicc-tar', cardsRoleTar(c, 'uiccRfm'));
|
||||
set('uicc-tar-msl', cardsRoleMsl(c, 'uiccRfm'));
|
||||
set('usim-tar', cardsRoleTar(c, 'usimRfm'));
|
||||
set('usim-tar-msl', cardsRoleMsl(c, 'usimRfm'));
|
||||
set('name', c.name); set('iccid', c.iccid);
|
||||
set('pin1', c.pin1); set('puk1', c.puk1);
|
||||
set('pin2', c.pin2); set('puk2', c.puk2);
|
||||
set('adm', c.adm);
|
||||
set('psk-id', c.pskIdentity); set('psk-key', c.pskKey);
|
||||
cardsKeysetRowsRender(spKeysetList(c));
|
||||
cardsTarRowsRender(cardsTarList(c).filter(e => !e.role));
|
||||
cardsTarRowsRender(cardsTarList(c));
|
||||
cardsSetFormMode();
|
||||
document.getElementById('cards-name').focus();
|
||||
cardsShowEditor();
|
||||
const el = document.getElementById('cards-name');
|
||||
if (el) el.focus();
|
||||
}
|
||||
|
||||
function cardsCancelEdit() {
|
||||
_cardsEditIdx = null;
|
||||
cardsSetFormMode();
|
||||
cardsClearForm();
|
||||
cardsShowList();
|
||||
}
|
||||
|
||||
function ramCardIdxAfterRemove(idx, removedIdx) {
|
||||
@@ -20810,6 +20903,15 @@ const LANG_RU = {
|
||||
'Duplicate TAR in the preset': 'Повторяющийся TAR в пресете',
|
||||
'SPI1 is below the MSL of this TAR': 'SPI1 ниже MSL этого TAR',
|
||||
'(empty)': '(пусто)',
|
||||
'Add preset': 'Добавить пресет',
|
||||
'Edit preset': 'Редактирование пресета',
|
||||
'PIN/ADM': 'PIN/ADM',
|
||||
'TAR': 'TAR',
|
||||
'Description': 'Описание',
|
||||
'Card presets are stored on the server. A preset carries the card codes (PIN/PUK/ADM), a TAR table (every TAR with its MSL) and several keysets, each with its own number and counter.': 'Пресеты карт хранятся на сервере. Пресет содержит коды карты (PIN/PUK/ADM), таблицу TAR (у каждого TAR свой MSL) и несколько keysets — у каждого свой номер и счётчик.',
|
||||
'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.': 'Все коды необязательны: PIN/PUK — 4–8 десятичных цифр (сохраняются для будущего использования операциями карты — пока не используются); ADM — администраторский ключ.',
|
||||
'The first three rows are the fixed role TARs; add further TARs as needed.': 'Первые три строки — фиксированные роли TAR; дополнительные TAR добавляются по необходимости.',
|
||||
'PIN/PUK codes must be 4-8 decimal digits': 'Коды PIN/PUK должны состоять из 4–8 десятичных цифр',
|
||||
'Keysets (number: KIc/KID — counter)': 'Keysets (номер: KIc/KID — счётчик)',
|
||||
'must be one hex byte': 'должен быть одним hex-байтом',
|
||||
'KIc and KID must carry the same keyset number (TS 102 225 A.2)': 'KIc и KID должны содержать один и тот же номер keyset (TS 102 225 A.2)',
|
||||
|
||||
@@ -2221,6 +2221,10 @@ video {
|
||||
.sm\:grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.sm\:grid-cols-3 {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
@@ -2239,6 +2243,10 @@ video {
|
||||
.lg\:grid-cols-3 {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.lg\:grid-cols-5 {
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
}
|
||||
}/* ===== Theme contrast adjustments ===== */
|
||||
|
||||
/* Light theme: darken softer (muted) text one step for higher contrast */
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v360';
|
||||
const CACHE = 'simple-v361';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -44,11 +44,13 @@ for (const fn of ['cardsFormValues', 'cardsClearForm', 'cardsEdit',
|
||||
'cardsTarList', 'cardsRoleDefault', 'cardsRoleTar', 'cardsRoleMsl',
|
||||
'cardsTarEntry', 'cardsTarMsl', 'cardsTarText', 'spMslWarningText',
|
||||
'cardsTarRowHtml', 'cardsTarRowsRender', 'cardsTarAdd', 'cardsTarRemove',
|
||||
'cardsFreeTarsFromForm', 'cardsTarsFromForm', 'cardsTarsReset']) {
|
||||
'cardsFreeTarsFromForm', 'cardsTarsFromForm', 'cardsTarsReset',
|
||||
'cardsShowList', 'cardsShowEditor', 'cardsSetFormMode', 'cardsNew',
|
||||
'cardsCancelEdit']) {
|
||||
code += extractFunc(html, fn, ASYNC_FNS.indexOf(fn) >= 0) + '\n';
|
||||
}
|
||||
code = extractConst(html, 'CARDS_TAR_DEFAULTS') + extractConst(html, 'CARDS_TAR_ROLES')
|
||||
+ extractConst(html, 'CARDS_MSL_DEFAULT')
|
||||
+ extractConst(html, 'CARDS_MSL_DEFAULT') + extractConst(html, 'CARDS_TAR_ROLE_DESC')
|
||||
+ 'var _cardsKeysetCount = 0;\nvar _cardsTarCount = 0;\n'
|
||||
+ 'globalThis.spPresetWarningRender = function() {};\n'
|
||||
+ 'globalThis.spMslWarningRender = function() {};\n'
|
||||
@@ -56,9 +58,8 @@ code = extractConst(html, 'CARDS_TAR_DEFAULTS') + extractConst(html, 'CARDS_TAR_
|
||||
eval(code);
|
||||
globalThis.t = s => s;
|
||||
|
||||
const CARD_IDS = ['cards-name','cards-iccid','cards-adm',
|
||||
'cards-tar','cards-tar-msl','cards-uicc-tar','cards-uicc-tar-msl',
|
||||
'cards-usim-tar','cards-usim-tar-msl','cards-tars',
|
||||
const CARD_IDS = ['cards-name','cards-iccid','cards-pin1','cards-puk1','cards-pin2','cards-puk2','cards-adm',
|
||||
'cards-tars','cards-list-view','cards-editor-view','cards-editor-title','cards-new-btn',
|
||||
'cards-psk-id','cards-psk-key','cards-keysets',
|
||||
'cards-add-btn','cards-cancel-btn',
|
||||
// the keyset editor rows (two rows is enough for the tests)
|
||||
@@ -66,9 +67,11 @@ const CARD_IDS = ['cards-name','cards-iccid','cards-adm',
|
||||
'cards-ks-0-kid-key','cards-ks-0-cntr',
|
||||
'cards-ks-1-kvn','cards-ks-1-kic','cards-ks-1-kid','cards-ks-1-kic-key',
|
||||
'cards-ks-1-kid-key','cards-ks-1-cntr',
|
||||
// the free TAR editor rows
|
||||
'cards-ft-0-tar','cards-ft-0-msl','cards-ft-0-desc',
|
||||
'cards-ft-1-tar','cards-ft-1-msl','cards-ft-1-desc'];
|
||||
// the TAR table rows: 0-2 are the fixed role rows, 3+ the free rows
|
||||
'cards-tr-0-tar','cards-tr-0-msl','cards-tr-1-tar','cards-tr-1-msl',
|
||||
'cards-tr-2-tar','cards-tr-2-msl',
|
||||
'cards-tr-3-tar','cards-tr-3-msl','cards-tr-3-desc',
|
||||
'cards-tr-4-tar','cards-tr-4-msl','cards-tr-4-desc'];
|
||||
const SP_IDS = ['sp-card-sel','sp-tar','sp-spi1','sp-spi2','sp-spi2-sm','sp-spi2-hex',
|
||||
'sp-kic-idx','sp-kic-alg','sp-kic-hex','sp-kid-idx','sp-kid-alg','sp-kid-hex',
|
||||
'sp-cntr','sp-kic-key','sp-kid-key','sp-apdu','sp-result','sp-spi1-hex','sp-tar-list',
|
||||
@@ -86,7 +89,7 @@ function ksRow(els, i, ks) {
|
||||
|
||||
function tarRow(els, i, e) {
|
||||
for (const field of ['tar', 'msl', 'desc']) {
|
||||
const id = 'cards-ft-' + i + '-' + field;
|
||||
const id = 'cards-tr-' + i + '-' + field;
|
||||
if (!els[id]) els[id] = fakeEl();
|
||||
els[id].value = e[field] || '';
|
||||
}
|
||||
@@ -103,12 +106,22 @@ function presetFixture(extra) {
|
||||
}
|
||||
|
||||
function fakeEl() {
|
||||
const classes = new Set();
|
||||
return {
|
||||
value: '',
|
||||
textContent: '',
|
||||
innerHTML: '',
|
||||
focus() {},
|
||||
classList: { add() {}, remove() {}, toggle() {}, contains: () => false },
|
||||
classList: {
|
||||
add: (...cs) => cs.forEach(c => classes.add(c)),
|
||||
remove: (...cs) => cs.forEach(c => classes.delete(c)),
|
||||
toggle: (c, on) => {
|
||||
const want = on === undefined ? !classes.has(c) : !!on;
|
||||
if (want) classes.add(c); else classes.delete(c);
|
||||
return want;
|
||||
},
|
||||
contains: c => classes.has(c),
|
||||
},
|
||||
setAttribute() {}, removeAttribute() {},
|
||||
};
|
||||
}
|
||||
@@ -133,8 +146,6 @@ function setup() {
|
||||
globalThis.cardsRender = () => {};
|
||||
globalThis.cardsRebuildSelect = () => {};
|
||||
globalThis.scp81PushPskMap = () => {};
|
||||
globalThis.cardsSetFormMode = () => {};
|
||||
globalThis.cardsCancelEdit = () => {};
|
||||
globalThis.cardsFindDuplicateIccid = () => -1;
|
||||
globalThis.ioStatus = () => {};
|
||||
globalThis.switchTab = () => {};
|
||||
@@ -226,62 +237,76 @@ test('preset completeness predicates used by the header markers', () => {
|
||||
|
||||
test('clearing the card form keeps the role TAR/MSL defaults', () => {
|
||||
const els = setup();
|
||||
els['cards-tar'].value = 'AABBCC';
|
||||
els['cards-tar-msl'].value = 'FF';
|
||||
els['cards-uicc-tar'].value = 'AABBCC';
|
||||
els['cards-usim-tar'].value = 'AABBCC';
|
||||
els['cards-pin1'].value = '1234';
|
||||
els['cards-adm'].value = 'DEAD';
|
||||
els['cards-name'].value = 'X';
|
||||
tarRow(els, 0, { tar: 'AF4D01', msl: '0A', desc: 'applet' });
|
||||
// a free row from a previous edit
|
||||
tarRow(els, 3, { tar: 'AF4D01', msl: '0A', desc: 'applet' });
|
||||
_cardsTarCount = 1;
|
||||
cardsClearForm();
|
||||
assert.strictEqual(els['cards-tar'].value, '000000');
|
||||
assert.strictEqual(els['cards-tar-msl'].value, '16');
|
||||
assert.strictEqual(els['cards-uicc-tar'].value, 'B00000');
|
||||
assert.strictEqual(els['cards-uicc-tar-msl'].value, '16');
|
||||
assert.strictEqual(els['cards-usim-tar'].value, 'B00001');
|
||||
assert.strictEqual(els['cards-pin1'].value, '');
|
||||
assert.strictEqual(els['cards-adm'].value, '');
|
||||
assert.strictEqual(els['cards-name'].value, '');
|
||||
assert.strictEqual(_cardsTarCount, 0, 'the free TAR rows are cleared');
|
||||
// the role rows render the pre-settled defaults
|
||||
assert.match(els['cards-tars'].innerHTML, /cards-tr-0-tar"[^>]*value="000000"/);
|
||||
assert.match(els['cards-tars'].innerHTML, /cards-tr-0-msl"[^>]*value="16"/);
|
||||
assert.match(els['cards-tars'].innerHTML, /cards-tr-1-tar"[^>]*value="B00000"/);
|
||||
assert.match(els['cards-tars'].innerHTML, /cards-tr-2-tar"[^>]*value="B00001"/);
|
||||
// ... and the table reads back as the three role entries
|
||||
assert.deepStrictEqual(cardsTarsFromForm(), [
|
||||
{ role: 'isd', tar: '000000', msl: '16', desc: '' },
|
||||
{ role: 'uiccRfm', tar: 'B00000', msl: '16', desc: '' },
|
||||
{ role: 'usimRfm', tar: 'B00001', msl: '16', desc: '' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('editing prefills the role TARs/MSLs and renders the free TAR rows', () => {
|
||||
test('editing prefills the code fields and renders the TAR table', () => {
|
||||
const els = setup();
|
||||
globalThis.cards = [
|
||||
{ name: 'Legacy', keysets: [] },
|
||||
presetFixture({ name: 'Full', adm: '0011', tars: [
|
||||
presetFixture({ name: 'Full', adm: '0011', pin1: '1234', puk1: '12345678',
|
||||
pin2: '0000', puk2: '87654321', tars: [
|
||||
{ role: 'isd', tar: '000001', msl: '0A' },
|
||||
{ role: 'uiccRfm', tar: 'B0000C', msl: '16' },
|
||||
{ role: 'usimRfm', tar: 'B0000D', msl: '16' },
|
||||
{ tar: 'AF4D02', msl: '1A', desc: 'applet' }] }),
|
||||
];
|
||||
cardsEdit(0);
|
||||
assert.strictEqual(els['cards-tar'].value, '000000');
|
||||
assert.strictEqual(els['cards-tar-msl'].value, '');
|
||||
assert.strictEqual(els['cards-uicc-tar'].value, 'B00000');
|
||||
// the editor opens; missing codes read as empty
|
||||
assert.ok(!els['cards-editor-view'].classList.contains('hidden'));
|
||||
assert.ok(els['cards-list-view'].classList.contains('hidden'));
|
||||
assert.strictEqual(els['cards-pin1'].value, '');
|
||||
assert.strictEqual(els['cards-adm'].value, '');
|
||||
cardsEdit(1);
|
||||
assert.strictEqual(els['cards-tar'].value, '000001');
|
||||
assert.strictEqual(els['cards-tar-msl'].value, '0A');
|
||||
assert.strictEqual(els['cards-uicc-tar'].value, 'B0000C');
|
||||
assert.strictEqual(els['cards-usim-tar'].value, 'B0000D');
|
||||
assert.strictEqual(els['cards-usim-tar-msl'].value, '16');
|
||||
assert.strictEqual(els['cards-pin1'].value, '1234');
|
||||
assert.strictEqual(els['cards-puk1'].value, '12345678');
|
||||
assert.strictEqual(els['cards-pin2'].value, '0000');
|
||||
assert.strictEqual(els['cards-puk2'].value, '87654321');
|
||||
assert.strictEqual(els['cards-adm'].value, '0011');
|
||||
assert.strictEqual(_cardsTarCount, 1);
|
||||
assert.match(els['cards-tars'].innerHTML, /cards-ft-0-tar"[^>]*value="AF4D02"/);
|
||||
assert.match(els['cards-tars'].innerHTML, /cards-ft-0-desc"[^>]*value="applet"/);
|
||||
assert.match(els['cards-tars'].innerHTML, /cards-tr-0-tar"[^>]*value="000001"/);
|
||||
assert.match(els['cards-tars'].innerHTML, /cards-tr-1-tar"[^>]*value="B0000C"/);
|
||||
assert.match(els['cards-tars'].innerHTML, /cards-tr-2-tar"[^>]*value="B0000D"/);
|
||||
assert.match(els['cards-tars'].innerHTML, /cards-tr-3-tar"[^>]*value="AF4D02"/);
|
||||
assert.match(els['cards-tars'].innerHTML, /cards-tr-3-desc"[^>]*value="applet"/);
|
||||
// the role rows carry the fixed descriptions and no remove button
|
||||
assert.match(els['cards-tars'].innerHTML, /ISD compact format/);
|
||||
assert.match(els['cards-tars'].innerHTML, /UICC shared FS RFM compact format/);
|
||||
assert.match(els['cards-tars'].innerHTML, /ADF FS RFM compact format/);
|
||||
assert.doesNotMatch(els['cards-tars'].innerHTML, /cardsTarRemove\(0\)/);
|
||||
assert.match(els['cards-tars'].innerHTML, /cardsTarRemove\(3\)/);
|
||||
});
|
||||
|
||||
test('the form values carry the ADM and the TAR table', () => {
|
||||
test('the form values carry the codes and the TAR table', () => {
|
||||
const els = setup();
|
||||
els['cards-tar'].value = ' 000001 ';
|
||||
els['cards-tar-msl'].value = '0a';
|
||||
els['cards-uicc-tar'].value = '';
|
||||
els['cards-uicc-tar-msl'].value = '';
|
||||
els['cards-usim-tar'].value = 'B00003';
|
||||
els['cards-usim-tar-msl'].value = '1A';
|
||||
els['cards-pin1'].value = ' 12 34 ';
|
||||
els['cards-puk1'].value = '12345678';
|
||||
els['cards-adm'].value = ' 00 11 22 33 ';
|
||||
tarRow(els, 0, { tar: 'af4d02', msl: '0A', desc: 'My applet' });
|
||||
tarRow(els, 0, { tar: ' 000001 ', msl: '0a' });
|
||||
tarRow(els, 1, { tar: '', msl: '' }); // uiccRfm -> spec default
|
||||
tarRow(els, 2, { tar: 'B00003', msl: '1A' });
|
||||
tarRow(els, 3, { tar: 'af4d02', msl: '0A', desc: 'My applet' });
|
||||
_cardsTarCount = 1;
|
||||
const v = cardsFormValues();
|
||||
assert.deepStrictEqual(v.tars, [
|
||||
@@ -290,6 +315,8 @@ test('the form values carry the ADM and the TAR table', () => {
|
||||
{ role: 'usimRfm', tar: 'B00003', msl: '1A', desc: '' },
|
||||
{ tar: 'AF4D02', msl: '0A', desc: 'My applet' },
|
||||
]);
|
||||
assert.strictEqual(v.pin1, '1234');
|
||||
assert.strictEqual(v.puk1, '12345678');
|
||||
assert.strictEqual(v.adm, '00112233');
|
||||
});
|
||||
|
||||
@@ -322,14 +349,14 @@ test('saving refuses a free TAR row without its MSL or with a duplicate TAR', as
|
||||
globalThis._cardsKeysetCount = 1;
|
||||
const seen = [];
|
||||
globalThis.alert = msg => seen.push(msg);
|
||||
tarRow(els, 0, { tar: 'AF4D02', msl: '', desc: '' });
|
||||
tarRow(els, 3, { tar: 'AF4D02', msl: '', desc: '' });
|
||||
_cardsTarCount = 1;
|
||||
await cardsAdd();
|
||||
assert.strictEqual(apiCalls.length, 0);
|
||||
assert.match(seen[0], /MSL/);
|
||||
// a TAR may appear once
|
||||
seen.length = 0;
|
||||
tarRow(els, 0, { tar: '000000', msl: '16', desc: '' });
|
||||
tarRow(els, 3, { tar: '000000', msl: '16', desc: '' });
|
||||
await cardsAdd();
|
||||
assert.strictEqual(apiCalls.length, 0);
|
||||
assert.match(seen[0], /Duplicate TAR/);
|
||||
@@ -506,23 +533,75 @@ test('packing without a selected preset uses the spec default TAR', () => {
|
||||
assert.strictEqual(els['sp-tar'].value, '000000');
|
||||
});
|
||||
|
||||
test('the TAR editor renders, adds and removes free rows', () => {
|
||||
test('the TAR table renders the role rows and adds/removes free rows', () => {
|
||||
const els = setup();
|
||||
cardsTarRowsRender([{ tar: 'AF4D01', msl: '0A', desc: 'one' },
|
||||
{ tar: 'AF4D02', msl: '0A', desc: 'two' }]);
|
||||
assert.strictEqual(_cardsTarCount, 2);
|
||||
assert.match(els['cards-tars'].innerHTML, /cards-ft-1-tar"[^>]*value="AF4D02"/);
|
||||
assert.match(els['cards-tars'].innerHTML, /cards-tr-0-tar"[^>]*value="000000"/);
|
||||
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"/);
|
||||
assert.match(els['cards-tars'].innerHTML, /cards-tr-4-desc"[^>]*value="two"/);
|
||||
assert.match(els['cards-tars'].innerHTML, /cardsTarRemove\(4\)/);
|
||||
// simulate the DOM the innerHTML produced
|
||||
tarRow(els, 0, { tar: 'AF4D01', msl: '0A', desc: 'one' });
|
||||
tarRow(els, 1, { tar: 'AF4D02', msl: '0A', desc: 'two' });
|
||||
cardsTarRemove(0);
|
||||
tarRow(els, 3, { tar: 'AF4D01', msl: '0A', desc: 'one' });
|
||||
tarRow(els, 4, { tar: 'AF4D02', msl: '0A', desc: 'two' });
|
||||
cardsTarRemove(3);
|
||||
assert.strictEqual(_cardsTarCount, 1);
|
||||
assert.match(els['cards-tars'].innerHTML, /value="AF4D02"/);
|
||||
assert.doesNotMatch(els['cards-tars'].innerHTML, /value="AF4D01"/);
|
||||
tarRow(els, 0, { tar: 'AF4D02', msl: '0A', desc: 'two' });
|
||||
tarRow(els, 3, { tar: 'AF4D02', msl: '0A', desc: 'two' });
|
||||
cardsTarAdd();
|
||||
assert.strictEqual(_cardsTarCount, 2);
|
||||
// the new row renders empty
|
||||
tarRow(els, 1, { tar: '', msl: '', desc: '' });
|
||||
tarRow(els, 4, { tar: '', msl: '', desc: '' });
|
||||
assert.deepStrictEqual(cardsFreeTarsFromForm(), [{ tar: 'AF4D02', msl: '0A', desc: 'two' }]);
|
||||
// the role rows are never removable
|
||||
cardsTarRemove(0);
|
||||
assert.strictEqual(_cardsTarCount, 2);
|
||||
});
|
||||
|
||||
test('the list view opens the editor for a new preset and Cancel returns', () => {
|
||||
const els = setup();
|
||||
globalThis.cards = [presetFixture()];
|
||||
cardsNew();
|
||||
assert.ok(!els['cards-editor-view'].classList.contains('hidden'), 'the editor is shown');
|
||||
assert.ok(els['cards-list-view'].classList.contains('hidden'), 'the list is hidden');
|
||||
assert.strictEqual(globalThis._cardsEditIdx, null);
|
||||
assert.strictEqual(els['cards-editor-title'].textContent, 'Add preset');
|
||||
assert.strictEqual(els['cards-add-btn'].textContent, 'Add');
|
||||
assert.strictEqual(_cardsTarCount, 0);
|
||||
cardsCancelEdit();
|
||||
assert.ok(!els['cards-list-view'].classList.contains('hidden'), 'the list is back');
|
||||
assert.ok(els['cards-editor-view'].classList.contains('hidden'), 'the editor is hidden');
|
||||
});
|
||||
|
||||
test('editing opens the editor with the preset title', () => {
|
||||
const els = setup();
|
||||
globalThis.cards = [presetFixture({ name: 'Foo' })];
|
||||
cardsEdit(0);
|
||||
assert.strictEqual(globalThis._cardsEditIdx, 0);
|
||||
assert.ok(!els['cards-editor-view'].classList.contains('hidden'));
|
||||
assert.strictEqual(els['cards-editor-title'].textContent, 'Edit preset: Foo');
|
||||
assert.strictEqual(els['cards-add-btn'].textContent, 'Save');
|
||||
});
|
||||
|
||||
test('the editor refuses PIN/PUK codes that are not 4-8 digits', async () => {
|
||||
const els = setup();
|
||||
els['cards-name'].value = 'X';
|
||||
ksRow(els, 0, { kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB' });
|
||||
globalThis._cardsKeysetCount = 1;
|
||||
const seen = [];
|
||||
globalThis.alert = msg => seen.push(msg);
|
||||
els['cards-pin1'].value = '123';
|
||||
await cardsAdd();
|
||||
assert.strictEqual(apiCalls.length, 0);
|
||||
assert.match(seen[0], /PIN\/PUK/);
|
||||
seen.length = 0;
|
||||
els['cards-pin1'].value = '1234';
|
||||
els['cards-puk2'].value = '12A4';
|
||||
await cardsAdd();
|
||||
assert.strictEqual(apiCalls.length, 0);
|
||||
assert.match(seen[0], /PUK2/);
|
||||
});
|
||||
|
||||
+52
-24
@@ -129,45 +129,73 @@ test('cards list shows the SCP81 PSK column with blue/red row buttons', () => {
|
||||
assert.match(fn[0], /cardsRemove\(' \+ i \+ '\)" class="[^"]*bg-red-600 text-white/);
|
||||
});
|
||||
|
||||
test('the card form groups the SCP80 and SCP81 fields into labelled fieldsets', () => {
|
||||
test('the card editor groups its fields into labelled fieldsets', () => {
|
||||
const fieldsets = [...html.matchAll(/<fieldset[\s\S]*?<\/fieldset>/g)].map(m => m[0]);
|
||||
const pinAdm = fieldsets.find(f => /data-l10n="PIN\/ADM"/.test(f));
|
||||
const tar = fieldsets.find(f => /data-l10n="TAR"/.test(f));
|
||||
const scp80 = fieldsets.find(f => /data-l10n="SCP80 \(GSM 03\.48, ETSI TS 102 225\)"/.test(f));
|
||||
const scp81 = fieldsets.find(f => /data-l10n="SCP81 \(HTTP OTA\)"/.test(f));
|
||||
assert.ok(pinAdm, 'PIN/ADM fieldset not found');
|
||||
assert.ok(tar, 'TAR fieldset not found');
|
||||
assert.ok(scp80, 'SCP80 card fieldset not found');
|
||||
assert.ok(scp81, 'SCP81 card fieldset not found');
|
||||
// the optional card codes (plain text; PIN/PUK are 4-8 decimal digits)
|
||||
for (const id of ['cards-pin1', 'cards-puk1', 'cards-pin2', 'cards-puk2', 'cards-adm']) {
|
||||
assert.ok(pinAdm.includes('id="' + id + '"'), id + ' not in the PIN/ADM fieldset');
|
||||
}
|
||||
// the TAR table: uniform rows rendered into the body, Add TAR below
|
||||
assert.match(tar, /<tbody id="cards-tars">/);
|
||||
assert.match(tar, /onclick="cardsTarAdd\(\)"/);
|
||||
assert.match(tar, />TAR<\/th>/);
|
||||
assert.match(tar, />MSL<\/th>/);
|
||||
assert.match(tar, />Description<\/th>/);
|
||||
// the keyset editor and the PSK pair stay as implemented
|
||||
assert.match(scp80, /id="cards-keysets"/);
|
||||
assert.match(scp80, /onclick="cardsKeysetAdd\(\)"/);
|
||||
assert.match(scp80, /id="cards-tar"[^>]*value="000000"/);
|
||||
assert.match(scp80, /id="cards-uicc-tar"[^>]*value="B00000"/);
|
||||
assert.match(scp80, /id="cards-usim-tar"[^>]*value="B00001"/);
|
||||
// the TAR fields are labelled and sit to the right of Counter
|
||||
assert.match(scp80, />ISD TAR<\/label>/);
|
||||
assert.match(scp80, />UICC RFM TAR<\/label>/);
|
||||
assert.match(scp80, />ADF RFM TAR<\/label>/);
|
||||
// the preset-wide SPI1/SPI2 inputs are gone: each TAR carries its MSL
|
||||
assert.ok(!scp80.includes('id="cards-spi1"'), 'the preset-wide SPI1 input is gone');
|
||||
assert.ok(!scp80.includes('id="cards-spi2"'), 'the preset-wide SPI2 input is gone');
|
||||
assert.ok(scp80.indexOf('id="cards-tar"') < scp80.indexOf('id="cards-tar-msl"'),
|
||||
'the ISD MSL input follows its TAR input');
|
||||
assert.match(scp80, /id="cards-tars"/);
|
||||
assert.match(scp80, /onclick="cardsTarAdd\(\)"/);
|
||||
assert.match(scp81, /id="cards-psk-id"/);
|
||||
assert.match(scp81, /id="cards-psk-key"/);
|
||||
// the PSK explanation lives inside the SCP81 group, not outside it
|
||||
assert.match(scp81, /data-l10n="SCP81 HTTP OTA: the listener picks the key/);
|
||||
// the ICCID field is not in either group
|
||||
// the preset-wide SPI1/SPI2 inputs are gone: each TAR row carries its MSL
|
||||
assert.ok(!html.includes('id="cards-spi1"'), 'the preset-wide SPI1 input is gone');
|
||||
assert.ok(!html.includes('id="cards-spi2"'), 'the preset-wide SPI2 input is gone');
|
||||
// the name/ICCID fields are in the editor but not inside the fieldsets
|
||||
assert.ok(!fieldsets.some(f => f.includes('id="cards-iccid"')));
|
||||
assert.ok(!fieldsets.some(f => f.includes('id="cards-name"')));
|
||||
});
|
||||
|
||||
test('the ADM field sits between From card and Add, outside the card fieldsets', () => {
|
||||
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"/);
|
||||
assert.match(html, /id="cards-new-btn"[^>]*onclick="cardsNew\(\)"/);
|
||||
assert.match(html, /id="cards-editor-title"/);
|
||||
const listView = html.slice(html.indexOf('id="cards-list-view"'),
|
||||
html.indexOf('id="cards-editor-view"'));
|
||||
const editorView = html.slice(html.indexOf('id="cards-editor-view"'),
|
||||
html.indexOf('id="event-send-modal"'));
|
||||
// the list view holds the table, the Add preset button and export/import
|
||||
assert.match(listView, /id="cards-tbody"/);
|
||||
assert.match(listView, /id="cards-file"/);
|
||||
assert.match(listView, /id="cards-io"/);
|
||||
assert.match(listView, /cardsExportFile\(\)/);
|
||||
assert.ok(!listView.includes('id="cards-keysets"'), 'the editor must not be in the list view');
|
||||
// the editor view holds the fieldsets and the save/cancel buttons
|
||||
assert.match(editorView, /id="cards-keysets"/);
|
||||
assert.match(editorView, /id="cards-pin1"/);
|
||||
assert.match(editorView, /id="cards-tars"/);
|
||||
assert.match(editorView, /id="cards-add-btn"[^>]*onclick="cardsAdd\(\)"/);
|
||||
assert.match(editorView, /onclick="cardsCancelEdit\(\)"/);
|
||||
assert.ok(!editorView.includes('id="cards-tbody"'), 'the list table must not be in the editor');
|
||||
});
|
||||
|
||||
test('the ADM field sits in the PIN/ADM fieldset of the editor', () => {
|
||||
const fieldsets = [...html.matchAll(/<fieldset[\s\S]*?<\/fieldset>/g)].map(m => m[0]);
|
||||
assert.ok(!fieldsets.some(f => f.includes('id="cards-adm"')));
|
||||
assert.match(html, /id="cards-adm"[^>]*placeholder="ADM \(optional\)"/);
|
||||
const fromIdx = html.indexOf('id="cards-iccid-from-card"');
|
||||
const admIdx = html.indexOf('id="cards-adm"');
|
||||
const addIdx = html.indexOf('id="cards-add-btn"');
|
||||
assert.ok(fromIdx >= 0 && fromIdx < admIdx && admIdx < addIdx,
|
||||
'ADM input must sit between the From card and Add buttons');
|
||||
const pinAdm = fieldsets.find(f => /data-l10n="PIN\/ADM"/.test(f));
|
||||
assert.ok(pinAdm, 'PIN/ADM fieldset not found');
|
||||
assert.match(pinAdm, /id="cards-adm"[^>]*placeholder="ADM \(optional\)"/);
|
||||
// the From card button sits in the editor header, above the code fields
|
||||
assert.ok(html.indexOf('id="cards-iccid-from-card"') < html.indexOf('id="cards-pin1"'),
|
||||
'the From card button must precede the code fields');
|
||||
});
|
||||
|
||||
test('card-dependent profiler buttons show the ICCID and need a readable one', () => {
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-simple-server"
|
||||
version = "3.10.0"
|
||||
version = "3.11.0"
|
||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||
requires-python = ">=3.8"
|
||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||
|
||||
@@ -20,6 +20,10 @@ for a TAR derives its SPI1 from that TAR's MSL; the preset no longer carries a
|
||||
card-wide SPI1/SPI2 (SPI2 is an operation property: whether and how a Proof of
|
||||
Receipt is requested).
|
||||
|
||||
The optional **card codes** (`pin1`/`puk1`/`pin2`/`puk2`/`adm`) are stored for
|
||||
later use by the card operations (nothing consumes them yet); the PIN/PUK codes
|
||||
are 4-8 decimal digits, the ADM key is hex or ASCII digits.
|
||||
|
||||
A preset holds **several keysets**; the b8..b5 nibble of KIc/KID numbers them
|
||||
(TS 102 225 5.1.2/A.2). Each keyset has its own KIc/KID keys and its own
|
||||
counter - "a dedicated counter shall be associated to each key version"
|
||||
@@ -59,18 +63,22 @@ from pathlib import Path
|
||||
|
||||
DEFAULT_DIR = '.pysim-simple-server'
|
||||
DEFAULT_FILENAME = 'card_presets.json'
|
||||
SCHEMA_VERSION = 3
|
||||
SCHEMA_VERSION = 4
|
||||
|
||||
# The fields a preset carries: the localStorage shape plus the id. The OTA key
|
||||
# material lives in `keysets` - the b8..b5 nibble of KIc/KID numbers them,
|
||||
# each with its own counter (TS 102 225 Annex A.1: "a dedicated counter shall
|
||||
# be associated to each key version"); the TAR table lives in `tars` (see
|
||||
# TAR_ROLES).
|
||||
PRESET_FIELDS = ('name', 'iccid', 'adm', 'pskIdentity', 'pskKey')
|
||||
# TAR_ROLES). The optional card codes (PIN/PUK/ADM) are stored for later use.
|
||||
PRESET_FIELDS = ('name', 'iccid', 'pin1', 'puk1', 'pin2', 'puk2', 'adm',
|
||||
'pskIdentity', 'pskKey')
|
||||
# The v3.8.0 flat key fields: converted into a single keyset on load/import.
|
||||
LEGACY_KEY_FIELDS = ('kic', 'kid', 'kicKey', 'kidKey', 'cntr')
|
||||
KEYSET_FIELDS = ('kic', 'kid', 'kicKey', 'kidKey', 'cntr')
|
||||
HEX_FIELDS = ('adm', 'pskKey')
|
||||
# The card codes: PIN/PUK are 4-8 decimal digits (validated in _validate),
|
||||
# ADM is hex or ASCII digits (HEX_FIELDS).
|
||||
DIGIT_FIELDS = ('pin1', 'puk1', 'pin2', 'puk2')
|
||||
KEYSET_HEX_FIELDS = ('kic', 'kid', 'kicKey', 'kidKey', 'cntr')
|
||||
# The TAR table: the three mandatory role entries (their values are pre-settled
|
||||
# in the Cards form) and free entries with an optional description. Every
|
||||
@@ -279,6 +287,8 @@ class PresetStore:
|
||||
v = '' if v is None else str(v).strip()
|
||||
if k in HEX_FIELDS:
|
||||
v = re.sub(r'\s', '', v).upper()
|
||||
elif k in DIGIT_FIELDS:
|
||||
v = re.sub(r'\s', '', v)
|
||||
out[k] = v
|
||||
out['tars'] = self._normalise_tars(fields)
|
||||
# Keysets: a v3.8.0 preset (flat kic/kid/kicKey/kidKey/cntr) converts
|
||||
@@ -361,6 +371,10 @@ class PresetStore:
|
||||
raise PresetError('PSK identity and PSK key must be set together')
|
||||
if p['pskKey'] and not re.fullmatch(r'[0-9A-F]{32}', p['pskKey']):
|
||||
raise PresetError('PSK key must be 32 hex characters')
|
||||
for k in DIGIT_FIELDS:
|
||||
if p[k] and not re.fullmatch(r'[0-9]{4,8}', p[k]):
|
||||
raise PresetError('%s must be 4-8 decimal digits (or empty)'
|
||||
% k.upper())
|
||||
if not p['keysets']:
|
||||
raise PresetError('at least one keyset is required')
|
||||
seen = set()
|
||||
|
||||
@@ -32,7 +32,7 @@ from osmocom.tlv import BER_TLV_IE
|
||||
from osmocom.utils import rpad
|
||||
|
||||
|
||||
VERSION = '3.10.0'
|
||||
VERSION = '3.11.0'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
|
||||
@@ -211,6 +211,29 @@ class StoreCrudTests(unittest.TestCase):
|
||||
with self.assertRaises(presets.PresetError):
|
||||
self.store.add(preset(name=''))
|
||||
|
||||
def test_pin_and_puk_codes_are_optional_and_normalised(self):
|
||||
p = self.store.add(preset())
|
||||
# absent/empty: all four default to ''
|
||||
self.assertEqual([p[k] for k in ('pin1', 'puk1', 'pin2', 'puk2')],
|
||||
['', '', '', ''])
|
||||
p = self.store.add(preset(name='Codes', pin1=' 12 34 ', puk1='12345678',
|
||||
pin2='0000', puk2=' 8765 4321 '))
|
||||
self.assertEqual(p['pin1'], '1234')
|
||||
self.assertEqual(p['puk1'], '12345678')
|
||||
self.assertEqual(p['pin2'], '0000')
|
||||
self.assertEqual(p['puk2'], '87654321')
|
||||
|
||||
def test_pin_and_puk_codes_must_be_4_to_8_digits(self):
|
||||
for field in ('pin1', 'puk1', 'pin2', 'puk2'):
|
||||
for bad in ('123', '123456789', '12A4', 'abcd'):
|
||||
with self.assertRaises(presets.PresetError) as ctx:
|
||||
self.store.add(preset(name='X', **{field: bad}))
|
||||
self.assertIn(field.upper(), str(ctx.exception))
|
||||
# 4 and 8 digits are the accepted bounds; empty stays optional
|
||||
p = self.store.add(preset(name='Bounds', pin1='1234', puk1='12345678'))
|
||||
self.assertEqual(p['pin1'], '1234')
|
||||
self.assertEqual(p['puk1'], '12345678')
|
||||
|
||||
def test_duplicate_iccid_is_refused_across_stored_forms(self):
|
||||
self.store.add(preset(iccid=DIGITS))
|
||||
with self.assertRaises(presets.PresetError) as ctx:
|
||||
@@ -362,11 +385,14 @@ class StorePersistenceTests(unittest.TestCase):
|
||||
def test_the_file_round_trips(self):
|
||||
store = make_store(self.tmp.name)
|
||||
p = store.add(preset(iccid=DIGITS, pskIdentity='id', pskKey='00' * 16,
|
||||
pin1='1234', puk1='12345678',
|
||||
keysets=[keyset(kic='15', kid='15'),
|
||||
keyset(kic='29', kid='29', cntr='0000000004')]))
|
||||
raw = json.loads(pathlib.Path(store.path).read_text(encoding='utf-8'))
|
||||
self.assertEqual(raw['version'], presets.SCHEMA_VERSION)
|
||||
self.assertEqual(raw['presets'][0]['keysets'][1]['cntr'], '0000000004')
|
||||
self.assertEqual(raw['presets'][0]['pin1'], '1234')
|
||||
self.assertEqual(raw['presets'][0]['puk1'], '12345678')
|
||||
again = make_store(self.tmp.name)
|
||||
self.assertEqual([x['id'] for x in again.list()], [p['id']])
|
||||
self.assertEqual(again.find_keyset(p['id'], 2)['cntr'], '0000000004')
|
||||
|
||||
Reference in New Issue
Block a user