Commit Graph

62 Commits

Author SHA1 Message Date
catarrh 85cc4b9535 v3.20.0: the server-owned SCP80 counter (an untouched field = the preset's next value)
The counter an operation consumed was re-entered by the PWA on every send
(the form field was filled from the preset and posted back), so a stale tab
could send a value the card rejects (cntr_low), and typing in the field
silently wrote the preset.  Now the store is the single source of truth and
the form field is a per-send override (Option B).

- _preset_counter_seed: an explicit `cntr` in the request wins (a hand send,
  the counter probe or a pre-built packet may deliberately pick one);
  otherwise the preset store's value for the packet's keyset.  Wired into
  /api/send-ota (the APDU path; the pre-built `sp` path keeps the caller's
  bookkeeping value - the packet carries its own counter), /api/ram-install,
  /api/ram-install-app, /api/cap-compat, /api/tar-probe and /api/sp-verify
  (the reference must use the counter the JS packet was built with).
- the send-ota response echoes the effective `cntr` (a caller that omitted
  one learns what was used) and the OTA SEND log line prints it.
- persistence is unchanged and monotonic (presets.set_counter refuses
  backwards writes), pinned by tests: an explicit lower counter is used for
  the packet but never lowers the store, a higher accepted one is saved.
- PWA: the Secured packet counter field starts empty with the preset's next
  value as its placeholder (auto: <cntr>); typing affects that send only and
  is never written to the preset (spCntrSyncPreset is gone), spParams /
  getRamSpParams omit an empty counter, the RAM/send flows sync only the
  local preset copy (forward-only) instead of filling the field, the +/-
  buttons start from the effective value, and the preview/verify packet is
  built from the effective counter (genSp).  Placeholder translated EN/RU.
- the runner and /api/counter-probe keep their existing seeding (the probe
  already starts at the failed value and walks above it).
- docs: api.md's optional-`cntr` contract + seeding notes on every endpoint,
  help EN/RU 3.1.  Tests: 822 frontend + 734 Python; sw cache simple-v393.
2026-10-05 22:23:19 +03:00
catarrh 3c5882d16d v3.14.2: the counter probe's verdict rule
The ladder only advances on cntr_low; any other verdict stops the search.
A stop on a non-por_ok verdict after at least one cntr_low keeps the value
anyway - the earlier rejection proves it is above the card's counter - but
the response reports success: false with counter_saved, the verdict and the
saved counter ("something is likely wrong with the security settings");
the PWA shows it amber.  Without a prior cntr_low nothing is saved, and a
missing PoR stays an error.  actual_response_sms_submit/ussd count as
accepted.

Docs: docs/api.md, repo AGENTS.  sw cache simple-v377.
Tests: 672 Python + 798 frontend green.
2026-10-02 14:58:18 +03:00
catarrh 475c13aeb8 v3.14.1: the counter probe demands its PoR
_counter_probe_params validates SPI2, forces b1 (PoR required; a caller's
00 becomes 01) and defaults to the RAM listing transport 21; the no-PoR
error names the effective SPI2, and ramSyncCounter posts
ramListingSpi2('40') for the RAM view (the failed operation's transport).

Live 2026-10-02 (NP BAP): the probe had sent the plain SCP80 form's 00
(no PoR) - the card answered the ENVELOPE 9000 and sent nothing, so the
probe stopped after one packet with "card verdict: no PoR".

Docs: docs/api.md, repo AGENTS.  sw cache simple-v376.
Tests: 670 Python + 798 frontend green.
2026-10-02 14:40:07 +03:00
catarrh cc480c30b7 v3.13.3: ELF version retry for both P1 values + query-based per-AID budget
elfTagListRetry walks the untried tag-list forms for P1=10 and P1=20
(skipping combinations the main pass already refused via the expandedTried
map; only the tag-list forms are retried - the no-tag-list form never
carries CE - and the walk stops as soon as a version appears).  The form
memory can skip the tag-list forms for P1=10 when the Apps listing served
only the no-tag-list form (live 2026-10-02: EP BAP - its P1=10 tag lists
were never queried while the P1=20 retry refused them).

elfPerAidVersions filters to packages that actually have an applet
candidate before the six-query cap: the budget counts queries, not list
positions (EP BAP's first six packages are Oracle/JC-API entries with no
app instances - they consumed the cap and the A113 packages were never
queried).

Docs: help EN/RU, repo AGENTS (v3.13.3 note).  sw cache simple-v373.
Tests: 667 Python + 796 frontend green.
2026-10-02 13:30:03 +03:00
catarrh 2c49c54d2d v3.13.2: ELF versions derived through the package's module AIDs
ramDeriveElfVersions matches an applet instance to its package through a
shared ramElfAppMatch: the app's C4 (Load File AID) when the card reports
the real one, then the package's module AIDs (the applet instance AIDs),
then the shared-prefix fallback (the same tiers ramElfAppletCandidate uses
for the per-AID queries).  Live 2026-10-02 (test-eSIM): the card returns
C4 = the applet's own AID (c4 10 ...3908 for package ...3900), so the old
C4-only derivation filled nothing; the module lists map ...3900 -> ...3908
(9.3), ...0200 -> ...0208/...494D4508 (1.1) and F0414C46416001 ->
F0414C4641600101 (0.1).

The ELF version hint names the remaining case: a package without an
applet instance has no version to derive either (the Oracle
A00000006203010101 stays bare - the card refuses every ELF tag-list form,
so its CE is unreachable).

Docs: help EN/RU, repo AGENTS (v3.13.2 note).  sw cache simple-v372.
Tests: 667 Python + 796 frontend green.
2026-10-02 13:16:08 +03:00
catarrh 5b8324ed02 v3.13.1: expanded-registry pages try every command format
The explorer's next-occurrence page (P2=03) is retried in the other
command formats (ramPageFormats: the expanded codings first - the serving
one first when it is expanded - and the compact command string last,
because a compact page attempt can consume the page without delivering
it) and the format that serves a page is kept for the following ones.
Live 2026-10-02 (test-eSIM card): a compact page 2 answered 61B6 and the
chained GET RESPONSE 6A86, after which the AA and AE 80 retries both saw
the occurrence already gone; the AA-wrapped page returns 182 B with the
remaining app instances' CE/CC/EA, while AE 80 is refused.  A page that
no format serves ends the listing.

Only an explicit 6310 promise counts for the "(first page only)" flag
(ramExpandedMore): a probe after a 61xx page that every format refuses is
the card's final answer, so that card's ELF listing is no longer flagged
(its expanded registry has three entries; the fourth package exists only
in the compact listing - the ELF tag-list forms and per-AID queries are
refused in every format).

In Auto the explorer also queries entries the bulk listing left without
expanded fields individually by AID (ramAppsMissingDetails, <= 6 packets,
the compact command string carries the GET RESPONSE for 61xx).

Docs: help EN/RU, repo AGENTS (v3.13.1 note).  sw cache simple-v371.
Tests: 667 Python + 794 frontend green.
2026-10-02 12:47:02 +03:00
catarrh 355bcadb59 v3.10.0 review fixes: repair /api/ram-install-app, escape quotes in esc
- /api/ram-install-app referenced undefined spi1/tar (the resolution block
  was only added to /api/ram-install and /api/cap-compat), so every request
  answered 500.  The same preset/MSL resolution is now in place, with
  regression tests for both modes (install / make selectable) and for the
  no-MSL refusal before any card I/O.
- esc() now escapes " and ' too: the TAR editor renders free-text
  descriptions (and other values) into attribute contexts, so a crafted
  preset import could inject attributes.  Three tests asserting the old
  unescaped output updated; a dedicated esc test added.
- _spi1_for_tar names the no-TAR case in its hint (a pre-built packet
  without tar/spi1 used to report "TAR ?").
- preset free TAR rows no longer store an empty "role" key (the documented
  shape); a non-list `tars` in an update is refused (400) instead of being
  silently ignored.

Live: the no-MSL request answers 400 with the hint; a make_selectable with
a bogus AID runs the format probe and the INSTALL (derived SPI1 16 from the
preset MSL), the card answers remote SW 6A88 and the counter advances - no
500.  Suites: 658 Python + 775 frontend.

No version bump: completes the unreleased v3.10.0 (6360dc5 was never
pushed).  sw cache simple-v359.
2026-10-02 08:40:42 +03:00
catarrh e147ab90b4 Code-review fixes: bad-format visibility, view-aware sync, counter hygiene
- The RAM step line (and the plain send's PoR line) now name a bad-format
  failure ("bad format 02 (wrong length)") instead of reading "PoR ok": the
  step record carries por_bad_format_name and ramStepLine renders it.
- ramSyncCounter follows the view the notice came from ('ram'/'sp' - preset
  and keyset selectors alike) and refreshes the keyset labels after a sync.
- The ETSI-release line no longer hides behind the memory block (it needs
  only the ELF list).
- /api/send-ota reports final_cntr only for a packet the card accepted: the
  failed-envelope path used to persist the packet's own counter on a
  rejection (the audit trail's small bump on cntr_low).
- _ram_detect_format's docstring corrected (both expanded codings execute on
  the live ISD; the earlier bad format was a length-mismatched template).
- Tests: ramStepLine bad format, the view-aware notice, the accepted/rejected
  final_cntr cases, and an end-to-end /api/counter-probe endpoint test
  (400s, no write on a rejected walk, accepted+1 stored).
- sw cache simple-v356.
2026-10-02 07:20:36 +03:00
catarrh 0f69c6d588 Fix the card ETSI release lookup: the CE tag is raw hex
The expanded registry returns the package version as two bytes of hex (010C)
and the ELF rows format it with ramFmtVersion (010C -> 1.12) - the release
lookup used the raw value, so the live card showed
"ETSI release: unknown (uicc.toolkit 010C)".  cardEtsiRelease formats it the
same way (a non-4-hex value passes through unchanged) and the tests now use
the raw vectors the card actually returns.

- sw cache simple-v355.
2026-10-02 00:53:00 +03:00
catarrh 00181ba3ad Infer the card's ETSI release from its installed uicc.toolkit version
The explorer's card-resources block now names the card's ETSI release, using
the same mapping as the CAP analysis (capmem.ETSI_TOOLKIT_RELEASE: uicc.toolkit
version -> REL-N, basis in brackets; the live card's 1.12 -> REL-15/16/17).
The table is mirrored in the PWA (drift-guarded by tests/test_cap_memory.py,
like JC_FRAMEWORK_SDK); the version comes from the expanded registry's CE tag
and degrades to the version hint when the listing has none; SIM-only cards (no
uicc.toolkit) get no label, the same rule as SIM-only CAPs.

- cardEtsiRelease(elfs) (pure) + UICC_TOOLKIT_AID / ETSI_TOOLKIT_RELEASE in the
  PWA; rendered after the free-memory lines with a tooltip naming the basis.
- Tests: helper vectors (1.12/1.11/1.5, unknown version, versionless,
  SIM-only), the rendering guard, and the table/AID drift guard.
- Help EN/RU; sw cache simple-v354.
2026-10-02 00:48:45 +03:00
catarrh 6e4eb5932c Expanded listings follow the command format; registry gets a format fallback
The TCA loader's Explore traces settle the format question: it sends the same
P2=00 listing queries wrapped and unchained (`AA0A2208 80F24000024F0000`,
pages `...01024F0000`) - no GET RESPONSE anywhere - while the live card needs
the chain and answers 6700 to the wrapped unchained form; and the live ISD
refuses the compact P2=02 registry query (6700) while serving it wrapped
(`AA11220F 80F2400209 4F005C054F9F70C5CC00` -> 6310 + entries with CE
versions).  Both the listing and the registry queries now follow the
operation's command format and fall back across formats.

- ramFormatIsExpanded(fmt) + ramFmtLadder(runFmt, working) (pure): the run's
  format first, then expanded, expanded-ae and compact as the last resort; a
  format that served data is tried first and never repeated.
- ramGetStatusApdu(p1, p2, fmt): the chain exists in compact only (TS 102 226
  5.2.1.1) - an expanded run sends the bare query.
- paginate: the P2=00 listing follows the run's format and pins it per send,
  so a GET RESPONSE can never ride inside an expanded packet.
- expandedPass: each registry form goes through the format ladder; the chained
  61xx retry is compact-only (it was guarded for 'expanded' but not
  'expanded-ae'); the serving format is remembered for the remaining listings
  and named in the report ([AA]/[AE 80]); ramExpandedGroups carries it.
- Tests: the helper + ladder vectors, the unchained expanded listing builder
  (both codings), the fallback/chain source guards, the report marker.
- Help EN/RU, the repo AGENTS.md; sw cache simple-v353.
2026-10-02 00:40:09 +03:00
catarrh 391d4a322d Low-counter reporting, counter sync probe, Bad-format decode, expanded forms
Live findings that drove this: the "card counter 0000000000" the low-counter
notice showed is not the card's counter at all (the PoR's CNTR is a copy of
the command's counter, TS 102 225 5.2 table 3, and a rejection leaves it
zeroed); and an earlier "no card accepts the expanded format" observation was
a hand-made template whose declared length did not match its body - the card
validates the lengths and answers with a Bad format TLV.

- _cntr_low_fields(status) reports the verdict as a flag; card_cntr/
  suggested_cntr and the UI's "(card counter ...)" text are gone - the value
  is not observable.  The notice offers a bounded Sync counter probe instead.
- POST /api/counter-probe: a doubling counter ladder (start+1, +2, +4, ...)
  with a read-only command until the card accepts one, then accepted+1 is
  persisted (monotonic, source counter-probe).  Rejected attempts change
  nothing on the card; the search stops at a ceiling (default FFFFFFFF, far
  below the 40-bit maximum where the counter blocks) and after max_attempts
  (default 40, clamped 1-64); a non-cntr_low failure stops it at once.
- _parse_response_scripting decodes the Bad format TLV (90, TS 101 220 table
  7.20; error types per TS 102 226 table 5.12) and returns a dict; the PoR
  carries bad_format/bad_format_name and _ram_step_result fails the step with
  "bad format 02 (wrong length)" instead of falling through to the compact
  decoder (which read the AB tag as 171 commands).
- _wrap_expanded_apdu(apdu, form) emits the definite (AA) or the indefinite
  (AE 80 ... 00 00) scripting template; the RAM command formats are compact/
  expanded/expanded-ae and _ram_detect_format probes compact, then both
  expanded codings (the expanded probes use Le='00' and no chained GET
  RESPONSE, TS 102 226 5.2.1.1).  The RAM form gains a Format selector.
- Live-verified: the verdict carries no value; the probe walked
  0x1000001/0x1000003 (low) then 0x1000007 (accepted, stored 0x1000008); the
  ISD executes both expanded codings (R-APDU 08A0000000030000000F80, SW 9000)
  and a length-mismatched template answers bad_format 02 - decoded now.
- Tests: the ladder, the probe walk + persistence, params validation, both
  template forms, the Bad-format TLV (definite and plain tag styles), the
  live ISD response through _decode_por, the step-result failure, and the PWA
  guards; help EN/RU, docs/api.md, the repo AGENTS.md; sw cache simple-v352.
2026-10-02 00:17:10 +03:00
catarrh 994ee6d2a3 ui: the Data Connection event gains the date-time object; the "more" flag is precise
- The 0x1D event form now offers the Date-Time and Time zone object
  (TS 102 223 8.39): an optional select that includes the host clock's
  date-time, built by eventDateTimeTlv() as seven semi-octet BCD bytes in the
  TS 23.040 9.2.3.11 order (units high, tens low) with the time zone in
  quarters of an hour and its sign in bit 3 of the first semi-octet.
- The Cards list's empty-state colspan is 8 now (the table has had 8 columns
  since the keyset refactor, not 10).
- The expanded-registry "(first page only)" note marks only an unfulfilled
  6310: ramExpandedMore() keeps the promise set when the follow-up page was
  refused/failed or the page cap stopped the paging, and clears it when an
  accepted page ended the listing (a completed multi-page listing was
  annotated wrongly before).

Tests: eventDateTimeTlv (fixed dates, both timezone signs), the 0x1D build
with and without the date-time, and ramExpandedMore's cases.  Help EN/RU and
both AGENTS files; sw cache simple-v347.
2026-10-01 22:07:24 +03:00
catarrh 0cccd62230 release: v3.9.0 - the operation start buttons wait for a preset and a keyset
Gating: the SCP80 "Send to Card", the RAM Execute and the TAR-probe Execute
buttons carry `data-needs-keyset`; pysimApplyAvailability() disables them
(with the hint "Select a card preset and its keyset" when the card itself is
fine) unless the view's preset is selected and the form's keyset number is one
of its keysets - `spKeysetStartReady`, the same rule the click-time guards
enforce, which stay as the last line of defence.  spKeysetSync() refreshes the
gate on every preset/keyset change, and the KIc/KID index selects now call
spKeysetSync() so the keyset selector follows a manual index change too.

Version 3.9.0: server VERSION, pyproject.toml and the PWA header; sw cache
simple-v346; the workspace AGENTS gains its v3.9.0 history entry.

Tests: indicator.test.js drives the gate (no preset -> disabled with the hint,
an undefined keyset -> disabled, a matching keyset -> enabled) and guards the
three buttons' attribute; the cards/keysets/ram sandboxes stub
pysimApplyAvailability.  Help EN/RU notes the disabled start buttons.
2026-10-01 22:03:36 +03:00
catarrh 31715ba58a fix: the TAR probe view auto-selects the preset; the keyset selectors refresh (v3.8.0)
Two selector bugs in the new TAR-probe view and one stale-list bug in the RAM
view, all from the lazy population of the view-specific preset selects:

- cardsAutoSelectByIccid() set sp-card-sel and ram-card-sel only, so the TAR
  probe view never got the equipped card's preset (and tarExecute() then ran
  with preset_id: '', i.e. without persisting the counters it consumed);
- tarRender()'s keep-fallback was -1 instead of the remembered _ramCardIdx, so
  the select stayed on "— Select card —" even when the auto-selection had
  remembered the preset; the TAR pill switch now also calls tarRender();
- the keyset selectors showed a stale "N — not defined in the preset":
  spKeysetSync() runs during the auto-selection, when ram/tar-card-sel have no
  options yet, so those lists were built with no preset; ramRender() now
  rebuilds them (spKeysetSync) once its preset is known - tarRender() already
  did.

Tests: the auto-select covers tar-card-sel; a DOM-driven regression test
(ramRender with a remembered preset and a two-keyset preset must show
value="2" selected and no "not defined" entry); source guards for ramRender's
sync, tarRender's keep and the TAR pill switch.  sw cache simple-v339.
2026-09-30 22:41:09 +03:00
catarrh 386958b261 feat: warn when the equipped card and the selected preset disagree (v3.8.0)
Autoselection normally keeps the SCP80/RAM preset in step with the card, so
the mismatch is easy to miss.  Both views now carry an amber banner above the
preset selector (the existing warning style - no new Tailwind classes):

- the equipped card matches no preset: "The equipped card <ICCID> does not
  match any card preset - the selected preset may belong to another card.
  Add it in the Cards tab (From card)"; with no presets at all the banner says
  so instead;
- a preset is selected but it is not the equipped card's: a softer line naming
  the matching preset ("check that its keys and counters belong to this card").

`spPresetWarning(iccid, presets, selectedIdx)` is pure (unit-tested with the
ICCID forms the store accepts: digits, spaced, raw EF hex), `spPresetWarningRender`
fills both banners and runs on every card-state poll, preset load, preset
selection and page init.  No card session or an unreadable ICCID shows nothing
(the card-iccid gating already explains that state); the banner warns only -
it never blocks a send (preparing a packet for another card stays possible).

Tests: frontend/tests/preset_warning.test.js (5), the card-state/preset
harnesses stub the DOM-bound renderer.  sw cache simple-v336.
2026-09-30 15:37:51 +03:00
catarrh cc2edb3d8a feat: one keyset per key version - indexed keys and counters (v3.8.0)
A card usually holds several OTA key sets, and the SCP80 counter is tracked
per key version on the card, so a preset now 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 the bytes.  The spec is explicit:
"a dedicated counter shall be associated to each key version" (Annex A.1) and
"the key version number indicated in the KIc and KID (bits b8..b5) fields
shall be identical when different from 0" - a mismatch is rejected by the card
with "Unidentified security error" (A.2).

Server (presets.py, schema 2):
- a preset carries `keysets: [{kic, kid, kicKey, kidKey, cntr}]`; validation
  refuses a KIc/KID version mismatch (A.2), a version outside 01-0F ('00'
  means "no security" and is a packet-level choice, not a keyset), duplicate
  versions and missing keys/counters; a v3.8.0 flat preset converts into one
  keyset on load and on import;
- `set_counter(..., kvn)` writes the counter of that keyset (monotonic per
  keyset), the audit line carries the kvn, and `find_keyset()` serves the
  lookups;
- the operations derive the key version from the request's KIc byte and
  persist into that keyset: /api/send-ota, the RAM install/install-app/compat
  steps (_ram_send_gp_apdu) and the test runner's SCP80 steps (which may name
  a `kvn` per step and keep one working counter per keyset);
