v3.29.1: the per-row keysets in the outfile preview

The import applied one global default keyset to every TAR entry of every
imported card.  The preview's TAR table (TAR + MSL, the three role rows +
"+ TAR") now has a keyset select per row, mirroring the preset editor: an
empty "— none —" plus the keysets found in the file with their KIc/KID,
the first preselected, and the editor's amber warning when a non-zero MSL
has no keyset.  Each TAR's packets then use exactly the chosen keyset.

- PWA: the global "Default keyset for the TARs" select (and the request's
  tar_keyset) is gone; cardsOutfileTarsFromDom/FromForm carry each row's
  keyset; cardsOutfileKeysets() became cardsOutfileKeysetOptions() (the
  union with the KIc/KID bytes for the labels).
- Server: a `tars` entry's own `keyset` (1-15; an unusable value is a 400)
  wins, then the optional global `tar_keyset` (the API shorthand), then the
  card's first keyset - parse_outfile no longer clobbers per-entry keysets
  with the card-level fallback.  presets.normalise_keyset_ref() is
  module-level (one rule for the store and the parser).
- Docs: help EN/RU, api.md (the tars entry keyset + the global fallback),
  the repo AGENTS.  Tests: the per-entry/global/card fallbacks, the
  bad-keyset 400, the options helper, the row select and round-trip, the
  import body.  925 frontend + 944 Python; sw cache simple-v448.
This commit is contained in:
2026-10-15 00:40:16 +03:00
parent cf7ee77a9e
commit 8a66ea5a0a
12 changed files with 168 additions and 95 deletions
+13 -2
View File
@@ -150,10 +150,18 @@ def normalise_tars(tars, tar_keyset=None):
raise ValueError('TAR must be three hex bytes: %r' % tar)
if not re.fullmatch(r'[0-9A-F]{2}', msl):
raise ValueError('MSL must be one hex byte: %r' % msl)
ks_raw = t.get('keyset')
if ks_raw in (None, ''):
keyset = None
else:
keyset = presets.normalise_keyset_ref(ks_raw)
if keyset is None:
raise ValueError('keyset must be 1-%02X: %r'
% (presets.KVN_MAX, ks_raw))
out.append({'role': role if role in presets.TAR_ROLES else '',
'tar': tar, 'msl': msl,
'desc': str(t.get('desc') or '').strip(),
'keyset': tar_keyset or None})
'keyset': keyset or (tar_keyset or None)})
seen = set()
for t in out:
if t['tar'] in seen:
@@ -248,8 +256,11 @@ def parse_outfile(text, name='', algorithm16='3des2', tars=None, tar_keyset=None
# Every TAR gets the caller's global default keyset; without one the
# card's first keyset is used (a multi-keyset card would otherwise
# fail the store's "a non-zero MSL needs a default keyset" rule).
# A per-entry keyset (chosen in the preview) wins; the caller's global
# default - else the card's first keyset - fills the rest.
card_keyset = tar_keyset or presets.keyset_kvn(keysets[0])
card_tars = [dict(t, keyset=card_keyset) for t in tars_base]
card_tars = [dict(t, keyset=t.get('keyset') or card_keyset)
for t in tars_base]
entry = {'name': str(name or '').strip() or iccid, 'iccid': iccid,
'keysets': keysets, 'tars': card_tars}
for field, col in (('pin1', 'PIN1'), ('pin2', 'PIN2'),
+20 -20
View File
@@ -218,6 +218,25 @@ def tar_keyset(preset, tar_hex):
return (tar_entry(preset, tar_hex) or {}).get('keyset') or None
def normalise_keyset_ref(v):
"""A TAR's default keyset number (int 1-15) or None. Accepts an int and
the decimal ('1'..'15') or hex-ish ('01'..'0F') string forms; an unusable
value becomes None - the validation reports a missing keyset where one is
mandatory. Shared by the store and the outfile import (v3.29.1)."""
if v is None or v == '':
return None
s = str(v).strip().upper()
if re.fullmatch(r'[0-9]{1,2}', s):
n = int(s, 10)
if 1 <= n <= KVN_MAX:
return n
if re.fullmatch(r'[0-9A-F]{1,2}', s):
n = int(s, 16)
if 1 <= n <= KVN_MAX:
return n
return None
class PresetError(Exception):
"""A refused mutation (validation, duplicate ICCID, ...)."""
@@ -360,7 +379,7 @@ class PresetStore:
'tar': re.sub(r'\s', '', str(t.get('tar') or '')).upper(),
'msl': re.sub(r'\s', '', str(t.get('msl') or '')).upper(),
'desc': str(t.get('desc') or '').strip(),
'keyset': self._normalise_keyset_ref(t.get('keyset')),
'keyset': normalise_keyset_ref(t.get('keyset')),
})
else:
msl = re.sub(r'\s', '', str(fields.get('spi1') or '')).upper()
@@ -392,25 +411,6 @@ class PresetStore:
'keyset': e.get('keyset')})
return out
@staticmethod
def _normalise_keyset_ref(v):
"""A TAR's default keyset number (int 1-15) or None. Accepts an int
and the decimal ('1'..'15') or hex-ish ('01'..'0F') string forms; an
unusable value becomes None - the validation reports a missing keyset
where one is mandatory (v3.26.0)."""
if v is None or v == '':
return None
s = str(v).strip().upper()
if re.fullmatch(r'[0-9]{1,2}', s):
n = int(s, 10)
if 1 <= n <= KVN_MAX:
return n
if re.fullmatch(r'[0-9A-F]{1,2}', s):
n = int(s, 16)
if 1 <= n <= KVN_MAX:
return n
return None
def _normalise_keyset(self, ks):
"""One keyset, cleaned. The counter is kept as typed here and
validated + padded in _validate_keyset() - a bad value must be
+1 -1
View File
@@ -37,7 +37,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
VERSION = '3.29.0'
VERSION = '3.29.1'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE