Commit Graph
129 Commits
Author SHA1 Message Date
catarrh a50a93a238 Decode the Items Next Action Indicator (TS 102 223 §8.24)
- tag '18' (TS 101 220: "18 only"): one byte per item, §9.4 coding;
  '00' and reserved values mean "no next action" (decoded to None)
- items[] now carry a per-item next_action; a list whose length differs
  from the item count is preserved raw (spec requires X = item count)
- detail view renders "Item 0xNN: text → COMMAND"; the APDU-list summary
  is unchanged — next actions are detail-only, as requested
- tests: 3 real SET UP MENU samples (local sessions 47/53/57) + a
  length-mismatch tolerance case
- v2.1.2; SW cache simtrace-analyser-v57
2026-09-24 20:16:31 +03:00
catarrh d4a378695c Decode cross-spec "reserved" values via the sibling specs
- REFRESH '07'/'08': use the TS 31.111 §8.6 names ("Steering of Roaming" /
  "... for I-WLAN", per TS 23.122 / TS 24.234) instead of "Reserved (...)"
- envelope tags 'E1'-'E3': TS 31.111 §9.1 leaves them reserved for future
  usage — drop the placeholder label and decode them as unknown
- TR result '28' = Bearer Independent Protocol temporary error (TS 102 223
  §8.12); '29' stays reserved
- PLI qualifier '0B' = MEID of the terminal added; '05' documented as
  TS 31.111's Timing Advance (ETSI text says "Reserved for GSM")
- ISO 7816-4:2005 Table 6: add '62 86' (sensor) and '63 81' (file filled)
- drop the two fabricated '66xx' names (no such rows in the pinned specs)
- tests: add the new value tests plus a guard that no decoder table emits
  a "Reserved ..." label
- v2.1.1; SW cache simtrace-analyser-v56
2026-09-20 14:06:40 +03:00
catarrh 561801a983 TERMINAL CAPABILITY: decode the A9 template (TS 102 221 §11.1.19)
- terminal power supply (tag 80): voltage class per Table 6.1, maximum
  current in mA, clock in 0.1 MHz steps ('FF' = not indicated)
- extended logical channels (tag 81) — zero length per §11.1.19.2.2, a
  nonzero length is preserved as raw
- additional interfaces (tag 82 b1 = UICC-CLF)
- eUICC device capabilities: tag 83 per GSMA SGP.22 v2.6 §3.4.2 Table 7
  (LUId/LPDd/LDSd/LUIe-SCWS); tag 84 preserved raw (no pinned SGP.32 spec)
- unknown/private TLVs preserved via raw_tlv
- concise APDU-list summary; decoded rows in the PWA detail view
- v2.1.0; SW cache simtrace-analyser-v55
- tests: 6 real samples from the local trace sessions (49/55/58/61/206/208)
2026-09-20 00:11:41 +03:00
catarrh 38650b4797 Rename project to SIMtrace Analyser (v2.0.0)
- move the analysis server out of simtrace2_pysniff into simtrace_analyser;
  simtrace2_pysniff stays as the simtrace2-sniff replacement capture utility
- new server CLI: simtrace-analyser-server; python -m simtrace_analyser
  (simtrace2-pysniff-server and python -m simtrace2_pysniff.server dropped)
- simtrace2_pysniff lazy-imports hardware symbols, so the server no longer
  pulls PyUSB at startup; the GSMTAP/PCAP codecs work without libusb
- version 2.0.0 everywhere (was 1.22.21); PWA SW cache simtrace-analyser-v54
- frontend: server name, clone URL and i18n strings updated; vendored-parser
  reference otaman -> SIMple
- README/README_RUS reworked product-first (analyser first, utility second)
- DB dir ~/.simtrace-analyser, /api status payload and hosted PWA unchanged
2026-09-19 12:34:16 +03:00
catarrh 8441b56e99 TERMINAL PROFILE: device characteristics in list + detail views
- decode.py: _decode_terminal_profile (TS 102 223 §5.2, b1 = LSB per the
  spec tables): class (declared ND/NK at byte 14 b6/b7, strict likely/
  handset rules), generation (positive-only 3G/HSDPA, 4G/E-UTRAN from
  byte 17 — never a 2G claim, real devices under-declare), BIP (byte 12
  b1, byte 13 bearers/channels, byte 17 transports), standards family
  (byte 18 IMEISV/ESN), UI facts (soft keys/screen/markup), era band
- list summary replaces raw hex: e.g. '3G/4G · BIP', 'modem · BIP',
  'no BIP', fallback '<n>-byte profile'
- detail view: tpRows() panel with class/reason, radio bits, BIP,
  standards, UI, profile length
- body row label for TERMINAL PROFILE: 'TLV data' -> 'Profile'
- tests: TERMINAL_PROFILE.md §10 regression anchors + synthetic
  declared-modem/handset vectors
- Bump 1.22.21, SW cache v52->v53
2026-09-19 01:39:34 +03:00
catarrh 2c373f50b7 Show Location Status value in EVENT DOWNLOAD list summaries
- _build_summary: fold cmd.location_status (Normal/Limited/No service)
  into the 'Location status' event entry, e.g. 'Location status: Normal
  service' (the detail view already showed it)
- tests: summary assertions for the Normal/No-service vectors
- Backend-only, no version bump
2026-09-19 00:43:58 +03:00
catarrh 04443878a7 Show destination address in MO SHORT MESSAGE CONTROL summaries
- _build_summary: append 'DA <tp_da>' after the SMSC part for the
  MO SHORT MESSAGE CONTROL envelope (the envelope decoder already
  stores the second address TLV as cmd.tp_da; the detail view already
  showed it)
- test: constructed D5 envelope with SMSC + destination address
- Backend-only, no version bump
2026-09-19 00:29:33 +03:00
catarrh 500e764a1b List view: decoded summaries for ATR/PPS/RST/VCC instead of raw hex
- decode.py: decode_sniff_msg builds list summaries — ATR: protocol(s),
  TA1 F/D + ratio, T=15 classes, data rate (when a CLK rate was known);
  PPS: protocol + F/D + ratio; RST: 'CLK x.xxx MHz' when the event
  carried a rate
- index.html: cmd_description returns summary || '' for these types and
  STATUS, never falling back to opaque bytes like 010100
- tests: ATR (plain/TA1/classes+rate/unparsed), PPS, RST CLK/no-CLK, VCC
- Bump 1.22.20, SW cache v51->v52
2026-09-19 00:17:47 +03:00
catarrh ccbb3f231b STATUS list entries: suppress raw hex when empty, describe FCP data
- decode.py: _build_summary emits 'Data: N bytes — FCP of <file> (...)' for
  STATUS responses with data (FCP already decoded into decoded.response);
  _fcp_brief builds the 'FCP of MF (DF or ADF)' / 'FCP of EF_xxx (...)'
  fragment
- index.html: cmd_description suppresses the raw-hex fallback for
  body-less STATUS rows (nothing to describe beyond type + SW)
- tests: no-data STATUS summary None; DF/EF FCP summaries; pluralization
- Bump 1.22.19, SW cache v50->v51
2026-09-19 00:10:09 +03:00
catarrh 39ee173a7d Rename RST de-asserted label to 'RST de-asserted — ATR follows'
- More precise than 'ATR STARTS' (the ATR starts after the de-assert
  delay, not at the edge) while staying human-friendly; reads as a pair
  with 'RESET ASSERTED'
- Update tests and both READMEs (backend-only, no version bump)
2026-09-18 23:59:45 +03:00
catarrh a46667d93f Decode standard PPS sub-types + RST CLK extension → ATR data rate
- capture.py: map GSMTAP PPS request/response (0x02/0x03) to 'pps',
  matching the pcap-import mapping (was decoded as TPDU live)
