docs: help/README catch up with v3.8.0; keyset-number wording; RU "пресет"

Documentation:
- help (§1/§3): the SCP80 tab is switched by three pills - the tab list and the
  section intro now name the TAR probe (RU "TAR-сканер"); §3.1/§3.2 describe the
  Keyset selector next to the preset selector (and that a successful PoR
  advances the counter only when SPI1 carries a counter check); §4 describes
  the keyset editor rows, the derived number and the per-keyset counters; the
  probe section keeps its counter rule.
- README/README_RUS: the SCP80 tab says three pills and gains a "TAR probe" /
  "TAR-сканер" section (standard list, verdicts, counter persistence); the
  Cards section is rewritten for the server-side store - keysets, server-owned
  per-keyset counters with the audit trail, JSON export/import as the migration
  path, the keyset editor, the equipped-card mismatch banner - and the field
  table, "Add a card" and "Edit / remove" follow.

Terminology:
- "key version" is no longer used for SCP80 keysets in the UI, the server error
  strings and the docs: the selector is "Keyset", its options "Keyset 1 - ...",
  the messages say "keyset N ...".  The TS 102 225 A.1/A.2 quotes and the GP
  PUT KEY P1 field keep the spec's own wording.
- RU keeps the word "keyset" untranslated ('Добавить keyset', 'Номер keyset',
  "не является keyset пресета", ...) and uses "пресет" instead of
  "предустановка" throughout the dictionary, the help and README_RUS.

sw cache simple-v340.  Tests: 742 Node / 619 Python.
This commit is contained in:
2026-09-30 23:48:32 +03:00
parent 31715ba58a
commit 9f7876b5fa
13 changed files with 207 additions and 180 deletions
+27 -27
View File
@@ -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()