- a non-zero key version the named preset does not define, or a KIc/KID
  mismatch, is refused before anything is built (_preset_keyset_check /
  _keyset_guard: 400, or a step error in the test runner).  Without a
  preset_id only the mismatch rule applies.

PWA:
- the Cards form's KIc/KID/counter/keys inputs become a keyset editor (one row
  per version: KIc, KID, both keys, that version's counter, Add/Remove; the
  version is shown derived from the bytes and a mismatch is flagged);
- the SCP80 and RAM views get a Key-version selector beside the preset
  selector, driving the KIc/KID bytes and loading that keyset's keys and
  counter; the packet builder refuses a version the preset does not define
  (spKeysetCheck/spKeysetGuard, pure and unit-tested);
- the counter syncs are per keyset (spCntrLocalSync/ramSaveCntr), the SCP80
  form's counter edit posts the keysets, and the test-run write-back syncs
  every counter the server reports (scp80_counters).

Docs/help/AGENTS updated; sw cache simple-v335.  Tests: 610 Python (store
keysets, endpoint refusals, the counter landing in the right keyset) and 726
Node (keysets.test.js plus the updated harnesses).
2026-09-30 15:15:21 +03:00
catarrh 666fc2cf6c fix: per-AID version queries - module->applet mapping + chained GET RESPONSE (v3.7.1)
Two bugs in the v3.7.0 per-AID fallback, both visible in the live log
(2026-09-30 11:21):