- decode.py: parse the optional RST CLK extension (sigrok v1.9.0+,
  payload flag bit 0); ATR gains clk_hz/data_rate/etu_us derived from
  TA1 Fi/Di (data_rate = clk_hz × D / F)
- database.py: CLK state is consumed by the next ATR (no stale rate
  for an ATR without preceding RST); a late-arriving RST measurement
  is backfilled onto the ATR it belongs to; replay context carries
  the CLK across pagination/live polling
- PWA: ATR detail shows CLK/Data rate/ETU; RST event detail shows CLK
- tests: PPS mapping, CLK parse with/without flag, rate math,
  DB consume + backfill round-trips
- README/RUS updated
- Bump 1.22.18, SW cache v49->v50
2026-09-18 00:30:26 +03:00
catarrh c2f605a391 Add sigrok_iso7816_stream to related projects in both READMEs 2026-09-11 08:05:06 +03:00
catarrh dbaa23b3da Add Russian README (README_RUS.md) + language links
- Full Russian translation of README.md
- Cross-link English/Russian versions
- Note Windows sniff-start.bat in startup section (no longer Linux-only)
2026-09-11 08:03:34 +03:00
catarrh ed7e99d671 Rename server-start.* to start.*, add sniff-start.bat
- git mv server-start.sh -> start.sh, server-start.bat -> start.bat
- Add Windows sniff-start.bat mirroring sniff-start.sh options
- Update usage comments and PWA help text references
- Bump 1.22.17, SW cache v48->v49
2026-09-11 08:02:15 +03:00
catarrh c7b87cb587 Widen APDU list vertical scrollbar to 12px
- .apdu-list::-webkit-scrollbar override (global default is 6px)
- Bump 1.22.16, SW cache v47->v48
2026-09-10 00:01:41 +03:00
catarrh ede38c985e Fix session name label language switch + add frontend screenshot
- Store raw session name in STATE.currentSessionRawName
- Re-render session-name-display in toggleLang() with current translation
- Add frontend.png screenshot to README
- Bump 1.22.15, SW cache v46->v47
2026-09-06 22:05:25 +03:00
catarrh af16eb1dd8 Add grip dots visual indicator to splitter bar
- ::after pseudo-element with radial-gradient dot pattern
- Dots darken on hover/active for contrast against accent background
- Bump 1.22.14, SW cache v45->v46
2026-09-06 21:52:13 +03:00
catarrh 347ff5387b Clear detail view on capture start and session load
- Reset apdu-detail to empty state in startCapture() and loadSession()
- Bump 1.22.13, SW cache v44->v45
2026-09-06 21:44:02 +03:00
catarrh 52243f496c Add draggable splitter between APDU list and detail view
- Insert .splitter div between .apdu-list and .apdu-detail
- Replace flex:1 / max-height:40% with --detail-height CSS var
- Drag handler with clamp 10%-80%, persisted in localStorage
- Bump 1.22.12, SW cache v43->v44
2026-09-06 21:36:33 +03:00
catarrh 7d8dca1ffd Fix GSMTAP capture restart: recreate UDP socket on start after stop
GsmtapListener.stop() closed the UDP socket permanently, but start()
only set _running=True without recreating it. Second capture session
would silently spin on the dead socket (OSError → None → continue).

