From b023719555dd6ced0a3f989549e35ddb73f4ebb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Wed, 14 Oct 2026 18:17:59 +0300 Subject: [PATCH] v3.28.3: the outfile ADM byte-pair rule The review of v3.28.2 found the parser accepting an odd-length ADM (e.g. 12345), while /api/verify-adm requires byte pairs ((?:[0-9A-F]{2}){2,8}) - the stored value could never verify and was refused with a misleading "adm must be 4-16 hex digits". The parser now applies the verifier's exact rule: an odd length (a half byte) or a value over 16 digits is dropped at import time with "ADM is not 4-16 hex digits in byte pairs". Docs: help EN/RU, api.md and the repo AGENTS say "4-16 hex digits in byte pairs". Tests: the odd-length and the over-16 drop cases (both real samples are even-length and unaffected: BAP 3632333239463337, AM0027 CFA3DD9C6B7F0F8C). 915 frontend + 925 Python; sw cache simple-v441. --- docs/api.md | 4 ++-- frontend/help-ru.html | 2 +- frontend/help.html | 2 +- frontend/index.html | 2 +- frontend/sw.js | 2 +- pyproject.toml | 2 +- pysim_simple_server/outfile.py | 14 +++++++++----- pysim_simple_server/server.py | 2 +- tests/test_outfile.py | 16 ++++++++++++++++ 9 files changed, 33 insertions(+), 13 deletions(-) diff --git a/docs/api.md b/docs/api.md index 9398532..39673c9 100644 --- a/docs/api.md +++ b/docs/api.md @@ -345,8 +345,8 @@ Recognised columns (case-insensitive): `ICCID` (the digits + odd-nibble F pad, the plain digits, spaced digits or the raw nibble-swapped EF hex all normalise to the same value), `PIN1`/`PIN2`, `PUK1`/`PUK2` (4–8 digits; an invalid value is dropped and reported, the card is kept), `ADM` (the key's -**hex value**, 4–16 hex digits — the length varies; this is the form the preset -store and `/api/verify-adm` use), `PSK` (the SCP81 key, +**hex value**, 4–16 hex digits in byte pairs — the rule `/api/verify-adm` +applies), `PSK` (the SCP81 key, exactly 32 hex chars) and `KICn`/`KIDn` — one keyset per numbered pair, up to 15. The **KIc/KID bytes are never in the file**: the keyset number is the column number (`KIC2` → keyset 2) and the algorithm nibble follows the key diff --git a/frontend/help-ru.html b/frontend/help-ru.html index 1ba4400..e128fd9 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -369,7 +369,7 @@

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

-

Импорт outfile загружает сразу много карт из outfile поставщика карт — текстового дампа инструмента персонализации: шапка с баннером и параметрами, затем разделённая пробелами строка колонок (среди токенов есть ICCID; первый токен может нести префикс вида var_out:) и по одной строке на карту. Всё до строки колонок и все неиспользуемые колонки игнорируются. Разбираются ICCID (цифры, цифры с добивочным F до чётного числа нибблов, цифры с разделителями или сырой hex EF — всё приводится к одной форме), PIN1/PIN2/PUK1/PUK2, ADM (hex-значение ключа, 4–16 hex-цифр — длина различается, например 3632333239463337), PSK (16 байт; необязательная колонка PSKID/PSK_ID задаёт её идентификатор, по умолчанию берётся ICCID карты) и нумерованные пары ключей KICn/KIDn — по одному keyset на пару, до 15. Байтов KIc/KID в файле нет: номер keyset — это номер колонки (KIC2 → keyset 2), а ниббл алгоритма определяется размером ключа — 8 байт DES, 24 байта 3DES3, а для 16 байт — выбором в предпросмотре: 3DES2 (по умолчанию) или AES; любой другой размер пропускает этот keyset с пометкой. TAR также не берутся из файла: предпросмотр показывает таблицу TAR, которую получат импортируемые карты (по умолчанию три ролевые записи с TAR по спецификации и MSL 16; их можно править, кнопка + TAR добавляет записи), и один keyset по умолчанию, применяемый ко всем записям TAR — он обязателен для карты с несколькими keysets (ненулевой MSL должен называть keyset). Общее имя применяется ко всем импортируемым картам (пустое — берётся ICCID); режим импорта выбирает Дополнить (по умолчанию — существующие пресеты сохраняются, дубли ICCID пропускаются и перечисляются) или Заменить (хранилище сначала очищается). Предпросмотр перечисляет разобранные карты (ICCID, keysets, найденные коды) и каждую пропущенную строку/ключ с причиной — до того, как что-либо сохранено.

+