- the candidate applet was searched by the AID prefix, but the applet
  instance AIDs are the package's module AIDs (package A113...0200 has
  modules ...0208 / ...494D4508, both present as applet instances - they
  differ from the package AID in the last byte), so that package produced no
  query at all.  ramElfAppletCandidate (pure) prefers the module AIDs and
  keeps the shared-prefix rule as a fallback;
- the single-AID expanded query answered 6134 (the card holds the data) and
  the empty response was read, so no version was derived.  The query is now
  retried with the GET RESPONSE chained into the compact command string
  (ramChainGetResponse) exactly like the ladder - a standalone GET RESPONSE
  answers 6700 on this card.

Tests: the module->applet mapping with the live AIDs (incl. the versioned
applet being skipped and the prefix fallback), plus the source guards for the
chained retry.

sw cache simple-v333; tests: 557 Python / 717 Node.
2026-09-30 11:30:40 +03:00
catarrh 032f70f509 feat: ELF versions via P1=20 retry and per-AID app queries (v3.7.0)
No live card accepts a tag list for the ELF queries (tags/noea/lean all
answer 6A88 on P1=10 - the log of 2026-09-30 11:11), so the version-carrying
CE tag was unreachable there; the ELF listing itself (notags + chained GET
RESPONSE) works and carries AID + life cycle + modules only.

Two fallbacks:
- when the ELF rows have no version and the details pass never got a
  tag-list form, the ladder is walked once more with P1=20 (a card's P1=10
  implementation can be stricter) and merged by AID;