- Add _closed flag to GsmtapReceiver, set in close()
- Store bind_port in GsmtapListener for socket recreation
- start() recreates GsmtapReceiver only if previous one was closed
- Uses getattr for backward compat with test fakes
2026-09-06 21:09:04 +03:00
catarrh 78c255464e Fix list view row border visibility in light theme + vertical-align buttons
- Change row border from var(--bg-input) to var(--border)
- Add vertical-align: middle to td cells
- Bump 1.22.11, SW cache v42->v43
2026-09-06 14:53:21 +03:00
catarrh 3dc59539d4 Fix list view name input dark mode + widen Name column
- Add .sessions-list input[type=text] with themed background/color/border
- Set Name column min-width 200px, input width 100%
- Bump 1.22.10, SW cache v41->v42
2026-09-06 14:44:27 +03:00
catarrh 2f49ee5f0f Show file size from FCI tag 0x80 in SELECT response detail view
- Bump 1.22.9, SW cache v40->v41
2026-09-06 14:14:29 +03:00
catarrh 340017c7cc Unify session action button styles: same padding, delete in red
- Add .session-copy CSS (was unstyled — browser default)
- .session-download/.session-copy: neutral gray, same padding 4px 12px
- .session-delete: red (--danger) instead of accent
- Bump 1.22.8, SW cache v39->v40
2026-09-06 14:04:54 +03:00
catarrh f9e399183c Fix imported PCAP session duration for relative timestamps
SIGROK-derived PCAPs have relative timestamps (0, 1.37, 15.02) rather
than epoch time. The _REAL_TS_MIN check failed, so close_session() set
ended to wall-clock (same as started → duration 0s). Now close_session
accepts an optional duration_secs param; the import handler passes
last_ts - first_ts so the session gets the correct capture duration.
2026-09-06 13:01:46 +03:00
catarrh 05d7a26f2e Reduce vertical padding for session list view rows (8px->4px)
- Bump 1.22.7, SW cache v38->v39
2026-09-06 12:08:46 +03:00
catarrh 1502ff80e3 Add list/table view for sessions panel alongside tiles view
- Toggle buttons (Tiles/List) in sessions header, preference
  persisted in localStorage
- List view: table with Name, Date, Duration, Messages, Mode, Actions
  columns; same fields and buttons as tiles, just tabular layout
- Refactored loadSessions() into buildGridHTML/buildListHTML helpers
  with shared attachSessionListeners() for both views
- i18n: Tiles, List, Name, Date, Duration, Messages, Mode, Actions
  keys added to LANG_RU
- Bump 1.22.6, SW cache v37->v38
2026-09-06 11:57:23 +03:00
catarrh 7e537abc55 Warn on data length mismatch in APDU list; rename SIM-ME to UICC
- error_names(m) now includes 'Data length mismatch' when
  decoded.length_mismatch is set, showing a ⚠ badge + tooltip
  in the APDU list row (alongside existing errors like bad FCS)
- Header title: SIM-ME -> UICC
- Bump 1.22.5, SW cache v36->v37
2026-09-06 11:04:38 +03:00
catarrh 0ec7db8d34 Move capture button left of search; always visible; disabled when server-disabled
- capture-btn moved to first child of trace-topbar (before search input)
- Button always visible: shows Start/Stop/Connect-to-server as before
- When server capture is disabled: grayed-out Start Capture with
  tooltip via existing i18n key 'Capture is disabled ...'
- No more hiding button when viewing saved session or when disabled
- Bump 1.22.4, SW cache v35->v36
2026-09-06 10:45:11 +03:00
catarrh 0fac73833d Log session open (load saved session for viewing)
- _handle_get_session now logs: Session opened: session=1
  name=my capture messages=1200
2026-09-05 23:47:25 +03:00
catarrh 811a6a1ed7 Make Stop Capture button red and prominent
- Add --danger/--danger-hover CSS variables (dark + light themes)
- .stop button now uses red background + white text with brighter hover
  (was muted gray, easy to miss)
- Bump 1.22.3, SW cache v34->v35
2026-09-05 23:41:49 +03:00
catarrh 70d288fd0f Log sessions by default, HTTP requests opt-in via --log-requests
- HTTP request logging now off by default (log_message guarded by
  RequestHandler.log_requests); --log-requests enables it
- Add session lifecycle logging: POST /api/sessions/import and
  DELETE /api/sessions/<id> emit session+name+messages/bytes or
  session= lines via the shared _log() helper
- Capture start/stop and periodic status keep logging as before
2026-09-05 23:36:14 +03:00
catarrh 6bdbbea209 Show T=15 as PPS capability, not a transport protocol
T=15 in the ATR is a Protocol-and-Parameter-Selection capability,
not a data protocol like T=0/T=1. Previously 'Protocols: T=0, T=15'
was misleading.