Импорт outfile загружает сразу много карт из outfile поставщика карт — текстового дампа инструмента персонализации: шапка с баннером и параметрами, затем разделённая пробелами строка колонок (среди токенов есть ICCID; первый токен может нести префикс вида var_out:) и по одной строке на карту. Всё до строки колонок и все неиспользуемые колонки игнорируются. Разбираются ICCID (цифры, цифры с добивочным F до чётного числа нибблов, цифры с разделителями или сырой hex EF — всё приводится к одной форме), PIN1/PIN2/PUK1/PUK2, ADM (hex-значение ключа, 4–16 hex-цифр парами байт — например 3632333239463337), PSK (16 байт; необязательная колонка PSKID/PSK_ID задаёт её идентификатор, по умолчанию берётся ICCID карты) и нумерованные пары ключей KICn/KIDn — по одному keyset на пару, до 15. Байтов KIc/KID в файле нет: номер keyset — это номер колонки (KIC2 → keyset 2), а ниббл алгоритма определяется размером ключа — 8 байт DES, 24 байта 3DES3, а для 16 байт — выбором в предпросмотре: 3DES2 (по умолчанию) или AES; любой другой размер пропускает этот keyset с пометкой. TAR также не берутся из файла: предпросмотр показывает таблицу TAR, которую получат импортируемые карты (по умолчанию три ролевые записи с TAR по спецификации и MSL 16; их можно править, кнопка + TAR добавляет записи), и один keyset по умолчанию, применяемый ко всем записям TAR — он обязателен для карты с несколькими keysets (ненулевой MSL должен называть keyset). Общее имя применяется ко всем импортируемым картам (пустое — берётся ICCID); режим импорта выбирает Дополнить (по умолчанию — существующие пресеты сохраняются, дубли ICCID пропускаются и перечисляются) или Заменить (хранилище сначала очищается). Предпросмотр перечисляет разобранные карты (ICCID, keysets, найденные коды) и каждую пропущенную строку/ключ с причиной — до того, как что-либо сохранено.

diff --git a/frontend/help.html b/frontend/help.html index 25b635d..d9ab06f 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -368,7 +368,7 @@

The SCP81 column shows whether the preset supplies a usable PSK pair: ✓ (green) — identity and key set; ⚠ (amber) — only one of the two, so the listener ignores the preset; — — no PSK. PSK identity and key must be set together. Add preset opens the editor with a fresh preset; Edit loads the row's preset into it so fields can be changed without re-entering the card; Save stores it and returns to the list, Cancel discards the edits; Remove deletes the row (list only). Presets can be shared with Export to file and restored with Import from file. 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 warning too); edits are pushed into a running SCP81 listener automatically.

-

Import outfile imports many cards at once from a card vendor's outfile — the personalization tool's plain-text dump: a banner and a configuration header, then a whitespace-separated column line (the tokens include ICCID; the first token may carry a var_out:-style prefix) and one row per card. Everything before the column line and every unused column is ignored. The parser reads ICCID (digits, the digits with an odd-nibble F pad, spaced digits or the raw EF hex — all normalised to one form), PIN1/PIN2/PUK1/PUK2, ADM (the key's hex value, 4–16 hex digits — the length varies, e.g. 3632333239463337), PSK (16 bytes; an optional PSKID/PSK_ID names its identity, defaulting to the card's ICCID) and the numbered key pairs KICn/KIDn — one keyset per pair, up to 15. The KIc/KID bytes are never in the file: the keyset number is the column number (KIC2 → keyset 2) and the algorithm nibble follows the key size — 8 bytes DES, 24 bytes 3DES3, and for 16 bytes the preview's choice, 3DES2 (default) or AES; any other size skips that keyset with a note. The TARs never come from the file either: the preview shows the TAR table the imported cards get (the three role entries with their spec TARs and MSL 16 by default, editable, with + TAR for extra entries) and one default keyset applied to every TAR entry — required for a card with several keysets (a non-zero MSL must name a keyset). A common name is applied to every imported card (empty falls back to the ICCID); Import mode chooses Merge (default — existing presets are kept, duplicate ICCIDs are skipped and reported) or Replace (the store is wiped first). The preview lists the parsed cards (ICCID, keysets, the codes found) and every skipped row/key with its reason before anything is stored.

+

Import outfile imports many cards at once from a card vendor's outfile — the personalization tool's plain-text dump: a banner and a configuration header, then a whitespace-separated column line (the tokens include ICCID; the first token may carry a var_out:-style prefix) and one row per card. Everything before the column line and every unused column is ignored. The parser reads ICCID (digits, the digits with an odd-nibble F pad, spaced digits or the raw EF hex — all normalised to one form), PIN1/PIN2/PUK1/PUK2, ADM (the key's hex value, 4–16 hex digits in byte pairs — e.g. 3632333239463337), PSK (16 bytes; an optional PSKID/PSK_ID names its identity, defaulting to the card's ICCID) and the numbered key pairs KICn/KIDn — one keyset per pair, up to 15. The KIc/KID bytes are never in the file: the keyset number is the column number (KIC2 → keyset 2) and the algorithm nibble follows the key size — 8 bytes DES, 24 bytes 3DES3, and for 16 bytes the preview's choice, 3DES2 (default) or AES; any other size skips that keyset with a note. The TARs never come from the file either: the preview shows the TAR table the imported cards get (the three role entries with their spec TARs and MSL 16 by default, editable, with + TAR for extra entries) and one default keyset applied to every TAR entry — required for a card with several keysets (a non-zero MSL must name a keyset). A common name is applied to every imported card (empty falls back to the ICCID); Import mode chooses Merge (default — existing presets are kept, duplicate ICCIDs are skipped and reported) or Replace (the store is wiped first). The preview lists the parsed cards (ICCID, keysets, the codes found) and every skipped row/key with its reason before anything is stored.