- any version still missing is fetched per applet AID: the AID search
  qualifier (4F <len> <AID>, GP 11.4.2.3 Table 11-35) plus the apps tag
  list selects a single applet entry, whose CE is its package's version
  (the applet <-> package mapping is the AID prefix; at most 6 queries).

Also:
- the app and ISD rows show their parsed CE version (the expanded apps
  listing carries it);
- an unfulfilled 6310 (the next-occurrence page refused, e.g. 6A86) is
  reported as '(first page only)' instead of looking complete;
- the page loop only continues when the chained GET RESPONSE was accepted -
  a refused retry no longer costs an extra P2=03 packet.

sw cache simple-v332; tests: 557 Python / 716 Node.
2026-09-30 11:17:12 +03:00
catarrh 162930eb96 feat: low-counter guard for every SCP80/RAM operation (v3.6.58)
A cntr_low PoR verdict is not recoverable by retrying - every packet whose
counter is not above the card's is rejected again - so it now stops the
operation and is stated as plain data instead of a generic PoR error.

Server:
- _cntr_low_fields(status, cntr) -> {cntr_low, card_cntr, suggested_cntr}
  (fixed-width wrap like the counter field); carried by /api/send-ota (plus
  an 'OTA CNTR-LOW: card counter ...' log line) and by the RAM install /
  install-app / cap-compat failure responses (from the failed step);
- _send_envelope's late-PoR poll now also runs after a warning ENVELOPE
  answer (62xx/63xx - the live card answers 6200 for a low counter) and
  _send_secured_packet passes the captured PoR on, so the verdict is named
  instead of a bare 'ENVELOPE failed' (that is why it only surfaced on the
  later SPI2=0x21 query in the live log).

PWA:
- spCntrLow(res) guards every flow; the Explore routes the whole run through
  a checked sendOta(), so the paginate/expanded loops stop, no further
  packet is sent and no counter is written back (delete and Send to Card
  check it too);
- the result states the card's counter and offers 'Go to preset'
  (ramGoToPreset: opens the failed operation's preset in the Cards tab for
  editing and focuses the counter field) - nothing is written
  automatically; the operation modal carries the same notice;
- help (EN/RU) documents the guard.

sw cache simple-v331; tests: 557 Python / 714 Node.
2026-09-30 11:10:54 +03:00
catarrh 81a7aace15 fix: BER-safe proactive TLV walks + hardened FETCH loop (v3.6.57)
The live 500 in the Explore ('OTA send error: index out of range') was a
parser bug, reproduced with the live FETCH shape: _parse_proactive_header
walked the command TLVs reading the length as a single byte, so the card's
SEND SHORT MESSAGE with its 150-byte SMS TPDU (8B 81 96) was mis-walked
into the TPDU and eventually indexed past the end (val[2] on a short
slice) - the FETCH was left unanswered and the request answered 500.

- _ber_tlv_list/_proactive_body helpers: BER lengths (_ber_len_at) plus a
  bounds check that stops at a truncated TLV instead of walking into the
  value; _parse_proactive_header, _parse_display_text, _parse_select_item,
  _parse_setup_menu_items, _tlv_map and _decode_poll_negotiation all use
  them now (the same latent bug for any long-form TLV);
- _run_proactive_chain guards every step (header parse, proactive log,
  on_fetch, BIP/timer handlers): an exception is logged with the offending
  FETCH bytes and the TERMINAL RESPONSE is still sent - a decoder bug can
  never leave a FETCH unanswered again;
- the expanded-registry report distinguishes a request error from a remote
  SW: 'ELFs: request error (index out of range) - compact listing used'.

Regression tests: the live long-form FETCH shape parses (header + TLV map +
TPDU), truncated TLVs stop the walk, and a raising parser still gets a TR.

sw cache simple-v330; tests: 553 Python / 711 Node.
2026-09-30 10:27:50 +03:00
catarrh 61bb1ab621 feat: chained GET RESPONSE + ELF versions from app instances (v3.6.56)
The live log (2026-09-30) showed our standalone GET RESPONSE was wrong: the
card answered 6700 (wrong length) to 00C00000<xx> for the 61xx answers of
the expanded ISD query and the ELF notags query.  The compact path proves
how this card wants it: GET STATUS and GET RESPONSE chained in one compact
command string (80F2..C0000000).

- a 61xx answer now retries the same query with ramChainGetResponse
  (+ C0000000, compact format only; the expanded AA/22 form has no command
  string, so no retry there);
- a 61xx page still continues to the next occurrence (the GET RESPONSE's own
  SW replaces the 6310 signal); the ISD stays a single occurrence;
- the standalone 00C00000<xx> attempt is gone (it only ever answered 6700).

This card also refuses every tag list for the ELF queries (tags/noea/lean
all 6A88), so CE cannot come from the ELF listing.  Its apps listing does
carry the load file version (CE) together with the package AID (C4), so
ramDeriveElfVersions fills a missing ELF version from its applet instance
and the row annotates it: 'Version: 9.3 (from the app instance)'.  The
version hint now shows whenever any ELF row lacks a version.

sw cache simple-v329; tests: 550 Python / 711 Node.
2026-09-30 10:08:24 +03:00
catarrh b7798a7fd9 feat: expanded registry handles 61xx/6310, lean form, grouped report (v3.6.55)
The complete live trace (2026-09-30) showed the card was not refusing nearly
as much as the report suggested:

- the ISD expanded query answered 6131 and the ELF notags query 617F/613F -
  response data pending, and no GET RESPONSE was ever sent;
- the apps expanded query answered 6310 (more occurrences) and only the
  first page was merged;
- the ELF tag-list forms (tags, noea) answered 6A88 because they still
  carried the application-only C5 privileges / C4 ELF AID.

Fixes:
- 61xx -> ramGetResponseApdu(sw) = 00C00000<xx> (up to 4 rounds);
- 6310 -> the same query as a next-occurrence page (P2.b1=1, P2=03, up to
  8 pages), so the apps listing is complete;
- a 'lean' ladder form with only the objects valid for the kind: ELFs get
  4F 9F70 CE 84 (AID, life cycle, version, modules), the ISD/apps get
  pySim's 4F 9F70 C5 CC; the ladder is tags -> noea -> lean -> notags;
- the report groups by listing label with the best outcome winning
  (entries > accepted-but-empty > refusal): the P1=10/P1=20 ELF double
  query no longer prints 'ELFs, ELFs' and a pass with data is never called
  a refusal; the ELF version hint is form-aware.

sw cache simple-v328; tests: 550 Python / 709 Node.
2026-09-30 09:57:41 +03:00
catarrh aa4bf8e008 feat: expanded registry on by default, form ladder + fallback report (v3.6.54)
The 'Fetch expanded registry details' checkbox is now on by default: the
expanded pass carries the package versions, the SD/load-file associations
and the applet menu entries; an explicit stored opt-out is still honoured.

Live 2026-09-30: a card with a non-default ISD AID answered 6A88 to the
reference expanded query (P2=02 + the tag list), so the ELF rows had no
versions.  Cards differ in what they accept, so the query is now tried in
three forms (RAM_EXPANDED_FORMS):

  tags   - the reference tag list (incl. the TS 102 226 EA SCP Registry Data)
  noea   - the same list without EA (a card that errors on it)
  notags - no tag list at all (GPPro's 4F00-only data field); the card
           builds the default E3

The first form that answers is remembered for the run and tried first for
the later listings; the ISD gets a single attempt - its 6A88/6A86 is normal
('no data to report', GPPro) and must not poison the detection for the
apps/ELF queries.

Every listing's outcome is reported in the explorer (ramExpandedReport):
'Expanded registry: tag list + EA (Apps, ELFs) - ISD: no expanded data
(6A88) - compact listing used', or 'Expanded registry refused by the card -
compact listing only (package versions unavailable)'; the ELF section hints
when no version came back.  Tooltip/help (EN/RU) updated.

sw cache simple-v327; tests: 550 Python / 707 Node.
2026-09-30 09:44:39 +03:00
catarrh 4906c91463 feat: install LOAD failures get the compatibility-test analysis (v3.6.53)
ramShowInstallResult appends ramCompatDetailHtml(data, capMem) to the
install modal (the modal content is HTML now - the step lines are escaped
and ramOpModalFinish takes the asHtml mode), so a failed LOAD / INSTALL
[for load] shows the same block analysis as the compatibility test:

- the failing block's CAP section (load-file byte range and components, and
  what completes there), with the JC VM spec 4.5.2 note - or 'the import
  list already passed' when the failure is later in the file;