- _decode_atr: exclude T=15 from protocols, set pps=True instead
- TCK presence: only-t0 rule now also requires no PPS (T=15 implies TCK)
- PWA ATR detail: 'Protocol: T=0' + 'PPS: supported' rows
- +1 test with the real UICC ATR (3B 9F 95 80 1F C7 ...)
- Bump 1.22.2, SW cache v33->v34
2026-09-05 23:20:16 +03:00
Трошин Антон Валерьевич 6376fb1591 Add Windows batch script for server startup 2026-09-01 16:21:52 +03:00
catarrh 674a979645 Enrich per-event RST/VCC labels (ATR STARTS / power-up) + decode server-side
- decode_line_event adds human label: RST de-asserted->ATR STARTS,
  VCC applied->VCC ON (power-up); keep canonical event string
- Enable rst/vcc in DB decode dispatch so PWA receives decoded
  (fixes bare RESET/VCC fallback and a latent showDetail crash)
- PWA line_event_label/showDetail render decoded.label
- Bump 1.22.1, SW cache v32->v33; +1 test
2026-08-27 00:07:35 +03:00
catarrh e4ac80fb34 Decode EF_IMPU/IMPI/DOMAIN empty records as empty, not text
_decode_nai (decode.py) only treated an all-0xFF record as empty.
A record that is a lone identity tag, or a tag with a zero-length
value (e.g. 80 00 + FF erase filler — a standard unused EF_IMPU
record) slipped through to an ascii fallback and rendered as garbage
text (the "Text: �" artifact).

- Treat a lone tag (80 / 81) or tag+zero-length-value (80 00 / 81 00)
  as empty.
- Only decode the TLV *value* bytes (UTF-8); never feed the TLV header
  or trailing FF filler to the text decoder.  Drop the fragile
  raw.rstrip(...).decode('ascii','replace') fallback.

Tests: +4 (empty 80 00+FF record, all-FF, real SIP URI, lone tag).
Backend-only; stays on 1.22.0.
2026-08-26 23:23:56 +03:00
catarrh 6887e53144 Add --log-interval flag and timestamp capture logs
Follow-ups from the capture-reliability review:
- All server-side capture log lines are now prefixed with a local
  timestamp (YYYY-MM-DD HH:MM:SS) for easier correlation.
- New --log-interval SECONDS CLI flag (default 60) tunes the
  "Capture alive" heartbeat interval; passed through to CaptureManager.

Stays on 1.22.0 (backend-only). 369 tests pass.
2026-08-26 22:35:59 +03:00
catarrh 51e1b4ae08 Keep GSMTAP capture alive + server-side liveness logging
The GSMTAP capture thread died silently whenever a single non-SIM or
truncated GSMTAP datagram arrived (read_packet() raises ValueError,
uncaught in the loop, killing the daemon thread while UDP kept
flowing) — matching the "stops listening after a sniffer restart"
symptom.

Fixes:
- GsmtapListener.iter_messages() now catches ValueError/struct.error
  from read_packet(), drops the offending datagram (counted via a new
  `dropped` counter) and keeps listening.
- CaptureManager._capture_loop() guards the DB insert so a transient
  error can't kill the thread; a top-level guard logs
  "ERROR: capture thread stopped on error …" + traceback and clears
  the session so /api/status no longer reports a stuck "active".
- Server now logs capture liveness to stderr: "Capture started", a
  periodic "Capture alive: session=N messages=M bytes=B dropped=D"
  heartbeat (default every 60 s, configurable via log_interval), and
  "Capture stopped" (or "empty session discarded").

Tests: +5 (listener survives non-SIM/short packet, capture loop
survives insert error, logs stop-on-error, heartbeat+stop log).
Stays on 1.22.0 (backend-only).
2026-08-26 22:21:14 +03:00
catarrh 79acb3773f Address code-review nits for BAD_FCS support
- decode.py: use the GSMTAP_FLAG_BAD_FCS constant instead of a literal
  0x01 in gsmtap_flag_names() (import from ..gsmtap); no behavior change.
