Commit Graph

71 Commits

Author SHA1 Message Date
catarrh 9e85f522f6 fix: recover the SMS-SUBMIT listings in RAM Explore (v3.6.4)
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.
2026-09-28 00:09:54 +03:00
catarrh 39c82f26f3 fix: SCP80/RAM re-read the preset before every operation; no counter bump on a rejected send (v3.6.3)
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.
2026-09-27 23:43:28 +03:00
catarrh 36d2f71bc7 fix: report the real PoR/remote-SW result of every RAM install step (v3.6.2)
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.
2026-09-27 23:21:56 +03:00
catarrh 18a36a8f27 feat: generic BIP terminal control (Simulator BIP pill) (v3.6.0)
BIP was only reachable through the SCP81 listener; cards that use TCP for
other purposes (an applet's OPEN CHANNEL, a push trigger without HTTP OTA)
now get a generic control surface, independent of SCP81.

Server:
- /api/bip/control starts a plain BIP session in three modes: sink
  (default; a local TcpDumpServer that accepts the card's channels and only
  logs conn/sink-rx/conn-close, never answering; port 0 = ephemeral, the
  bound port is reported), passthru (dial the OPEN CHANNEL destination, TCP
  client only) and redirect (fixed host:port).
- /api/bip/status returns {owner, bip, listener}; /api/bip/log and
  /api/bip/log-clear share the BIP event log with /api/scp81/log.
- The session state is shared with the SCP81 listener and only one session
  runs at a time: _bip_session_stop() stops whichever control started it
  and the control responses report it as `replaced` (both directions,
  SCP81 <-> BIP).  State renamed _SCP81_MODE/_TARGET/_LISTENER/_LINK_EVENTS
  -> _BIP_* plus _BIP_OWNER.
- TcpDumpServer logs conn-close and counts accepted connections; stop()
  joins the accept thread so the port is really free on restart.
- The new control endpoints are blocked during test-script runs.

PWA:
- New Simulator pill BIP (after TR Config): mode select with notes,
  Host/Port rules, Start/Stop with a "replaced" notice, status line (mode,
  bound address, owner, channels), a Channels box and the shared BIP log
  (reuses the SCP81 log renderer, now showing `peer`).
- The SCP81 status line marks a session owned by the BIP pill.

Help EN/RU 8.10, docs/api.md, AGENTS; CAT_TP/UDP recorded as not
implemented.

Tests: tests/test_bip.py (9) and frontend/tests/bip.test.js (6).
600 frontend / 482 Python green; version 3.6.0; sw simple-v273.
2026-09-27 15:28:23 +03:00
catarrh 0255a956e2 feat: show the CAP's required libraries, package identity and components (v3.5.16)
The Import component (JC VM spec 6.6) is now exposed by /api/cap-info and
rendered in the CAP analysis box:

- imports: the libraries the CAP is linked against with the export-file
  versions and the number of distinct constant-pool references (6.7),
  displayed as "name >= version" (a card resolves an import only with the
  same major and a minor >= the recorded one, 4.5.2).  Standard names come
  from the AID table; each gets a family label (Oracle JavaCard / ETSI SIM
  2G / ETSI UICC / 3GPP USIM-ISIM / GlobalPlatform) and, for
  javacard.framework, a Java Card SDK release hint derived from the local
  Oracle SDK kit corpus (jc211..jc305u4 exports; unknown versions stay
  unhinted).  Vendor/applet AIDs stay bare.
- Header package flags (Table 6-4: int / exports / applet package) and the
  optional JC 2.2 package_name (absent in all CAP 2.1 files).
- components: every archive entry in load-file order with its size and
  share of the load file (including the Directory/Export entries capmem
  does not parse); the sizes sum to load_file_bytes.
- The PWA box leads with "Requires: ..." when imports exist, keeps the
  compiled-against line (Java Card hint + CAP format), the package/applet
  identity, the import details (family, refs, AID) and the component
  breakdown in Details; a memory-only response still renders.

Tests: Python +2 (imports/flags/name/components; header flags + package
name) with the synthetic CAP builder extended; frontend +2 renderer cases
(unknown AIDs, no-import responses) plus jcAidNorm/jcAidFamily tests; the
jcAidNorm extraction added to the ram/scp81 harnesses.
Help EN/RU, docs/api.md, AGENTS.

591 frontend / 473 Python green; version 3.5.16; sw simple-v269.
2026-09-27 02:23:03 +03:00
catarrh 18e0db75a9 feat: approximate NVRAM requirement from the CAP analysis (v3.5.15)
GlobalPlatform models this explicitly (GP Card Spec v2.3.1 Table 11-48,
load parameters): C6 = non-volatile code, C7 = volatile data, C8 =
non-volatile data, and 11.5.2.3.7 - when the card makes no code/data
distinction the required minimum is C6 + C8.  The analysis now reports it:

- capmem.memory_json() accepts the load file size (all CAP components -
  the package image the card stores) and returns code.load_file,
  nvram.requirement = load_file + persistent data, and sets the suggested
  C6 to the load file (the bytecode-only Method.cap figure stays in
  code.method_component); _cap_info_body passes the size it already
  computed.
- The CAP estimate box (both the RAM installer and the SCP81
  Install-from-.cap form) leads with "NVRAM requirement ≈ code image +
  data" plus the RAM estimate, keeps the bytecode/other-component split and
  the full data breakdown in Details, and carries the GP citation with the
  caveats (card memory management, allocation rounding and the registry
  entry are not included; the static field image appears in both parts).
- An older server response without code.load_file still renders (the
  bytecode size is used as the fallback).

Tests: Python +1 (load-file semantics) with extended cap-info assertions,
frontend +1 (renderer + fallback).  Help EN/RU, docs/api.md, AGENTS.

587 frontend / 472 Python green; version 3.5.15; sw simple-v268.
2026-09-27 02:14:49 +03:00
catarrh d1eb88d4d7 fix: test-script review findings (v3.5.13)
PWA:
- The SCP80 "Source" switch sticks: the choice is stored in
  `params.source`, switching keeps both values (the server honors the
  explicit source when both are present, and the form validates only the
  selected one).
- The SW check field is empty by default (placeholder "default: 9000
  (91?? when polling)"), so the server defaults apply - previously the
  pre-filled 9000 defeated the polling STATUS default and a card that
  announced a command made the step fail with "expected 9000".
- Item text checks offer contains/exact only (the mask mode was rejected
  by the server validation).
- The SCP80 counter is written back to the preset even when the run is
  observed after a page reload or was started elsewhere (resolved by
  ICCID or preset name from the run snapshot).

Server:
- Step-entry mutations happen under `_TEST_LOCK`
  (`_test_entry_update`/`_test_finish_entry`): the status endpoint
  serializes the state with json.dumps, so entries must not change while
  it iterates them.
- The pending-command drains (unexpected command, error, stop) hold
  `_CARD_LOCK` like every other card conversation.
- `_int` accepts plain decimals with leading zeros and 0x hex.
- The scripted TERMINAL RESPONSE text string uses the CR-set tag `8D`
  (consistent with the other TR TLVs; both are legal).
- A script-driven menu selection mirrors `server.menu_active`.

Tests: frontend +4 (source switch/round-trip, status SW default, render
checks, item modes), Python +3 (integer parsing, menu_active, source
selection).  Help/docs unaffected beyond api.md's `source` note.

583 frontend / 470 Python green.
2026-09-27 01:48:12 +03:00
catarrh 3a4a098f1a feat: test script engine and server-side runner (phase 1)
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.
2026-09-27 00:59:53 +03:00
catarrh 4c3fa64c95 feat: polling on by default and an effective-state toggle (v3.5.10)
- Background STATUS polling is enabled by default, per the spec's idle
  polling rule (TS 102 221 14.6.2): `_POLL_ENABLED = True`, __main__ runs
  `_poll_enable()` regardless of card presence (--poll-interval 0 still
  disables) and `_do_status_poll` keeps ticking while enabled even without
  a session, so a cardless start resumes as soon as a card appears.
- A new card session clears a POLLING OFF from the previous card in
  `_apply_equipped_card` before re-enabling polling.
- The Phone-tab toggle now shows the *effective* state: OFF in amber
  ("card disabled polling (POLLING OFF)") while the card suspended
  proactive polling, back to ON on the next POLL INTERVAL.  The 5 s
  background poll now passes `card_disabled` through (it was dropped, so
  an autonomously received POLLING OFF never reached the UI).
- Tests: poll_ui updated (button OFF while suspended, warning path),
  test_poll +2 (cardless ticking, module default via a subprocess check).
- Help EN/RU, docs/api.md, AGENTS; version 3.5.10; sw cache simple-v263.

573 frontend / 441 Python green.
2026-09-26 13:09:44 +03:00
catarrh 25f001a778 feat: card-driven STATUS polling and the Poll Interval Negotiation event (v3.5.9)
- EVENT_NAMES (server + PWA) corrected against TS 102 223 v18.3.0 8.25:
  0x14 is "Access technology change (multiple)" (not Change of UICC
  Access), 0x19 Profile container, 0x1A Void, 0x1B Secured profile
  container, 0x1C Poll interval negotiation, 0x20-0x22 reserved.  Values
  the CAT spec leaves "Reserved for 3GPP" now carry the concrete TS 31.111
  event name + clause (0x11 (I-)WLAN access status, 0x12 Network
  rejection, 0x15 CSG cell selection, 0x17 IMS registration, 0x18 Incoming
  IMS data, 0x1D Data connection status change, 0x1E CAG cell selection,
  0x1F Slices status change).
- Poll Interval Negotiation (0x1C) in the Phone tab: the form proposes a
  Duration (unit + interval) and the UICC's answer (TS 102 223 8.97
  accepted / rejected / modified + optional Duration) is decoded and
  shown; a "modified" duration becomes the background poll interval.  The
  form appears when the card subscribed to the event, like every other.
- Polling emulation is card-driven: a POLL INTERVAL adopts its Duration
  (minutes/seconds/tenths -> 1..255 s, logged) for the background poll,
  is echoed in the TERMINAL RESPONSE (6.8.4) and clears a POLLING OFF
  suspension; POLLING OFF (6.4.14) suspends proactive polling until a new
  POLL INTERVAL - the manual Send STATUS button and presence detection are
  unaffected.  /api/poll-status and /api/poll-toggle report card_disabled
  (+ a warning when enabling while suspended); the PWA shows it in amber.
- Tests: tests/test_poll.py +7, test_proactive_names.py +3, frontend
  event_forms +2 and poll_ui (4).  Help EN/RU, docs/api.md, AGENTS.

573 frontend / 439 Python green; version 3.5.9; sw cache simple-v262.
2026-09-26 12:56:18 +03:00
catarrh d9e6c6c9dd feat: CAP memory estimation with a confirm step before install (v3.5.8)
Selecting a .cap in the RAM installer or the SCP81 "Install from .cap"
template now runs a read-only analysis (POST /api/cap-info) before any
APDU is built: the archive is validated structurally (a corrupt or
wrong-format file fails here) and the bundled capmem analyzer estimates
the code size and the persistent (NVRAM) / volatile (RAM) requirements,
with the tool's suggested C6/C7/C8 quotas shown as information.  The
form's action button (Execute / Generate) stays disabled until the
analysis succeeds - pressing it is the user's confirmation to continue.

- pysim_simple_server/capmem.py: bundled analyzer (component parsers +
  JCVM opcode table + method-bytecode allocation scan), adapted to take
  the CAP archive as bytes and return report/memory dicts; output
  verified byte-identical to the workspace tool on 21 real CAPs.
- _cap_info_body + POST /api/cap-info (read-only; the install endpoints
  stay unchanged and self-sufficient).
- PWA: shared capAnalyzeFile/capMemHtml/capGateOk helpers, estimate box
  under both CAP inputs (reusing the idle #ram-cap-info div, new
  #scripts-cap-info), data-cap-gate gating in pysimApplyAvailability,
  stale-response guard, Retry, EN/RU strings.
- Tests: tests/test_cap_memory.py (synthetic CAPs: new/newarray/
  makeTransientByteArray/static fields/unknown-opcode warnings/corrupt
  input), frontend capmem.test.js (renderer, gate, analyze flow).
- help EN/RU, docs/api.md, AGENTS; version 3.5.8; sw cache simple-v261.

567 frontend / 429 Python green.
2026-09-26 08:50:46 +03:00
catarrh 43f50d3b72 feat: SCP80 SMS concatenation + packet size / SMS count display (v3.2.0)
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.
2026-09-22 23:02:13 +03:00
catarrh fa600cb45d ui: rename the SCP81 script templates to Explore ISD / Delete AID (v3.1.3)
'Explore' and 'Delete' were ambiguous next to the other SCP81 actions.

- template selector: Explore -> Explore ISD, Delete -> Delete AID (EN/RU);
- a new Explore script is named 'Explore ISD'; the Delete template keeps
  its count-based name (e.g. 'Delete 2 AIDs');
- new scriptKindLabel() maps the stored kinds (unchanged:
  explore/delete/install/empty, so existing localStorage scripts keep
  working) to those labels and is now used for the scripts table kind
  column, which used to print the raw lowercase kind untranslated;
- docs: help EN/RU, README/RUS, docs/api.md examples, AGENTS;
- sw.js simple-v238.
2026-09-22 22:07:57 +03:00
catarrh f87f3e9841 esim: decode the notification operations from the card's TLV shape
ListNotification showed 'pmo' as the operation for every notification:
pySim's ProfileMgmtOperation is a Struct whose first (ignored) byte is
the padding-bits octet, so a TLV parsed from the card nests the flags
under 'pmo' while an object built from decoded flags (the unit-test path)
does not.  The mapping iterated the outer dict and reported the nested
dict as a truthy key.

- esim._profile_operations() accepts both shapes and returns the set
  flags in spec/bit order (install, enable, disable, delete).
- tests: a notification parsed from the card-shaped raw TLV (padding
  octet included) plus the helper's both-shapes/multi-flag cases.
- docs: /api/esim/notifications and the repo AGENTS note.
2026-09-22 00:30:32 +03:00
catarrh d3f5aaa443 esim: show the profile icon image and its data size
pySim already requests the Icon tag (0x94) together with the other
ProfileInfo tags; the mapping dropped it, so the PWA only had the icon
type.

- esim.profiles(): each profile now carries `icon` (the image bytes as
  hex) and `icon_size` (byte count), null when the card sent no image.
- PWA: esimIconDataUrl() builds a data: URL (png/jpg -> image/png|jpeg)
  and the profile card shows the image unscaled to the left of the
  metadata rows; the Icon row keeps the type and adds the data size
  (e.g. 'png · 1234 B').
- tests: profiles icon/icon_size mapping (+ the absent case) and the
  frontend data URL / row / render checks; docs and sw simple-v231.
2026-09-22 00:21:43 +03:00
catarrh 86808ce55d esim: drive the profile switch ourselves; guard the FCP metadata
Disabling a profile failed with 6985 and left the card stuck until an
equip.  pySim's send_apdu_checksw auto-handler keeps flushing proactive
commands after the REFRESH TERMINAL RESPONSE; the card is then mid-switch
and answers 6985 to the next FETCH, which propagated as a 500 and skipped
the re-initialization.  Per SGP.22 v2.6 5.7.16/5.7.17 a 91XX answer is
the ISD-R's 'result OK before REFRESH' (step 6) and the switch completes
on the TERMINAL RESPONSE or the following RESET (step 8) - lpac treats
91XX the same way and never retries.

- esim.py: build_switch_apdu/parse_switch_response/switch_profile split
  out of set_profile_state; the switch is one raw STORE DATA via
  scc._tp.send_apdu, a 91XX runs our own FETCH/TR chain (status_poll=False)
  and is reported as ok, the STORE DATA is never retried and a chain
  failure still counts the accepted switch.
- server.py: /api/esim/profile answers the REFRESH with our chain, then
  re-initializes the card and re-reads the profile list, returning
  verified/state_after; _handle_proactive_chain grew status_poll.
- /api/status and /api/select: FCP metadata via _fcp_value - an ADF or a
  failed select (card with the active profile disabled) has no
  file_descriptor and used to crash the request handler; _get_file_type
  no longer raises either.
- esim._restore logs a failed selection restore instead of swallowing it.
- PWA: esimSwitchStatus shows the verified state / not-confirmed warning.
- tests: the switch flow (9000 / 91XX / error SW / chain failure), the
  FCP guards and the status helper; docs and sw simple-v230.
2026-09-22 00:14:02 +03:00
catarrh 91c642a646 esim: decode EUICCInfo1/2 and the RAT per SGP.22 (no version bump yet)
The chip endpoint returned pySim's flattened EuiccInfo dict, whose classes
are incomplete: the capability fields are raw GreedyBytes, extCardResource
is raw bytes and several SGP.22 TLVs are missing from the class, so cards
showed 'unknown_ber_tlv_ie_99' and raw hex instead of decoded values.

- request the EUICCInfo1/2 and configured-address TLVs raw and decode them
  in esim.py per SGP.22 v2.6 5.7.8, cross-checked against lpac's
  es10c_ex.c: extended card resource, UICC/RSP capability bit lists (first
  octet = unused bits, MSB-first), CI PKI lists, category (both the
  implicit 0x8B and explicit 0xAB tag encodings), forbidden profile policy
  rules (0x99), ppVersion (0x04), sasAcreditationNumber (0x0C) and the
  optional certification data object / TRE fields; undecoded TLVs stay in
  raw_tlvs instead of being dropped.
- add the ES10b GetRat rules authorisation table (PPR ids, allowed
  operators, consent flag) to the chip response.
- PWA: label every new field, map nested labels per path component (the
  old code only matched whole keys), group the view into EUICCInfo1 /
  EUICCInfo2 / Addresses / RAT sections, render arrays of objects with
  index labels and translate the labels (RU).
- tests: decoders against a real card's values (077F3E1F80, 0490, 0640,
  81010082040006B32C83022646, the RAT fixture) and the frontend label
  mapping; sw.js simple-v229.
2026-09-21 23:56:41 +03:00
catarrh d087632573 docs: align the READMEs, help and API reference with the current UI (v3.0.1)
- docs/api.md: document GET /api/net-state and POST /api/net-state-refresh
  (state shape, monitored keys, refresh filter, 503); add eid/euicc to
  /api/status; correct the /api/net-sim FPLMN wording (roaming_denied
  appends, attach clears; 5GS files untouched) and its response
  (net_state); move the stray /api/event-send example back into its
  section; refresh the version examples to 3.x.
- README/RUS: Cards — ADM field, the three TARs, the SCP80/SCP81
  fieldsets and the header markers; File Browser — Read raw/Read decoded
  pills, Edit raw, named FCI block, Verify ADM; Profiler — Clone, three
  list tabs; Custom files — root/parent/FID/alias form, canonical paths,
  cascade delete, legacy-path resolution; Phone simulator — three pills,
  eSIM, Network state monitor, Home network button, corrected FPLMN
  wording, proactive-log row content; RU CLI table gaps (--sms-oa/
  --sms-sm-sc, --terminal-profile, --mcc-mnc-list) and the missing
  event-form bullets.
- help EN/RU: three profiler list tabs, 3.x compatibility example.
- Version 3.0.1 (docs release), sw.js simple-v228.
2026-09-21 23:38:59 +03:00
catarrh a4d5415034 esim: local eUICC operations in the Phone simulator tab (v2.8.0)
New eSIM pill (Phone simulator) for SGP.22/32 cards, built on pySim's
ES10 static API — no lpac, no new dependencies, no pysim patches, no
SM-DP+ interaction:

- Chip: EID, EUICCInfo1/2, configured addresses (ES10a/b).
- Profiles: GetProfilesInfo with metadata (state, nickname, provider,
  ICCID, ISD-P AID, class, owner, icon).
- Notifications: read-only ListNotification viewer.
- Switch: Enable/DisableProfile with RefreshFlag=1; the card's REFRESH
  (fetched/answered/logged by the transport's proactive handler) or an ok
  result triggers _esim_reinit() — reset + equip + _apply_equipped_card —
  so the ICCID, network state and cached views are re-read.
- Server: pysim_simple_server/esim.py, GET /api/esim/chip|profiles|
  notifications, POST /api/esim/profile (all under _CARD_LOCK; 400
  not_an_euicc), euicc/eid in /api/status.
- Tests: tests/test_esim.py (fake scc, monkeypatched store_data_tlv),
  frontend/tests/esim.test.js; help EN/RU, docs/api.md, AGENTS.
2026-09-21 23:10:52 +03:00
catarrh 2c793720f6 ui: verify the ADM PIN from the matched card preset (v2.7.8)
The preset ADM key was stored but never used: the header badge showed
whether a key exists and whether the card was verified, yet the only way
to verify was the pySim command line.

- POST /api/verify-adm builds the TS 102 221 VERIFY itself (CHV number
  from the card model, short keys padded to 8 bytes with 'f') so the raw
  SW is reported: 63Cx -> attempts_left, 6983/9804 -> blocked, 6982 ->
  security error.  The key is never stored and is redacted from request
  logs.
- PWA: the header ADM badge is clickable when the matched preset has a
  key; a failed file-manager read/write (6982/9804) shows a Verify ADM
  button next to the error.  Every retry after a failure asks for
  confirmation and shows the remaining attempts (stronger text on the
  last attempt); a blocked ADM disables both entry points until the card
  session changes.  No automatic retries.
- tests: tests/test_adm_verify.py (fake scc, APDU/SW mapping, redaction)
  and frontend/tests/adm_verify.test.js (retry prompt, SW classifier,
  wiring) + card_state indicator expectations
- docs/api.md, help EN/RU, AGENTS; version trio 2.7.8; sw cache v211
2026-09-20 22:09:39 +03:00
catarrh 99183acc89 net-sim: skip files missing on the card; /api/write accepts a full path
Live-testing against a UICC without the EPS files (no USIM service 85)
and without the Kc files showed the runner aborting when EPSLOCI/Kc were
absent.  write_binary/write_record now take an 'optional' flag: a missing
candidate file is logged as a skip step instead of failing the scenario
(store_epsnsc, real/dummy locations, churn and Kc writes).  Also allow
POST /api/write to select by 'path' like /api/select and /api/read (the
netsim live test needed it to restore the captured file values).
2026-09-19 22:53:24 +03:00
catarrh 0d92ea8f24 net-sim: network-condition simulation in the Phone tab (v2.5.0)
Server (pysim_simple_server/netsim.py + POST /api/net-sim):
- pure builders for the observed phone write vocabulary (UICC_NAA.md §13):
  EPSNSC (A0 TLV with KSI/KASME/counts/algo, padded to the card's FCP record
  size; invalidate-wipe and invalidate-keep-key), LOCI/PSLOCI/EPSLOCI real
  and dummy forms (PLMN kept, LAC FFFE, status 01/02), Kc/KcGPRS (9-byte
  USIM and 33-byte GSM forms, 07 invalidates), EF.SMSstatus counter bump
  (read-modify-write), CBMI/CBMIR, the Location status event (9B 01 <st>,
  with the optional 13 07 location info for normal service) and the
  AUTHENTICATE APDU (TS 31.102 7.1.2.1) with DB/DC response parsing
- scenario runner executes the recipes under _CARD_LOCK, picks the first
  existing candidate path (ADF.USIM vs DF.GSM/DF.TELECOM), reads FCP/current
  records where the format is card-specific, skips the event unless the card
  subscribed to Location status, honours the scenario toggles and returns a
  per-step log; only D6/DC, ENVELOPE and AUTHENTICATE are ever sent
- GET /api/mcc-mnc serves the optional worldwide operator list
  (--mcc-mnc-list, default <workspace>/samples/mcc-mnc-list.json): ?q=
  search and ?random=1&exclude= for roaming; the list stays out of the repo

PWA:
- Phone pill gains a 'Network simulation' fieldset: one button per scenario
  (cold boot, EPS/2G attach, service lost, limited service, roaming denied,
  churn, SMS received, CB reconfig, AUTHENTICATE), a collapsed Parameters
  block (operator search + random roaming, LAC/Cell ID/TAC/RAC, optional
  identities, toggles, churn count/delay) and a step log with SWs
- i18n EN/RU, help/README/api.md/AGENTS.md updated; version 2.5.0,
  SW cache simple-v193

Tests: tests/test_netsim.py (19), frontend/tests/netsim.test.js (3) and
html guards - 423 frontend / 280 Python, all passing
2026-09-19 22:24:57 +03:00
catarrh 4f6e48f0e0 docs: fix version compatibility (PWA vs server majors), broaden descriptions, drop the header slogan (v2.3.1)
- version check in pysimConnect() now compares the server major against
  SIMPLE_VERSION (2.3.1) instead of the hardcoded 1.x assumption; the header
  version is rendered from the same constant
- removed the 'SIM OTA with a Human Face' slogan (span, i18n key, translate
  hook) to free header space
- broaden the PWA description in manifest.json/package.json and the help
  overview (EN/RU): APDU workbench, OTA lab, CAT/STK simulator, card profiler
- version compatibility tables in README/RUS, help EN/RU and docs/api.md now
  describe the major-version rule
- README/RUS: Cards promoted to a top-level section; EN duplicate
  Theme/Localisation sections removed
- docs: /api/cardinfo documented; SCP81 'expose framing options' next-work
  item dropped (done); local AGENTS.md not-implemented note corrected
- SW cache simple-v190
2026-09-19 08:54:33 +03:00
catarrh ac18c3e2cf rename OTAMan → SIMple (v2.3.0)
- package pysim_otaman_server → pysim_simple_server; distribution and
  console script pysim-otaman-server → pysim-simple-server
- PWA branding SIMple: title, header, manifest, help EN/RU, GitHub links
- localStorage keys otaman_* → simple_* (clean break, no migration)
- export filenames simple-cards.json / simple-custom-files.json
- service worker cache simple-v189
- README/docs/setup/start scripts updated; no 'otaman' left in the tree
2026-09-19 08:25:20 +03:00
catarrh 6d5d23487a cards: read EF.ICCID at equip and auto-select the matching preset (v2.2.16)
- Server: _decode_iccid (nibble-swapped E.118 digits, trailing-F pad) and
  _read_iccid (best-effort MF/2FE2 read through the parent-scoped select
  helper; the previous selection is restored, the read never raises). The
  equip path records the digit string before the TERMINAL PROFILE, i.e.
  before any CAT session is active; a startup with a card does the same.
  The value is cleared on card removal and exposed as /api/status 'iccid'
  (only while connected).
- PWA: when a connected status update reports a *new* ICCID, the matching
  card preset is selected in both SCP80 views - Secured Packet (sp-card-sel
  + form fill) and RAM (ram-card-sel + _ramCardIdx). Matching normalizes
  digits and accepts the raw EF hex form, leading zeros ignored; a manual
  choice for the same card is kept until the next equip, and a card removal
  re-arms the auto-selection. The status line shows the ICCID.
- Tests: tests/test_iccid.py (decode variants, model + probe read, equip
  recording, disconnect clearing) and frontend/tests/cards_iccid.test.js
  (normalize, find, select, no-override, card swap); the card_state test
  harness stubs the new hook and covers the guard reset.
- Docs: api.md /api/status fields, help EN+RU (SCP80 intro + Cards tab),
  READMEs, AGENTS. SW cache otaman-v183.
2026-09-18 21:05:25 +03:00
catarrh a59e374ff9 scp81: always reuse the connection, collapsible options, label polish (v2.2.15)
- Keep-alive is no longer a setting: the TLS connection stays open between
  POSTs for the whole dialog (the card, as HTTP client, may still dial a new
  one at any time - GP Am. B 4.3.1) and only the 204 ends it, with a clean
  close_notify while the response is still buffered, then FIN. Dropped
  keep_alive/on_before_close/_scp81_wait_drained and the Connection-header
  'close' value (the API, status and UI no longer carry a keep-alive knob).
- The Listener Options block is a collapsed <details> with a 'custom' badge
  when anything differs from the reference defaults; the Reset button moved
  into the body so it cannot toggle the panel.
- X-Admin-Targeted-Application is opt-in (checkbox + //aid/... field, field
  disabled while off); X-Admin-Next-URI has a checkbox + hint explaining the
  one-shot rule of GP Am. B 4.4.2; both dependent fields grey out when
  unchecked.
- Labels/i18n: 'Chunked body (Transfer-Encoding: chunked)' stays English,
  'Show link events (...)', 'Теги comprehension-required'; the compact-header
  wording now spells out that it omits the optional space after ':' (legal
  per RFC 7230 3.2 OWS; saves one byte per header).
- Tests: a 200 keeps the socket for the next POST; the 204 closes with a
  mutual close_notify exchange; options helper/badge unit tests; removed the
  close-per-response and drain-wait tests. SW cache otaman-v181.
2026-09-17 09:25:23 +03:00
catarrh f4462c14e7 scp81: UI framing options, permissive TLS, drop the Apache-header mimicry (v2.2.14)
Listener Options block (applied at Start, persisted in localStorage, Reset to
defaults): HTTP framing - chunked body, chunk size (0 = one TLS record),
keep-alive, Connection header, compact headers, Next-URI (unchecked = omit);
script framing - indefinite/definite Command Scripting template, CR tag,
targeted app; link events (now parsed on the common start path, so every mode
honors them). scp81OptionsFromForm() is unit-tested.

TLS is automatic: 'auto' (min 1.0, max 1.2 + :@SECLEVEL=0) is the new
default, all six PSK suites are offered and OpenSSL negotiates the highest;
the negotiated version/cipher is logged (tls-handshake) and reported as
version_seen/cipher_seen in /api/scp81/status, and a handshake failing for a
TLS/cipher reason logs tls-handshake-failed (post-handshake record errors
stay tls-error). tls_version/cipher/keylog/answer_delay stay as API-only pins.

Dropped the Apache-style header mimicry completely: no Date/Server/
X-Powered-By, no Content-Length-before-Content-Type ordering, no Content-Type
on 204 - the minimal response set is X-Admin-Protocol (+ X-Admin-Next-URI /
Targeted-Application), Content-Type on 200s, and Transfer-Encoding or
Content-Length per the chunked flag. Docs, help (EN/RU), READMEs and the
AGENTS notes updated; SW cache otaman-v180.
2026-09-17 08:50:40 +03:00
catarrh 404fdf5a1c scp81: rename passthru to redirect, add a true passthru mode (v2.2.13)
The former 'passthru' mode is now 'redirect': it pins the configured target
and every BIP channel is connected there (the card's requested address is
only logged; host/port required). The name 'passthru' now means the new mode:
no listener and no target - each channel dials the destination the card
requests in OPEN CHANNEL (Other address + Transport level port, TCP client,
remote, 0x02 only). The specs define no default port (TS 102 223 8.59), so an
incomplete or non-TCP request fails the channel with result 3A and an
open-fail log reason.

BipTerminal gains a mode (enable(host, port, mode=...), open(..., proto=...)),
reports it in status(), and the control API/status expose redirect (target)
and passthru (per-channel targets). The PWA mode selector shows four modes
with per-mode notes and disables Host/Port in passthru; the status line shows
each channel's actual target. Docs, help (EN/RU), READMEs and the AGENTS notes
updated; SW cache otaman-v179.

Breaking API change: mode:'passthru' has the new semantics - use
mode:'redirect' for the previous behavior (no alias).
2026-09-17 08:17:28 +03:00
catarrh eaa2453853 http ota: spec-correct retry timer, TLV and APDU edges (v2.2.12)
Audit against GP v2.2 Am.B 4.7 / TS 102 226 / GP Card Spec 11.11 found the
retry waiting delay encoded as plain hex instead of the TP-SCTS semi-octet
order required by TS 102 223 8.38 -> TS 23.040 9.1.2.3 (1 min must be 10,
20 s must be 02); the builder now encodes semi-octets, clamps 0-59 / 0-99 and
pads the 2-byte counter. Odd-length hex is padded instead of producing
fractional BER lengths; empty 83/84/89 sub-TLVs are omitted (lengths are 1-n
per Tables 4-3/4-5/4-8..10) and an empty trigger becomes 81 00; a store
payload above a short APDU is chained as P1.b8=0 STORE DATA blocks (P2 =
block number). Connection presets fixed: device identities 82, alpha 05,
command details 81, bearer 35/03; the A5 store tag and the 'B0,00=unlimited'
counter hint are marked unverified (not in the pinned spec). Docs:
UICC_SPECS.md 9.6.5 example annotated 2 s, findings 10-minute timer corrected
to 1 minute; help updated. SW cache otaman-v177.
2026-09-17 00:23:56 +03:00
catarrh aa33d25466 terminal profile at runtime + canonical custom-file paths (v2.2.3)
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.
2026-09-16 20:46:40 +03:00
catarrh 20fbfd99a5 scp81 passthru, proactive decoders, ADM badge, file manager layout (v2.2.1)
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.
2026-09-16 15:29:01 +03:00
catarrh 8c90958718 scp81: PSK by identity, scripts page, exact snapshots, SCP80 LOAD fit (v2.2.0)
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.
2026-09-16 13:32:26 +03:00
catarrh f6c1dd0751 docs: installed applet verified in all registries
explore with the fixed response parser ran 18 commands / 10 continuation
pages (real 63 10/9000 statuses) and shows the installed applet:
AA1902BC22580101 (life=07 selectable, elf=AA1902BC225801) in the
applications listing, AA1902BC225801 (life=01) in the ELF registry and with
its module in the ELF+modules listing.
2026-09-16 08:42:13 +03:00
catarrh e957757fda scp81: BER length for the Response Scripting template TLVs (v2.1.17)
The R-APDU TLV length was read as a raw byte, so a listing page above 127
bytes (AF 80 23 81 FC <252 bytes> 00 00) was cut to its first 127 bytes with
a bogus status word (the data's last two bytes: CAFE/0001/9F70 instead of
the real 63 10 "more data"). The bogus SW also stopped the SW CAFE/6310
pagination, which is why later registry entries - e.g. the installed package
AA1902BC225801 - never showed up. Uses httpota.ber_len_read now.

Tests: 250-byte long-form page and short-form regression (204 python);
findings updated; service worker v154
2026-09-16 08:29:00 +03:00
catarrh c341571300 scp81: correct GET STATUS pagination and registry P1s (v2.1.16)
- continuation repeats the SAME GET STATUS command with P2.b1 set (the
  pagination state lives in the card); changing the 4F criterion is a match
  filter, not a position - P2=03 with the last AID is rejected with 6A80 and
  P2=02 with it returns that single match (the earlier duplicate)
- handle the standard "more data available" warning SW 63 10 (Table 11-38)
  in addition to the live card's proprietary CA FE
- explore script: P1=40 is applications+SDs, P1=20 the ELF registry, P1=10
  ELF+modules (Table 11-33) - the ELF-only registry was never queried, which
  hid the installed package; labels and the results decoder show C4 (ELF AID)
  and CC (SD AID) too
- UICC_SPECS.md: GET STATUS P1/P2 tables made explicit with the pagination
  rule, plus BER length coding notes for the scripting templates and the
  TS 102 223 channel data TLV (the two >127-byte traps)

201 python + 346 frontend; service worker v153
2026-09-16 08:22:59 +03:00
catarrh 3403abd6b9 scp81: continuation pages use GET STATUS P2=03 (next occurrence) (v2.1.15)
P2=02 means "get first or all occurrence(s)" (Table 11-34), so every
continuation re-returned the first listing (the criterion's single match),
pagination stopped after one extra page and the installed package
AA1902BC225801 never showed up in the ELF registry. P2=03 = "get next
occurrence(s)" is the correct value for the SW CAFE continuation.

Tests updated with the new continuation bytes; findings documented;
service worker v152
2026-09-16 08:12:36 +03:00
catarrh 5e3fac260b docs: RAM install over SCP81 live-verified end-to-end
INSTALL [for load] -> LOAD x6 -> INSTALL [for install] (SW 9000) against the
live card. Records the parameter-less 6A80 finding and the working
INSTALL [for install] command built from the Remote APDU -> RAM form and
queued via "Queue in SCP81".
2026-09-16 08:10:24 +03:00
catarrh b811906751 scp81: queue explicit commands, expanded-format output for the RAM chain (v2.1.14)
- POST /api/scp81/queue takes an explicit APDU list (or single APDU) and
  queues it as the SCP81 script; entries that already are Command Scripting
  templates (AA.../AE80..., the expanded format) are sent verbatim instead of
  being wrapped again
- Remote APDU -> RAM chain: "To expanded" builds each command in the
  TS 102 226 expanded form (AA definite / AE80 indefinite selector) and
  "Queue in SCP81" queues the built commands for the next card POST, so the
  full-featured RAM/INSTALL [for install] form (AIDs, privileges, TK/STK
  parameters) can drive the HTTP OTA install
- RU strings; api.md; service worker v151
2026-09-16 08:05:57 +03:00
catarrh ff7a1ad5d3 scp81: fix RAM LOAD block splitting (overlapping 1-byte shifts) (v2.1.12)
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.
2026-09-16 07:51:59 +03:00
catarrh 92f646df08 scp81: BER-encode the Command Scripting template lengths (v2.1.11)
The C-APDU TLV length was written as a raw byte: a 245-byte LOAD command
produced 'AE 80 22 F5 ...', which BER reads as a long-form marker, so the
card mis-parsed every script command over 127 bytes. Small commands worked,
which made a RAM install look alive: the card answered the LOAD steps with a
degenerate 'AF 80' body (no R-APDU), and the final INSTALL [for install]
failed with 6A88 because the package never loaded.

Both the indefinite ('22' TLV) and definite ('AA' outer) lengths are now
BER-encoded (same rule as the BIP channel data TLV fix). Tests: 245-byte
LOAD body, short form, definite variant (200 python, 346 frontend);
findings doc updated; service worker v148.
2026-09-16 07:45:14 +03:00
catarrh 3463a68292 scp81: RAM install over HTTP OTA (v2.1.8)
- 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
2026-09-16 02:05:20 +03:00
catarrh a352d8fe3f scp81: auto-continue SW CAFE listing pages (v2.1.6)
Long GET STATUS listings answer SW CAFE with 127-byte pages. The script
responder now extracts the last complete AID from the page and inserts a
next-occurrence GET STATUS (80F2 <P1> 02 <Lc> 4F <len> <AID> 00) as the
next command, until the listing ends. Pages are logged (script-page), a
repeated page logs script-page-stalled and stops, inserted continuations are
dropped at session start.

Live-verified: ELF registry and applications collected completely in two
pages each (7/7 commands, all script-status ok).

- tests: page parsing (truncated tails, live FC-prefixed junk), continuation
  bytes, auto-insert, stall guard, per-session purge (192 python + 337 frontend)
- UI: page number and script-status log rendering; service worker v143
2026-09-16 01:56:29 +03:00
catarrh 175ca934d8 scp81: PSK TLS server and command scripting (phases B/C) + BIP fix (v2.1.5)
- 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
2026-09-16 01:50:29 +03:00
catarrh 0370fa58ad release: v2.1.2
Version bumped in server.py, pyproject.toml, the PWA header and the
docs/api.md example; SW cache v131 -> v132.
2026-09-13 11:35:02 +03:00
catarrh dfb9551eb7 release: v2.1.1
Version bumped in server.py, pyproject.toml, the PWA header and the
docs/api.md example; SW cache v124 -> v125.
2026-09-12 22:25:07 +03:00
catarrh a5cccee17c release: v2.1.0
Version bumped in server.py, pyproject.toml, the PWA header and the
docs/api.md example; SW cache v122 -> v123.
2026-09-12 22:16:46 +03:00
catarrh ddc6cb8f9d fix: parent-scoped file selection; never mutate the pySim model
pySim's lchan.select() resolves names against global selectables (self +
parent chain + MF children + applications) and falls back to probe_file(),
which blindly SELECTs an unknown FID and permanently injects a dynamically
named DF.XXXX/EF.XXXX into the running filesystem model. Probing a whole tree
or scanning a snapshot with custom files therefore polluted the model, made
tree branches show children of the wrong object, and could persist phantom
files into snapshots.

- server: new _select_with_parent()/_select_path() walk the requested parent
  path (new parent_path field, parent_sel kept as legacy fallback) strictly
  through the model and call lchan.select_file() only; model-unknown 4-hex
  segments are probed only with allow_probe and the temporary child pySim
  adds is detached again via the cleanup callable that the four handlers
  (/api/tree|select|read|write) now run in a finally block
- frontend: getParentPath() builds the segment chain (MF, ADF names, FIDs)
  and all tree/select/read/write bodies plus the snapshot/profile walker send
  parent_path; allow_probe is set only for custom files; the blind retries
  in the file manager were dropped
- tests: tests/test_select_scope.py (duplicate-FID resolution, no APDU for
  unknown non-custom files, probe+detach, model unchanged); fs_load/fs_probe
  assertions for parent_path and allow_probe; docs/api.md and AGENTS.md
  document the contract; SW cache v117 -> v118.
2026-09-12 21:45:57 +03:00
catarrh f1ed6a2a48 release: v2.0.0 2026-09-12 15:08:04 +03:00
catarrh 6d30e7095e snapshots: measure SELECT / READ commands and show timing stats
Server measures every classified APDU (A4 select, B0 read binary, B2 read
record) from command to response: _collect_apdu_times() enables the tracer
(reattaching it if pySim nulled it) and /api/select + /api/read return
'apdu_times': [{type, ms}]. Collection is safe: handlers hold _CARD_LOCK.

Snapshots store per-file {select_ms, read_ms}, a ms value per record and
snapshot-level stats {select, read_binary, read_record}: {min, max, avg,
count} plus total_ms (wall time of the scan). The snapshot view gets a
summary under the title (files/records counts, scan time, min/avg/max per
command type) and shows select/read per file and read time per record.
Timings are display-only: checks, snapshots comparison and imports ignore
them (old snapshots simply show 'No timing data').

Tests: Python classifier/collection (tests/test_apdu_timing.py) and
frontend stats/accumulator/format/build-file/summary. SW cache v99 ->
v100; help, README and docs/api.md updated.
2026-09-12 14:56:59 +03:00
catarrh b50be83da4 server: guarantee a TERMINAL RESPONSE for paused proactive commands
Every FETCHed proactive command must be answered, otherwise the card is
left in an unfinished session and stops issuing commands (e.g. it will
not deliver a PoR for SEND SM). The menu handlers now share
_menu_send_response, and a server-side watchdog (_arm_menu_timeout /
_menu_timeout_fire, --menu-timeout, default 60s, 0 disables) sends the
timeout result (0x12) when the user never answers. The timer is armed
while a command is pending and cancelled on any response, equip, rescue
and card disconnect.

Also fixes docs/api.md, which had back (0x11) and timeout (0x12) codes
swapped. Tests for arm/cancel/clamping and the flat timeout TR. SW cache
v89 -> v90.
2026-09-12 12:31:22 +03:00