Two reported SCP80/RAM issues, both confirmed against the specs:
- docs/api.md called `stk_params` a "Hex CA TLV", but TS 102 226 8.2.1.3.2.1
puts the CA *inside* the System Specific Parameters EF ('EF'), and the PWA
builds `EF <len> CA <len> ...` (the SIM access domain leads the CA value as
`01 <ad>` / `00`). A client following the doc sent a CA at the wrong level.
- the RAM composition appended the STK part after a caller-composed
C9+EF(quotas) prefix, producing TWO EF objects. The card read the first
(the quotas) and ignored the CA: the install was accepted (por_ok) but the
toolkit parameters were never registered, so no STK menu appeared (live
2026-10-05). Per GPD_SPE_013 v1.1 Table 6-5 (pySim's
gen_install_parameters) the quotas (C7/C8) and the CA share ONE EF; an
EA-form STK part is a sibling of EF (the reference TCA loader's
`C9 00 EF 00 EA ...`).
- server: `_compose_install_params()` (used by `_cap_install_apdu` ->
`_cap_apdu_sequence`, hence /api/ram-install, /api/ram-install-app,
/api/scp81/gen-install and the PWA's install script template) enforces the
rule: a caller-composed EF plus an EF-form `stk_params`, a bare CA, a
malformed EF and quotas next to a non-empty `install_params` are refused
(400 / `{"ok": false}` with the fix named); with an empty `install_params`
the quotas and an EF-form CA are composed into one EF
(`C900 EF{C7?,C8?,CA}`), an EA part stays a sibling.
- the documented `nv_quota`/`volatile_quota` fields are now real (they were
named in the docs but implemented nowhere): integers composing C8/C7.
- PWA: `composeRamInstallParams()` composes the same single EF itself (a
CA-form STK field takes the quotas into its EF, the merged EF travels in
`stk_params`), the RAM install bodies send it, the placeholder shows it
with a note when the quotas moved into the STK EF, and a refused request
(no steps) reports the error instead of "FAILED at step: undefined".
- docs/api.md: the corrected `stk_params` description with both accepted
shapes, the composition rule paragraph with the refusal cases, the fixed
example (`C90000` -> `C900`), and the /api/ram-install-app and
/api/scp81/gen-install cross-references.
- tests: composition unit tests (merge/refusals/EA-sibling) and the
ram-install-app 400 paths (Python); `ramQuotaTlvs`/`composeRamInstallParams`/
`ramInstallParamsCompose` pins incl. the CA merge (Node).
Tests: 824 frontend + 740 Python; sw cache simple-v395.
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.
pySim's CompactRemoteResp was applied to every response packet, so a
third-party applet's own TAR response parsed as count=128/SW=0110/data=''
(live 2026-10-04: the alfa-dsa applet's `80 01 10`) - a fabricated status
word and a lost response.
_decode_por now takes the plaintext command script's length (cmd_len): the
compact structure's command count cannot exceed it (TS 102 226 5.1.2
Table 5.1 - each command needs at least one byte), so a larger count is
classified `response_type: 'raw'`, the secured data becomes
`decoded.last_response_data` (a por.data check matches it) and no SW is
decoded; `secured_data` is exposed in every case.
Callers pass cmd_len where they know the plaintext: the test runner's scp80
step (None for a pre-built packet; carried in ctx['last_scp80'] for the
submit-PoR path), /api/send-ota (both paths), the RAM step sender and the
counter probe; a caller without it keeps the previous parse.
Docs: test-scripts.md and api.md describe the raw fallback.
Tests: 821 frontend + 729 Python; sw cache simple-v390.
The review of v3.18.3 found the event steps logging twice: the shared sender
writes its generic `ENVELOPE(Event Download): type=... data=...` /
`ENVELOPE SW: ...` pair, and the runner's step-annotated line repeated the
same data. `_send_event_download` gained `log=True`; the runner's envelope
and event branches pass `log=False`, so an event step logs exactly one line -
the annotated one, which already carries the step number, the event name,
the effective source and the response SW. All other callers keep their
output unchanged (netsim, the interactive event form, the BIP/timer paths).
The review's test-pinning gaps are closed:
- the shared sender's per-segment answer line
(`OTA SEND: ENVELOPE i/N -> <SW>`) is asserted in the multi-segment test;
- the scp80 step's report `sent` C-APDU/SP is asserted in the run-log test;
- the single-ENVELOPE-line behaviour (and the source override) is asserted
in the event-action test.
Tests: 816 frontend + 718 Python; sw cache simple-v386.
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.
_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.
An audit of every event form against the pinned specs (31.111 7.5.x/9.3,
102 223 8.87, TS 101 220's master tag table) found three missing objects,
three malformed/wrong ones, and settled the slice-data scope.
- 0x00 MT call: the IMS URI object is cut at 100 bytes and now carries the
URI truncated object (8.135, F3 00) when the cut happened.
- 0x1F Slices: the form gains the Partial NSSAI object (8.154, F9, rel 18);
a single envelope carrying it is marked as the Last Envelope (8.79,
F0 00) automatically (7.5.27.2).
- 0x06 Card reader status and 0x07 Language: the builders now emit their
object tags (A0 01 ..., AD ...) - they emitted bare values before.
- 0x18 Incoming IMS data: the IARI tag is F6 (76/F6), not F7 (which is the
IMPU list tag - the two-column 9.3 table had been misread).
- Single-envelope mandate: the event data is capped at 244 bytes (inner 252;
the D6 long-form length and the APDU Lc stay in one byte). The server
refuses more with a 400 and the PWA shows the size error before sending.
Chained (multi-envelope) delivery, the More Data object and envelope
sequences are not implemented - documented in the help and the repo AGENTS.
- Tests: event form vectors (URI truncation, the restored tags, partial /
Last Envelope, the size helper) and the server guard; the release
reference and its PWA/code mirrors gain the three objects and the
corrected IARI key.
- sw cache simple-v375.
- /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.
Presets drop the card-wide SPI1/SPI2 and the fixed tar/uiccTar/usimTar
fields: a preset now carries `tars` - the three mandatory role entries
(isd/uiccRfm/usimRfm, spec defaults pre-settled) plus free TARs with an
optional description, each with its MSL (Minimum SPI1, TS 102 226
8.2.1.3.2.4). v3.9.1 presets convert on load/import (the spi1 seeds all
three MSLs); duplicate TARs and empty TAR/MSL values are refused; the
store schema version is 3.
Operations derive a packet's SPI1 from its TAR's MSL (_spi1_for_tar): the
RAM ops, the test runner (per-step overrides still win), the counter probe
and each TAR-probe row; the plain send keeps an explicit SPI1 with a
`warning` when it is below the MSL, and a TAR outside the preset without an
explicit SPI1 is refused with a hint. The TAR probe resolves per checked
TAR before sending and reports the effective spi1 per row. The secured
packet form's SPI2 default is 00 (the RAM/probe operations pin their own).
PWA: the Cards form edits the TAR table (role rows with MSL inputs plus
Add/Remove free rows with a description), the list shows a TARs (MSL)
column, the SCP80 TAR field suggests the preset's entries (datalist) and a
preset apply seeds the SPI1 from the operation TAR's MSL with an amber
warning below it; the preset-wide SPI1/SPI2 fields and the header marker's
SPI requirement are gone (completeness = a keyset + the three role entries
with their MSLs). Help EN/RU, docs/api.md and AGENTS updated; sw cache
simple-v358.
Live: TAR probe 000000 at its MSL 16 (registered, por_ok, counter
advanced); a send without spi1 derived 16 (por_ok, counter advanced); a
send with spi1 0A returned the warning and the card refused it (6200, no
counter consumed); an unknown TAR without spi1 was refused with the hint.
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.
Live finding (Network rejection events): the card asks the terminal for the
current date and time (PROVIDE LOCAL INFORMATION, qualifier 03) and our TR
answered "performed successfully" with no data. TS 102 223 6.4.15 says the
terminal shall return the current date and time, and the 6.8.0 TR structure
requires the Date-Time and Time zone object (8.39) for that qualifier.
- _pli_data_hex(qual): the TR Config dictionary entry wins; when it is empty
the date/time qualifier is answered from the host clock via _encode_scts
(the SMS timestamp builder, TP-SCTS coding).
- _build_tr and _DefaultProactiveHandler put the command-specific data objects
after the Result TLV per the 6.8.0 order (the POLL INTERVAL Duration echo
too; real terminal responses pin the order: 8103010300 02028281 030100
0402011E).
- _decode_tr decodes qualifier 03 with the same 8.39 coding (swapped
semi-octet BCD digits, time-zone sign bit 0x08, 'FF' = unknown); it read
plain decimal bytes and a 0x80 sign before.
- The PWA PLI dictionary codec for the date/time entry writes the same coding
(it wrote plain hex bytes and a 0x80 sign), and the TR Config panel notes
the empty-entry default. sw cache simple-v351.
Tests: builder vectors (east/west/unknown zone) + dictionary override + the
empty non-date/time qualifier + the object order (PLI and POLL INTERVAL) + the
handler path + the TR decode + a frontend codec round-trip test.
MT call, Call connected, Call disconnected, Browser termination, (I-)WLAN
access status and CSG cell selection no longer fall back to the generic
"send a bare notification" prompt:
- MT call (TS 31.111 7.5.1A.2): transaction identifier, calling party number
+ TON -> Address (EFADN BCD coding, encBcdDigits/eventAddressTlv),
subaddress, IMS URI, media type; source network.
- Call connected (7.5.2A.2) / Call disconnected (7.5.3A.2): transaction
identifier, near/far-end source select, media type, and a cause - absent,
the zero-length radio-link-timeout form or custom bytes (the hex field shows
only for "custom").
- Browser termination (TS 102 223 7.5.9.2): user / error cause.
- (I-)WLAN access status (TS 31.111 7.5.1.2): no coverage / coverage /
connection on-going.
- CSG cell selection (TS 31.111 7.5.3.2): status + selection mechanism,
optional access technology, CSG ID, HNB name and PLMN ID; source network.
The device-identities source (TS 102 223 8.7: 82 terminal / 83 network) is now
selectable: EVENT_FORMS carries a fixed `src` or a `srcField`, and
_send_event_download()/`/api/event-send` take an `event_src` override (an
invalid value falls back to the terminal).
Tests: the BCD/Address builders, each event's build vectors, the "no not_yet
left" guard and the server source override. Help EN/RU, both AGENTS files;
sw cache simple-v348.
EVENT_FORMS[0x1D] was missing, so its Send button posted event_data: null -
the ENVELOPE carried only the event list and device identities, none of the
event's data objects. The form now builds the TS 31.111 7.5.25.2 object set
(tags per TS 101 220 / TS 31.111 9.3): data connection status (9D), connection
type (AA), optional (E/5G)SM cause (AE), transaction identifier (1C), optional
location information (13, CR flag 0 as the spec requires for this command),
optional access technology (BF), location status (9B), optional APN (C7) and
PDP/PDN/PDU type (0B).
Adjacent spec fixes found while verifying:
- the Access Technology Change options (0x0B) did not match the pinned
TS 102 223 V18.3.0 8.61 coding (GSM Compact / GSM w-EGPRS / UTRAN w-HSDPA
...); they now use the shared ACCESSTECH list, whose two labels were also
corrected (TIA/EIA-553-A, TIA/EIA-136-270);
- _send_event_download() wrote the D6 length as a single byte - a long APN
would overflow it; 128..255 bytes now use the BER long form (81 <len>).
Tests: the 0x1D build (defaults, full form, fallbacks), the 8.61 option list,
the long-form D6. Help EN/RU and both AGENTS files; sw cache simple-v342.
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.
Three things learned from probing a live card, plus the capability they enabled:
1. Counter tracking follows SPI1.b5b4 (TS 102 225 5.1.1): with 00 the counter
is "present, ignored, never updated", so a packet without a counter check
must leave every counter untouched. _counter_tracked() now guards
/api/send-ota (no final_cntr, no persistence, a log line), the RAM step
sender and the test runner; _preset_counter_persist() requires a non-zero
key version (the "single keyset" fallback could advance a keyset spuriously
for a keyless send). The PWA advances the form counter only when the server
reports final_cntr and says "counter not tracked" otherwise.
2. The spec's no-security form builds again: _ota_keyset() refused the valid
KIc/KID '00' (TS 102 225 A.2: '00' is legal with SPI1.b3=0 for KIc and
SPI1.b2b1=00 for KID) because pySim wants an algorithm. An unknown nibble
is now refused only when the algorithm is actually used (ciphering / a
ciphered PoR for KIc, RC/CC/DS / a MAC'd PoR for KID) and 'implicit' codes
the nibble as 0 otherwise - POST /api/send-ota {apdu, spi1:"00", kic:"00",
kid:"00"} now builds 0D00010000B0000000000000010000A40000, the exact packet
the card accepted (a fake 15/15 was refused with 6200). A no-security PoR
is decodable again too, and a requested-but-missing PoR sets por_missing
on the plain path as well.
3. TAR probe (POST /api/tar-probe, RAM op "Probe TARs"): one secured packet per
TAR - the standard allocations of TS 101 220 V18.3.0 Annex D, Table D.1
(TAR_PROBE_TARS), or a custom list - carrying a harmless C-APDU (SELECT MF)
with the preset's keyset and SPI 16/01. Each TAR is classified
(registered / no_answer / no_por / refused) from the ENVELOPE verdict and
the application's PoR, counters advance and persist normally, and the run is
refused when SPI1 has no counter check. Live on a card: 000000, B00000 and
B00001 registered (compact-format applications, answering 6D00 / 6B00),
the expanded-format TARs, USAT, multiplexing and CASD refused with 6200 -
with the keyless form the same card accepted every TAR and answered no PoR.
Docs (api.md, help EN/RU, AGENTS), sw cache simple-v337. Tests: 618 Python
(no-security build, the algorithm-only-when-used rule, the tracking rule, the
persist guard, the probe classification) and 736 Node (the probe renderer and
wiring, spCounterTracked).
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.
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.
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.
The TERMINAL PROFILE chain parsed the SET UP MENU title with 'tag == 0x05'
only, so the live Alfa card's title (CR-set 0x85, 'Alfa Mobile') was
dropped: /api/menu returned items without a title and the PWA showed
'No menu set by the card' - card-specific only because cards pick the tag
variant (this one sends plain 0x8F items and a CR-set title in the same
command).
- _parse_setup_menu_command (pure, tested with the live FETCH bytes)
replaces the inline parse; the PWA shows a title-less menu as 'STK: Menu'
instead of 'No menu set by the card' (stkMenuLabel, pure).
- _cmd_tlv is now the single lookup helper: exact tag, then tag ^ 0x80
(bit 8 is the CR flag, TS 101 220 7.1.1); the old '| 0x80'-only helper and
the duplicate _cmd_tlv_any are gone.
- Remaining single-style checks fixed: _parse_setup_menu_items (0x8F/0x0F),
_parse_select_item title (0x85/0x05), _find_sms_tpdu (0x8B/0x0B - the
plain variant lost the SEND SHORT MESSAGE PoR TPDU), the response
scripting parsers (R-APDU 0x23/0xA3, executed count 0x80/0x00) and the
FF21 resource tags (0x81-0x83/0x01-0x03).
- tests/test_tag_variants.py flips the CR flag on every top-level tag of
real command fixtures and asserts identical parse results for all
parsers (verified to fail when a single-style lookup is reintroduced).
sw cache simple-v313; tests: 524 Python / 675 Node.
The menu chain paused only for DISPLAY TEXT / SELECT ITEM / SET UP MENU,
so a GET INPUT was auto-answered with a bare success TR and no Text
string - the live RemMobileID PIN prompt got an empty answer.
Server: _parse_get_inkey/_parse_get_input read the prompt, qualifier
flags (digits-only, UCS2, Yes/No, immediate, hidden, packed, help),
Response length 91 and Default Text 9D; the fetch handler pauses with
them; _menu_send_response(..., text=) validates the answer (digits set,
min/max, one char for GET INKEY) and builds the Text string TLV 8D in the
requested coding (SMS unpacked 04 / UCS2 08 / packed 00 via _pack_gsm7),
Yes/No as 01/00, empty input as the null text string, help as result 13
and the variable-timeout Duration echoed (6.8.4). /api/menu-respond
takes text; /api/stk-status carries pending_data so a card-initiated
prompt opens the panel; _decode_cmd decodes both commands.
PWA: stkInputHtml/stkInputHint/stkInputValidate render the constrained
panel (password field for hidden entry, numeric keyboard, default text,
Yes/No buttons, help, immediate-digit auto-submit); OK submits through
stkMenuOk; the poll opens the panel for card-initiated prompts.
sw cache simple-v311; tests: 517 Python / 674 Node.
_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.
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.
Comparing the new TCA vendor traces (Explore_EP/ET/NC/NP/SE) with our flow
showed why only one card works:
- The PoR capture was gated on SPI2 bit 0x20. The traced cards deliver the
PoR as a proactive SEND SHORT MESSAGE; with our ISD/memory queries
(SPI2=0x01) the fetched TPDU was thrown away - the Explore died with
"no data" and the RAM install steps silently degraded to an unverified
"no_por". The capture is now unconditional (inline ENVELOPE responses and
proactive submits both work), and the STATUS-poll fallback is enabled for
every card.
- `_ram_next_cntr` still wrapped at 32 bits (`% (2 ** 32)`): a 10-digit
counter like 10000AAAC8 came back as 00000AAAC9 after a RAM/Explore step -
the server-side half of the v3.6.23 counter report. It now advances the
full 40 bits.
- RAM command format (TS 102 226 5.2.1: compact C-APDU vs the expanded
`AA` > `22` form) is detected per operation and never stored:
/api/ram-install and /api/ram-install-app always probe, /api/send-ota
probes when the caller passes `ram_format`. The probe is a read-only
GET STATUS [ISD] as the first step (compact, then wrapped) and every
command of the operation uses the detected format. `_wrap_expanded_apdu`
is pinned to the reference bytes `AA0A220880F24000024F0000`; responses
carry `ram_format` and `final_cntr` (accepted probes consume counters, so
the views use the server's next counter). Applet-directed payloads stay
byte-exact; only RAM views (and RAM/GP chain packs) opt in.
504 Python / 653 frontend green; version 3.6.24; sw simple-v297.
`_ram_send_gp_apdu` (the shared step sender of /api/ram-install and
/api/ram-install-app) now logs every step's plaintext APDU and packed
packet, like /api/send-ota's unconditional lines:
RAM C-APDU (INSTALL [for load]): 80E602000C07F0414C4641610100000000
RAM SECURED-PACKET (INSTALL [for load]): 00281516212525000000...
No flag - the step name keeps the sequence readable, and --apdu-trace stays
the card-level (ENVELOPE) trace. Tests: the logging pinned with stubbed
packet builders.
498 Python / 650 frontend green; version 3.6.19; sw simple-v292.
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.
INSTALL [for load]/LOAD/INSTALL [for install] carried a trailing Le and an
explicit ISD AID in the Security Domain field; the card executed the extra
byte as a second (phantom) command, so the compact response reported count=2
with SW 6700 - which the v3.6.2 remote-SW check correctly treated as a
failure, aborting at step 1.
Decrypted from the live trace (KIc/KID 25/25, 3DES): ours was
80E6020014 07F0414C46416101 08A000000003000000 00 00 00 00
the reference tool sends
80E602000C 07<aid> 00 00 00 00 (empty SD, no Le)
and its response is count=1 / 9000 while ours was count=2 / 6700.
- `_cap_apdu_sequence`: the SD AID is only sent when a custom one was
supplied (empty -> '00', the card defaults to the ISD; GP 11.5.2.3.1
Table 11-42) and all three RAM install APDUs are case 3 (no Le).
- tests: the expected INSTALL bytes updated, a no-Le assertion for every
APDU in the sequence, and a custom-SD-AID case.
615 frontend / 496 Python green; version 3.6.8; sw simple-v281.
Explore still missed the F0414C46416101 package on a card whose responses
wrap the R-APDU in the TS 102 226 5.2.2 Response Scripting template
(`AB <len> 80 <count> 23 <len> <R-APDU>`): `_decode_por` parsed that as a
compact response, so the frontend got `last_status_word` 81d0/7680 and data
starting `80 01 01 23 ...` instead of the listing.
- server: `_parse_response_scripting()` (AB definite / AF 80 ... 00 00
indefinite) extracts the executed-command count and the last R-APDU's SW
and data; `_decode_por` exposes it as `response_type: scripting` in the
same `decoded` shape as compact, so the RAM explore paging and the RAM
install `por_sw` see the real 9000/6310.
- frontend: the compact listing walk is deterministic on the AID length
(`len AID life ver`; P1=10 adds `module_count (len module_AID)*`).
`_parseRawAppEntry` no longer guesses `rawLen-1` for >8-byte AIDs (16-byte
A113 applet AIDs were truncated), and `_parseRawElfEntry` no longer scans
for `0x10` (a length/AID byte equal to 0x10 derailed the walk: a live page
parsed to 1 entry with no F0414C46416101).
- tests: exact trace fixtures - the ELF page lists F0414C46416101 (11
entries), the P1=10 page attaches its F0414C4641610101 module, the app page
keeps the 16-byte A113 AIDs; Python covers the scripting template
(definite/indefinite) against the live `AB 12` ISD and listing vectors.
610 frontend / 495 Python green; version 3.6.5; sw simple-v278.
Explore returned partial data (no ELF/module entries, the installed package
missing) and then failed with cntr_low. Two causes, both in the
actual-response SMS-SUBMIT path the card uses for big listings:
- `_find_sms_tpdu` read TLV lengths as a single byte; the FETCH carries
`8B 81 97 ...` (BER long form) for the big pages, so it returned a
corrupted, truncated TPDU.
- `_calc_ud_offset` treated the SMS-SUBMIT relative validity period (VPF=10)
as 7 bytes instead of 1, shifting the UD offset: `_parse_sms_concat` then
read a bogus UDH and reported no concatenation, so the segments never
assembled and the PoR/data was dropped ("RAM RESPONSE-PACKET: empty").
The step was marked failed, the counter did not advance, the same counter
was retried and the card answered `cntr_low`, which also blocked P1=10
(modules) where the installed package appears.
- the captured segments now always store the assembled UD (`submit_ud_hex`);
`_sms_submit_por()` rebuilds the DELIVER-style packet (`02 71 00` + UD) for
`_decode_por`. Verified against the live capture: por_ok, remote SW 6310,
438 hex chars of listing data (the exact bytes of the P1=20 page).
- `spPorAccepted` counts `actual_response_sms_submit` (0x0B) as accepted, so
the counter advances when the data follows via SMS-SUBMIT.
Explore queries follow GP Card Spec v2.3.1 11.4.2.2: compact listings
(P2.b2=0) with the chained GET RESPONSE (`ramGetStatusApdu`, paging P2=00 ->
P2=01), the malformed P2=02 attempt is gone, and the ISD-only query (P1=80)
never pages with next-occurrence (the card shall reject it).
Tests: Python SmsSubmitCaptureTest with the live FETCH bytes (TPDU length,
UD offset, concat reassembly, 027100+UD decode); ram.test.js checks the APDU
builder and the missing P2=02 attempt; cards_counter covers 0x0B.
608 frontend / 492 Python green; version 3.6.4; sw simple-v277.
The SCP80/RAM forms hold a copy of the preset (counter, keys, TAR, SPI).
Editing the preset on the Cards tab saved correctly, but the form kept the
old copy: the operation sent the stale counter (the card answers cntr_low)
and the post-send sync wrote the stale value back over the preset - the
saved counter silently reverted. Reproduced in a real DOM:
after select in SCP80: preset=0000000001 sp=0000000001
after Cards edit+save: preset=00000000AA sp=0000000001
after a sync: preset=0000000001 (edit lost)
- `cardsApply()` split into `cardsApplyFields()` (field copy, no packet) and
`cardsApply()` = fields + genSp; new `spRefreshFromPreset(selId)` re-reads
the selected preset from `sp-card-sel` / `ram-card-sel` and re-applies it.
- `pysimSendOta()` and `ramExecute()` call it before starting, so every
SCP80/RAM operation uses the preset as it is now.
- A rejected send no longer advances the counter: new `spPorAccepted(por)`
gates the advance+write-back in `pysimSendOta`, the Explore pagination and
its GET DATA step, and the server's RAM install (`_ram_next_cntr`: advance
only for `por_ok`/`no_por` steps). A failed install still returns
`final_cntr` (the accepted prefix) and the PWA persists it, so a retry
never replays a counter the card already consumed.
- tests: cards_counter.test.js (the stale-form regression, RAM selector,
fields-without-genSp, spPorAccepted) and `_ram_next_cntr` cases; the
cards_form/ram harnesses updated for the split.
- docs/api.md counter semantics; AGENTS preset-source-of-truth rule.
607 frontend / 488 Python green; version 3.6.3; sw simple-v276.
The RAM installer read `por['decoded']['response_status']`, but the compact
response decoder's `decoded` only carries {number_of_commands,
last_status_word, last_response_data} - so the suffix was always empty and
every step showed the useless `por_error_`, even when the PoR was por_ok.
The PoR verdict is the top-level `response_status`.
With that fixed, the decoded details also show that the remote command's own
status word was the real verdict all along: a captured live install returned
por_ok with `last_status_word` 6700/6F00 (the card rejected every RAM APDU)
while the installer reported success.
- new `_ram_step_result()`/`_por_remote_sw()`/`_ram_remote_sw_ok()`: a step
fails on a non-por_ok PoR, on a remote SW outside the success set (9000,
61xx more data, 62xx/63xx warnings, CAFE GP "more data"), or on an
undecodable PoR (a 9000 transport SW with no PoR at all is `no_por`, not a
failure). A decoded 61xx is explicitly success, per GP/ISO.
- step records gain por_sw/por_type/por_cntr/por_data/por_raw and
`por_error`; the response carries `failed_step` and a detailed error such
as `LOAD (1/9): remote SW 6700`; the log line now prints
`status=por_ok remote_sw=6700`.
- PWA: new pure `ramStepLine()` renders e.g.
`❌ Шаг 2: LOAD (1/9) — PoR ok · remote SW 6700 (Wrong length in Lc) · 274 B / 3 SMS`
(SW meaning via the existing lookupSw decoder) and the transport SW only
when it is not 9000.
- tests: tests/test_ota_helpers.py RamPorStepTest (success set incl. 61xx,
the captured 6700 failure, no-PoR/undecodable, expanded responses);
ram.test.js ramStepLine cases.
- docs/api.md RAM install step fields + failure semantics; AGENTS updated.
603 frontend / 487 Python green; version 3.6.2; sw simple-v275.
A test script drives a deterministic dialogue with the card: action steps
(ENVELOPE event / Menu Selection, raw APDU, SCP80 secured packet with a
card-preset, file update/read, STATUS) with SW/data/PoR checks, and
proactive-command expectations that fetch, check (command type, qualifier,
text/item/raw) and answer with a scripted TERMINAL RESPONSE.
- pysim_simple_server/testscript.py: pure engine (validation, exact/mask
matchers with '?' nibble wildcards, item/text checks, TERMINAL RESPONSE
building).
- server.py: worker thread + state, /api/test/run|status|stop|clear, the
card-endpoint guard (409 while running), background STATUS polling
suspended, 'error terminates / warning continues', a pending command is
drained with a cancel TR on stop/error, no-drain modes for the ENVELOPE
and SCP80 senders (the pending command belongs to the next expectation).
- Expectations never poll: a command must be pending (91XX) from the
previous step, otherwise it is an error (TS 102 221 7.4.2.1 / TS 102 223
6.3); scripts add an explicit `status` action (attempts/interval) when
the card delivers on poll.
- SCP80 steps require a complete card preset, may override TAR/SPI1/SPI2
only, and the counter is advanced per send and reported
(`scp80_counter`) for the PWA to write back.
- tests/test_testscript.py (26 tests: engine, matchers, TR building, the
STK menu dialogue, error/warning termination, status polling,
unexpected-command drain, SCP80 preset/counter, file actions, guards).
- docs/api.md endpoint reference.
467 Python / 573 frontend green.
SET UP MENU / SELECT ITEM items carry an optional Items Next Action Indicator
(TS 102 223 8.24, tag '18'): one byte per item, in list order, coded with the
Table 9.4 values marked "Used for Next Action Indicator". The decoder ignored
it, so neither the proactive log nor the STK menu showed what the card would
do when an item is selected.
- server: NAI_TYPES whitelist (the ToC-only values - e.g. '26', '27', '47' -
are reserved there and are ignored, as are '00' and unlisted values),
_nai_name(), _attach_nai() (a short NAI list leaves the tail without an
indicator, extra bytes are ignored); _parse_select_item() and
_parse_setup_menu_items() plus the TERMINAL PROFILE SET UP MENU walk attach
nai/nai_name to the items; _decode_cmd() renders '1. Menu -> SET UP MENU'.
- PWA: stkMenuNaiSuffix() adds a small gray '<name>' suffix to STK menu items
that carry an NAI; the proactive log picks the decode up via cmd_decoded.
- tests: SET UP MENU / SELECT ITEM vectors with NAIs, a reserved value that is
ignored, a short NAI list, and the stkMenuNaiSuffix unit test.
- docs: UICC_SPECS 6.5 gained the '18' Items next action indicator row
(8.24/9.4); help EN/RU mention the menu suffix; AGENTS files updated.
548 frontend / 413 Python green; version 3.5.3; sw cache simple-v255.
Three fixes found while filling the ETSI/3GPP registry gaps against the
SIMalliance Stepping Stones R7:
- SCP80 builder/verification parity (TS 31.115 Table 1 NOTE / 4.2 / 4.3):
the CPL is now transmitted whenever the packet is ciphered or carries
RC/CC/DS - it is part of their input - and whenever the packet needs SMS
concatenation; a single unprotected SM keeps pySim's CHL-first form.
Before, the JS dropped the CPL for every unciphered packet while the
server reference re-added it, so "Verify vs pySim" reported a false
MISMATCH for every unciphered RC/CC packet (SPI1 01/02/0A/12/1A...).
All ten offered SPI1 values now match the server reference byte-for-byte.
- RC (SPI1 b2b1 = 01) was offered but not built: the JS now computes CRC-32
(TS 102 225 5.1.3.2, pySim zlib.crc32 parity) over the same CPL frame as
the CC; the packet no longer silently omits the 4-byte RC field.
- Server: _build_secured_packet/_ota_reference add the CPL to a concatenated
unprotected packet too (Table 1 NOTE / 4.3).
- fcpLifeCycle: unlisted values with b8 clear are RFU, b8 set is proprietary
(Table 11.7b); previously all unmatched values were labelled proprietary.
- EF.ARR decoder now decodes the expanded format (AM_DO/SC_DO per ISO 7816-4
5.4.3.2 + TS 102 221 9.2.7): operation bit masks, INCREASE/RESIZE AM_DO
0x84, OR/AND/NOT templates, PIN key references with usage qualifiers.
- FCP 'C6' PS template DO decoded (PS_DO bitmap + key references + usage
qualifiers, TS 102 221 11.1.1.4.10/9.5.2) with a shared key-reference map.
Tests: sp.test.js (CPL/RC vectors + crc32 known answer), ef_decode.test.js
(expanded-format ARR vectors), profiler.test.js (LCSI RFU/proprietary, C6),
test_ota_helpers.py (RC reference, unprotected single-SM vs concatenated).
Help EN/RU and READMEs: CPL size 2 (SMS), RC/CC/DS 4-8, RC bullet, CPL rule.
547 frontend / 397 Python green; version 3.5.1; sw cache simple-v253.
Packets longer than one SMS now go out as concatenated SMS-PP downloads
per TS 31.115 4.3 and the UI shows how many SMS a packet needs.
Server:
- `_split_secured_packet` cuts the command packet at the exact SMS
user-data capacities (first SM 132 octets: concat IE 5 + CPI IE 2;
following ones 134; a single-SM packet may be 137 with the CPI IE) and
`_build_sms_tpdu` tags every segment with the fixed concatenation
reference 01; `_build_sms_tpdu` also enforces the 140-octet budget.
- `_send_secured_packet` (shared by /api/send-ota and /api/ram-install)
sends one ENVELOPE per segment in order, refuses more than
MAX_ENVELOPE_SEGMENTS (5, the card's concatenation buffer) and reports
`bytes`/`segments` in the response (RAM install per step as well).
- `_build_secured_packet`/`_encode_cmd_unlimited`: our own TS 102 225
5.1.1 encoder on pySim's keyset/header constructors, byte-identical to
pySim for packets <= 140 octets (tests) and not limited to one SMS
(pySim refuses the longer ones, which is why they never went out).
- RAM LOAD blocks are no longer clamped to one SMS: 1-240 bytes of
payload with the default 240 (the GP maximum); `load_block_size_auto`
replaces `load_block_size_clamped`.
PWA:
- `scp80SegmentInfo` / `spSizeInfoText` show "N bytes . M SMS
(concatenated)" under the packet field, turn red past 5 SMS and report
size/SMS in the send result and the RAM step log; LOAD block hints and
placeholders updated; EN/RU.
Tests/docs: Python +2 cases incl. segment order/capacities and byte
identity with pySim; Node drift guard against the server constants and
UI text tests; README/README_RUS, help EN/RU, docs/api.md, AGENTS.
TERMINAL PROFILE:
- GET /api/terminal-profile returns the profile in effect + the CLI default;
POST /api/terminal-profile validates ({profile}, hex, even, 1-255 bytes),
stores it in memory and re-sends it, resetting the STK session like
/api/rescue (the shared _resend_terminal_profile helper; rescue now
delegates to it). __main__ keeps server.cli_terminal_profile.
- Phone tab: a TERMINAL PROFILE block next to STATUS and Polling with the
current hex/byte count, Send (re-send) and Configure. The Configure
dialog has a device-model preset selector, a hex field and a per-bit
form generated from a 264-entry table for TS 102 223 V18.3.0 5.2 bytes
1-33 (pySim's table as scaffold, later bytes/3GPP bits added from the
spec; beyond the table generic RFU labels). Form <-> hex sync both
ways, hex authoritative, bits preserved. Apply posts the new value.
- Presets: Xiaomi Mi A1 (project default), Quectel GSM module example.
In-memory only, no persistence.
Custom files:
- Canonical paths rooted at MF / ADF.USIM / ADF.ISIM; entries are
{path, name, kind}. The editor now uses root + parent-DF selector +
4-hex FID + alias, requires the parent DF to be defined first, rejects
duplicates, rewrites descendants when a DF's path changes and cascades
deletes after a confirmation.
- Legacy forms are normalized on load/import (3F00/... -> MF/..., relative
a153/4954 resolved against the custom DFs); unresolvable entries are
dropped and reported in the list.
- Tree injection matches by exact parent path via the new pysimFsNodePath
(same-FID DFs under different parents no longer collide);
profilerCustomNameForPath uses the same normalization.
SW cache otaman-v167; help EN/RU + docs/api.md + AGENTS updated.
Tests: 236 Python + 371 frontend.
SCP81:
- New listener mode "passthru": no local listener - the card's BIP
channels connect straight to a configured external platform
(host/port required), which terminates TLS and runs the dialog.
The status API reports mode/target (_SCP81_MODE/_SCP81_TARGET) and
clears them on stop. PWA mode select, hint, Start validation; TLS PSK
stays the default.
Proactive command decoding (Phone tab log):
- SEND SHORT MESSAGE (0x13) is now decoded: alpha, address (TON/NPI +
number), 3GPP-SMS TPDU (type, TP-MR, TP-DA, TP-PID 0x7F flagged as
SIM data download, TP-DCS, TP-VP, TP-UDL), UDH concatenation IEs, and
the TP-UD as text (GSM-7 with a septet unpacker, UCS2, 8-bit) or as a
TS 31.115 secured packet for PID 0x7F; malformed TPDUs fall back to
the raw hex line.
- PROVIDE LOCAL INFORMATION qualifier names completed per TS 102 223
V18.3.0: ESN (07), MEID (0B), Supported RATs (1A); 05 relabelled
"Reserved for GSM (Timing Advance)". Fixed in the server dict and
both frontend tables.
Header:
- Compact ADM badge next to the card indicator: "ADM ✓" green when
pySim's adm_verified is set, "ADM ✗" red otherwise, hidden without a
card session or when the server is down; updated ahead of the card
state-key early return so it never disturbs the connect/reset flow.
File manager:
- Sort pills (FID/Name), Probe all files button and progress line are
pinned above the tree instead of scrolling with it; the tree box cap
grows from 420px to 65vh.
SW cache otaman-v165; help EN/RU updated; tests 234 Python + 361 frontend.
Cards / SCP81:
- Cards is a top-level tab; presets gain PSK identity + key, HTTP-OTA
column, Edit/Update and a live PSK-map push into a running listener.
- SCP81 has Listener/Scripts pills; scripts are named local APDU lists
(Empty / Explore / Install from .cap / Delete templates), sent to the
server explicitly at start. The listener takes mode/host/port/script
only; PSK inputs and the .cap row are gone.
- Multi-PSK TLS listener: identity -> key lookup from the card presets
(POST /api/scp81/psk-map updates a running listener), unknown
identities log tls-psk-unknown and fail; handshake logs carry psk_match.
- Script engine: execution tracking (next/done/pending/results), a
resumed dialog sends only the unexecuted tail (unreported APDU is
resent), a fresh dialog restarts, listing continuation pages are
tracked separately (pending.pages/complete). Restart script button.
- POST /api/scp81/gen-install replaces the SCP81 ram-install queueing
(generation only; the .cap is never stored).
Profiler / snapshots:
- Snapshot comparison is always exact (mask checkboxes removed; the
first-4-bytes mask remains a profile-creation option).
- "matching records" line shows count + #record numbers.
- New Clone action: copy named "Copy of <profile>", opened in the editor.
- Matched-record count/numbers fix ("1 из 8 (#8)").
SCP80:
- Configurable / auto-fitted LOAD block size: each LOAD APDU encodes into
one SMS (pySim rejects secured packets above 140 octets, so a 240-byte
block could never be sent). Response reports the effective size and
clamps; encode failures are reported per step with the pySim message.
SW cache otaman-v161; docs/api.md, scp81-findings and help EN/RU updated.
Tests: 226 Python + 356 frontend.
The block slicer used the block number as a character offset
(loadfile_tlv[i * 2:(i + 240) * 2]), so every LOAD block after the first was
a 1-byte-shifted copy of the previous one - the cap header repeated every
239 bytes on the wire. A live install accepted three blocks, failed block 4
with SW 6400, then 6985, and INSTALL [for install] answered 6A88. The same
slicing was inherited by the SCP81 helper from the SCP80 path, so
multi-block caps could not install there either; both are fixed.
Tests: blocks are consecutive and reassemble the C4 TLV byte-for-byte
(200 python); findings updated; service worker v149.
- shared _cap_apdu_sequence helper (INSTALL [for load] -> 240-byte LOAD
blocks -> INSTALL [for install]); the SCP80 /api/ram-install path now uses
it too (one source of truth; byte-level tests pin the APDUs)
- POST /api/scp81/ram-install: parses the .cap server-side and queues the
APDU sequence as the SCP81 command script (one C-APDU per POST, runs on the
card's next push); refused while a script is mid-run unless force
- /api/scp81/script reports the script kind (explore/none/custom/ram-install)
- tab: RAM install row (CAP file + SD AID + Queue button); RU strings
- docs: api.md, findings, AGENTS; service worker v145
- scp81.py: PSK TLS listener (stdlib ssl PSK callbacks) speaking the GP
HTTP administration dialog; configurable framing (chunked/Content-Length,
TLS record split, Apache-style/compact headers, Connection header,
keep-alive, Next-URI template with %d, TLS version/cipher, answer delay,
keylog for capture decryption)
- server.py: script responder + Response Scripting parsing (AF/AB, 80/23
TLVs), memory decoder, SCP81 start options, terminal-side timer
management, background-mode BIP events, permissive OPEN CHANNEL
- BIP fix: the RECEIVE DATA channel-data TLV length is BER long form
(36 81 <len>) above 127 bytes; a raw length byte is mis-parsed on the
card, so the large TLS records never reached its stack (a live card
fetched the script response and silently never processed it - endless
resume). The card now executes scripts and returns R-APDUs: memory
(13 applets, 50646 B NV free, 2402 B volatile), ISD, stored HTTP OTA
parameters, ELF and application registries
- frontend: SCP81 tab (listener, script selection, HTTP OTA log), phone
event forms, i18n; service worker v141
- docs: api.md, scp81-findings.md (attempt matrix + root cause analysis);
tools/scp81_decrypt.py decrypts listener captures via the keylog
- tests: 187 python + 337 frontend
Incoming: _parse_sms_concat parses UDH for IEI 0x00 (8-bit ref) and
IEI 0x08 (16-bit ref). PoRSubmitHandler accumulates segments, sorts by
num, and reassembles complete payload when all parts arrive.
Outgoing: MAX_ENVELOPE_SEGMENTS=5 limits the secured packet to 5
ENVELOPE chunks (650B max at 130B/chunk).
10 new tests for concat parsing and reassembly.
- pysimSendOta: Response Parser filled strictly from decoded PoR
(por.decoded) for both SPI2 variants; envelope SW/failures no longer
leak into it — they render in a new inline result line (#sp-send-result)
next to Send to Card, always including SW on failure
- Default-level stderr tracing per send (no flags needed):
'OTA SEND: SPI .. KIc .. KID .. TAR .. CNTR .. LEN ..B CHUNKS N',
'OTA SEND FAILED: chunk N SW xxxx' and
'OTA PoR[envelope|sms-submit]: status=.. TAR=.. CNTR=.. PCNTR=..
RPL=.. RHL=..' (+ compact summary / undecodable raw / none fallbacks)
- _decode_por: surface every parsed PoR field verbatim (cntr, rpl, rhl,
cc_rc, raw) instead of status/tar/pcntr only
- genSp: CNTR normalization padEnd -> padStart so short input like '1'
becomes 0000000001, not 1000000000 (counter is big-endian 5 bytes)
- tests: TestDecodePor completeness + cntr_low field-report cases
Version 1.9.3 -> 1.9.4 everywhere; SW cache otaman-v14 -> otaman-v15
Server:
- Track all TERMINAL RESPONSEs (chain, TP, pySim auto-handler, menu-respond
link to paused entry) with shared _build_tr/_record_tr
- Log entry ids + cmd_num; server-side decode of fetched commands and TRs
(compact PLI port); Result CTLV extracted as tr_result/tr_result_name
- _LoggingApduTracer captures TR SW for pySim auto-handler responses
- _DefaultProactiveHandler answers PLI with editor data (no re-request)
- VERSION 1.8.0
Frontend:
- Expandable proactive log rows (click to expand, state survives re-render)
- Response section: Result name + hex, decoded fields, width-restrained
readonly hex inputs; no SW display
- Rename Expanded Script tab/pill/button/docs; proactive UICC view font
sizing increase; card reader pills text-sm