- tests: add a DB round-trip test (insert_message with BAD_FCS flag →
  get_messages → decoded.errors) covering the end-to-end path, not just
  the direct decode_sniff_msg call.
2026-08-26 22:11:29 +03:00
catarrh 66094a0114 Surface GSMTAP BAD_FCS desync flag end-to-end
sigrok-iso7816-stream sets GSMTAP_FLAG_BAD_FCS (0x01) in the GSMTAP
header `res` byte on desynced TPDUs (mis-framed exchanges kept for
forensics). Propagate that flag through the whole pipeline so such
packets are explicitly surfaced instead of decoding as garbage:

- gsmtap.py: GSMTAP_FLAG_BAD_FCS const; GsmtapReceiver.read_packet()
  now returns (sub_type, data, flags) from the res byte.
- server/capture.py: GsmtapListener forwards the flags to the DB.
- pcap.py: _parse_gsmtap_hdr/_scan_gsmtap/extract_gsmtap and
  parse_pcap/parse_pcapng now yield a (ts, type, payload, flags)
  tuple, so the flag survives a PCAP/PCAPNG round-trip.
- server/server.py: PCAP import stores the flags.
- server/decode.py: gsmtap_flag_names() maps bit 0x01 to
  'bad FCS (desynced)'; merged into decoded errors for tpdu/atr/pps
  and rst/vcc, so the frontend ⚠ badge + Errors row flag the entry.

User-visible: desynced TPDUs show a ⚠ "bad FCS (desynced)" marker
in the trace and detail views. Tests: +4 (decode BAD_FCS markers,
PCAP flag round-trip); existing tests updated for the new tuple
arities. Version 1.22.0, SW cache simtrace-analyser-v32.
2026-08-26 18:08:25 +03:00
catarrh 7eaf7e9b53 Decode sigrok-iso7816-stream RST/VCC line events (GSMTAP 0x10/0x11)
Support the sigrok-iso7816-stream decoder (FX2 logic-analyzer passive
SIM sniffer, github.com/anttro/sigrok_iso7816_stream) which streams
GSMTAP-SIM on UDP 4729 and adds two custom sub-types for line events.

- gsmtap.py: GSMTAP_SIM_RST_EVENT (0x10) / GSMTAP_SIM_VCC_EVENT (0x11).
- capture.py: gsmtap_msg_type() maps 0x10→'rst', 0x11→'vcc' (shared by
  the listener and tests); pcap.py _SUB_TYPE_NAMES gains the same
  entries so PCAP re-imports of these captures preserve the events.
- decode.py: decode_line_event() decodes [direction, level, reserved]
  into reset asserted/de-asserted or power applied/removed + level;
  routed through decode_sniff_msg.
- database.py: reset-assert and power-removal invalidate the per-channel
  file selection (card-level transition); de-assert / power-on are
  markers only.
- frontend: RST/VCC labels (RESET ASSERTED / VCC OFF, …), a detail-view
  branch (Event + Level + raw), and list styling (.apdu-type.rst/.vcc).

README notes the custom sub-type compatibility. Tests: +8 (line-event
vectors, sniff routing, subtype + PCAP round-trip mappings) — 359
total. Version 1.21.0, SW cache simtrace-analyser-v31.
2026-08-26 17:29:36 +03:00
catarrh 3e21d093d9 Compact C-APDU detail rows; drop duplicated Data hex
- Pair CLA+INS and P1+P2 into single rows (halves the header rows
  per C-APDU): "· CLA, INS  00 — UICC, A4 — SELECT".
- Skip the " — desc" suffix when the parser's desc merely repeats
  the hex (generic Data fields showed "6F46 — 6F46").
- Restore the implied-CLA annotation on the C-APDU title row (was
  dead code behind an operator-precedence bug).