diff --git a/frontend/index.html b/frontend/index.html index 9c96cfe..3d214dd 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1910,7 +1910,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.28.2'; +const SIMPLE_VERSION = '3.28.3'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== view history (the browser Back/Forward buttons) ===== diff --git a/frontend/sw.js b/frontend/sw.js index aeaad97..a054505 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v440'; +const CACHE = 'simple-v441'; const URLS = [ 'index.html', 'help.html', diff --git a/pyproject.toml b/pyproject.toml index c99a4b6..eae9688 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.28.2" +version = "3.28.3" 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. diff --git a/pysim_simple_server/outfile.py b/pysim_simple_server/outfile.py index 9a12147..b126cc1 100644 --- a/pysim_simple_server/outfile.py +++ b/pysim_simple_server/outfile.py @@ -8,9 +8,9 @@ prefix) - and continues with one whitespace-separated row per card. The header and every column the presets do not use are ignored. Recognised columns (case-insensitive): ``ICCID``, ``PIN1``/``PIN2``, -``PUK1``/``PUK2``, ``ADM`` (the key's hex value - the length varies, e.g. 8 or -16 hex digits; this is the form the preset store and ``/api/verify-adm`` use, -where pySim pads it with 'f' to the 8 CHV bytes), ``PSK`` (the SCP81 key, 16 bytes; an +``PUK1``/``PUK2``, ``ADM`` (the key's hex value - 4-16 hex digits in **byte +pairs**, the rule ``/api/verify-adm`` applies; this is the form the preset store +uses, where pySim pads it with 'f' to the 8 CHV bytes), ``PSK`` (the SCP81 key, 16 bytes; an optional ``PSKID``/``PSK_ID`` column names its identity, defaulting to the card's ICCID) and ``KICn``/``KIDn`` - one keyset per numbered pair, up to 15. Everything else (``IMSI*``, ``EKI``, ``KIKn`` - the key-management key, no @@ -269,10 +269,14 @@ def parse_outfile(text, name='', algorithm16='3des2', tars=None, tar_keyset=None # The ADM column carries the key's hex value itself (the length # varies) - the form the preset store and /api/verify-adm use. adm = re.sub(r'[^0-9A-Fa-f]', '', str(toks[i])).upper() - if re.fullmatch(r'[0-9A-F]{4,16}', adm): + # Byte pairs, exactly like /api/verify-adm: an odd length is a half + # byte and the verifier would always refuse the stored value + # (v3.28.3). + if re.fullmatch(r'(?:[0-9A-F]{2}){2,8}', adm): entry['adm'] = adm else: - skip('line %d: ADM is not 4-16 hex digits - dropped' % row_no) + skip('line %d: ADM is not 4-16 hex digits in byte pairs - dropped' + % row_no) i = idx['PSK'] if 0 <= i < len(toks) and toks[i]: psk = _hex_value(toks[i]) diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index 6d68367..d65f738 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -37,7 +37,7 @@ from osmocom.tlv import BER_TLV_IE from osmocom.utils import rpad -VERSION = '3.28.2' +VERSION = '3.28.3' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE diff --git a/tests/test_outfile.py b/tests/test_outfile.py index bad2ae8..405b0ac 100644 --- a/tests/test_outfile.py +++ b/tests/test_outfile.py @@ -262,6 +262,22 @@ class OptionTests(unittest.TestCase): self.assertNotIn('adm', res['presets'][0]) self.assertTrue(any('ADM is not 4-16 hex digits' in s for s in res['report']['skipped'])) + # an odd length is a half byte: /api/verify-adm would always refuse the + # stored value, so it is dropped at import time (v3.28.3) + text = ('ICCID KIC1 KID1 ADM\n' + DIGITS_F + ' ' + K16A + ' ' + K16B + + ' 12345\n') + res = outfile.parse_outfile(text) + self.assertNotIn('adm', res['presets'][0]) + self.assertTrue(any('ADM is not 4-16 hex digits in byte pairs' in s + for s in res['report']['skipped'])) + # ...and so is a value over the 16-digit limit (the verifier pads to 8 + # CHV bytes, a longer value would overflow) + text = ('ICCID KIC1 KID1 ADM\n' + DIGITS_F + ' ' + K16A + ' ' + K16B + + ' ' + 'AB' * 9 + '\n') + res = outfile.parse_outfile(text) + self.assertNotIn('adm', res['presets'][0]) + self.assertTrue(any('ADM is not 4-16 hex digits in byte pairs' in s + for s in res['report']['skipped'])) # a present but invalid PIN is dropped, the card kept text = ('ICCID KIC1 KID1 PIN1\n' + DIGITS_F + ' ' + K16A + ' ' + K16B + ' 12\n') res = outfile.parse_outfile(text)