v3.28.2: the outfile ADM encoding fix

The ADM column carries the key's hex value (the length varies, 4-16 hex
digits) - the form the preset store and /api/verify-adm use (pySim pads
it with 'f' to the 8 CHV bytes).  The parser treated it as an ASCII
string hex-encoded and either mangled it (3632333239463337 -> 62329F37,
a wrong ADM that would fail VERIFY and consume an attempt) or dropped it
(CFA3DD9C6B7F0F8C - the live report with AM0027_alfa_dsa.out).

- The ADM is stored as-is: non-hex stripped, uppercased, [0-9A-F]{4,16};
  a non-hex or too-short value is dropped with "ADM is not 4-16 hex
  digits".
- _ascii_from_hex() is gone (its only user was the ADM column).
- Docs: help EN/RU, api.md and the repo AGENTS now say "the key's hex
  value, 4-16 hex digits (the length varies)".

Note: presets imported from a BAP-style file before this fix hold the
mangled ADM - re-import or correct them.

Tests: the BAP fixture (the hex as-is), the AM0027 shape, a
short/lowercase value and the drop cases.  Both real samples verified
(BAP 3632333239463337, AM0027 CFA3DD9C6B7F0F8C; the ALFA sample has no
ADM column and is unchanged).  915 frontend + 925 Python; sw cache
simple-v440.
This commit is contained in:
2026-10-14 18:07:04 +03:00
parent 91996a941b
commit 09d8a12add
9 changed files with 45 additions and 25 deletions
+3 -2
View File
@@ -344,8 +344,9 @@ into a preview.
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 ASCII
string, hex-encoded — `3632333239463337` → `62329F37`), `PSK` (the SCP81 key,
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,
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
+1 -1
View File
@@ -369,7 +369,7 @@
</tbody>
</table>
<p class="text-sm mb-3">Столбец <strong>SCP81</strong> показывает, задана ли в пресете рабочая пара PSK: <strong>&#10003;</strong> (зелёный) — идентификатор и ключ заданы; <strong>&#9888;</strong> (жёлтый) — задано только одно из двух, и слушатель такой пресет игнорирует; <strong>&mdash;</strong> — PSK нет. Идентификатор и ключ PSK задаются вместе. Кнопка <strong>Добавить пресет</strong> открывает редактор с новым пресетом; <strong>Редактировать</strong> загружает пресет строки в редактор, поэтому поля можно менять без повторного ввода карты; <strong>Сохранить</strong> сохраняет и возвращает к списку, <strong>Отмена</strong> отменяет правки; <strong>Удалить</strong> удаляет строку (только в списке). Обмен пресетами: <strong>Экспорт в файл</strong> для выгрузки и <strong>Импорт из файла</strong> для загрузки. Выбранный пресет автоматически заполняет форму Secured Packet и устанавливает TAR для операции (RAM/GP &rarr; ISD TAR, SIM RFM &rarr; UICC RFM TAR, USIM RFM &rarr; ADF RFM TAR); TAR, введённый в форму Secured Packet вручную, сохраняется при отправке - счётчик, ключи и MSL этого TAR по-прежнему берутся из пресета; SPI1 берётся из MSL, а меньшее значение даёт предупреждение (сервер тоже возвращает его как <code class="font-mono text-sm">warning</code>); изменения сразу передаются работающему слушателю SCP81.</p>
<p class="text-sm mb-3"><strong>Импорт outfile</strong> загружает сразу много карт из <em>outfile</em> поставщика карт &mdash; текстового дампа инструмента персонализации: шапка с баннером и параметрами, затем разделённая пробелами <strong>строка колонок</strong> (среди токенов есть <code class="font-mono text-sm">ICCID</code>; первый токен может нести префикс вида <code class="font-mono text-sm">var_out:</code>) и по одной строке на карту. Всё до строки колонок и все неиспользуемые колонки игнорируются. Разбираются <code class="font-mono text-sm">ICCID</code> (цифры, цифры с добивочным <code class="font-mono text-sm">F</code> до чётного числа нибблов, цифры с разделителями или сырой hex EF &mdash; всё приводится к одной форме), <code class="font-mono text-sm">PIN1</code>/<code class="font-mono text-sm">PIN2</code>/<code class="font-mono text-sm">PUK1</code>/<code class="font-mono text-sm">PUK2</code>, <code class="font-mono text-sm">ADM</code> (строка ASCII, закодированная hex &mdash; <code class="font-mono text-sm">3632333239463337</code> это <code class="font-mono text-sm">62329F37</code>), <code class="font-mono text-sm">PSK</code> (16 байт; необязательная колонка <code class="font-mono text-sm">PSKID</code>/<code class="font-mono text-sm">PSK_ID</code> задаёт её идентификатор, по умолчанию берётся ICCID карты) и нумерованные пары ключей <code class="font-mono text-sm">KICn</code>/<code class="font-mono text-sm">KIDn</code> &mdash; по одному keyset на пару, до 15. <strong>Байтов KIc/KID в файле нет</strong>: номер keyset &mdash; это номер колонки (<code class="font-mono text-sm">KIC2</code> &rarr; keyset 2), а ниббл алгоритма определяется размером ключа &mdash; 8 байт DES, 24 байта 3DES3, а для <strong>16 байт</strong> &mdash; выбором в предпросмотре: <strong>3DES2</strong> (по умолчанию) или <strong>AES</strong>; любой другой размер пропускает этот keyset с пометкой. TAR также не берутся из файла: предпросмотр показывает таблицу TAR, которую получат импортируемые карты (по умолчанию три ролевые записи с TAR по спецификации и MSL 16; их можно править, кнопка <strong>+ TAR</strong> добавляет записи), и один <strong>keyset по умолчанию</strong>, применяемый ко всем записям TAR &mdash; он обязателен для карты с несколькими keysets (ненулевой MSL должен называть keyset). <strong>Общее имя</strong> применяется ко всем импортируемым картам (пустое &mdash; берётся ICCID); <strong>режим импорта</strong> выбирает <strong>Дополнить</strong> (по умолчанию &mdash; существующие пресеты сохраняются, дубли ICCID пропускаются и перечисляются) или <strong>Заменить</strong> (хранилище сначала очищается). Предпросмотр перечисляет разобранные карты (ICCID, keysets, найденные коды) и каждую пропущенную строку/ключ с причиной &mdash; до того, как что-либо сохранено.</p>
<p class="text-sm mb-3"><strong>Импорт outfile</strong> загружает сразу много карт из <em>outfile</em> поставщика карт &mdash; текстового дампа инструмента персонализации: шапка с баннером и параметрами, затем разделённая пробелами <strong>строка колонок</strong> (среди токенов есть <code class="font-mono text-sm">ICCID</code>; первый токен может нести префикс вида <code class="font-mono text-sm">var_out:</code>) и по одной строке на карту. Всё до строки колонок и все неиспользуемые колонки игнорируются. Разбираются <code class="font-mono text-sm">ICCID</code> (цифры, цифры с добивочным <code class="font-mono text-sm">F</code> до чётного числа нибблов, цифры с разделителями или сырой hex EF &mdash; всё приводится к одной форме), <code class="font-mono text-sm">PIN1</code>/<code class="font-mono text-sm">PIN2</code>/<code class="font-mono text-sm">PUK1</code>/<code class="font-mono text-sm">PUK2</code>, <code class="font-mono text-sm">ADM</code> (hex-значение ключа, 4&ndash;16 hex-цифр &mdash; длина различается, например <code class="font-mono text-sm">3632333239463337</code>), <code class="font-mono text-sm">PSK</code> (16 байт; необязательная колонка <code class="font-mono text-sm">PSKID</code>/<code class="font-mono text-sm">PSK_ID</code> задаёт её идентификатор, по умолчанию берётся ICCID карты) и нумерованные пары ключей <code class="font-mono text-sm">KICn</code>/<code class="font-mono text-sm">KIDn</code> &mdash; по одному keyset на пару, до 15. <strong>Байтов KIc/KID в файле нет</strong>: номер keyset &mdash; это номер колонки (<code class="font-mono text-sm">KIC2</code> &rarr; keyset 2), а ниббл алгоритма определяется размером ключа &mdash; 8 байт DES, 24 байта 3DES3, а для <strong>16 байт</strong> &mdash; выбором в предпросмотре: <strong>3DES2</strong> (по умолчанию) или <strong>AES</strong>; любой другой размер пропускает этот keyset с пометкой. TAR также не берутся из файла: предпросмотр показывает таблицу TAR, которую получат импортируемые карты (по умолчанию три ролевые записи с TAR по спецификации и MSL 16; их можно править, кнопка <strong>+ TAR</strong> добавляет записи), и один <strong>keyset по умолчанию</strong>, применяемый ко всем записям TAR &mdash; он обязателен для карты с несколькими keysets (ненулевой MSL должен называть keyset). <strong>Общее имя</strong> применяется ко всем импортируемым картам (пустое &mdash; берётся ICCID); <strong>режим импорта</strong> выбирает <strong>Дополнить</strong> (по умолчанию &mdash; существующие пресеты сохраняются, дубли ICCID пропускаются и перечисляются) или <strong>Заменить</strong> (хранилище сначала очищается). Предпросмотр перечисляет разобранные карты (ICCID, keysets, найденные коды) и каждую пропущенную строку/ключ с причиной &mdash; до того, как что-либо сохранено.</p>
</section>
<section class="mb-10">
+1 -1
View File
@@ -368,7 +368,7 @@
</tbody>
</table>
<p class="text-sm mb-3">The <strong>SCP81</strong> column shows whether the preset supplies a usable PSK pair: <strong>&#10003;</strong> (green) — identity and key set; <strong>&#9888;</strong> (amber) — only one of the two, so the listener ignores the preset; <strong>&mdash;</strong> — no PSK. PSK identity and key must be set together. <strong>Add preset</strong> opens the editor with a fresh preset; <strong>Edit</strong> loads the row's preset into it so fields can be changed without re-entering the card; <strong>Save</strong> stores it and returns to the list, <strong>Cancel</strong> discards the edits; <strong>Remove</strong> deletes the row (list only). Presets can be shared with <strong>Export to file</strong> and restored with <strong>Import from file</strong>. The selected card preset auto-fills the Secured Packet form and sets the TAR for the operation (RAM/GP &rarr; ISD TAR, SIM RFM &rarr; UICC RFM TAR, USIM RFM &rarr; ADF RFM TAR); a TAR typed into the Secured Packet form by hand is kept when sending - the counter, keys and the TAR's MSL are still re-read from the preset; the SPI1 is seeded from the TAR's MSL and a lower value warns (the server returns it as a <code class="font-mono text-sm">warning</code> too); edits are pushed into a running SCP81 listener automatically.</p>
<p class="text-sm mb-3"><strong>Import outfile</strong> imports many cards at once from a card vendor's <em>outfile</em> — the personalization tool's plain-text dump: a banner and a configuration header, then a whitespace-separated <strong>column line</strong> (the tokens include <code class="font-mono text-sm">ICCID</code>; the first token may carry a <code class="font-mono text-sm">var_out:</code>-style prefix) and one row per card. Everything before the column line and every unused column is ignored. The parser reads <code class="font-mono text-sm">ICCID</code> (digits, the digits with an odd-nibble <code class="font-mono text-sm">F</code> pad, spaced digits or the raw EF hex — all normalised to one form), <code class="font-mono text-sm">PIN1</code>/<code class="font-mono text-sm">PIN2</code>/<code class="font-mono text-sm">PUK1</code>/<code class="font-mono text-sm">PUK2</code>, <code class="font-mono text-sm">ADM</code> (the ASCII string, hex-encoded — <code class="font-mono text-sm">3632333239463337</code> is <code class="font-mono text-sm">62329F37</code>), <code class="font-mono text-sm">PSK</code> (16 bytes; an optional <code class="font-mono text-sm">PSKID</code>/<code class="font-mono text-sm">PSK_ID</code> names its identity, defaulting to the card's ICCID) and the numbered key pairs <code class="font-mono text-sm">KICn</code>/<code class="font-mono text-sm">KIDn</code> — one keyset per pair, up to 15. The <strong>KIc/KID bytes are never in the file</strong>: the keyset number is the column number (<code class="font-mono text-sm">KIC2</code> &rarr; keyset 2) and the algorithm nibble follows the key size — 8 bytes DES, 24 bytes 3DES3, and for <strong>16 bytes</strong> the preview's choice, <strong>3DES2</strong> (default) or <strong>AES</strong>; 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 <strong>+ TAR</strong> for extra entries) and one <strong>default keyset</strong> applied to every TAR entry — required for a card with several keysets (a non-zero MSL must name a keyset). A <strong>common name</strong> is applied to every imported card (empty falls back to the ICCID); <strong>Import mode</strong> chooses <strong>Merge</strong> (default — existing presets are kept, duplicate ICCIDs are skipped and reported) or <strong>Replace</strong> (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.</p>
<p class="text-sm mb-3"><strong>Import outfile</strong> imports many cards at once from a card vendor's <em>outfile</em> — the personalization tool's plain-text dump: a banner and a configuration header, then a whitespace-separated <strong>column line</strong> (the tokens include <code class="font-mono text-sm">ICCID</code>; the first token may carry a <code class="font-mono text-sm">var_out:</code>-style prefix) and one row per card. Everything before the column line and every unused column is ignored. The parser reads <code class="font-mono text-sm">ICCID</code> (digits, the digits with an odd-nibble <code class="font-mono text-sm">F</code> pad, spaced digits or the raw EF hex — all normalised to one form), <code class="font-mono text-sm">PIN1</code>/<code class="font-mono text-sm">PIN2</code>/<code class="font-mono text-sm">PUK1</code>/<code class="font-mono text-sm">PUK2</code>, <code class="font-mono text-sm">ADM</code> (the key's hex value, 4&ndash;16 hex digits — the length varies, e.g. <code class="font-mono text-sm">3632333239463337</code>), <code class="font-mono text-sm">PSK</code> (16 bytes; an optional <code class="font-mono text-sm">PSKID</code>/<code class="font-mono text-sm">PSK_ID</code> names its identity, defaulting to the card's ICCID) and the numbered key pairs <code class="font-mono text-sm">KICn</code>/<code class="font-mono text-sm">KIDn</code> — one keyset per pair, up to 15. The <strong>KIc/KID bytes are never in the file</strong>: the keyset number is the column number (<code class="font-mono text-sm">KIC2</code> &rarr; keyset 2) and the algorithm nibble follows the key size — 8 bytes DES, 24 bytes 3DES3, and for <strong>16 bytes</strong> the preview's choice, <strong>3DES2</strong> (default) or <strong>AES</strong>; 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 <strong>+ TAR</strong> for extra entries) and one <strong>default keyset</strong> applied to every TAR entry — required for a card with several keysets (a non-zero MSL must name a keyset). A <strong>common name</strong> is applied to every imported card (empty falls back to the ICCID); <strong>Import mode</strong> chooses <strong>Merge</strong> (default — existing presets are kept, duplicate ICCIDs are skipped and reported) or <strong>Replace</strong> (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.</p>
</section>
<section class="mb-10">
+1 -1
View File
@@ -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.1';
const SIMPLE_VERSION = '3.28.2';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== view history (the browser Back/Forward buttons) =====
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v439';
const CACHE = 'simple-v440';
const URLS = [
'index.html',
'help.html',
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
version = "3.28.1"
version = "3.28.2"
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.
+9 -14
View File
@@ -8,8 +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 ASCII string, hex-encoded - the vendors print
``3632333239463337`` for ``62329F37``), ``PSK`` (the SCP81 key, 16 bytes; an
``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
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
@@ -175,14 +176,6 @@ def _hex_value(tok):
return v if v and len(v) % 2 == 0 else ''
def _ascii_from_hex(v):
"""The ADM column: an ASCII string hex-encoded. '' when it is not."""
try:
return bytes.fromhex(v).decode('ascii')
except (ValueError, UnicodeDecodeError):
return ''
def parse_outfile(text, name='', algorithm16='3des2', tars=None, tar_keyset=None):
"""Parse an outfile into preset dicts (the store's shape).
@@ -273,11 +266,13 @@ def parse_outfile(text, name='', algorithm16='3des2', tars=None, tar_keyset=None
% (row_no, col, raw))
i = idx['ADM']
if 0 <= i < len(toks) and toks[i]:
adm = _ascii_from_hex(_hex_value(toks[i]))
if re.fullmatch(r'[0-9A-Fa-f]{4,16}', adm or ''):
entry['adm'] = adm.upper()
# 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):
entry['adm'] = adm
else:
skip('line %d: ADM is not an ASCII-hex key - dropped' % row_no)
skip('line %d: ADM is not 4-16 hex digits - dropped' % row_no)
i = idx['PSK']
if 0 <= i < len(toks) and toks[i]:
psk = _hex_value(toks[i])
+1 -1
View File
@@ -37,7 +37,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
VERSION = '3.28.1'
VERSION = '3.28.2'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
+27 -3
View File
@@ -139,7 +139,7 @@ class BapShapeTests(unittest.TestCase):
self.assertEqual(p['pin2'], '4321')
self.assertEqual(p['puk1'], '12345678')
self.assertEqual(p['puk2'], '87654321')
self.assertEqual(p['adm'], '62329F37') # the ASCII-hex decode
self.assertEqual(p['adm'], '3632333239463337') # the key's hex value as-is
# EKI, KIK and the IMSI columns are ignored - one keyset only
self.assertEqual(len(p['keysets']), 1)
self.assertEqual(p['keysets'][0]['kicKey'], K16A)
@@ -233,11 +233,35 @@ class OptionTests(unittest.TestCase):
self.assertEqual(len(res['presets']), 1)
self.assertEqual(res['presets'][0]['pin1'], '1234')
self.assertNotIn('adm', res['presets'][0])
# a present ADM is decoded from its ASCII-hex form
# the ADM column is the key's hex value, stored as-is (v3.28.2): the
# length varies, and the value is never ASCII-decoded
text = ('ICCID KIC1 KID1 ADM\n' + DIGITS_F + ' ' + K16A + ' ' + K16B +
' 3632333239463337\n')
res = outfile.parse_outfile(text)
self.assertEqual(res['presets'][0]['adm'], '62329F37')
self.assertEqual(res['presets'][0]['adm'], '3632333239463337')
# ...an 8-byte binary-looking value (the AM0027 sample's shape)
text = ('ICCID KIC1 KID1 ADM\n' + DIGITS_F + ' ' + K16A + ' ' + K16B +
' CFA3DD9C6B7F0F8C\n')
res = outfile.parse_outfile(text)
self.assertEqual(res['presets'][0]['adm'], 'CFA3DD9C6B7F0F8C')
# ...a shorter key (4 hex digits) and the lowercase form
text = ('ICCID KIC1 KID1 ADM\n' + DIGITS_F + ' ' + K16A + ' ' + K16B +
' 1a2b3c4d\n')
res = outfile.parse_outfile(text)
self.assertEqual(res['presets'][0]['adm'], '1A2B3C4D')
# a non-hex or too-short ADM is dropped with its reason
text = ('ICCID KIC1 KID1 ADM\n' + DIGITS_F + ' ' + K16A + ' ' + K16B +
' ZZZZ\n')
res = outfile.parse_outfile(text)
self.assertNotIn('adm', res['presets'][0])
self.assertTrue(any('ADM is not 4-16 hex digits' in s
for s in res['report']['skipped']))
text = ('ICCID KIC1 KID1 ADM\n' + DIGITS_F + ' ' + K16A + ' ' + K16B +
' 123\n')
res = outfile.parse_outfile(text)
self.assertNotIn('adm', res['presets'][0])
self.assertTrue(any('ADM is not 4-16 hex digits' 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)