Version 1.20.2, SW cache simtrace-analyser-v30.
2026-08-24 23:05:58 +03:00
catarrh acbd2bda4a README: document C-APDU decoding of secured-packet payloads 2026-08-24 22:55:06 +03:00
catarrh 6d7e87b52a Add node:test suite for vendored C-APDU parser and SCP80 decryption
frontend/tests/ (otaman-style function extraction from index.html):
- apdu_parse.test.js: PARSE_INS registry, parseOneApdu (SELECT by FID,
  READ BINARY Le-form, UPDATE BINARY, GET DATA, lowercase input),
  parseCompactApdus (concatenated implied-CLA stream, graceful
  unrecognized-byte degradation).
- scp80_decrypt.test.js: decryptSecured known-answer tests with
  node-crypto generated ciphertexts — 3DES-CBC 2/3-key (real CNTR=200
  revealed), AES-CBC 128, single DES, wrong-key padding guard, key
  length validation, unsupported-algorithm rejection.
- helpers.js: brace-matched function/const/section extraction from
  the single-file frontend; package.json ("test": "node --test").

Fix AES-CBC decryption path found by the new tests: SubtleCrypto CBC
decrypt always validates PKCS#7, which SCP80 PCNTR-style padding
violates. Workaround: append a synthetic block C* = E(C_last XOR
padBlock) so the validator sees a full 0x10 padding block, decrypt,
then discard the synthetic plaintext. Validated: CNTR=200 case
decrypts correctly.

Version 1.20.1, SW cache simtrace-analyser-v29.
2026-08-24 22:34:23 +03:00
catarrh 6da5acca21 Decode C-APDU commands inside secured-packet payloads (frontend)
The secured data of an SCP80 packet is a stream of C-APDUs (e.g.
SELECT / UPDATE BINARY sequences for OTA file management). Vendor the
working C-APDU parser from the otaman project (frontend-only, ~10 KB):
PARSE_INS table, describeP1P2, describeInstallDataLv, parseOneApdu and
parseCompactApdus (concatenated streams with implied-CLA handling).

Detail view now shows an "Application message" section for secured
packets:
- unciphered packets (SPI1.b3=0): the on-wire secured data decodes
  immediately;
- ciphered packets: decodes from the client-side decrypted payload
  once the user supplies the key (before that, ciphertext rows only);
- each C-APDU renders as a numbered row with indented CLA/INS/P1/P2/
  Lc/Data/Le + descriptions (nested LV for INSTALL), with implied-CLA
  annotation; raw-hex fallback when nothing parses;