- the remote status word with its name;
- the import list at the CAP's own versions (a real install runs no probe);
  the closing hint points at the compatibility test ('Set all 0.0' first,
  then bisect), while a probe run keeps the lower-the-versions wording.

ramInstallFailHint stays as the short inline hint; INSTALL [for install] and
/api/ram-install-app failures show no block (the LOAD guard in the helper).

sw cache simple-v326; tests: 550 Python / 704 Node.
2026-09-30 09:27:42 +03:00
catarrh ee747003f2 feat: CAP compatibility test in the popup + per-AID failure analysis (v3.6.52)
The compatibility test now runs in the same operation modal as the installs
(live progress from the server's ram_progress, Close disabled until done) and
its result gains a per-AID detail block instead of the comma list:

- the probe summary (probe: 2 imports -> 0.0 . +1 appended);
- the import list as tested (ramCompatImportRows): the CAP's imports with the
  version the card was asked for and the CAP's own version when the probe
  lowered it, appended synthetic entries and unmatched lines - each with the
  resolved library name (jcAidName), so a raw AID list becomes readable rows;
- on a rejection, the failure analysis (ramCompatFailureHtml): which CAP
  section the failing block covers (load-file byte range and component names,
  'completes Import'), what the card checks at that point (JC VM spec 4.5.2)
  or that the import list already passed, the remote status word with its
  name, and what the probe state implies (every import at 0.0 -> a missing
  package or another load-file check is the cause; otherwise lower the
  versions and bisect).

ramCompatVerdict names the remote SW; the inline result keeps the verdict +
short probe note and the modal carries the escaped step lines + the detail
HTML (ramOpModalFinish gained an asHtml mode); the server reports
probe_unmatched as [{'aid', 'version'}] (_probe_unmatched), documented in
docs/api.md.  The same failure block for the real install is the next step.

sw cache simple-v325; tests: 550 Python / 703 Node.
2026-09-30 09:22:22 +03:00
catarrh 599d84fd66 feat: Explore exposes the rest of the registry data + install NV footprint (v3.6.51)
The compact (legacy) GET STATUS entry is <aid_len><AID><life cycle>
<privileges> + (P1=10) <module count> (<len><module AID>)*, cross-checked
against GlobalPlatformPro's populate_legacy (the 4th byte is privileges;
0x00 is required for load files).  The parser read it as a 'version' one
byte late, so ELF rows showed the module count / the next entry's length as
Version: 00/10 - the compact listing has no version at all; it now comes
from the expanded CE tag (ramFmtVersion: 2 bytes -> major.minor, GP Table
11-37 Note 1) and merges (the bogus compact value no longer blocked it).

New data exposed:
- EA SCP Registry Data (TS 102 226 8.2.1.6) -> 80 Menu Parameters -> the
  applet's STK menu entries: STK menu entry: 0x80 (position 1, Enabled)
- unknown registry objects are preserved (rawTlvs) and shown as
  'Other registry data: 8F=752B . 90=012D ...' instead of being dropped
- 84 parses the GP coding (one Executable Module AID per TLV, repeated; the
  concatenated 4F<len><aid> form stays as fallback) and the ELF expanded
  queries add 84 to the tag list (with a tag list the card returns only the
  listed tags - the fallback path would otherwise lose the module lists and
  their Install buttons)
- life cycle names decode per object kind (GP Tables 11-3..11-6: ISD/card,
  app, SD - detected via the Security Domain privilege - and ELF), with
  multi-byte 9F70 (02 0700 on live cards) decoding the first byte and
  surfacing non-zero extras; the R-APDU parser's LIFECYCLE_MAP labels were
  aligned with the same tables

/api/ram-install measures the NV footprint: a silent GET DATA FF21 read
before and after the chain (also on failure) returns nv_before/nv_after/
nv_delta; the reads never fail the operation (no FF21 -> no fields) and the
PWA shows 'NV used: ~N B (free A -> B)' in the install result.

sw cache simple-v324; tests: 548 Python / 698 Node.
2026-09-30 09:00:33 +03:00
catarrh be411a7b4b feat: toolkit parameters in three columns + one access row (v3.6.50)
The SIM/UICC Toolkit parameters grid is grid-cols-3 now: the 12 narrow
fields (priority, timers, text length, menus, first pos/id, last pos/id,
channels, MSL, TAR, access domain) fit in four clean rows, Max services
keeps its own row (it was a lone half-row in the 2-column layout) and the
file-access group is one col-span-3 row: File system access (full) ·
ADF.USIM access (full) · ADF AID (hex) + From card.

The ADF AID cell is its own rc-tk-adfaid-row; the whole group is wrapped
in rc-tk-access-row, which updateRcTkMode hides in SIM/CA mode (hiding
only the old checkbox rows would leave the AID field behind).

grid-cols-3/col-span-3 were already compiled (no Tailwind rebuild); the
html test pins the 3-column grid and the spanning access row with its
three cells, the JS guard pins the CA/EA visibility.

sw cache simple-v323; tests: 540 Python / 689 Node.
2026-09-30 08:22:46 +03:00
catarrh 80c26f14f2 fix: CAP compatibility test review fixes (v3.6.49)
Review of v3.6.48 found six issues; the two that matter in use:

- ramProbeParse('') errored, so the basic 'is this CAP compatible with
  this card at all?' run (no overrides) was refused by an alert.  An empty
  list is valid now: the test runs with the CAP's own versions.
- ramCompatVerdict said 'Import list rejected' for every failure.  It now
  distinguishes: a request error (no steps), a failed format probe (the
  card not answering), an INSTALL [for load] refusal, and only then the
  import list - each with its own headline.

