diff --git a/README.md b/README.md index 2d6ce28..360068a 100644 --- a/README.md +++ b/README.md @@ -404,7 +404,7 @@ Decodes a raw command response: pick the command that was sent, enter the SW (e. ## SCP80 tab -The **SCP80** top-level tab groups SCP80-related views, switched by two pills: **Secured Packet** and **RAM**. Assembles secured packets per ETSI TS 102 225. When a card is equipped the server reads its EF.ICCID (2FE2); if a preset carries the same number (digits, or the raw EF hex), it is selected automatically in both views. +The **SCP80** top-level tab groups SCP80-related views, switched by three pills: **Secured Packet**, **RAM** and **TAR probe**. Assembles secured packets per ETSI TS 102 225. When a card is equipped the server reads its EF.ICCID (2FE2); if a preset carries the same number (digits, or the raw EF hex), it is selected automatically in all three SCP80 views. ### Secured Packet @@ -515,9 +515,23 @@ Delete confirms via a browser prompt before sending the GP `DELETE` command via --- +### TAR probe + +The third SCP80 pill probes **which OTA applications the card has registered**: it sends one secured packet per +selected TAR — the standard allocations of TS 101 220 V18.3.0 Annex D, Table D.1 (pre-filled and all checked) plus +custom TARs added with **Add TAR** and kept in `localStorage` — each carrying a harmless C-APDU (`SELECT MF` by +default), with the view's preset and keyset and SPI `16/01` (counter check + PoR). Every answer is classified: +**registered** (ENVELOPE 9000 + a PoR carrying the application's own status word and data), **accepted, no answer** +(a PoR without an R-APDU), **accepted, no PoR** (the packet was silently dropped) and **refused** (the ENVELOPE +itself was rejected). Each packet the card accepts consumes a counter, which is saved into the preset's keyset; the +probe is refused when SPI1 has no counter check or the named preset does not define the keyset number. +`POST /api/tar-probe` (see `docs/api.md`). + +--- + ## Cards -Stores saved card configurations (presets) in `localStorage`. A preset holds the cryptographic keys, SPI settings, TARs and replay counter for SCP80 operations, the optional **ADM** key, plus the **PSK identity / PSK key** pair used by the SCP81 HTTP OTA listener. Cards is a **top-level tab**. The form groups the fields into two bordered blocks — **SCP80 (GSM 03.48, ETSI TS 102 225)** and **SCP81 (HTTP OTA)** — with the optional **ADM** field in the top row. When the card is equipped its EF.ICCID is read and the preset with the same ICCID is selected automatically in both SCP80 views. The header shows gray **SCP80** / **SCP81** markers and a key glyph on the **ADM** badge when the preset matching the equipped card's ICCID has those settings filled in. +Stores saved card configurations (presets) in a **server-side store** — `~/.pysim-simple-server/card_presets.json` (`--card-presets` overrides it). A preset holds the cryptographic keys, SPI settings, TARs and the replay counters for SCP80 operations, the optional **ADM** key, plus the **PSK identity / PSK key** pair used by the SCP81 HTTP OTA listener. A preset carries **several keysets** — a keyset's number is the `b8..b5` 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), `00` (no security) is not a preset keyset, and each keyset has its own counter on the card (Annex A.1) — the server owns those counters, persists every consumed increment and appends it to an audit trail (`card_presets.json.audit.jsonl`). The list is loaded when the PWA connects; JSON export/import (which also accepts the old flat presets) is the migration path from the browser-local store. Cards is a **top-level tab**. The form groups the fields into two bordered blocks — **SCP80 (GSM 03.48, ETSI TS 102 225)** and **SCP81 (HTTP OTA)** — with the optional **ADM** field in the top row; the keysets are edited as rows (one per number, **Add keyset** / **Remove keyset**). 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 **no** preset — or the selected preset belongs to another card — all three SCP80 views show an amber warning above the preset selector. The header shows gray **SCP80** / **SCP81** markers and a key glyph on the **ADM** badge when the preset matching the equipped card's ICCID has those settings filled in. | Field | Description | |---|---| @@ -525,20 +539,20 @@ Stores saved card configurations (presets) in `localStorage`. A preset holds the | ICCID | Optional card identifier | | ADM | Optional administrator PIN (hex, or up to 8 ASCII digits), stored for the file manager; not used by the SCP80/SCP81 views | | SPI1 / SPI2 | Security level and PoR settings | -| KIc / KID index | Key version number (required together with the keys) | +| KIc / KID index | Keyset number (`01`–`0F`, from the KIc/KID byte; required together with the keys) | | KIc / KID key | Encryption and MAC key hex | | ISD TAR | Issuer Security Domain TAR (TS 101 220 Annex D), used for RAM/GP operations; spec default `000000` | | UICC RFM TAR | UICC Shared File System RFM TAR (TS 102 226 §7.2), used by the SIM RFM view; spec default `B00000` | | ADF RFM TAR | ADF RFM TAR (TS 102 226 §7.3), linked to the ADF AID (ADF.USIM in the USIM RFM view); spec default `B00001` | -| Counter (CNTR) | 10-digit hex replay counter, auto-incremented after each successful SCP80 send | +| Counter (CNTR) | 10-digit hex replay counter, one per keyset; the server advances and saves it after each operation that consumes it (nothing is tracked when SPI1 b5 b4 = `00`, TS 102 225 §5.1.1) | | PSK identity | SCP81 HTTP OTA: the identity the card sends in the TLS handshake | | PSK key | SCP81 HTTP OTA: 32 hex chars (16 bytes); the listener picks it by the identity the card presents | The **SCP81** column shows whether the preset supplies a usable PSK pair: **✓** (identity and key), **⚠** (only one of the two — the listener ignores such a preset), **—** (no PSK). Identity and key must be set together. -**Add a card:** fill in the name, ICCID (optional — **From card** fills it from the equipped card's EF.ICCID), the optional **ADM**, SPI1/SPI2, KIc/KID keys and indices, the three TARs, the SCP81 PSK pair (optional) and click **Add**. A duplicate ICCID (compared ignoring spaces and the raw-hex form) is refused, naming the conflicting preset. The card appears in the list and becomes available in the RAM tab's **Card preset** dropdown. +**Add a card:** fill in the name, ICCID (optional — **From card** fills it from the equipped card's EF.ICCID), the optional **ADM**, SPI1/SPI2, one or more keysets (KIc/KID + keys per number, **Add keyset**), the three TARs, the SCP81 PSK pair (optional) and click **Add**. A duplicate ICCID (compared ignoring spaces and the raw-hex form) is refused, naming the conflicting preset. The card appears in the list and becomes available in the RAM tab's **Card preset** dropdown. -**Edit / remove:** **Edit** loads a preset into the form (the Add button becomes **Save**; **Cancel** clears the form); **Remove** deletes the row from `localStorage`. A successful SCP80 send advances and stores the replay counter, and edits are pushed into a running SCP81 listener automatically. +**Edit / remove:** **Edit** loads a preset into the form (the Add button becomes **Save**; **Cancel** clears the form); **Remove** deletes the preset from the server store. A successful SCP80 send advances and stores the counter of the keyset it used, and edits are pushed into a running SCP81 listener automatically. --- diff --git a/README_RUS.md b/README_RUS.md index 8833c85..31c9c34 100644 --- a/README_RUS.md +++ b/README_RUS.md @@ -379,7 +379,7 @@ CLA = `80` (GlobalPlatform v2.3.1). Удалённое управление со ## Вкладка SCP80 -Вкладка **SCP80** группирует SCP80-виды, переключаемые двумя пиллами: **Secured Packet** и **RAM**. Сборка защищённых пакетов по ETSI TS 102 225. При подключении карты сервер читает её EF.ICCID (2FE2); если в пресете указан тот же номер (цифрами или сырым hex из EF), он автоматически выбирается в обоих видах. +Вкладка **SCP80** группирует SCP80-виды, переключаемые тремя пиллами: **Secured Packet**, **RAM** и **TAR-сканер**. Сборка защищённых пакетов по ETSI TS 102 225. При подключении карты сервер читает её EF.ICCID (2FE2); если в пресете указан тот же номер (цифрами или сырым hex из EF), он автоматически выбирается во всех трёх видах SCP80. ### Secured Packet @@ -490,9 +490,23 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на --- +### TAR-сканер + +Третья пилюля вкладки SCP80 показывает, **какие OTA-приложения зарегистрированы на карте**: отправляется по одному +защищённому пакету на каждый выбранный TAR — стандартные значения из TS 101 220 V18.3.0 Annex D, Table D.1 +(предзаполнены, все отмечены) плюс свои TAR, добавляемые кнопкой **Добавить TAR** и хранимые в `localStorage` — с +безобидной C-APDU (по умолчанию `SELECT MF`), пресетом и keyset этого представления и SPI `16/01` (проверка +счётчика + PoR). Каждый ответ классифицируется: **зарегистрировано** (ENVELOPE 9000 + PoR со SW и данными +приложения), **принято, нет ответа** (PoR без R-APDU), **принято, нет PoR** (пакет молча отброшен) и +**отклонено** (ENVELOPE отклонён). Каждый принятый картой пакет расходует счётчик, который сохраняется в keyset +пресета; проба отклоняется, если в SPI1 нет проверки счётчика или в пресете нет такого номера keyset. +`POST /api/tar-probe` (см. `docs/api.md`). + +--- + ## Карты -Хранит сохранённые конфигурации карт (пресеты) в `localStorage`. Пресет содержит криптографические ключи, настройки SPI, TAR-ы и счётчик повторов для SCP80-операций, необязательный ключ **ADM**, а также пару **PSK identity / PSK key** для слушателя SCP81 HTTP OTA. «Карты» — **верхнеуровневая вкладка**. Поля формы сгруппированы в два блока с рамкой — **SCP80 (GSM 03.48, ETSI TS 102 225)** и **SCP81 (HTTP OTA)**; необязательное поле **ADM** находится в верхней строке. При подключении карты читается её EF.ICCID, и пресет с тем же ICCID автоматически выбирается в обоих видах SCP80. В шапке отображаются серые маркеры **SCP80** / **SCP81** и значок ключа на бейдже **ADM**, когда в пресете с ICCID подключённой карты эти поля заполнены. +Хранит сохранённые конфигурации карт (пресеты) в **серверном хранилище** — `~/.pysim-simple-server/card_presets.json` (переопределяется `--card-presets`). Пресет содержит криптографические ключи, настройки SPI, TAR-ы и счётчики повторов для SCP80-операций, необязательный ключ **ADM**, а также пару **PSK identity / PSK key** для слушателя SCP81 HTTP OTA. Пресет содержит **несколько keysets** — номер keyset задаётся нибблом `b8..b5` его KIc/KID (TS 102 225 §5.1.2/A.2): KIc и KID одного keyset должны нести один и тот же номер (`01`–`0F`; A.2 отвергает несовпадение), `00` («без защиты») не является keyset пресета, и у каждого keyset на карте свой счётчик (Annex A.1) — счётчиками владеет сервер: он сохраняет каждый израсходованный инкремент и пишет его в журнал аудита (`card_presets.json.audit.jsonl`). Список загружается при подключении PWA; экспорт/импорт JSON (принимает и старые плоские пресеты) — путь миграции из браузерного хранилища. «Карты» — **верхнеуровневая вкладка**. Поля формы сгруппированы в два блока с рамкой — **SCP80 (GSM 03.48, ETSI TS 102 225)** и **SCP81 (HTTP OTA)**; необязательное поле **ADM** находится в верхней строке, а keysets редактируются строками (по одной на номер, **Добавить keyset** / **Удалить keyset**). При подключении карты читается её EF.ICCID, и пресет с тем же ICCID автоматически выбирается во всех трёх видах SCP80; если подключённая карта не совпадает ни с одним пресетом — или выбранный пресет принадлежит другой карте — все три вида SCP80 показывают жёлтое предупреждение над выбором пресета. В шапке отображаются серые маркеры **SCP80** / **SCP81** и значок ключа на бейдже **ADM**, когда в пресете с ICCID подключённой карты эти поля заполнены. | Поле | Описание | |---|---| @@ -500,20 +514,20 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на | ICCID | Опциональный идентификатор карты; кнопка **С карты** подставляет EF.ICCID подключённой карты (активна, только когда ICCID читается) | | ADM | Необязательный администраторский PIN (hex или до 8 ASCII-цифр), сохраняется для файлового менеджера; представлениями SCP80/SCP81 не используется | | SPI1 / SPI2 | Уровень безопасности и настройки PoR | -| Индекс KIc / KID | Номер версии ключа (задаётся вместе с ключами) | +| Индекс KIc / KID | Номер keyset (`01`–`0F`, из байта KIc/KID; задаётся вместе с ключами) | | Ключ KIc / KID | Hex-ключи шифрования и MAC | | ISD TAR | TAR Issuer Security Domain (TS 101 220 Annex D), используется для операций RAM/GP; умолчание `000000` | | UICC RFM TAR | TAR UICC Shared File System RFM (TS 102 226 §7.2), используется в представлении SIM RFM; умолчание `B00000` | | ADF RFM TAR | TAR ADF RFM (TS 102 226 §7.3), привязан к AID приложения (ADF.USIM в представлении USIM RFM); умолчание `B00001` | -| Счётчик (CNTR) | 10-значный hex-счётчик повторов, автоматически увеличивается после каждой успешной отправки SCP80 | +| Счётчик (CNTR) | 10-значный hex-счётчик повторов, свой у каждого keyset; сервер увеличивает и сохраняет его после каждой операции, которая его расходует (при SPI1 b5 b4 = `00` счётчик не ведётся, TS 102 225 §5.1.1) | | PSK identity | SCP81 HTTP OTA: идентификатор, который карта присылает в TLS-рукопожатии | | PSK key | SCP81 HTTP OTA: 32 hex-символа (16 байт); ключ выбирается по идентификатору, который предъявляет карта | Столбец **SCP81** показывает, задана ли в пресете рабочая пара PSK: **✓** (идентификатор и ключ), **⚠** (только одно из двух — слушатель такой пресет игнорирует), **—** (PSK нет). Идентификатор и ключ задаются вместе. -**Добавить карту:** заполните имя, ICCID (опционально — кнопка **С карты** подставляет EF.ICCID подключённой карты), необязательный **ADM**, SPI1/SPI2, ключи и индексы KIc/KID, три TAR-а, при необходимости пару PSK и нажмите **Add**. Дубликат ICCID (сравнение без пробелов и с учётом сырой hex-формы) отклоняется с указанием конфликтующего пресета. Карта появится в списке и станет доступна в выпадающем списке **Card preset** на вкладке RAM. +**Добавить карту:** заполните имя, ICCID (опционально — кнопка **С карты** подставляет EF.ICCID подключённой карты), необязательный **ADM**, SPI1/SPI2, один или несколько keysets (KIc/KID + ключи на номер, **Добавить keyset**), три TAR-а, при необходимости пару PSK и нажмите **Add**. Дубликат ICCID (сравнение без пробелов и с учётом сырой hex-формы) отклоняется с указанием конфликтующего пресета. Карта появится в списке и станет доступна в выпадающем списке **Card preset** на вкладке RAM. -**Изменить/удалить:** **Edit** загружает пресет в форму (кнопка Add становится **Save**; **Cancel** очищает форму); **Remove** удаляет строку из `localStorage`. Успешная отправка SCP80 увеличивает и сохраняет счётчик повторов, а изменения сразу передаются работающему слушателю SCP81. +**Изменить/удалить:** **Edit** загружает пресет в форму (кнопка Add становится **Save**; **Cancel** очищает форму); **Remove** удаляет пресет из серверного хранилища. Успешная отправка SCP80 увеличивает и сохраняет счётчик использованного keyset, а изменения сразу передаются работающему слушателю SCP81. --- @@ -531,7 +545,7 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на - **Read raw** / **Read decoded** — чтение выбранного файла как hex-дампа или как таблицы декодированных полей (клиентские декодеры: IMSI, ICCID, SPN, списки PLMN, LOCI/EPSLOCI, ADN/MSISDN, таблицы сервисов, SUME, …); подсвеченная пилла — текущий вид, клик по любой из них перечитывает файл. Серверный pySim JSON того же чтения остаётся в свёрнутом блоке *pySim JSON (server)*; **Read decoded** отключается, когда клиентские декодеры не покрывают файл (определяется по имени/FID до чтения) - **Edit raw** — редактирование сырых hex-данных и **Save** для записи (или **Cancel**); для transparent-файлов — textarea, для record-файлов — по полю на запись с флажками выбора. Декодированный вид только для чтения — **Edit raw** сначала переключается на hex и при необходимости читает файл - **Проверить все файлы** — обход всего дерева (включая пользовательские) с пометкой «есть/нет», прогрессом *N / всего*, возможностью остановки и сводкой в конце; сам просмотр остаётся ленивым. Отсутствующие файлы показаны красным (✗); существующий пустой DF — `(пусто)` -- **ADM** — файлы, требующие администраторский PIN, возвращают `6982`/`9804`; если в подходящей предустановке карты (тот же ICCID) есть ключ ADM, рядом с ошибкой появляется кнопка **«Проверить ADM»**, а значок в заголовке (`ADM ✓/✗ ⚿`) становится кликабельным. Каждый неверный ключ расходует попытку (остаток показывается, повторная попытка требует подтверждения); заблокированный ADM требует ключа разблокировки карты +- **ADM** — файлы, требующие администраторский PIN, возвращают `6982`/`9804`; если в подходящем пресете карты (тот же ICCID) есть ключ ADM, рядом с ошибкой появляется кнопка **«Проверить ADM»**, а значок в заголовке (`ADM ✓/✗ ⚿`) становится кликабельным. Каждый неверный ключ расходует попытку (остаток показывается, повторная попытка требует подтверждения); заблокированный ADM требует ключа разблокировки карты ### Подсказки команд diff --git a/docs/api.md b/docs/api.md index 25fb2db..e428525 100644 --- a/docs/api.md +++ b/docs/api.md @@ -197,8 +197,8 @@ it; `Path.home()` resolves the same way on Linux, macOS and Windows). The file is written atomically and every counter change is appended to `card_presets.json.audit.jsonl`. -**Keysets.** A preset carries one keyset per GlobalPlatform key version — the -`b8..b5` nibble of KIc/KID (TS 102 225 §5.1.2/A.2); the version is implicit in +**Keysets.** A preset carries several keysets — a keyset's number is the +`b8..b5` nibble of its KIc/KID (TS 102 225 §5.1.2/A.2); the number is implicit in the bytes, there is no separate field: ```json @@ -211,24 +211,24 @@ the bytes, there is no separate field: ]} ``` -Rules (all refused with `400`): KIc and KID must carry the **same** key version +Rules (all refused with `400`): KIc and KID must carry the **same** keyset number (A.2 — the card rejects a mismatch with "Unidentified security error"), the -version must be `01`–`0F` (`00` means "no security" and is a packet-level -choice, not a preset keyset), no two keysets may share a version, both keys and +number must be `01`–`0F` (`00` means "no security" and is a packet-level +choice, not a preset keyset), no two keysets may share a number, both keys and a counter (1–10 hex digits) are required, and the counters are stored in the fixed-width 10-hex form. A v3.8.0 flat preset (`kic`/`kid`/`kicKey`/`kidKey`/ `cntr` at the top level) converts into a single keyset on load and on import, so old files and exports keep working. **Counters.** The store is the source of truth for the SCP80 counters, one per -key version ("a dedicated counter shall be associated to each key version", -Annex A.1). The operations below accept a `preset_id` and **persist the +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 counter they consumed themselves** (monotonic per keyset — a stale value never regresses it), so a closed tab, a lost response or a second browser window can no longer lose an increment. A request whose KIc/KID name a **non-zero key -version the preset does not define** is refused (`400 {"error": "key version N +number the preset does not define** is refused (`400 {"error": "keyset N is not defined in preset '…' - add it in the Cards tab"}`), and so is a -KIc/KID version mismatch. The Cards tab's export/import uses this API; the +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": 2, "presets": [{…}]}` @@ -254,7 +254,7 @@ a `tars` list is given), each carrying a harmless C-APDU (`SELECT MF` by default), with the preset's keyset and SPI `16/01` (counter check + PoR). Every accepted packet consumes a counter, which is persisted into the keyset like any other operation, so the probe is refused when the SPI1 has no counter -check (b5b4 = 00) or the named preset does not define the key version. +check (b5b4 = 00) or the named preset does not define the keyset number. ```json {"preset_id": "…", "kic": "25", "kid": "25", "kicKey": "…", "kidKey": "…", @@ -413,10 +413,10 @@ scripts) must not be wrapped. the counter that was sent, advanced by one when the card accepted the packet. A request without a `cntr` (nothing to advance) simply reports no `final_cntr` — it never fails the send. With `preset_id` the server persists -the counter into the keyset of the key version the packet used (see *Card +the counter into the keyset the packet used (see *Card presets*), so a lost response or a closed tab cannot lose the increment; the -request is refused when KIc/KID carry different key versions (TS 102 225 A.2) -or a non-zero key version the preset does not define. +request is refused when KIc/KID carry different keyset numbers (TS 102 225 A.2) +or a non-zero keyset number the preset does not define. **Counter tracking follows SPI1.b5b4** (TS 102 225 §5.1.1): with `00` the counter field is "present, ignored, never updated", so a packet whose SPI1 has diff --git a/frontend/help-ru.html b/frontend/help-ru.html index f0a1b2c..80b158b 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -42,9 +42,9 @@

1.1 Интерфейс

@@ -56,7 +56,7 @@

2.1 SIM RFM

CLA = A0 (GSM 11.11 / TS 151 011, ISO 7816-4). Удалённое управление файлами классических SIM-карт.

-

Команды собираются в виде цепочки: нажмите кнопку + SELECT (или другую командную кнопку), чтобы добавить строку, заполните её поля — предпросмотр цепочки (над кнопкой упаковки) обновится автоматически. Добавьте строку GET RESPONSE, чтобы получить данные после SELECT. Кнопка Упаковать в Secured packet упаковывает всю цепочку в пакет SCP80 и устанавливает TAR SCP80 в UICC RFM TAR выбранной предустановки (по умолчанию B00000) — TAR UICC Shared File System RFM: MF выбирается неявно, ADF недоступны, поэтому пути SELECT адресуют файлы от MF.

+

Команды собираются в виде цепочки: нажмите кнопку + SELECT (или другую командную кнопку), чтобы добавить строку, заполните её поля — предпросмотр цепочки (над кнопкой упаковки) обновится автоматически. Добавьте строку GET RESPONSE, чтобы получить данные после SELECT. Кнопка Упаковать в Secured packet упаковывает всю цепочку в пакет SCP80 и устанавливает TAR SCP80 в UICC RFM TAR выбранного пресета (по умолчанию B00000) — TAR UICC Shared File System RFM: MF выбирается неявно, ADF недоступны, поэтому пути SELECT адресуют файлы от MF.

@@ -109,7 +109,7 @@
КомандаINSОписание
@@ -311,10 +311,10 @@
  • AES требует счётчик с защитой от повтора: биты SPI1 b5 b4 должны быть 10 (счётчик больше) или 11 (счётчик +1) согласно TS 102 225 §5.1.2/§5.1.3.1
  • Байт паддинга настраивается (00 по умолчанию или FF)
  • -

    Кнопка Проверить в pySim сверяет собранный пакет с эталонной реализацией OtaDialectSms.encode_cmd. Кнопка Отправить на карту доставляет пакет через ENVELOPE SMS-PP-DOWNLOAD (при подключении к серверу). 2-байтовый префикс CPL передаётся, когда пакет зашифрован или несёт RC/CC/DS (входит в их входные данные), а также при конкатенации SMS; одиночное незащищённое SMS сохраняет форму pySim, начинающуюся с CHL (TS 31.115 Table 1 NOTE). Под полем пакета показывается его размер и число SMS; пакет, не помещающийся в одно SMS, отправляется конкатенированной загрузкой (TS 31.115 §4.3) — пакет делится на части SMS-пользовательских данных (первое SMS 132 октета, последующие по 134; первое дополнительно несёт информационные элементы конкатенации и CPI), и сегменты отправляются по порядку. Более 5 сегментов отклоняется: это практический предел буфера конкатенации карты. Полученный Proof of Receipt декодируется и показывается строкой статуса PoR (статус, TAR, счётчик, сырой PoR); статусное слово и данные ответа последней команды подставляются в подвкладку «Разбор R-APDU» (Remote APDU → «Разбор»), а успешный PoR увеличивает счётчик повторов и очищает пакет.

    +

    Кнопка Проверить в pySim сверяет собранный пакет с эталонной реализацией OtaDialectSms.encode_cmd. Кнопка Отправить на карту доставляет пакет через ENVELOPE SMS-PP-DOWNLOAD (при подключении к серверу). 2-байтовый префикс CPL передаётся, когда пакет зашифрован или несёт RC/CC/DS (входит в их входные данные), а также при конкатенации SMS; одиночное незащищённое SMS сохраняет форму pySim, начинающуюся с CHL (TS 31.115 Table 1 NOTE). Под полем пакета показывается его размер и число SMS; пакет, не помещающийся в одно SMS, отправляется конкатенированной загрузкой (TS 31.115 §4.3) — пакет делится на части SMS-пользовательских данных (первое SMS 132 октета, последующие по 134; первое дополнительно несёт информационные элементы конкатенации и CPI), и сегменты отправляются по порядку. Более 5 сегментов отклоняется: это практический предел буфера конкатенации карты. Полученный Proof of Receipt декодируется и показывается строкой статуса PoR (статус, TAR, счётчик, сырой PoR); статусное слово и данные ответа последней команды подставляются в подвкладку «Разбор R-APDU» (Remote APDU → «Разбор»), а успешный PoR увеличивает счётчик повторов (когда в SPI1 есть проверка счётчика — b5 b4 ≠ 00; без неё счётчик не увеличивается и не сохраняется) и очищает пакет.

    3.2 RAM

    -

    Выполняет операции удалённого управления приложениями (Remote Application Management) как защищённые пакеты SCP80 через SMS-PP-DOWNLOAD ENVELOPE. Карта должна поддерживать SCP03 (AES или 3DES). Предустановка карты со вкладки Карты обеспечивает SPI, ключи, TAR и счётчик (RAM использует ISD TAR предустановки, по умолчанию 000000).

    +

    Выполняет операции удалённого управления приложениями (Remote Application Management) как защищённые пакеты SCP80 через SMS-PP-DOWNLOAD ENVELOPE. Карта должна поддерживать SCP03 (AES или 3DES). Пресет карты со вкладки Карты обеспечивает SPI, ключи, TAR и счётчик (RAM использует ISD TAR пресета, по умолчанию 000000). Селектор Keyset рядом с выбором пресета указывает, из какого keyset берутся ключи и счётчик операции.

    Операции

    ПолеРазмерОписание
    @@ -340,31 +340,31 @@

    3.3 TAR-сканер

    -

    Третья пилюля вкладки SCP80 показывает, какие OTA-приложения зарегистрированы на карте: отправляется по одному защищённому пакету на каждый выбранный TAR — стандартные значения из TS 101 220 V18.3.0 Annex D, Table D.1, каждая строка несёт документ-источник — с безобидной C-APDU (по умолчанию SELECT MF), предустановкой и набором ключей этого представления, SPI 16/01 (проверка счётчика + PoR).

    +

    Третья пилюля вкладки SCP80 показывает, какие OTA-приложения зарегистрированы на карте: отправляется по одному защищённому пакету на каждый выбранный TAR — стандартные значения из TS 101 220 V18.3.0 Annex D, Table D.1, каждая строка несёт документ-источник — с безобидной C-APDU (по умолчанию SELECT MF), пресетом и keyset этого представления, SPI 16/01 (проверка счётчика + PoR).

    Список предзаполнен стандартными TAR (все отмечены; кнопки Выбрать все / Снять все переключают быстро), свои TAR добавляются кнопкой Добавить TAR (три hex-байта и необязательное описание). Пользовательские записи хранятся в localStorage по возможности — их потеря безвредна.

    Каждый TAR классифицируется по ответу карты: зарегистрировано (ENVELOPE 9000 + PoR со SW и данными приложения), принято, нет ответа (PoR без R-APDU), принято, нет PoR (пакет молча отброшен) и отклонено (ENVELOPE не 9000, обычно 6200 для TAR без зарегистрированного приложения). Таблица результата появляется в окне операции; строка под кнопкой Execute показывает зарегистрировано N / M.

    -

    Каждый принятый картой пакет расходует счётчик, который сохраняется в набор ключей предустановки — проба отклоняется, если в SPI1 нет проверки счётчика или в предустановке нет указанной версии ключа. На живой карте: 000000, B00000 и B00001 зарегистрированы (приложения компактного формата), расширенный формат, USAT, мультиплексирование и CASD отклонены с 6200.

    +

    Каждый принятый картой пакет расходует счётчик, который сохраняется в keyset пресета — проба отклоняется, если в SPI1 нет проверки счётчика или в пресете нет такого номера keyset. На живой карте: 000000, B00000 и B00001 зарегистрированы (приложения компактного формата), расширенный формат, USAT, мультиплексирование и CASD отклонены с 6200.

    4. Вкладка «Карты»

    -

    Хранит предустановки карт на сервере (~/.pysim-simple-server/card_presets.json; переопределяется --card-presets), чтобы представление Secured Packet могло автоматически подставлять ключи и параметры, слушатель SCP81 HTTP OTA — находить PSK-ключи, а счётчики SCP80, израсходованные операцией, сохранялись самой операцией — закрытая вкладка или второе окно браузера больше не могут потерять инкремент. Предустановка содержит один набор ключей на версию ключа (ниббл b8..b5 KIc/KID, TS 102 225 §5.1.2/A.2): KIc и KID набора должны содержать одну версию (01–0F; A.2 отвергает несовпадение), и у каждой версии ключа на карте свой счётчик (Annex A.1), поэтому счётчики ведутся по наборам. Список загружается при подключении PWA и экспортируется/импортируется как JSON (импорт принимает и предустановки старых сборок — плоские и с наборами ключей). «Карты» — верхнеуровневая вкладка. При подключении карты читается её EF.ICCID, и предустановка с тем же ICCID автоматически выбирается в обоих представлениях SCP80; если подключённая карта не совпадает ни с одной предустановкой — или выбранная предустановка принадлежит другой карте — оба представления SCP80/RAM показывают жёлтое предупреждение над выбором предустановки. Поля формы сгруппированы в два блока с рамкой: SCP80 (GSM 03.48, ETSI TS 102 225) (KIc/KID, SPI1/SPI2, TAR-ы ISD/UICC RFM/ADF RFM, счётчик и ключи) и SCP81 (HTTP OTA) (пара PSK identity/ключ с пояснением о выборе ключа). Необязательное поле ADM находится в верхней строке и сохраняется для последующего использования файловым менеджером. В шапке отображаются серые маркеры SCP80/SCP81 и значок ключа на бейдже ADM, когда в предустановке с ICCID подключённой карты эти поля заполнены.

    +

    Хранит пресеты карт на сервере (~/.pysim-simple-server/card_presets.json; переопределяется --card-presets), чтобы представление Secured Packet могло автоматически подставлять ключи и параметры, слушатель SCP81 HTTP OTA — находить PSK-ключи, а счётчики SCP80, израсходованные операцией, сохранялись самой операцией — закрытая вкладка или второе окно браузера больше не могут потерять инкремент. Пресет содержит несколько keysets — номер keyset задаётся нибблом b8..b5 его KIc/KID (TS 102 225 §5.1.2/A.2): KIc и KID одного keyset должны нести один и тот же номер (01–0F; A.2 отвергает несовпадение), и у каждого keyset на карте свой счётчик (Annex A.1), поэтому счётчики ведутся по keysets. В форме keysets редактируются строками — по одной на номер, с кнопками Добавить keyset / Удалить keyset; номер в строке вычисляется из байта KIc/KID, а 00 («без защиты») не принимается как keyset. Список загружается при подключении PWA и экспортируется/импортируется как JSON (импорт принимает и пресеты старых сборок — плоские и с keysets). «Карты» — верхнеуровневая вкладка. При подключении карты читается её EF.ICCID, и пресет с тем же ICCID автоматически выбирается во всех трёх представлениях SCP80; если подключённая карта не совпадает ни с одним пресетом — или выбранный пресет принадлежит другой карте — все три представления SCP80 показывают жёлтое предупреждение над выбором пресета. Поля формы сгруппированы в два блока с рамкой: SCP80 (GSM 03.48, ETSI TS 102 225) (KIc/KID, SPI1/SPI2, TAR-ы ISD/UICC RFM/ADF RFM, счётчик и ключи) и SCP81 (HTTP OTA) (пара PSK identity/ключ с пояснением о выборе ключа). Необязательное поле ADM находится в верхней строке и сохраняется для последующего использования файловым менеджером. В шапке отображаются серые маркеры SCP80/SCP81 и значок ключа на бейдже ADM, когда в пресете с ICCID подключённой карты эти поля заполнены.

    - + - +
    ПолеОписание
    NameПонятная метка (обязательна)
    ICCIDОпциональный идентификатор карты. Кнопка С карты подставляет EF.ICCID подключённой карты (активна, только когда ICCID читается); при сохранении ICCID, уже присутствующий в списке, отклоняется (сравнение без пробелов и с учётом сырой hex-формы, конфликтующий пресет называется)
    ADMНеобязательный администраторский PIN (hex или до 8 ASCII-цифр), сохраняется в предустановке для последующего использования файловым менеджером; представлениями SCP80/SCP81 не используется
    ADMНеобязательный администраторский PIN (hex или до 8 ASCII-цифр), сохраняется в пресете для последующего использования файловым менеджером; представлениями SCP80/SCP81 не используется
    KIc / KIDИндикаторы ключа и алгоритма (например, 15 = индекс 1, 3DES-CBC2; x2 = AES); задаются вместе с ключами
    SPI1 / SPI2Security Parameter Indicators
    ISD TARTAR Issuer Security Domain (TS 101 220, приложение D); используется для операций RAM/GP (представление SCP80 RAM, цепочки RAM/GP); по умолчанию по спецификации 000000, подставляется при пустом значении
    UICC RFM TARTAR UICC Shared File System RFM (TS 102 226 §7.2, TS 101 220 приложение D): MF выбирается неявно и доступны только файлы под MF (ADF недоступны); используется представлением SIM RFM; по умолчанию B00000
    ADF RFM TARTAR ADF RFM (TS 102 226 §7.3, TS 101 220 приложение D): ADF выбирается неявно, TAR привязан к AID приложения — ADF.USIM в представлении USIM RFM; по умолчанию B00001
    CounterСчётчик повторов (5 байт); увеличивается и сохраняется после каждой успешной отправки SCP80
    CounterСчётчик повторов (5 байт); свой у каждого keyset, сервер увеличивает и сохраняет его после каждой операции, которая его расходует
    KIc key / KID key16/24/32 hex-символа (ключи 8/16/24 байта 3DES) или 32/48/64 hex-символа (ключи 16/24/32 байта AES)
    PSK identitySCP81 HTTP OTA: идентификатор, который карта присылает в TLS-рукопожатии (например, 89390100000129506903)
    PSK keySCP81 HTTP OTA: 32 hex-символа (16 байт) для PSK TLS-слушателя; ключ выбирается по идентификатору, который предъявляет карта
    -

    Столбец SCP81 показывает, задана ли в предустановке рабочая пара PSK: ✓ (зелёный) — идентификатор и ключ заданы; ⚠ (жёлтый) — задано только одно из двух, и слушатель такую предустановку игнорирует; — — PSK нет. Идентификатор и ключ PSK задаются вместе. Кнопка Редактировать загружает предустановку в форму (кнопка Add становится Сохранить; Отмена очищает форму), поэтому поля можно менять без повторного ввода карты; Удалить удаляет строку. Обмен предустановками: Экспорт в файл для выгрузки и Импорт из файла для загрузки. Выбранная предустановка автоматически заполняет форму Secured Packet и устанавливает TAR для операции (RAM/GP → ISD TAR, SIM RFM → UICC RFM TAR, USIM RFM → ADF RFM TAR); TAR, введённый в форму Secured Packet вручную, сохраняется при отправке - счётчик, ключи и SPI по-прежнему берутся из предустановки; изменения сразу передаются работающему слушателю SCP81.

    +

    Столбец SCP81 показывает, задана ли в пресете рабочая пара PSK: ✓ (зелёный) — идентификатор и ключ заданы; ⚠ (жёлтый) — задано только одно из двух, и слушатель такой пресет игнорирует; — — PSK нет. Идентификатор и ключ PSK задаются вместе. Кнопка Редактировать загружает пресет в форму (кнопка Add становится Сохранить; Отмена очищает форму), поэтому поля можно менять без повторного ввода карты; Удалить удаляет строку. Обмен пресетами: Экспорт в файл для выгрузки и Импорт из файла для загрузки. Выбранный пресет автоматически заполняет форму Secured Packet и устанавливает TAR для операции (RAM/GP → ISD TAR, SIM RFM → UICC RFM TAR, USIM RFM → ADF RFM TAR); TAR, введённый в форму Secured Packet вручную, сохраняется при отправке - счётчик, ключи и SPI по-прежнему берутся из пресета; изменения сразу передаются работающему слушателю SCP81.

    @@ -373,7 +373,7 @@

    5.1 Слушатель

    Кнопки Start / Остановить управляют целью; Перезапустить скрипт ставит выбранный скрипт в очередь заново с force, начиная прогон с первого APDU. Режимы:

    6.2 Командная строка pySim

    diff --git a/frontend/help.html b/frontend/help.html index 450059f..091f952 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -44,7 +44,7 @@ @@ -278,10 +278,10 @@

    3. SCP80 tab

    -

    The SCP80 top-level tab groups the SCP80-related views. It is switched by two pills: Secured Packet and RAM. Assembles SCP80 secured packets per ETSI TS 102 225. When a card is equipped the server reads its EF.ICCID (2FE2); if a card preset carries the same number (digits, or the raw EF hex), it is selected automatically in both views.

    +

    The SCP80 top-level tab groups the SCP80-related views. It is switched by three pills: Secured Packet, RAM and TAR probe. Assembles SCP80 secured packets per ETSI TS 102 225. When a card is equipped the server reads its EF.ICCID (2FE2); if a card preset carries the same number (digits, or the raw EF hex), it is selected automatically in all three SCP80 views.

    3.1 Secured Packet

    -

    Builds SCP80 secured packets per ETSI TS 102 225.

    +

    Builds SCP80 secured packets per ETSI TS 102 225. The Keyset selector next to the card preset selector picks which keyset of the preset supplies the keys and the counter; the send is refused when the preset does not define that keyset number or when its KIc/KID carry different numbers (TS 102 225 A.2).

    Packet structure

    @@ -310,10 +310,10 @@
  • AES requires a replay-protected counter: SPI1 bits b5 b4 must be 10 (counter higher) or 11 (counter +1) per TS 102 225 §5.1.2/§5.1.3.1
  • Padding byte configurable (00 default, or FF)
  • -

    A “Verify vs pySim” button cross-checks the assembled packet against pySim’s reference OtaDialectSms.encode_cmd. A “Send to Card” button delivers it via SMS-PP-DOWNLOAD ENVELOPE (when connected to the server). The 2-byte CPL prefix is transmitted whenever the packet is ciphered or carries RC/CC/DS (it is part of their input) and whenever the packet is concatenated; a single unprotected SMS keeps pySim’s CHL-first form (TS 31.115 Table 1 NOTE). Under the packet field the app reports its size and how many SMS it takes; a packet that does not fit one SMS is sent as a concatenated download (TS 31.115 §4.3) — the packet is split into SMS user-data parts (first SM 132 octets, following ones 134; the first one additionally carries the concatenation and CPI information elements) and the segments are sent in order. More than 5 segments is refused, that being the practical limit of the card’s concatenation buffer. The returned Proof of Receipt is decoded and shown as a PoR status line (status, TAR, counter, raw PoR); the last command’s status word and response data are filled into the R-APDU parser panel, and a successful PoR advances the replay counter and clears the packet. A low-counter verdict (cntr_low, or a warning ENVELOPE answer that still carries such a PoR) is not recoverable by retrying: every SCP80/RAM operation stops immediately and the result states the card's counter with a Go to preset button that opens the matching preset in the Cards tab, where the counter must be raised above it — nothing is written automatically.

    +

    A “Verify vs pySim” button cross-checks the assembled packet against pySim’s reference OtaDialectSms.encode_cmd. A “Send to Card” button delivers it via SMS-PP-DOWNLOAD ENVELOPE (when connected to the server). The 2-byte CPL prefix is transmitted whenever the packet is ciphered or carries RC/CC/DS (it is part of their input) and whenever the packet is concatenated; a single unprotected SMS keeps pySim’s CHL-first form (TS 31.115 Table 1 NOTE). Under the packet field the app reports its size and how many SMS it takes; a packet that does not fit one SMS is sent as a concatenated download (TS 31.115 §4.3) — the packet is split into SMS user-data parts (first SM 132 octets, following ones 134; the first one additionally carries the concatenation and CPI information elements) and the segments are sent in order. More than 5 segments is refused, that being the practical limit of the card’s concatenation buffer. The returned Proof of Receipt is decoded and shown as a PoR status line (status, TAR, counter, raw PoR); the last command’s status word and response data are filled into the R-APDU parser panel, and a successful PoR advances the replay counter (when SPI1 carries a counter check — b5 b4 ≠ 00; without one no counter is advanced or saved) and clears the packet. A low-counter verdict (cntr_low, or a warning ENVELOPE answer that still carries such a PoR) is not recoverable by retrying: every SCP80/RAM operation stops immediately and the result states the card's counter with a Go to preset button that opens the matching preset in the Cards tab, where the counter must be raised above it — nothing is written automatically.

    3.2 RAM

    -

    Delivers Remote Application Management operations as SCP80 secured packets via SMS-PP-DOWNLOAD ENVELOPE. The card must support SCP03 (AES or 3DES). A saved card preset from the Cards tab provides the SPI, keys, TAR, and counter (RAM uses the preset's ISD TAR, 000000 by default).

    +

    Delivers Remote Application Management operations as SCP80 secured packets via SMS-PP-DOWNLOAD ENVELOPE. The card must support SCP03 (AES or 3DES). A saved card preset from the Cards tab provides the SPI, keys, TAR, and counter (RAM uses the preset's ISD TAR, 000000 by default). The Keyset selector next to the preset selector picks the keyset whose keys and counter the operation uses.

    Operations

    FieldSizeDescription
    @@ -342,10 +342,10 @@

    The third SCP80 pill probes which OTA applications the card has registered: it sends one secured packet per selected TAR — the standard allocations of TS 101 220 V18.3.0 Annex D, Table D.1, each row carrying the document it comes from — with a harmless C-APDU (default SELECT MF) using the view's preset and keyset, SPI 16/01 (counter check + PoR).

    The list is pre-filled with the standard TARs (all checked; Select all / Select none switch them quickly) and custom TARs can be added with Add TAR (three hex bytes plus an optional description). Custom entries are kept in localStorage on a best-effort basis — losing them is harmless.

    Every TAR is classified from the card's answer: registered (ENVELOPE 9000 + a PoR carrying the application's own status word and data), accepted, no answer (a PoR without an R-APDU), accepted, no PoR (the packet was silently dropped) and refused (a non-9000 ENVELOPE, typically 6200 for a TAR without a registered application). The result table appears in the operation popup; the line under Execute summarises registered N / M.

    -

    Each packet the card accepts consumes a counter, which is saved into the preset's keyset — the probe is refused when SPI1 has no counter check or the named preset does not define the key version. Observed on a live card: 000000, B00000 and B00001 registered (compact-format applications), the expanded-format TARs, USAT, multiplexing and CASD refused with 6200.

    +

    Each packet the card accepts consumes a counter, which is saved into the preset's keyset — the probe is refused when SPI1 has no counter check or the named preset does not define the keyset number. Observed on a live card: 000000, B00000 and B00001 registered (compact-format applications), the expanded-format TARs, USAT, multiplexing and CASD refused with 6200.

    4. Cards tab

    -

    Stores card presets on the server (~/.pysim-simple-server/card_presets.json; --card-presets 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 one keyset per key version (the b8..b5 nibble of KIc/KID, TS 102 225 §5.1.2/A.2): KIc and KID of a keyset must carry the same version (01–0F; A.2 rejects a mismatch), and each key version has its own counter on the card (Annex A.1), so counters are tracked per 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 both SCP80 views; when the equipped card matches no preset — or the selected preset belongs to another card — both SCP80/RAM views show an amber warning above the preset selector. The form groups the fields into two bordered blocks: SCP80 (GSM 03.48, ETSI TS 102 225) (KIc/KID, SPI1/SPI2, the ISD/UICC RFM/ADF RFM TARs, counter and keys) and SCP81 (HTTP OTA) (the PSK identity/key pair with the key-selection note). The optional ADM field sits in the top row and is stored for later use by the file manager. The header shows gray SCP80/SCP81 markers and a key glyph on the ADM badge when the preset matching the equipped card's ICCID has those settings filled in.

    +

    Stores card presets on the server (~/.pysim-simple-server/card_presets.json; --card-presets 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 several keysets — a keyset's number is the b8..b5 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 Add keyset / Remove keyset; each row shows the number derived from its KIc/KID byte, and 00 (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 no preset — or the selected preset belongs to another card — all three SCP80 views show an amber warning above the preset selector. The form groups the fields into two bordered blocks: SCP80 (GSM 03.48, ETSI TS 102 225) (KIc/KID, SPI1/SPI2, the ISD/UICC RFM/ADF RFM TARs, counter and keys) and SCP81 (HTTP OTA) (the PSK identity/key pair with the key-selection note). The optional ADM field sits in the top row and is stored for later use by the file manager. The header shows gray SCP80/SCP81 markers and a key glyph on the ADM badge when the preset matching the equipped card's ICCID has those settings filled in.

    @@ -357,7 +357,7 @@ - + diff --git a/frontend/index.html b/frontend/index.html index 31cfc48..a0ca203 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -528,7 +528,7 @@ - + @@ -695,7 +695,7 @@
    - +
    @@ -922,7 +922,7 @@
    - +
    @@ -1580,7 +1580,7 @@
    - OTA keysets — one per key version, each with its own counter + OTA keysets — each with its own number and counter
    -

    KIc and KID must carry the same key version (their high nibble) in the range 01-0F — TS 102 225 A.2; each key version has its own counter on the card (Annex A.1).

    +

    KIc and KID must carry the same keyset number (their high nibble) in the range 01-0F — TS 102 225 A.2; each keyset has its own counter on the card (Annex A.1).

    SCP81 (HTTP OTA) @@ -1629,7 +1629,7 @@
    - + @@ -4941,7 +4941,7 @@ function crc32Bytes(data) { // CPL = octets from CHL to end incl. padding; CHL = 13 + len_sig (CC 8, RC 4). // CPI (0x70) and CHI (null) are NOT part of the SMS packet data. function genSp() { - // The packet's key version must be defined in the selected preset + // The packet's keyset number must be defined in the selected preset // (TS 102 225 A.2 / the "keyset must exist" rule). const ksErr = spKeysetGuard(); if (ksErr) { alert(ksErr); return; } @@ -7140,7 +7140,7 @@ function testStepRender() { {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')); - html += testFormRow('Key version (kvn, optional)', + 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')); html += testFormRow('SPI1 override (hex, optional)', testFormInput('test-f-spi1', p.spi1, '16')); @@ -7541,7 +7541,7 @@ async function testPollRun() { function testWriteBackCounter() { const st = _testRunState; if (!st || st.running) return; - // The server persists every counter the run consumed (one per key version); + // The server persists every counter the run consumed (one per keyset number); // this only keeps the local copies in step with what it reports. const counters = (st.scp80_counters && typeof st.scp80_counters === 'object') ? st.scp80_counters : null; @@ -10823,7 +10823,7 @@ function ramGoToPreset(idx) { switchTab('cards'); const i = Number(idx); if (Number.isInteger(i) && i >= 0 && cards[i]) cardsEdit(i); - // Focus the counter of the key version the failed operation used (the + // Focus the counter of the keyset number the failed operation used (the // keyset rows are keyed by their version; the first row otherwise). const kvn = spKeysetKvnOf((document.getElementById('sp-kic-hex') || {}).value) || spKeysetKvnOf((document.getElementById('sp-kid-hex') || {}).value) || 0; @@ -11304,15 +11304,15 @@ function cardsTarValue(key, value) { return value || defaults[key] || ''; } -// ===== OTA keysets (one per GlobalPlatform key version) ===== -// A preset carries one keyset per key version: the b8..b5 nibble of KIc/KID +// ===== OTA keysets (one per keyset number) ===== +// A preset carries several keysets: a keyset's number is the b8..b5 nibble of KIc/KID // (TS 102 225 5.1.2/A.2). The version is implicit in the bytes - the "key -// index" - and each key version has its own counter on the card ("a dedicated +// index" - and each keyset has its own counter on the card ("a dedicated // counter shall be associated to each key version", Annex A.1). A packet may -// use any key version the preset defines; '00'/'00' means no security (A.2) +// use any keyset the preset defines; '00'/'00' means no security (A.2) // and needs no keyset. -// The key version of a KIc/KID byte, or null when it is not a hex byte. +// The keyset number of a KIc/KID byte, or null when it is not a hex byte. function spKeysetKvnOf(hex) { const s = String(hex || '').trim().toUpperCase(); if (!/^[0-9A-F]{2}$/.test(s)) return null; @@ -11340,14 +11340,14 @@ function spKeysetList(preset) { return []; } -// The keyset of a preset for a key version, or null. +// The keyset of a preset with the given number, or null. function spKeysetFor(preset, kvn) { return spKeysetList(preset).find(ks => spKeysetKvnOf(ks.kic) === kvn) || null; } // The packet's key-version rules (pure): '' when the pair is usable, else the -// reason. With a preset, the (non-zero) key version must be defined in it - -// a packet may use any key version, but only one the preset defines. +// reason. With a preset, the (non-zero) keyset number must be defined in it - +// a packet may use any keyset, but only one the preset defines. function spKeysetCheck(preset, kicHex, kidHex) { const kic = String(kicHex || '').trim().toUpperCase(); const kid = String(kidHex || '').trim().toUpperCase(); @@ -11358,12 +11358,12 @@ function spKeysetCheck(preset, kicHex, kidHex) { } const kicV = spKeysetKvnOf(kic), kidV = spKeysetKvnOf(kid); if (kicV && kidV && kicV !== kidV) { - return t('KIc and KID must carry the same key version (TS 102 225 A.2)') + ': ' + kic + ' / ' + kid; + return t('KIc and KID must carry the same keyset number (TS 102 225 A.2)') + ': ' + kic + ' / ' + kid; } const kvn = kicV || kidV || 0; if (!kvn) return ''; // '00'/'00': no security if (preset && !spKeysetFor(preset, kvn)) { - return t('Key version') + ' ' + kvn + ' ' + t('is not defined in the preset') + + return t('Keyset') + ' ' + kvn + ' ' + t('is not defined in the preset') + (preset.name ? ' \u2014 ' + preset.name : ''); } return ''; @@ -11380,11 +11380,11 @@ function spKeysetOptionsHtml(preset, kicHex, kidHex) { if (v === null) return; if (v === kvn) found = true; html += ''; }); if (kvn && !found) { - html += ''; } if (!html) html = ''; @@ -11398,7 +11398,7 @@ function cardsKeysetRowHtml(ks, 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'; const kvn = spKeysetKvnOf(ks.kic); return '
    ' + - '' + (kvn === null ? '?' : kvn) + '' + + '' + (kvn === null ? '?' : kvn) + '' + '' + '' + '' + @@ -11438,8 +11438,8 @@ function cardsKeysetKvnUpdate(i) { const bad = (a !== null && b !== null && a !== b) || a === 0 || b === 0; el.textContent = a === null ? '?' : a; el.classList.toggle('text-red-500', !!bad); - el.title = bad ? t('KIc and KID must carry the same key version (TS 102 225 A.2)') - : t('Key version (b8..b5 of KIc/KID)'); + el.title = bad ? t('KIc and KID must carry the same keyset number (TS 102 225 A.2)') + : t('Keyset number (b8..b5 of KIc/KID)'); } // The keyset rows as preset entries (empty rows are dropped). @@ -11461,7 +11461,7 @@ function cardsKeysetsFromForm() { // ===== Keyset selectors (SCP80 / RAM) ===== // The selector follows the KIc/KID bytes: a keyset of the selected preset that -// matches their key version is selected; a version the preset does not define +// matches their keyset number is selected; a number the preset does not define // shows as "not defined" (and the packet actions refuse it). function spKeysetSync() { const spIdx = spPresetIdx(); @@ -11574,7 +11574,7 @@ function ramPresetIdx() { } // '' when the SCP80 form may build a packet, else the reason. With a preset -// selected the key version must be one of its keysets; without one only the +// selected the keyset number must be one of its keysets; without one only the // byte rules apply (hand-entered keys stay possible, a workbench flow). function spKeysetGuard() { const idx = spPresetIdx(); @@ -11649,13 +11649,13 @@ async function cardsAdd() { const ksErr = spKeysetCheck(null, ks.kic, ks.kid); if (ksErr) { alert(ksErr); return; } if (!spKeysetKvnOf(ks.kic) || !spKeysetKvnOf(ks.kid)) { - alert(t('Key version 00 means "no security" and is not a preset keyset')); + alert(t('Keyset 00 means "no security" and is not a preset keyset')); return; } } const kvns = v.keysets.map(ks => spKeysetKvnOf(ks.kic)); if (new Set(kvns).size !== kvns.length) { - alert(t('Duplicate key version in the keysets')); + alert(t('Duplicate keyset number in the keysets')); return; } if ((v.pskKey && !v.pskIdentity) || (!v.pskKey && v.pskIdentity)) { @@ -11887,7 +11887,7 @@ function cardsApplyFields(idx, keepTar) { const smSel = document.getElementById('sp-spi2-sm'); if (smSel) smSel.value = String((spi2Val >> 5) & 1); document.getElementById('sp-spi2-hex').value = c.spi2; - // The keyset: keep the key version the form carries when this preset + // The keyset: keep the keyset number the form carries when this preset // defines it, otherwise start from the preset's first keyset. const kicNow = (document.getElementById('sp-kic-hex') || {}).value; const kidNow = (document.getElementById('sp-kid-hex') || {}).value; @@ -18937,7 +18937,7 @@ const LANG_RU = { 'GET DATA tag': 'Tag GET DATA', 'Response data (hex)': 'Данные ответа (hex)', '— Select command —': '— Выберите команду —', - '— select preset —': '— выберите предустановленный —', + '— select preset —': '— выберите пресет —', 'Error: specify SELECT parameters': 'Ошибка: укажите параметры SELECT', 'Error: unknown command': 'Ошибка: неизвестная команда', 'Error: specify data': 'Ошибка: укажите данные', @@ -19771,28 +19771,27 @@ const LANG_RU = { '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: слушатель выбирает ключ по PSK identity, которую карта присылает в TLS-рукопожатии. Карты без PSK слушателем игнорируются.', 'Card with this ICCID already exists': 'Карта с таким ICCID уже есть в списке', 'Card equipped but its ICCID is not readable': 'Карта подключена, но её ICCID не читается', - 'OTA keysets — one per key version, each with its own counter': 'Наборы ключей OTA — по одному на версию ключа, у каждой свой счётчик', - 'Add keyset': 'Добавить набор ключей', - 'Remove keyset': 'Удалить набор ключей', - 'Key version (keyset)': 'Версия ключа (набор ключей)', - 'Key version': 'Версия ключа', - 'Key version (b8..b5 of KIc/KID)': 'Версия ключа (биты b8..b5 KIc/KID)', - 'Keysets (key version: KIc/KID — counter)': 'Наборы ключей (версия: KIc/KID — счётчик)', + 'OTA keysets — each with its own number and counter': 'OTA keysets — у каждого свой номер и счётчик', + 'Add keyset': 'Добавить keyset', + 'Remove keyset': 'Удалить keyset', + 'Keyset': 'Keyset', + 'Keyset number (b8..b5 of KIc/KID)': 'Номер keyset (b8..b5 KIc/KID)', + 'Keysets (number: KIc/KID — counter)': 'Keysets (номер: KIc/KID — счётчик)', 'must be one hex byte': 'должен быть одним hex-байтом', - 'KIc and KID must carry the same key version (TS 102 225 A.2)': 'KIc и KID должны содержать одну и ту же версию ключа (TS 102 225 A.2)', - 'is not defined in the preset': 'не определена в предустановке', - 'not defined in the preset': 'не определена в предустановке', - '— no keyset —': '— нет набора ключей —', + 'KIc and KID must carry the same keyset number (TS 102 225 A.2)': 'KIc и KID должны содержать один и тот же номер keyset (TS 102 225 A.2)', + 'is not defined in the preset': 'не определён в пресете', + 'not defined in the preset': 'не определён в пресете', + '— no keyset —': '— нет keyset —', 'counter': 'счётчик', - 'Define at least one keyset (KIc/KID and their keys)': 'Определите хотя бы один набор ключей (KIc/KID и их ключи)', + 'Define at least one keyset (KIc/KID and their keys)': 'Определите хотя бы один keyset (KIc/KID и его ключи)', 'Each keyset needs KIc, KID and both keys': 'Каждому набору нужны KIc, KID и оба ключа', - 'Key version 00 means "no security" and is not a preset keyset': 'Версия ключа 00 означает «без защиты» и не является набором ключей предустановки', - 'Duplicate key version in the keysets': 'Дублирующаяся версия ключа в наборах', - 'Select a card preset - its keysets define the key versions': 'Выберите предустановку карты — её наборы ключей определяют версии ключей', - 'SCP80 counters saved to the preset.': 'Счётчики SCP80 сохранены в предустановке.', + 'Keyset 00 means "no security" and is not a preset keyset': 'keyset 00 означает «без защиты» и не является keyset пресета', + 'Duplicate keyset number in the keysets': 'Повторяющийся номер keyset', + 'Select a card preset - its keysets define the keyset numbers': 'Выберите пресет карты — его keysets задают номера keyset', + 'SCP80 counters saved to the preset.': 'Счётчики SCP80 сохранены в пресете.', 'counter not tracked (SPI1 has no counter check)': 'счётчик не отслеживается (в SPI1 нет проверки счётчика)', 'TAR probe': 'TAR-сканер', - 'Sends one secured packet per selected TAR (TS 101 220 Annex D, Table D.1) carrying a harmless command, and classifies the card\'s answer - which OTA applications it has registered. Every packet the card accepts consumes a counter, which is saved to the preset.': 'Отправляет по одному защищённому пакету на каждый выбранный TAR (TS 101 220 Annex D, Table D.1) с безобидной командой и классифицирует ответ карты — какие OTA-приложения на ней зарегистрированы. Каждый принятый картой пакет расходует счётчик, который сохраняется в предустановке.', + 'Sends one secured packet per selected TAR (TS 101 220 Annex D, Table D.1) carrying a harmless command, and classifies the card\'s answer - which OTA applications it has registered. Every packet the card accepts consumes a counter, which is saved to the preset.': 'Отправляет по одному защищённому пакету на каждый выбранный TAR (TS 101 220 Annex D, Table D.1) с безобидной командой и классифицирует ответ карты — какие OTA-приложения на ней зарегистрированы. Каждый принятый картой пакет расходует счётчик, который сохраняется в пресете.', 'TARs to probe — TS 101 220 V18.3.0 Annex D, Table D.1': 'TAR для пробы — TS 101 220 V18.3.0 Annex D, Table D.1', 'Select all': 'Выбрать все', 'Select none': 'Снять все', @@ -19803,7 +19802,7 @@ const LANG_RU = { 'Probe TARs (registered OTA applications)': 'Проба TAR (зарегистрированные OTA-приложения)', 'Probe APDU (hex)': 'APDU пробы (hex)', 'TARs to probe (hex, one per line; empty = all standard TARs of TS 101 220 Annex D)': 'TAR для пробы (hex, по одному в строке; пусто = все стандартные TAR из TS 101 220 Annex D)', - 'One secured packet per TAR: each packet the card accepts consumes a counter, which is saved to the preset.': 'Один защищённый пакет на каждый TAR: каждый принятый картой пакет расходует счётчик, который сохраняется в предустановке.', + 'One secured packet per TAR: each packet the card accepts consumes a counter, which is saved to the preset.': 'Один защищённый пакет на каждый TAR: каждый принятый картой пакет расходует счётчик, который сохраняется в пресете.', 'Specify the probe APDU': 'Укажите APDU пробы', 'Registered applications:': 'Зарегистрированные приложения:', 'Application': 'Приложение', @@ -19815,14 +19814,14 @@ const LANG_RU = { 'accepted, no PoR': 'принято, нет PoR', 'No card presets defined — the SCP80/RAM views have no keys for the equipped card. Add one in the Cards tab.': 'Предустановки карт не заданы — в представлениях SCP80/RAM нет ключей для подключённой карты. Добавьте её на вкладке «Карты».', 'The equipped card': 'Подключённая карта', - 'does not match any card preset — the selected preset may belong to another card. Add it in the Cards tab (From card).': 'не совпадает ни с одной предустановкой — выбранная предустановка может принадлежать другой карте. Добавьте её на вкладке «Карты» (кнопка «С карты»).', - "The selected preset is not the equipped card's": 'Выбранная предустановка не принадлежит подключённой карте', + 'does not match any card preset — the selected preset may belong to another card. Add it in the Cards tab (From card).': 'не совпадает ни с одним пресетом — выбранный пресет может принадлежать другой карте. Добавьте его на вкладке «Карты» (кнопка «С карты»).', + "The selected preset is not the equipped card's": 'Выбранный пресет не принадлежит подключённой карте', 'check that its keys and counters belong to this card.': 'проверьте, что её ключи и счётчики относятся к этой карте.', - 'Key version (kvn, optional)': 'Версия ключа (kvn, опционально)', - "empty = the preset's first keyset": 'пусто = первый набор ключей предустановки', - 'Keysets (key version: KIc/KID — counter)': 'Наборы ключей (версия: KIc/KID — счётчик)', - 'KIc and KID must carry the same key version (their high nibble) in the range 01-0F — TS 102 225 A.2; each key version has its own counter on the card (Annex A.1).': 'KIc и KID должны содержать одну и ту же версию ключа (старший ниббл) в диапазоне 01-0F — TS 102 225 A.2; у каждой версии ключа свой счётчик на карте (Annex A.1).', - 'Card presets are stored on the server. JSON export/import buttons are below the card list; a preset carries one keyset per key version, each with its own counter.': 'Предустановки карт хранятся на сервере. Кнопки экспорта/импорта JSON — под списком карт; предустановка содержит один набор ключей на версию, у каждой свой счётчик.', + 'Keyset number (optional)': 'Номер keyset (опционально)', + "empty = the preset's first keyset": 'пусто = первый keyset пресета', + 'Keysets (number: KIc/KID — counter)': 'Keysets (номер: KIc/KID — счётчик)', + 'KIc and KID must carry the same keyset number (their high nibble) in the range 01-0F — TS 102 225 A.2; each keyset has its own counter on the card (Annex A.1).': 'KIc и KID должны содержать один и тот же номер keyset (старший ниббл) в диапазоне 01-0F — TS 102 225 A.2; у каждого keyset свой счётчик на карте (Annex A.1).', + '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.': 'Пресеты карт хранятся на сервере. Кнопки экспорта/импорта JSON — под списком карт; пресет содержит несколько keysets, у каждого свой номер и счётчик.', 'Save error': 'Ошибка сохранения', 'Delete error': 'Ошибка удаления', 'Counter not saved:': 'Счётчик не сохранён:', @@ -19883,9 +19882,9 @@ const LANG_RU = { 'Show link events (Channel status ENVELOPEs, TS 102 223 7.5.11)': 'Показывать события канала (ENVELOPE Channel status, TS 102 223 7.5.11)', 'ADM verified': 'ADM подтверждён', 'ADM not verified': 'ADM не подтверждён', - 'ADM key in the card preset — verified': 'Ключ ADM в предустановке — подтверждён', - 'ADM key in the card preset — not verified': 'Ключ ADM в предустановке — не подтверждён', - 'ADM key in the card preset — click to verify': 'Ключ ADM в предустановке — нажмите для проверки', + 'ADM key in the card preset — verified': 'Ключ ADM в пресете — подтверждён', + 'ADM key in the card preset — not verified': 'Ключ ADM в пресете — не подтверждён', + 'ADM key in the card preset — click to verify': 'Ключ ADM в пресете — нажмите для проверки', 'Verify ADM': 'Проверить ADM', 'Verifying ADM...': 'Проверка ADM...', 'ADM verification failed': 'Проверка ADM не удалась', @@ -19895,8 +19894,8 @@ const LANG_RU = { 'This is the last attempt before the ADM is blocked. Try again?': 'Это последняя попытка — неверный ключ заблокирует ADM. Попробовать снова?', 'ADM is blocked': 'ADM заблокирован', 'ADM is blocked — unblock the card to try again': 'ADM заблокирован — разблокируйте карту, чтобы повторить', - 'Verified — no ADM key in the card preset': 'Подтверждён — ключа ADM в предустановке нет', - 'No ADM key in the card preset — not verified': 'Ключа ADM в предустановке нет — не подтверждён', + 'Verified — no ADM key in the card preset': 'Подтверждён — ключа ADM в пресете нет', + 'No ADM key in the card preset — not verified': 'Ключа ADM в пресете нет — не подтверждён', 'SCP80 preset complete': 'Предустановка SCP80 заполнена', 'SCP81 preset complete': 'Предустановка SCP81 заполнена', 'Network state': 'Сетевое состояние', diff --git a/frontend/sw.js b/frontend/sw.js index 41f0d13..dc994d2 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v339'; +const CACHE = 'simple-v340'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/keysets.test.js b/frontend/tests/keysets.test.js index b561dfe..b29b88f 100644 --- a/frontend/tests/keysets.test.js +++ b/frontend/tests/keysets.test.js @@ -38,13 +38,13 @@ const PRESET = { ], }; -test('spKeysetKvnOf reads the key version from the high nibble', () => { +test('spKeysetKvnOf reads the keyset number from the high nibble', () => { assert.strictEqual(spKeysetKvnOf('15'), 1); assert.strictEqual(spKeysetKvnOf('29'), 2); assert.strictEqual(spKeysetKvnOf('3A'), 3); assert.strictEqual(spKeysetKvnOf('05'), 0); assert.strictEqual(spKeysetKvnOf('ff'), 15); - // not a byte: no key version + // not a byte: no keyset number assert.strictEqual(spKeysetKvnOf(''), null); assert.strictEqual(spKeysetKvnOf('5'), null); assert.strictEqual(spKeysetKvnOf('ZZ'), null); @@ -60,15 +60,15 @@ test('spKeysetList converts a v3.8.0 flat preset into one keyset', () => { assert.deepStrictEqual(spKeysetList({}), []); }); -test('spKeysetFor finds a keyset by key version', () => { +test('spKeysetFor finds a keyset by number', () => { assert.strictEqual(spKeysetFor(PRESET, 1).cntr, '0000000001'); assert.strictEqual(spKeysetFor(PRESET, 2).cntr, '0000000005'); assert.strictEqual(spKeysetFor(PRESET, 3), null); assert.strictEqual(spKeysetFor(null, 1), null); }); -test('spKeysetCheck enforces the key-version rules', () => { - // a defined key version passes +test('spKeysetCheck enforces the keyset-number rules', () => { + // a defined keyset number passes assert.strictEqual(spKeysetCheck(PRESET, '15', '15'), ''); assert.strictEqual(spKeysetCheck(PRESET, '29', '29'), ''); // '00'/'00' means no security: no keyset needed (TS 102 225 A.2) @@ -76,8 +76,8 @@ test('spKeysetCheck enforces the key-version rules', () => { // ... and one zero byte with a defined version passes too assert.strictEqual(spKeysetCheck(PRESET, '15', '00'), ''); // KIc/KID versions must agree when both are non-zero (A.2) - assert.match(spKeysetCheck(PRESET, '15', '25'), /same key version/); - // the key version must be defined in the preset + assert.match(spKeysetCheck(PRESET, '15', '25'), /same keyset number/); + // the keyset number must be defined in the preset assert.match(spKeysetCheck(PRESET, '35', '35'), /3 .*not defined/); // one zero byte: the other byte's version applies (keyset 2 is defined) assert.strictEqual(spKeysetCheck(PRESET, '00', '25'), ''); @@ -90,7 +90,7 @@ test('spKeysetCheck enforces the key-version rules', () => { assert.match(spKeysetCheck(PRESET, '15', 'ZZ'), /KID/); }); -test('spKeysetOptionsHtml lists the keysets and marks an undefined version', () => { +test('spKeysetOptionsHtml lists the keysets and marks an undefined number', () => { const html1 = spKeysetOptionsHtml(PRESET, '15', '15'); assert.match(html1, /value="1" selected/); assert.match(html1, /value="2"/); @@ -102,7 +102,7 @@ test('spKeysetOptionsHtml lists the keysets and marks an undefined version', () // a preset without keysets: the form's version shows as undefined const html3 = spKeysetOptionsHtml({ keysets: [] }, '15', '15'); assert.match(html3, /not defined in the preset/); - // no key version in the form and no keysets at all + // no keyset number in the form and no keysets at all assert.match(spKeysetOptionsHtml({}, '', ''), /no keyset/); }); diff --git a/pysim_simple_server/presets.py b/pysim_simple_server/presets.py index 79f0afb..815e8ca 100644 --- a/pysim_simple_server/presets.py +++ b/pysim_simple_server/presets.py @@ -9,13 +9,13 @@ owns the store: an accepted counter is persisted in the same operation, and a closed tab, a lost response or a second browser window can no longer lose an increment. -A preset holds one **keyset per GlobalPlatform key version** (the b8..b5 nibble -of KIc/KID, TS 102 225 5.1.2/A.2), each with its own KIc/KID keys and its own +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" -(TS 102 225 Annex A.1). The two bytes of a keyset must carry the same key -version (A.2: the card rejects a mismatch with "Unidentified security error"); -key version '00' is reserved and not a keyset (it means "no security" in a -packet, which is a form-level choice, not a preset). +(TS 102 225 Annex A.1, the spec's wording). The two bytes of a keyset must +carry the same number (A.2: the card rejects a mismatch with "Unidentified +security error"); number '00' is reserved and not a keyset (it means "no +security" in a packet, which is a form-level choice, not a preset). Layout (``--card-presets`` overrides the file; ``Path.home()`` resolves the same way on Linux, macOS and Windows):: @@ -50,9 +50,9 @@ SCHEMA_VERSION = 2 # The fields a preset carries: the localStorage shape plus the id. Defaults # mirror the Cards form (spi1/spi2 and the per-target TARs). The OTA key -# material lives in `keysets` - one entry per GlobalPlatform key version (the -# b8..b5 nibble of KIc/KID), each with its own counter (TS 102 225 Annex A.1: -# "a dedicated counter shall be associated to each key version"). +# 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"). PRESET_FIELDS = ('name', 'iccid', 'adm', 'spi1', 'spi2', 'tar', 'uiccTar', 'usimTar', 'pskIdentity', 'pskKey') # The v3.8.0 flat key fields: converted into a single keyset on load/import. @@ -63,7 +63,7 @@ KEYSET_HEX_FIELDS = ('kic', 'kid', 'kicKey', 'kidKey', 'cntr') TAR_DEFAULTS = {'tar': '000000', 'uiccTar': 'B00000', 'usimTar': 'B00001'} COUNTER_BITS = 40 # the SCP80 counter is 5 bytes (TS 31.115) COUNTER_WIDTH = 10 # ... shown as 10 hex digits -KVN_MAX = 0x0F # key version '00' is reserved: no key set, no counter +KVN_MAX = 0x0F # keyset number '00' is reserved: no key set, no counter def default_path(): @@ -72,9 +72,9 @@ def default_path(): def keyset_kvn(keyset): - """The key version of a keyset: the high nibble of KIc (bits b8..b5 of - KIc/KID, TS 102 225 5.1.2/A.2 - both bytes must agree, so KIc is enough). - None when the byte is unusable.""" + """The keyset number: the high nibble of KIc (bits b8..b5 of KIc/KID, + TS 102 225 5.1.2/A.2 - both bytes must agree, so KIc is enough). None + when the byte is unusable.""" kic = str((keyset or {}).get('kic') or '').strip().upper() if not re.fullmatch(r'[0-9A-F]{2}', kic): return None @@ -210,7 +210,7 @@ class PresetStore: pass def _audit(self, pid, name, old, new, source, kvn=None): - """One JSONL line per counter change (the key version included - each + """One JSONL line per counter change (the keyset number included - each keyset has its own counter): the forensic trail for "did an increment get lost?" questions.""" try: @@ -279,7 +279,7 @@ class PresetStore: self._validate_keyset(ks) kvn = keyset_kvn(ks) if kvn in seen: - raise PresetError('duplicate key version %02X in this preset' % kvn) + raise PresetError('duplicate keyset number %02X in this preset' % kvn) seen.add(kvn) norm = normalize_iccid(p['iccid']) if norm: @@ -292,20 +292,20 @@ class PresetStore: return p def _validate_keyset(self, ks): - """One keyset: KIc/KID carry the key version in b8..b5 and must agree - (TS 102 225 A.2 - a mismatch is rejected by the card), the version is + """One keyset: KIc/KID carry its number in b8..b5 and must agree + (TS 102 225 A.2 - a mismatch is rejected by the card), the number is 01-0F ('00' means "no security" and has no keys to define, so it is not a preset keyset), both keys are present and the counter is - 1-10 hex digits (the dedicated counter of that key version, A.1).""" + 1-10 hex digits (the dedicated counter of that keyset, A.1).""" for key in ('kic', 'kid'): if not re.fullmatch(r'[0-9A-F]{2}', ks[key]): raise PresetError('%s must be one hex byte' % key) kic_v, kid_v = int(ks['kic'], 16) >> 4, int(ks['kid'], 16) >> 4 if kic_v != kid_v: - raise PresetError('KIc and KID must carry the same key version ' + raise PresetError('KIc and KID must carry the same keyset number ' '(TS 102 225 A.2): %s / %s' % (ks['kic'], ks['kid'])) if not 1 <= kic_v <= KVN_MAX: - raise PresetError('key version must be 01-0F: %s' % ks['kic']) + raise PresetError('keyset number must be 01-0F: %s' % ks['kic']) for key in ('kicKey', 'kidKey'): if not ks[key]: raise PresetError('%s is required' % key) @@ -397,8 +397,8 @@ class PresetStore: return True def find_keyset(self, pid, kvn): - """The keyset of a preset for a key version (the b8..b5 nibble of - KIc/KID), or None when the preset or the key version is unknown.""" + """The keyset of a preset with the given number (the b8..b5 nibble of + KIc/KID), or None when the preset or the number is unknown.""" with self._lock: p = self._find(pid) if p is None: @@ -409,9 +409,9 @@ class PresetStore: return None def set_counter(self, pid, cntr, source='operation', kvn=None): - """Persist the counter an operation accepted, into the keyset of the - key version it used (each key version has its own dedicated counter, - TS 102 225 A.1). Monotonic per keyset: the counter only moves + """Persist the counter an operation accepted, into the keyset it used + (each keyset has its own dedicated counter, TS 102 225 A.1). + Monotonic per keyset: the counter only moves forward, so a stale caller can never regress it. `kvn=None` picks the preset's only keyset (a convenience for single-keyset callers). Returns the stored preset (or None when the id is unknown).""" @@ -426,13 +426,13 @@ class PresetStore: ks = cand break if ks is None: - sys.stderr.write('PRESETS: no key version %s in %s (%s)\n' + sys.stderr.write('PRESETS: no keyset %s in %s (%s)\n' % (kvn, p['name'], source)) return copy.deepcopy(p) elif len(p['keysets']) == 1: ks = p['keysets'][0] else: - sys.stderr.write('PRESETS: %s has several key versions - the ' + sys.stderr.write('PRESETS: %s has several keysets - the ' 'counter needs one (%s)\n' % (p['name'], source)) return copy.deepcopy(p) new = str(cntr or '').strip().upper() diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index ad28755..9ca9e10 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -1447,7 +1447,7 @@ def _counter_valid(cntr): def _kvn_of(kic_hex, kid_hex): - """The key version a packet uses: the b8..b5 nibble of KIc/KID (TS 102 225 + """The keyset number a packet uses: the b8..b5 nibble of KIc/KID (TS 102 225 5.1.2/A.2). Returns ``(kvn, error)``: kvn is 0 when neither byte carries one ('00' = no security, legal per A.2), error names a mismatch - A.2: the versions shall be identical when different from 0, else the card rejects @@ -1460,15 +1460,15 @@ def _kvn_of(kic_hex, kid_hex): kic_v = int(kic, 16) >> 4 if kic else 0 kid_v = int(kid, 16) >> 4 if kid else 0 if kic_v and kid_v and kic_v != kid_v: - return None, ('KIc and KID must carry the same key version ' + return None, ('KIc and KID must carry the same keyset number ' '(TS 102 225 A.2): %s / %s' % (kic, kid)) return (kic_v or kid_v), None def _preset_keyset_check(server, preset_id, kic, kid): - """The "the key version must be defined in the preset" rule: an error - string when the packet's (non-zero) key version has no keyset in the named - preset, else None. Without a preset_id (a raw API caller) there is + """The "the keyset number must be defined in the preset" rule: an error + string when the packet's (non-zero) keyset number has no keyset in the + named preset, else None. Without a preset_id (a raw API caller) there is nothing to check; '00'/'00' (no security) needs no keyset either.""" if not preset_id: return None @@ -1482,19 +1482,19 @@ def _preset_keyset_check(server, preset_id, kic, kid): return None if store.find_keyset(preset_id, kvn) is None: preset = store.get(preset_id) or {} - return ('key version %d is not defined in preset %s - add it in the ' + return ('keyset %d is not defined in preset %s - add it in the ' 'Cards tab' % (kvn, preset.get('name') or preset_id)) return None def _preset_counter_persist(server, preset_id, cntr, source, kvn=None): """Persist the counter an operation accepted into the server-side preset - store (v3.8.0), into the keyset of the key version it used (each key - version has its own dedicated counter, TS 102 225 A.1). The counter is + store (v3.8.0), into the keyset it used (each keyset has its own dedicated + counter, TS 102 225 A.1). The counter is card state, so the server writes it with the operation that consumed it - a closed tab, a lost response or a second browser window can no longer lose an increment. The store itself is monotonic (a stale value never regresses - it). A key version is mandatory: a packet with no key version (KIc/KID + it). A keyset number is mandatory: a packet with no number (KIc/KID '00', or a SPI1 without a counter check) has no counter to write. Best effort: a store failure must never fail the card operation.""" if not preset_id or not cntr or not kvn: @@ -5313,9 +5313,9 @@ def _test_preset_error(script, preset): want = int(p['kvn']) ks = next((k for k in keysets if presets.keyset_kvn(k) == want), None) if ks is None: - return 'step %d: key version %d is not defined in the preset' % (i + 1, want) + return 'step %d: keyset %d is not defined in the preset' % (i + 1, want) if not _keyset_complete(ks): - return ('step %d: key version %d is incomplete (KIc, KID, both ' + return ('step %d: keyset %d is incomplete (KIc, KID, both ' 'keys and a counter)' % (i + 1, want)) if not (p.get('tar') or preset.get('tar')): return 'step %d: no TAR (neither in the step nor in the preset)' % (i + 1) @@ -5445,16 +5445,16 @@ def _test_run_scp80(server, step, ctx): scc = server.scc preset = ctx['preset'] p = step['params'] - # The keyset: a step may name a key version (`kvn`), otherwise the preset's - # first keyset is used (a preset carries one keyset per key version, each - # with its own keys and counter - TS 102 225 Annex A.1). + # The keyset: a step may name a keyset number (`kvn`), otherwise the + # preset's first keyset is used (each keyset has its own keys and counter - + # TS 102 225 Annex A.1). keysets = _preset_keysets(preset) keyset = None if p.get('kvn') is not None: want = int(p['kvn']) keyset = next((ks for ks in keysets if presets.keyset_kvn(ks) == want), None) if keyset is None: - raise testscript.ScriptError('key version %d is not defined in the ' + raise testscript.ScriptError('keyset %d is not defined in the ' 'selected card preset' % want) elif keysets: keyset = keysets[0] @@ -5823,11 +5823,11 @@ class PysimHandler(BaseHTTPRequestHandler): self._log_resp(resp) def _keyset_guard(self, body, kic, kid): - """The key-version checks shared by the RAM operations: the KIc/KID - versions must agree (TS 102 225 A.2 - the card rejects a mismatch) and a - named preset must define the key version ("the keyset must exist"). - Returns ``(kvn, error)``: kvn is None when the packet carries no key - version ('00'/'00' = no security, legal per A.2), error an error string + """The keyset-number checks shared by the RAM operations: the KIc/KID + numbers must agree (TS 102 225 A.2 - the card rejects a mismatch) and a + named preset must define the number ("the keyset must exist"). + Returns ``(kvn, error)``: kvn is None when the packet carries no number + ('00'/'00' = no security, legal per A.2), error an error string the caller answers with 400.""" kvn, err = _kvn_of(kic, kid) if err: @@ -6914,7 +6914,7 @@ class PysimHandler(BaseHTTPRequestHandler): self._send_json({'error': _err('reader_not_init', lang)}, 503) self._log_resp({'error': _err('reader_not_init', lang)}) return - # The packet's key version (b8..b5 of KIc/KID, TS 102 225 A.2) must + # The packet's keyset number (b8..b5 of KIc/KID, TS 102 225 A.2) must # be consistent - a mismatch is invalid input (the card rejects it # with "Unidentified security error") - and, when a preset is named, # defined in it. Both are refused before anything is built (and @@ -7160,7 +7160,7 @@ class PysimHandler(BaseHTTPRequestHandler): make_selectable = body.get('make_selectable', True) privileges_hex = body.get('privileges', '').replace(' ', '') or '00' - # The key version must be consistent and defined in the preset + # The keyset number must be consistent and defined in the preset # (TS 102 225 A.2 / the "keyset must exist" rule) - checked # before the format probe consumes a counter. kvn, keyset_err = self._keyset_guard(body, kic, kid) @@ -7330,7 +7330,7 @@ class PysimHandler(BaseHTTPRequestHandler): 'tar': body.get('tar', '000000'), 'cntr': body.get('cntr', '00000000'), 'kic_key': body.get('kicKey', ''), 'kid_key': body.get('kidKey', ''), 'include_cpi': body.get('includeCpi', True)} - # The key version must be consistent and defined in the preset + # The keyset number must be consistent and defined in the preset # (TS 102 225 A.2 / the "keyset must exist" rule) - checked # before the format probe consumes a counter. kvn, keyset_err = self._keyset_guard(body, sp_state['kic'], sp_state['kid']) diff --git a/tests/test_fetch_routes.py b/tests/test_fetch_routes.py index 0262202..b74f6b1 100644 --- a/tests/test_fetch_routes.py +++ b/tests/test_fetch_routes.py @@ -613,7 +613,7 @@ class PresetStoreHttpTests(unittest.TestCase): 'sp': '00', 'spi1': '16', 'spi2': '01', 'kic': '15', 'kid': '25', 'tar': '000000', 'cntr': '0000000001'}) self.assertEqual(status, 400, resp) - self.assertIn('same key version', resp['error']) + self.assertIn('same keyset number', resp['error']) def test_send_ota_refuses_an_undefined_key_version(self): p = self.store.add(self._preset(iccid='8970119000004600098')) @@ -622,7 +622,7 @@ class PresetStoreHttpTests(unittest.TestCase): 'sp': '00', 'spi1': '16', 'spi2': '01', 'kic': '35', 'kid': '35', 'tar': '000000', 'cntr': '0000000001', 'preset_id': p['id']}) self.assertEqual(status, 400, resp) - self.assertIn('key version 3 is not defined', resp['error']) + self.assertIn('keyset 3 is not defined', resp['error']) def test_the_counter_lands_in_the_keyset_of_the_packet(self): p = self.store.add(self._preset(keysets=[ @@ -659,7 +659,7 @@ class PresetStoreHttpTests(unittest.TestCase): status, resp = self._post('/api/ram-install', { 'cap_hex': _mini_cap_hex(), 'kic': '35', 'kid': '35', 'preset_id': p['id']}) self.assertEqual(status, 400, resp) - self.assertIn('key version 3 is not defined', resp['error']) + self.assertIn('keyset 3 is not defined', resp['error']) def test_a_keyless_send_leaves_every_counter_untouched(self): # SPI1 00: the counter is "present, ignored, never updated" diff --git a/tests/test_ota_helpers.py b/tests/test_ota_helpers.py index cc7e46c..f8cff93 100644 --- a/tests/test_ota_helpers.py +++ b/tests/test_ota_helpers.py @@ -2116,7 +2116,7 @@ class NoSecurityAndCounterTrackingTests(unittest.TestCase): server = types.SimpleNamespace(card_presets=store) S._preset_counter_persist(server, 'pid', '0000000002', 'send-ota', None) S._preset_counter_persist(server, 'pid', '0000000002', 'send-ota', 0) - self.assertFalse(store.set_counter.called, 'no key version -> no write') + self.assertFalse(store.set_counter.called, 'no keyset number -> no write') S._preset_counter_persist(server, 'pid', '0000000002', 'send-ota', 2) store.set_counter.assert_called_once_with('pid', '0000000002', 'send-ota', 2) diff --git a/tests/test_presets.py b/tests/test_presets.py index 679f285..3aa0db6 100644 --- a/tests/test_presets.py +++ b/tests/test_presets.py @@ -3,7 +3,7 @@ The store replaces the localStorage presets; the ICCID normalisation must stay byte-compatible with the PWA's `cardsNormIccid` (digits / spaced / raw nibble-swapped EF hex), every keyset carries its own monotonic counter (one per -key version, TS 102 225 Annex A.1), and the file must survive a corrupt store +keyset number, TS 102 225 Annex A.1), and the file must survive a corrupt store without silently destroying it. """ @@ -122,7 +122,7 @@ class StoreCrudTests(unittest.TestCase): # TS 102 225 A.2: a mismatch is rejected by the card - refuse it here with self.assertRaises(presets.PresetError) as ctx: self.store.add(preset(keysets=[keyset(kic='15', kid='25')])) - self.assertIn('same key version', str(ctx.exception)) + self.assertIn('same keyset number', str(ctx.exception)) def test_key_version_00_and_duplicates_are_refused(self): with self.assertRaises(presets.PresetError) as ctx: @@ -131,7 +131,7 @@ class StoreCrudTests(unittest.TestCase): with self.assertRaises(presets.PresetError) as ctx: self.store.add(preset(keysets=[keyset(kic='15', kid='15'), keyset(kic='15', kid='15')])) - self.assertIn('duplicate key version', str(ctx.exception)) + self.assertIn('duplicate keyset number', str(ctx.exception)) # the byte must be a full hex byte with self.assertRaises(presets.PresetError): self.store.add(preset(keysets=[keyset(kic='1', kid='1')])) @@ -237,7 +237,7 @@ class StoreCounterTests(unittest.TestCase): self.assertEqual(self.counter(), '0000000005') def test_each_key_version_has_its_own_counter(self): - # TS 102 225 A.1: a dedicated counter per key version + # TS 102 225 A.1: a dedicated counter per keyset p = self.store.update(self.p['id'], {'keysets': [ keyset(kic='15', kid='15', cntr='0000000002'), keyset(kic='29', kid='29', cntr='0000000007')]})
    FieldDescription
    ISD TARIssuer Security Domain TAR (TS 101 220 Annex D); used for RAM/GP operations (SCP80 RAM view, RAM/GP chains); spec default 000000, prefilled when empty
    UICC RFM TARUICC 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 B00000
    ADF RFM TARADF 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 B00001
    CounterReplay counter (5 bytes); advanced and saved after each successful SCP80 send
    CounterReplay counter (5 bytes); one per keyset, advanced and saved by the server after each operation that consumes it
    KIc key / KID key16/24/32 hex chars (8/16/24-byte 3DES) or 32/48/64 hex chars (16/24/32-byte AES) keys
    PSK identitySCP81 HTTP OTA: the identity the card sends in the TLS handshake (e.g. 89390100000129506903)
    PSK keySCP81 HTTP OTA: 32 hex chars (16 bytes) for the PSK TLS listener, picked by the identity the card presents
    Name ICCIDKeysets (key version: KIc/KID \u2014 counter)Keysets (number: KIc/KID \u2014 counter) SPI1 SPI2 TAR (ISD/UICC RFM/ADF RFM)