- vendored parser uppercases input (otaman's expects uppercase hex).

Tests deferred to the follow-up step (node:test infra, planned).
Version 1.20.0, SW cache simtrace-analyser-v28.
2026-08-24 21:55:15 +03:00
catarrh 6f058392f8 README: document client-side secured-packet decryption 2026-08-24 21:23:05 +03:00
catarrh d21997764f Client-side SCP80 decryption: pure-JS 3DES/AES with user-supplied keys
Adds optional decryption of ciphered secured packets, entirely in the
browser — the key never leaves the frontend.

- decode.py: _decode_secured_packet exposes 'cipher_block' (the
  contiguous on-wire ciphered octets CNTR..end, ready for CBC) and
  raw kic/kid bytes for algorithm selection. Additive only.
- index.html: pure-JS DES/3DES core (FIPS 46-3 bit-array tables,
  parity ignored) + CBC; AES-CBC via native SubtleCrypto; both with
  zero IV per TS 102 225. decryptSecured() picks the algorithm from
  the KIc nibble (02 AES-CBC, 05 3DES-2K, 09 3DES-3K, 01 DES) with
  key-length validation.
- UI: ciphered packets show a key-hex input + Decrypt strip below the
  detail table; keys are memoized per key-set for the session in an
  in-memory OTA_KEYS map — never sent to the server, never persisted.
- Decrypted results cached by cipher_block (survive live-polling
  re-renders); plaintext rows rendered as CNTR/PCNTR/RC/Secured data
  (decrypted) alongside the ciphertext; wrong-key guard via the
  decrypted padding counter (PCNTR > remaining → "invalid padding").
- 3DES implementation validated byte-identical against openssl
  des-ede3-cbc (3-key, 2-key ×3 random vectors, single-key).

No CC verification (out of scope). Tests: +2 (cipher_block
contiguity, AES registry guard) — 350 total. Version 1.19.0,
SW cache simtrace-analyser-v27.
2026-08-24 21:12:24 +03:00
catarrh 840e233baf Flag ciphered SCP80 fields: on-wire CNTR/PCNTR/RC are ciphertext
User-reported: the decoded CNTR (0x1830A44E41 ≈ 104 billion) cannot
be the real counter — the OTA platform shows ~200 for that card.

Root cause: per TS 102 225 Table 2 note 1, when SPI indicates
ciphering the whole block from CNTR through the secured data
(CNTR, PCNTR, RC/CC/DS, secured data) is ENCRYPTED. Only CPI, CPL,
CHI/CHL, SPI, KIc, KID and TAR travel in the clear. Our decoder
parsed the ciphertext bytes as if they were the real CNTR/PCNTR/RC
and presented them as such — misleading (the true values are only
recoverable after 3DES decryption with the card's keys).

Fix:
- _decode_secured_packet: emit 'ciphered': True when SPI1.b3 is set
  (wire hex values kept for reference); docstring documents the
  ciphered block.
- _decode_response_packet: accepts ciphered (command SPI2.b5, PoR
  ciphering) and flags the response likewise; database context now
  carries por_ciphered from the ENVELOPE so the GET RESPONSE PoR
  inherits it. Standalone SMS-SUBMIT PoRs carry no SPI — state
  unknowable, defaults to False (documented).
- Frontend: CNTR/PCNTR/RC-CC-DS labels get "(encrypted)" when the
  respective packet is ciphered (command, response, and ENVELOPE
  response renderings).

Tests: +4 TestSecuredCiphering (ciphered flagged, unciphered plain
CNTR e.g. 200, response flag, PoR-via-context). 348 total. Version
1.18.0, SW cache simtrace-analyser-v26.
2026-08-24 19:51:40 +03:00
catarrh 14445ae38f Fix EF_SPDI decode crash: parse inner '80' TLVs, emit dict plmns
stk_test1 msg 14036 regression: READ BINARY of EF_SPDI returned
"No decoded data available" — _decode_spdi (added v1.16.0) parsed
the A3 container value as bare 3-byte records (missing the inner
'80' value TLVs) and returned a list of strings, while
_build_summary/_file_summary expects the EF_PLMNwAcT shape
(list of {mcc, mnc} dicts). The resulting TypeError aborted the
whole message decode — this was the source of the 28 "Decode
error ... string indices" messages seen across all sessions.

_decode_spdi now unwraps inner '80' TLVs (bare-records fallback
kept) and always returns {'plmns': [{mcc, mnc}...]}. Replay of all
33 sessions: decode exceptions 28 → 0 (2 remaining undecoded tpdu
are 1-byte truncated capture records, not decodable by nature).

Tests: +3 (standard inner-80 form, bare-records fallback, exact
stk_test1 regression via decode_message). 344 total. Version
1.17.1, SW cache simtrace-analyser-v25.
2026-08-24 13:03:31 +03:00
catarrh 07deab9c26 Show absolute wall-clock time in the APDU detail heading
Next to the relative elapsed time, the detail-view header now shows
the absolute local time (YYYY-MM-DD HH:MM:SS.mmm) computed as
session start + message elapsed offset.

- STATE.sessionStarted populated from /api/capture/start (live
  capture), data.session.started (session view), and PCAP imports
  (sessions.started already derives from the first packet timestamp
  when real).
- Graceful fallback to relative-only when the session start is
  unknown; applies to all detail header variants (gap, no-decoded,
  state change, Fi/Di, ATR/PPS, APDU).

Version 1.17.0, SW cache simtrace-analyser-v24.
2026-08-24 08:53:27 +03:00