Plus:
- the result line shows the applied probe (ramCompatProbeNote: 'probe:
  7 imports -> 0.0 · +1 appended', unmatched AIDs included);
- #ram-compat-block validates 1..240 like the install's block size;
- the server reports probe_unmatched (override AIDs matching no entry,
  previously silent when another override matched) and logs them;
- tests: the endpoint's probe-400 path (a mini CAP + a dummy scc),
  _cap_import_aids, the verdict cases and the probe note.

sw cache simple-v322; tests: 540 Python / 687 Node.
2026-09-30 08:13:12 +03:00
catarrh 0dc79a3ba4 feat: CAP compatibility test as its own operation + append-only imports (v3.6.48)
The import probe no longer lives in 'Install Package (.cap file)': a
diagnostic that can leave a LOADED-only package does not belong in the
operation meant to produce a working applet.  /api/ram-install no longer
takes probe_imports; the install form's probe fieldset is gone.

New RAM operation CAP compatibility test (POST /api/cap-compat): the
server runs the format check, INSTALL [for load] and the LOAD blocks only
up to the block that completes the Import component (_cap_import_end +
_cap_compat_blocks, computed from the load file and the C4 TLV framing),
then stops - no last-block flag, no INSTALL [for install], so nothing is
committed and nothing needs deleting.  The verdict (imports_ok,
boundary_block/total_blocks) states it covers the LOAD/import gate only;
the link gate stays the real install's business.  Verified on the real
RemMobileID CAP: the boundary is LOAD 1/64, 2/128, 3/240 for 240/120/64 B
blocks - exactly the blocks where the live cards rejected the load.

_probe_import_versions(load_file, overrides, additions) now supports
append-only synthetic imports: +AID=version appends a new entry, the
Import component's size field and the Directory's Import size are patched,
and existing entries keep their positions (constant-pool import indices
stay valid); an AID that is already imported becomes a version override.
This is the card-capability query ('does this card export package X at
>= Y?') without building a probe applet.

PWA: new op section (CAP file + analysis + the version list + block size),
ramCompatTest with inline results and ramCompatVerdict, ramProbeParse
understands +AID=version; help EN/RU documents the operation, the two-step
bisect and the gate scope.

sw cache simple-v321; tests: 538 Python / 686 Node.
2026-09-30 07:57:11 +03:00
catarrh ef949e6a17 fix: cached GETs bypass the card lock; compact install step lines (v3.6.47)
The install modal's progress bar froze around 70% and jumped to 100% at
the end: not a math bug - the UI's other polls (/api/stk-status,
/api/poll-status, /api/proactive-log, /api/menu, ...) were card-locked in
do_GET even though they only read cached state, queued behind the install
and exhausted the browser's 6-connections-per-host budget, starving
/api/status.  _CARD_FREE_GET lists every card-free GET (status, version,
stk-status, poll-status, proactive-log, menu, events, terminal-profile,
pli-*, commands, net-state, mcc-mnc, scp81/bip status|script|log);
/api/cardinfo and /api/esim/* stay locked (they touch the card).

tests/test_fetch_routes.py holds _CARD_LOCK and asserts the cached
endpoints still answer - verified to time out on the pre-fix list.

The install modal is wider (max-w-3xl) and successful step lines are
compact: '✅ Step 3: LOAD (1/240) - 6101 · 274 B / 3 SMS' (no verbose SW
name, B instead of bytes, the SMS count only when >1); failed steps keep
the full detail and the hint.

sw cache simple-v320; tests: 533 Python / 684 Node.
2026-09-30 00:02:52 +03:00
catarrh cf9c9b7bab feat: clean up the Explore UX (v3.6.45)
- Explore runs inline: no blocking modal (that stays for the install
  operations, which can leave a half-loaded package; Explore leaves the
  card consistent).  A test guards that ramExplore never calls the modal.
- Minimal success line: just OK - the details live in the explorer below;
  error runs keep the detailed 'Partial - ...' line.
- Counts moved into the section headings (Applications / Applet Instances
  (n), Executable Load Files (ELFs) / Packages (n)); the redundant '1 ISD'
  count is gone (P1=80 returns the ISD as a single occurrence, GP
  11.4.2.1).
- The RAM command format line gained a tooltip: compact = TS 102 226
  5.2.1 command string, expanded = the AA/22 scripting template (5.2.1.0),
  detected per operation with a read-only GET STATUS probe.
- The expanded-registry checkbox is relabelled 'Fetch expanded registry
  details (load-file / SD associations)' with a tooltip (extra secured
  packets, card support varies, the fallback is automatic) and is now a
  sticky per-browser preference (simple_ram_expanded).

sw cache simple-v318; tests: 531 Python / 681 Node.
2026-09-29 23:34:42 +03:00
catarrh 755470e4ba feat: RAM-install import probe for LOAD rejections (v3.6.42)
Cards that cannot satisfy the CAP's import list reject the LOAD as soon
as the Import component is complete (live 2026-09-29: 6438/6985 in the
block containing its last byte, on several cards incl. an eUICC profile
whose ISD is not the default AID and which does not answer GET DATA 0066 -
no direct platform-version readout exists).

_probe_import_versions (pure) rewrites the Import component's two version
bytes per entry in the assembled load file: same length, component sizes,
Directory and offsets untouched; unknown AIDs and bad versions are
rejected.  /api/ram-install takes probe_imports ({'all': '0.0'} or
per-AID), echoes the applied changes in the response and logs them.  The
PWA's RAM install form gains a collapsed Import probe fieldset (checkbox,
AID=version lines prefilled from the CAP analysis, 'Set all 0.0') and the
result line reports the applied changes.  Diagnostic only - never used for
a real install.

Also verified _cap_parse's component order against the reference TCA load
trace (Header, Directory, Import, Applet, Class, Method, StaticField,
ConstantPool, RefLocation, Descriptor - Import before Applet, not
ascending tag order), so the order is correct and the import list remains
the prime suspect.

sw cache simple-v315; tests: 530 Python / 676 Node.
2026-09-29 03:41:07 +03:00
catarrh ddf5cbb8e7 fix: late-PoR handler on every path, one poll per packet, LOAD-step components (v3.6.37)
_send_envelope defined its SEND SHORT MESSAGE capture handler inside the
91xx branch, but the 'STATUS poll (PoR not captured)' path used it: a
packet whose ENVELOPE answered 9000 and whose late PoR arrived on the
STATUS poll raised UnboundLocalError, returned 500 and aborted the RAM
operation mid-packet with the card's command left pending (live
2026-09-29, 120-byte LOAD blocks).  The handler is now defined for every
path, and the late-PoR poll runs only after a packet's last segment
(poll_status from _send_secured_packet) - a concatenated download makes
no spurious mid-packet STATUS exchanges.

The PWA annotates every LOAD step with the CAP components its block
covers (ramStepComponents: 'completes Import' / 'inside Method' /
'Header...Import') and ramInstallFailHint names the component the failing
block completed - the live import-verification rejection lands in the
block completing Import (6438/6985 at both 240 B and 64 B blocks).

sw cache simple-v310; tests: 509 Python / 672 Node.
2026-09-29 01:14:37 +03:00
catarrh f007d5c9be feat: CAP platform requirement + LOAD rejection hint (v3.6.36)
capmem derives the minimum platform from the CAP's Import component
(javacard.framework -> Java Card level via the SDK-kit corpus, int flag)
and /api/cap-info exposes it; the CAP details show it as a Platform row
('Java Card >= 2.2.2 - CAP format 2.1 - int support').

A failed RAM install at a LOAD / INSTALL [for load] step now appends a
hint naming the import requirement and the smaller-block fallback
(ramInstallFailHint, pure): JC VM spec 4.5.2 makes every import a
'>= version' demand on the card's own packages checked while the load
file is verified - live 2026-09-29, a CAP requiring Java Card 2.2.2 was
rejected exactly at the block containing the end of Import (6438 at
240 B blocks, 6985 at 64 B), not at a step count.

sw cache simple-v309; tests: 507 Python / 670 Node.
2026-09-29 00:59:17 +03:00
catarrh 0b7b6b8157 feat: RAM operations run behind a blocking modal (v3.6.34)
A CAP install is a long chain of LOAD commands whose progress and results
were only visible at the bottom of a large form - and an interrupted
install can leave a half-loaded package.

- Server: _RAM_PROGRESS (+ _ram_progress_begin/step/end) records the live
  step of the running RAM operation; /api/ram-install and
  /api/ram-install-app wrap their chains in it (total = steps + 1 for the
  format check) and /api/status carries it as ram_progress - lock-free, so
  the 2 s poll works while the request holds _CARD_LOCK.
- PWA: #ram-op-modal (the STK panel pattern) blocks everything while a RAM
  operation runs: title, spinner, status line, progress bar, per-step
  result lines, Close disabled until it finishes; a beforeunload guard
  protects the window during the run.  install-cap, install-app /
  make-selectable and Explore all use the shared helpers
  (ramOpModalBegin/Status/Finish); the install results stay in the form
  area as before once the modal is closed.  The status poll feeds the modal
  via ramOpProgress (headline from the pure ramOpProgressText).
- Tests: ramOpProgressText (JS) + the progress lifecycle (Python); the
  card-state and capmem suites stub the DOM-bound hooks.
- Stylesheet rebuilt; version 3.6.34; sw simple-v307.
2026-09-29 00:09:35 +03:00
catarrh e820a7ed41 feat: toolkit block emphasised + auto CA/EA from the CAP (v3.6.32)
The RAM install form's toolkit parameters are the make-or-break field for
toolkit applets, and CA vs EA is easy to get wrong (the earlier live 6A80s).

- "SIM/UICC Toolkit parameters" is now an accented bordered fieldset (blue
  border/legend, always visible in the cap / install / make-selectable
  modes) with the checkbox in the legend and the fields inside.
- ramCapToolkitMode(imports) maps the CAP's linked libraries to the install
  parameter family: sim.* (AID tree A0000000090003FFFFFFFF89...) -> SIM
  Toolkit (Tag CA), uicc.* / uicc.usim.* (A0000000090005FFFFFFFF89...,
  A0000000871005FFFFFFFF89...) -> UICC Toolkit (Tag EA) - family-based, so
  access-only applets get the right field too.  Applied when the block is
  enabled and when the CAP is swapped while it is on, with a "detected from
  the CAP" hint under the select; no CAP keeps the manual choice.
- Rebuilt style.css for the new accent classes; tests pin the fieldset and
  the family mapping.

663 frontend / 504 Python green; version 3.6.32; sw simple-v305.
2026-09-28 23:49:00 +03:00
catarrh c3117e90fa fix: rebuilt stylesheet + visible explorer rows (v3.6.30)
The compiled Tailwind stylesheet (frontend/style.css) had been stale since
2026-09-22: classes introduced later were missing from it, so the Install /
Make selectable chips had no background or text colour in light mode (dark
mode only looked green because an older dark:text-emerald-400 rule matched)
and the new w-28 action column did not apply.

- Rebuilt with `npm run build` (Tailwind + appended src/contrast.css);
  verified the previously missing classes (bg-emerald-100, text-emerald-700,
  hover:bg-emerald-200, bg-red-100, bg-blue-100, w-28, ...).
- Explorer rows are visible chips now: rounded bg-gray-50 dark:bg-slate-800/60
  with a hover:bg-gray-100 highlight and the colored left accent kept (module
  rows a subtler variant), so a right-column button is unambiguously tied to
  its row; ISD/app/ELF/module rows all use the same treatment.
- AGENTS working conventions: an index.html change that introduces new
  utility classes also requires `npm run build` in frontend/.

660 frontend / 504 Python green; version 3.6.30; sw simple-v303.
2026-09-28 23:38:06 +03:00
catarrh 3b5a1495f5 feat: table-align the explorer action buttons, Install green (v3.6.29)
The explorer buttons sat inline after each AID/detail line, so their columns
moved with the text length.  Now every entry (app / ELF) and every module
line is a row with a fixed-width action column (w-28):

- left cell: the text (module rows are siblings of their ELF entry, indented
  pl-6, so their buttons share the same column as the entry buttons);
- right cell: the buttons, full width of the column, so Delete / Delete All /
  Make selectable / Install line up vertically across rows.

All buttons go through ramActionBtn(label, action, tone): Danger (red) for
Delete / Delete All, Ok (emerald) for Make selectable and the **Install**
buttons, which were blue.  Test pins the column width and the Install tone.

660 frontend / 504 Python green; version 3.6.29; sw simple-v302.
2026-09-28 23:29:55 +03:00
catarrh 90fe409dec fix: correct the GP privilege decode and remove the duplicate (v3.6.28)
The RAM explorer's ramFmtPrivileges used a private table with wrong bits and
invented names ("Cum Deletion Ctr", "GSM Card Binding", "Global PIN",
"RFM"/"CFM") and no case for 0x80: a live ISD 9E printed four wrong names
and dropped Security Domain.  Per GP Card Spec v2.3.1 11.1.2 Tables
11-7/8/9, 9E = Security Domain, Card Lock, Card Terminate, Card Reset, CVM
Management.

- ramFmtPrivileges now delegates to decodePrivileges (one spec table).
- decodePrivileges decodes byte 1's DAP pair per Table 11-7: b8+b7 = DAP
  Verification, b8+b7+b1 = Mandated DAP Verification; the flag bits alone
  are no privilege.
- The file had two decodePrivileges copies (the runtime used the later one,
  the C-APDU parser test the earlier); the duplicate and its private table
  are gone - the single implementation plus PRIVILEGE_NAMES live in the
  parser section, which the test evaluates (converted to var like PARSE_INS).
- Tests: 9E/9A live values, the DAP pair, byte 2/3 spot checks; the
  PRIVILEGE_NAMES spec pin moved to apdu_parse.

659 frontend / 504 Python green; version 3.6.28; sw simple-v301.
2026-09-28 23:29:01 +03:00
catarrh 85ba2a709d fix: explain the FF21 resource block and move the format line last (v3.6.27)
The explorer's FF21 block was titled "Memory (GET DATA FF21)" although the
tag is the Extended Card Resources Information (GP tag FF21, TS 102 226
8.2.1.7.2: installed applications, free non-volatile and free volatile
memory), and the detected RAM command format line sat between the Free NV
and Free Volatile lines.

- Heading is now "Card resources (GET DATA FF21)" with a tooltip naming the
  tag and its three fields (EN/RU).
- The "RAM command format:" line moved after the resource lines.
- Help EN/RU spells out what FF21 returns; test asserts the format line
  follows the resource lines.

657 frontend / 504 Python green; version 3.6.27; sw simple-v300.
2026-09-28 23:18:00 +03:00
catarrh 7c81f02308 feat: Explore expanded (E3) listing fallback + details pass + format display (v3.6.25)
The E3 parsers existed since the vendor-trace work; what was missing was
issuing the query.  Per GP 11.4.2.2 the compact listing format (P2.b2=0) is
deprecated and a card may reject it, while the expanded form (P2=02 + the
tag list 4F 9F70 C5 C4 CC CE EA) returns the richer registry data.

- ramExpandedQueryApdu(p1): the reference trace bytes
  (80F2 <p1> 02 0C 4F005C084F9F70C5C4CCCEEA 00), pinned in a test.
- Explore: when the compact pass for a P1 yields nothing, the query is
  retried in the expanded format and the result becomes the listing (the
  compact error only surfaces if the retry also fails); the new "Query
  expanded registry details (E3)" checkbox runs the expanded pass on top of
  a working compact listing and merges the expanded-only fields
  (privileges, ELF AID C4, SD AID CC, module lists) into the entries.
- The detected RAM command format (v3.6.24 ram_format) is now shown in the
  Explore result + explorer view, the delete verdict and the plain-send
  message.
- Tests parse the real trace payloads (NC ISD, ET application, NP ELF E3
  templates); help EN/RU updated.

656 frontend / 504 Python green; version 3.6.25; sw simple-v298.
2026-09-28 22:51:20 +03:00
catarrh 5372a770eb fix: keep a hand-edited packet TAR in the SCP80 send (v3.6.21)
The plain "Send to Card" path refreshed the whole preset right before
sending (spRefreshFromPreset -> cardsApply -> cardsApplyFields) and
re-applied the preset's slot TAR - so a TAR typed for a push/link trigger,
an applet OTA message or the HTTP OTA admin params was silently replaced by
the preset's ISD TAR (the pack buttons for those sources deliberately leave
the TAR manual; the help even said so).

- spRefreshFromPreset(selId, keepTar): the plain send passes keepTar, so
  the form's TAR survives; the counter, keys and SPI are still re-read.
- selecting a card preset, ramApplyCard and the chain packs still set the
  TAR; the RAM operations keep the preset's ISD TAR by design.
- spParams()/getRamSpParams() normalise the TAR (strip/upper/pad to 6);
  only the preview padded before, a short TAR went to the server raw.
- help EN/RU clarified, cards_counter/ram tests updated, sw simple-v294.

652 frontend / 499 Python green; version 3.6.21.
2026-09-28 21:43:57 +03:00
catarrh face2268ca feat: single-APDU INSTALL [for install] / [for make selectable] ops (v3.6.18)
Iterating the final install step no longer needs a delete + re-load of the
.cap:

- `POST /api/ram-install-app` runs INSTALL [for install] (`mode: "install"`,
  P1 0x0C/0x04 by make_selectable) or INSTALL [for make selectable]
  (`mode: "make_selectable"`, GP 11.5.2.3.3 Table 11-44) for an already
  loaded package.  The APDU builders are shared with `_cap_apdu_sequence`
  (`_cap_install_apdu`, `_cap_make_selectable_apdu` - one builder, no drift)
  and the per-step send/PoR/counter logic is the extracted shared
  `_ram_send_gp_apdu` (used by /api/ram-install too).
- PWA: the RAM op selector gains "INSTALL [for install] (loaded package)"
  and "INSTALL [for make selectable]" with Load File / Module / Instance AID
  inputs (instance defaults to the module AID); the result renders through
  the shared `ramShowInstallResult` and `final_cntr` keeps the preset in
  sync.
- Explore: an **Install** button on a package's module AIDs that have no
  instance yet, and a **Make selectable** button on INSTALLED (0x03)
  instances - both switch the op and pre-fill the AIDs.
- tests: the install APDU pinned to the live no-STK packet and to the
  sequence tail, the make-selectable APDU, ramHasInstance, the Explore
  buttons and the op wiring; docs/api.md updated.

650 frontend / 498 Python green; version 3.6.18; sw simple-v291.
2026-09-28 08:10:40 +03:00
catarrh bab787fac1 fix: DELETE APDU is case 3 - no phantom command (v3.6.15)
The Explore delete showed "Failed: por_ok - remote SW 6700" while the delete
executed: ramDeleteApdu appended the trailing Le, and this card's SCP80
layer counts a trailing byte as a phantom second command whose SW 6700
masked the real result (count=2, last SW 6700) - the same quirk as
INSTALL [for load] before v3.6.8.

- ramDeleteApdu and the chain builder's DELETE row emit the case-3 APDU
  (4F AID TLV, no Le); the SCP81 Delete-AID template follows (it reuses
  ramDeleteApdu).
- tests updated (delete APDU expectations in ram/ram_counter_flow/scripts).

641 frontend / 496 Python green; version 3.6.15; sw simple-v288.
2026-09-28 03:16:10 +03:00
catarrh 138760f75c fix: Explore delete - real SPI2, no-PoR verdict, local removal (v3.6.13)
"Failed: 9000" although the delete executed: the RAM helpers sent
`sp-spi2` - the base SPI2 select, whose default is "00 - No PoR" - so the
card answered the envelope 9000 with no PoR and the verdict treated the
missing PoR as a failure.  `cardsApplyFields` also set `sp-spi2-hex` from
the preset and then `updateSp()` recomputed it from the selects (which the
preset never updated), clobbering the preset byte for anything reading it.

- `getRamSpParams()` reads the computed `sp-spi2-hex`; `cardsApplyFields`
  syncs the SPI2 selects from the preset byte (base = low 5 bits, bit 0x20
  = PoR via SMS-SUBMIT, dynamic option for unlisted bases) and the base
  select gains the missing cipher + RC/CC/DS combinations (15/19/1D).
- a missing PoR is no longer a failure (the SPI may request none, and the
  card sometimes refuses one): `OK` with the SW, or `OK (no PoR)`.
- a successful delete drops the object from the Explore result locally (no
  re-explore): an applet row goes away; a cascade package delete also drops
  the applets whose AID starts with the package AID.  The consumed counter
  is still saved.
- tests: ramRemoveFromExplorer (app/cascade/prefix), the no-PoR flow, the
  SPI2 source guard, and the updated delete-flow stubs.

641 frontend / 496 Python green; version 3.6.13; sw simple-v286.
2026-09-28 03:03:53 +03:00
catarrh a0704a8fd0 fix: Explore Apps listing uses the SMS-submit PoR transport (v3.6.10)
The Apps query (P1=40) was sent with SPI2=0x01 while only the ELF queries
(P1=20/10) used SPI2=0x21.  The Apps listing can exceed the ENVELOPE
response, and the card then answers the envelope PoR with
`actual_response_sms_submit` - the actual response would follow as an
SMS-SUBMIT, which is never sent unless the PoR is requested in submit mode.
The paginate only accepted `por_ok`, so the Explore reported the raw status
as an error ("Partial - Apps: actual_response_sms_submit") and the Apps list
never rendered (live 2026-09-28).

- `ramListingSpi2(p1)`: 40/20/10 -> '21', else '01'; the paginate uses it.
- the paginate accepts `actual_response_sms_submit` via `spPorAccepted` and,
  when no data arrived, notes "response via SMS not captured" instead of
  reporting the card status as a failure.
- the Explore delete verdict follows the same acceptance rule (an accepted
  SMS-submit delete no longer shows "Failed" while its counter advanced) and
  no longer prints a dangling " -> " without a SW.
- tests: ramListingSpi2 + the delete-accepted-via-sms-submit flow.

633 frontend / 496 Python green; version 3.6.10; sw simple-v283.
2026-09-28 02:13:13 +03:00
catarrh 9fb2ad5d2b fix: send the STK install parameters as entered (v3.6.9)
The RAM install form sent the `STK parameters (hex)` field, which was only
regenerated when the toolkit checkbox or the mode select changed - editing
the TAR (or any other toolkit field) afterwards was silently dropped.  The
live install therefore carried the form defaults (TAR B00001, textLen 0,
menus 0, MSL 16, channels 0) instead of the entered AF4D01 / MSL 12 /
channels 1, and the card rejected the install parameters with 6A80
(TS 102 226 8.2.1.3.2.7: a TAR already assigned on the card).

- every `rc-tk-*` field regenerates the hex on input/change; the install
  recomputes it at send time unless the user hand-edited the hex
  (`dataset.manual`), so the edit order can no longer drop values;
- one pure `stkParamsBuild` serves the RAM form (`buildRcToolkitParams`) and
  the RAM/GP chain rows (`buildTkParams`) - no drift between them (the chain
  row also gains the menu-ID <= 7F check);
- applet TAR field: empty by default, no placeholder - B0 00 01 is the
  allocated ADF RFM TAR (TS 101 220 Annex D), not an applet TAR; an empty
  field is coded as TAR length 00 (the card may take a TAR from the AID only
  when the AID carries one, PIX hex digits 15-20);
- the chain builder's INSTALL/LOAD rows drop the trailing Le (the v3.6.8
  server form; a trailing Le made the card execute a phantom command);
- tests: stk_params.test.js (the decrypted live parameters, empty-TAR
  coding, CA wrapper, long-form lengths, rejection cases, form wiring, and
  the form -> hex path with the real builders) and ram_counter_flow.test.js
  (the Explore delete persists the consumed counter through the real
  ramSaveCntr); STK fixtures no longer use B00001.

631 frontend / 496 Python green; version 3.6.9; sw simple-v282.
2026-09-28 01:49:35 +03:00
catarrh 50fa8744fd fix: keep the preset counter intact through the Explore delete flow (v3.6.7)
Delete All worked but reset the preset counter: the handler never re-read
the preset (unlike ramExecute), saved N+1, then re-ran the Explore with the
OLD sp (counter N).  Every page answered cntr_low, nothing was accepted, and
ramExplore's final ramSaveCntr wrote N back over the preset.

- ramDeleteFromExplorer re-reads the preset first (spRefreshFromPreset
  'ram-card-sel'), advances and saves the counter only for a packet the card
  accepted (spPorAccepted) - a rejected packet leaves it untouched - and
  continues the follow-up Explore from the consumed counter instead of
  replaying it.
- the delete result also checks the remote command's SW via a new
  `ramRemoteSwOk` (9000 / 61xx / 62xx / 63xx / CAFE, mirroring the server's
  `_ram_remote_sw_ok`): a por_ok packet whose DELETE was refused (e.g. 6A88)
  no longer shows "OK" (the counter still advances, the card consumed it).
- ramExplore only saves the counter when it advanced, so a stale caller can
  never lower a preset counter again.
- tests: flow tests for accepted / rejected / refused-remote-SW deletes
  (refresh order, saved counter, the counter passed to the re-explore) and
  the ramRemoteSwOk set.

615 frontend / 495 Python green; version 3.6.7; sw simple-v280.
2026-09-28 00:50:07 +03:00
catarrh c39250f1b4 fix: make the Explore Delete buttons send the GP DELETE APDU (v3.6.6)
ramDeleteFromExplorer() called an undefined `_ber_len()` helper, so clicking
Delete / Delete All raised a ReferenceError right after the confirm and no
APDU was ever sent.  The inline builder also omitted the mandatory Le byte.

- new pure `ramDeleteApdu(aid, withCascade)` = GP Card Spec v2.3.1 Table
  11-20/23 form: `80 E4 00 <p2> <Lc> 4F <len> <AID> 00` (P2 00 = object,
  80 = object and related objects), used by the Explore handler.
- `scp81DeleteApdus()` (the "Delete AID" script template) now shares the
  same builder: it used to send the raw AID without the mandatory '4F' TLV.
- tests: exact bytes for 7/16-byte AIDs and both P2 modes, the SCP81 script
  expectations updated to the TLV form, and a wiring check that the undefined
  helper call does not come back (`_ber_len(`).

611 frontend / 495 Python green; version 3.6.6; sw simple-v279.
2026-09-28 00:42:37 +03:00