Compare commits

..

188 Commits

Author SHA1 Message Date
catarrh c9ae494f62 fix: snapshot/profile scans skipped DF subtrees (parent_path off-by-one)
profilerScanCard's walkDir passed each child dir's own segment in
parent_path (dir.parentPath.concat(childSeg) as the child's parent path),
so /api/tree for every DF child walked the target as its own parent and
failed: DF.GSM, DF.TELECOM and DF.GSM-ACCESS subtrees were silently
missing from New snapshot, Profile from card and Profile from snapshot
(ADF children survived only because application names/AIDs resolve
globally).

- walkDir now stores each dir's full path and sends
  parent_path = fullPath.slice(0, -1) (omitted for MF); the child's full
  path is fullPath.concat(aid/fid/name)
- verified against the running server: the walker now enumerates 189 EFs
  and captures 92, matching the file-manager probe exactly (was 111/48)
- test: fake MF -> DF.GSM tree asserts parent_path [undefined, ['MF']]
  and that both subtrees' rules are produced; SW cache v132 -> v133.
2026-09-13 21:39:15 +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 2213e25385 ui: generate a profile from a card snapshot
- new 'Profile from snapshot' button in the Profiles toolbar: reuses the
  snapshot picker (profilerSnapshotPickListHtml refactor) and then opens
  the same scan form as Profile from card (profilerScanOpenForm), with
  the profile name prefilled from the snapshot
- new profiler-build target 'profile-snapshot': the form's ignore list,
  mask options and FCP/FCI mode apply to profilerScanSnapshot(), which
  walks the snapshot's captured files instead of the card; uncaptured
  contents produce rules without a content check, record files map to
  exact record content, transparent files honor the masks
- profilerScanStart() pushes the generated profile and opens the editor
  like the live scan; _scanSnapshot is cleared in the finally block
- RU entry 'Профиль из снимка'; tests for the snapshot rule builder
  (exact/mask/ignore/uncaptured/record), the snapshot scan progress, the
  picker wiring and the form title, plus a structural button check;
  help EN/RU and READMEs updated; SW cache v130 -> v131.
2026-09-13 11:32:18 +03:00
catarrh bf8abbb0fd ui: descriptive check-results headers with the check type first
- new profilerResultsHeaderText() builds the header from a structured
  {kind, from, to}: profile checks read 'Profile verification results
  for: <profile> -> <card ICCID | snapshot name>', snapshot comparison
  reads 'Snapshot comparison results: <master> -> <checked>' (the arrow
  matches the one already used in the compare header)
- profilerRunProfile() stores the header and profilerRenderResultsView()
  writes it, so the header follows a language switch and the ICCID stays
  current; RU keys added ('Результаты проверки профиля:',
  'Результаты сравнения снимков:')
- tests: header builder variants + render wiring; flow tests now expect
  header objects; help EN/RU and READMEs updated; SW cache v129 -> v130.
2026-09-13 11:23:59 +03:00
catarrh a9a4d03b09 ui: label check reports with the profile, card ICCID / snapshot name
- new profilerLabelText() resolves expected/actual labels at render time;
  profilerRenderReport and fcpDiffHtml (including the decode-failure
  notes) use it, so a language switch keeps the labels localized
- Check card: reads EF.ICCID via /api/read (MF/2FE2) and labels the
  report 'expected (<profile>)' / 'actual (<card ICCID>)'; the results
  title becomes '<profile> — <ICCID>' (falls back to the plain title and
  'actual' when EF.ICCID is unreadable)
- Check card snapshot: labels 'expected (<profile>)' / 'actual
  (<snapshot name>)
- snapshot-vs-snapshot compare keeps verbatim master/check names;
  the mismatch label column widens to w-48 for prefixed labels
- tests: profilerLabelText, prefixed report/diff labels, profilerCardIccid
  decode/fallback, both check flows pass the labels; help EN/RU, READMEs
  updated; SW cache v128 -> v129.
2026-09-13 11:14:19 +03:00
catarrh fb6f80ef1d ui: translate the RAM operations hint
The data-l10n key for 'RAM operations perform atomic GlobalPlatform
commands over SCP80. Card keys are taken from the saved preset.' was
missing from LANG_RU, so the hint stayed English. SW cache v127 -> v128.
2026-09-12 23:25:42 +03:00
catarrh 2494e04984 ui: clear stale RAM status on op change, remember the card preset
- ramOpChanged() now clears the previous execution status (result line,
  steps, explorer, progress) only when the Operation value actually
  changes, so switching Explore -> Install Package no longer shows the
  old 'Partial - ELF Modules: no data' until Execute; re-entering the
  RAM subtab still preserves the last result
- ramClearResults() also hides the progress caption
- the Card preset dropdown no longer resets to the placeholder after
  every executed operation (ramSaveCntr -> ramRender rebuilt it) or on
  subtab re-entry: ramRender keeps the current/remembered index,
  ramApplyCard and ramExecute remember the selection for the session,
  and cardsRemove keeps _ramCardIdx aligned via ramCardIdxAfterRemove
- tests: op-change clearing, selection preservation across rebuilds,
  pick/execute remembering, index bookkeeping; SW cache v126 -> v127.
2026-09-12 23:20:26 +03:00
catarrh b49e6728b4 ui: translate SCP80/RAM explorer labels, buttons and result strings
The RAM explorer HTML is generated after load, so its data-l10n sections
and Delete/Delete All buttons stayed English until a language switch, and
the memory/AID/lifecycle labels were hardcoded with no key at all.

- ramRenderExploreHtml() now builds every label and button with t()
  (reusing the existing Delete/Delete All/heading/AID keys and wiring the
  three previously-unused Application/Instance, Load File and Module AID
  keys); new RU keys cover Applications:, Free NV:, Free Volatile:, AID:,
  Lifecycle:, Privileges:, SD AID:, Implicit sel:, Version: and (none)
- the explorer dataset is cached in _ramExplorerData and re-rendered by
  refreshDynamicI18n() on language switch (cleared on op change/results
  reset)
- RAM flow strings localized too: Partial/OK summaries, error labels
  (Memory/ISD/Apps/ELFs/ELF Modules/no data), GET DATA progress, delete
  confirm/labels, install alerts/progress/steps and Error:
- tests: ram.test.js asserts every explorer label/button goes through t()
  and the (none) placeholder; SW cache v125 -> v126.
2026-09-12 23:07:28 +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 8459040856 ui: auto-refresh the proactive log and STK menu state
The proactive command list only re-rendered on tab/pill switches, so new
fetched commands (background STATUS polling, menu traffic) stayed
invisible while the Phone view was open.

- /api/status now exposes proactive_seq (_PROACTIVE_ENTRY_ID, monotonic
  and not reset with the log), so the existing 2s status poll detects
  changes with no extra request; pysimCardStateUpdate() re-renders the
  log when the sequence changed and the Phone/Phone view is visible
- the 5s backend timer no longer fetches the log (it was discarded) and
  now uses the already-fetched stk-status: when active/pending changes
  while the Phone tab is visible, stkCheckMenu() refreshes the menu
  button without a tab switch
- pysimProactiveLogRender() keeps its scroll position and only shows
  Loading... on a first/empty paint
- tests: proactive seq + STK signature helpers and poll-driven render
  behaviour in card_state.test.js; SW cache v123 -> v124.
2026-09-12 22:22:55 +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 2a559371ae ui: keep the selected file in state, drop the File: title line
The File: EF.DIR [LF] line doubled as the selection store: pysimFsClickFile
wrote a formatted display string there and pysimFsRead/Save parsed it back
(stripping '✗ ' and ' [TYPE]'). The pane now keeps the name in
pysimFsSelected, the redundant title is gone (FID/type/size/FCI remain in
the info block), and the dead pysimFsSelect() plus the unused 'File:' RU
entry are removed. Read/Save still re-select on the server first: pySim's
current selection is a shared cursor that tree expansion, scans and
commands move, so it cannot identify the file shown in the pane.

SW cache v121 -> v122; structural test updated.
2026-09-12 22:12:25 +03:00
catarrh e0e86e31af ui: drop the Decoded FCI caption in the file manager info block
The decoded FCI items now follow the FID/type/size line directly; the
snapshot detail view keeps its caption. Test updated to assert the
caption is absent; SW cache v120 -> v121.
2026-09-12 21:59:41 +03:00
catarrh 2ec7a2d6c0 ui: show FID, layout and decoded FCI in the file manager detail pane
- selecting an EF now renders an info block between the File: title and
  the contents: FID, file type, size, record length/count (nulls skipped)
  followed by the decoded FCI via the existing profilerFciPreviewItems()
  decoder (no raw hex), matching the snapshot detail view
- pysimFsInfoHtml() is pure and reused by pysimFsClickFile(); the [LF]
  shorthand stays in the title; custom files get the same metadata from
  the temporary select probe
- tests: pysimFsInfoHtml in profiler.test.js (full response, missing
  fci_hex, skipped nulls) and a structural check for #pysim-fs-info;
  help EN/RU and READMEs updated; SW cache v119 -> v120.
2026-09-12 21:54:06 +03:00
catarrh 26f0063696 ui: reword the custom files hint
- EN: Custom files are added in file manager tree and included in card scan.
- RU: Пользовательские файлы добавляются в менеджер файлов и учитываются
  при чтении карт.
- SW cache v118 -> v119.
2026-09-12 21:47:23 +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 85a66af7a5 ui: hide, don't clear, children of non-selectable DFs
- pysimFsLoadChildren failure now only marks exists=false and re-renders;
  loaded children stay in the node and are hidden by the renderer (red
  cross, no toggle, no (empty)), so a failed DF is re-attempted on the
  next probe/refresh instead of being short-circuited
- dropped the parent_sel-less /api/tree retry: error payloads now carry
  exists:false, so it doubled requests for every absent DF and re-selected
  the same FID without its parent (pySim probe_file fallback)
- probe walks strictly on exists===true and never collects files under a
  non-existing DF; no longer clears children on a failed EF select
- tests updated: single request per failed load, children untouched,
  200 exists:false marks absent without retry, render hides stale
  children, probe never fetches/selects them; SW cache v116 -> v117.
2026-09-12 21:23:31 +03:00
catarrh 92271d6726 ui: probe-all-files walk and honest file tree coloring
- pysimFsLoadChildren now treats {success:false,error} (and any error
  payload) as a failed listing: absent DFs/ADFs render as a red cross
  without an expand arrow instead of silently opening empty; the
  /api/tree 500 body also carries exists:false for shape consistency
  with /api/select
- an expanded directory with zero children shows a gray (empty)
  placeholder
- new Probe all files button (data-needs=card) in the tree header:
  walks the whole model tree from MF, verifies every DF/ADF via
  /api/tree and every file (incl. custom entries) via /api/select,
  skips subtrees of absent dirs, shows N/total progress, toggles to
  Stop, and reports present/absent counts with elapsed time; results
  colour the current tree only
- tests: fs_load (error/retry/empty), fs_render (red cross, (empty)),
  fs_probe (flags, absent-dir skip, custom files, stop, summary);
  html structural check; help EN/RU, READMEs, AGENTS updated;
  SW cache v115 -> v116.
2026-09-12 21:14:09 +03:00
catarrh 4598a70db2 ui: edit/delete buttons for custom files
- each row now shows Edit and Delete buttons (same styling as the
  profiler list buttons) instead of the red X glyph
- Edit reloads the entry into the top form: the Add button becomes
  Save (data-l10n updated too) and a Cancel button appears; submit
  updates the entry in place, recomputing fid/parentFid and excluding
  the edited row from the duplicate-path check
- Delete removes without confirmation; deleting the row being edited
  cancels the edit and an earlier deletion shifts the edit index
- pysimCustomAdd renamed to pysimCustomSubmit; tests for the whole
  flow; SW cache v114 -> v115.
2026-09-12 20:14:24 +03:00
catarrh 82fb2c8800 ui: move the Profiler tab next to SCP80
- top-level tab order is now Remote APDU, SCP80, Profiler, Card reader,
  Phone simulator; the profiler view moved with its button so the DOM
  order matches the tab strip
- docs (help EN/RU, READMEs) and the tab-order test updated; SW cache
  v113 -> v114.
2026-09-12 20:09:47 +03:00
catarrh f16e24ba11 ui: enlarge the header sim/nosim icon to 30px
- the SVG artwork occupies only ~46% x 63% of its 183x183 canvas, so
  the 18px render showed ~8x11px of ink; 30px roughly doubles the
  visible glyph while staying under the 32px h1 line-height, so the
  header row height is unchanged
- guard test asserts the inline size stays within the 24-32px budget;
  SW cache v112 -> v113.
2026-09-12 20:03:03 +03:00
catarrh 8b814eb2f8 ui: status indicator no longer uses the text cursor
- the wrapper is select-none with an inline cursor:default (the compiled
  CSS has no cursor-default utility), so hovering the dot/icons no
  longer shows the I-beam or allows text selection
- SW cache v111 -> v112.
2026-09-12 19:57:05 +03:00
catarrh 56c0e60462 ui: tooltip on the status dot shows the server state
- the dot carries its own title (Connecting... / No server connection),
  matching the wrapper tooltip; it is cleared when the sim icons replace
  the dot
- tests assert the dot title in both server states and its removal once
  the server is up; SW cache v110 -> v111.
2026-09-12 19:55:14 +03:00
catarrh 68aa37f093 ui: header indicator shows sim/nosim icons once the server is up
- #state-indicator is now a wrapper with a dot plus an 18px dark:invert
  image; no new compiled Tailwind classes (h-4/w-5 are not in the
  prebuilt CSS), so sizing uses an inline style and the header height
  is unchanged
- gray dot while probing, red dot when the server is unreachable;
  server up -> nosim.svg, animated sim_anim.svg while equipping, and
  sim.svg when the card is equipped (`_pysimEquipping` tracked from
  status.equipping, refreshed on every 2s poll)
- i18n reuses existing strings; tests for all five states, dot color
  transitions and the markup; SW cache v109 -> v110.
2026-09-12 19:53:29 +03:00
catarrh 5485d61390 ui: sort the file manager tree by FID or symbolic name
- pysimFsSortChildren() groups DFs above EFs, then sorts each group by
  FID (default) or case-insensitive symbolic name; missing names fall
  back to the FID as the sort key; deterministic tie-break; the input
  array is not mutated
- render uses the sorted copy, so lazily loaded directories and custom
  file injections are sorted too
- FID / Name pills above the tree (pysimFsSetSort) persist the choice in
  localStorage (otaman_fs_sort) and re-render without refetching
- tests for the comparator (DF priority, both keys, fallbacks, custom
  entries, no mutation) and the pill wiring; help/README updated;
  SW cache v108 -> v109.
2026-09-12 19:41:49 +03:00
catarrh 45c72d874f stk: never shadow a paused command with a new menu selection
The stuck-card pattern: after a Back TR the card re-issues the parent menu
as a new SELECT ITEM (91XX -> FETCH, paused, awaiting TR), but the UI's
back/timeout branch ignored that response and showed the cached top menu;
the next item click then sent ENVELOPE(Menu Selection) while the card was
waiting for the TR. The card answers such an ENVELOPE with 9000 (not the
usual 91XX), and menu-select cleared the pending command without a TR,
leaving an unfinished proactive session until reset/equip.

- server: _finish_pending_menu() answers a paused command with a cancel TR
  (0x10) before /api/menu-select sends its ENVELOPE and drains a 91XX
  follow-up, so a new selection can never shadow an unanswered FETCH
- frontend: stkMenuRespond('back'|'timeout') renders the follow-up command
  from the server response (SELECT ITEM / DISPLAY TEXT) and shows the
  cached top menu only when the TR answer carries no command (9000)
- tests: finish-pending cancel TR + chain drain (Python); stkMenuRespond
  back/timeout/cancel/ok rendering (frontend); help/AGENTS updated;
  SW cache v107 -> v108.
2026-09-12 19:34:27 +03:00
catarrh 1fe5c6347d ui: do not show 'initializing' when no card is inserted
The message branch treated the static auto_equip config flag as if
initialization were in progress, so with auto-equip on (default) a
cardless server showed 'Card inserted — initializing...' instead of the
no-card message. The initializing state now requires equipping or an
actually present card with auto-equip enabled; regression test added.
SW cache v106 -> v107.
2026-09-12 18:09:06 +03:00
catarrh c4ed064318 ui: center second-level pill rows on every page
SCP80, Card reader and Profiler list pill rows now use the same centered
layout as Remote APDU and Phone simulator (flex-wrap justify-center).
SW cache v105 -> v106.
2026-09-12 18:05:58 +03:00
catarrh 2a43424ae6 ui: use pills for the Profiler list tabs
Profiles / Card snapshots / Custom files switch from top-bar style tabs
(border-b, rounded-t) to the rounded-full pill style used by the other
second-level switchers. SW cache v104 -> v105.
2026-09-12 18:03:37 +03:00
catarrh 5b9e821d6d ui: gate controls by server/card state; move Custom files to Profiler
Availability model: server-down / server-up-no-card / card-equipped.
- header shows a symbolic indicator dot (red/yellow/green, tooltip)
- a 5s probe tracks /api/version+/api/status while not fully ready; the
  2s status poll takes over once connected
- controls declare data-needs='server' or 'card'; disabled + tooltip when
  the state does not satisfy them: equip (server), status/reset, file
  manager read/save/edit, raw APDU, profile-from-card, new snapshot,
  profiler list Check card, event Send, RAM install/explore delete,
  STK menu, Send STATUS, Verify vs pySim (server), pySim execute (server),
  PLI save/poll toggle (server)
- pysimConnect no longer conflates server and card: it records both and
  Connect stays usable without a card
- ramSendOta/ramInstallCap now use pysimFetch instead of raw fetch
  (relative /api URLs broke custom server URLs)

Custom files moved from the Card reader sub-tabs to the Profiler list
tabs (Profiles / Card snapshots / Custom files); help/README/AGENTS
updated. Tests: availability gating, indicator structure, moved tab.
SW cache v103 -> v104.
2026-09-12 17:58:30 +03:00
catarrh f1ed6a2a48 release: v2.0.0 2026-09-12 15:08:04 +03:00
catarrh 4b19b58bb0 ui: label comparison mismatches with snapshot names
Snapshot comparison results now use the master/checked snapshot names
where profile checks keep expected/actual: the raw-data field rows, the
generic mismatch line and the decoded FCI comparison table headers
(profilerRenderReport/fcpDiffHtml take an optional {expected, actual}
labels object; the name column widens to w-40 and wraps). Labels are
stored with the results so the language switch and the Only mismatches
filter re-render correctly; Check card snapshot and live checks are
unchanged. Tests for both labeled and default rendering; help/README
updated; SW cache v101 -> v102.
2026-09-12 15:06:51 +03:00
catarrh 6f63b5172c ui: show total scan time in the card snapshots list
Each snapshot row now renders '(N files, scanned in X.XX sec)' when the
snapshot has timing data (profilerSnapshotCountLabel); old/imported
snapshots without timing keep the plain '(N files)'. RU wording 'за X.XX
сек'. SW cache v100 -> v101.
2026-09-12 15:00:58 +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 a261675aad ui: focus scan name input and start scanning on Enter
'Profile from card' and 'New snapshot' now focus the name field when the
dialog opens, and Enter in that field starts the scan
(profilerScanNameKeydown, ignored while the Scan button is disabled).
Tests for the key handler and the input wiring; SW cache v98 -> v99.
2026-09-12 14:41:35 +03:00
catarrh 450f1de68a sw: never resolve respondWith to undefined on offline navigation
When a navigation fetch failed and the cache had no entry for the exact
URL (e.g. '/' while only 'index.html' is precached, common while the
local server restarts), the fetch handler resolved respondWith() to
undefined, producing 'TypeError: Failed to convert value to Response'.

The navigate fallback now chains caches.match(request) -> cached
index.html -> an explicit 503 offline Response. sw.js requests are
network-only with the same offline Response (previously they fell into
the navigate branch), and a new unit test loads sw.js in a VM sandbox
covering navigate/cache/asset/api paths. SW cache v97 -> v98.
2026-09-12 14:33:31 +03:00
catarrh 74efc20bf5 theme: brighten normal dark-mode text above the muted gray level
After the gray text was raised to #cbd5e1 in dark mode, elements using
dark:text-slate-300 (inactive tabs, labels, list text) matched the muted
level. Map dark:text-slate-300 to #e2e8f0 (slate-200, the body's dark
text color) so normal text stays one step brighter than muted gray.
SW cache v96 -> v97.
2026-09-12 14:31:27 +03:00
catarrh d2bd2ed798 theme: brighten gray text and borders in dark mode
Elements using bare text-gray-300/400/500/600 (proactive log ids and
timestamps, expand markers, event codes, ...) kept the light-theme gray
values in dark mode, which made them hard to read on slate-800/900.

- dark .text-gray-*:not([class*='dark:text-']) rules map them to the same
  level as the other dark muted text (slate-300 #cbd5e1, slate-200/100 for
  gray-700/800)
- dark:text-gray-400/600 and dark:text-slate-500 normalized to #cbd5e1
- dark:border-slate-600/700/800 (+ /50) brightened one step
- light theme unchanged
- the whole contrast block moved to src/contrast.css, appended by the
  npm build scripts after Tailwind so it stays reproducible; SW cache
  v95 -> v96
2026-09-12 14:28:41 +03:00
catarrh ccd0b6018c ui: separate STK menu, STATUS and events blocks in Phone view
Wrap each block of the phone row in a bordered card (border, rounded,
p-3) with items-start and gap-4 so they read as distinct panels and wrap
cleanly. SW cache v94 -> v95.
2026-09-12 14:20:35 +03:00
catarrh 9363f209ee ui: split Phone simulator into Phone and TR Config pills
- Phone pill: STK menu, STATUS and polling and the subscribed-events list
  in one row, proactive command log full-width below (room for more
  elements)
- TR Config pill: response data injected into TERMINAL RESPONSEs;
  currently the PROVIDE LOCAL INFORMATION dictionary, structured as
  heading + body blocks for future proactive-command responses
- phoneSwitchSubtab() mirrors the other sub-tab switchers and sets help
  anchors stk-menu / pli-dict; switchTab('phone') always opens Phone
- refreshDynamicI18n renders only the visible phone panel
- help EN/RU section 6 regrouped (6.4 STATUS polling under Phone, 6.5
  TR Config response data), READMEs and AGENTS.md updated
- structural and behavioral tests (html.test.js, phone_tabs.test.js);
  SW cache v93 -> v94
2026-09-12 14:17:48 +03:00
catarrh 33443d4f28 ui: regroup tabs — Remote APDU, Card reader, Profiler, Phone simulator
- C-APDU tab renamed Remote APDU (label untranslated EN/RU); Response
  parser moved from a top-level tab to a pill under it
- Profiler moved from the Card reader sub-tabs to a top-level tab
- Proactive UICC moved to a top-level tab and renamed Phone simulator
- Card reader keeps File manager, Custom files, pySim command line,
  Raw APDU
- modals (event send, profiler scan/snapshot, STK menu) moved outside
  the tab containers so they can open from any tab
- STK overlay disables the top-level tabs while waiting for user input
- OTA PoR jump now goes to Remote APDU > Response parser
- help EN/RU restructured (2.8 Response parser, 5 Profiler, 6 Phone
  simulator, subsequent sections renumbered, anchors kept); READMEs and
  AGENTS.md updated
- html.test.js asserts the new tab/pill structure; SW cache v92 -> v93
2026-09-12 14:01:28 +03:00
catarrh fe10603aea server+ui: auto-equip on card insertion; reset card views on session change
- _apply_equipped_card() centralizes the post-equip refresh + TERMINAL
  PROFILE (shared by the /api/command equip branch and auto-equip)
- server tracks card_session (bumped on equip and disconnect) and
  equipping; /api/status exposes connected, card_present, card_session,
  equipping, auto_equip and is exempt from _CARD_LOCK (pure cached state)
- auto-equip is on by default (--no-auto-equip; off with --no-card-init):
  the presence observer spawns a one-shot worker after insertion, which
  runs equip under _CARD_LOCK and applies the same refresh; the monitor
  starts after the startup init so pyscard's initial 'already present'
  event does not re-equip a fresh session
- UI: /api/status polls every 2s (other views stay at 5s); when
  card_session changes it runs pysimResetCardData() (STK overlay, file
  tree, events, proactive log, PLI, status) — the same reset as a manual
  Equip; messages: initializing / press Equip / no card

Tests for the observer and auto-equip rules, session bumps,
_apply_equipped_card, and the UI state machine. SW cache v91 -> v92.
2026-09-12 13:34:40 +03:00
catarrh 57eb412b6d server+ui: detect card removal passively and reflect it within 2s
The UI only noticed a removed card when some user action ran a real card
command (e.g. Check status); /api/status is a cached-state read that kept
returning the old card, and _handle_card_disconnect() did not clear
app.card/rs.

- start_card_monitor() registers a pyscard CardObserver for our reader;
  it only polls SCardGetStatusChange (no APDU, no connection, no extra
  process), and on removal sets server.card_present=False and calls
  _handle_card_disconnect() under _CARD_LOCK
- /api/status now exposes connected (session usable) and card_present
  (physically inserted) and masks card/profile/atr/selection when not
  connected; _CARD_CONNECTED is initialized from card presence instead of
  being unconditionally True
- the 2s UI poll includes /api/status; on disconnect it switches to the
  existing 'No card detected. Insert card and click Equip' state, or the
  new 'Card inserted — press Equip' hint when the card is back; the old
  _hadData heuristic is gone

Tests for the observer (filtering, removal, insertion) and the UI state
transitions. SW cache v90 -> v91.
2026-09-12 13:21:33 +03:00
catarrh 7288c22830 fastinit: escalate soft reset to physical on SW mismatch; retry/fallback
A probe can leave a card in a context where CLA-00 file access returns
6d00, so the software MF select fails (stock pysim init survives only
because it resets physically after probing). Fast init now recovers the
same way, on demand:

- FastRuntimeState.reset() falls back to hard_reset() on
  SwMatchError/ProtocolError (logged as FAST-RESET)
- init_card_fast() retries once after sl.reset_card() (FAST-INIT)
- __main__ falls back to stock pysim init once and skips
  TERMINAL PROFILE/drain when no card was initialized (no more
  6d00/6985 noise after a failed init)
- do_equip_fast() no longer pre-unregisters command sets; PysimApp.equip
  does that after a successful init, so a failed equip keeps the
  previous card/rs instead of leaving the app unequipped

Tests for the escalation, the retry and failed-equip state retention.
No card-model special cases.
2026-09-12 13:02:33 +03:00
catarrh 25787017e5 server: make reset-free fast init the default; add --full-pysim-init
Fast init (fastinit.py) is now the default for startup and equip: all
profile probes run on one connection, RuntimeState uses a software reset
and only explicit equip/reset reconnect the card. Measured on the
reference reader: equip 1729ms vs 9850ms and zero RESET events vs 7-8.

--full-pysim-init restores pysim's stock init_card/equip for
compatibility/debugging; the former --fast-init flag is kept as a hidden
no-op alias so existing command lines keep parsing. Docs updated.
2026-09-12 12:43:43 +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
catarrh 6c57ff4fd5 server: add --fast-init to skip redundant card resets
pysim's init_card() resets the card once per profile candidate in
CardProfile.pick(), once in RuntimeState.__init__ and again in
PysimApp.equip(); on typical readers each reconnect costs ~1.3s and a
normal init/equip does 7-8 of them (measured: equip 9.85s, startup
~14.8s).

fastinit.py mirrors pySim.app.init_card() with the resets removed:
pick_profile_no_reset() runs all profile probes back-to-back on one
connection, FastRuntimeState.reset() is a software reset (select MF,
clear selected_adf/scp, ATR from the transport) and the equip/reset
commands are routed through do_equip_fast (one reconnect via
wait_for_card) and do_reset_fast (always a physical reset). Enabled
with --fast-init; stock behavior remains the default.

Tests for the reset-free pick, the soft reset and the explicit reset
paths. Docs updated; SW cache v88 -> v89.
2026-09-12 12:27:03 +03:00
catarrh 895d0b7b36 server: serialize card access, fix poll-interval 0, add --timing
--poll-interval 0 now really disables background STATUS polling (it was
clamped to 1s, and the equip branch force-enabled it anyway). Request
handlers and the poll thread now share _CARD_LOCK so a poll can never
interleave a FETCH/TERMINAL RESPONSE pair — the baseline log showed
AUTO-STATUS chains and duplicate FETCHes inside the equip TP chain.

--timing adds elapsed timestamps, per-reset logging (RESET #n) and
phase durations for startup (init_reader, card_init, pysim_app,
terminal_profile_drain) and equip (onecmd, terminal profile).

AGENTS.md documents the proactive-TR invariant: never fetch without
answering, never fetch twice, one APDU conversation at a time.
2026-09-12 12:25:07 +03:00
catarrh ef00b2c3f4 docs: keep a local AGENTS.md out of the repo; fix RAM-install note
Add AGENTS.md to .gitignore so the local agent guide (goal, implemented
features, pySim usage) stays untracked. Correct the /api/ram-install
description: the .cap archive is parsed server-side by _cap_parse, not
by pySim.javacard/global_platform.
2026-09-12 11:46:32 +03:00
catarrh 0fa9548c59 docs: cover C-APDU parser, HTTP OTA, profiler, snapshots
README(.md/_RUS.md) gain the sections that existed only in the in-app
help: C-APDU Parser, HTTP OTA (GP GPC v2.2 Amd B v1.1 4.7), and the
Profiler subtree (profile list, filesystem rules, scan options, check
report incl. Only mismatches, card snapshots, Check card snapshot and
Compare snapshots). Also correct the tab/sub-tab counts and add
Theme/Localization sections to the English README.

docs/api.md: remove the duplicate /api/ram-install section, document
fci_hex/file_size/record_len/num_of_rec in /api/select, the records
shape of /api/read, bump the version example, and add the missing
endpoint sections (events, event-send, proactive-log, status-poll,
rescue, poll-status, poll-toggle, pli-qualifiers, pli-dict).
2026-09-12 08:22:02 +03:00
catarrh 30ff0f7a74 profiler: compare two card snapshots
New Compare snapshots button on the Card snapshots tab opens a dialog
with Master snapshot / Snapshot to check selects and the two mask
options (Match first 4 bytes for EF.IMSI / EF.ICCID, checked by
default). The comparison runs like a profile check where the master
snapshot takes the place of the profile: every file must match exactly
(exact FCI, contents), except the first 4 bytes of masked EF.IMSI/EF.ICCID.

profilerRulesFromSnapshot() synthesizes exact-match rules from the
master; profilerSnapshotSource() of the checked snapshot is the data
source; files present only in the checked snapshot are appended as
failed 'extra file' results via profilerExtraFileResults() and the new
optional extraResults param of profilerRunProfile(). Results reuse the
existing view, summary and Only mismatches filter; Back to list returns
to the snapshots tab. Tests for rule synthesis, masking, comparison and
extra files. SW cache v87 -> v88.
2026-09-12 08:14:03 +03:00
catarrh 5d096453fb profiler: add 'Only mismatches' filter to check results
The results view header (live card and snapshot checks) gets an Only
mismatches checkbox that hides all passing files and keeps only failures
and errors; when everything passes under the filter, a 'No mismatches'
note is shown instead. The pass/fail/error summary always reflects all
results. Helps with large profiles on cards where most files match.
SW cache v86 -> v87.
2026-09-11 23:29:02 +03:00
catarrh d728a5a341 profiler: style Check card snapshot like Check card 2026-09-11 23:25:39 +03:00
catarrh f64f2dac7c profiler: check a profile against a stored card snapshot
Each profile row gets a Check card snapshot button next to Check card.
It opens a picker listing the stored snapshots (name, ICCID, date, file
count) and runs the profile rules offline against the selected snapshot,
showing the usual report titled 'profile — snapshot'.

profilerRunRule() accepts an optional data source: profilerLiveSource()
(default) wraps /api/select + /api/read, profilerSnapshotSource() serves
select/read from the snapshot files. Contents missing from the snapshot
are reported as unverifiable errors ('Content not captured in snapshot')
rather than mismatches. The shared check loop moved into
profilerRunProfile().

Tests for the snapshot source and snapshot-based checks; help docs
updated; SW cache v84 -> v85.
2026-09-11 23:16:14 +03:00
catarrh 612f267349 i18n: re-render visible dynamic views on language switch
translatePage() only rewrites static [data-l10n] elements, so labels built
at render time (Check card/Edit/Export/Delete, Open, Remove, Send, ...)
stayed in the old language. toggleLang() now calls refreshDynamicI18n(),
which re-renders only the visible dynamic views:
- profiler sub-tab: current view (list, snapshot list, editor, results via
  the new profilerRenderResultsView with profilerResults stored, snapshot
  via profilerRenderSnapshotData which leaves the name input alone)
- open scan modal: profilerScanRefreshOptions re-translates the mask labels
  without touching checkbox state
- cards table (cardsRender), proactive events/log/PLI (async, only when
  the sub-tab is visible)
Also removed the duplicated translatePage() call in toggleLang.
New test for profilerScanRefreshOptions. SW cache v83 -> v84.
2026-09-11 23:05:22 +03:00
catarrh 00c84307aa profiler: use tab-style buttons for Profiles / Card snapshots
Rectangular top-rounded tabs with a bottom border, matching the app's
top-level tab bar (incl. dark:bg-blue-500/dark:text-white active state,
toggled by profilerListSwitch). SW cache v82 -> v83.
2026-09-11 22:58:10 +03:00
catarrh efbdb5024c profiler: switch Profiles / Card snapshots with tabs instead of two columns
The list view now has a centered pill row (Profiles | Card snapshots) and
shows one list at a time; the last active tab persists while the app is
open. Snapshot flows (scan, import) switch to the snapshots tab, profile
import switches to profiles. Help docs reworded (tabs). New DOM test for
profilerListSwitch. SW cache v81 -> v82.
2026-09-11 22:56:35 +03:00
catarrh fa4064f1e8 i18n: rename RU snapshots label 'Снимки карты' -> 'Снимки карт'
LANG_RU value and both help-ru references. SW cache v80 -> v81.
2026-09-11 22:53:41 +03:00
catarrh 71da10b637 profiler: card snapshots (Снимки карты) list, scan, view
New read-only capture entity stored in localStorage 'otaman_snapshots':

- profiler list view is now two labelled columns: Profiles and Card
  snapshots; each snapshot row shows name, decoded ICCID, date, file
  count and Open / Export / Delete buttons
- New snapshot reuses the scan modal in a snapshot mode (name only, no
  ignore/mask/FCP options) via profilerScanCard(..., 'snapshot') ->
  profilerBuildSnapshotFile: metadata + raw FCI + exact contents for
  every readable file (no ignore list, no masking). The scan returns to
  the list afterwards.
- ICCID is decoded from EF.ICCID (2FE2) with decIccid() (nibble-swapped
  E.118 digits, F pad per TS 102 221 13.2) and stored as an immutable
  snapshot field, shown next to the name in the list and the view.
- Open shows all captured data read-only (attributes, raw FCI + decoded
  FCI, contents or 'Not captured'); only the name is editable/saveable.
- Import/Export/Delete + profilerValidateSnapshot; quota-safe save.

Tests: decIccid, profilerSnapshotIccid, validation, snapshot builder
(exact contents/no mask/unreadable), snapshot-mode scan with ICCID.
Docs synced (help/help-ru). SW cache v79 -> v80.
2026-09-11 22:42:32 +03:00
catarrh aa6a9ed1b5 i18n: rename 'FCP parameters' -> 'FCI parameters' in the check report (RU: 'Параметры FCI')
SW cache v78 -> v79.
2026-09-11 21:54:11 +03:00
catarrh 36281e02fd profiler scan: check/uncheck-all toggle for the ignore-contents list
Adds an 'All' header checkbox next to 'Ignore contents of files:' that
checks or unchecks every file; it reflects the list state (checked only
when all are checked, indeterminate on a mixed selection) and updates on
individual checkbox changes. RU label: 'Все'. Docs synced, tests for the
toggle/state helpers. SW cache v77 -> v78.
2026-09-11 21:52:57 +03:00
catarrh 88f67f5e13 profiler: add EF.ACC/EPSNSC/START-HFN/ARR to the ignore-contents list
- EF.ACC 6F78, EF.EPSNSC 6FE4, EF.START-HFN 6F5B (FIDs per UICC_FILES.md /
  TS 31.102 & TS 51.011), EF.ARR 2F06 (TS 102 221 13.4; the ADF.USIM copy
  6F06 is covered by the name fallback)
- entries may carry 'checked: false'; EF.ARR is unchecked by default
- tests extended (FID sanity + default-checked state), docs updated.
SW cache v76 -> v77.
2026-09-11 21:51:42 +03:00
catarrh 994075920d i18n: rename 'Decoded FCP' -> 'Decoded FCI' (RU: 'Декодированный FCI')
Label in the rule editor, LANG_RU value and both help files.
SW cache v75 -> v76.
2026-09-11 21:41:33 +03:00
catarrh 130b9d7e3b profiler: explicit decode-failure display for corrupt FCI, keep partial results
fcpDecode now parses with a partial-aware walker (fcpParseTlvs) that keeps
every complete TLV it encounters and reports why it stopped:
- 'TLV 62 declares N bytes, only M available' (truncation)
- 'Truncated length field at offset X' / 'incomplete TLV header'
- 'Invalid length form' / 'Trailing data after TLV 62'
- inner A5/C6 errors prefixed with their context ('In A5: ...')

The editor preview shows the decoded parameters plus a red
'Decode failed: <reason>' line (RU: 'Ошибка декодирования'), and the check
report's decoded FCI diff appends per-side error notes (expected/actual)
while still showing whatever decoded on either side.

SW cache v74 -> v75.
2026-09-11 21:38:32 +03:00
catarrh 1e067dc883 fix BER long-form lengths: parseBerLen read b length bytes instead of b & 0x7F
parseBerLen treated the first long-form length byte (0x81/0x82/...) as the
count of length bytes, so any TLV with a long-form length parsed as
garbage: the profiler FCI decoder returned ok:false (decoded preview
disappeared and never came back after editing an FCP with a '62 81 xx'
outer length or a long-form inner TLV), and the same bug hit parseTlvList
(C-APDU parser, INSTALL param walker), parseBerScript (expanded script
rows >= 128 bytes) and readLvField.

Per ISO 7816-4 5.2 / UICC_SPECS.md 1.7 the long form is 81-84 followed by
(b & 0x7F) length bytes. Fixed; short-form behavior unchanged.

New tests: parseBerLen short/81/82 forms, parseTlvList long-form outer and
inner TLVs, fcpDecode long-form regression (81/82, nested A5, >=128-byte
FCP) incl. the editor preview path. SW cache v73 -> v74.
2026-09-11 21:32:23 +03:00
catarrh 6bfbb00994 profiler editor: decoded FCP panel beside the raw FCI field
The exact-FCI row is now a two-column flex layout: the editable raw FCI
textarea on the left, the decoded FCP list on the right (50/50, textarea
4 rows). Preview rendering split into profilerFciPreviewItems; a new
profilerFciInput hook updates the rule and re-decodes the panel on every
keystroke (no editor re-render, no focus loss), showing an empty body
while the hex is incomplete/invalid. Docs wording updated. SW cache
v72 -> v73.
2026-09-11 21:22:18 +03:00
catarrh 944fccf67b profiler: decode FCP/FCI and show per-parameter diffs
Adds a spec-verified FCP/FCI decoder (ISO 7816-4 5.3.3 Tables 12-14,
TS 102 221 11.1.1.4; cross-checked against pySim ts_102_221.py — spec wins
on the data coding byte and termination mask). fcpDecode accepts the FCP
template '62', an FCI '6F' wrapper and bare FCP content, and decodes:
file size/total size, file descriptor (access/shareable, file type,
structure incl. BER-TLV/SIMPLE-TLV, data coding byte, record length/count),
FID, DF name, SFI, life cycle status, security attributes, the A5
proprietary sub-DOs (UICC characteristics, power, clock, memory, file
details, sizes, commands, environmental conditions, test config) and the C6
PIN status template DO. Unknown TLVs are preserved raw.

Usage:
- check report: an 'Exact FCI' mismatch now renders a decoded
  expected/actual table under the raw fields, highlighting differing
  parameters and showing missing ones as '—'
- rule editor: the FCI hex textarea shows a live decoded FCP preview

10 new tests (vectors incl. pySim linear-fixed, malformed inputs, diff
highlighting, report integration). Help docs updated. SW cache v71 -> v72.
2026-09-11 21:15:42 +03:00
catarrh 758603a9ad profiler: merge rule attributes into a single row
Path, FCP/FCI check, file type and size (or record length + count) now
share one flex-wrap row; path column narrowed from w-72 to w-48. The FCI
hex textarea (Exact FCI mode) keeps its own full-width row and the
contents check stays on the following row. SW cache v70 -> v71.
2026-09-11 21:04:49 +03:00
catarrh 47539c594f i18n: profile Check button -> 'Check card' / 'Проверить карту'
Renamed the i18n key/value and both help references (profile list and
Check description). SW cache v69 -> v70.
2026-09-11 20:58:09 +03:00
catarrh 8d725642dd i18n: rename RU FCP/FCI mode 'Точный FCI' -> 'Полный FCI'
Applied to the LANG_RU value and both help-ru references. SW cache v68 -> v69.
2026-09-11 20:54:49 +03:00
catarrh 2d7b2f4ef0 profiler: compact ignore-contents list in scan dialog
The scan dialog now shows a single 'Ignore contents of files:' label and a
two-column checkbox grid with just the file names (the per-row 'Ignore
contents of ...' prefix was redundant). Drop the now-unused i18n key and
add the new label (RU: 'Игнорировать содержимое файлов:'). Help docs
updated. SW cache v67 -> v68.
2026-09-11 20:47:51 +03:00
catarrh 2f299c769d profiler: Check first in profile list, add playback glyph
Profile row actions are now Check, Edit, Export, Delete; the Check button
shows 'Check ▶' (RU 'Проверить ▶'). Help docs updated. SW cache v66 -> v67.
2026-09-11 07:57:47 +03:00
catarrh 74741a37cd profiler: list matching record numbers when only the record count differs
The 'matching records: N-M' note was suppressed when every compared record
matched but the record count differed (e.g. 30 expected vs 10 read): the
renderer only looked for failed per-record checks, so the report showed
just the count lines. profilerRunRule now sets res.recordsMismatch whenever
the count or any record differs, and the renderer uses it (with the old
per-record check as fallback), so the matching-record ranges are listed in
that case too. Tests cover the count-only mismatch and the per-record case.
SW cache v65 -> v66.
2026-09-11 00:45:17 +03:00
catarrh 26ee8974cc http ota: use a sample ICCID in PSK Identity / agent ID placeholders
Both placeholders now show a synthetic sample ICCID (89012345678901234567)
instead of Test123 / v1.0. SW cache v64 -> v65.
2026-09-11 00:38:12 +03:00
catarrh 025e90a270 docs: use actual Russian UI labels in help-ru
The Russian help referred to the English i18n keys even though the app
translates those controls. Replaced every UI-control reference with the
label a Russian user actually sees (buttons, pills, tabs, toggles, field
labels): Новый профиль / Профиль с карты / Импорт профиля, Добавить
правило, Сохранить, Проверить, Экспорт, Удалить, Подключиться /
Подключить карту, Отправить на карту, Проверить в pySim, Отправить STATUS,
Опрос, Декодировать, Упаковать в Secured packet, Экспорт/Импорт JSON
buttons, Карты, Файловый менеджер, Пользовательские файлы, Профайлер,
Командная строка pySim, Отправка APDU, Проактивный UICC, Парсер ответов,
Картридер, Разбор C-APDU, справка, Меню STK, Триггер (Push SMS),
Обёртка в Command Scripting template ('AA'), mode labels and scan options.

Also translated the section headings that name UI views (2.6, 3.2, 4, 5,
5.1-5.6) and the proactive-log 'Ответ:' line. Terms without a Russian
translation (SIM RFM, Expanded Script, Secured Packet, RAM, Silent
(P2=0C), event names, spec terms) stay English as in the UI.

Both files: fixed the documented '+ Command' button that does not exist
(the chain builder's buttons are + SELECT, + READ RECORD, ...).

SW cache v63 -> v64.
2026-09-10 23:46:49 +03:00
catarrh 0e459d1224 ui: use the same blue style for all three profiler list buttons
'Import profile' now matches 'New profile' and 'Profile from card'
(bg-blue-600). SW cache v62 -> v63.
2026-09-10 23:39:16 +03:00
catarrh 2e889e0dce ui: neutral HTTP OTA placeholders, rename disconnect button
- HTTP OTA host/URI placeholders: megafon.ru -> example.com
- 'Disconnect from pySim' -> 'Disconnect from server'
  (RU: 'Отключиться от сервера')

SW cache v61 -> v62.
2026-09-10 23:38:23 +03:00
catarrh a9275477fb docs: sync help/help-ru with current UI, fix deep-link anchors
Audited both help files against the UI and fixed stale/missing content:

Fixed:
- Expanded Script: remove the nonexistent 'Response Type' TLV row; Error
  Action is 'one of four forms', not three
- 'Expanded Remote Response' section described a UI that does not exist;
  rewritten as response decoding (PoR shown in the Secured Packet view,
  last SW/data filled into the Response parser)
- Cards/Custom files: document all five export/import buttons
- Reader auto-detection: drop the stale /dev/ttyUSB0 start.sh bullet, fix
  'Reader: none' and the 'Equip card' button name, note the server-side
  PC/SC probe with retries
- Profiler: 'Check contents' label, symbolic names next to rule paths,
  Add rule/Save buttons, aligned read-only expected/actual fields for
  raw-data mismatches

Added:
- 1.1 Interface (header chrome: INSTALL PWA, github/help links, EN/RU and
  theme toggles, localStorage persistence, help deep-links)
- GPC v2.2 Amendment B v1.1 in the standards list
- Card reader connect workflow (URL, Connect, status, Equip card)
- File manager tree browser + Save/Cancel
- Send to Card PoR behavior in 3.1/4

Also fixed the app help anchors: C-APDU Parser now opens #c-apdu-parser
and the Profiler sub-tab opens #profiler. SW cache v60 -> v61.
2026-09-10 23:37:09 +03:00
catarrh dbf9e8559b profiler: show raw-data mismatches as aligned monospace fields
FCI and content (incl. record) mismatches are now rendered as two read-only
monospace inputs — expected on top, actual directly beneath — sharing a
fixed-width right-aligned label column (w-24) so both fields start at
exactly the same horizontal position and stretch to fill the row
(flex-1/min-w-0, no overlap). A title attribute shows the full value on
hover. Non-raw checks (fileSize, recordLen, numRecords, content.records,
read failure) keep the inline line.

New profilerRawDataCheck helper; 2 new tests. SW cache v59 -> v60.
2026-09-10 23:14:50 +03:00
catarrh ad543fd731 profiler: dedupe redundant record-count failure in check report
A record file with both a numRecords metadata check and record content
checks reported the same count mismatch twice (numRecords and
content.records). profilerRunRule now omits the content.records line when
the numRecords check already covers it, keeping it only when no numRecords
check ran (FCP/FCI 'type' mode or numRecords cleared) or when numRecords
passes but the read returns a different count.

3 new tests. SW cache v58 -> v59.
2026-09-10 23:08:17 +03:00
catarrh 16c183f79c profiler: mask-first-4-bytes options + lock scan form during scan
Profile-from-card scan form gains two checked-by-default options
'Match first 4 bytes for EF.IMSI / EF.ICCID', surfacing the previously
hardcoded mask behavior. Unchecking captures those files' contents
exactly. profilerBuildFileRule now takes a maskFids set (undefined keeps
the legacy mask default); scan start collects it and threads it through
profilerScanCard.

While a scan runs the options form (ignore list, mask options, FCP/FCI
selector) is hidden, Cancel and Scan are disabled (with visible
disabled styling), and the profile-name input becomes read-only — fixing
the latent cancel-mid-scan bug that left the scan running and popped the
editor open. profilerFromCard performs a full fresh-open reset.

4 new tests (mask/exact/legacy + scan threading). RU i18n key, help docs
synced. SW cache v57 -> v58.
2026-09-10 23:04:17 +03:00
catarrh d7204b3e96 profiler: richer check report (what was verified, matched records)
The Check report now states exactly what was verified for each file:
- passing files append a plain summary to the path line
  ('filetype and size, contents' / 'filetype and records, contents' /
  'Exact FCI, contents')
- mixed results mark each aspect inline
  ('filetype ✓, size ✗, contents ✓'), with the existing red detail lines
  for the mismatches; 'Exact FCI' subsumes type/size/records
- record files with a contents mismatch add a 'matching records: 1-5,
  7-10' note listing the records that did match

New pure helpers: profilerResultAspects (derives checked aspects from the
result checks), profilerAspectSummary (plain vs marked text),
profilerNumRanges (compresses record numbers into ranges). profilerRunRule
now also returns res.recordsMatched. 6 new tests incl. an end-to-end report
render. RU i18n keys added. Help docs synced. SW cache v56 -> v57.
2026-09-10 22:53:13 +03:00
catarrh 8f24200537 profiler: drop percentage from scan progress line
Show only 'N / total files — path' during 'Profile from card'; the
percentage was redundant. Help docs updated. SW cache v55 -> v56.
2026-09-10 22:40:59 +03:00
catarrh 0ecfd5f720 profiler: compact rule editor layout
- Path field narrowed to w-72 with the FCP/FCI selector to its right in
  one row
- File type + size (or record length + count) now sit in a single flex
  row (fixes the accidental wrap from the unavailable md:grid-cols-4 class
  in the prebuilt CSS)
- 'Contents' label renamed to 'Check contents' and the None/Exact/Mask
  radios moved inline to its right, saving one line per rule

Rendering-only change; no logic or test behavior affected. SW cache v54 -> v55.
2026-09-10 22:31:25 +03:00
catarrh d9b2cb7db2 profiler: show scan progress during 'Profile from card'
Split profilerScanCard into two phases so the file total is known up
front: phase 1 walks the filesystem (tree calls only) collecting every
file entry, phase 2 builds a rule per file. The optional onProgress(done,
total, path) callback reports 'N / total files (%) — path'; the scan
dialog shows it under the FCP/FCI selector, starting with 'Discovering…'
during phase 1. No extra card I/O (tree calls are no longer interleaved
with select/read). Backward compatible: onProgress is optional.

2 new tests (progress sequence 0/total then 1..total with paths; scan
without callback). RU i18n keys (Discovering..., files). Help docs synced.
SW cache v53 -> v54.
2026-09-10 22:26:30 +03:00
catarrh 486f57eb30 profiler: per-rule FCP/FCI verification modes
Each rule now carries an fciMode ('type' | 'type_size' | 'exact') plus an
fciHex (raw SELECT response) so rules can verify different depths of file
control information:

- Filetype only (FCP): exists + file type
- Filetype + size (FCP): adds file size (or record length/count) - previous
  behavior
- Exact FCI: adds byte-for-byte comparison of the raw SELECT response (the
  FCP '62' template), catching FID/AID, life-cycle, security-attribute and
  proprietary-parameter changes

/profile from card/ gains a matching FCP/FCI selector (default Filetype +
size); rules store fciMode and always capture fciHex so they can be
upgraded to Exact FCI in the editor without rescanning. The rule editor
adds the selector, hides size/record fields in 'type' mode and shows an
editable FCI hex textarea in 'exact' mode. Contents checks stay independent.

Server /api/select now returns fci_hex (raw FCP template hex, uppercased).
profilerValidateProfile accepts the new mode; profilerNormHexStrict added
for byte-exact comparison (no '?' wildcards). Legacy rules without fciMode
default to type_size.

10 new tests (build-rule fields, run-rule modes incl. byte compare +
missing-FCI, validation). Docs + RU i18n synced. SW cache v52 -> v53,
version 1.9.28.
2026-09-10 22:17:57 +03:00
catarrh 69b4fbfb42 profiler editor: show symbolic file names next to rule paths
Rules now persist the pySim symbolic name captured during 'Profile from
card' (profilerBuildFileRule stores c.name || sel.name), and the profile
edit view renders it next to the Path label using the same lookup order
as the check view: profilerCustomNameForPath(path) || rule.name.

Path edits call the new profilerUpdateRulePath, which drops the stale
scan-time name and refreshes the label from the custom-files dictionary
live, so the displayed name always corresponds to the current path.

Manually added rules get name: null (resolved via custom-file lookup).
Old profiles without rule.name still work.

4 new tests (rule.name from child/select/null, custom name lookup, path
edit clears name + refreshes label). SW cache v51 -> v52.
2026-09-10 21:54:32 +03:00
catarrh 5355a62d88 profiler: apply 'ignore contents' to real card files (KcGPRS FID fix + name fallback)
The 'Profile from card' ignore checkboxes matched only by FID, but the
list had EF.KcGPRS at 4F52 (TS 31.102 DF.GSM-ACCESS) while the live card
exposes it at 6F52 (TS 51.011 DF.GSM, verified in TS 51.011 v4.15.0
10.3.32 and the DF.GSM allocation table). The miss made profilerBuildFileRule
capture full contents (Exact) for a file the user had checked to ignore.

- Correct EF.KcGPRS FID to 6F52 (spec-verified)
- Match ignores by FID OR by pySim name: each checkbox now carries
  data-ignore-name, profilerScanStart builds an ignoreNames set, and
  profilerBuildFileRule checks both. Covers FID variants (6F52 vs 4F52)
  and future constant typos; ignoreNames is optional for back-compat.
- 6 new tests: ignore-list FID/name sanity (incl. KcGPRS=6F52), duplicates,
  ignored-by-FID, ignored-by-name-only regression case, non-ignored control,
  back-compat when ignoreNames is omitted.

SW cache v50 -> v51.
2026-09-10 21:49:51 +03:00
catarrh 7614067b8f v1.9.27: align version markers (server VERSION, pyproject.toml, PWA header) 2026-09-10 21:31:16 +03:00
catarrh 181544b360 frontend: add HTTP OTA constructor (GP RAM-over-HTTP v1.1)
New 'HTTP OTA' sub-tab in the C-APDU constructor builds Remote Application
Management over HTTP payloads (GPC v2.2 Amendment B v1.1 §4.7):

- Trigger (Push SMS): 81 > 83 > 84/[85]/[86]/89, with optional Command
  Scripting template ('AA') wrap per TS 102 226 §5.2.1
- Store (SD admin params): STORE DATA TLV mode (80 E2 90 00), tag 85/A5
- Connection parameters: OPEN CHANNEL comprehension-TLV row editor + presets
  (Device Identities 02, Alpha 80, Bearer 01)
- Security parameters (Table 4-6): LV PSK Identity + LV KVN/KID
- Retry policy (Table 4-7): retry counter + TS 102 223 timer TLV (25 03)
- HTTP POST (Tables 4-8/9/10): 8A host / 8B agent / 8C URI, text→octets
- 'Pack into Secured packet' reuses packToSp for SPI/counter filling

Pure builder functions covered by 12 new tests reproducing the byte layout
from the spec tables. i18n entries, help.html/help-ru.html §2.7 added.
SW cache bumped otaman-v49 → v50.
2026-09-10 21:30:06 +03:00
catarrh c31885d40e ui: move chain delete button to right, use ✕ glyph
- C-APDU chain rows: delete button moved after the hex field (matches
  Expanded Script layout)
- Use ✕ uniformly across delete icons (was &times; in chain rows)

SW cache v49.
2026-09-10 09:21:37 +03:00
catarrh 3ca014991a ui: unify C-APDU chain add buttons with Expanded Script style
- Regular chain add buttons: faint blue tint -> solid blue (bg-blue-600)
- '+ GET RESPONSE' (SIM/USIM/RAM): gray/blue -> emerald to stand out
  (also fixes RAM GET RESPONSE which was styled inconsistently)

SW cache v48.
2026-09-10 09:14:44 +03:00
catarrh 30969f8f04 profiler editor: keep record/content fields visible
- Seed one empty record row when record content is created or reset
  (filetype change or Contents None->Exact/Mask) via profilerEmptyRecordContent
- Add per-record delete and '+ Record' add controls so the record view is
  always editable; delete is hidden on the last remaining row

SW cache v47.
2026-09-09 23:46:04 +03:00
catarrh 569f308cec profiler: skip fileSize for linear-fixed/cyclic files
- Do not store fileSize on record-file rules during 'Profile from card'
- Do not check/report fileSize mismatch for record-file rules; only
  recordLen and numRecords are reported for LF/CY files

SW cache v46.
2026-09-09 23:35:21 +03:00
catarrh 61ab7bc304 profiler editor: filetype-driven fields and content view
- Show Size only for transparent/ber_tlv; Record length/count only for
  linear_fixed/cyclic; hide all size/record fields for df (unknown shows all)
- Hide the Contents section entirely for df (existence-only check)
- Changing file type clears now-inapplicable fields and resets content
  (keeps mode) when its kind switches between record and transparent
- profilerSetContent defaults content kind from the selected file type

Add profilerFileFields + profilerContentKindForFileType helpers + tests.
SW cache v45.
2026-09-09 23:24:18 +03:00
catarrh 9879675df2 profiler: mask-match IMSI/ICCID contents on 'Profile from card'
When generating a ruleset from the card, capture EF.IMSI (6F07) and
EF.ICCID (2FE2) contents as a mask matching only the first 4 bytes
(e.g. 08290591??????????), the rest are '?' wildcards.

Add profilerMaskPrefix4 helper + PROFILER_MASK_PREFIX4_FIDS map + tests.
SW cache v44.
2026-09-09 23:06:48 +03:00
catarrh dd18a24062 profiler: min-length content compare on size/record mismatch
When a declared fileSize/recordLen/numRecords attribute mismatches, compare
file contents only over the overlapping (shorter) portion, so a length
difference alone doesn't fail the content check when the common bytes match.
When sizes match, contents are compared fully as before.

Add profilerMatchMin helper + tests. SW cache v43.
2026-09-09 22:49:00 +03:00
catarrh 5c279147c1 theme: darker light-gray shades (light), lighter gray fonts (dark); larger profiler progress
- Light theme: darken bg-gray-50/100/200, border-gray-100/200/300,
  text-gray-300 and hover variants one step
- Dark theme: lighten dark:text-gray-400/600 gray fonts
- Profiler: bump 'Checking ..' progress line from text-xs to text-base

SW cache v42.
2026-09-09 22:37:30 +03:00
catarrh 330a1f0070 fonts: bump text-xs (extra small) to text-sm size
Redefine .text-xs to render at 0.875rem/1.25rem (text-sm), covering the PWA
and both help pages via the shared stylesheet. Explicit leading-* overrides
still apply.

SW cache v41.
2026-09-09 22:28:22 +03:00
catarrh 1daced1e18 theme: increase contrast of muted text, lighten red in dark mode
- Light theme: darken gray-400/500/600 muted text one step
- Dark theme: lighten slate-400/500 muted text one step
- Dark theme: lighten red-500/600/700 text (was too dark on dark bg)

SW cache v40.
2026-09-09 22:21:28 +03:00
catarrh 9d7fa11871 profiler: show symbolic file names next to paths in results
- Capture the pySim file name (sel.name) during each check
- Resolve custom-file aliases from pysimCustomFiles (frontend dict)
- Render 'EF.ADN (MF/7F10/6F3A)' in the results report

SW cache v39.
2026-09-09 17:16:32 +03:00
catarrh 7c1d623ca2 i18n: rename Профилёр → Профайлер 2026-09-09 16:31:12 +03:00
catarrh 7388f39bd6 docs: sync help/help-ru with current UI
- C-APDU tab: describe chain builder for SIM RFM/USIM RFM/RAM-GP (row-based
  + Command buttons, chain preview, GET RESPONSE, Le stripping, USIM silent
  select); remove obsolete single-command options (Start with SELECT,
  Selection mode, Record size, Allow P1/P2 editing)
- Add DISABLE/ENABLE/UNBLOCK PIN + GET RESPONSE to SIM/USIM command table
- Fix RAM/GP INSTALL P1 (make-sel 08, registry-update 40, extradition 10),
  LOAD P1=80, STORE DATA add E0, add GET RESPONSE; privileges are a
  length-value field (drop incorrect 'Tag C7')
- Add new 'C-APDU Parser' sub-tab (section 2.6)
- Card reader: add Profiler sub-tab (section 5.6) and STK menu block (5.5.1);
  renumber proactive/scenario subsections
- Fix Russian response-parser section (was a duplicated copy of the Cards
  table)

No index.html change -> no sw.js cache bump needed.
2026-09-09 16:17:00 +03:00
catarrh 201b0df278 v1.9.26: profiler (card file-structure & content verification)
New 'Profiler' sub-tab in card-reader view:
- Named rulesets (profiles) persisted in localStorage; each has an ordered
  list of filesystem rules (type 'file', extensible to OTA/TAR checks later)
- List page: New profile / Profile from card / Import profile + per-profile
  Edit / Check / Export / Delete
- Editor page: inline rule fields (path, file type, size, record length,
  record count, contents with exact/mask/'?'-wildcard modes)
- Results page: sequential rule checks with live progress and a pass/fail
  report (existence + FCI attributes + content match)
- 'Profile from card' scans the equipped card; creates a rule only for files
  that exist (FCI present). A scan-options dialog lets the user skip contents
  of frequently-overwritten dynamic files (LOCI/PSLOCI/EPSLOCI/5GS3GPPLOCI/
  Keys/KeysPS/SMS/Kc/KcGPRS/LOCIGPRS/CBMID/SMSS), checked by default
- ADF-rooted paths use the AID; paths resolve via a new server _select_path

Server:
- _select_path() resolves MF/ADF-AID-rooted paths (pySim can't select ADF by AID)
- /api/select and /api/read accept 'path'; /api/select returns file_size,
  record_len, num_of_rec

Also fix: silent SELECT (P2=0x0C) no longer emits a trailing Le byte
(chain builder) - matches ETSI TS 102 221 silent-select behavior.

Tests: +profiler.test.js; frontend 117 pass, Python 61 pass.
SW cache v37.
2026-09-09 15:51:48 +03:00
catarrh 1db6c12d52 v1.9.25: move STK menu from sub-tab pill to Proactive UICC view
- Removed STK pill from pysim sub-tab bar
- Added 'STK menu' block atop 'STATUS and Polling' in Proactive UICC view
- Shows emerald button when card issued SET UP MENU, 'no menu' text otherwise
- stkCheckMenu toggles button vs no-menu text; refreshed on proactive tab open
- Dropped dead stk-menu-btn exemption in stkSetPillsDisabled
- i18n: Меню STK / Меню не задано картой
- SW cache v36
2026-09-09 13:59:27 +03:00
catarrh f7b826b243 v1.9.24: add GET RESPONSE to RAM/GP chain builder
- Added get-response to CHAIN_CMDS_RAM
- Added + GET RESPONSE button to RAM/GP sub-tab
- Added Le input field template in chainRamFieldsHtml
- Added CLA=80 GET RESPONSE handler in chainRamBuildRowHex
- Le stripping for Case 4 + GET RESPONSE already handled by chainBuildHex
2026-09-01 21:14:18 +03:00
catarrh 8b55a7159f v1.9.24: chain builder for SIM/USIM/RAM C-APDU sub-tabs
Replaced single-command builders with row-based chain builder:
- chainInit/chainAddRow/chainDeleteRow/chainRender
- chainSimBuildRowHex/chainRamBuildRowHex per-sub-tab hex builders
- Auto-updating chain preview (no Generate button)
- GET RESPONSE as fixed row in SIM chain
- Le stripping for Case 4 + GET RESPONSE (ETSI TS 102 226)
- Each row: command dropdown + context fields + delete x + hex preview
- + SIM: SELECT (FID/path/chain/GET RESPONSE), PIN ops, file ops
- + USIM: SELECT with FCP requests, silent mode, record ops
- + RAM/GP: INSTALL[for install/delete/move], LOAD, STORE DATA, GET STATUS
- UICC toolkit nested inside EA (C900 EF00 EA...), SIM CA (EF CA)
- Updated tests: 21 SIM + 11 RAM tests for new chain builder API
- Removed old genSimUsim/genRam/OPS/updateSimUsimFields and friends
- SW cache v35, version 1.9.24
2026-09-01 07:50:14 +03:00
catarrh 8c41655e18 v1.9.23: auto-detect PC/SC reader with retry on startup
__main__.py:
- Probe smartcard.System.readers() before init_reader when no reader
  was explicitly specified
- Retry 3 times with 2s delay to handle late pcscd startup and
  USB enumeration delays

start.sh:
- Also match pcscd.bin process name (some distros)
- Wait 1s for pcscd if not running yet
2026-08-31 22:49:52 +03:00
catarrh 29c3143909 v1.9.22: Execute button inline with Operation selector, i18n for SCP80/RAM
- Moved Execute button to the right of the Operation dropdown (same row)
- Removed standalone Execute button that was on its own row
- Added LANG_RU translations: Card preset, Operation, Explore Card, Install Package
2026-08-31 22:43:40 +03:00
catarrh 57c414de07 v1.9.21: SCP80 docs as parent section, GP+JavaCard AID labels, UX fixes
Docs:
- SCP80 tab is now a parent section (3) with subsections 3.1/3.2/3.3
- Renumbered sections: Response parser→4, Card reader→5, Server→6, Version compat→7
- Applied to help.html, help-ru.html, README.md, README_RUS.md

C-APDU/RAM form labels (GP + JavaCard terminology):
- AID → Application / Instance AID
- ELF AID → Load File AID / Package AID
- Module AID → Executable Module AID / Applet Class AID

SCP80/RAM explorer labels (GP + JavaCard terminology):
- ISD → ISD (Issuer Security Domain)
- Applications → Applications / Applet Instances
- Executable Load Files → Executable Load Files (ELFs) / Packages
- AID → Application/Instance AID or Load File AID/Package AID (context-dependent)
- Module AIDs → Executable Module AIDs / Applet Class AIDs

UX fixes:
- Delete All button now matches Delete button style (red)
- Explorer results cleared when switching away from Explore Card operation
- KIc/KID dropdowns (index + algorithm) now update when selecting saved card
- Added LANG_RU translations for new labels
2026-08-31 22:32:32 +03:00
catarrh 2c0889a8a3 docs: complete SCP80/RAM coverage — cards, presets, counter, PoR
- Fix duplicate /api/ram-install in api.md endpoints table
- Add Card Preset section to RAM tab (fields, counter auto-increment)
- Add SCP80 delivery mechanism explanation (ENVELOPE wrapping)
- Add Cards subtab documentation (add/edit/delete presets, CNTR management)
- Add PoR section (delivery PoR vs submit PoR, SPI2 bit 5)
2026-08-31 20:48:15 +03:00
catarrh b5f871641f docs: update RAM section, add explorer delete buttons, add /api/ram-install
- README/README_RUS: RAM tab now describes Explore Card + Install Package operations,
  explorer view with inline Delete/Delete All buttons for Applications and ELFs
- docs/api.md: add full /api/ram-install endpoint documentation
- index.html: remove Delete from dropdown, add ramDeleteFromExplorer() with confirm dialog,
  add Delete buttons in explorer for Apps and ELFs (ELFs get cascade option)
2026-08-31 09:09:20 +03:00
catarrh 8de81a6a33 extract more details about installed ELFs 2026-08-31 00:37:20 +03:00
catarrh 856e691255 v1.9.18: SMS concatenation — incoming reassembly + outgoing limit
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.
2026-08-30 23:26:55 +03:00
catarrh 54dfb2f6b6 v1.9.17: fix explore card — chain GET STATUS + GET RESPONSE inside SCP80
Key fix: re-add chained C0000000 (GET RESPONSE Le=00) in paginate() so the
full GP APDU inside SCP80 is 80F2<p1>p2024F0000C0000000, matching the
working tool. The card's SCP80 layer executes both commands internally
(GET STATUS → 61XX → GET RESPONSE) and puts the final 9000 + data in
the PoR.

Also: server logging improvements (RAM RESPONSE-PACKET label, no truncation
of FETCH/PoR hex), docs for /api/ram-install endpoint, minor test fix.
2026-08-30 14:13:13 +03:00
catarrh 32985a2951 Docs: reflect SCP80 tab grouping (Secured Packet + Cards); SW v24
The Secured Packet and Cards views moved under a single top-level SCP80
tab switched by two pills. Update README and help (EN+RU) section
titles/descriptions to match; bump service-worker cache so the precached
help pages are re-fetched.
2026-08-27 02:26:11 +03:00
catarrh 629c9101bc STATUS: use P2=0C (phone-like, no FCP echo); v1.9.12
_send_status sent F2 00 00 -> the card echoes the FCP of the currently
selected DF on every poll/one-shot STATUS. Phones use P2=0C (no
response expected) to avoid that overhead. Keep P1=00 and the SIM/UICC
P3 distinction (SIM=0x23, UICC=0x00):

  CLA F2 00 0C P3

Affects one-time /api/status-poll and the automatic polling timer.

Version 1.9.11 -> 1.9.12 everywhere; SW cache otaman-v22 -> otaman-v23
2026-08-27 01:11:55 +03:00
catarrh 4f8dae041d Cards table renders at startup and on pill entry; v1.9.11
cardsRender() was mutation-only (add/import/remove/counter-sync), so
after a reload the SCP80 -> Cards table showed 'No cards defined.'
even while localStorage held presets and the SP dropdown listed them
correctly. Now:

- cardsLoad() renders the table right after rebuilding the select
- entering the Cards pill re-renders (mirrors pysim-pill pattern)

Version 1.9.10 -> 1.9.11 everywhere; SW cache otaman-v21 -> otaman-v22
2026-08-25 20:04:55 +03:00
catarrh 7e1916628e SP form: compact layout; v1.9.10
- SPI1/SPI2 side by side in one row
- KIc/KID byte selectors side by side in one row
- KIc/KID key textareas moved directly under the KIc/KID selectors
- Card preset selector moved to the top of the form, full width
- Padding byte joins TAR + Counter in one three-column row
- Pure HTML reflow: no id/handler changes, all behavior preserved
  (invalidation, presets sync, por_ok counter bump)

Version 1.9.9 -> 1.9.10 everywhere; SW cache otaman-v20 -> otaman-v21
2026-08-25 18:39:17 +03:00
catarrh 07c3cdc923 UI: SCP80 top-level tab hosting Secured packet + Cards pills; v1.9.9
Secured Packet and Cards pages move into a new SCP80 top-level tab
(positioned next to C-APDU), switched by rounded-full pills - both
features are SCP80 concerns and more SCP80 operations are planned.

- nav: 'sp'/'cards' buttons replaced by single data-tab=scp80 button
- new #tab-scp80 with scp80-subtab pills and re-parented page wrappers
  (#scp80-sub-sp / #scp80-sub-cards); all element IDs preserved so
  genSp/verify/send-to-card/CNTR-sync/preset flows are untouched
- switchTab(): sp/cards branches replaced by scp80 -> help anchor
- new scp80SwitchSubtab(): pill styling, wrapper visibility, per-pill
  help anchor (secured-packet | cards)
- packToSp() hands off via switchTab('scp80') + Secured packet pill

Version 1.9.8 -> 1.9.9 everywhere; SW cache otaman-v19 -> otaman-v20
2026-08-25 17:06:36 +03:00
catarrh 269ad51f7e SP page: por_ok syncs the selected card preset's counter; v1.9.8
When a secured packet is accepted (por_ok) and a card is selected in
the SP-page card dropdown, the new normalized counter is written back
into that preset (cards[].cntr), persisted via cardsSave() and
reflected in the presets table via cardsRender(). Placeholder option
and unselected state are skipped. Form bump + preset sync happen in
the same okPor block, so form and stored preset always climb together.

Version 1.9.7 -> 1.9.8 everywhere; SW cache otaman-v18 -> otaman-v19
2026-08-25 00:11:56 +03:00
catarrh eb21715050 OTA status stays on the SP page, not in the header; v1.9.7
pysimSendOta wrote its progress/verdict into #pysim-status - the shared
header line owned by connect/equip/poll logic - clobbering the card
status shown under 'Equip card'. OTA outcomes now live exclusively on
the Secured Packet page:

- send start: small gray 'Sending OTA...' in #sp-send-result
- verdicts: big #sp-por-status + details line, as before
- header #pysim-status untouched by OTA flows

Version housekeeping: v1.9.6 -> 1.9.7 everywhere; SW cache
otaman-v17 -> otaman-v18. Also realigns pyproject.toml which the
multi-amend rounds had left stale at 1.9.5.
2026-08-24 23:28:35 +03:00
catarrh bb944e7548 start.sh: drop forced logging flags (use defaults) 2026-08-24 23:06:45 +03:00
catarrh 7ece8a531c Response parser: add TS 51.011 SIM status word families; v1.9.6
OTA inner-APDU SWs from SIM-domain applications surfaced as 'Unknown
status word' (e.g. 9404 on a failed SELECT inside an authenticated
B00000 packet). SW_MAP.generic gains the complete TS 51.011 §9.4
families, so they decode in every resp-cmd context:

- 9200/9240 memory management (retry / memory problem)
- 9400/9402/9404/9408 referencing management (no EF selected,
  out of range, file ID/pattern not found, file inconsistent)
- 9802/9804/9808/9810/9840/9850 security management (CHV/access-
  condition/invalidation contradictions and blocked states; 9850
  also added to generic alongside its gp/uicc copy)
- 9EXX/9FXX data-download error length / response length wildcards

Exact keys safely coexist with the 92XX proactive wildcard (exact
match wins). TS 102 221 sweep found no further stragglers: 63C0-C9
PIN retry counters are covered by the existing 63CX wildcard.

Secured packet page: enforce fresh packet per send
- any change to APDU/TAR/CNTR/keys/padding/SPI now clears the secured
  packet textarea via spInvalidate(), so Send cannot reuse a stale
  packet ('No secured packet to send' guards the path)
- on por_ok the CNTR field auto-increments (10-digit hex normalized,
  wraps modulo 2^40) and the textarea clears; security-error verdicts
  leave both untouched for retry after fixing the cause

SIM/USIM SELECT: P1/P2 per spec + live RFM idioms
- USIM FID selects requested no response data (P2=0C) - that coding is
  reserved for the select-MF-by-empty-data special case; ordinary FID
  selects now request the FCP template (P2=04) with Le='00' per
  TS 102 221 Table 11.2 / pySim sel_ctrl convention; preset tables
  fixed in both genSimUsim and updateP1P2Display
- path method gains base selector: from MF (P1=08) / from current DF
  (P1=09) - reproduces the dominant live RFM idiom 09/0C
- 'silent' checkbox on fid/path/chain emits P2=0C without Le for hops
  where the FCI is not needed
- chain syntax gains GET RESPONSE hops: 'C0' emits CLA C0 00 00 00 and
  'C0:NN' sets explicit Le, enabling classic SELECT -> 9FXX ->
  GET RESPONSE pairs in a single secured packet so the PoR carries the
  actual FCI/response bytes instead of a bare length SW
- SW dictionary: 9FXX reworded to point at GET RESPONSE
- placeholders show the new chain syntax
- findings & backport decisions written to ~/WSL/RFM_notes.md

response_map.test.js: +8 assertions across the new families.
sim.test.js: USIM FID expectation updated; silent/base/chain cases.
apdu_parse.test.js: live capture lines 1-3 as regression fixtures.
Version 1.9.5 -> 1.9.6 everywhere; SW cache otaman-v16 -> otaman-v17
2026-08-24 23:04:54 +03:00
catarrh 33255dc35e start.sh: enable request logging and APDU tracing by default 2026-08-24 20:56:18 +03:00
catarrh 73f46e99c4 genSp: unciphered packets drop CPL prefix + prominent PoR status; v1.9.5
pySim encode_cmd transmits the 2-byte CPL only when ciphering is
applied; unciphered packets start at CHL. genSp always emitted it,
so every SPI1=0x00 packet diverged from the pySim reference at byte 0
(sp-verify MISMATCH) and carried a length octet pair real cards need
not expect.

- genSp output: bytesToHex(ciphering ? packet : packet.subarray(2));
  MAC input unchanged (still covers the virtual-CPL frame, matching
  pySim's sign-then-strip convention)
- Secured packet page: PoR verdict now rendered prominently in a
  dedicated text-base semibold line ('PoR: por_ok' green / other
  statuses red) above the small detail line; hidden when no PoR was
  requested or the ENVELOPE failed
- sp.test.js: three unciphered expectations updated to CHL-first form;
  ciphered vectors untouched (byte-identical)
- version 1.9.4 -> 1.9.5 everywhere; SW cache otaman-v15 -> otaman-v16
2026-08-24 20:55:00 +03:00
catarrh a36f26e8e1 OTA send UX + PoR transparency; v1.9.4
- 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
2026-08-24 20:09:37 +03:00
catarrh 7327a879ce Card presets + custom files: file & pasted JSON import, file export; v1.9.3
Both sections (card presets, custom files) gain two import paths and a
file download alongside the existing clipboard import:
- Import from file: hidden <input type=file accept=.json> + FileReader
  feeding the unchanged dedupe/merge logic (works on plain-HTTP LAN
  origins where navigator.clipboard is unavailable)
- Paste & import: reuses the IO textarea; guard requires '[' prefix so
  status messages are never mis-imported; empty state reveals + focuses
  the field with a hint placeholder
- Export to file: Blob download (otaman-cards.json /
  otaman-custom-files.json) via shared downloadJson() helper
- Import functions take optional text arg; clipboard stays the fallback
- data-l10n on all five buttons per section; LANG_RU entries for the
  three new labels

Version 1.9.2 -> 1.9.3 everywhere; SW cache otaman-v13 -> otaman-v14
2026-08-24 15:49:58 +03:00
catarrh e607637782 Code review fixes: CB label, trailing newlines
- PARSE_INS CB: rename 'RETRIEVE DATA (GET DATA)' -> 'RETRIEVE DATA'
  (CA = GET DATA, CB = RETRIEVE DATA are separate commands per TS 102 221)
- Add trailing newlines to sim.test.js + response_map.test.js
2026-08-23 10:07:56 +03:00
catarrh 072e8fe08b C-APDU encoder features + GET STATUS E3 coverage; v1.9.2
SIM/USIM encoder (registry §2.3):
- New PIN commands: DISABLE '26' / ENABLE '28' (single FF-padded PIN,
  Lc=08) and UNBLOCK '2C' (unblock+new, Lc=10); dynamic field labels
- ACTIVATE/DEACTIVATE FILE target selection: Current EF (case 1),
  by FID (P1=00), path from MF (P1=08), path from current DF (P1=09)
  with Lc+FID/path data form
- LANG_RU entries for new labels

Tests:
- sim.test.js: new stub-DOM genSimUsim harness; exact-hex for all PIN
  ops, ACTIVATE/DEACTIVATE forms, SELECT Le rules, RECORD P1=00 rule
- response_map.test.js: lookupSw wildcards (91XX / 63CX), LIFECYCLE_MAP
  per GPC v2.3, PRIVILEGE_NAMES vs Tables 11-7/11-8/11-9

GET STATUS E3 template decoding verified already present
(TLV_TAG_NAMES + decodeTlvValue cover 4F/9F70/C5/CF/C4/CC/CE/84).

Version 1.9.1 -> 1.9.2, SW cache otaman-v12 -> otaman-v13
2026-08-22 23:54:49 +03:00
catarrh feeefa3018 C-APDU parser + Response parser alignment with updated spec registry
PARSE_INS: +24 command labels per TS 102 221 table 10.5 / TS 102 222 /
GPC (TERMINAL PROFILE, ENVELOPE, FETCH, TERMINAL RESPONSE, SEARCH RECORD,
INCREASE, RETRIEVE DATA, PUT/SET DATA, GET CHALLENGE, AUTHENTICATE odd
INS, SUSPEND UICC, TERMINAL CAPABILITY, MANAGE SECURE CHANNEL, TRANSACT
DATA, GET IDENTITY, EXCHANGE CAPABILITIES, MANAGE LSI, CREATE FILE,
RESIZE FILE 'D4' (registry fixes 'D8' typo), INITIALIZE UPDATE, PUT KEY,
MANAGE CHANNEL; DISABLE/ENABLE/UNBLOCK PIN). INS '88' relabelled
AUTHENTICATE per TS 102 221 §11.1.16.

describeP1P2: SELECT P2 response field per ETSI b5-b3 ('001' FCP /
'011' none); READ/UPDATE BINARY offset / SFI-mode descriptors.

Response parser maps:
- LIFECYCLE_MAP: SD PERSONALIZED is '1F'; '0F' = SECURED (card);
  '01' dual-context OP_READY (card) / LOADED (ELF)
- SW_MAP.uicc: add '91XX' wildcard (proactive lengths vary)
- SW_MAP.generic: add 6A85 / 6A89 / 6A8A; 63CX notes blocked/no-counter
- SW_MAP.gp: reword stale 6310 (P2=42 option was removed as RFU)
2026-08-22 23:39:50 +03:00
catarrh 32509b102c Theme-adaptive favicon: transparent PNG + prefers-color-scheme SVG
- sim.png regenerated from sim.svg with true transparency (was flattened
  onto opaque white; favicon showed a white box)
- new favicon.svg: same glyph, fill moved to presentation attribute +
  embedded @media (prefers-color-scheme: dark) -> white glyph on dark
  browser themes (interface sim.svg untouched, keeps dark:invert)
- index.html: dual icon links (svg for modern browsers, png Safari fallback)
- sw.js: precache favicon.svg, cache bump otaman-v11 -> otaman-v12
2026-08-22 11:17:00 +03:00
catarrh 71d2e80242 C-APDU parser: P1/P2 descriptors, INSTALL LV decode, structural fixes; v1.9.1
Parser labels (§8.3, verified against GPC v2.3 / TS 102 221 PDFs):
- describeP1P2: SELECT P1/P2 (FID/DF-name/path-MF/path-DF; FCP/no-data),
  READ/UPDATE RECORD modes (next/previous/absolute + SFI, P1-ignored note),
  GET STATUS P1 (ISD/Apps/ELF/ELF+Modules) and P2 formats,
  INSTALL P1 bit-aware roles ('for install + for make selectable'),
  SET STATUS P1 (80 ISD / 40 App-or-SSD / 60 SD+associated) with
  card states vs lock/unlock P2, VERIFY/CHANGE PIN ref

Parser structure (§8.4):
- describeInstallDataLv: exact-sum LV walker for all 5 INSTALL layouts
  with privilege bit names and params tag nesting (C9, EF->CA, EA->80);
  falls back to legacy TLV view when lengths do not sum exactly
- GET DATA: case-2 (P3=Le) vs case-4 (Lc + tag list + Le)
- SET STATUS data: 'ignored for ISD' / raw AID / legacy 4F-TLV labeled
- Trailing single byte consumed as Le at end of compact chain
- ACTIVATE/DEACTIVATE: case-1 (4 bytes), legacy empty-Lc, FID/path forms
- Expanded script C-APDU rows now decode into structured APDU nodes
- Compact matcher accepts CLA 84-87 (GP secure messaging)

Version 1.9.0 -> 1.9.1, SW cache otaman-v10 -> otaman-v11
2026-08-22 08:36:20 +03:00
catarrh 7870682f3d Spec-truth pass: decodeTextData disambiguation, privilege tables per GPC v2.3
decodeTextData: legacy no-DCS UCS2 text whose first byte is 00/04 was
misrouted into DCS-aware branches (e.g. 'HI' = 00480049 decoded as GSM7
garbage). Now: dcs=08 strict (fail = '?', no reinterpretation); ambiguous
lead byte 00/04 prefers whole-buffer UCS2 when >=8 hex chars, %4==0,
printable, uniform high bytes; else DCS-aware path; legacy fallback kept.

Privileges per GPC v2.3 Tables 11-7/11-8/11-9 (verified against PDF):
- decodePrivileges rewritten table-driven; fixes wrong byte-2 mapping
  (was: Global Lock/Final Application/Receipt Generation on wrong bits)
  and byte-1 Card Terminate/Card Reset swap
- Reverted 'Token Management' rename: spec uses 'Token Verification'
  (66x vs single Table 11-8 outlier); checkbox + parser restored
- PLAY TONE qualifiers aligned to TS 102 223 §8.6 exact wording

Registry docs/uicc/UICC_SPECS.md corrected from primary sources:
- SET STATUS P1: '20' app/suppl SD -> '40' (GPC Table 11-86)
- SELECT P2 response field is b4b3 ('01' FCP / '11' none), not b3b2b1
  (TS 102 221 Table 11.2)
2026-08-22 08:14:06 +03:00
catarrh 0fd91d7771 C-APDU parser + BER fixes: DCS prefix, qualifier tables, EFRMA refs, CLA labels; v1.9.0
Parser + BER constructor (TS 102 223 / TS 102 226):
- DCS byte: genBerPcValue prepends DCS to 8D (00=GSM7, 08=UCS2, 04=unpacked)
- decodeTextData: consume DCS first, legacy no-DCS fallback kept
- BER_QUAL.display['80']: 'Clear after delay' -> 'Wait for user to clear message'
- BER_QUAL.tone: 'Monotonous/Alternating' -> 'Vibrate alert' names
- describeProactiveCommand: map type byte -> correct BER_QUAL key
  (01->refresh, 21->display, 20->tone); proper command names
- parseActionRow: values 01-7F -> 'Reference to EFRMA record'
- parseOneApdu: CLA 84-87 -> 'GlobalPlatform (secure messaging)'
- PARSE_INS: added GET RESPONSE (INS C0)

Version: 1.8.1 -> 1.9.0, SW cache otaman-v9 -> otaman-v10
2026-08-21 22:53:16 +03:00
catarrh 288c2e5954 C-APDU constructor spec fixes (TS 102 221 / GPC v2.3 / TS 102 226)
SIM/USIM tab (TS 102 221):
- Record P2 modes: 04/06/02 -> 04/02/03; P1=00 for next/previous
- SELECT Le rules: USIM FID no Le, path/dfname P2=04+Le; SIM no Le
- Path placeholder: 3F007FFF6FC5 -> 7FFF6FC5
- ACTIVATE/DEACTIVATE: 5-byte -> 4-byte case-1
- VERIFY PIN: dedicated input, Lc=08, FF-pad to 8 bytes
- CHANGE PIN: old/new inputs, Lc=10, two 8-byte fields
- ERASE BINARY removed from USIM tab

RAM tab (GPC v2.3):
- INSTALL: all 5 variants rewritten to strict LV + Le 00
- ELF/Module AID inputs for install-install
- Privileges: 1 byte if only byte-1, else 3 bytes
- InstallParams: C9 00 + toolkit TLV (mandatory)
- LOAD/DELETE/GET STATUS: append Le 00
- GET STATUS P2: drop 40/42, keep 02/03/00
- GET DATA: 2F00/5031 -> case-4 5C00 data
- SET STATUS: raw AID (no 4F), ISD card states, P1=60
- EXT AUTH: CLA 84, P1 security level, Lc=10
- INT AUTH: CLA 00

Toolkit (TS 102 226):
- MSL: 01 <msl> -> 00 (no check) or 02 01 <msl>
- SIM CA wrapped in EF: C9 00 EF <len> CA...
- TAR length %3 validation
- Menu IDs <= 7F validation

Script Chaining (TS 102 226 Table 5.9a):
- Removed Script ID + Additional Data fields
- Emit strictly 83 01 <flags>
- Deleted generateScriptIdHint

Privilege label: Token Verification -> Token Management
2026-08-21 22:37:39 +03:00
catarrh 0857c5f68b C-APDU Parser: remove tree border lines, keep indentation only 2026-08-20 23:22:52 +03:00
catarrh b832372ac8 C-APDU Parser: indigo hex data color + rebuild CSS 2026-08-20 23:21:03 +03:00
catarrh 1d4693305d C-APDU Parser: descriptive action indicator names (81=session indication, 82=early response) 2026-08-20 23:06:26 +03:00
catarrh 2782ff35c4 C-APDU Parser: increase decoded tree font from text-xs to text-sm 2026-08-20 22:54:40 +03:00
catarrh 5f2eaf6e9d C-APDU Parser: space-separated placeholder for consistency with Response parser 2026-08-20 22:46:44 +03:00
catarrh b7d0d53b94 C-APDU Parser: include raw qualifier value in description 2026-08-20 22:37:51 +03:00
catarrh 1ce23f021c C-APDU Parser: change Parse button to Decode (existing i18n key) 2026-08-20 22:21:39 +03:00
catarrh 72b0116bad C-APDU Parser: client-side hex parser for APDU/expanded script decode
New 'Parser' subtab in C-APDU section. Detects format (compact C-APDU
chain, expanded AA/AE80 script) and renders collapsible tree with decoded
parameters. Key features:
- BER-TLV parser with multi-byte tag support and CR-bit handling
- Proactive command decoder (types from BER_QUAL, devices from BER_DEVICES)
- SIM/UICC toolkit parameter decoder (CA/EA/80)
- GSM7 text decode (7-bit packed, escape sequences)
- Best-effort UCS2/GSM7 text detection
- Compact C-APDU chain with implied-CLA support (SELECT + op chaining)
- 18 test cases from exchange vectors (apdu_parse.test.js)
2026-08-20 22:11:55 +03:00
catarrh 8019326cfb Error Action: render builder directly, remove redundant subtype select
Error Action (tag 82) now renders buildErrorActionRow() directly in
onBerTypeChange, bypassing the shared ber-subtype select which had
confusing 'Action indicator' option and dead 'Proactive command'/'Custom
hex' choices. Removed the now-unreachable type === 'error' branch from
onBerSubTypeChange.
2026-08-20 21:39:59 +03:00
catarrh 01931b9693 Fix Error Action proactive: filter REFRESH out of allowed commands
When Error Action (tag 82) uses the Proactive command subtype, only
DISPLAY TEXT and PLAY TONE are allowed per TS 102 226 Table 5.9.
Pass allowed=['display','tone'] to buildBerPcHtml for error-type rows.
2026-08-20 21:34:38 +03:00
catarrh 03ec13364c Fix onErrorActionTypeChange: sel.nextElementSibling -> sel.parentElement.nextElementSibling
The Error Action HTML structure wraps the select in a flex div sibling to
the .error-action-body div, so sel.nextElementSibling was null.
2026-08-20 21:29:07 +03:00
catarrh e6602c40bf RAM constructor fixes: EA/CA nesting, access domain order, menu pairs, BER lengths, services byte (TS 102 226)
- UICC Toolkit tag 80 nested inside EA (EA 12 80 10 ...)
- ram-tk-mode value ea (was 80), label 'UICC Toolkit (Tag EA)'
- SIM (CA) access domain FIRST in payload, blank = 00
- Menu entries: 2×m pairs (i=1 first, i=m last, else 0000)
- CA/EA lengths via berLenStr() (supports 81xx long form)
- UICC trailing 'Maximum number of services' byte (ram-tk-services)
- DELETE: P1=00 fixed, mode in P2 (80E4 00 <mode> ...)
- STORE DATA: ram-enc 00/40/80/C0/E0 with correct labels
- LOAD: P1 fixed to 0x80 (last block)
- USIM SELECT: selP1 0x09 -> 0x00 (select by file id)
- ram.test.js: 11 exact-hex tests from exchange vectors
- v1.8.1, SW cache otaman-v9
2026-08-20 21:23:37 +03:00
catarrh 4a6a142c3d Expanded Format spec fixes: REFRESH qualifiers, Error Action forms, Script Chaining flags, CR-bit tags (TS 102 226)
- BER_QUAL.refresh: full TS 102 223 §8.6 table (00-0A)
- Error Action rebuilt as single select: Proactive (DISPLAY/PLAY TONE only) /
  No action (82 00) / EFRMA ref (82 01 xx) / Custom hex; drop bogus
  Action Type / Trigger Conditions / Retry Count UI
- buildBerPcHtml(allowed): no Custom option; typeBytes tone 31 -> 20
- DISPLAY TEXT Duration control emitting 84 02 <unit> <interval>
- File list tag 12 -> 92 (CR bit set)
- Script Chaining flags 01/11/02/03 + keep-across-reset checkbox (RFM)
- ber.test.js: exact-hex tests for all above (exchange vectors)
2026-08-20 21:13:41 +03:00
catarrh 0217f8dc92 Error Action Recovery: add Action Indicator / Proactive Command / None selector per TS 102 226 §7.3.3 2026-08-20 02:04:18 +03:00
catarrh 6d8ef55398 Fix Error Action Recovery Action: extract buildBerPcHtml, fix container vs element in genBerPcValue call 2026-08-20 01:48:15 +03:00
catarrh 43242210ab Fix Error Action Recovery Action builder: append to DOM before querying 2026-08-20 01:41:09 +03:00
catarrh 53daf2c6ea Auto-connect on same-origin detection 2026-08-20 01:36:54 +03:00
catarrh 79cf003444 Fix Script Chaining selector bugs in genScriptChainingValue
Fixed all three selectors to use rowIdx from dataset.berIdx:
- 'chaining-][value="first"]' → 'chaining-{rowIdx}[value="first"]'
- 'chaining][value="intermediary"]' → 'chaining-{rowIdx}[value="intermediary"]'
- Already fixed: 'chaining]-last' → 'chaining-{rowIdx}-last'

Now bits 0/1/2 correctly set based on First/Intermediary/Last checkboxes.

Frontend: frontend/index.html
Tests: 14 Node green
2026-08-19 23:51:10 +03:00
catarrh e8a8ef3d1b Fix Script Chaining Last Script checkbox selector typo
Fixed malformed querySelector in genScriptChainingValue:
- 'chaining-]-last' → 'chaining-{rowIdx}-last'
- Last Script checkbox (bit 2) now correctly contributes to flags byte
- First+Last = 0x05, Intermediary+Last = 0x06, Last alone = 0x04

Frontend: frontend/index.html
Tests: 14 Node green
2026-08-19 23:44:33 +03:00
catarrh d75604a751 TS 102 226: Expanded Script compliance with Error Action, Script Chaining, Expanded Response
Server:
- ExpandedRemoteResponse parser for per-command results (TS 102 226 §5.2.2)
- _decode_por() tries expanded parsing first, fallback to CompactRemoteResp
- response_type indicator: 'expanded'/'compact'/'none' + per-command details

Frontend:
- Error Action TLV (tag 82): structured UI with Action Type, SW pattern builder, recovery command, retry count
- Script Chaining TLV (tag 83): First/Intermediary/Last flags, Script ID with auto-increment hints, Additional Data
- Error Action replaces old 'action indicator' dropdown (breaking change)
- genErrorActionValue()/genScriptChainingValue() encoding functions
- updateSwMaskDisplay() for visual SW pattern builder

Documentation:
- help.html/help-ru.html: Error Action, Script Chaining, Expanded Response sections

Tests:
- TestExpandedRemoteResponse: construct parsing, error handling, chaining context
- 49 Python + 14 Node tests green
2026-08-19 23:20:24 +03:00
catarrh 80a860549a v1.8.0: proactive log overhaul, Expanded Script rename, proactive view sizing
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
2026-08-19 18:04:08 +03:00
catarrh a916e8a255 PLI list: vertical layout, full descriptions, wider inputs, row separators
- Drop truncation: full qualifier descriptions (wrap)
- Raw hex input moves under the description, fills row width (flex-1)
- Subtle bottom-border separator between qualifiers
2026-08-16 00:47:06 +03:00
catarrh 62ba09d25e Probe: require JSON version field to confirm same-origin API
SPA-fallback static hosts return 200 HTML for /api/version, which falsely
triggered same-origin mode. Now validate the body parses as JSON with a string
version field before switching to a relative base.
2026-08-15 14:33:00 +03:00
catarrh 966ada21eb Replace embedded flag with same-origin API probe
- Server serves frontend verbatim (drop window.PYSIM_EMBEDDED injection)
- Frontend probes /api/version once; res.ok -> relative base + cleared URL input,
  otherwise fall back to http://127.0.0.1:8080
- sw.js: never intercept/cache /api/* (live card data)
2026-08-15 14:10:24 +03:00
catarrh 1971eca381 pyproject: restrict package discovery to pysim_otaman_server
Fix setuptools flat-layout error ('Multiple top-level packages discovered'
for frontend/ node_modules/ pysim_otaman_server/) by explicit find.include.
2026-08-14 16:02:44 +03:00
catarrh d2c292e5b8 Merge pysim-otaman-server into otaman (monorepo, v1.7.0)
- Move PWA into frontend/; server package at pysim_otaman_server/
- Server now serves the PWA (same origin -> no CORS/PNA): static file handler
  + --web-dir flag + injects window.PYSIM_EMBEDDED marker into index.html
- Frontend defaults to relative API base when embedded (pysimBase = '')
- Version 1.7.0 aligned: server.py VERSION, pyproject.toml, PWA header
- SW cache bump otaman-v7 -> otaman-v8
- Docs: combined README + docs/api.md endpoint reference; update links
- Fix stale package.json repository URL
2026-08-14 15:48:38 +03:00
catarrh 8e96c5d4a0 Reduce input field vertical padding (py-2.5->py-1.5) in C-APDU, Secured Packet, Response parser and Cards tabs 2026-08-14 14:13:30 +03:00
catarrh 05096a184a Reduce top bar height (py-4->py-2, mb-6->mb-3); rebuild Tailwind CSS 2026-08-14 13:44:02 +03:00
catarrh 65127b8cfe Show connect-phase messages in visible slot (was writing to hidden #pysim-status)
#pysim-status lives inside the hidden #pysim-connected-row, so 'Checking card…',
version warnings, and the connection-failure hint were never visible. Add
#pysim-connect-msg inside the disconnected info block and route pre-connect
messages to it.
2026-08-14 00:18:22 +03:00
catarrh a88930b7b1 Show actionable warning when card server fetch fails; fix connect-button locale
- Detect likely browser PNA/local-network block via origin-vs-target heuristic
- Show warning mentioning both causes (server down + browser block)
- Fix 'Connect' button resetting to English after failed connect (t('Connect'))
- Fix 'Connection failed:' translation never matching (trailing-space key)
2026-08-14 00:14:32 +03:00
catarrh ab78455ff6 docs: document browser local-network permission for public-hosted PWA 2026-08-13 23:59:53 +03:00
catarrh e8158fa5c4 v1.6.1: document Private Network Access for public-hosted PWA 2026-08-13 23:35:54 +03:00
catarrh 3c03b04157 Add AES-128/192/256 support to secured packet builder (v1.6.0)
- Bundle aes-js as aes-bundle.js (standalone aesjs global), precache in SW (v7)
- Add AES-CBC (zero IV, 16/24/32B) + AES-CMAC (SP 800-38B, 8-octet) helpers
- genSp(): algorithm dispatch, AES counter hard-block (b5b4 = 10/11 per Rel-18)
- Cross-checked AES vectors against pySim OtaDialectSms.encode_cmd
- Docs: AES + 3DES deprecation, fix stale SPI1 value table
2026-08-13 22:56:35 +03:00
catarrh 6e2a670fc8 Help manual: note SMPP bridge omitted on Windows 2026-08-13 22:11:26 +03:00
catarrh 65aa38dc1b Help manual: recommend Python 3.10-3.13 for pyscard wheels on Windows 2026-08-13 21:41:10 +03:00
catarrh 6418e1e37d Help manual: note pyscard precompiled wheels + C++ Build Tools fallback 2026-08-13 21:31:32 +03:00
catarrh e3b8664286 Fix disconnect message: pySim -> pysim-otaman-server 2026-08-13 21:11:34 +03:00
catarrh 925ff80d80 Add offline HTML help manual (EN/RU) with sidebar TOC + context-aware help link 2026-08-13 15:26:08 +03:00
catarrh 69a20baa9f v1.5.1 2026-08-11 23:54:46 +03:00
catarrh f764760b0a Proactive log: decode PLI response data using PLI_CODECS; hide empty Response 2026-08-11 23:26:05 +03:00
catarrh a2f7b60333 Proactive log: qualifier names for all multi-qual commands, Response line under command 2026-08-11 23:13:32 +03:00
catarrh 3b7dbf1743 Proactive log: inline TR hex, remove expand toggle + polling re-render 2026-08-11 23:05:34 +03:00
catarrh 7c5d0051d4 PWA poll loop: detect card disconnect, show 'Card disconnected' 2026-08-11 22:36:27 +03:00
catarrh 302a1f87a2 Rebuild CSS (gap-6 was purged) 2026-08-11 22:22:17 +03:00
catarrh 52bcd31387 Auto-poll toggle + PWA backend poll loop (5s) for proactive tab 2026-08-11 21:44:37 +03:00
catarrh d4f40aaaef Fix privilege byte 1 decode: add missing Mandated DAP Verification checkbox (bit 1) 2026-08-10 21:41:30 +03:00
catarrh bdf3d1a352 Remove byte count from proactive command list 2026-08-10 20:16:34 +03:00
catarrh c0784a10c6 Proactive log: show TERMINAL RESPONSE hex + SW on expand 2026-08-10 20:12:59 +03:00
catarrh 4de1bd49d8 Proactive log: expandable hex dump + decoded PLI qualifier names 2026-08-10 20:02:54 +03:00
catarrh e4ef33c097 Add Send STATUS button above event list in Proactive UICC 2026-08-10 19:53:56 +03:00
catarrh 6e460908d8 README: Proactive UICC pill features 2026-08-10 19:46:23 +03:00
catarrh ef754befab v1.5.0: PLI data dictionary, event send, rejection cause dropdown, clipboard import fix 2026-08-10 19:44:25 +03:00
65 changed files with 22531 additions and 4182 deletions
+6
View File
@@ -1 +1,7 @@
node_modules/
.venv/
__pycache__/
*.egg-info/
dist/
build/
AGENTS.md
+375 -129
View File
@@ -1,31 +1,49 @@
# OTAMan — APDU Helper & Secured Packet Builder, SIM OTA in PWA
# OTAMan — SIM OTA toolkit: PWA + local card server
Standalone offline HTML/JS tool for building APDU commands for SIM, USIM, and GlobalPlatform RAM, assembling secure packets per ETSI TS 102 225, and constructing BER-TLV command scripts per ETSI TS 102 226.
OTAMan is an offline HTML/JS PWA for building APDU commands (SIM, USIM, GlobalPlatform RAM), assembling SCP80 secured packets per ETSI TS 102 225, and constructing Expanded Remote Application data format APDU per ETSI TS 102 226. A bundled [`pysim-otaman-server`](pysim_otaman_server/) exposes a local HTTP API over pySim for live card operations: file manager, raw APDU, SIM Toolkit menu browsing, and OTA (SCP80) delivery.
Open `index.html` in any modern browser. No server required.
**Demo:** [otaman.atroshin.ru](https://otaman.atroshin.ru) — the PWA alone, for experimenting. Install the server (below) for card-reader functions.
**Demo:** [otaman.atroshin.ru](https://otaman.atroshin.ru)
## Quick start
**PWA only (client-side tools):** open `frontend/index.html` in any browser, or serve `frontend/` with any static server. No Python required.
**Full (PWA + card server):**
```sh
git clone https://github.com/anttro/otaman.git
cd otaman
./setup.sh # or setup.bat on Windows — creates .venv, installs pysim + server
./start.sh # or start.bat — starts the server (it serves the PWA too)
```
Then open http://127.0.0.1:8080 — the UI and API share one origin, so no CORS or browser-permission setup is needed.
## Build
Tailwind CSS is used for styling. After cloning, rebuild the CSS:
```sh
cd frontend
npm install
npm run build
```
## Interface
Six tabs, each with a form and a "Сгенерировать" button.
Five top-level tabs: **Remote APDU**, **SCP80**, **Profiler**, **Card reader**, and **Phone simulator**. **Remote APDU** and **SCP80** use pill sub-tabs; the Card reader tab has three sub-tabs: **File manager**, **pySim command line**, and **Raw APDU**; the Profiler tab lists **Profiles**, **Card snapshots**, and **Custom files**.
---
## SIM RFM Tab
## Remote APDU tab
Builds command APDUs (C-APDUs). Seven sub-tabs cover different card generations, command sets and decoding tools: **SIM RFM**, **USIM RFM**, **Expanded Script**, **RAM/GP**, **HTTP OTA**, **C-APDU Parser**, and **Response parser**.
### SIM RFM
CLA = `A0` (GSM 11.11 / ISO 7816-4).
### Commands
#### Commands
| Command | INS | Description |
|---|---|---|
@@ -40,56 +58,48 @@ CLA = `A0` (GSM 11.11 / ISO 7816-4).
| VERIFY PIN | 20 | Verify PIN1 or PIN2 |
| CHANGE PIN | 24 | Change PIN1 or PIN2 |
### SELECT methods
#### SELECT methods
| Method | P1 | P2 | Input |
|---|---|---|---|
| По FID | 00 | 00 | 2-byte FID (4 hex) |
| По полному пути от MF | 08 | 00 | Full path hex from MF |
| По DF name / AID | 04 | 00 | AID (application ID) |
| ADF RFM цепочка | 00 | 00 | Comma-separated FIDs, each selected in turn |
| By FID | 00 | 00 | 2-byte FID (4 hex) |
| By full path from MF | 08 | 00 | Full path hex from MF |
| By DF name / AID | 04 | 00 | AID (application ID) |
| ADF RFM chain | 00 | 00 | Comma-separated FIDs, each selected in turn |
### Options
#### Options
- **Начать с SELECT** — checkbox to prepend a SELECT command before the operation. When unchecked, the operation is sent standalone with CLA.
- **Режим выборки (P2)** — for record commands: Absolute (04), Next (06), Previous (02).
- **Размер записи** — pad/truncate data to the specified byte count.
- **Переопределить P1/P2** — checkbox to enable manual override of P1/P2 bytes.
- **Start with SELECT** — checkbox to prepend a SELECT command before the operation. When unchecked, the operation is sent standalone with CLA.
- **Selection mode (P2)** — for record commands: Absolute (04), Next (06), Previous (02).
- **Record size** — pad/truncate data to the specified byte count.
- **Allow P1/P2 editing** — checkbox to enable manual override of P1/P2 bytes.
### Conversion sidebar
A conversion panel is embedded in the right-hand column, supporting IMSI, MSISDN, ICCID, SPN, PLMN, and Nibble swap conversions.
### References
#### References
- ISO/IEC 7816-4: Organization, security and commands for interchange
- ETSI TS 102 226: Remote APDU structure for UICC based applications
- GSM 11.11: SIM-ME Interface
---
## USIM RFM Tab
### USIM RFM
CLA = `00` (ETSI TS 102 221). Same commands as SIM, but SELECT uses P1=09, P2=0C (by FID from current directory).
### References
#### References
- ETSI TS 102 221: UICC-Terminal Interface; Physical and Logical Characteristics
- ETSI TS 102 226: Remote APDU structure for UICC based applications
---
## BER-TLV Tab
### Expanded Script
Builds Expanded Remote Application data format per ETSI TS 102 226 §5.2.1.
### Format
#### Format
Two encoding variants:
- **Definite (AA)**: `AA` + length + Command TLVs
- **Indefinite (AE)**: `AE` + `80` + Command TLVs + `00 00`
### Command TLVs
#### Command TLVs
| Type | Tag | Description |
|---|---|---|
@@ -98,7 +108,7 @@ Two encoding variants:
| Error Action | 82 | Proactive command on error |
| Script Chaining | 83 | Chaining data for multi-packet scripts |
### Immediate Action builder
#### Immediate Action builder
When the type is set to Immediate Action, the tool provides a structured builder for:
@@ -108,19 +118,17 @@ When the type is set to Immediate Action, the tool provides a structured builder
Error Action supports the same builder (DISPLAY TEXT, PLAY TONE).
### References
#### References
- ETSI TS 102 226 V13.0.0 §5.2.1: Expanded Remote Application data format
- ETSI TS 102 223: Card Application Toolkit (CAT) — proactive command structure
- ETSI TS 101 220: BER-TLV tag assignments
---
## RAM Tab
### RAM/GP
CLA = `80` (GlobalPlatform Card Specification v2.3.1). Remote Application Management commands for card content management.
### Commands
#### GP Commands Reference
| Command | INS | P1 | Description |
|---|---|---|---|
@@ -138,7 +146,7 @@ CLA = `80` (GlobalPlatform Card Specification v2.3.1). Remote Application Manage
| EXTERNAL AUTHENTICATE | 82 | 00 | SCP host authentication |
| INTERNAL AUTHENTICATE | 88 | 00 | Card challenge-response |
### INSTALL [for install] — Privilege Builder
#### INSTALL [for install] — Privilege Builder
Tag `C7` in the INSTALL data field. Built from 3 privilege bytes (GP spec Tables 11-7, 11-8, 11-9):
@@ -169,7 +177,7 @@ Tag `C7` in the INSTALL data field. Built from 3 privilege bytes (GP spec Tables
|---|---|
| b8 | Receipt Generation |
### INSTALL [for install] — SIM/UICC Toolkit Parameters
#### INSTALL [for install] — SIM/UICC Toolkit Parameters
Optional TLV objects appended to the INSTALL data field:
@@ -186,7 +194,7 @@ Optional TLV objects appended to the INSTALL data field:
| 16 | RC/DS/CC + MAC + Cipher |
| 19 | RC/DS/CC + MAC + Cipher + DS |
### GET STATUS P1 values
#### GET STATUS P1 values
| Value | Meaning |
|---|---|
@@ -195,7 +203,7 @@ Optional TLV objects appended to the INSTALL data field:
| 20 | Executable Load Files |
| 10 | ELF and their Executable Modules |
### GET STATUS P2 values
#### GET STATUS P2 values
| Value | Meaning |
|---|---|
@@ -204,7 +212,7 @@ Optional TLV objects appended to the INSTALL data field:
| 00 | First/all, old format (deprecated) |
| 02 | Next, old format (deprecated) |
### GET DATA tag values
#### GET DATA tag values
| Tag | Data Object |
|---|---|
@@ -222,14 +230,14 @@ Optional TLV objects appended to the INSTALL data field:
| 7F21 | Certificate (SD public key) |
| 5031 | Certificate info (EF.OD) |
### DELETE P1 values
#### DELETE P1 values
| Value | Meaning |
|---|---|
| 00 | By AID |
| 80 | Delete associated objects |
### STORE DATA P1 values
#### STORE DATA P1 values
| Value | Meaning |
|---|---|
@@ -238,7 +246,7 @@ Optional TLV objects appended to the INSTALL data field:
| 80 | Last block, encrypted |
| C0 | More blocks, encrypted |
### SET STATUS parameters
#### SET STATUS parameters
**P1 (Status Type)**:
| Value | Target |
@@ -253,18 +261,101 @@ Optional TLV objects appended to the INSTALL data field:
| 00 | Unlock (return to previous state) |
| 80 | Lock (LOCKED state) |
### References
#### References
- GlobalPlatform Card Specification v2.3.1 (GPC_Spec_v2.3.1): Commands, Privileges, TLV structures
- ETSI TS 102 226 V13.0.0 §8.2.1.3.2: SIM/UICC Toolkit parameters, MSL, TAR, Access Domain
### Conversion (SIM/USIM sidebars)
Value encoding conversions embedded in the SIM RFM and USIM RFM tabs.
#### IMSI → EF.IMSI
Per TS 31.102 §4.2.3. Encodes a 15-digit IMSI into the 9-byte EF.IMSI format:
- Byte 0: number of subsequent bytes (8)
- Odd/even indicator nibble in the last byte
- BCD digits, swapped nibble pairs per identity
Input: 15 decimal digits. Output: 18 hex characters.
#### MSISDN → BCD
Strips leading `+`, pads odd length with `f`, swaps nibble pairs.
#### ICCID → hex
Swaps nibble pairs of the ICCID string.
#### Provider Name → SPN
Per 3GPP TS 31.102 §4.2.5 (EF_SPN). Three encoding paths:
1. **GSM 7-bit packed** (all chars in GSM 7-bit default alphabet): prefix `01`, DCS byte (spare bits), packed septets, 0xFF padding to 16 bytes.
2. **UCS2 non-BMP** (emoji / chars > U+FFFF): prefix `00`, DCS `80`, UTF-16BE, 0xFF padding to 16 bytes.
3. **UCS2 BMP non-GSM7** (Cyrillic, etc.): prefix `00`, DCS `81`, base byte, per-char offsets, 0xFF padding to 16 bytes.
GSM 7-bit alphabet per 3GPP TS 23.038. Full extension table supported.
#### PLMN → EF_PLMNsel / PLMNwAcT
Per TS 31.102 §4.2.3. 3-byte BCD encoding for PLMN, plus optional 2-byte Access Technology selector.
#### Nibble swap
Swaps nibble pairs of an even-length hex string.
#### References
- 3GPP TS 31.102: Characteristics of the USIM Application
- 3GPP TS 23.038: Alphabets and language information
- ETSI TS 102 225: Secured packet structure for (U)SIM toolkit
- pySim: enc_imsi() implementation
### C-APDU Parser
Pastes raw APDU hex and renders a collapsible tree. It auto-detects the container: an **Expanded Script** (leading `AA` or `AE80`, decoded per ETSI TS 102 226 §5.2.1) or a **Compact C-APDU chain** (a sequence of ISO 7816 C-APDUs). Each node shows its label, hex, and a short description; parent nodes expand to reveal their sub-elements.
### HTTP OTA
Builds the Remote Application Management over HTTP payloads defined in GlobalPlatform **GPC v2.2 Amendment B v1.1** (§4.7). Two modes:
- **Trigger (Push SMS)** — administration session triggering parameters (`81 > 83 > 84/[85]/[86]/89`, Table 4-3). This is the message that asks the card's Security Domain to dial out and start an HTTP session.
- **Store (SD admin params)** — writes the same parameters as card (Security Domain) data via **STORE DATA in TLV mode** (`80 E2 90 00`, P1=90 = last block + BER-TLV per GP v2.2 Amendment B v1.1.3), wrapped in tag `85` (or `A5`) per Table 4-4.
| Section | Tag | Contents |
|---|---|---|
| Connection parameters | `84` | COMPREHENSION-TLVs needed to open the TCP connection (OPEN CHANNEL per TS 102 223): Device Identities `02`, Alpha `80`, Bearer `01`, vendor TLVs. Row editor + presets, editable hex. |
| Security parameters | `85` | Table 4-6: LV PSK Identity (text), LV Key version/KID. Identifies the PSK TLS key (RFC 4279). |
| Retry policy | `86` | Table 4-7: retry counter (2 bytes, e.g. `B000`), retry waiting delay as the TS 102 223 timer TLV (`25 03 HH MM SS`), optional vendor-specific report-failure TLV. |
| HTTP POST | `89` | Tables 4-8/9/10: Host header (`8A`), X-Admin-From agent ID (`8B`), URI (`8C`) — text converted to octets. |
The **Command Scripting template** checkbox wraps the whole `81` triggering command in the definite-length Expanded Remote Application data format (`AA`, ETSI TS 102 226 §5.2.1) for TARs that process the expanded format. **Pack into Secured packet** sends the built payload to the SCP80 tab for SPI/counter filling — insert the TAR the SD listens on (typically the OTASD TAR) there.
---
## Secured Packet Tab
Assembles secured packets per ETSI TS 102 225.
### Response parser
### Packet structure
Decodes a raw command response: pick the command that was sent, enter the SW (e.g. `9000`) and the response data hex, then press **Decode**.
- **Command** — SIM/USIM group (SELECT, STATUS, READ/UPDATE, PIN ops, CAT commands like TERMINAL PROFILE/ENVELOPE/FETCH/TERMINAL RESPONSE, MANAGE CHANNEL, ...) or RAM/GP group (INSTALL, LOAD, DELETE, GET/STORE DATA, auth, SCP commands).
- **SW decode** — status words resolved against generic, UICC (TS 102 221), and GlobalPlatform maps, with context auto-detected.
- **Privilege decode** — GET DATA / INSTALL response payloads decode the privilege bytes into human-readable flags.
- **Response data** — raw hex rendered and interpreted per command (e.g. SELECT FCP templates).
---
## SCP80 tab
The **SCP80** top-level tab groups SCP80-related views, switched by three pills: **Secured Packet**, **Cards**, and **RAM**. Assembles secured packets per ETSI TS 102 225.
### Secured Packet
Builds SCP80 secured packets per ETSI TS 102 225.
#### Packet structure
| Field | Size | Description |
|---|---|---|
@@ -281,22 +372,27 @@ Assembles secured packets per ETSI TS 102 225.
| RC/CC/DS | 8 | Cryptographic Checksum / MAC |
| Secured Data | variable | Padded APDU (encrypted if required) |
### SPI1 (Security Level)
#### SPI1 (Security Level)
| Value | Security | Ciphering | Counter |
SPI1 bit layout (TS 102 225 §5.1.1): `b8b6` padding, `b5b4` counter, `b3` ciphering, `b2b1` RC/CC/DS.
| Value | Security | Ciphering | Counter (b5 b4) |
|---|---|---|---|
| 00 | None | No | None |
| 01 | RC | No | None |
| 02 | CC/MAC | No | None |
| 06 | CC/MAC | Yes | None |
| 12 | CC/MAC | No | Available |
| 16 | CC/MAC | Yes | Available |
| 22 | CC/MAC | No | Check higher |
| 26 | CC/MAC | Yes | Check higher |
| 32 | CC/MAC | No | Check +1 |
| 36 | CC/MAC | Yes | Check +1 |
| 00 | None | No | 00 none |
| 01 | RC | No | 00 none |
| 02 | CC/MAC | No | 00 none |
| 06 | CC/MAC | Yes | 00 none |
| 0A | CC/MAC | No | 01 available |
| 0E | CC/MAC | Yes | 01 available |
| 12 | CC/MAC | No | 10 higher |
| 16 | CC/MAC | Yes | 10 higher |
| 1A | CC/MAC | No | 11 +1 |
| 1E | CC/MAC | Yes | 11 +1 |
### SPI2 (PoR settings)
> **AES requires `b5 b4 = 10` (higher) or `11` (+1)** per TS 102 225 §5.1.2 and §5.1.3.1.
> The 3DES values `00/01/02/06` (no counter) remain valid for 3DES only.
#### SPI2 (PoR settings)
| Value | Mode | Security | Cipher |
|---|---|---|---|
@@ -309,97 +405,181 @@ Assembles secured packets per ETSI TS 102 225.
| 02 | PoR on error | None | No |
| 06 | PoR on error | RC | No |
### Crypto
#### Crypto
- **3DES-CBC** encryption (zero ICV), supporting 8, 16, and 24 byte keys
- **Retail MAC** (ISO 9797-1 MAC algorithm 3) for cryptographic checksum
- **3DES-CBC** encryption (zero ICV), supporting 8, 16, and 24 byte keys — deprecated since Rel-18, still supported for backwards compatibility
- **AES-CBC** encryption (zero ICV, zero-padded to 16), supporting 16, 24, and 32 byte keys (TS 102 225 §5.1.2, KIc `x2`)
- **Retail MAC** (ISO 9797-1 MAC algorithm 3) for the DES/3DES cryptographic checksum
- **AES-CMAC** (NIST SP 800-38B, truncated to 8 octets) for the AES cryptographic checksum (TS 102 225 §5.1.3.1, KID `x2`)
- Padding byte configurable (`00` per TS 102 225 default, or `FF`)
### References
#### PoR (Proof of Reception)
- ETSI TS 102 225 V13.0.0: Secured packet structure for UICC based applications
PoR confirms the card received and executed the secured packet. Two modes:
| SPI2 (bit 5) | Mode | Description |
|---|---|---|
| `0x00` | Delivery PoR | PoR is returned in the ENVELOPE response SW+data |
| `0x20` | Submit PoR | PoR is sent back as an SMS-SUBMIT via a proactive FETCH command |
Delivery PoR (SPI2 `01`) is simpler — the card returns the PoR directly in the ENVELOPE response. Submit PoR (SPI2 `21`) is used when the card cannot respond inline (e.g. during ELF operations where the ENVELOPE response space is limited).
#### References
- ETSI TS 102 225 V18.1.0: Secured packet structure for UICC based applications
- ETSI TS 102 226: Remote APDU structure for UICC based applications
- ISO 9797-1: MAC algorithms
- NIST SP 800-38B: CMAC
### Cards
Stores saved card configurations (presets). Each preset stores the cryptographic keys, SPI settings, TAR, and replay counter needed for SCP80 operations.
| Field | Description |
|---|---|
| SPI1 / SPI2 | Security level and PoR settings |
| KIc / KID key | Encryption and MAC key hex |
| KIc / KID index | Key version number |
| TAR | Toolkit Application Reference (3 bytes) |
| Counter (CNTR) | 10-digit hex replay counter, auto-incremented after each successful SCP80 send |
**Add a card:** fill in the name, SPI1/SPI2, KIc/KID keys and indices, TAR, and click **Add**. The card appears in the list and becomes available in the RAM tab's **Card preset** dropdown.
**Edit a card:** click a card in the list, modify fields, click **Save**.
**Delete a card:** select a card, click **Delete**. Removes the preset from `localStorage`.
**Counter:** the 10-digit hex counter (CNTR) is auto-incremented after each successful SCP80 send (both manual Secured Packet sends and RAM operations). The updated counter is saved back to the preset automatically.
### RAM
All RAM operations are delivered as SCP80 secured packets (ETSI TS 102 225) via SMS-PP-DOWNLOAD ENVELOPE. The card must support SCP03 (AES or 3DES) for secure transport.
Select a saved card configuration from the **Card preset** dropdown. If no preset is selected, the RAM tab warns and refuses to execute.
The RAM subtab offers two operations selected from the **Operation** dropdown:
| Operation | Description |
|---|---|
| **Explore Card (all GP data)** | Queries GET STATUS for ISD, Applications, ELFs, and ELF Modules, plus GET DATA FF21 for memory info. Results appear in an explorer view with per-item **Delete** buttons. |
| **Install Package (.cap file)** | Sends a `.cap` file to the card via the server: INSTALL\[for load\] → LOAD ×N → INSTALL\[for install (+make selectable)\]. |
#### Explorer View
After "Explore Card" runs, the explorer view displays:
- **ISD** — AID, lifecycle, privileges (no delete; the ISD cannot be removed)
- **Applications** — AID, lifecycle, privileges, associated ELF/SD. Each has a **Delete** button (GP `DELETE` by AID).
- **Executable Load Files** — AID, lifecycle, version, module AIDs. Each has **Delete** (ELF only) and **Delete All** (cascade: ELF + modules + installed Applications, P2=0x80) buttons.
Delete confirms via a browser prompt before sending the GP `DELETE` command via SCP80. The explorer auto-refreshes after a successful deletion.
---
## Conversion (SIM/USIM sidebars)
Value encoding conversions embedded in the SIM RFM and USIM RFM tabs.
### IMSI → EF.IMSI
Per TS 31.102 §4.2.3. Encodes a 15-digit IMSI into the 9-byte EF.IMSI format:
- Byte 0: number of subsequent bytes (8)
- Odd/even indicator nibble in the last byte
- BCD digits, swapped nibble pairs per identity
Input: 15 decimal digits. Output: 18 hex characters.
### MSISDN → BCD
Strips leading `+`, pads odd length with `f`, swaps nibble pairs.
### ICCID → hex
Swaps nibble pairs of the ICCID string.
### Provider Name → SPN
Per 3GPP TS 31.102 §4.2.5 (EF_SPN). Three encoding paths:
1. **GSM 7-bit packed** (all chars in GSM 7-bit default alphabet): prefix `01`, DCS byte (spare bits), packed septets, 0xFF padding to 16 bytes.
2. **UCS2 non-BMP** (emoji / chars > U+FFFF): prefix `00`, DCS `80`, UTF-16BE, 0xFF padding to 16 bytes.
3. **UCS2 BMP non-GSM7** (Cyrillic, etc.): prefix `00`, DCS `81`, base byte, per-char offsets, 0xFF padding to 16 bytes.
GSM 7-bit alphabet per 3GPP TS 23.038. Full extension table supported.
### PLMN → EF_PLMNsel / PLMNwAcT
Per TS 31.102 §4.2.3. 3-byte BCD encoding for PLMN, plus optional 2-byte Access Technology selector.
### Nibble swap
Swaps nibble pairs of an even-length hex string.
### References
- 3GPP TS 31.102: Characteristics of the USIM Application
- 3GPP TS 23.038: Alphabets and language information
- ETSI TS 102 225: Secured packet structure for (U)SIM toolkit
- pySim: enc_imsi() implementation
---
## Card Reader (pySim integration)
Connects to a local [pysim-otaman-server](https://github.com/anttro/pysim-otaman-server) for live card operations.
Connects to the bundled [`pysim-otaman-server`](pysim_otaman_server/) for live card operations.
> **Browser restriction:** when the PWA is served from a public HTTPS host, reaching the local server (`http://127.0.0.1:8080`) requires two things: the server must send `Access-Control-Allow-Private-Network: true` (pysim-otaman-server ≥ 1.6.1 does this automatically), and the browser must be allowed to access the local network — in Chrome/Edge/Vivaldi: Site settings → Local network access → allow the site (or accept the permission prompt). Without the browser permission, the request to `127.0.0.1` is blocked before any preflight is sent.
### File Browser
Browse the UICC filesystem in a tree view. Files are shown with names, FIDs, and AIDs (for ADFs). Click to read contents.
- Entries are grouped with DFs above EFs and sorted by **FID** or symbolic **Name** (pills above the tree, remembered in `localStorage`)
- **Read** — reads the selected file (auto-detects transparent vs record files)
- **Edit** — switch to edit mode, modify hex data, click **Save** to write back
- **Raw / Decoded** — toggle between hex dump and pysim-decoded JSON view
### Custom Files
Files not in pysim's model can be added manually:
1. Switch to the **Custom files** sub-tab
2. Enter the file path (e.g., `3F00/6F46`) and an alias (e.g., `EF.SPN`)
3. Click **Add** — the file appears in the tree in italics (unverified)
4. Click the file to verify existence — on success, it behaves like a model file
Custom files persist in `localStorage` across sessions. Export/import as JSON for sharing.
- Selecting a file shows its FID, file type, size / record layout and the decoded FCI above the contents
- Missing files are shown in red (✗); a present but empty DF shows `(empty)`
- **Probe all files** — walks the whole tree (incl. custom files), marks every entry present/absent with *N / total* progress, stoppable, and ends with a summary; browsing itself stays lazy
### Command Hints
Type a command name in the **pySim command line** input. Usage hints appear as a tooltip after 300ms. Command autocomplete suggestions appear above the input.
---
## Profiler
Verifies that a card matches a named **profile** — an ordered set of rules describing the expected file system and, optionally, file contents. Profiles are stored in `localStorage`.
- **New profile** creates an empty ruleset; **Profile from card** scans the equipped card and generates one rule per existing file; **Profile from snapshot** generates the same ruleset from a saved snapshot (same ignore/mask/FCP-FCI options, no card reader, name prefilled from the snapshot); **Import profile** loads a ruleset from JSON (the name is stored inside the file).
- Each profile row has **Check card ▶** (run against the equipped card), **Check card snapshot** (run offline against a saved snapshot), **Edit**, **Export**, and **Delete**.
A filesystem rule is defined by:
- **Path** — `MF`-rooted (e.g. `MF/7F10/6F3A`) or ADF AID-rooted (e.g. `A0000000871002/6F07`).
- **FCP/FCI check** — **Filetype only (FCP)**, **Filetype + size (FCP)** (adds file size, or record length/count for record files), or **Exact FCI** (byte-for-byte comparison of the raw SELECT FCP template `'62'`, catching FID/AID, life-cycle status, security-attribute, and proprietary-parameter changes).
- **File attributes** — file type, size, record length and record count, taken from the FCP template (any may be left unset).
- **Check contents** (optional) — **Exact** hex equality, or **Mask** where `?` is a per-nibble wildcard (a mask with no `?` is a prefix match, e.g. `0891` for the IMSI MCC/MNC). Record files store a per-record list.
The check report marks each verified aspect (e.g. *filetype ✓, size ✗, contents ✓*), lists mismatches as read-only monospace expected/actual fields aligned in one column, and shows a decoded per-parameter FCI comparison for FCI mismatches. Corrupt FCI data shows whatever decoded before the faulty part plus an explicit decode-failure note; record mismatches list the *matching records*. In the report the mismatch fields and FCI comparison columns are labelled `expected (profile name)` and `actual (card ICCID)` for a live check, or `actual (snapshot name)` for a snapshot check; the results header reads `Profile verification results for: <profile> → <card ICCID>` (or `… → <snapshot name>`; snapshot comparison: `Snapshot comparison results: <master> → <checked>`). **Only mismatches** in the results header hides all passing files and keeps failures and errors only.
#### “Profile from card” scan options
The scan dialog asks for a profile name and offers the FCP/FCI mode described above, an **Ignore contents of files** checklist of frequently-overwritten files (all checked by default except `EF.ARR`; the header checkbox toggles the whole list) — `EF.LOCI`, `EF.PSLOCI`, `EF.EPSLOCI`, `EF.5GS3GPPLOCI`, `EF.Keys`, `EF.KeysPS`, `EF.SMS`, `EF.Kc`, `EF.KcGPRS`, `EF.LOCIGPRS`, `EF.CBMID`, `EF.SMSS`, `EF.ACC`, `EF.EPSNSC`, `EF.START-HFN`, `EF.ARR` — and two checked-by-default mask options that capture only the first 4 bytes of `EF.IMSI` and `EF.ICCID` (uncheck for exact matching). A progress line shows *N / total files* with the current path; the options are locked while scanning. Rules are created only for files that actually exist (a FCP template is returned); custom files from the **Custom files** sub-tab are included under the same existence check.
#### Card snapshots
The list view has two tabs — **Profiles** and **Card snapshots**. A snapshot is an immutable capture of the card filesystem: for every existing file it stores the path, symbolic name, file type, size (or record length/count), the raw FCI from the SELECT response, and the contents whenever the file is readable (no ignore list, no masking). The ICCID is decoded from EF.ICCID and shown next to the snapshot name. The scan also measures every card command (SELECT / READ BINARY / READ RECORD) from command to response and stores min/avg/max per type plus the total scan time; the snapshot view shows these in the summary and the select/read time per file (read time per record). Timings are display-only and ignored by checks/comparisons.
- **New snapshot** scans the card; **Import snapshot** loads JSON.
- Each snapshot row has **Open** (all captured data read-only, raw FCI with decoded FCI and contents; only the name is editable), **Export**, and **Delete**.
- **Check card snapshot** on a profile row runs the profile rules against a snapshot picked from the list, without a card reader. Files whose contents were not captured are reported as unverifiable errors.
- **Compare snapshots** compares two snapshots offline exactly like a profile check: pick the *master* snapshot and the *snapshot to check*, optionally masking the first 4 bytes of EF.IMSI/EF.ICCID (on by default), and get the same report. Every file must match exactly (exact FCI, contents); files present only in the checked snapshot are reported as extra files. In the comparison report the mismatch fields and FCI comparison columns are labeled with the master/checked snapshot names instead of expected/actual.
---
---
#### Custom files
Files not in pysim's model can be added manually:
1. Switch to the **Custom files** tab in the Profiler list
2. Enter the file path (e.g., `3F00/6F46`) and an alias (e.g., `EF.SPN`)
3. Click **Add** — the file appears in the tree in italics (unverified)
4. Use **Edit** on a row to reload it into the form (the button becomes **Save** and a **Cancel** button appears) or **Delete** to remove it
5. Click the file to verify existence — on success, it behaves like a model file
Custom files persist in `localStorage` across sessions. Export/import as JSON for sharing.
## Phone simulator
The **Phone simulator** tab provides real-time CAT session interaction. It has two pills: **Phone** (STK menu, STATUS and polling, subscribed events, proactive command log) and **TR Config** (response data injected into TERMINAL RESPONSEs for proactive commands).
**Subscribed Events** — the card's SET UP EVENT LIST is displayed with per-event **Send** buttons. Clicking opens a form specific to the event type:
- **No-data events** (User Activity, Idle Screen, etc.) — single-click confirmation
- **Location Status** — dropdown for Normal / Limited / No service
- **Access Technology Change** — dropdown for all 13 RAT types
- **Card Reader Status, Language, UICC Access** — appropriate inputs
- **Network Rejection** — full adaptive form with registration type dropdown
(LU / GPRS / EPS / 5GS), location fields (MCC, MNC, LAC, RAC, TAC), access
technology selection, and 53-cause unified rejection cause code dropdown
covering EMM, GMM, 5GMM, and LU causes
**Proactive Command Log** — chronological list of proactive commands encountered (seconds elapsed, type code, name, byte count). Covers SET UP MENU, SET UP EVENT LIST, POLL INTERVAL, DISPLAY TEXT, SELECT ITEM, and PROVIDE LOCAL INFORMATION.
**TR Config: PLI data dictionary** — editable per-qualifier hex values for all 22 PROVIDE LOCAL INFORMATION qualifiers (TS 102 223 + TS 131 111). 10 qualifiers have inline decode/encode forms (toggle):
| Code | Decoded fields |
|------|--------------|
| 00 | MCC, MNC, LAC/TAC |
| 01 | IMEI (15 digits) |
| 03 | Date, Time, TZ offset |
| 04 | Language (2-char code) |
| 05 | ME Status, Timing Advance |
| 06 | Access Technology (dropdown) |
| 08 | IMEISV (16 digits) |
| 09 | Search Mode (Auto/Manual) |
| 0A | Battery charge (%) |
| 0E | Multiple Access Technologies (comma-list) |
Values persist on the server until restart. Apply → hex updates; Save → POSTs to server. The server will use these values to populate TERMINAL RESPONSE data for future PLI proactive commands.
## PWA
OTAMan is a Progressive Web App and can be installed for offline use. Use the **INSTALL PWA** button in the header, or use the browser's install prompt.
@@ -409,6 +589,72 @@ OTAMan is a Progressive Web App and can be installed for offline use. Use the **
## Theme
A dark theme is included. It follows the system preference and can be toggled manually with the header button (🌙/☀️); the choice is stored in `localStorage`.
## Localization
The interface is in English with Russian support. The language is detected from `navigator.language`; the header toggle (EN/RU) stores the choice in `localStorage`. Switching the language also re-renders visible dynamic views (profile lists, check reports, snapshots, cards, proactive views).
## Server (pysim-otaman-server)
The bundled Python server wraps [pySim](https://osmocom.org/projects/pysim/wiki) and serves both the OTAMan PWA (from `frontend/`) and a JSON API under `/api/*`.
### Prerequisites
- **Python 3.8+** with `pip`, and **Git**
- **Smart card reader** (PC/SC or serial/FTDI) — PC/SC is preferable (`pcsc-lite` + `ccid` on Linux)
- **Windows** — use Python 3.103.13 (3.13 recommended): `pyscard` ships precompiled wheels for these versions. On 3.9 / 3.14 it builds from source (needs MSVC C++ Build Tools). The SMPP bridge (`smpp.twisted3`) is intentionally skipped on Windows.
### Scripts
| Script | What it does |
|--------|-------------|
| `setup.sh` / `setup.bat` | Creates `.venv/`, installs pysim and the server. Run once after cloning. |
| `start.sh` / `start.bat` | Starts the server from the venv (serves the PWA + API on `:8080`). |
`start.sh` auto-detects the reader (PC/SC if `pcscd` is running, else `/dev/ttyUSB0`); `start.bat` always uses `-p 0` (PC/SC is built into Windows). If no reader is found the server still starts ("Reader: none") — initialize the card later via the **Equip** button.
### Manual installation
```sh
python3 -m venv .venv
source .venv/bin/activate # Linux/macOS (Windows: .venv\Scripts\activate)
pip install git+https://github.com/osmocom/pysim.git
pip install -e . # editable — serves frontend/ from the source tree
pysim-otaman-server --http-port 8080
```
### CLI options
| Option | Description |
|--------|-------------|
| `--http-host` | Bind address (default: `127.0.0.1`) |
| `--http-port` | TCP port (default: `8080`) |
| `--web-dir` | Directory with PWA static files (default: `<repo>/frontend`) |
| `-p` / `--pcsc-device` | PC/SC reader slot number |
| `-d` / `--device` | Serial device path |
| `--no-card-init` | Skip card init to preserve the CAT session (no file manager) |
| `--apdu-trace` | Log APDU-level traces to stderr |
| `--log-requests` | Log request/response payloads to stderr |
| `--sms-oa` / `--sms-sm-sc` | SMS-DELIVER originating address / SM-SC for PoR-in-submit |
| `--terminal-profile` | TERMINAL PROFILE payload hex (default 10-byte GSM profile) |
| `--poll-interval` | Idle interval before automatic STATUS polling (default 30s; `0` disables polling) |
| `--full-pysim-init` | Use pysim's stock init/equip (redundant card resets). The default init/equip is reset-free — only explicit equip/reset reconnect the card |
| `--no-auto-equip` | Do not initialize a card automatically right after it is inserted (default: auto-equip on) |
| `--menu-timeout` | Auto-answer a paused STK command with a timeout TERMINAL RESPONSE (default 60s; `0` disables) |
| `--timing` | Log phase durations, card resets and APDU counters with elapsed timestamps |
### Troubleshooting
- **"Failed to establish context: Access denied"** — `pcscd` isn't running or the user lacks permission: `sudo systemctl enable --now pcscd && sudo usermod -a -G pcscd $USER`.
- **"device file /dev/ttyUSB0 does not exist"** — no serial reader; connect a USB reader or pass `-d` explicitly. The server still starts without a reader.
### API reference
See [docs/api.md](docs/api.md) for the full endpoint reference.
## Theme
Dark theme is supported. The app follows the OS preference on first visit, and a manual toggle button (🌙/☀️) at the top-right corner persists the choice in `localStorage`.
## Localisation
+339 -105
View File
@@ -1,31 +1,49 @@
# OTAMan — APDU Helper & Secured Packet Builder, SIM OTA в PWA
# OTAMan — SIM OTA toolkit: PWA + локальный сервер карт
Автономный offline-инструмент на HTML/JS для создания APDU-команд для SIM, USIM и GlobalPlatform RAM, сборки защищённых пакетов по ETSI TS 102 225 и построения BER-TLV скриптов по ETSI TS 102 226.
OTAMan — автономный offline-PWA (HTML/JS) для создания APDU-команд (SIM, USIM, GlobalPlatform RAM), сборки защищённых пакетов SCP80 по ETSI TS 102 225 и построения Expanded Remote Application data format APDU по ETSI TS 102 226. В комплекте — [`pysim-otaman-server`](pysim_otaman_server/) — локальный HTTP-сервер поверх pySim для работы с картой: файловый менеджер, сырые APDU, меню SIM Toolkit и доставка OTA.
Откройте `index.html` в любом современном браузере. Сервер не требуется.
**Демо:** [otaman.atroshin.ru](https://otaman.atroshin.ru) — только PWA, для экспериментов. Для функций картридера установите сервер (ниже).
**Демо:** [otaman.atroshin.ru](https://otaman.atroshin.ru)
## Быстрый старт
**Только PWA (клиентские функции):** откройте `frontend/index.html` в любом браузере или раздайте `frontend/` любым статическим сервером. Python не нужен.
**Полная установка (PWA + сервер карт):**
```sh
git clone https://github.com/anttro/otaman.git
cd otaman
./setup.sh # или setup.bat в Windows — создаёт .venv, ставит pysim + сервер
./start.sh # или start.bat — запускает сервер (он же раздаёт PWA)
```
Затем откройте http://127.0.0.1:8080 — интерфейс и API на одном origin, поэтому CORS и разрешения браузера не нужны.
## Сборка
Для стилей используется Tailwind CSS. После клонирования пересоберите CSS:
```sh
cd frontend
npm install
npm run build
```
## Интерфейс
Шесть вкладок, каждая с формой и кнопкой «Generate APDU».
Пять вкладок: **Remote APDU**, **SCP80**, **Profiler**, **Card reader** и **Phone simulator**. Вкладки Remote APDU и SCP80 используют пиллы-подвкладки; во вкладке Card reader три подвкладки: **File manager**, **pySim command line** и **Raw APDU**; во вкладке Profiler — **Profiles**, **Card snapshots** и **Custom files**.
---
## Вкладка SIM RFM
## Вкладка Remote APDU
Построение команд APDU (C-APDU). Семь подвкладок для разных поколений карт, наборов команд и инструментов разбора: **SIM RFM**, **USIM RFM**, **Expanded Script**, **RAM/GP**, **HTTP OTA**, **Разбор C-APDU** и **«Парсер ответов»**.
### SIM RFM
CLA = `A0` (GSM 11.11 / ISO 7816-4).
### Команды
#### Команды
| Команда | INS | Описание |
|---|---|---|
@@ -40,7 +58,7 @@ CLA = `A0` (GSM 11.11 / ISO 7816-4).
| VERIFY PIN | 20 | Проверка PIN1 или PIN2 |
| CHANGE PIN | 24 | Смена PIN1 или PIN2 |
### Методы SELECT
#### Методы SELECT
| Метод | P1 | P2 | Ввод |
|---|---|---|---|
@@ -49,46 +67,38 @@ CLA = `A0` (GSM 11.11 / ISO 7816-4).
| By DF name / AID | 04 | 00 | AID |
| ADF RFM chain | 00 | 00 | FID через запятую |
### Опции
#### Опции
- **Start with SELECT** — добавить SELECT перед командой.
- **Selection mode (P2)** — для record-команд: Absolute (04), Next (06), Previous (02).
- **Record size** — дополнить/обрезать данные до указанного размера.
- **Allow P1/P2 editing** — ручное редактирование P1/P2.
### Боковая панель конвертации
Поддерживает IMSI, MSISDN, ICCID, SPN, PLMN, Nibble swap.
### Ссылки
#### Ссылки
- ISO/IEC 7816-4: Organization, security and commands for interchange
- ETSI TS 102 226: Remote APDU structure for UICC based applications
- GSM 11.11: SIM-ME Interface
---
## Вкладка USIM RFM
### USIM RFM
CLA = `00` (ETSI TS 102 221). Те же команды, что и SIM, но SELECT использует P1=09, P2=0C.
### Ссылки
#### Ссылки
- ETSI TS 102 221: UICC-Terminal Interface
- ETSI TS 102 226: Remote APDU structure
---
## Вкладка BER-TLV
### Expanded Script
Построение Expanded Remote Application data format по ETSI TS 102 226 §5.2.1.
### Формат
#### Формат
- **Definite (AA)**: `AA` + длина + Command TLV
- **Indefinite (AE)**: `AE` + `80` + Command TLV + `00 00`
### Command TLV
#### Command TLV
| Тип | Тег | Описание |
|---|---|---|
@@ -97,25 +107,23 @@ CLA = `00` (ETSI TS 102 221). Те же команды, что и SIM, но SELE
| Error Action | 82 | Proactive-команда при ошибке |
| Script Chaining | 83 | Данные для многопакетных скриптов |
### Сборщик Immediate Action
#### Сборщик Immediate Action
- **Action indicator**: `81` / `82`
- **Proactive command**: REFRESH, DISPLAY TEXT, PLAY TONE
- **Custom hex**: ручной ввод
### Ссылки
#### Ссылки
- ETSI TS 102 226 §5.2.1
- ETSI TS 102 223: Card Application Toolkit
- ETSI TS 101 220: BER-TLV tag assignments
---
### RAM/GP
## Вкладка RAM
CLA = `80` (GlobalPlatform v2.3.1). Удалённое управление содержимым карты через SCP80.
CLA = `80` (GlobalPlatform v2.3.1). Команды удалённого управления приложениями.
### Команды
#### Справочник GP-команд
| Команда | INS | P1 | Описание |
|---|---|---|---|
@@ -133,7 +141,7 @@ CLA = `80` (GlobalPlatform v2.3.1). Команды удалённого упра
| EXTERNAL AUTHENTICATE | 82 | 00 | Аутентификация SCP |
| INTERNAL AUTHENTICATE | 88 | 00 | Challenge-response |
### Привилегии (INSTALL)
#### Привилегии (INSTALL [for install])
Три байта привилегий по GP Spec Tables 11-7, 11-8, 11-9.
@@ -164,7 +172,7 @@ CLA = `80` (GlobalPlatform v2.3.1). Команды удалённого упра
|---|---|
| b8 | Receipt Generation |
### Параметры SIM/UICC Toolkit
#### Параметры SIM/UICC Toolkit
- **Tag `CA`** (SIM Toolkit): Priority, Timers, Text Length, Menu Entries, Positions, Channels, MSL, TAR, Access Domain
- **Tag `80`** (UICC Toolkit, внутри `EA`): те же поля без Access Domain
@@ -179,7 +187,7 @@ CLA = `80` (GlobalPlatform v2.3.1). Команды удалённого упра
| 16 | RC/DS/CC + MAC + Cipher |
| 19 | RC/DS/CC + MAC + Cipher + DS |
### GET STATUS P1
#### GET STATUS P1
| Значение | Описание |
|---|---|
@@ -188,7 +196,7 @@ CLA = `80` (GlobalPlatform v2.3.1). Команды удалённого упра
| 20 | Executable Load Files |
| 10 | ELF и модули |
### GET STATUS P2
#### GET STATUS P2
| Значение | Описание |
|---|---|
@@ -197,7 +205,7 @@ CLA = `80` (GlobalPlatform v2.3.1). Команды удалённого упра
| 00 | Первые/все, старый формат (deprecated) |
| 02 | Следующие, старый формат (deprecated) |
### GET DATA теги
#### GET DATA теги
| Тег | Объект данных |
|---|---|
@@ -215,14 +223,14 @@ CLA = `80` (GlobalPlatform v2.3.1). Команды удалённого упра
| 7F21 | Certificate (SD public key) |
| 5031 | Certificate info (EF.OD) |
### DELETE P1
#### DELETE P1
| Значение | Описание |
|---|---|
| 00 | Только AID |
| 80 | AID и связанные объекты |
### STORE DATA P1
#### STORE DATA P1
| Значение | Описание |
|---|---|
@@ -231,24 +239,97 @@ CLA = `80` (GlobalPlatform v2.3.1). Команды удалённого упра
| 80 | Последний блок, с шифрованием |
| C0 | Ещё блоки, с шифрованием |
### SET STATUS
#### SET STATUS
**P1:** 80 = ISD, 40 = Приложение или SSD, 60 = SD и его приложения
**P2:** 00 = Разблокировать, 80 = Заблокировать (LOCKED)
### Ссылки
#### Ссылки
- GlobalPlatform Card Specification v2.3.1
- ETSI TS 102 226 §8.2.1.3.2: Параметры SIM/UICC Toolkit
### Конвертация (боковые панели SIM/USIM)
#### IMSI → EF.IMSI
15-значный IMSI → 9 байт EF.IMSI.
#### MSISDN → BCD
Удаление `+`, добавление `f`, обмен полубайтов.
#### ICCID → hex
Обмен полубайтов строки ICCID.
#### Provider Name → SPN
По 3GPP TS 31.102 §4.2.5. Три варианта кодирования:
1. GSM 7-bit packed
2. UCS2 non-BMP
3. UCS2 BMP non-GSM7
#### PLMN → EF_PLMNsel / PLMNwAcT
3-байтное BCD-кодирование + опциональный Access Technology.
#### Nibble swap
Обмен полубайтов hex-строки.
#### Ссылки
- 3GPP TS 31.102
- 3GPP TS 23.038
- ETSI TS 102 225
- pySim: enc_imsi()
### Разбор C-APDU
Вставьте сырой hex APDU — отобразится раскрывающееся дерево. Контейнер определяется автоматически: **Expanded Script** (ведущие `AA` или `AE80`, разбор по ETSI TS 102 226 §5.2.1) или **компактная цепочка C-APDU** (последовательность ISO 7816 C-APDU). У каждого узла — метка, hex и краткое описание; родительские узлы раскрываются до подэлементов.
### HTTP OTA
Сборка payload-ов Remote Application Management over HTTP по GlobalPlatform **GPC v2.2 Amendment B v1.1** (§4.7). Два режима:
- **Trigger (Push SMS)** — параметры запуска административной сессии (`81 > 83 > 84/[85]/[86]/89`, Table 4-3). Это сообщение просит Security Domain карты выйти в сеть и начать HTTP-сессию.
- **Store (SD admin params)** — запись тех же параметров как данных карты (Security Domain) через **STORE DATA в TLV-режиме** (`80 E2 90 00`, P1=90 = последний блок + BER-TLV по GP v2.2 Amendment B v1.1.3), обёрнутых в тег `85` (или `A5`) по Table 4-4.
| Секция | Тег | Содержимое |
|---|---|---|
| Параметры соединения | `84` | COMPREHENSION-TLV для открытия TCP-соединения (OPEN CHANNEL по TS 102 223): Device Identities `02`, Alpha `80`, Bearer `01`, вендорские TLV. Редактор строк + пресеты, редактируемый hex. |
| Параметры безопасности | `85` | Table 4-6: LV PSK Identity (текст), LV Key version/KID. Идентифицирует ключ PSK TLS (RFC 4279). |
| Политика повторов | `86` | Table 4-7: счётчик повторов (2 байта, напр. `B000`), задержка повтора как timer TLV TS 102 223 (`25 03 HH MM SS`), опциональный вендорский TLV отчёта об ошибке. |
| HTTP POST | `89` | Tables 4-8/9/10: заголовок Host (`8A`), X-Admin-From agent ID (`8B`), URI (`8C`) — текст преобразуется в октеты. |
Чекбокс **Command Scripting template** оборачивает всю команду `81` в формат Expanded Remote Application с определённой длиной (`AA`, ETSI TS 102 226 §5.2.1) для TAR, обрабатывающих расширенный формат. **Pack into Secured packet** отправляет собранный payload на вкладку SCP80 для заполнения SPI/счётчика — укажите там TAR, который слушает SD (обычно OTASD).
---
## Вкладка Secured Packet
Сборка защищённых пакетов по ETSI TS 102 225.
### Парсер ответов
### Структура пакета
Декодирование ответа команды: выберите отправленную команду, введите SW (например, `9000`) и данные ответа в hex, затем нажмите **Decode**.
- **Команда** — группа SIM/USIM (SELECT, STATUS, READ/UPDATE, операции с PIN, CAT-команды TERMINAL PROFILE/ENVELOPE/FETCH/TERMINAL RESPONSE, MANAGE CHANNEL, ...) или группа RAM/GP (INSTALL, LOAD, DELETE, GET/STORE DATA, аутентификация, команды SCP).
- **Декодирование SW** — статусные слова по картам generic, UICC (TS 102 221) и GlobalPlatform с автоопределением контекста.
- **Декодирование привилегий** — байты привилегий из ответов GET DATA / INSTALL в читаемые флаги.
- **Данные ответа** — hex с интерпретацией по команде (например, шаблоны FCP из SELECT).
---
## Вкладка SCP80
Вкладка **SCP80** группирует SCP80-виды, переключаемые тремя пиллами: **Secured Packet**, **Cards** и **RAM**. Сборка защищённых пакетов по ETSI TS 102 225.
### Secured Packet
Сборка защищённых пакетов SCP80 по ETSI TS 102 225.
#### Структура пакета
| Поле | Размер | Описание |
|---|---|---|
@@ -265,22 +346,27 @@ CLA = `80` (GlobalPlatform v2.3.1). Команды удалённого упра
| RC/CC/DS | 8 | Контрольная сумма / MAC |
| Secured Data | переменная | APDU (с шифрованием при необходимости) |
### SPI1 (Уровень безопасности)
#### SPI1 (Уровень безопасности)
| Значение | Безопасность | Шифрование | Счётчик |
Битовое поле SPI1 (TS 102 225 §5.1.1): `b8b6` — паддинг, `b5b4` — счётчик, `b3` — шифрование, `b2b1` — RC/CC/DS.
| Значение | Безопасность | Шифрование | Счётчик (b5 b4) |
|---|---|---|---|
| 00 | Нет | Нет | Нет |
| 01 | RC | Нет | Нет |
| 02 | CC/MAC | Нет | Нет |
| 06 | CC/MAC | Да | Нет |
| 12 | CC/MAC | Нет | Available |
| 16 | CC/MAC | Да | Available |
| 22 | CC/MAC | Нет | Check higher |
| 26 | CC/MAC | Да | Check higher |
| 32 | CC/MAC | Нет | Check +1 |
| 36 | CC/MAC | Да | Check +1 |
| 00 | Нет | Нет | 00 нет |
| 01 | RC | Нет | 00 нет |
| 02 | CC/MAC | Нет | 00 нет |
| 06 | CC/MAC | Да | 00 нет |
| 0A | CC/MAC | Нет | 01 available |
| 0E | CC/MAC | Да | 01 available |
| 12 | CC/MAC | Нет | 10 higher |
| 16 | CC/MAC | Да | 10 higher |
| 1A | CC/MAC | Нет | 11 +1 |
| 1E | CC/MAC | Да | 11 +1 |
### SPI2 (PoR)
> **AES требует `b5 b4 = 10` (higher) или `11` (+1)** согласно TS 102 225 §5.1.2 и §5.1.3.1.
> Значения `00/01/02/06` (без счётчика) допустимы только для 3DES.
#### SPI2 (PoR)
| Значение | Режим |
|---|---|
@@ -293,58 +379,177 @@ CLA = `80` (GlobalPlatform v2.3.1). Команды удалённого упра
| 02 | PoR on error, no security |
| 06 | PoR on error, RC |
### Крипто
#### Крипто
- **3DES-CBC** шифрование, ключи 8/16/24 байт
- **Retail MAC** (ISO 9797-1 MAC algorithm 3)
- **3DES-CBC** шифрование, ключи 8/16/24 байт — устарело с Rel-18, но поддерживается для обратной совместимости
- **AES-CBC** шифрование (нулевой ICV, дополнение нулями до 16), ключи 16/24/32 байта (TS 102 225 §5.1.2, KIc `x2`)
- **Retail MAC** (ISO 9797-1 MAC algorithm 3) для DES/3DES
- **AES-CMAC** (NIST SP 800-38B, усечённый до 8 октетов) для AES (TS 102 225 §5.1.3.1, KID `x2`)
- Padding byte: `00` (по умолчанию) или `FF`
### Ссылки
#### PoR (Proof of Reception)
- ETSI TS 102 225 V13.0.0
PoR подтверждает, что карта получила и выполнила защищённый пакет. Два режима:
| SPI2 (бит 5) | Режим | Описание |
|---|---|---|
| `0x00` | Delivery PoR | PoR возвращается в ответе ENVELOPE (SW+данные) |
| `0x20` | Submit PoR | PoR отправляется обратно как SMS-SUBMIT через прокоманду FETCH |
Delivery PoR (SPI2 `01`) проще — карта возвращает PoR напрямую в ответе ENVELOPE. Submit PoR (SPI2 `21`) используется, когда карта не может ответить inline (ограничено пространство ответа ENVELOPE).
#### Ссылки
- ETSI TS 102 225 V18.1.0
- ETSI TS 102 226
- ISO 9797-1
- NIST SP 800-38B (CMAC)
### Cards
Пилл **Cards** управляет сохранёнными конфигурациями карт (пресеты). Каждый пресет хранит криптографические ключи, настройки TAR и счётчик повторов для SCP80-операций.
| Поле | Описание |
|---|---|
| SPI1 / SPI2 | Уровень безопасности и настройки PoR |
| Ключ KIc / KID | Hex ключи шифрования и MAC |
| Индекс KIc / KID | Номер версии ключа |
| TAR | Toolkit Application Reference (3 байта) |
| Счётчик (CNTR) | 10-значный hex счётчик повторов, автоматически увеличивается после каждой успешной отправки SCP80 |
**Добавить карту:** заполните имя, SPI1/SPI2, ключи KIc/KID и их индексы, TAR, нажмите **Add**. Карта появится в списке и станет доступна в выпаданом списке **Card preset** на RAM-вкладке.
**Редактировать карту:** выберите карту в списке, измените поля, нажмите **Save**.
**Удалить карту:** выберите карту, нажмите **Delete**. Удаляет пресет из `localStorage`.
**Счётчик:** 10-значный hex-счётчик (CNTR) автоматически увеличивается после каждой успешной отправки SCP80 (ручные отправки Secured Packet и RAM-операции). Обновлённый счётчик автоматически сохраняется обратно в пресет.
### RAM
Все операции RAM отправляются как защищённые пакеты SCP80 (ETSI TS 102 225) через SMS-PP-DOWNLOAD ENVELOPE. Карта должна поддерживать SCP03 (AES или 3DES) для безопасной транспортировки.
Выберите сохранённую конфигурацию карты из выпадающего списка **Card preset**. Если пресет не выбран, RAM-вкладка предупреждает и отказывается выполнять.
В RAM-подвкладке доступны две операции через выпадающий список **Operation**:
| Операция | Описание |
|---|---|
| **Explore Card (all GP data)** | Запрос GET STATUS для ISD, приложений, ELF и модулей ELF, а также GET DATA FF21 для информации о памяти. Результаты отображаются в обзоре с кнопками **Delete** для каждого элемента. |
| **Install Package (.cap file)** | Отправка `.cap` файла на карту через сервер: INSTALL\[for load\] → LOAD ×N → INSTALL\[for install (+make selectable)\]. |
#### Обзор карты (Explorer View)
После выполнения "Explore Card" отображается:
- **ISD** — AID, жизненный цикл, привилегии (без удаления; ISD нельзя удалить)
- **Приложения** — AID, жизненный цикл, привилегии, связанный ELF/SD. Каждое имеет кнопку **Delete** (GP `DELETE` по AID).
- **Executable Load Files** — AID, жизненный цикл, версии, AID модулей. Каждый имеет **Delete** (только ELF) и **Delete All** (каскадное: ELF + модули + установленные приложения, P2=0x80).
Удаление подтверждается через диалог браузера перед отправкой команды GP `DELETE` через SCP80. Обзор автоматически обновляется после успешного удаления.
---
## Конвертация (боковые панели SIM/USIM)
## Card Reader (интеграция с pySim)
### IMSI → EF.IMSI
Подключение к встроенному [`pysim-otaman-server`](pysim_otaman_server/) для работы с картой.
15-значный IMSI → 9 байт EF.IMSI.
> **Ограничение браузера:** если PWA раздаётся с публичного HTTPS-хоста, для доступа к локальному серверу (`http://127.0.0.1:8080`) нужны два условия: сервер должен отправлять `Access-Control-Allow-Private-Network: true` (pysim-otaman-server ≥ 1.6.1 делает это автоматически), и браузеру должно быть разрешено обращаться к локальной сети — в Chrome/Edge/Vivaldi: Настройки сайта → Доступ к локальной сети → разрешить сайт (или подтвердить запрос). Без разрешения браузера запрос к `127.0.0.1` блокируется ещё до отправки preflight.
### MSISDN → BCD
### Файловый менеджер
Удаление `+`, добавление `f`, обмен полубайтов.
Дерево файлов UICC. Отображаются имена, FID и AID (для ADF). Клик для чтения содержимого.
### ICCID → hex
- Элементы сгруппированы (DF выше EF) и отсортированы по **FID** или символьному **имени** (пиллы над деревом, выбор сохраняется в `localStorage`)
- **Read** — чтение файла (автоопределение transparent/record)
- **Edit** — режим редактирования, измените hex-данные и нажмите **Save** для записи
- **Raw / Decoded** — переключение между hex-дампом и декодированным JSON
- При выборе файла над содержимым показываются FID, тип файла, размер / структура записей и декодированный FCI
- Отсутствующие файлы показаны красным (✗); существующий пустой DF — `(пусто)`
- **Проверить все файлы** — обход всего дерева (включая пользовательские) с пометкой «есть/нет», прогрессом *N / всего*, возможностью остановки и сводкой в конце; сам просмотр остаётся ленивым
Обмен полубайтов строки ICCID.
### Подсказки команд
### Provider Name → SPN
По 3GPP TS 31.102 §4.2.5. Три варианта кодирования:
1. GSM 7-bit packed
2. UCS2 non-BMP
3. UCS2 BMP non-GSM7
### PLMN → EF_PLMNsel / PLMNwAcT
3-байтное BCD-кодирование + опциональный Access Technology.
### Nibble swap
Обмен полубайтов hex-строки.
### Ссылки
- 3GPP TS 31.102
- 3GPP TS 23.038
- ETSI TS 102 225
- pySim: enc_imsi()
Введите имя команды в **pySim command line**. Подсказки по использованию появляются через 300 мс. Автодополнение команд — над полем ввода.
---
## Профайлер
Проверка соответствия карты именованному **профилю** — упорядоченному набору правил, описывающих ожидаемую файловую систему и (опционально) содержимое файлов. Профили хранятся в `localStorage`.
- **Новый профиль** создаёт пустой набор правил; **Профиль с карты** сканирует подключённую карту и создаёт по правилу на каждый существующий файл; **Профиль из снимка** создаёт тот же набор правил из сохранённого снимка (те же опции игнорирования/масок/FCP-FCI, без картридера, имя подставляется из снимка); **Импорт профиля** загружает набор из JSON (имя хранится внутри файла).
- В каждой строке профиля: **Проверить карту ▶** (на подключённой карте), **Проверить снимок карты** (offline по сохранённому снимку), **Редактировать**, **Экспорт** и **Удалить**.
Правило файловой системы задаётся:
- **Путь** — от `MF` (напр. `MF/7F10/6F3A`) или от AID ADF (напр. `A0000000871002/6F07`).
- **Проверка FCP/FCI** — **Только тип файла (FCP)**, **Тип файла + размер (FCP)** (добавляет размер файла или длину/число записей) либо **Полный FCI** (побайтовое сравнение сырого шаблона FCP `'62'` из ответа SELECT — ловит изменения FID/AID, life-cycle, security attributes и проприетарных параметров).
- **Атрибуты файла** — тип, размер, длина и число записей из шаблона FCP (любое можно не задавать).
- **Проверка содержимого** (опционально) — **Exact** (точное равенство hex) или **Mask**, где `?` — пониббловый джокер (маска без `?` — префиксное совпадение, напр. `0891` для MCC/MNC IMSI). Для record-файлов хранится список записей.
Отчёт проверки помечает каждый аспект (напр. *тип файла ✓, размер ✗, содержимое ✓*), показывает расхождения как поля только для чтения (ожидаемое/фактическое в одной колонке) и декодированное сравнение параметров FCI для расхождений FCI. Повреждённые FCI показывают всё, что удалось декодировать, плюс явное сообщение об ошибке; для записей указываются *совпадающие записи*. В отчёте поля расхождений и колонки сравнения FCI подписаны `ожидалось (имя профиля)` и `фактически (ICCID карты)` для проверки карты либо `фактически (имя снимка)` для проверки снимка; в заголовке отчёта — `Результаты проверки профиля: <профиль> → <ICCID карты>` (или `… → <имя снимка>`; для сравнения снимков — `Результаты сравнения снимков: <эталон> → <проверяемый>`). Опция **«Только расхождения»** скрывает все совпавшие файлы, оставляя несовпадения и ошибки.
#### Опции сканирования «Профиль с карты»
Диалог сканирования запрашивает имя профиля и предлагает режим FCP/FCI, список **«Игнорировать содержимое файлов»** (все включены, кроме `EF.ARR`; чекбокс в заголовке переключает весь список) — `EF.LOCI`, `EF.PSLOCI`, `EF.EPSLOCI`, `EF.5GS3GPPLOCI`, `EF.Keys`, `EF.KeysPS`, `EF.SMS`, `EF.Kc`, `EF.KcGPRS`, `EF.LOCIGPRS`, `EF.CBMID`, `EF.SMSS`, `EF.ACC`, `EF.EPSNSC`, `EF.START-HFN`, `EF.ARR` — и две включённые по умолчанию маски, сохраняющие только первые 4 байта `EF.IMSI` и `EF.ICCID`. Строка прогресса показывает *N / всего файлов* с текущим путём; во время сканирования опции заблокированы. Правила создаются только для существующих файлов; пользовательские файлы из подвкладки **Custom files** проверяются на существование так же.
#### Снимки карт
Представление списка имеет две вкладки — **«Профили»** и **«Снимки карт»**. Снимок — неизменяемая фиксация файловой системы: путь, символьное имя, тип, размер (или длина/число записей), сырой FCI и содержимое (если читается) каждого существующего файла. ICCID декодируется из EF.ICCID и показывается рядом с именем. При сканировании также измеряется время каждой команды карты (SELECT / READ BINARY / READ RECORD) от отправки до ответа; сохраняются min/сред/max по типам и общее время сканирования — они показываются в сводке снимка и по файлам/записям. Время носит информационный характер и не используется при проверках и сравнении.
- **Новый снимок** сканирует карту; **Импорт снимка** загружает JSON.
- В строке снимка: **Открыть** (все данные только для чтения, сырой FCI с декодированным и содержимое; редактируется только имя), **Экспорт**, **Удалить**.
- **Проверить снимок карты** в строке профиля выполняет правила профиля на выбранном снимке без картридера. Файлы без захваченного содержимого помечаются как непроверяемые ошибки.
- **Сравнить снимки** сравнивает два снимка offline так же, как проверка профиля: выберите *эталонный* снимок и *снимок для проверки*, при необходимости включите маску первых 4 байт EF.IMSI/EF.ICCID (включена по умолчанию). Всё должно совпадать точно (FCI, содержимое); файлы только в проверяемом снимке помечаются как лишние. В отчёте поля расхождений и колонки сравнения FCI подписаны именами эталонного и проверяемого снимков вместо expected/actual.
---
---
#### Пользовательские файлы
Файлы, отсутствующие в модели pysim, можно добавить вручную:
1. Перейдите на вкладку **Custom files** в списке профайлера
2. Введите путь (например, `3F00/6F46`) и псевдоним (например, `EF.SPN`)
3. Нажмите **Add** — файл появится в дереве курсивом (непроверенный)
4. Кнопка **Edit** загружает запись в форму (кнопка становится **Save**, появляется **Cancel**), **Delete** удаляет запись без подтверждения
5. Кликните для проверки существования — при успехе работает как обычный файл
Пользовательские файлы сохраняются в `localStorage`. Экспорт/импорт в JSON для обмена.
## Симулятор телефона
Вкладка **«Симулятор телефона»** обеспечивает взаимодействие с CAT-сессией в реальном времени. Две подвкладки: **«Телефон»** (меню STK, STATUS и опрос, подписанные события, журнал проактивных команд) и **«Конфигурация TR»** (данные ответов, подставляемые в TERMINAL RESPONSE для проактивных команд).
**Subscribed Events** — список событий SET UP EVENT LIST с кнопками **Send**. Клик открывает форму для конкретного типа события:
- **События без данных** (User Activity, Idle Screen и др.) — однократное уведомление
- **Location Status** — выпадающий список: Normal / Limited / No service
- **Access Technology Change** — 13 типов RAT
- **Network Rejection** — полная адаптивная форма: тип регистрации (LU / GPRS / EPS / 5GS), поля локации (MCC, MNC, LAC, RAC, TAC), доступные технологии, 53-позиционный выпадающий список причин отказа (EMM, GMM, 5GMM, LU)
**Proactive Command Log** — хронологический список проактивных команд. Каждая строка показывает время, код типа, имя и декодированный квалификатор.
**Конфигурация TR: словарь PLI** — редактируемые hex-значения для всех 22 квалификаторов PROVIDE LOCAL INFORMATION (TS 102 223 + TS 131 111). 10 квалификаторов имеют встроенные формы декодирования/кодирования:
| Код | Декодированные поля |
|------|--------------|
| 00 | MCC, MNC, LAC/TAC |
| 01 | IMEI (15 цифр) |
| 03 | Дата, время, TZ |
| 04 | Язык (2-символьный код) |
| 05 | ME Status, Timing Advance |
| 06 | Access Technology (выпадающий список) |
| 08 | IMEISV (16 цифр) |
| 09 | Search Mode (Auto/Manual) |
| 0A | Battery charge (%) |
| 0E | Multiple Access Technologies (список через запятую) |
Значения сохраняются на сервере до перезапуска. Apply → hex обновляется; Save → POST на сервер.
## PWA
OTAMan — Progressive Web App. Можно установить для offline-использования через кнопку **INSTALL PWA** или через браузер.
@@ -358,7 +563,7 @@ OTAMan — Progressive Web App. Можно установить для offline-
## Локализация
Интерфейс на английском с поддержкой русского языка. Язык определяется из `navigator.language`. Кнопка переключения (EN/RU) в заголовке сохраняет выбор в `localStorage`.
Интерфейс на английском с поддержкой русского языка. Язык определяется из `navigator.language`. Кнопка переключения (EN/RU) в заголовке сохраняет выбор в `localStorage`. При переключении языка динамические представления (списки профилей, отчёты проверок, снимки, карты, proactive) перерисовываются.
## Совместимость версий
@@ -372,29 +577,58 @@ PWA проверяет версию сервера при подключении
---
## Card Reader (интеграция с pySim)
## Сервер (pysim-otaman-server)
Подключение к локальному [pysim-otaman-server](https://github.com/anttro/pysim-otaman-server) для работы с картой.
Встроенный Python-сервер оборачивает [pySim](https://osmocom.org/projects/pysim/wiki) и раздаёт как PWA (из `frontend/`), так и JSON API по `/api/*`.
### Файловый менеджер
### Требования
Дерево файлов UICC. Отображаются имена, FID и AID (для ADF). Клик для чтения содержимого.
- **Python 3.8+** с `pip`, и **Git**
- **Смарт-картридер** (PC/SC или serial/FTDI) — предпочтителен PC/SC (`pcsc-lite` + `ccid` на Linux)
- **Windows** — используйте Python 3.10–3.13 (рекомендуется 3.13): у `pyscard` есть готовые wheel. На 3.9 / 3.14 он собирается из исходников (нужны MSVC C++ Build Tools). SMPP-мост (`smpp.twisted3`) в Windows сознательно не ставится.
- **Read** — чтение файла (автоопределение transparent/record)
- **Edit** — режим редактирования, измените hex-данные и нажмите **Save** для записи
- **Raw / Decoded** — переключение между hex-дампом и декодированным JSON
### Скрипты
### Пользовательские файлы
| Скрипт | Назначение |
|--------|-------------|
| `setup.sh` / `setup.bat` | Создаёт `.venv/`, ставит pysim и сервер. Запускать один раз после клонирования. |
| `start.sh` / `start.bat` | Запускает сервер из venv (раздаёт PWA + API на `:8080`). |
Файлы, отсутствующие в модели pysim, можно добавить вручную:
`start.sh` автоопределяет ридер (PC/SC при работающем `pcscd`, иначе `/dev/ttyUSB0`); `start.bat` всегда использует `-p 0`. Без ридера сервер всё равно стартует («Reader: none») — карту можно инициализировать позже кнопкой **Equip**.
1. Перейдите на вкладку **Custom files**
2. Введите путь (например, `3F00/6F46`) и псевдоним (например, `EF.SPN`)
3. Нажмите **Add** — файл появится в дереве курсивом (непроверенный)
4. Кликните для проверки существования — при успехе работает как обычный файл
### Ручная установка
Пользовательские файлы сохраняются в `localStorage`. Экспорт/импорт в JSON для обмена.
```sh
python3 -m venv .venv
source .venv/bin/activate # Linux/macOS (Windows: .venv\Scripts\activate)
pip install git+https://github.com/osmocom/pysim.git
pip install -e . # editable — раздаёт frontend/ из исходного дерева
pysim-otaman-server --http-port 8080
```
### Подсказки команд
### Параметры CLI
Введите имя команды в **pySim command line**. Подсказки по использованию появляются через 300 мс. Автодополнение команд — над полем ввода.
| Параметр | Описание |
|----------|-------------|
| `--http-host` | Адрес привязки (по умолчанию `127.0.0.1`) |
| `--http-port` | Порт (по умолчанию `8080`) |
| `--web-dir` | Каталог со статикой PWA (по умолчанию `<repo>/frontend`) |
| `-p` / `--pcsc-device` | Номер слота PC/SC |
| `-d` / `--device` | Путь к serial-устройству |
| `--no-card-init` | Пропустить инициализацию карты (сохранить CAT-сессию) |
| `--apdu-trace` | Лог APDU-трафика в stderr |
| `--log-requests` | Лог запросов/ответов в stderr |
| `--poll-interval` | Интервал автоопроса STATUS (по умолчанию 30с; `0` отключает опрос) |
| `--full-pysim-init` | Штатная инициализация/equip из pysim (с лишними сбросами карты). По умолчанию инициализация без лишних сбросов — карта переподключается только по явным equip/reset |
| `--no-auto-equip` | Не инициализировать карту автоматически сразу после вставки (по умолчанию автоинициализация включена) |
| `--menu-timeout` | Автоответ timeout TERMINAL RESPONSE на приостановленную STK-команду (по умолчанию 60с; `0` отключает) |
| `--timing` | Лог длительности фаз, сбросов карты и счётчиков APDU с отметками времени |
### Устранение неполадок
- **"Failed to establish context: Access denied"** — `pcscd` не запущен или нет прав: `sudo systemctl enable --now pcscd && sudo usermod -a -G pcscd $USER`.
- **"device file /dev/ttyUSB0 does not exist"** — нет serial-ридера; подключите USB-ридер или укажите `-d`. Сервер всё равно стартует без ридера.
### Справочник API
Полный справочник endpoints: [docs/api.md](docs/api.md).
+446
View File
@@ -0,0 +1,446 @@
# pysim-otaman-server — HTTP API reference
The server exposes a JSON HTTP API under `/api/*`. All responses carry
`Access-Control-Allow-Origin: *` (plus `Access-Control-Allow-Private-Network: true`
on the preflight), so the API is reachable from a separately-hosted PWA.
## Version compatibility
| Server | PWA (OTAMan) | Status |
|--------|-------------|--------|
| 1.x.x | 1.x.x | ✅ Compatible |
| 0.x.x | 1.x.x | ❌ Outdated — update server |
| 2.x.x+ | 1.x.x | ⚠️ Server newer — update PWA |
The server reports its version via `GET /api/version`. The PWA checks this on
connect and warns if versions are incompatible.
## Endpoints
| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/api/version` | GET | Server version string |
| `/api/status` | GET | Card reader + card info + current selection |
| `/api/command` | POST | pySim command (equip, status, tree, etc.) |
| `/api/commands` | GET | List available pySim commands |
| `/api/tree` | POST | File tree browser for given FID/name |
| `/api/select` | POST | Select a file by name or FID |
| `/api/read` | POST | Read file content |
| `/api/write` | POST | Write raw hex data to a file |
| `/api/apdu` | POST | Raw APDU send |
| `/api/help` | POST | pySim help for a given command |
| `/api/send-ota` | POST | SCP80 OTA secured packet delivery |
| `/api/ram-install` | POST | Install a Java Card `.cap` file via SCP80 (INSTALL[for load] → LOAD ×N → INSTALL[for install]) |
| `/api/sp-verify` | POST | Verify secured packet against pySim reference |
| `/api/menu` | GET | Current STK menu (title + items + active) |
| `/api/menu-select` | POST | ENVELOPE(Menu Selection) with item_id |
| `/api/menu-respond` | POST | TERMINAL RESPONSE for paused STK command |
| `/api/stk-status` | GET | STK session state (active/pending/type) |
| `/api/events` | GET | Event list from SET UP EVENT LIST |
| `/api/event-send` | POST | Send ENVELOPE(Event Download) |
| `/api/proactive-log` | GET | Last 50 proactive commands |
| `/api/status-poll` | POST | Manual STATUS poll + FETCH if 91XX |
| `/api/rescue` | POST | Re-send TERMINAL PROFILE to recover CAT session |
| `/api/poll-status` | GET | Background STATUS polling state |
| `/api/poll-toggle` | POST | Enable/disable background polling |
| `/api/pli-qualifiers` | GET | List of qualifier codes with descriptions |
| `/api/pli-dict` | GET | Current dictionary (hex values per qualifier) |
| `/api/pli-dict` | POST | Update dictionary entries |
## Endpoint details
### `GET /api/version`
Returns server version for compatibility checking.
**Example response:**
```json
{"version": "2.1.2"}
```
### `GET /api/status`
Card reader, card type, current selection, and card state.
### `GET /api/commands`
List all available shell commands for the current card profile.
### `POST /api/command`
Execute any pysim-shell command.
```json
{"cmd": "select MF"}
```
Returns:
```json
{"output": "..."}
```
### `POST /api/apdu`
Send a raw APDU to the card.
```json
{"apdu": "00A4040000..."}
```
Returns:
```json
{"response": "...", "sw": "9000"}
```
### `POST /api/help`
Get structured help for a shell command.
```json
{"cmd": "apdu"}
```
Returns:
```json
{"usage": "apdu [-h] [--expect-sw EXPECT_SW] [--raw] APDU", "description": "...", "args": [{"name": "APDU", "type": "positional", "help": "..."}]}
```
### `POST /api/send-ota`
Send an OTA command (SCP80) to the card via SMS-PP-DOWNLOAD ENVELOPE.
The secured packet is delivered in an SMS-DELIVER TPDU wrapped in an ENVELOPE command.
**Request body:**
```json
{
"sp": "00201516011515b00000...",
"spi1": "16",
"spi2": "01",
"kic": "15",
"kid": "15",
"tar": "b00000",
"cntr": "0000000001",
"kicKey": "D6FCC023...",
"kidKey": "1B07E7E0..."
}
```
**Response (delivery PoR):**
```json
{"success": true, "sw": "9000", "response_data": "027100000e0a...",
"por": {"response_status": "por_ok", "tar": "B00000", "pcntr": 0,
"decoded": {"number_of_commands": 1, "last_status_word": "6e00",
"last_response_data": ""}}}
```
**Response (submit PoR):** PoR is extracted from the SMS-SUBMIT TPDU
fetched via a proactive command (FETCH). The response contains the
same `por` structure if decoding succeeds.
The SPI2 `por_in_submit` bit (0x20) selects submit-mode PoR.
### `POST /api/ram-install`
Install a Java Card `.cap` file on the card via GlobalPlatform commands (INSTALL[for load] → LOAD ×N → INSTALL[for install (+ make selectable)]) wrapped in SCP80 secured packets. Each step is sent via ENVELOPE and its PoR is checked; the sequence aborts on the first PoR error. The `.cap` archive (a ZIP of nested components) is parsed server-side in `_cap_parse`; no external tooling is required.
**Request body:**
```json
{
"cap_hex": "DECAFFED...",
"sd_aid": "A000000003000000",
"install_params": "C90000",
"stk_params": "",
"nv_quota": 0,
"volatile_quota": 0,
"make_selectable": true,
"spi1": "0E", "spi2": "01",
"kic": "15", "kid": "15",
"tar": "000000",
"cntr": "0000000001",
"kicKey": "D6FCC023...",
"kidKey": "1B07E7E0..."
}
```
| Field | Req | Description |
|---|---|---|
| `cap_hex` | yes | Even-length hex of the `.cap` file (zipped Java Card CAP), max 48 kB (98304 hex chars) |
| `sd_aid` | no | Security Domain AID for INSTALL[for load]; empty → default ISD `A000000003000000` |
| `install_params` | no | Hex C9 TLV install parameters; if empty, `gen_install_parameters()` is used with the quota/stk params |
| `stk_params` | no | Hex CA TLV (TS 102 226 §8.2.1.3.2.1) for SIM toolkit app-specific params |
| `nv_quota` / `volatile_quota` | no | Integer memory quotas (bytes) for `gen_install_parameters()` |
| `make_selectable` | no | If true (default), final INSTALL uses P1=`0C` (install + make selectable) |
**Response (success):**
```json
{"success": true, "failed_step": null,
"steps": [{"name": "install_for_load", "apdu": "80E60200...", "por_status": "por_ok", "sw": "9000"},
{"name": "load_0", "apdu": "80E80000...", "por_status": "por_ok", "sw": "9000"},
{"name": "install_for_install", "apdu": "80E60C00...", "por_status": "por_ok", "sw": "9000"}],
"final_cntr": "0000000004",
"load_file_aid": "A000000003000000",
"module_aid": "A000000003000000",
"application_aid": "A000000003000000"}
```
**Response (failure):**
```json
{"success": false, "failed_step": "load_1",
"steps": [{"name": "install_for_load", "por_status": "por_ok", "sw": "9000"},
{"name": "load_1", "por_status": "rc_error", "sw": null}],
"error": "..."}
```
The `steps` array contains one entry per GP command. `final_cntr` is the counter value after all successful steps (use it to update the card preset). The response is not streamed — all steps run server-side before the JSON is returned.
### `POST /api/sp-verify`
Cross-check a secured packet against pySim's `OtaDialectSms.encode_cmd`
reference. Returns the JS-generated packet, pySim reference, a match flag,
and the decoded SPI fields.
```json
{"spi1": "16", "spi2": "01", "kic": "15", "kid": "15", "tar": "b00000",
"cntr": "0000000001", "apdu": "00a40000023f00",
"kicKey": "D6FCC023...", "kidKey": "1B07E7E0..."}
```
**Response:**
```json
{"js_sp": "...", "py_sp": "...", "match": true,
"diffs": [], "spi": {"counter": "counter_must_be_higher", ...}}
```
### `GET /api/menu`
Returns the SIM Toolkit SETUP MENU captured from the card's TERMINAL PROFILE
response at startup. Empty `{"items": []}` if the card didn't send a menu.
**Response:**
```json
{"command_number": 1, "items": [{"id": 128, "text": "Настройки/Settings"}],
"title": "Alfa Mobile", "active": false}
```
### `POST /api/menu-select`
Sends an `ENVELOPE(MENU SELECTION)` with the selected item ID, then handles
the card's proactive response (DISPLAY TEXT or SELECT ITEM).
```json
{"item_id": 128}
```
**Response:**
```json
{"type": "display_text", "text": "Hello", "sw": "9122"}
```
or
```json
{"type": "select_item", "items": [{"id": 1, "text": "Sub-menu"}], "sw": "9122"}
```
### `POST /api/menu-respond`
Sends `TERMINAL RESPONSE` to the current proactive command with the given result
code. Continues the proactive chain if the card responds with `91XX`. If no
response arrives within `--menu-timeout` seconds (default 60, `0` disables), the
server watchdog sends the `timeout` result itself.
```json
{"result": "ok", "item_id": 1}
```
| `result` | TERMINAL RESPONSE code | Meaning |
|---|---|---|
| `ok` | `0x00` | Command performed successfully |
| `cancel` | `0x10` | Proactive session terminated by the user |
| `back` | `0x11` | Backward move in the proactive session requested by the user |
| `timeout` | `0x12` | No response from the user |
### `GET /api/stk-status`
Returns the current STK session state.
```json
{"active": true, "pending": true, "pending_type": "select_item"}
```
### `POST /api/read`
Read file content. Auto-detects transparent vs record files.
```json
{"name": "EF.ICCID", "fid": "2FE2", "parent_path": ["MF"], "mode": "raw"}
```
Returns transparent data:
```json
{"success": true, "sw": "9000", "file_type": "transparent", "data": "...",
"apdu_times": [{"type": "select", "ms": 12}, {"type": "read_binary", "ms": 9}]}
```
Returns records:
```json
{"success": true, "sw": "9000", "file_type": "linear_fixed",
"records": [{"num": 1, "data": "..."}, {"num": 2, "data": "..."}],
"apdu_times": [{"type": "select", "ms": 12},
{"type": "read_record", "ms": 11}, {"type": "read_record", "ms": 13}]}
```
`apdu_times` reports each command's duration (command sent to response
received) classified as `select`, `read_binary` or `read_record`; the PWA uses
it for snapshot timing statistics. Other commands are not reported.
### `POST /api/write`
Write raw hex data to a file.
```json
{"name": "EF.ICCID", "fid": "2FE2", "data": "A0A1A2...", "parent_path": ["MF"]}
```
For record files:
```json
{"name": "EF.ADN", "fid": "6F3A", "data": "A0A1...", "record_nr": 1, "parent_path": ["MF", "7F10"]}
```
Returns:
```json
{"success": true, "sw": "9000"}
```
### `POST /api/select`
Select a file by name or FID, with optional parent selection.
```json
{"name": "EF.ICCID", "fid": "2FE2", "parent_path": ["MF"]}
```
`parent_path` lists the path segments from MF to the parent (ADF names or
FIDs); the legacy single-segment `parent_sel` is still accepted but is only
unambiguous for ADFs. Resolution is strictly parent-scoped: model-known files
are selected through the requested parent only (pySim `select_file()`), never
via pySim's global selectables or its `probe_file()` model injection, so a
same-FID file under another parent is never picked and the filesystem model
is not modified. `allow_probe: true` (PWA custom files) additionally allows a
model-unknown 4-hex FID to be selected directly; any temporary model object
created for it is detached again before the response is sent.
Returns:
```json
{"name": "EF.ICCID", "fid": "2FE2", "file_type": "transparent",
"file_size": 10, "record_len": null, "num_of_rec": null,
"fci_hex": "621082024021...",
"apdu_times": [{"type": "select", "ms": 12}], "exists": true}
```
`fci_hex` is the raw FCP template (`'62'`) from the SELECT response, used by
the PWA's Exact FCI checks; `file_size` / `record_len` / `num_of_rec` drive
the profiler's size and record checks. When the file does not exist the
endpoint responds `404` with `{"error": "...", "exists": false}`.
### `POST /api/tree`
Get directory listing with typed children.
```json
{"name": "MF", "fid": "3F00"}
```
Use `parent_path` (or the legacy `parent_sel`) to list a subdirectory, e.g.
`{"name": "DF.GSM-ACCESS", "fid": "5F3B", "parent_path": ["MF", "ADF.USIM"]}`.
Returns:
```json
{"exists": true, "name": "MF", "fid": "3F00", "file_type": "df", "children": [{"name": "EF.ICCID", "fid": "2fe2", "isDir": false}]}
```
### `GET /api/events`
Returns the event list captured from the card's SET UP EVENT LIST (an array of
event byte values, or `[]` when none was received).
### `POST /api/event-send`
Sends an `ENVELOPE(Event Download)` for a subscribed event.
```json
{"event_type": 4, "event_data": "01A0"}
```
`event_type` is required (the SET UP EVENT LIST event byte); `event_data` is
optional hex for events that carry data. Returns the SW and any response data:
```json
{"sw": "9000", "data": "..."}
```
### `GET /api/proactive-log`
Returns the last 50 proactive commands fetched during CAT sessions, newest
first:
```json
[{"type_hex": "25", "type_name": "SET UP MENU", "elapsed": 3.2, "bytes": 97}]
```
### `POST /api/status-poll`
Manually sends `STATUS` (F2) and, if the card answers `91XX`, runs the
proactive chain (FETCH → TERMINAL RESPONSE) until it settles. Returns:
```json
{"sw": "9000", "proactive": true}
```
### `POST /api/rescue`
Recovers a stuck CAT session by clearing the pending state and re-sending the
TERMINAL PROFILE. Returns whether a menu and event list were captured again:
```json
{"menu": true, "events": [4, 5]}
```
### `GET /api/poll-status`
Background STATUS polling state.
```json
{"enabled": true, "interval": 300}
```
### `POST /api/poll-toggle`
Turns background STATUS polling on or off.
```json
{"enabled": true}
```
Returns the new state (`{"enabled": ..., "interval": ...}`).
### `GET /api/pli-qualifiers`
Lists the PROVIDE LOCAL INFORMATION qualifier codes with their names.
```json
[{"code": "00", "name": "Location Information"}, {"code": "0A", "name": "Battery Charge Level"}]
```
### `GET /api/pli-dict`
Returns the current PLI data dictionary as a qualifier-code map.
```json
{"00": "0291...", "0A": "64"}
```
### `POST /api/pli-dict`
Updates dictionary entries. Body is a map of qualifier code to hex value; keys
must be known qualifiers and values valid hex, otherwise they are ignored.
Returns the updated dictionary.
+807
View File
@@ -0,0 +1,807 @@
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.aesjs = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({"aes-js":[function(require,module,exports){
/*! MIT License. Copyright 2015-2018 Richard Moore <me@ricmoo.com>. See LICENSE.txt. */
(function(root) {
"use strict";
function checkInt(value) {
return (parseInt(value) === value);
}
function checkInts(arrayish) {
if (!checkInt(arrayish.length)) { return false; }
for (var i = 0; i < arrayish.length; i++) {
if (!checkInt(arrayish[i]) || arrayish[i] < 0 || arrayish[i] > 255) {
return false;
}
}
return true;
}
function coerceArray(arg, copy) {
// ArrayBuffer view
if (arg.buffer && arg.name === 'Uint8Array') {
if (copy) {
if (arg.slice) {
arg = arg.slice();
} else {
arg = Array.prototype.slice.call(arg);
}
}
return arg;
}
// It's an array; check it is a valid representation of a byte
if (Array.isArray(arg)) {
if (!checkInts(arg)) {
throw new Error('Array contains invalid value: ' + arg);
}
return new Uint8Array(arg);
}
// Something else, but behaves like an array (maybe a Buffer? Arguments?)
if (checkInt(arg.length) && checkInts(arg)) {
return new Uint8Array(arg);
}
throw new Error('unsupported array-like object');
}
function createArray(length) {
return new Uint8Array(length);
}
function copyArray(sourceArray, targetArray, targetStart, sourceStart, sourceEnd) {
if (sourceStart != null || sourceEnd != null) {
if (sourceArray.slice) {
sourceArray = sourceArray.slice(sourceStart, sourceEnd);
} else {
sourceArray = Array.prototype.slice.call(sourceArray, sourceStart, sourceEnd);
}
}
targetArray.set(sourceArray, targetStart);
}
var convertUtf8 = (function() {
function toBytes(text) {
var result = [], i = 0;
text = encodeURI(text);
while (i < text.length) {
var c = text.charCodeAt(i++);
// if it is a % sign, encode the following 2 bytes as a hex value
if (c === 37) {
result.push(parseInt(text.substr(i, 2), 16))
i += 2;
// otherwise, just the actual byte
} else {
result.push(c)
}
}
return coerceArray(result);
}
function fromBytes(bytes) {
var result = [], i = 0;
while (i < bytes.length) {
var c = bytes[i];
if (c < 128) {
result.push(String.fromCharCode(c));
i++;
} else if (c > 191 && c < 224) {
result.push(String.fromCharCode(((c & 0x1f) << 6) | (bytes[i + 1] & 0x3f)));
i += 2;
} else {
result.push(String.fromCharCode(((c & 0x0f) << 12) | ((bytes[i + 1] & 0x3f) << 6) | (bytes[i + 2] & 0x3f)));
i += 3;
}
}
return result.join('');
}
return {
toBytes: toBytes,
fromBytes: fromBytes,
}
})();
var convertHex = (function() {
function toBytes(text) {
var result = [];
for (var i = 0; i < text.length; i += 2) {
result.push(parseInt(text.substr(i, 2), 16));
}
return result;
}
// http://ixti.net/development/javascript/2011/11/11/base64-encodedecode-of-utf8-in-browser-with-js.html
var Hex = '0123456789abcdef';
function fromBytes(bytes) {
var result = [];
for (var i = 0; i < bytes.length; i++) {
var v = bytes[i];
result.push(Hex[(v & 0xf0) >> 4] + Hex[v & 0x0f]);
}
return result.join('');
}
return {
toBytes: toBytes,
fromBytes: fromBytes,
}
})();
// Number of rounds by keysize
var numberOfRounds = {16: 10, 24: 12, 32: 14}
// Round constant words
var rcon = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91];
// S-box and Inverse S-box (S is for Substitution)
var S = [0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16];
var Si =[0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d];
// Transformations for encryption
var T1 = [0xc66363a5, 0xf87c7c84, 0xee777799, 0xf67b7b8d, 0xfff2f20d, 0xd66b6bbd, 0xde6f6fb1, 0x91c5c554, 0x60303050, 0x02010103, 0xce6767a9, 0x562b2b7d, 0xe7fefe19, 0xb5d7d762, 0x4dababe6, 0xec76769a, 0x8fcaca45, 0x1f82829d, 0x89c9c940, 0xfa7d7d87, 0xeffafa15, 0xb25959eb, 0x8e4747c9, 0xfbf0f00b, 0x41adadec, 0xb3d4d467, 0x5fa2a2fd, 0x45afafea, 0x239c9cbf, 0x53a4a4f7, 0xe4727296, 0x9bc0c05b, 0x75b7b7c2, 0xe1fdfd1c, 0x3d9393ae, 0x4c26266a, 0x6c36365a, 0x7e3f3f41, 0xf5f7f702, 0x83cccc4f, 0x6834345c, 0x51a5a5f4, 0xd1e5e534, 0xf9f1f108, 0xe2717193, 0xabd8d873, 0x62313153, 0x2a15153f, 0x0804040c, 0x95c7c752, 0x46232365, 0x9dc3c35e, 0x30181828, 0x379696a1, 0x0a05050f, 0x2f9a9ab5, 0x0e070709, 0x24121236, 0x1b80809b, 0xdfe2e23d, 0xcdebeb26, 0x4e272769, 0x7fb2b2cd, 0xea75759f, 0x1209091b, 0x1d83839e, 0x582c2c74, 0x341a1a2e, 0x361b1b2d, 0xdc6e6eb2, 0xb45a5aee, 0x5ba0a0fb, 0xa45252f6, 0x763b3b4d, 0xb7d6d661, 0x7db3b3ce, 0x5229297b, 0xdde3e33e, 0x5e2f2f71, 0x13848497, 0xa65353f5, 0xb9d1d168, 0x00000000, 0xc1eded2c, 0x40202060, 0xe3fcfc1f, 0x79b1b1c8, 0xb65b5bed, 0xd46a6abe, 0x8dcbcb46, 0x67bebed9, 0x7239394b, 0x944a4ade, 0x984c4cd4, 0xb05858e8, 0x85cfcf4a, 0xbbd0d06b, 0xc5efef2a, 0x4faaaae5, 0xedfbfb16, 0x864343c5, 0x9a4d4dd7, 0x66333355, 0x11858594, 0x8a4545cf, 0xe9f9f910, 0x04020206, 0xfe7f7f81, 0xa05050f0, 0x783c3c44, 0x259f9fba, 0x4ba8a8e3, 0xa25151f3, 0x5da3a3fe, 0x804040c0, 0x058f8f8a, 0x3f9292ad, 0x219d9dbc, 0x70383848, 0xf1f5f504, 0x63bcbcdf, 0x77b6b6c1, 0xafdada75, 0x42212163, 0x20101030, 0xe5ffff1a, 0xfdf3f30e, 0xbfd2d26d, 0x81cdcd4c, 0x180c0c14, 0x26131335, 0xc3ecec2f, 0xbe5f5fe1, 0x359797a2, 0x884444cc, 0x2e171739, 0x93c4c457, 0x55a7a7f2, 0xfc7e7e82, 0x7a3d3d47, 0xc86464ac, 0xba5d5de7, 0x3219192b, 0xe6737395, 0xc06060a0, 0x19818198, 0x9e4f4fd1, 0xa3dcdc7f, 0x44222266, 0x542a2a7e, 0x3b9090ab, 0x0b888883, 0x8c4646ca, 0xc7eeee29, 0x6bb8b8d3, 0x2814143c, 0xa7dede79, 0xbc5e5ee2, 0x160b0b1d, 0xaddbdb76, 0xdbe0e03b, 0x64323256, 0x743a3a4e, 0x140a0a1e, 0x924949db, 0x0c06060a, 0x4824246c, 0xb85c5ce4, 0x9fc2c25d, 0xbdd3d36e, 0x43acacef, 0xc46262a6, 0x399191a8, 0x319595a4, 0xd3e4e437, 0xf279798b, 0xd5e7e732, 0x8bc8c843, 0x6e373759, 0xda6d6db7, 0x018d8d8c, 0xb1d5d564, 0x9c4e4ed2, 0x49a9a9e0, 0xd86c6cb4, 0xac5656fa, 0xf3f4f407, 0xcfeaea25, 0xca6565af, 0xf47a7a8e, 0x47aeaee9, 0x10080818, 0x6fbabad5, 0xf0787888, 0x4a25256f, 0x5c2e2e72, 0x381c1c24, 0x57a6a6f1, 0x73b4b4c7, 0x97c6c651, 0xcbe8e823, 0xa1dddd7c, 0xe874749c, 0x3e1f1f21, 0x964b4bdd, 0x61bdbddc, 0x0d8b8b86, 0x0f8a8a85, 0xe0707090, 0x7c3e3e42, 0x71b5b5c4, 0xcc6666aa, 0x904848d8, 0x06030305, 0xf7f6f601, 0x1c0e0e12, 0xc26161a3, 0x6a35355f, 0xae5757f9, 0x69b9b9d0, 0x17868691, 0x99c1c158, 0x3a1d1d27, 0x279e9eb9, 0xd9e1e138, 0xebf8f813, 0x2b9898b3, 0x22111133, 0xd26969bb, 0xa9d9d970, 0x078e8e89, 0x339494a7, 0x2d9b9bb6, 0x3c1e1e22, 0x15878792, 0xc9e9e920, 0x87cece49, 0xaa5555ff, 0x50282878, 0xa5dfdf7a, 0x038c8c8f, 0x59a1a1f8, 0x09898980, 0x1a0d0d17, 0x65bfbfda, 0xd7e6e631, 0x844242c6, 0xd06868b8, 0x824141c3, 0x299999b0, 0x5a2d2d77, 0x1e0f0f11, 0x7bb0b0cb, 0xa85454fc, 0x6dbbbbd6, 0x2c16163a];
var T2 = [0xa5c66363, 0x84f87c7c, 0x99ee7777, 0x8df67b7b, 0x0dfff2f2, 0xbdd66b6b, 0xb1de6f6f, 0x5491c5c5, 0x50603030, 0x03020101, 0xa9ce6767, 0x7d562b2b, 0x19e7fefe, 0x62b5d7d7, 0xe64dabab, 0x9aec7676, 0x458fcaca, 0x9d1f8282, 0x4089c9c9, 0x87fa7d7d, 0x15effafa, 0xebb25959, 0xc98e4747, 0x0bfbf0f0, 0xec41adad, 0x67b3d4d4, 0xfd5fa2a2, 0xea45afaf, 0xbf239c9c, 0xf753a4a4, 0x96e47272, 0x5b9bc0c0, 0xc275b7b7, 0x1ce1fdfd, 0xae3d9393, 0x6a4c2626, 0x5a6c3636, 0x417e3f3f, 0x02f5f7f7, 0x4f83cccc, 0x5c683434, 0xf451a5a5, 0x34d1e5e5, 0x08f9f1f1, 0x93e27171, 0x73abd8d8, 0x53623131, 0x3f2a1515, 0x0c080404, 0x5295c7c7, 0x65462323, 0x5e9dc3c3, 0x28301818, 0xa1379696, 0x0f0a0505, 0xb52f9a9a, 0x090e0707, 0x36241212, 0x9b1b8080, 0x3ddfe2e2, 0x26cdebeb, 0x694e2727, 0xcd7fb2b2, 0x9fea7575, 0x1b120909, 0x9e1d8383, 0x74582c2c, 0x2e341a1a, 0x2d361b1b, 0xb2dc6e6e, 0xeeb45a5a, 0xfb5ba0a0, 0xf6a45252, 0x4d763b3b, 0x61b7d6d6, 0xce7db3b3, 0x7b522929, 0x3edde3e3, 0x715e2f2f, 0x97138484, 0xf5a65353, 0x68b9d1d1, 0x00000000, 0x2cc1eded, 0x60402020, 0x1fe3fcfc, 0xc879b1b1, 0xedb65b5b, 0xbed46a6a, 0x468dcbcb, 0xd967bebe, 0x4b723939, 0xde944a4a, 0xd4984c4c, 0xe8b05858, 0x4a85cfcf, 0x6bbbd0d0, 0x2ac5efef, 0xe54faaaa, 0x16edfbfb, 0xc5864343, 0xd79a4d4d, 0x55663333, 0x94118585, 0xcf8a4545, 0x10e9f9f9, 0x06040202, 0x81fe7f7f, 0xf0a05050, 0x44783c3c, 0xba259f9f, 0xe34ba8a8, 0xf3a25151, 0xfe5da3a3, 0xc0804040, 0x8a058f8f, 0xad3f9292, 0xbc219d9d, 0x48703838, 0x04f1f5f5, 0xdf63bcbc, 0xc177b6b6, 0x75afdada, 0x63422121, 0x30201010, 0x1ae5ffff, 0x0efdf3f3, 0x6dbfd2d2, 0x4c81cdcd, 0x14180c0c, 0x35261313, 0x2fc3ecec, 0xe1be5f5f, 0xa2359797, 0xcc884444, 0x392e1717, 0x5793c4c4, 0xf255a7a7, 0x82fc7e7e, 0x477a3d3d, 0xacc86464, 0xe7ba5d5d, 0x2b321919, 0x95e67373, 0xa0c06060, 0x98198181, 0xd19e4f4f, 0x7fa3dcdc, 0x66442222, 0x7e542a2a, 0xab3b9090, 0x830b8888, 0xca8c4646, 0x29c7eeee, 0xd36bb8b8, 0x3c281414, 0x79a7dede, 0xe2bc5e5e, 0x1d160b0b, 0x76addbdb, 0x3bdbe0e0, 0x56643232, 0x4e743a3a, 0x1e140a0a, 0xdb924949, 0x0a0c0606, 0x6c482424, 0xe4b85c5c, 0x5d9fc2c2, 0x6ebdd3d3, 0xef43acac, 0xa6c46262, 0xa8399191, 0xa4319595, 0x37d3e4e4, 0x8bf27979, 0x32d5e7e7, 0x438bc8c8, 0x596e3737, 0xb7da6d6d, 0x8c018d8d, 0x64b1d5d5, 0xd29c4e4e, 0xe049a9a9, 0xb4d86c6c, 0xfaac5656, 0x07f3f4f4, 0x25cfeaea, 0xafca6565, 0x8ef47a7a, 0xe947aeae, 0x18100808, 0xd56fbaba, 0x88f07878, 0x6f4a2525, 0x725c2e2e, 0x24381c1c, 0xf157a6a6, 0xc773b4b4, 0x5197c6c6, 0x23cbe8e8, 0x7ca1dddd, 0x9ce87474, 0x213e1f1f, 0xdd964b4b, 0xdc61bdbd, 0x860d8b8b, 0x850f8a8a, 0x90e07070, 0x427c3e3e, 0xc471b5b5, 0xaacc6666, 0xd8904848, 0x05060303, 0x01f7f6f6, 0x121c0e0e, 0xa3c26161, 0x5f6a3535, 0xf9ae5757, 0xd069b9b9, 0x91178686, 0x5899c1c1, 0x273a1d1d, 0xb9279e9e, 0x38d9e1e1, 0x13ebf8f8, 0xb32b9898, 0x33221111, 0xbbd26969, 0x70a9d9d9, 0x89078e8e, 0xa7339494, 0xb62d9b9b, 0x223c1e1e, 0x92158787, 0x20c9e9e9, 0x4987cece, 0xffaa5555, 0x78502828, 0x7aa5dfdf, 0x8f038c8c, 0xf859a1a1, 0x80098989, 0x171a0d0d, 0xda65bfbf, 0x31d7e6e6, 0xc6844242, 0xb8d06868, 0xc3824141, 0xb0299999, 0x775a2d2d, 0x111e0f0f, 0xcb7bb0b0, 0xfca85454, 0xd66dbbbb, 0x3a2c1616];
var T3 = [0x63a5c663, 0x7c84f87c, 0x7799ee77, 0x7b8df67b, 0xf20dfff2, 0x6bbdd66b, 0x6fb1de6f, 0xc55491c5, 0x30506030, 0x01030201, 0x67a9ce67, 0x2b7d562b, 0xfe19e7fe, 0xd762b5d7, 0xabe64dab, 0x769aec76, 0xca458fca, 0x829d1f82, 0xc94089c9, 0x7d87fa7d, 0xfa15effa, 0x59ebb259, 0x47c98e47, 0xf00bfbf0, 0xadec41ad, 0xd467b3d4, 0xa2fd5fa2, 0xafea45af, 0x9cbf239c, 0xa4f753a4, 0x7296e472, 0xc05b9bc0, 0xb7c275b7, 0xfd1ce1fd, 0x93ae3d93, 0x266a4c26, 0x365a6c36, 0x3f417e3f, 0xf702f5f7, 0xcc4f83cc, 0x345c6834, 0xa5f451a5, 0xe534d1e5, 0xf108f9f1, 0x7193e271, 0xd873abd8, 0x31536231, 0x153f2a15, 0x040c0804, 0xc75295c7, 0x23654623, 0xc35e9dc3, 0x18283018, 0x96a13796, 0x050f0a05, 0x9ab52f9a, 0x07090e07, 0x12362412, 0x809b1b80, 0xe23ddfe2, 0xeb26cdeb, 0x27694e27, 0xb2cd7fb2, 0x759fea75, 0x091b1209, 0x839e1d83, 0x2c74582c, 0x1a2e341a, 0x1b2d361b, 0x6eb2dc6e, 0x5aeeb45a, 0xa0fb5ba0, 0x52f6a452, 0x3b4d763b, 0xd661b7d6, 0xb3ce7db3, 0x297b5229, 0xe33edde3, 0x2f715e2f, 0x84971384, 0x53f5a653, 0xd168b9d1, 0x00000000, 0xed2cc1ed, 0x20604020, 0xfc1fe3fc, 0xb1c879b1, 0x5bedb65b, 0x6abed46a, 0xcb468dcb, 0xbed967be, 0x394b7239, 0x4ade944a, 0x4cd4984c, 0x58e8b058, 0xcf4a85cf, 0xd06bbbd0, 0xef2ac5ef, 0xaae54faa, 0xfb16edfb, 0x43c58643, 0x4dd79a4d, 0x33556633, 0x85941185, 0x45cf8a45, 0xf910e9f9, 0x02060402, 0x7f81fe7f, 0x50f0a050, 0x3c44783c, 0x9fba259f, 0xa8e34ba8, 0x51f3a251, 0xa3fe5da3, 0x40c08040, 0x8f8a058f, 0x92ad3f92, 0x9dbc219d, 0x38487038, 0xf504f1f5, 0xbcdf63bc, 0xb6c177b6, 0xda75afda, 0x21634221, 0x10302010, 0xff1ae5ff, 0xf30efdf3, 0xd26dbfd2, 0xcd4c81cd, 0x0c14180c, 0x13352613, 0xec2fc3ec, 0x5fe1be5f, 0x97a23597, 0x44cc8844, 0x17392e17, 0xc45793c4, 0xa7f255a7, 0x7e82fc7e, 0x3d477a3d, 0x64acc864, 0x5de7ba5d, 0x192b3219, 0x7395e673, 0x60a0c060, 0x81981981, 0x4fd19e4f, 0xdc7fa3dc, 0x22664422, 0x2a7e542a, 0x90ab3b90, 0x88830b88, 0x46ca8c46, 0xee29c7ee, 0xb8d36bb8, 0x143c2814, 0xde79a7de, 0x5ee2bc5e, 0x0b1d160b, 0xdb76addb, 0xe03bdbe0, 0x32566432, 0x3a4e743a, 0x0a1e140a, 0x49db9249, 0x060a0c06, 0x246c4824, 0x5ce4b85c, 0xc25d9fc2, 0xd36ebdd3, 0xacef43ac, 0x62a6c462, 0x91a83991, 0x95a43195, 0xe437d3e4, 0x798bf279, 0xe732d5e7, 0xc8438bc8, 0x37596e37, 0x6db7da6d, 0x8d8c018d, 0xd564b1d5, 0x4ed29c4e, 0xa9e049a9, 0x6cb4d86c, 0x56faac56, 0xf407f3f4, 0xea25cfea, 0x65afca65, 0x7a8ef47a, 0xaee947ae, 0x08181008, 0xbad56fba, 0x7888f078, 0x256f4a25, 0x2e725c2e, 0x1c24381c, 0xa6f157a6, 0xb4c773b4, 0xc65197c6, 0xe823cbe8, 0xdd7ca1dd, 0x749ce874, 0x1f213e1f, 0x4bdd964b, 0xbddc61bd, 0x8b860d8b, 0x8a850f8a, 0x7090e070, 0x3e427c3e, 0xb5c471b5, 0x66aacc66, 0x48d89048, 0x03050603, 0xf601f7f6, 0x0e121c0e, 0x61a3c261, 0x355f6a35, 0x57f9ae57, 0xb9d069b9, 0x86911786, 0xc15899c1, 0x1d273a1d, 0x9eb9279e, 0xe138d9e1, 0xf813ebf8, 0x98b32b98, 0x11332211, 0x69bbd269, 0xd970a9d9, 0x8e89078e, 0x94a73394, 0x9bb62d9b, 0x1e223c1e, 0x87921587, 0xe920c9e9, 0xce4987ce, 0x55ffaa55, 0x28785028, 0xdf7aa5df, 0x8c8f038c, 0xa1f859a1, 0x89800989, 0x0d171a0d, 0xbfda65bf, 0xe631d7e6, 0x42c68442, 0x68b8d068, 0x41c38241, 0x99b02999, 0x2d775a2d, 0x0f111e0f, 0xb0cb7bb0, 0x54fca854, 0xbbd66dbb, 0x163a2c16];
var T4 = [0x6363a5c6, 0x7c7c84f8, 0x777799ee, 0x7b7b8df6, 0xf2f20dff, 0x6b6bbdd6, 0x6f6fb1de, 0xc5c55491, 0x30305060, 0x01010302, 0x6767a9ce, 0x2b2b7d56, 0xfefe19e7, 0xd7d762b5, 0xababe64d, 0x76769aec, 0xcaca458f, 0x82829d1f, 0xc9c94089, 0x7d7d87fa, 0xfafa15ef, 0x5959ebb2, 0x4747c98e, 0xf0f00bfb, 0xadadec41, 0xd4d467b3, 0xa2a2fd5f, 0xafafea45, 0x9c9cbf23, 0xa4a4f753, 0x727296e4, 0xc0c05b9b, 0xb7b7c275, 0xfdfd1ce1, 0x9393ae3d, 0x26266a4c, 0x36365a6c, 0x3f3f417e, 0xf7f702f5, 0xcccc4f83, 0x34345c68, 0xa5a5f451, 0xe5e534d1, 0xf1f108f9, 0x717193e2, 0xd8d873ab, 0x31315362, 0x15153f2a, 0x04040c08, 0xc7c75295, 0x23236546, 0xc3c35e9d, 0x18182830, 0x9696a137, 0x05050f0a, 0x9a9ab52f, 0x0707090e, 0x12123624, 0x80809b1b, 0xe2e23ddf, 0xebeb26cd, 0x2727694e, 0xb2b2cd7f, 0x75759fea, 0x09091b12, 0x83839e1d, 0x2c2c7458, 0x1a1a2e34, 0x1b1b2d36, 0x6e6eb2dc, 0x5a5aeeb4, 0xa0a0fb5b, 0x5252f6a4, 0x3b3b4d76, 0xd6d661b7, 0xb3b3ce7d, 0x29297b52, 0xe3e33edd, 0x2f2f715e, 0x84849713, 0x5353f5a6, 0xd1d168b9, 0x00000000, 0xeded2cc1, 0x20206040, 0xfcfc1fe3, 0xb1b1c879, 0x5b5bedb6, 0x6a6abed4, 0xcbcb468d, 0xbebed967, 0x39394b72, 0x4a4ade94, 0x4c4cd498, 0x5858e8b0, 0xcfcf4a85, 0xd0d06bbb, 0xefef2ac5, 0xaaaae54f, 0xfbfb16ed, 0x4343c586, 0x4d4dd79a, 0x33335566, 0x85859411, 0x4545cf8a, 0xf9f910e9, 0x02020604, 0x7f7f81fe, 0x5050f0a0, 0x3c3c4478, 0x9f9fba25, 0xa8a8e34b, 0x5151f3a2, 0xa3a3fe5d, 0x4040c080, 0x8f8f8a05, 0x9292ad3f, 0x9d9dbc21, 0x38384870, 0xf5f504f1, 0xbcbcdf63, 0xb6b6c177, 0xdada75af, 0x21216342, 0x10103020, 0xffff1ae5, 0xf3f30efd, 0xd2d26dbf, 0xcdcd4c81, 0x0c0c1418, 0x13133526, 0xecec2fc3, 0x5f5fe1be, 0x9797a235, 0x4444cc88, 0x1717392e, 0xc4c45793, 0xa7a7f255, 0x7e7e82fc, 0x3d3d477a, 0x6464acc8, 0x5d5de7ba, 0x19192b32, 0x737395e6, 0x6060a0c0, 0x81819819, 0x4f4fd19e, 0xdcdc7fa3, 0x22226644, 0x2a2a7e54, 0x9090ab3b, 0x8888830b, 0x4646ca8c, 0xeeee29c7, 0xb8b8d36b, 0x14143c28, 0xdede79a7, 0x5e5ee2bc, 0x0b0b1d16, 0xdbdb76ad, 0xe0e03bdb, 0x32325664, 0x3a3a4e74, 0x0a0a1e14, 0x4949db92, 0x06060a0c, 0x24246c48, 0x5c5ce4b8, 0xc2c25d9f, 0xd3d36ebd, 0xacacef43, 0x6262a6c4, 0x9191a839, 0x9595a431, 0xe4e437d3, 0x79798bf2, 0xe7e732d5, 0xc8c8438b, 0x3737596e, 0x6d6db7da, 0x8d8d8c01, 0xd5d564b1, 0x4e4ed29c, 0xa9a9e049, 0x6c6cb4d8, 0x5656faac, 0xf4f407f3, 0xeaea25cf, 0x6565afca, 0x7a7a8ef4, 0xaeaee947, 0x08081810, 0xbabad56f, 0x787888f0, 0x25256f4a, 0x2e2e725c, 0x1c1c2438, 0xa6a6f157, 0xb4b4c773, 0xc6c65197, 0xe8e823cb, 0xdddd7ca1, 0x74749ce8, 0x1f1f213e, 0x4b4bdd96, 0xbdbddc61, 0x8b8b860d, 0x8a8a850f, 0x707090e0, 0x3e3e427c, 0xb5b5c471, 0x6666aacc, 0x4848d890, 0x03030506, 0xf6f601f7, 0x0e0e121c, 0x6161a3c2, 0x35355f6a, 0x5757f9ae, 0xb9b9d069, 0x86869117, 0xc1c15899, 0x1d1d273a, 0x9e9eb927, 0xe1e138d9, 0xf8f813eb, 0x9898b32b, 0x11113322, 0x6969bbd2, 0xd9d970a9, 0x8e8e8907, 0x9494a733, 0x9b9bb62d, 0x1e1e223c, 0x87879215, 0xe9e920c9, 0xcece4987, 0x5555ffaa, 0x28287850, 0xdfdf7aa5, 0x8c8c8f03, 0xa1a1f859, 0x89898009, 0x0d0d171a, 0xbfbfda65, 0xe6e631d7, 0x4242c684, 0x6868b8d0, 0x4141c382, 0x9999b029, 0x2d2d775a, 0x0f0f111e, 0xb0b0cb7b, 0x5454fca8, 0xbbbbd66d, 0x16163a2c];
// Transformations for decryption
var T5 = [0x51f4a750, 0x7e416553, 0x1a17a4c3, 0x3a275e96, 0x3bab6bcb, 0x1f9d45f1, 0xacfa58ab, 0x4be30393, 0x2030fa55, 0xad766df6, 0x88cc7691, 0xf5024c25, 0x4fe5d7fc, 0xc52acbd7, 0x26354480, 0xb562a38f, 0xdeb15a49, 0x25ba1b67, 0x45ea0e98, 0x5dfec0e1, 0xc32f7502, 0x814cf012, 0x8d4697a3, 0x6bd3f9c6, 0x038f5fe7, 0x15929c95, 0xbf6d7aeb, 0x955259da, 0xd4be832d, 0x587421d3, 0x49e06929, 0x8ec9c844, 0x75c2896a, 0xf48e7978, 0x99583e6b, 0x27b971dd, 0xbee14fb6, 0xf088ad17, 0xc920ac66, 0x7dce3ab4, 0x63df4a18, 0xe51a3182, 0x97513360, 0x62537f45, 0xb16477e0, 0xbb6bae84, 0xfe81a01c, 0xf9082b94, 0x70486858, 0x8f45fd19, 0x94de6c87, 0x527bf8b7, 0xab73d323, 0x724b02e2, 0xe31f8f57, 0x6655ab2a, 0xb2eb2807, 0x2fb5c203, 0x86c57b9a, 0xd33708a5, 0x302887f2, 0x23bfa5b2, 0x02036aba, 0xed16825c, 0x8acf1c2b, 0xa779b492, 0xf307f2f0, 0x4e69e2a1, 0x65daf4cd, 0x0605bed5, 0xd134621f, 0xc4a6fe8a, 0x342e539d, 0xa2f355a0, 0x058ae132, 0xa4f6eb75, 0x0b83ec39, 0x4060efaa, 0x5e719f06, 0xbd6e1051, 0x3e218af9, 0x96dd063d, 0xdd3e05ae, 0x4de6bd46, 0x91548db5, 0x71c45d05, 0x0406d46f, 0x605015ff, 0x1998fb24, 0xd6bde997, 0x894043cc, 0x67d99e77, 0xb0e842bd, 0x07898b88, 0xe7195b38, 0x79c8eedb, 0xa17c0a47, 0x7c420fe9, 0xf8841ec9, 0x00000000, 0x09808683, 0x322bed48, 0x1e1170ac, 0x6c5a724e, 0xfd0efffb, 0x0f853856, 0x3daed51e, 0x362d3927, 0x0a0fd964, 0x685ca621, 0x9b5b54d1, 0x24362e3a, 0x0c0a67b1, 0x9357e70f, 0xb4ee96d2, 0x1b9b919e, 0x80c0c54f, 0x61dc20a2, 0x5a774b69, 0x1c121a16, 0xe293ba0a, 0xc0a02ae5, 0x3c22e043, 0x121b171d, 0x0e090d0b, 0xf28bc7ad, 0x2db6a8b9, 0x141ea9c8, 0x57f11985, 0xaf75074c, 0xee99ddbb, 0xa37f60fd, 0xf701269f, 0x5c72f5bc, 0x44663bc5, 0x5bfb7e34, 0x8b432976, 0xcb23c6dc, 0xb6edfc68, 0xb8e4f163, 0xd731dcca, 0x42638510, 0x13972240, 0x84c61120, 0x854a247d, 0xd2bb3df8, 0xaef93211, 0xc729a16d, 0x1d9e2f4b, 0xdcb230f3, 0x0d8652ec, 0x77c1e3d0, 0x2bb3166c, 0xa970b999, 0x119448fa, 0x47e96422, 0xa8fc8cc4, 0xa0f03f1a, 0x567d2cd8, 0x223390ef, 0x87494ec7, 0xd938d1c1, 0x8ccaa2fe, 0x98d40b36, 0xa6f581cf, 0xa57ade28, 0xdab78e26, 0x3fadbfa4, 0x2c3a9de4, 0x5078920d, 0x6a5fcc9b, 0x547e4662, 0xf68d13c2, 0x90d8b8e8, 0x2e39f75e, 0x82c3aff5, 0x9f5d80be, 0x69d0937c, 0x6fd52da9, 0xcf2512b3, 0xc8ac993b, 0x10187da7, 0xe89c636e, 0xdb3bbb7b, 0xcd267809, 0x6e5918f4, 0xec9ab701, 0x834f9aa8, 0xe6956e65, 0xaaffe67e, 0x21bccf08, 0xef15e8e6, 0xbae79bd9, 0x4a6f36ce, 0xea9f09d4, 0x29b07cd6, 0x31a4b2af, 0x2a3f2331, 0xc6a59430, 0x35a266c0, 0x744ebc37, 0xfc82caa6, 0xe090d0b0, 0x33a7d815, 0xf104984a, 0x41ecdaf7, 0x7fcd500e, 0x1791f62f, 0x764dd68d, 0x43efb04d, 0xccaa4d54, 0xe49604df, 0x9ed1b5e3, 0x4c6a881b, 0xc12c1fb8, 0x4665517f, 0x9d5eea04, 0x018c355d, 0xfa877473, 0xfb0b412e, 0xb3671d5a, 0x92dbd252, 0xe9105633, 0x6dd64713, 0x9ad7618c, 0x37a10c7a, 0x59f8148e, 0xeb133c89, 0xcea927ee, 0xb761c935, 0xe11ce5ed, 0x7a47b13c, 0x9cd2df59, 0x55f2733f, 0x1814ce79, 0x73c737bf, 0x53f7cdea, 0x5ffdaa5b, 0xdf3d6f14, 0x7844db86, 0xcaaff381, 0xb968c43e, 0x3824342c, 0xc2a3405f, 0x161dc372, 0xbce2250c, 0x283c498b, 0xff0d9541, 0x39a80171, 0x080cb3de, 0xd8b4e49c, 0x6456c190, 0x7bcb8461, 0xd532b670, 0x486c5c74, 0xd0b85742];
var T6 = [0x5051f4a7, 0x537e4165, 0xc31a17a4, 0x963a275e, 0xcb3bab6b, 0xf11f9d45, 0xabacfa58, 0x934be303, 0x552030fa, 0xf6ad766d, 0x9188cc76, 0x25f5024c, 0xfc4fe5d7, 0xd7c52acb, 0x80263544, 0x8fb562a3, 0x49deb15a, 0x6725ba1b, 0x9845ea0e, 0xe15dfec0, 0x02c32f75, 0x12814cf0, 0xa38d4697, 0xc66bd3f9, 0xe7038f5f, 0x9515929c, 0xebbf6d7a, 0xda955259, 0x2dd4be83, 0xd3587421, 0x2949e069, 0x448ec9c8, 0x6a75c289, 0x78f48e79, 0x6b99583e, 0xdd27b971, 0xb6bee14f, 0x17f088ad, 0x66c920ac, 0xb47dce3a, 0x1863df4a, 0x82e51a31, 0x60975133, 0x4562537f, 0xe0b16477, 0x84bb6bae, 0x1cfe81a0, 0x94f9082b, 0x58704868, 0x198f45fd, 0x8794de6c, 0xb7527bf8, 0x23ab73d3, 0xe2724b02, 0x57e31f8f, 0x2a6655ab, 0x07b2eb28, 0x032fb5c2, 0x9a86c57b, 0xa5d33708, 0xf2302887, 0xb223bfa5, 0xba02036a, 0x5ced1682, 0x2b8acf1c, 0x92a779b4, 0xf0f307f2, 0xa14e69e2, 0xcd65daf4, 0xd50605be, 0x1fd13462, 0x8ac4a6fe, 0x9d342e53, 0xa0a2f355, 0x32058ae1, 0x75a4f6eb, 0x390b83ec, 0xaa4060ef, 0x065e719f, 0x51bd6e10, 0xf93e218a, 0x3d96dd06, 0xaedd3e05, 0x464de6bd, 0xb591548d, 0x0571c45d, 0x6f0406d4, 0xff605015, 0x241998fb, 0x97d6bde9, 0xcc894043, 0x7767d99e, 0xbdb0e842, 0x8807898b, 0x38e7195b, 0xdb79c8ee, 0x47a17c0a, 0xe97c420f, 0xc9f8841e, 0x00000000, 0x83098086, 0x48322bed, 0xac1e1170, 0x4e6c5a72, 0xfbfd0eff, 0x560f8538, 0x1e3daed5, 0x27362d39, 0x640a0fd9, 0x21685ca6, 0xd19b5b54, 0x3a24362e, 0xb10c0a67, 0x0f9357e7, 0xd2b4ee96, 0x9e1b9b91, 0x4f80c0c5, 0xa261dc20, 0x695a774b, 0x161c121a, 0x0ae293ba, 0xe5c0a02a, 0x433c22e0, 0x1d121b17, 0x0b0e090d, 0xadf28bc7, 0xb92db6a8, 0xc8141ea9, 0x8557f119, 0x4caf7507, 0xbbee99dd, 0xfda37f60, 0x9ff70126, 0xbc5c72f5, 0xc544663b, 0x345bfb7e, 0x768b4329, 0xdccb23c6, 0x68b6edfc, 0x63b8e4f1, 0xcad731dc, 0x10426385, 0x40139722, 0x2084c611, 0x7d854a24, 0xf8d2bb3d, 0x11aef932, 0x6dc729a1, 0x4b1d9e2f, 0xf3dcb230, 0xec0d8652, 0xd077c1e3, 0x6c2bb316, 0x99a970b9, 0xfa119448, 0x2247e964, 0xc4a8fc8c, 0x1aa0f03f, 0xd8567d2c, 0xef223390, 0xc787494e, 0xc1d938d1, 0xfe8ccaa2, 0x3698d40b, 0xcfa6f581, 0x28a57ade, 0x26dab78e, 0xa43fadbf, 0xe42c3a9d, 0x0d507892, 0x9b6a5fcc, 0x62547e46, 0xc2f68d13, 0xe890d8b8, 0x5e2e39f7, 0xf582c3af, 0xbe9f5d80, 0x7c69d093, 0xa96fd52d, 0xb3cf2512, 0x3bc8ac99, 0xa710187d, 0x6ee89c63, 0x7bdb3bbb, 0x09cd2678, 0xf46e5918, 0x01ec9ab7, 0xa8834f9a, 0x65e6956e, 0x7eaaffe6, 0x0821bccf, 0xe6ef15e8, 0xd9bae79b, 0xce4a6f36, 0xd4ea9f09, 0xd629b07c, 0xaf31a4b2, 0x312a3f23, 0x30c6a594, 0xc035a266, 0x37744ebc, 0xa6fc82ca, 0xb0e090d0, 0x1533a7d8, 0x4af10498, 0xf741ecda, 0x0e7fcd50, 0x2f1791f6, 0x8d764dd6, 0x4d43efb0, 0x54ccaa4d, 0xdfe49604, 0xe39ed1b5, 0x1b4c6a88, 0xb8c12c1f, 0x7f466551, 0x049d5eea, 0x5d018c35, 0x73fa8774, 0x2efb0b41, 0x5ab3671d, 0x5292dbd2, 0x33e91056, 0x136dd647, 0x8c9ad761, 0x7a37a10c, 0x8e59f814, 0x89eb133c, 0xeecea927, 0x35b761c9, 0xede11ce5, 0x3c7a47b1, 0x599cd2df, 0x3f55f273, 0x791814ce, 0xbf73c737, 0xea53f7cd, 0x5b5ffdaa, 0x14df3d6f, 0x867844db, 0x81caaff3, 0x3eb968c4, 0x2c382434, 0x5fc2a340, 0x72161dc3, 0x0cbce225, 0x8b283c49, 0x41ff0d95, 0x7139a801, 0xde080cb3, 0x9cd8b4e4, 0x906456c1, 0x617bcb84, 0x70d532b6, 0x74486c5c, 0x42d0b857];
var T7 = [0xa75051f4, 0x65537e41, 0xa4c31a17, 0x5e963a27, 0x6bcb3bab, 0x45f11f9d, 0x58abacfa, 0x03934be3, 0xfa552030, 0x6df6ad76, 0x769188cc, 0x4c25f502, 0xd7fc4fe5, 0xcbd7c52a, 0x44802635, 0xa38fb562, 0x5a49deb1, 0x1b6725ba, 0x0e9845ea, 0xc0e15dfe, 0x7502c32f, 0xf012814c, 0x97a38d46, 0xf9c66bd3, 0x5fe7038f, 0x9c951592, 0x7aebbf6d, 0x59da9552, 0x832dd4be, 0x21d35874, 0x692949e0, 0xc8448ec9, 0x896a75c2, 0x7978f48e, 0x3e6b9958, 0x71dd27b9, 0x4fb6bee1, 0xad17f088, 0xac66c920, 0x3ab47dce, 0x4a1863df, 0x3182e51a, 0x33609751, 0x7f456253, 0x77e0b164, 0xae84bb6b, 0xa01cfe81, 0x2b94f908, 0x68587048, 0xfd198f45, 0x6c8794de, 0xf8b7527b, 0xd323ab73, 0x02e2724b, 0x8f57e31f, 0xab2a6655, 0x2807b2eb, 0xc2032fb5, 0x7b9a86c5, 0x08a5d337, 0x87f23028, 0xa5b223bf, 0x6aba0203, 0x825ced16, 0x1c2b8acf, 0xb492a779, 0xf2f0f307, 0xe2a14e69, 0xf4cd65da, 0xbed50605, 0x621fd134, 0xfe8ac4a6, 0x539d342e, 0x55a0a2f3, 0xe132058a, 0xeb75a4f6, 0xec390b83, 0xefaa4060, 0x9f065e71, 0x1051bd6e, 0x8af93e21, 0x063d96dd, 0x05aedd3e, 0xbd464de6, 0x8db59154, 0x5d0571c4, 0xd46f0406, 0x15ff6050, 0xfb241998, 0xe997d6bd, 0x43cc8940, 0x9e7767d9, 0x42bdb0e8, 0x8b880789, 0x5b38e719, 0xeedb79c8, 0x0a47a17c, 0x0fe97c42, 0x1ec9f884, 0x00000000, 0x86830980, 0xed48322b, 0x70ac1e11, 0x724e6c5a, 0xfffbfd0e, 0x38560f85, 0xd51e3dae, 0x3927362d, 0xd9640a0f, 0xa621685c, 0x54d19b5b, 0x2e3a2436, 0x67b10c0a, 0xe70f9357, 0x96d2b4ee, 0x919e1b9b, 0xc54f80c0, 0x20a261dc, 0x4b695a77, 0x1a161c12, 0xba0ae293, 0x2ae5c0a0, 0xe0433c22, 0x171d121b, 0x0d0b0e09, 0xc7adf28b, 0xa8b92db6, 0xa9c8141e, 0x198557f1, 0x074caf75, 0xddbbee99, 0x60fda37f, 0x269ff701, 0xf5bc5c72, 0x3bc54466, 0x7e345bfb, 0x29768b43, 0xc6dccb23, 0xfc68b6ed, 0xf163b8e4, 0xdccad731, 0x85104263, 0x22401397, 0x112084c6, 0x247d854a, 0x3df8d2bb, 0x3211aef9, 0xa16dc729, 0x2f4b1d9e, 0x30f3dcb2, 0x52ec0d86, 0xe3d077c1, 0x166c2bb3, 0xb999a970, 0x48fa1194, 0x642247e9, 0x8cc4a8fc, 0x3f1aa0f0, 0x2cd8567d, 0x90ef2233, 0x4ec78749, 0xd1c1d938, 0xa2fe8cca, 0x0b3698d4, 0x81cfa6f5, 0xde28a57a, 0x8e26dab7, 0xbfa43fad, 0x9de42c3a, 0x920d5078, 0xcc9b6a5f, 0x4662547e, 0x13c2f68d, 0xb8e890d8, 0xf75e2e39, 0xaff582c3, 0x80be9f5d, 0x937c69d0, 0x2da96fd5, 0x12b3cf25, 0x993bc8ac, 0x7da71018, 0x636ee89c, 0xbb7bdb3b, 0x7809cd26, 0x18f46e59, 0xb701ec9a, 0x9aa8834f, 0x6e65e695, 0xe67eaaff, 0xcf0821bc, 0xe8e6ef15, 0x9bd9bae7, 0x36ce4a6f, 0x09d4ea9f, 0x7cd629b0, 0xb2af31a4, 0x23312a3f, 0x9430c6a5, 0x66c035a2, 0xbc37744e, 0xcaa6fc82, 0xd0b0e090, 0xd81533a7, 0x984af104, 0xdaf741ec, 0x500e7fcd, 0xf62f1791, 0xd68d764d, 0xb04d43ef, 0x4d54ccaa, 0x04dfe496, 0xb5e39ed1, 0x881b4c6a, 0x1fb8c12c, 0x517f4665, 0xea049d5e, 0x355d018c, 0x7473fa87, 0x412efb0b, 0x1d5ab367, 0xd25292db, 0x5633e910, 0x47136dd6, 0x618c9ad7, 0x0c7a37a1, 0x148e59f8, 0x3c89eb13, 0x27eecea9, 0xc935b761, 0xe5ede11c, 0xb13c7a47, 0xdf599cd2, 0x733f55f2, 0xce791814, 0x37bf73c7, 0xcdea53f7, 0xaa5b5ffd, 0x6f14df3d, 0xdb867844, 0xf381caaf, 0xc43eb968, 0x342c3824, 0x405fc2a3, 0xc372161d, 0x250cbce2, 0x498b283c, 0x9541ff0d, 0x017139a8, 0xb3de080c, 0xe49cd8b4, 0xc1906456, 0x84617bcb, 0xb670d532, 0x5c74486c, 0x5742d0b8];
var T8 = [0xf4a75051, 0x4165537e, 0x17a4c31a, 0x275e963a, 0xab6bcb3b, 0x9d45f11f, 0xfa58abac, 0xe303934b, 0x30fa5520, 0x766df6ad, 0xcc769188, 0x024c25f5, 0xe5d7fc4f, 0x2acbd7c5, 0x35448026, 0x62a38fb5, 0xb15a49de, 0xba1b6725, 0xea0e9845, 0xfec0e15d, 0x2f7502c3, 0x4cf01281, 0x4697a38d, 0xd3f9c66b, 0x8f5fe703, 0x929c9515, 0x6d7aebbf, 0x5259da95, 0xbe832dd4, 0x7421d358, 0xe0692949, 0xc9c8448e, 0xc2896a75, 0x8e7978f4, 0x583e6b99, 0xb971dd27, 0xe14fb6be, 0x88ad17f0, 0x20ac66c9, 0xce3ab47d, 0xdf4a1863, 0x1a3182e5, 0x51336097, 0x537f4562, 0x6477e0b1, 0x6bae84bb, 0x81a01cfe, 0x082b94f9, 0x48685870, 0x45fd198f, 0xde6c8794, 0x7bf8b752, 0x73d323ab, 0x4b02e272, 0x1f8f57e3, 0x55ab2a66, 0xeb2807b2, 0xb5c2032f, 0xc57b9a86, 0x3708a5d3, 0x2887f230, 0xbfa5b223, 0x036aba02, 0x16825ced, 0xcf1c2b8a, 0x79b492a7, 0x07f2f0f3, 0x69e2a14e, 0xdaf4cd65, 0x05bed506, 0x34621fd1, 0xa6fe8ac4, 0x2e539d34, 0xf355a0a2, 0x8ae13205, 0xf6eb75a4, 0x83ec390b, 0x60efaa40, 0x719f065e, 0x6e1051bd, 0x218af93e, 0xdd063d96, 0x3e05aedd, 0xe6bd464d, 0x548db591, 0xc45d0571, 0x06d46f04, 0x5015ff60, 0x98fb2419, 0xbde997d6, 0x4043cc89, 0xd99e7767, 0xe842bdb0, 0x898b8807, 0x195b38e7, 0xc8eedb79, 0x7c0a47a1, 0x420fe97c, 0x841ec9f8, 0x00000000, 0x80868309, 0x2bed4832, 0x1170ac1e, 0x5a724e6c, 0x0efffbfd, 0x8538560f, 0xaed51e3d, 0x2d392736, 0x0fd9640a, 0x5ca62168, 0x5b54d19b, 0x362e3a24, 0x0a67b10c, 0x57e70f93, 0xee96d2b4, 0x9b919e1b, 0xc0c54f80, 0xdc20a261, 0x774b695a, 0x121a161c, 0x93ba0ae2, 0xa02ae5c0, 0x22e0433c, 0x1b171d12, 0x090d0b0e, 0x8bc7adf2, 0xb6a8b92d, 0x1ea9c814, 0xf1198557, 0x75074caf, 0x99ddbbee, 0x7f60fda3, 0x01269ff7, 0x72f5bc5c, 0x663bc544, 0xfb7e345b, 0x4329768b, 0x23c6dccb, 0xedfc68b6, 0xe4f163b8, 0x31dccad7, 0x63851042, 0x97224013, 0xc6112084, 0x4a247d85, 0xbb3df8d2, 0xf93211ae, 0x29a16dc7, 0x9e2f4b1d, 0xb230f3dc, 0x8652ec0d, 0xc1e3d077, 0xb3166c2b, 0x70b999a9, 0x9448fa11, 0xe9642247, 0xfc8cc4a8, 0xf03f1aa0, 0x7d2cd856, 0x3390ef22, 0x494ec787, 0x38d1c1d9, 0xcaa2fe8c, 0xd40b3698, 0xf581cfa6, 0x7ade28a5, 0xb78e26da, 0xadbfa43f, 0x3a9de42c, 0x78920d50, 0x5fcc9b6a, 0x7e466254, 0x8d13c2f6, 0xd8b8e890, 0x39f75e2e, 0xc3aff582, 0x5d80be9f, 0xd0937c69, 0xd52da96f, 0x2512b3cf, 0xac993bc8, 0x187da710, 0x9c636ee8, 0x3bbb7bdb, 0x267809cd, 0x5918f46e, 0x9ab701ec, 0x4f9aa883, 0x956e65e6, 0xffe67eaa, 0xbccf0821, 0x15e8e6ef, 0xe79bd9ba, 0x6f36ce4a, 0x9f09d4ea, 0xb07cd629, 0xa4b2af31, 0x3f23312a, 0xa59430c6, 0xa266c035, 0x4ebc3774, 0x82caa6fc, 0x90d0b0e0, 0xa7d81533, 0x04984af1, 0xecdaf741, 0xcd500e7f, 0x91f62f17, 0x4dd68d76, 0xefb04d43, 0xaa4d54cc, 0x9604dfe4, 0xd1b5e39e, 0x6a881b4c, 0x2c1fb8c1, 0x65517f46, 0x5eea049d, 0x8c355d01, 0x877473fa, 0x0b412efb, 0x671d5ab3, 0xdbd25292, 0x105633e9, 0xd647136d, 0xd7618c9a, 0xa10c7a37, 0xf8148e59, 0x133c89eb, 0xa927eece, 0x61c935b7, 0x1ce5ede1, 0x47b13c7a, 0xd2df599c, 0xf2733f55, 0x14ce7918, 0xc737bf73, 0xf7cdea53, 0xfdaa5b5f, 0x3d6f14df, 0x44db8678, 0xaff381ca, 0x68c43eb9, 0x24342c38, 0xa3405fc2, 0x1dc37216, 0xe2250cbc, 0x3c498b28, 0x0d9541ff, 0xa8017139, 0x0cb3de08, 0xb4e49cd8, 0x56c19064, 0xcb84617b, 0x32b670d5, 0x6c5c7448, 0xb85742d0];
// Transformations for decryption key expansion
var U1 = [0x00000000, 0x0e090d0b, 0x1c121a16, 0x121b171d, 0x3824342c, 0x362d3927, 0x24362e3a, 0x2a3f2331, 0x70486858, 0x7e416553, 0x6c5a724e, 0x62537f45, 0x486c5c74, 0x4665517f, 0x547e4662, 0x5a774b69, 0xe090d0b0, 0xee99ddbb, 0xfc82caa6, 0xf28bc7ad, 0xd8b4e49c, 0xd6bde997, 0xc4a6fe8a, 0xcaaff381, 0x90d8b8e8, 0x9ed1b5e3, 0x8ccaa2fe, 0x82c3aff5, 0xa8fc8cc4, 0xa6f581cf, 0xb4ee96d2, 0xbae79bd9, 0xdb3bbb7b, 0xd532b670, 0xc729a16d, 0xc920ac66, 0xe31f8f57, 0xed16825c, 0xff0d9541, 0xf104984a, 0xab73d323, 0xa57ade28, 0xb761c935, 0xb968c43e, 0x9357e70f, 0x9d5eea04, 0x8f45fd19, 0x814cf012, 0x3bab6bcb, 0x35a266c0, 0x27b971dd, 0x29b07cd6, 0x038f5fe7, 0x0d8652ec, 0x1f9d45f1, 0x119448fa, 0x4be30393, 0x45ea0e98, 0x57f11985, 0x59f8148e, 0x73c737bf, 0x7dce3ab4, 0x6fd52da9, 0x61dc20a2, 0xad766df6, 0xa37f60fd, 0xb16477e0, 0xbf6d7aeb, 0x955259da, 0x9b5b54d1, 0x894043cc, 0x87494ec7, 0xdd3e05ae, 0xd33708a5, 0xc12c1fb8, 0xcf2512b3, 0xe51a3182, 0xeb133c89, 0xf9082b94, 0xf701269f, 0x4de6bd46, 0x43efb04d, 0x51f4a750, 0x5ffdaa5b, 0x75c2896a, 0x7bcb8461, 0x69d0937c, 0x67d99e77, 0x3daed51e, 0x33a7d815, 0x21bccf08, 0x2fb5c203, 0x058ae132, 0x0b83ec39, 0x1998fb24, 0x1791f62f, 0x764dd68d, 0x7844db86, 0x6a5fcc9b, 0x6456c190, 0x4e69e2a1, 0x4060efaa, 0x527bf8b7, 0x5c72f5bc, 0x0605bed5, 0x080cb3de, 0x1a17a4c3, 0x141ea9c8, 0x3e218af9, 0x302887f2, 0x223390ef, 0x2c3a9de4, 0x96dd063d, 0x98d40b36, 0x8acf1c2b, 0x84c61120, 0xaef93211, 0xa0f03f1a, 0xb2eb2807, 0xbce2250c, 0xe6956e65, 0xe89c636e, 0xfa877473, 0xf48e7978, 0xdeb15a49, 0xd0b85742, 0xc2a3405f, 0xccaa4d54, 0x41ecdaf7, 0x4fe5d7fc, 0x5dfec0e1, 0x53f7cdea, 0x79c8eedb, 0x77c1e3d0, 0x65daf4cd, 0x6bd3f9c6, 0x31a4b2af, 0x3fadbfa4, 0x2db6a8b9, 0x23bfa5b2, 0x09808683, 0x07898b88, 0x15929c95, 0x1b9b919e, 0xa17c0a47, 0xaf75074c, 0xbd6e1051, 0xb3671d5a, 0x99583e6b, 0x97513360, 0x854a247d, 0x8b432976, 0xd134621f, 0xdf3d6f14, 0xcd267809, 0xc32f7502, 0xe9105633, 0xe7195b38, 0xf5024c25, 0xfb0b412e, 0x9ad7618c, 0x94de6c87, 0x86c57b9a, 0x88cc7691, 0xa2f355a0, 0xacfa58ab, 0xbee14fb6, 0xb0e842bd, 0xea9f09d4, 0xe49604df, 0xf68d13c2, 0xf8841ec9, 0xd2bb3df8, 0xdcb230f3, 0xcea927ee, 0xc0a02ae5, 0x7a47b13c, 0x744ebc37, 0x6655ab2a, 0x685ca621, 0x42638510, 0x4c6a881b, 0x5e719f06, 0x5078920d, 0x0a0fd964, 0x0406d46f, 0x161dc372, 0x1814ce79, 0x322bed48, 0x3c22e043, 0x2e39f75e, 0x2030fa55, 0xec9ab701, 0xe293ba0a, 0xf088ad17, 0xfe81a01c, 0xd4be832d, 0xdab78e26, 0xc8ac993b, 0xc6a59430, 0x9cd2df59, 0x92dbd252, 0x80c0c54f, 0x8ec9c844, 0xa4f6eb75, 0xaaffe67e, 0xb8e4f163, 0xb6edfc68, 0x0c0a67b1, 0x02036aba, 0x10187da7, 0x1e1170ac, 0x342e539d, 0x3a275e96, 0x283c498b, 0x26354480, 0x7c420fe9, 0x724b02e2, 0x605015ff, 0x6e5918f4, 0x44663bc5, 0x4a6f36ce, 0x587421d3, 0x567d2cd8, 0x37a10c7a, 0x39a80171, 0x2bb3166c, 0x25ba1b67, 0x0f853856, 0x018c355d, 0x13972240, 0x1d9e2f4b, 0x47e96422, 0x49e06929, 0x5bfb7e34, 0x55f2733f, 0x7fcd500e, 0x71c45d05, 0x63df4a18, 0x6dd64713, 0xd731dcca, 0xd938d1c1, 0xcb23c6dc, 0xc52acbd7, 0xef15e8e6, 0xe11ce5ed, 0xf307f2f0, 0xfd0efffb, 0xa779b492, 0xa970b999, 0xbb6bae84, 0xb562a38f, 0x9f5d80be, 0x91548db5, 0x834f9aa8, 0x8d4697a3];
var U2 = [0x00000000, 0x0b0e090d, 0x161c121a, 0x1d121b17, 0x2c382434, 0x27362d39, 0x3a24362e, 0x312a3f23, 0x58704868, 0x537e4165, 0x4e6c5a72, 0x4562537f, 0x74486c5c, 0x7f466551, 0x62547e46, 0x695a774b, 0xb0e090d0, 0xbbee99dd, 0xa6fc82ca, 0xadf28bc7, 0x9cd8b4e4, 0x97d6bde9, 0x8ac4a6fe, 0x81caaff3, 0xe890d8b8, 0xe39ed1b5, 0xfe8ccaa2, 0xf582c3af, 0xc4a8fc8c, 0xcfa6f581, 0xd2b4ee96, 0xd9bae79b, 0x7bdb3bbb, 0x70d532b6, 0x6dc729a1, 0x66c920ac, 0x57e31f8f, 0x5ced1682, 0x41ff0d95, 0x4af10498, 0x23ab73d3, 0x28a57ade, 0x35b761c9, 0x3eb968c4, 0x0f9357e7, 0x049d5eea, 0x198f45fd, 0x12814cf0, 0xcb3bab6b, 0xc035a266, 0xdd27b971, 0xd629b07c, 0xe7038f5f, 0xec0d8652, 0xf11f9d45, 0xfa119448, 0x934be303, 0x9845ea0e, 0x8557f119, 0x8e59f814, 0xbf73c737, 0xb47dce3a, 0xa96fd52d, 0xa261dc20, 0xf6ad766d, 0xfda37f60, 0xe0b16477, 0xebbf6d7a, 0xda955259, 0xd19b5b54, 0xcc894043, 0xc787494e, 0xaedd3e05, 0xa5d33708, 0xb8c12c1f, 0xb3cf2512, 0x82e51a31, 0x89eb133c, 0x94f9082b, 0x9ff70126, 0x464de6bd, 0x4d43efb0, 0x5051f4a7, 0x5b5ffdaa, 0x6a75c289, 0x617bcb84, 0x7c69d093, 0x7767d99e, 0x1e3daed5, 0x1533a7d8, 0x0821bccf, 0x032fb5c2, 0x32058ae1, 0x390b83ec, 0x241998fb, 0x2f1791f6, 0x8d764dd6, 0x867844db, 0x9b6a5fcc, 0x906456c1, 0xa14e69e2, 0xaa4060ef, 0xb7527bf8, 0xbc5c72f5, 0xd50605be, 0xde080cb3, 0xc31a17a4, 0xc8141ea9, 0xf93e218a, 0xf2302887, 0xef223390, 0xe42c3a9d, 0x3d96dd06, 0x3698d40b, 0x2b8acf1c, 0x2084c611, 0x11aef932, 0x1aa0f03f, 0x07b2eb28, 0x0cbce225, 0x65e6956e, 0x6ee89c63, 0x73fa8774, 0x78f48e79, 0x49deb15a, 0x42d0b857, 0x5fc2a340, 0x54ccaa4d, 0xf741ecda, 0xfc4fe5d7, 0xe15dfec0, 0xea53f7cd, 0xdb79c8ee, 0xd077c1e3, 0xcd65daf4, 0xc66bd3f9, 0xaf31a4b2, 0xa43fadbf, 0xb92db6a8, 0xb223bfa5, 0x83098086, 0x8807898b, 0x9515929c, 0x9e1b9b91, 0x47a17c0a, 0x4caf7507, 0x51bd6e10, 0x5ab3671d, 0x6b99583e, 0x60975133, 0x7d854a24, 0x768b4329, 0x1fd13462, 0x14df3d6f, 0x09cd2678, 0x02c32f75, 0x33e91056, 0x38e7195b, 0x25f5024c, 0x2efb0b41, 0x8c9ad761, 0x8794de6c, 0x9a86c57b, 0x9188cc76, 0xa0a2f355, 0xabacfa58, 0xb6bee14f, 0xbdb0e842, 0xd4ea9f09, 0xdfe49604, 0xc2f68d13, 0xc9f8841e, 0xf8d2bb3d, 0xf3dcb230, 0xeecea927, 0xe5c0a02a, 0x3c7a47b1, 0x37744ebc, 0x2a6655ab, 0x21685ca6, 0x10426385, 0x1b4c6a88, 0x065e719f, 0x0d507892, 0x640a0fd9, 0x6f0406d4, 0x72161dc3, 0x791814ce, 0x48322bed, 0x433c22e0, 0x5e2e39f7, 0x552030fa, 0x01ec9ab7, 0x0ae293ba, 0x17f088ad, 0x1cfe81a0, 0x2dd4be83, 0x26dab78e, 0x3bc8ac99, 0x30c6a594, 0x599cd2df, 0x5292dbd2, 0x4f80c0c5, 0x448ec9c8, 0x75a4f6eb, 0x7eaaffe6, 0x63b8e4f1, 0x68b6edfc, 0xb10c0a67, 0xba02036a, 0xa710187d, 0xac1e1170, 0x9d342e53, 0x963a275e, 0x8b283c49, 0x80263544, 0xe97c420f, 0xe2724b02, 0xff605015, 0xf46e5918, 0xc544663b, 0xce4a6f36, 0xd3587421, 0xd8567d2c, 0x7a37a10c, 0x7139a801, 0x6c2bb316, 0x6725ba1b, 0x560f8538, 0x5d018c35, 0x40139722, 0x4b1d9e2f, 0x2247e964, 0x2949e069, 0x345bfb7e, 0x3f55f273, 0x0e7fcd50, 0x0571c45d, 0x1863df4a, 0x136dd647, 0xcad731dc, 0xc1d938d1, 0xdccb23c6, 0xd7c52acb, 0xe6ef15e8, 0xede11ce5, 0xf0f307f2, 0xfbfd0eff, 0x92a779b4, 0x99a970b9, 0x84bb6bae, 0x8fb562a3, 0xbe9f5d80, 0xb591548d, 0xa8834f9a, 0xa38d4697];
var U3 = [0x00000000, 0x0d0b0e09, 0x1a161c12, 0x171d121b, 0x342c3824, 0x3927362d, 0x2e3a2436, 0x23312a3f, 0x68587048, 0x65537e41, 0x724e6c5a, 0x7f456253, 0x5c74486c, 0x517f4665, 0x4662547e, 0x4b695a77, 0xd0b0e090, 0xddbbee99, 0xcaa6fc82, 0xc7adf28b, 0xe49cd8b4, 0xe997d6bd, 0xfe8ac4a6, 0xf381caaf, 0xb8e890d8, 0xb5e39ed1, 0xa2fe8cca, 0xaff582c3, 0x8cc4a8fc, 0x81cfa6f5, 0x96d2b4ee, 0x9bd9bae7, 0xbb7bdb3b, 0xb670d532, 0xa16dc729, 0xac66c920, 0x8f57e31f, 0x825ced16, 0x9541ff0d, 0x984af104, 0xd323ab73, 0xde28a57a, 0xc935b761, 0xc43eb968, 0xe70f9357, 0xea049d5e, 0xfd198f45, 0xf012814c, 0x6bcb3bab, 0x66c035a2, 0x71dd27b9, 0x7cd629b0, 0x5fe7038f, 0x52ec0d86, 0x45f11f9d, 0x48fa1194, 0x03934be3, 0x0e9845ea, 0x198557f1, 0x148e59f8, 0x37bf73c7, 0x3ab47dce, 0x2da96fd5, 0x20a261dc, 0x6df6ad76, 0x60fda37f, 0x77e0b164, 0x7aebbf6d, 0x59da9552, 0x54d19b5b, 0x43cc8940, 0x4ec78749, 0x05aedd3e, 0x08a5d337, 0x1fb8c12c, 0x12b3cf25, 0x3182e51a, 0x3c89eb13, 0x2b94f908, 0x269ff701, 0xbd464de6, 0xb04d43ef, 0xa75051f4, 0xaa5b5ffd, 0x896a75c2, 0x84617bcb, 0x937c69d0, 0x9e7767d9, 0xd51e3dae, 0xd81533a7, 0xcf0821bc, 0xc2032fb5, 0xe132058a, 0xec390b83, 0xfb241998, 0xf62f1791, 0xd68d764d, 0xdb867844, 0xcc9b6a5f, 0xc1906456, 0xe2a14e69, 0xefaa4060, 0xf8b7527b, 0xf5bc5c72, 0xbed50605, 0xb3de080c, 0xa4c31a17, 0xa9c8141e, 0x8af93e21, 0x87f23028, 0x90ef2233, 0x9de42c3a, 0x063d96dd, 0x0b3698d4, 0x1c2b8acf, 0x112084c6, 0x3211aef9, 0x3f1aa0f0, 0x2807b2eb, 0x250cbce2, 0x6e65e695, 0x636ee89c, 0x7473fa87, 0x7978f48e, 0x5a49deb1, 0x5742d0b8, 0x405fc2a3, 0x4d54ccaa, 0xdaf741ec, 0xd7fc4fe5, 0xc0e15dfe, 0xcdea53f7, 0xeedb79c8, 0xe3d077c1, 0xf4cd65da, 0xf9c66bd3, 0xb2af31a4, 0xbfa43fad, 0xa8b92db6, 0xa5b223bf, 0x86830980, 0x8b880789, 0x9c951592, 0x919e1b9b, 0x0a47a17c, 0x074caf75, 0x1051bd6e, 0x1d5ab367, 0x3e6b9958, 0x33609751, 0x247d854a, 0x29768b43, 0x621fd134, 0x6f14df3d, 0x7809cd26, 0x7502c32f, 0x5633e910, 0x5b38e719, 0x4c25f502, 0x412efb0b, 0x618c9ad7, 0x6c8794de, 0x7b9a86c5, 0x769188cc, 0x55a0a2f3, 0x58abacfa, 0x4fb6bee1, 0x42bdb0e8, 0x09d4ea9f, 0x04dfe496, 0x13c2f68d, 0x1ec9f884, 0x3df8d2bb, 0x30f3dcb2, 0x27eecea9, 0x2ae5c0a0, 0xb13c7a47, 0xbc37744e, 0xab2a6655, 0xa621685c, 0x85104263, 0x881b4c6a, 0x9f065e71, 0x920d5078, 0xd9640a0f, 0xd46f0406, 0xc372161d, 0xce791814, 0xed48322b, 0xe0433c22, 0xf75e2e39, 0xfa552030, 0xb701ec9a, 0xba0ae293, 0xad17f088, 0xa01cfe81, 0x832dd4be, 0x8e26dab7, 0x993bc8ac, 0x9430c6a5, 0xdf599cd2, 0xd25292db, 0xc54f80c0, 0xc8448ec9, 0xeb75a4f6, 0xe67eaaff, 0xf163b8e4, 0xfc68b6ed, 0x67b10c0a, 0x6aba0203, 0x7da71018, 0x70ac1e11, 0x539d342e, 0x5e963a27, 0x498b283c, 0x44802635, 0x0fe97c42, 0x02e2724b, 0x15ff6050, 0x18f46e59, 0x3bc54466, 0x36ce4a6f, 0x21d35874, 0x2cd8567d, 0x0c7a37a1, 0x017139a8, 0x166c2bb3, 0x1b6725ba, 0x38560f85, 0x355d018c, 0x22401397, 0x2f4b1d9e, 0x642247e9, 0x692949e0, 0x7e345bfb, 0x733f55f2, 0x500e7fcd, 0x5d0571c4, 0x4a1863df, 0x47136dd6, 0xdccad731, 0xd1c1d938, 0xc6dccb23, 0xcbd7c52a, 0xe8e6ef15, 0xe5ede11c, 0xf2f0f307, 0xfffbfd0e, 0xb492a779, 0xb999a970, 0xae84bb6b, 0xa38fb562, 0x80be9f5d, 0x8db59154, 0x9aa8834f, 0x97a38d46];
var U4 = [0x00000000, 0x090d0b0e, 0x121a161c, 0x1b171d12, 0x24342c38, 0x2d392736, 0x362e3a24, 0x3f23312a, 0x48685870, 0x4165537e, 0x5a724e6c, 0x537f4562, 0x6c5c7448, 0x65517f46, 0x7e466254, 0x774b695a, 0x90d0b0e0, 0x99ddbbee, 0x82caa6fc, 0x8bc7adf2, 0xb4e49cd8, 0xbde997d6, 0xa6fe8ac4, 0xaff381ca, 0xd8b8e890, 0xd1b5e39e, 0xcaa2fe8c, 0xc3aff582, 0xfc8cc4a8, 0xf581cfa6, 0xee96d2b4, 0xe79bd9ba, 0x3bbb7bdb, 0x32b670d5, 0x29a16dc7, 0x20ac66c9, 0x1f8f57e3, 0x16825ced, 0x0d9541ff, 0x04984af1, 0x73d323ab, 0x7ade28a5, 0x61c935b7, 0x68c43eb9, 0x57e70f93, 0x5eea049d, 0x45fd198f, 0x4cf01281, 0xab6bcb3b, 0xa266c035, 0xb971dd27, 0xb07cd629, 0x8f5fe703, 0x8652ec0d, 0x9d45f11f, 0x9448fa11, 0xe303934b, 0xea0e9845, 0xf1198557, 0xf8148e59, 0xc737bf73, 0xce3ab47d, 0xd52da96f, 0xdc20a261, 0x766df6ad, 0x7f60fda3, 0x6477e0b1, 0x6d7aebbf, 0x5259da95, 0x5b54d19b, 0x4043cc89, 0x494ec787, 0x3e05aedd, 0x3708a5d3, 0x2c1fb8c1, 0x2512b3cf, 0x1a3182e5, 0x133c89eb, 0x082b94f9, 0x01269ff7, 0xe6bd464d, 0xefb04d43, 0xf4a75051, 0xfdaa5b5f, 0xc2896a75, 0xcb84617b, 0xd0937c69, 0xd99e7767, 0xaed51e3d, 0xa7d81533, 0xbccf0821, 0xb5c2032f, 0x8ae13205, 0x83ec390b, 0x98fb2419, 0x91f62f17, 0x4dd68d76, 0x44db8678, 0x5fcc9b6a, 0x56c19064, 0x69e2a14e, 0x60efaa40, 0x7bf8b752, 0x72f5bc5c, 0x05bed506, 0x0cb3de08, 0x17a4c31a, 0x1ea9c814, 0x218af93e, 0x2887f230, 0x3390ef22, 0x3a9de42c, 0xdd063d96, 0xd40b3698, 0xcf1c2b8a, 0xc6112084, 0xf93211ae, 0xf03f1aa0, 0xeb2807b2, 0xe2250cbc, 0x956e65e6, 0x9c636ee8, 0x877473fa, 0x8e7978f4, 0xb15a49de, 0xb85742d0, 0xa3405fc2, 0xaa4d54cc, 0xecdaf741, 0xe5d7fc4f, 0xfec0e15d, 0xf7cdea53, 0xc8eedb79, 0xc1e3d077, 0xdaf4cd65, 0xd3f9c66b, 0xa4b2af31, 0xadbfa43f, 0xb6a8b92d, 0xbfa5b223, 0x80868309, 0x898b8807, 0x929c9515, 0x9b919e1b, 0x7c0a47a1, 0x75074caf, 0x6e1051bd, 0x671d5ab3, 0x583e6b99, 0x51336097, 0x4a247d85, 0x4329768b, 0x34621fd1, 0x3d6f14df, 0x267809cd, 0x2f7502c3, 0x105633e9, 0x195b38e7, 0x024c25f5, 0x0b412efb, 0xd7618c9a, 0xde6c8794, 0xc57b9a86, 0xcc769188, 0xf355a0a2, 0xfa58abac, 0xe14fb6be, 0xe842bdb0, 0x9f09d4ea, 0x9604dfe4, 0x8d13c2f6, 0x841ec9f8, 0xbb3df8d2, 0xb230f3dc, 0xa927eece, 0xa02ae5c0, 0x47b13c7a, 0x4ebc3774, 0x55ab2a66, 0x5ca62168, 0x63851042, 0x6a881b4c, 0x719f065e, 0x78920d50, 0x0fd9640a, 0x06d46f04, 0x1dc37216, 0x14ce7918, 0x2bed4832, 0x22e0433c, 0x39f75e2e, 0x30fa5520, 0x9ab701ec, 0x93ba0ae2, 0x88ad17f0, 0x81a01cfe, 0xbe832dd4, 0xb78e26da, 0xac993bc8, 0xa59430c6, 0xd2df599c, 0xdbd25292, 0xc0c54f80, 0xc9c8448e, 0xf6eb75a4, 0xffe67eaa, 0xe4f163b8, 0xedfc68b6, 0x0a67b10c, 0x036aba02, 0x187da710, 0x1170ac1e, 0x2e539d34, 0x275e963a, 0x3c498b28, 0x35448026, 0x420fe97c, 0x4b02e272, 0x5015ff60, 0x5918f46e, 0x663bc544, 0x6f36ce4a, 0x7421d358, 0x7d2cd856, 0xa10c7a37, 0xa8017139, 0xb3166c2b, 0xba1b6725, 0x8538560f, 0x8c355d01, 0x97224013, 0x9e2f4b1d, 0xe9642247, 0xe0692949, 0xfb7e345b, 0xf2733f55, 0xcd500e7f, 0xc45d0571, 0xdf4a1863, 0xd647136d, 0x31dccad7, 0x38d1c1d9, 0x23c6dccb, 0x2acbd7c5, 0x15e8e6ef, 0x1ce5ede1, 0x07f2f0f3, 0x0efffbfd, 0x79b492a7, 0x70b999a9, 0x6bae84bb, 0x62a38fb5, 0x5d80be9f, 0x548db591, 0x4f9aa883, 0x4697a38d];
function convertToInt32(bytes) {
var result = [];
for (var i = 0; i < bytes.length; i += 4) {
result.push(
(bytes[i ] << 24) |
(bytes[i + 1] << 16) |
(bytes[i + 2] << 8) |
bytes[i + 3]
);
}
return result;
}
var AES = function(key) {
if (!(this instanceof AES)) {
throw Error('AES must be instanitated with `new`');
}
Object.defineProperty(this, 'key', {
value: coerceArray(key, true)
});
this._prepare();
}
AES.prototype._prepare = function() {
var rounds = numberOfRounds[this.key.length];
if (rounds == null) {
throw new Error('invalid key size (must be 16, 24 or 32 bytes)');
}
// encryption round keys
this._Ke = [];
// decryption round keys
this._Kd = [];
for (var i = 0; i <= rounds; i++) {
this._Ke.push([0, 0, 0, 0]);
this._Kd.push([0, 0, 0, 0]);
}
var roundKeyCount = (rounds + 1) * 4;
var KC = this.key.length / 4;
// convert the key into ints
var tk = convertToInt32(this.key);
// copy values into round key arrays
var index;
for (var i = 0; i < KC; i++) {
index = i >> 2;
this._Ke[index][i % 4] = tk[i];
this._Kd[rounds - index][i % 4] = tk[i];
}
// key expansion (fips-197 section 5.2)
var rconpointer = 0;
var t = KC, tt;
while (t < roundKeyCount) {
tt = tk[KC - 1];
tk[0] ^= ((S[(tt >> 16) & 0xFF] << 24) ^
(S[(tt >> 8) & 0xFF] << 16) ^
(S[ tt & 0xFF] << 8) ^
S[(tt >> 24) & 0xFF] ^
(rcon[rconpointer] << 24));
rconpointer += 1;
// key expansion (for non-256 bit)
if (KC != 8) {
for (var i = 1; i < KC; i++) {
tk[i] ^= tk[i - 1];
}
// key expansion for 256-bit keys is "slightly different" (fips-197)
} else {
for (var i = 1; i < (KC / 2); i++) {
tk[i] ^= tk[i - 1];
}
tt = tk[(KC / 2) - 1];
tk[KC / 2] ^= (S[ tt & 0xFF] ^
(S[(tt >> 8) & 0xFF] << 8) ^
(S[(tt >> 16) & 0xFF] << 16) ^
(S[(tt >> 24) & 0xFF] << 24));
for (var i = (KC / 2) + 1; i < KC; i++) {
tk[i] ^= tk[i - 1];
}
}
// copy values into round key arrays
var i = 0, r, c;
while (i < KC && t < roundKeyCount) {
r = t >> 2;
c = t % 4;
this._Ke[r][c] = tk[i];
this._Kd[rounds - r][c] = tk[i++];
t++;
}
}
// inverse-cipher-ify the decryption round key (fips-197 section 5.3)
for (var r = 1; r < rounds; r++) {
for (var c = 0; c < 4; c++) {
tt = this._Kd[r][c];
this._Kd[r][c] = (U1[(tt >> 24) & 0xFF] ^
U2[(tt >> 16) & 0xFF] ^
U3[(tt >> 8) & 0xFF] ^
U4[ tt & 0xFF]);
}
}
}
AES.prototype.encrypt = function(plaintext) {
if (plaintext.length != 16) {
throw new Error('invalid plaintext size (must be 16 bytes)');
}
var rounds = this._Ke.length - 1;
var a = [0, 0, 0, 0];
// convert plaintext to (ints ^ key)
var t = convertToInt32(plaintext);
for (var i = 0; i < 4; i++) {
t[i] ^= this._Ke[0][i];
}
// apply round transforms
for (var r = 1; r < rounds; r++) {
for (var i = 0; i < 4; i++) {
a[i] = (T1[(t[ i ] >> 24) & 0xff] ^
T2[(t[(i + 1) % 4] >> 16) & 0xff] ^
T3[(t[(i + 2) % 4] >> 8) & 0xff] ^
T4[ t[(i + 3) % 4] & 0xff] ^
this._Ke[r][i]);
}
t = a.slice();
}
// the last round is special
var result = createArray(16), tt;
for (var i = 0; i < 4; i++) {
tt = this._Ke[rounds][i];
result[4 * i ] = (S[(t[ i ] >> 24) & 0xff] ^ (tt >> 24)) & 0xff;
result[4 * i + 1] = (S[(t[(i + 1) % 4] >> 16) & 0xff] ^ (tt >> 16)) & 0xff;
result[4 * i + 2] = (S[(t[(i + 2) % 4] >> 8) & 0xff] ^ (tt >> 8)) & 0xff;
result[4 * i + 3] = (S[ t[(i + 3) % 4] & 0xff] ^ tt ) & 0xff;
}
return result;
}
AES.prototype.decrypt = function(ciphertext) {
if (ciphertext.length != 16) {
throw new Error('invalid ciphertext size (must be 16 bytes)');
}
var rounds = this._Kd.length - 1;
var a = [0, 0, 0, 0];
// convert plaintext to (ints ^ key)
var t = convertToInt32(ciphertext);
for (var i = 0; i < 4; i++) {
t[i] ^= this._Kd[0][i];
}
// apply round transforms
for (var r = 1; r < rounds; r++) {
for (var i = 0; i < 4; i++) {
a[i] = (T5[(t[ i ] >> 24) & 0xff] ^
T6[(t[(i + 3) % 4] >> 16) & 0xff] ^
T7[(t[(i + 2) % 4] >> 8) & 0xff] ^
T8[ t[(i + 1) % 4] & 0xff] ^
this._Kd[r][i]);
}
t = a.slice();
}
// the last round is special
var result = createArray(16), tt;
for (var i = 0; i < 4; i++) {
tt = this._Kd[rounds][i];
result[4 * i ] = (Si[(t[ i ] >> 24) & 0xff] ^ (tt >> 24)) & 0xff;
result[4 * i + 1] = (Si[(t[(i + 3) % 4] >> 16) & 0xff] ^ (tt >> 16)) & 0xff;
result[4 * i + 2] = (Si[(t[(i + 2) % 4] >> 8) & 0xff] ^ (tt >> 8)) & 0xff;
result[4 * i + 3] = (Si[ t[(i + 1) % 4] & 0xff] ^ tt ) & 0xff;
}
return result;
}
/**
* Mode Of Operation - Electonic Codebook (ECB)
*/
var ModeOfOperationECB = function(key) {
if (!(this instanceof ModeOfOperationECB)) {
throw Error('AES must be instanitated with `new`');
}
this.description = "Electronic Code Block";
this.name = "ecb";
this._aes = new AES(key);
}
ModeOfOperationECB.prototype.encrypt = function(plaintext) {
plaintext = coerceArray(plaintext);
if ((plaintext.length % 16) !== 0) {
throw new Error('invalid plaintext size (must be multiple of 16 bytes)');
}
var ciphertext = createArray(plaintext.length);
var block = createArray(16);
for (var i = 0; i < plaintext.length; i += 16) {
copyArray(plaintext, block, 0, i, i + 16);
block = this._aes.encrypt(block);
copyArray(block, ciphertext, i);
}
return ciphertext;
}
ModeOfOperationECB.prototype.decrypt = function(ciphertext) {
ciphertext = coerceArray(ciphertext);
if ((ciphertext.length % 16) !== 0) {
throw new Error('invalid ciphertext size (must be multiple of 16 bytes)');
}
var plaintext = createArray(ciphertext.length);
var block = createArray(16);
for (var i = 0; i < ciphertext.length; i += 16) {
copyArray(ciphertext, block, 0, i, i + 16);
block = this._aes.decrypt(block);
copyArray(block, plaintext, i);
}
return plaintext;
}
/**
* Mode Of Operation - Cipher Block Chaining (CBC)
*/
var ModeOfOperationCBC = function(key, iv) {
if (!(this instanceof ModeOfOperationCBC)) {
throw Error('AES must be instanitated with `new`');
}
this.description = "Cipher Block Chaining";
this.name = "cbc";
if (!iv) {
iv = createArray(16);
} else if (iv.length != 16) {
throw new Error('invalid initialation vector size (must be 16 bytes)');
}
this._lastCipherblock = coerceArray(iv, true);
this._aes = new AES(key);
}
ModeOfOperationCBC.prototype.encrypt = function(plaintext) {
plaintext = coerceArray(plaintext);
if ((plaintext.length % 16) !== 0) {
throw new Error('invalid plaintext size (must be multiple of 16 bytes)');
}
var ciphertext = createArray(plaintext.length);
var block = createArray(16);
for (var i = 0; i < plaintext.length; i += 16) {
copyArray(plaintext, block, 0, i, i + 16);
for (var j = 0; j < 16; j++) {
block[j] ^= this._lastCipherblock[j];
}
this._lastCipherblock = this._aes.encrypt(block);
copyArray(this._lastCipherblock, ciphertext, i);
}
return ciphertext;
}
ModeOfOperationCBC.prototype.decrypt = function(ciphertext) {
ciphertext = coerceArray(ciphertext);
if ((ciphertext.length % 16) !== 0) {
throw new Error('invalid ciphertext size (must be multiple of 16 bytes)');
}
var plaintext = createArray(ciphertext.length);
var block = createArray(16);
for (var i = 0; i < ciphertext.length; i += 16) {
copyArray(ciphertext, block, 0, i, i + 16);
block = this._aes.decrypt(block);
for (var j = 0; j < 16; j++) {
plaintext[i + j] = block[j] ^ this._lastCipherblock[j];
}
copyArray(ciphertext, this._lastCipherblock, 0, i, i + 16);
}
return plaintext;
}
/**
* Mode Of Operation - Cipher Feedback (CFB)
*/
var ModeOfOperationCFB = function(key, iv, segmentSize) {
if (!(this instanceof ModeOfOperationCFB)) {
throw Error('AES must be instanitated with `new`');
}
this.description = "Cipher Feedback";
this.name = "cfb";
if (!iv) {
iv = createArray(16);
} else if (iv.length != 16) {
throw new Error('invalid initialation vector size (must be 16 size)');
}
if (!segmentSize) { segmentSize = 1; }
this.segmentSize = segmentSize;
this._shiftRegister = coerceArray(iv, true);
this._aes = new AES(key);
}
ModeOfOperationCFB.prototype.encrypt = function(plaintext) {
if ((plaintext.length % this.segmentSize) != 0) {
throw new Error('invalid plaintext size (must be segmentSize bytes)');
}
var encrypted = coerceArray(plaintext, true);
var xorSegment;
for (var i = 0; i < encrypted.length; i += this.segmentSize) {
xorSegment = this._aes.encrypt(this._shiftRegister);
for (var j = 0; j < this.segmentSize; j++) {
encrypted[i + j] ^= xorSegment[j];
}
// Shift the register
copyArray(this._shiftRegister, this._shiftRegister, 0, this.segmentSize);
copyArray(encrypted, this._shiftRegister, 16 - this.segmentSize, i, i + this.segmentSize);
}
return encrypted;
}
ModeOfOperationCFB.prototype.decrypt = function(ciphertext) {
if ((ciphertext.length % this.segmentSize) != 0) {
throw new Error('invalid ciphertext size (must be segmentSize bytes)');
}
var plaintext = coerceArray(ciphertext, true);
var xorSegment;
for (var i = 0; i < plaintext.length; i += this.segmentSize) {
xorSegment = this._aes.encrypt(this._shiftRegister);
for (var j = 0; j < this.segmentSize; j++) {
plaintext[i + j] ^= xorSegment[j];
}
// Shift the register
copyArray(this._shiftRegister, this._shiftRegister, 0, this.segmentSize);
copyArray(ciphertext, this._shiftRegister, 16 - this.segmentSize, i, i + this.segmentSize);
}
return plaintext;
}
/**
* Mode Of Operation - Output Feedback (OFB)
*/
var ModeOfOperationOFB = function(key, iv) {
if (!(this instanceof ModeOfOperationOFB)) {
throw Error('AES must be instanitated with `new`');
}
this.description = "Output Feedback";
this.name = "ofb";
if (!iv) {
iv = createArray(16);
} else if (iv.length != 16) {
throw new Error('invalid initialation vector size (must be 16 bytes)');
}
this._lastPrecipher = coerceArray(iv, true);
this._lastPrecipherIndex = 16;
this._aes = new AES(key);
}
ModeOfOperationOFB.prototype.encrypt = function(plaintext) {
var encrypted = coerceArray(plaintext, true);
for (var i = 0; i < encrypted.length; i++) {
if (this._lastPrecipherIndex === 16) {
this._lastPrecipher = this._aes.encrypt(this._lastPrecipher);
this._lastPrecipherIndex = 0;
}
encrypted[i] ^= this._lastPrecipher[this._lastPrecipherIndex++];
}
return encrypted;
}
// Decryption is symetric
ModeOfOperationOFB.prototype.decrypt = ModeOfOperationOFB.prototype.encrypt;
/**
* Counter object for CTR common mode of operation
*/
var Counter = function(initialValue) {
if (!(this instanceof Counter)) {
throw Error('Counter must be instanitated with `new`');
}
// We allow 0, but anything false-ish uses the default 1
if (initialValue !== 0 && !initialValue) { initialValue = 1; }
if (typeof(initialValue) === 'number') {
this._counter = createArray(16);
this.setValue(initialValue);
} else {
this.setBytes(initialValue);
}
}
Counter.prototype.setValue = function(value) {
if (typeof(value) !== 'number' || parseInt(value) != value) {
throw new Error('invalid counter value (must be an integer)');
}
// We cannot safely handle numbers beyond the safe range for integers
if (value > Number.MAX_SAFE_INTEGER) {
throw new Error('integer value out of safe range');
}
for (var index = 15; index >= 0; --index) {
this._counter[index] = value % 256;
value = parseInt(value / 256);
}
}
Counter.prototype.setBytes = function(bytes) {
bytes = coerceArray(bytes, true);
if (bytes.length != 16) {
throw new Error('invalid counter bytes size (must be 16 bytes)');
}
this._counter = bytes;
};
Counter.prototype.increment = function() {
for (var i = 15; i >= 0; i--) {
if (this._counter[i] === 255) {
this._counter[i] = 0;
} else {
this._counter[i]++;
break;
}
}
}
/**
* Mode Of Operation - Counter (CTR)
*/
var ModeOfOperationCTR = function(key, counter) {
if (!(this instanceof ModeOfOperationCTR)) {
throw Error('AES must be instanitated with `new`');
}
this.description = "Counter";
this.name = "ctr";
if (!(counter instanceof Counter)) {
counter = new Counter(counter)
}
this._counter = counter;
this._remainingCounter = null;
this._remainingCounterIndex = 16;
this._aes = new AES(key);
}
ModeOfOperationCTR.prototype.encrypt = function(plaintext) {
var encrypted = coerceArray(plaintext, true);
for (var i = 0; i < encrypted.length; i++) {
if (this._remainingCounterIndex === 16) {
this._remainingCounter = this._aes.encrypt(this._counter._counter);
this._remainingCounterIndex = 0;
this._counter.increment();
}
encrypted[i] ^= this._remainingCounter[this._remainingCounterIndex++];
}
return encrypted;
}
// Decryption is symetric
ModeOfOperationCTR.prototype.decrypt = ModeOfOperationCTR.prototype.encrypt;
///////////////////////
// Padding
// See:https://tools.ietf.org/html/rfc2315
function pkcs7pad(data) {
data = coerceArray(data, true);
var padder = 16 - (data.length % 16);
var result = createArray(data.length + padder);
copyArray(data, result);
for (var i = data.length; i < result.length; i++) {
result[i] = padder;
}
return result;
}
function pkcs7strip(data) {
data = coerceArray(data, true);
if (data.length < 16) { throw new Error('PKCS#7 invalid length'); }
var padder = data[data.length - 1];
if (padder > 16) { throw new Error('PKCS#7 padding byte out of range'); }
var length = data.length - padder;
for (var i = 0; i < padder; i++) {
if (data[length + i] !== padder) {
throw new Error('PKCS#7 invalid padding byte');
}
}
var result = createArray(length);
copyArray(data, result, 0, 0, length);
return result;
}
///////////////////////
// Exporting
// The block cipher
var aesjs = {
AES: AES,
Counter: Counter,
ModeOfOperation: {
ecb: ModeOfOperationECB,
cbc: ModeOfOperationCBC,
cfb: ModeOfOperationCFB,
ofb: ModeOfOperationOFB,
ctr: ModeOfOperationCTR
},
utils: {
hex: convertHex,
utf8: convertUtf8
},
padding: {
pkcs7: {
pad: pkcs7pad,
strip: pkcs7strip
}
},
_arrayTest: {
coerceArray: coerceArray,
createArray: createArray,
copyArray: copyArray,
}
};
// node.js
if (typeof exports !== 'undefined') {
module.exports = aesjs
// RequireJS/AMD
// http://www.requirejs.org/docs/api.html
// https://github.com/amdjs/amdjs-api/wiki/AMD
} else if (typeof(define) === 'function' && define.amd) {
define([], function() { return aesjs; });
// Web Browsers
} else {
// If there was an existing library at "aesjs" make sure it's still available
if (root.aesjs) {
aesjs._aesjs = root.aesjs;
}
root.aesjs = aesjs;
}
})(this);
},{}]},{},[])("aes-js")
});
+41
View File
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="183"
height="183"
viewBox="-56.5 -44 183 183"
version="1.1"
id="svg1"
xml:space="preserve"
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
sodipodi:docname="sim.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="1.3469697"
inkscape:cx="113.2171"
inkscape:cy="187.82902"
inkscape:window-width="1707"
inkscape:window-height="1007"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" /><style type="text/css">@media (prefers-color-scheme: dark) { path { fill: #ffffff; } }</style><defs
id="defs1" /><g
inkscape:label="Слой 1"
inkscape:groupmode="layer"
id="layer1"><path
fill="#000000"
d="m -0.1304947,103.98892 c -2.832044,-0.76743 -5.441699,-3.02673 -6.734761,-5.830592 l -0.600371,-1.30184 V 47.511687 -1.833103 l 0.690582,-1.45521 c 1.069347,-2.25335 2.620862,-3.8111129 4.935149,-4.9550229 l 2.01929,-0.9981 24.2233257,-0.07338 c 15.628147,-0.04735 24.667267,0.02165 25.474467,0.194438 2.93081,0.627379 3.24328,0.889045 14.83492,12.422955 9.75955,9.7109599 11.15853,11.2053299 11.90625,12.7181399 l 0.84727,1.71423 v 39.56077 39.560781 l -0.69058,1.4552 c -1.06939,2.253442 -2.62086,3.811122 -4.93526,4.955032 l -2.01939,0.9981 -34.27739,0.0512 c -29.0101883,0.0433 -34.4919167,-0.007 -35.6735017,-0.32711 z M 69.807477,99.400158 c 1.54279,-0.70076 2.94311,-2.44775 3.28556,-4.09897 0.19262,-0.92877 0.25873,-13.13398 0.21014,-38.793661 -0.0617,-32.61006 -0.12207,-37.5513 -0.46776,-38.31279 -0.58495,-1.28854 -21.30517,-21.96092 -22.62188,-22.56965 -1.00491,-0.46458 -2.26697,-0.48928 -25.003117,-0.48928 -22.6963917,0 -24.0007447,0.0254 -25.0178957,0.48742 -1.533296,0.69645 -2.941608,2.44785 -3.296415,4.09949004 -0.20721,0.96458 -0.267588,14.95269996 -0.209699,48.58271996 l 0.0813,47.228131 0.571082,1.05833 c 0.314094,0.58209 0.909407,1.37919 1.322916,1.77134 1.695763,1.60819 -0.536882,1.51482 36.3382887,1.51979 32.21189,0.004 33.78281,-0.0175 34.80748,-0.48287 z M 15.146229,88.018118 c -2.69069,-0.54101 -5.08953,-2.61032 -6.0729303,-5.23871 -0.53439,-1.42827 -0.54303,-1.82376 -0.47185,-21.602461 l 0.0725,-20.14755 0.77957,-1.40728 c 0.9491803,-1.71346 2.1625903,-2.81514 4.1031503,-3.72534 l 1.46728,-0.68821 h 19.976048 19.97604 l 1.46728,0.68821 c 1.94057,0.9102 3.15397,2.01188 4.10315,3.72534 l 0.77957,1.40728 0.0725,20.14755 c 0.0712,19.778701 0.0625,20.174191 -0.47184,21.602461 -0.72128,1.92779 -2.23869,3.6008 -4.13783,4.56212 l -1.54825,0.78371 -19.57917,0.0405 c -10.768537,0.0223 -20.000388,-0.0442 -20.515228,-0.14766 z m 9.535021,-10.07934 v -5.82084 h -5.820831 -5.82084 v 4.60991 4.60991 l 0.71449,0.84912 c 1.16572,1.38538 1.8679,1.55091 6.6277,1.56237 l 4.299481,0.0104 z m 16.139577,0.01 v -5.811 l -5.82107,-0.076 -5.821067,-0.076 2.4e-4,5.88698 2.4e-4,5.88698 h 5.820827 5.82083 z m 13.96991,5.43925 c 0.39296,-0.20445 1.01213,-0.68997 1.37593,-1.07893 0.64718,-0.69193 0.66325,-0.80958 0.7443,-5.4491 l 0.0829,-4.7419 -5.04378,-0.0331 c -2.77409,-0.0182 -5.22238,-0.0331 -5.44066,-0.0331 -1.24803,0 -1.19063,-0.28384 -1.19063,5.88698 v 5.82083 h 4.37877 c 3.24522,0 4.56372,-0.0962 5.09322,-0.37174 z m 2.16968,-21.720971 v -5.95312 H 41.470267 25.98011 l -0.64943,-0.64943 -0.64943,-0.64944 v -7.42036 -7.42036 h -4.241831 c -4.6105,0 -5.44266,0.18168 -6.60609,1.44224 l -0.66145,0.71669 -0.0734,12.94345 -0.0734,12.94346 h 21.967688 21.96769 z m -16.13959,-16.13974 v -5.95313 h -5.82083 -5.820827 v 5.77674 c 0,3.17721 0.0794,5.85611 0.17639,5.95313 0.097,0.097 2.716387,0.17638 5.820827,0.17638 h 5.64444 z m 16.13959,1.36204 c 0,-4.33264 -0.0338,-4.64169 -0.60059,-5.4901 -1.07012,-1.60187 -1.89152,-1.82419 -6.7416,-1.82465 l -4.29948,-4.2e-4 v 5.77674 c 0,3.17721 0.0794,5.85611 0.17639,5.95313 0.097,0.097 2.71639,0.17638 5.82083,0.17638 h 5.64445 z"
id="path1" /></g></svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

+562
View File
@@ -0,0 +1,562 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OTAMan — Справка</title>
<link rel="stylesheet" href="style.css">
</head>
<body class="bg-neutral-50 dark:bg-slate-900 text-gray-800 dark:text-slate-200">
<div class="flex">
<aside class="sticky top-0 h-screen overflow-y-auto w-72 shrink-0 border-r border-gray-300 dark:border-slate-700 px-4 py-6">
<a href="index.html" class="block text-xs text-gray-400 hover:text-gray-600 dark:text-slate-500 dark:hover:text-slate-300 mb-4">&larr; Назад к приложению</a>
<nav id="toc" class="text-sm"></nav>
</aside>
<main class="flex-1 min-w-0 px-6 py-8">
<h1 class="text-2xl font-bold text-heading mb-6">OTAMan <span class="text-xs text-gray-400 dark:text-slate-500 ml-2">Документация</span></h1>
<section class="mb-10">
<h2 id="overview" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">1. Обзор</h2>
<p class="mb-3">OTAMan — Progressive Web App (PWA) для построения APDU-команд, сборки защищённых пакетов SCP80, просмотра меню SIM Toolkit (STK) и симуляции реальной сетевой среды для тестирования SIM/USIM/UICC-карт через PC/SC-ридер.</p>
<p class="mb-3">Приложение — один статический файл <code class="font-mono text-sm">index.html</code>. Все вычисления выполняются в браузере; доступ к карте — через локальный HTTP-сервер (<code class="font-mono text-sm">pysim-otaman-server</code>), оборачивающий библиотеку <a href="https://osmocom.org/projects/pysim" class="text-blue-600 dark:text-blue-400 hover:underline">pySim</a>.</p>
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">Браузер (OTAMan PWA) &rarr; HTTP :8080 &rarr; pysim-otaman-server &rarr; pySim &rarr; PC/SC &rarr; ридер &rarr; UICC/SIM</pre>
<p class="mb-3">Стандарты, на которые опирается приложение:</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li>ETSI TS 102 221 — интерфейс UICC-терминал (CLA 00, файлы USIM)</li>
<li>ETSI TS 102 222 — административные команды</li>
<li>ETSI TS 102 223 — Card Application Toolkit (CAT, проактивные команды)</li>
<li>ETSI TS 102 225 — структура защищённых пакетов для (U)SIM toolkit</li>
<li>ETSI TS 102 226 — структура удалённых APDU для UICC-приложений</li>
<li>ETSI TS 151 011 — интерфейс SIM-ME (CLA A0)</li>
<li>3GPP TS 131 111 — USIM Application Toolkit (USAT)</li>
<li>3GPP TS 31.102 — характеристики приложения USIM</li>
<li>3GPP TS 23.038 — алфавит GSM 7-bit и DCS</li>
<li>3GPP TS 24.008 / 24.301 / 24.501 — коды причин NAS</li>
<li>GlobalPlatform Card Specification v2.3.1</li>
<li>GlobalPlatform GPC v2.2 Amendment B v1.1 — Remote Application Management over HTTP</li>
<li>ISO/IEC 7816-4 — команды обмена</li>
<li>ISO/IEC 9797-1 — алгоритмы MAC</li>
</ul>
<h3 id="interface" class="text-lg font-medium mb-2">1.1 Интерфейс</h3>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Шапка</strong> — версия приложения, кнопка <strong>INSTALL PWA</strong> (появляется, когда браузер предлагает установку, для офлайн-работы), ссылки на проект на GitHub и на эту справку, переключатель языка <strong>EN/RU</strong> и переключатель тёмной/светлой <strong>темы</strong>.</li>
<li>Выбор языка и темы хранится в <code class="font-mono text-sm">localStorage</code> и сохраняется между перезагрузками.</li>
<li>Вкладки верхнего уровня: <strong>Remote APDU</strong> (<strong>SIM RFM</strong>, <strong>USIM RFM</strong>, <strong>Expanded Script</strong>, <strong>RAM/GP</strong>, <strong>HTTP OTA</strong>, <strong>Разбор C-APDU</strong>, <strong>&laquo;Парсер ответов&raquo;</strong>), <strong>SCP80</strong> (<strong>Secured Packet</strong>, <strong>Карты</strong>, <strong>RAM</strong>), <strong>&laquo;Профайлер&raquo;</strong> (вкладки <strong>&laquo;Профили&raquo;</strong>, <strong>&laquo;Снимки карт&raquo;</strong>, <strong>&laquo;Пользовательские файлы&raquo;</strong>), <strong>&laquo;Картридер&raquo;</strong> (<strong>Файловый менеджер</strong>, <strong>Командная строка pySim</strong>, <strong>Отправка APDU</strong>) и <strong>&laquo;Симулятор телефона&raquo;</strong>.</li>
<li>Ссылка <strong>справка</strong> открывает эту документацию на разделе, соответствующем текущему представлению (например, вкладка &laquo;Профайлер&raquo; открывает &sect;5).</li>
</ul>
<section class="mb-10">
<h2 id="c-apdu" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">2. Вкладка Remote APDU</h2>
<p class="mb-3">Построение командных APDU (C-APDU). Семь подвкладок охватывают разные поколения карт, наборы команд и инструменты разбора: <strong>SIM RFM</strong>, <strong>USIM RFM</strong>, <strong>Expanded Script</strong>, <strong>RAM/GP</strong>, <strong>HTTP OTA</strong>, <strong>Разбор C-APDU</strong> и <strong>&laquo;Парсер ответов&raquo;</strong>.</p>
<h3 id="sim-rfm" class="text-lg font-medium mb-2">2.1 SIM RFM</h3>
<p class="mb-2">CLA = <code class="font-mono text-sm">A0</code> (GSM 11.11 / TS 151 011, ISO 7816-4). Удалённое управление файлами классических SIM-карт.</p>
<p class="mb-2">Команды собираются в виде <strong>цепочки</strong>: нажмите кнопку <code class="font-mono text-sm">+&nbsp;SELECT</code> (или другую командную кнопку), чтобы добавить строку, заполните её поля — предпросмотр цепочки (над кнопкой упаковки) обновится автоматически. Добавьте строку <strong>GET RESPONSE</strong>, чтобы получить данные после SELECT. Кнопка <strong>Упаковать в Secured packet</strong> упаковывает всю цепочку в пакет SCP80.</p>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700">
<th class="text-left py-1 px-2">Команда</th><th class="text-left py-1 px-2">INS</th><th class="text-left py-1 px-2">Описание</th>
</tr></thead>
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">SELECT</td><td class="py-1 px-2 font-mono">A4</td><td class="py-1 px-2">Выбор EF/DF по FID, пути, AID или цепочке</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">UPDATE RECORD</td><td class="py-1 px-2 font-mono">DC</td><td class="py-1 px-2">Обновление записи</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">UPDATE BINARY</td><td class="py-1 px-2 font-mono">D6</td><td class="py-1 px-2">Обновление бинарных данных</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">READ RECORD</td><td class="py-1 px-2 font-mono">B2</td><td class="py-1 px-2">Чтение записи</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">READ BINARY</td><td class="py-1 px-2 font-mono">B0</td><td class="py-1 px-2">Чтение бинарных данных</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">ERASE BINARY</td><td class="py-1 px-2 font-mono">0E</td><td class="py-1 px-2">Стирание бинарных данных</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">ACTIVATE FILE</td><td class="py-1 px-2 font-mono">44</td><td class="py-1 px-2">Активация файла</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">DEACTIVATE FILE</td><td class="py-1 px-2 font-mono">04</td><td class="py-1 px-2">Деактивация файла</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">VERIFY PIN</td><td class="py-1 px-2 font-mono">20</td><td class="py-1 px-2">Проверка PIN1 или PIN2</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CHANGE PIN</td><td class="py-1 px-2 font-mono">24</td><td class="py-1 px-2">Смена PIN1 или PIN2</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">DISABLE PIN</td><td class="py-1 px-2 font-mono">26</td><td class="py-1 px-2">Отключение PIN</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">ENABLE PIN</td><td class="py-1 px-2 font-mono">28</td><td class="py-1 px-2">Включение PIN</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">UNBLOCK PIN</td><td class="py-1 px-2 font-mono">2C</td><td class="py-1 px-2">Разблокировка PIN с помощью PUK</td></tr>
<tr><td class="py-1 px-2 font-mono">GET RESPONSE</td><td class="py-1 px-2 font-mono">C0</td><td class="py-1 px-2">Получение данных, на которые указывает предшествующий <code class="font-mono text-sm">61XX</code>/<code class="font-mono text-sm">9FXX</code></td></tr>
</tbody>
</table>
<h4 id="sim-select-methods" class="font-medium mb-1">Методы SELECT</h4>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700">
<th class="text-left py-1 px-2">Метод</th><th class="text-left py-1 px-2">P1</th><th class="text-left py-1 px-2">P2</th><th class="text-left py-1 px-2">Ввод</th>
</tr></thead>
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">По FID</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">FID (4 hex)</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">По полному пути от MF</td><td class="py-1 px-2 font-mono">08</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Полный путь от MF</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">По DF name / AID</td><td class="py-1 px-2 font-mono">04</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">AID</td></tr>
<tr><td class="py-1 px-2">Цепочка</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">FID через запятую; токен <code class="font-mono text-sm">C0</code> (или <code class="font-mono text-sm">C0:NN</code>) вставляет GET RESPONSE</td></tr>
</tbody>
</table>
<p class="text-sm mb-3">Для record-команд режим P2: <strong>Absolute (04)</strong>, <strong>Next (02)</strong> или <strong>Previous (03)</strong>. Если за Case&nbsp;4 командой сразу следует строка GET RESPONSE, сборщик цепочки автоматически убирает её байт Le (ETSI TS 102 226 &sect;5.1.1). В правой колонке — панель конвертации (IMSI, MSISDN, ICCID, SPN, PLMN, nibble swap). См. <a href="#conversion" class="text-blue-600 dark:text-blue-400 hover:underline">&sect;2.5</a>.</p>
<h3 id="usim-rfm" class="text-lg font-medium mb-2">2.2 USIM RFM</h3>
<p class="mb-2">CLA = <code class="font-mono text-sm">00</code> (ETSI TS 102 221). Тот же сборщик цепочки и набор команд, что и SIM. Отличия:</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>SELECT</strong> по умолчанию запрашивает FCP (P2=<code class="font-mono text-sm">04</code>) и добавляет Le=<code class="font-mono text-sm">00</code>; флажок <strong>Silent (P2=0C)</strong> выбирает файл без запроса FCP (без Le, без данных ответа).</li>
<li><strong>По пути</strong> предлагает выбор <strong>from MF</strong> (P1=<code class="font-mono text-sm">08</code>) или <strong>from current DF</strong> (P1=<code class="font-mono text-sm">09</code>).</li>
<li>Каждый переход SELECT в цепочке запрашивает FCP, если не отмечен как silent.</li>
</ul>
<h3 id="ber-tlv" class="text-lg font-medium mb-2">2.3 Expanded Script</h3>
<p class="mb-2">Построение формата Expanded Remote Application data по ETSI TS 102 226 &sect;5.2.1.</p>
<h4 id="ber-format" class="font-medium mb-1">Формат</h4>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Definite (AA)</strong>: <code class="font-mono text-sm">AA</code> + длина + Command TLV</li>
<li><strong>Indefinite (AE)</strong>: <code class="font-mono text-sm">AE</code> + <code class="font-mono text-sm">80</code> + Command TLV + <code class="font-mono text-sm">00 00</code></li>
</ul>
<h4 id="ber-command-tlvs" class="font-medium mb-1">Command TLV</h4>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700">
<th class="text-left py-1 px-2">Тип</th><th class="text-left py-1 px-2">Тег</th><th class="text-left py-1 px-2">Описание</th>
</tr></thead>
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">C-APDU</td><td class="py-1 px-2 font-mono">22</td><td class="py-1 px-2">APDU</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Immediate Action</td><td class="py-1 px-2 font-mono">81</td><td class="py-1 px-2">Проактивная команда или action indicator</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Error Action</td><td class="py-1 px-2 font-mono">82</td><td class="py-1 px-2">Условное восстановление при ошибках с action indicator или проактивной командой</td></tr>
<tr><td class="py-1 px-2">Script Chaining</td><td class="py-1 px-2 font-mono">83</td><td class="py-1 px-2">Многопакетное выполнение скрипта с флагами First/Intermediary/Last</td></tr>
</tbody>
</table>
<p class="text-sm mb-3">Сборщик Immediate Action предлагает action indicator (<code class="font-mono text-sm">81</code>/<code class="font-mono text-sm">82</code>), структурированный сборщик проактивных команд (REFRESH, DISPLAY TEXT, PLAY TONE с авто-генерацией COMPREHENSION-TLV), или ручной hex-ввод.</p>
<h4 id="ber-error-action" class="font-medium mb-2 text-base">Error Action TLV (Tag 82)</h4>
<p class="text-sm mb-2">Восстановление при ошибках по TS 102 226 §5.2.1.3 — одна из четырёх форм:</p>
<ul class="text-sm list-disc pl-5 mb-2">
<li><strong>Проактивная команда:</strong> набор COMPREHENSION-TLV с DISPLAY TEXT или PLAY TONE (в Error Action допустимы только эти две, TS 102 226 Table 5.9)</li>
<li><strong>Без действия:</strong> <code class="font-mono text-sm">82 00</code></li>
<li><strong>Ссылка на запись EFRMA:</strong> <code class="font-mono text-sm">82 01 &lt;ref&gt;</code> с номером записи <code class="font-mono text-sm">01</code><code class="font-mono text-sm">7F</code></li>
<li><strong>Произвольный hex:</strong> произвольное значение TLV</li>
</ul>
<h4 id="ber-script-chaining" class="font-medium mb-2 text-base">Script Chaining TLV (Tag 83)</h4>
<p class="text-sm mb-2">Многопакетное выполнение скрипта с сохранением контекста:</p>
<ul class="text-sm list-disc pl-5 mb-2">
<li><strong>Флаги цепочки:</strong> <code class="font-mono text-sm">01</code> первый скрипт (удалять инфо о цепочке при сбросе), <code class="font-mono text-sm">11</code> первый скрипт (сохранять инфо о цепочке при сбросе, только RFM), <code class="font-mono text-sm">02</code> последующий скрипт (будут ещё), <code class="font-mono text-sm">03</code> последующий скрипт (последний)</li>
<li><strong>Идентификатор скрипта:</strong> Корреляционный идентификатор между пакетами (1-4 байта hex, авто-инкремент подсказки)</li>
<li><strong>Дополнительные данные:</strong> Расширенная информация цепочки (опционально hex)</li>
<li><strong>Сохранение контекста:</strong> UICC сохраняет состояние безопасности/транзакции между пакетами</li>
</ul>
<h4 id="expanded-response" class="font-medium mb-2 text-base">Декодирование ответов (TS 102 226 §5.2.2)</h4>
<p class="text-sm mb-2">Входящие ответы Proof of Receipt декодируются сервером — формат expanded Remote Application response data (TS 102 226 §5.2.2) или компактный формат. Представление Secured Packet показывает результат после <strong>Отправить на карту</strong> (см. <a href="#secured-packet" class="text-blue-600 dark:text-blue-400 hover:underline">&sect;3.1</a>): статус PoR (TAR, счётчик, сырой PoR), а статусное слово и данные ответа последней команды подставляются в подвкладку <strong>&laquo;Парсер ответов&raquo;</strong> (Remote APDU).</p>
<h3 id="ram-gp" class="text-lg font-medium mb-2">2.4 RAM/GP</h3>
<p class="mb-2">CLA = <code class="font-mono text-sm">80</code> (GlobalPlatform Card Specification v2.3.1). Команды удалённого управления приложениями. Строятся тем же сборщиком цепочки, что и SIM/USIM.</p>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700">
<th class="text-left py-1 px-2">Команда</th><th class="text-left py-1 px-2">INS</th><th class="text-left py-1 px-2">P1</th><th class="text-left py-1 px-2">Описание</th>
</tr></thead>
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INSTALL [for load]</td><td class="py-1 px-2 font-mono">E6</td><td class="py-1 px-2 font-mono">02</td><td class="py-1 px-2">Регистрация загружаемого файла</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INSTALL [for install]</td><td class="py-1 px-2 font-mono">E6</td><td class="py-1 px-2 font-mono">0C</td><td class="py-1 px-2">Установка приложения или SD</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INSTALL [make selectable]</td><td class="py-1 px-2 font-mono">E6</td><td class="py-1 px-2 font-mono">08</td><td class="py-1 px-2">Сделать приложение выбираемым</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INSTALL [registry update]</td><td class="py-1 px-2 font-mono">E6</td><td class="py-1 px-2 font-mono">40</td><td class="py-1 px-2">Обновление реестра</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INSTALL [extradition]</td><td class="py-1 px-2 font-mono">E6</td><td class="py-1 px-2 font-mono">10</td><td class="py-1 px-2">Перемещение между SD</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">LOAD</td><td class="py-1 px-2 font-mono">E8</td><td class="py-1 px-2 font-mono">80</td><td class="py-1 px-2">Загрузка блока кода (P1=80 последний блок, номер блока в P2)</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">DELETE</td><td class="py-1 px-2 font-mono">E4</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Удаление приложения или SD (P2=00 только AID / 80 AID + связанные объекты)</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">GET STATUS</td><td class="py-1 px-2 font-mono">F2</td><td class="py-1 px-2 font-mono">80/40/20/10</td><td class="py-1 px-2">Статус карты</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">GET DATA</td><td class="py-1 px-2 font-mono">CA</td><td class="py-1 px-2 font-mono">tag</td><td class="py-1 px-2">Чтение объектов данных</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">STORE DATA</td><td class="py-1 px-2 font-mono">E2</td><td class="py-1 px-2 font-mono">00/40/80/C0/E0</td><td class="py-1 px-2">Запись данных</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">SET STATUS</td><td class="py-1 px-2 font-mono">F0</td><td class="py-1 px-2 font-mono">80/40/60</td><td class="py-1 px-2">Управление жизненным циклом</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">EXTERNAL AUTHENTICATE</td><td class="py-1 px-2 font-mono">82</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Аутентификация SCP</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INTERNAL AUTHENTICATE</td><td class="py-1 px-2 font-mono">88</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Challenge-response</td></tr>
<tr><td class="py-1 px-2 font-mono">GET RESPONSE</td><td class="py-1 px-2 font-mono">C0</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Получение данных после <code class="font-mono text-sm">61XX</code> (Le настраивается)</td></tr>
</tbody>
</table>
<h4 id="ram-privileges" class="font-medium mb-1">Привилегии (INSTALL [for install])</h4>
<p class="text-sm mb-2">Три байта привилегий (таблицы 11-7/8/9 спецификации GP), кодируются как length-value поле внутри данных INSTALL:</p>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700">
<th class="text-left py-1 px-2">Бит</th><th class="text-left py-1 px-2">Байт 1</th><th class="text-left py-1 px-2">Байт 2</th><th class="text-left py-1 px-2">Байт 3</th>
</tr></thead>
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">b8</td><td class="py-1 px-2">Security Domain</td><td class="py-1 px-2">Trusted Path</td><td class="py-1 px-2">Receipt Generation</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">b7</td><td class="py-1 px-2">DAP Verification</td><td class="py-1 px-2">Authorized Management</td><td class="py-1 px-2"></td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">b6</td><td class="py-1 px-2">Delegated Management</td><td class="py-1 px-2">Token Verification</td><td class="py-1 px-2"></td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">b5</td><td class="py-1 px-2">Card Lock</td><td class="py-1 px-2">Global Delete</td><td class="py-1 px-2"></td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">b4</td><td class="py-1 px-2">Card Terminate</td><td class="py-1 px-2">Global Lock</td><td class="py-1 px-2"></td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">b3</td><td class="py-1 px-2">Card Reset</td><td class="py-1 px-2">Global Registry</td><td class="py-1 px-2"></td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">b2</td><td class="py-1 px-2">CVM Management</td><td class="py-1 px-2">Final Application</td><td class="py-1 px-2"></td></tr>
<tr><td class="py-1 px-2 font-mono">b1</td><td class="py-1 px-2">Mandated DAP Verification</td><td class="py-1 px-2"></td><td class="py-1 px-2"></td></tr>
</tbody>
</table>
<h4 id="ram-msl" class="font-medium mb-1">MSL (Minimum Security Level) &mdash; байт SPI1 по TS 102 225</h4>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Значение</th><th class="text-left py-1 px-2">Описание</th></tr></thead>
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Нет проверки</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">11</td><td class="py-1 px-2">RC/CC/DS</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">12</td><td class="py-1 px-2">RC/DS/CC</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">15</td><td class="py-1 px-2">RC/DS/CC + MAC</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">16</td><td class="py-1 px-2">RC/DS/CC + MAC + Cipher</td></tr>
<tr><td class="py-1 px-2 font-mono">19</td><td class="py-1 px-2">RC/DS/CC + MAC + Cipher + DS</td></tr>
</tbody>
</table>
<p class="text-sm mb-3">Полные таблицы GET STATUS P1/P2, тегов GET DATA, DELETE P1, STORE DATA P1 и SET STATUS см. в GlobalPlatform v2.3.1 и ETSI TS 102 226 &sect;8.2.1.3.2.</p>
<h3 id="conversion" class="text-lg font-medium mb-2">2.5 Конвертация (боковые панели SIM/USIM)</h3>
<ul class="list-disc list-inside text-sm space-y-1">
<li><strong>IMSI &rarr; EF.IMSI</strong> — 15-значный IMSI в 9-байтный формат (TS 31.102 &sect;4.2.3).</li>
<li><strong>MSISDN &rarr; BCD</strong> — удалить <code class="font-mono text-sm">+</code>, дополнить нечётную длину символом <code class="font-mono text-sm">f</code>, поменять полубайты.</li>
<li><strong>ICCID &rarr; hex</strong> — поменять полубайты строки ICCID.</li>
<li><strong>Provider Name &rarr; SPN</strong> — GSM 7-bit packed, UCS2 non-BMP или UCS2 BMP (TS 31.102 &sect;4.2.5, TS 23.038).</li>
<li><strong>PLMN &rarr; EF_PLMNsel / PLMNwAcT</strong> — 3-байтный BCD + опциональный селектор технологии доступа.</li>
<li><strong>Nibble swap</strong> — поменять пары полубайтов hex-строки чётной длины.</li>
</ul>
<h3 id="c-apdu-parser" class="text-lg font-medium mb-2">2.6 Разбор C-APDU</h3>
<p class="text-sm mb-3">Вставка raw APDU hex и отображение сворачиваемого дерева. Автоматически определяет контейнер: <strong>Expanded Script</strong> (начало <code class="font-mono text-sm">AA</code> или <code class="font-mono text-sm">AE80</code>, декодируется по ETSI TS 102 226 &sect;5.2.1) или <strong>Compact C-APDU chain</strong> (последовательность C-APDU ISO 7816). Каждый узел показывает метку, hex и краткое описание; родительские узлы раскрываются в подэлементы.</p>
<h3 id="http-ota" class="text-lg font-medium mb-2">2.7 HTTP OTA</h3>
<p class="text-sm mb-3">Сборка payload&rsquo;ов Remote Application Management over HTTP по GlobalPlatform <strong>GPC v2.2 Amendment B v1.1</strong> (&sect;4.7). Два режима:</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Триггер (Push SMS)</strong> — параметры запуска административной сессии (<code class="font-mono text-sm">81 &gt; 83 &gt; 84/[85]/[86]/89</code>, таблица 4-3). Сообщение, которое просит Security Domain карты исходящим запросом начать HTTP-сессию.</li>
<li><strong>Store (параметры SD)</strong> — записывает те же параметры как данные карты (параметры Security Domain) командой <strong>STORE DATA в TLV-режиме</strong> (<code class="font-mono text-sm">80 E2 90 00</code>, P1=90 = последний блок + BER-TLV по GP v2.2 Amendment B v1.1.3), обёрнутые в тег <code class="font-mono text-sm">85</code> (или <code class="font-mono text-sm">A5</code>) по таблице 4-4.</li>
</ul>
<p class="text-sm mb-2">Разделы соответствуют таблицам спецификации:</p>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Раздел</th><th class="text-left py-1 px-2">Tag</th><th class="text-left py-1 px-2">Содержимое</th></tr></thead>
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Параметры соединения</td><td class="py-1 px-2 font-mono">84</td><td class="py-1 px-2">Любые COMPREHENSION-TLV для открытия TCP-соединения (OPEN CHANNEL по TS 102 223): Device Identities <code class="font-mono text-sm">02</code>, Alpha <code class="font-mono text-sm">80</code>, Bearer <code class="font-mono text-sm">01</code>, вендорские TLV. Редактор строк + пресеты, редактируемый hex.</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Параметры безопасности</td><td class="py-1 px-2 font-mono">85</td><td class="py-1 px-2">Таблица 4-6: LV PSK Identity (текст), LV Key version/KID. Идентифицирует ключ PSK TLS (RFC 4279).</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Политика повтора</td><td class="py-1 px-2 font-mono">86</td><td class="py-1 px-2">Таблица 4-7: счётчик повторов (2 байта, напр. <code class="font-mono text-sm">B000</code>), задержка повторной попытки как timer TLV из TS 102 223 (<code class="font-mono text-sm">25 03 HH MM SS</code>), опциональный вендорский TLV отчёта о сбое.</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">HTTP POST</td><td class="py-1 px-2 font-mono">89</td><td class="py-1 px-2">Таблицы 4-8/9/10: Host-заголовок (<code class="font-mono text-sm">8A</code>), X-Admin-From agent ID (<code class="font-mono text-sm">8B</code>), URI (<code class="font-mono text-sm">8C</code>) — текст преобразуется в октеты.</td></tr>
</tbody>
</table>
<p class="text-sm mb-2">Флажок <strong>Обёртка в Command Scripting template ('AA')</strong> оборачивает всю команду <code class="font-mono text-sm">81</code> в формат Expanded Remote Application data с определённой длиной (<code class="font-mono text-sm">AA</code>, ETSI TS 102 226 &sect;5.2.1) для TAR-ов, обрабатывающих расширенный формат (RAM-over-HTTP &sect;4.7).</p>
<p class="text-sm mb-2"><strong>Упаковать в Secured packet</strong> отправляет готовый payload на вкладку SCP80 для заполнения SPI/счётчика — там укажите TAR, который слушает SD (обычно TAR OTASD).</p>
<h3 id="response-parser" class="text-lg font-medium mb-2">2.8 &laquo;Парсер ответов&raquo;</h3>
<p class="mb-3">Декодирует raw-ответ команды: выберите отправленную команду, введите SW (например, <code class="font-mono text-sm">9000</code>) и hex данных ответа, затем нажмите <strong>Декодировать</strong>. Поля также автоматически заполняются статусным словом и данными ответа последней команды после успешного нажатия <strong>Отправить на карту</strong> (см. <a href="#secured-packet" class="text-blue-600 dark:text-blue-400 hover:underline">&sect;3.1</a>).</p>
<ul class="list-disc list-inside text-sm space-y-1">
<li><strong>Команда</strong> — группа SIM/USIM (SELECT, STATUS, READ/UPDATE, PIN-операции, CAT-команды типа TERMINAL PROFILE/ENVELOPE/FETCH/TERMINAL RESPONSE, MANAGE CHANNEL, &hellip;) или группа RAM/GP (INSTALL, LOAD, DELETE, GET/STORE DATA, auth, SCP-команды).</li>
<li><strong>Декодирование SW</strong> — статусные слова разрешаются по generic-, UICC- (TS 102 221) и GlobalPlatform-таблицам, контекст определяется автоматически.</li>
<li><strong>Декодирование привилегий</strong> — ответы GET DATA / INSTALL декодируют байты привилегий в читаемые флаги.</li>
<li><strong>Данные ответа</strong> — raw hex отображается и интерпретируется согласно команде (например, FCP-шаблоны SELECT).</li>
</ul>
<section class="mb-10">
<h2 id="scp80" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">3. Вкладка SCP80</h2>
<p class="mb-3">Верхнеуровневая вкладка <strong>SCP80</strong> объединяет разделы, связанные с SCP80. Переключение — тремя переключателями: <strong>Secured Packet</strong>, <strong>Карты</strong> и <strong>RAM</strong>. Собирает защищённые пакеты SCP80 по ETSI TS 102 225.</p>
<h3 id="secured-packet" class="text-lg font-medium mb-2">3.1 Secured Packet</h3>
<p class="mb-2">Собирает защищённые пакеты SCP80 по ETSI TS 102 225.</p>
<h4 id="packet-structure" class="font-medium mb-1">Структура пакета</h4>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Поле</th><th class="text-left py-1 px-2">Размер</th><th class="text-left py-1 px-2">Описание</th></tr></thead>
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CPI</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Command Packet Identifier (02)</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CPL</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Command Packet Length</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CHI</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Command Header Identifier (01)</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CHL</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Command Header Length</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">SPI</td><td class="py-1 px-2">2</td><td class="py-1 px-2">Security Parameter Indicator</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">KIc</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Key Identifier для шифрования</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">KID</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Key Identifier для MAC</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">TAR</td><td class="py-1 px-2">3</td><td class="py-1 px-2">Toolkit Application Reference</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CNTR</td><td class="py-1 px-2">5</td><td class="py-1 px-2">Счётчик повторов</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">PCNTR</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Padding counter</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">RC/CC/DS</td><td class="py-1 px-2">8</td><td class="py-1 px-2">Контрольная сумма / MAC</td></tr>
<tr><td class="py-1 px-2 font-mono">Secured Data</td><td class="py-1 px-2">пер.</td><td class="py-1 px-2">APDU (с шифрованием при необходимости)</td></tr>
</tbody>
</table>
<h4 id="packet-crypto" class="font-medium mb-1">Крипто</h4>
<ul class="list-disc list-inside text-sm space-y-1">
<li><strong>3DES-CBC</strong> шифрование (нулевой ICV), ключи 8/16/24 байта &mdash; устарело с Rel-18, но поддерживается для обратной совместимости</li>
<li><strong>AES-CBC</strong> шифрование (нулевой ICV, дополнение нулями до 16), ключи 16/24/32 байта (TS 102 225 &sect;5.1.2, KIc <code class="font-mono text-sm">x2</code>)</li>
<li><strong>Retail MAC</strong> (ISO 9797-1, MAC algorithm 3) для контрольной суммы DES/3DES</li>
<li><strong>AES-CMAC</strong> (NIST SP 800-38B, усечённый до 8 октетов) для контрольной суммы AES (TS 102 225 &sect;5.1.3.1, KID <code class="font-mono text-sm">x2</code>)</li>
<li>AES требует счётчик с защитой от повтора: биты SPI1 b5&nbsp;b4 должны быть <code class="font-mono text-sm">10</code> (счётчик больше) или <code class="font-mono text-sm">11</code> (счётчик +1) согласно TS 102 225 &sect;5.1.2/&sect;5.1.3.1</li>
<li>Байт паддинга настраивается (<code class="font-mono text-sm">00</code> по умолчанию или <code class="font-mono text-sm">FF</code>)</li>
</ul>
<p class="text-sm mb-3">Кнопка <strong>Проверить в pySim</strong> сверяет собранный пакет с эталонной реализацией <code class="font-mono text-sm">OtaDialectSms.encode_cmd</code>. Кнопка <strong>Отправить на карту</strong> доставляет пакет через ENVELOPE SMS-PP-DOWNLOAD (при подключении к серверу). Полученный Proof of Receipt декодируется и показывается строкой статуса PoR (статус, TAR, счётчик, сырой PoR); статусное слово и данные ответа последней команды подставляются в подвкладку <strong>&laquo;Парсер ответов&raquo;</strong> (Remote APDU), а успешный PoR увеличивает счётчик повторов и очищает пакет.</p>
<h3 id="cards" class="text-lg font-medium mb-2">3.2 Карты</h3>
<p class="mb-2">Хранит предустановки карт локально в браузере (<code class="font-mono text-sm">localStorage</code>), чтобы представление Secured Packet могло автоматически подставлять ключи и параметры.</p>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Поле</th><th class="text-left py-1 px-2">Описание</th></tr></thead>
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Name</td><td class="py-1 px-2">Понятная метка</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">ICCID</td><td class="py-1 px-2">Опциональный идентификатор карты</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">KIc / KID</td><td class="py-1 px-2">Индикаторы ключа и алгоритма (например, 15 = индекс 1, 3DES-CBC2; x2 = AES)</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">SPI1 / SPI2</td><td class="py-1 px-2">Security Parameter Indicators</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">TAR</td><td class="py-1 px-2">Toolkit Application Reference</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Counter</td><td class="py-1 px-2">Счётчик повторов (5 байт)</td></tr>
<tr><td class="py-1 px-2">KIc key / KID key</td><td class="py-1 px-2">16/24/32 hex-символа (ключи 8/16/24 байта 3DES) или 32/48/64 hex-символа (ключи 16/24/32 байта AES)</td></tr>
</tbody>
</table>
<p class="text-sm mb-3">Обмен предустановками: <strong>Экспорт в JSON</strong> и <strong>Экспорт в файл</strong> для выгрузки, <strong>Импорт из файла</strong>, <strong>Вставить и импортировать</strong> или <strong>Импорт JSON из буфера</strong> для загрузки. Выбранная предустановка автоматически заполняет форму Secured Packet.</p>
<h3 id="ram" class="text-lg font-medium mb-2">3.3 RAM</h3>
<p class="mb-2">Выполняет операции удалённого управления приложениями (Remote Application Management) как защищённые пакеты SCP80 через SMS-PP-DOWNLOAD ENVELOPE. Карта должна поддерживать SCP03 (AES или 3DES). Предустановка карты из подвкладки <strong>Карты</strong> обеспечивает SPI, ключи, TAR и счётчик.</p>
<h4 id="ram-operations" class="font-medium mb-1">Операции</h4>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Операция</th><th class="text-left py-1 px-2">Описание</th></tr></thead>
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Обзор карты (все данные GP)</td><td class="py-1 px-2">Запрос GET STATUS для ISD, приложений, ELF и модулей ELF, а также GET DATA FF21 для информации о памяти. Результаты отображаются в обзоре с кнопками <strong>Удалить</strong> для каждого элемента.</td></tr>
<tr><td class="py-1 px-2">Установка пакета (.cap файл)</td><td class="py-1 px-2">Отправка <code class="font-mono text-sm">.cap</code> файла на карту через сервер: INSTALL[for load] &rarr; LOAD &times;N &rarr; INSTALL[for install (+make selectable)].</td></tr>
</tbody>
</table>
<h4 id="ram-explorer" class="font-medium mb-1">Обзор карты (Explorer View)</h4>
<p class="text-sm mb-2">После выполнения &laquo;Обзор карты&raquo; отображается:</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>ISD</strong> &mdash; AID, жизненный цикл, привилегии (без удаления; ISD нельзя удалить)</li>
<li><strong>Приложения</strong> &mdash; AID, жизненный цикл, привилегии, связанный ELF/SD. Каждое имеет кнопку <strong>Удалить</strong> (GP <code class="font-mono text-sm">DELETE</code> по AID).</li>
<li><strong>Исполняемые файлы загрузки (ELF) / пакеты</strong> &mdash; AID, жизненный цикл, версии, AID модулей. Каждый имеет <strong>Удалить</strong> (только ELF) и <strong>Удалить все</strong> (каскадное: ELF + модули + установленные приложения, P2=<code class="font-mono text-sm">0x80</code>).</li>
</ul>
<p class="text-sm mb-3">Удаление подтверждается через диалог браузера перед отправкой команды GP <code class="font-mono text-sm">DELETE</code> через SCP80. Обзор автоматически обновляется после успешного удаления.</p>
<section class="mb-10">
<h2 id="card-reader" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">4. Вкладка &laquo;Картридер&raquo; (pySim)</h2>
<p class="mb-3">Подключение к локальному <a href="https://github.com/anttro/otaman" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> для работы с картой: введите URL сервера (по умолчанию <code class="font-mono text-sm">http://127.0.0.1:8080</code>) и нажмите <strong>Подключиться</strong>. Область статуса показывает состояние ридера/карты, а <strong>Подключить карту</strong> (пере)инициализирует карту после вставки. Подвкладки: <strong>Файловый менеджер</strong>, <strong>Командная строка pySim</strong> и <strong>Отправка APDU</strong>. <strong>&laquo;Профайлер&raquo;</strong> и <strong>&laquo;Симулятор телефона&raquo;</strong> — отдельные вкладки верхнего уровня.</p>
<h3 id="file-manager" class="text-lg font-medium mb-2">4.1 Файловый менеджер</h3>
<p class="text-sm mb-2">Дерево файловой системы отображается слева; выбор файла открывает панель деталей справа. Элементы сгруппированы: DF выше EF, сортировка по <strong>FID</strong> или символьному <strong>имени</strong> (пиллы над деревом; выбор сохраняется в <code class="font-mono text-sm">localStorage</code>). При выборе файла над содержимым также показываются FID, тип файла, размер / структура записей и декодированный FCI.</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Прочитать</strong> — чтение файла (автоопределение transparent/record)</li>
<li><strong>Редактировать</strong> — изменение hex-данных, <strong>Сохранить</strong> для записи (или <strong>Отмена</strong>)</li>
<li><strong>Данные как на карте / Декодированные данные</strong> — переключение между hex-дампом и декодированным JSON</li>
<li><strong>Проверить все файлы</strong> — обход всего дерева (включая пользовательские файлы) с пометкой каждого элемента: есть (обычный вид) или нет (красный ✗, без стрелки разворачивания); существующие пустые DF показывают <code class="font-mono text-sm">(пусто)</code>. Отображается прогресс <em>N / всего</em>, обход можно остановить; в конце — сводка «есть/нет». Файлы проверяются только при разворачивании или проверке — просмотр остаётся ленивым.</li>
</ul>
<h3 id="pysim-cmdline" class="text-lg font-medium mb-2">4.2 Командная строка pySim</h3>
<p class="text-sm mb-3">Выполнение любых команд pySim-shell с подсказками (300&nbsp;мс) и автодополнением.</p>
<h3 id="raw-apdu" class="text-lg font-medium mb-2">4.3 Отправка APDU</h3>
<p class="text-sm mb-3">Отправка произвольного APDU и просмотр ответа.</p>
<h3 id="usage-scenarios" class="text-lg font-medium mb-2">4.4 Сценарии использования</h3>
<h4 id="scenario-a" class="font-medium mb-1">Сценарий A &mdash; Работа с файлами, не входящими в модель pySim (&laquo;Пользовательские файлы&raquo;)</h4>
<ol class="list-decimal list-inside text-sm space-y-1 mb-3">
<li>Получите FID целевого файла (документация вендора или анализ ATR/файловой системы; такие файлы часто отсутствуют в открытых спецификациях).</li>
<li>Откройте вкладку <strong>&laquo;Картридер&raquo;</strong> &rarr; подвкладку <strong>&laquo;Пользовательские файлы&raquo;</strong>.</li>
<li>Введите полный путь (например, <code class="font-mono text-sm">3F00/7F20/6F46</code>) и псевдоним (например, <code class="font-mono text-sm">EF.SPN</code>).</li>
<li>Нажмите <strong>Добавить</strong> — файл появится в дереве курсивом (непроверенный).</li>
<li>Кликните по файлу для проверки существования; при успехе (<code class="font-mono text-sm">9000</code>) он работает как обычный файл.</li>
<li>Читайте, редактируйте и сохраняйте hex-данные; переключайте <strong>Данные как на карте</strong> / <strong>Декодированные данные</strong>.</li>
<li>Экспортируйте список пользовательских файлов в JSON для переноса на другие машины.</li>
</ol>
<h4 id="scenario-b" class="font-medium mb-1">Сценарий B &mdash; Симуляция реальной сетевой среды для тестирования SIM</h4>
<p class="text-sm mb-1"><strong>B.1 Ответы на PROVIDE LOCAL INFORMATION (PLI)</strong></p>
<ol class="list-decimal list-inside text-sm space-y-1 mb-3">
<li>Откройте <strong>Симулятор телефона</strong> &rarr; <strong>Данные для PROVIDE LOCAL INFORMATION</strong>.</li>
<li>Используйте формы декодирования/кодирования для IMEI (<code class="font-mono text-sm">01</code>), Location Info (<code class="font-mono text-sm">00</code>), Access Technology (<code class="font-mono text-sm">06</code>) и т.д.</li>
<li>Нажмите <strong>Сохранить</strong> — значения сохранятся на сервере.</li>
<li>Включите <strong>Опрос</strong> (интервал 30&nbsp;с), чтобы карта периодически выдавала PLI.</li>
<li>Сервер вставляет значения словаря в каждый TERMINAL RESPONSE.</li>
<li>Проверьте в журнале проактивных команд: запись PLI покажет декодированный ответ.</li>
</ol>
<p class="text-sm mb-1"><strong>B.2 Симуляция сетевых действий через ENVELOPE (event download)</strong></p>
<ol class="list-decimal list-inside text-sm space-y-1 mb-3">
<li>Проверьте список <strong>подписанных событий</strong> (из SET UP EVENT LIST).</li>
<li>Нажмите <strong>Отправить</strong> на событии (например, Location Status) и заполните форму; будет отправлен <code class="font-mono text-sm">ENVELOPE(Event Download)</code>.</li>
<li>Для <strong>Network Rejection</strong> выберите тип регистрации &rarr; поля местоположения &rarr; технологию доступа &rarr; причину отклонения.</li>
<li>Карта может ответить проактивной командой, которую обработчик цепочки зарегистрирует и обработает автоматически.</li>
</ol>
<p class="text-sm mb-1"><strong>B.3 Проверка симулированной среды</strong></p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li>Журнал проактивных команд показывает полный цикл (команда + байты TERMINAL RESPONSE).</li>
<li>Кнопка <strong>Отправить STATUS</strong> / автопросмотр поддерживают сессию CAT (цикл дренажа).</li>
</ul>
<section class="mb-10">
<h2 id="profiler" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">5. Профайлер</h2>
<p class="text-sm mb-2">Проверяет соответствие карты именованному <strong>профилю</strong> — упорядоченному набору правил, описывающих ожидаемую файловую систему и (опционально) содержимое файлов. Профили хранятся в <code class="font-mono text-sm">localStorage</code>.</p>
<h4 class="font-medium mb-1">Список профилей</h4>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Новый профиль</strong> — создаёт пустой набор правил, запросив имя.</li>
<li><strong>Профиль с карты</strong> — сканирует подключённую карту и создаёт по одному правилу на каждый существующий файл (см. ниже), затем открывает редактор.</li>
<li><strong>Профиль из снимка</strong> — выбирает сохранённый снимок карты и создаёт по правилу на каждый захваченный файл с теми же опциями сканирования (см. ниже), без картридера; имя профиля подставляется из имени снимка.</li>
<li><strong>Импорт профиля</strong> — загружает набор правил из JSON-файла (имя хранится внутри JSON).</li>
<li>В каждой строке профиля показаны имя и время создания, а также действия <strong>Проверить карту ▶</strong>, <strong>Проверить снимок карты</strong>, <strong>Редактировать</strong>, <strong>Экспорт</strong> (скачать JSON) и <strong>Удалить</strong>.</li>
</ul>
<h4 class="font-medium mb-1">Правила файловой системы</h4>
<p class="text-sm mb-2">Правила выполняются последовательно. Редактор показывает символьное имя файла pySim (если известно) рядом с путём правила; <strong>Добавить правило</strong> добавляет правило, <strong>Сохранить</strong> сохраняет изменения. Правило файловой системы задаётся:</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Путь</strong> — начинается с <code class="font-mono text-sm">MF</code> (например, <code class="font-mono text-sm">MF/7F10/6F3A</code>) или с AID ADF (например, <code class="font-mono text-sm">A0000000871002/6F07</code>).</li>
<li><strong>Проверка FCP/FCI</strong> — какая часть информации об управлении файлом проверяется: <strong>Только тип файла (FCP)</strong> (существование + тип файла), <strong>Тип файла + размер (FCP)</strong> (добавляются размер файла либо длина/число записей для record-файлов) или <strong>Полный FCI</strong> (добавляется побайтовое сравнение сырого ответа SELECT — шаблона FCP <code class="font-mono text-sm">'62'</code> — выявляет изменения FID/AID, жизненного цикла, атрибутов безопасности и проприетарных параметров).</li>
<li><strong>Атрибуты файла</strong> — тип файла, размер, длина записи и число записей из FCP-шаблона (любой можно оставить незаданным).</li>
<li><strong>Проверить содержимое</strong> (опционально) — <strong>Точное</strong> (точное совпадение hex) или <strong>Маска</strong>, где <code class="font-mono text-sm">?</code> — шаблон на один полубайт (маска без <code class="font-mono text-sm">?</code> — совпадение префикса, например <code class="font-mono text-sm">0891</code> для MCC/MNC из IMSI). Для record-файлов хранится список по записям.</li>
</ul>
<p class="text-sm mb-3"><strong>Проверить карту</strong> выполняет каждое правило на подключённой карте и показывает строку прогресса и отчёт прохождения. Рядом с путём файла указывается, что именно проверялось (например, <em>тип файла и размер, содержимое</em> или <em>полный FCI</em>); если часть проверок прошла, а часть нет — каждый аспект помечается (<em>тип файла ✓, размер ✗, содержимое ✓</em>), а расхождения расписываются ниже. Несовпавшие сырые данные (FCI, содержимое, данные записей) показываются как поля только для чтения с моноширинным шрифтом — ожидаемое над фактическим, в одной и той же колонке — для удобного сравнения; для расхождений FCI дополнительно показывается декодированное сравнение по параметрам (размер файла, дескриптор/структура, жизненный цикл, FID, SFI, проприетарные параметры…). Декодированный просмотр FCI также отображается рядом с полем FCI hex при редактировании правила. Если данные FCI повреждены, показывается всё, что удалось декодировать до места ошибки, вместе с явным сообщением об ошибке декодирования. Для record-файлов при расхождении содержимого добавляется пометка <em>совпадающие записи: 1-5, 7-10</em> со списком записей, которые совпали. В отчёте поля расхождений и колонки сравнения FCI подписаны <em>ожидалось (имя профиля)</em> и <em>фактически (ICCID карты)</em>, а в заголовке отчёта выводится <em>Результаты проверки профиля: &lt;профиль&gt; &rarr; &lt;ICCID карты&gt;</em>. Опция <strong>«Только расхождения»</strong> в заголовке отчёта скрывает все совпавшие файлы и оставляет только несовпадения и ошибки.</p>
<h4 class="font-medium mb-1">Опции сканирования &laquo;Профиль с карты&raquo;</h4>
<p class="text-sm mb-2">Диалог сканирования запрашивает имя профиля и предлагает селектор <strong>&laquo;Проверка FCP/FCI&raquo;</strong> (те же три режима, по умолчанию <strong>Тип файла + размер (FCP)</strong>), применяемый ко всем создаваемым правилам, а также список <strong>&laquo;Игнорировать содержимое файлов&raquo;</strong> (все отмечены по умолчанию, кроме <code class="font-mono text-sm">EF.ARR</code>; флажок в заголовке отмечает или снимает весь список) часто перезаписываемых файлов, содержимое которых пропускается: <code class="font-mono text-sm">EF.LOCI</code>, <code class="font-mono text-sm">EF.PSLOCI</code>, <code class="font-mono text-sm">EF.EPSLOCI</code>, <code class="font-mono text-sm">EF.5GS3GPPLOCI</code>, <code class="font-mono text-sm">EF.Keys</code>, <code class="font-mono text-sm">EF.KeysPS</code>, <code class="font-mono text-sm">EF.SMS</code>, <code class="font-mono text-sm">EF.Kc</code>, <code class="font-mono text-sm">EF.KcGPRS</code>, <code class="font-mono text-sm">EF.LOCIGPRS</code>, <code class="font-mono text-sm">EF.CBMID</code>, <code class="font-mono text-sm">EF.SMSS</code>, <code class="font-mono text-sm">EF.ACC</code>, <code class="font-mono text-sm">EF.EPSNSC</code>, <code class="font-mono text-sm">EF.START-HFN</code>, <code class="font-mono text-sm">EF.ARR</code>. Ещё две отмеченные по умолчанию опции <strong>&laquo;Сравнивать первые 4 байта для&raquo;</strong> <code class="font-mono text-sm">EF.IMSI</code> и <code class="font-mono text-sm">EF.ICCID</code> захватывают содержимое этих файлов как маску только первых 4 байт (снимите для точного сравнения). Строка прогресса показывает <em>N / всего файлов</em> с текущим путём файла во время сканирования; при сканировании опции скрываются, а кнопки блокируются. Правила создаются только для файлов, которые реально существуют на карте (возвращён FCP-шаблон); отсутствующие файлы пропускаются. Пользовательские файлы из подвкладки <strong>&laquo;Пользовательские файлы&raquo;</strong> включаются с той же проверкой существования. Тот же диалог и опции использует <strong>&laquo;Профиль из снимка&raquo;</strong>: вместо карты обходятся захваченные файлы выбранного снимка; для файлов, содержимое которых не было захвачено, правило создаётся без проверки содержимого (при последующей проверке профиля они помечаются как непроверяемые).</p>
<h4 id="card-snapshots" class="font-medium mb-1">Снимки карт</h4>
<p class="text-sm mb-2">Представление списка имеет две вкладки &mdash; <strong>&laquo;Профили&raquo;</strong> и <strong>&laquo;Снимки карт&raquo;</strong>. Снимок карты — неизменяемая фиксация файловой системы карты: для каждого существующего файла сохраняются путь, символьное имя, тип, размер (или длина/число записей), сырой FCI из ответа SELECT и содержимое, если файл читается (без списка игнорирования и без масок). ICCID декодируется из EF.ICCID и показывается рядом с именем снимка. При сканировании измеряется время каждой команды карты (SELECT, READ BINARY, READ RECORD) от отправки до ответа; снимок хранит min/сред/max по каждому типу команд и общее время сканирования, а в представлении эти значения показываются в сводке под заголовком, время select/read — для каждого файла и время чтения — для каждой записи. Время носит информационный характер и не используется при проверках и сравнении.</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Новый снимок</strong> &mdash; запрашивает имя и сканирует карту, затем возвращает к списку.</li>
<li><strong>Импорт снимка</strong> &mdash; загружает снимок из JSON-файла.</li>
<li>В каждой строке снимка — <strong>Открыть</strong>, <strong>Экспорт</strong> и <strong>Удалить</strong>. <strong>Открыть</strong> показывает все захваченные данные только для чтения (сырой FCI с декодированным FCI, содержимое); редактируется только имя снимка.</li>
<li><strong>Проверить снимок карты</strong> в строке профиля выполняет правила профиля на выбранном из списка снимке, без картридера. Отчёт такой же, как при проверке карты: фактическая сторона подписана именем снимка (<em>фактически (имя снимка)</em>), а в заголовке — <em>Результаты проверки профиля: &lt;профиль&gt; &rarr; &lt;имя снимка&gt;</em>; файлы, содержимое которых не было захвачено при сканировании, помечаются как непроверяемые ошибки.</li>
<li><strong>Сравнить снимки</strong> сравнивает два снимка без картридера так же, как проверка профиля: выберите <em>эталонный</em> снимок и <em>снимок для проверки</em>, при необходимости включите маску первых 4 байт EF.IMSI/EF.ICCID (включена по умолчанию) и получите такой же отчёт; в этом отчёте заголовок — <em>Результаты сравнения снимков: &lt;эталон&gt; &rarr; &lt;проверяемый&gt;</em>, а поля расхождений и колонки сравнения FCI подписаны именами эталонного и проверяемого снимков вместо expected/actual. Файлы, которые есть только в проверяемом снимке, помечаются как лишние. «К списку» возвращает на вкладку «Снимки карт».</li>
</ul>
</section>
<h4 id="custom-files" class="font-medium mb-1">Пользовательские файлы</h4>
<p class="text-sm mb-3">Добавление файлов, не покрытых моделью pySim: введите полный путь (например, <code class="font-mono text-sm">3F00/7F20/6F46</code>) и псевдоним (например, <code class="font-mono text-sm">EF.SPN</code>), затем нажмите <strong>Добавить</strong>; добавленные файлы появляются в дереве &laquo;Файловый менеджер&raquo;. В каждой строке есть кнопки <strong>&laquo;Редактировать&raquo;</strong> (загружает запись в форму — кнопка становится <strong>&laquo;Сохранить&raquo;</strong>, появляется <strong>&laquo;Отмена&raquo;</strong>) и <strong>&laquo;Удалить&raquo;</strong> (без подтверждения). Список сохраняется в <code class="font-mono text-sm">localStorage</code>; обмен — <strong>Экспорт в JSON</strong> / <strong>Экспорт в файл</strong> и <strong>Импорт из файла</strong> / <strong>Вставить и импортировать</strong> / <strong>Импорт JSON из буфера</strong>.</p>
<section class="mb-10">
<h2 id="proactive-uicc" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">6. Симулятор телефона</h2>
<p class="text-sm mb-3">Работа с сессией Card Application Toolkit. Две подвкладки: <strong>&laquo;Телефон&raquo;</strong> (меню STK, STATUS и опрос, подписанные события, журнал проактивных команд) и <strong>&laquo;Конфигурация TR&raquo;</strong> (данные ответов, подставляемые в TERMINAL RESPONSE для проактивных команд).</p>
<h3 id="stk-menu" class="text-lg font-medium mb-2">6.1 Меню STK</h3>
<p class="text-sm mb-3">Если карта выдала команду SET UP MENU, вверху этого представления появляется блок &laquo;Меню STK&raquo; с изумрудной кнопкой <strong>STK: &lt;название&gt;</strong>, открывающей оверлей меню (браузер STK-меню карты). Если карта не задала меню, вместо кнопки показывается &laquo;Меню не задано картой&raquo;. Состояние меню обновляется при каждом открытии представления. Интерактивные проактивные команды всегда получают TERMINAL RESPONSE: оверлей ждёт вашего выбора, и если вы не ответили и не нажали <strong>Timeout</strong>, сервер сам отвечает результатом timeout через <code class="font-mono text-sm">--menu-timeout</code> секунд (по умолчанию 60, <code class="font-mono text-sm">0</code> отключает). <strong>Назад</strong> и <strong>Timeout</strong> продолжают диалог с картой: если карта в ответ выдаёт следующую проактивную команду (SELECT ITEM или DISPLAY TEXT), панель показывает её; кэшированное верхнее меню появляется только когда карте больше нечего выполнять.</p>
<h3 id="subscribed-events" class="text-lg font-medium mb-2">6.2 Подписанные события (SET UP EVENT LIST)</h3>
<p class="text-sm mb-2">События, которые отслеживает карта. У каждого события есть кнопка <strong>Отправить</strong>, открывающая форму, специфичную для типа события:</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>События без данных</strong> (User Activity, Idle Screen, Data Available, &hellip;) — уведомление в один клик</li>
<li><strong>Location Status</strong> — выпадающий список: Normal / Limited / No service (тег <code class="font-mono text-sm">9B</code>)</li>
<li><strong>Access Technology Change</strong> — 13 типов RAT (тег <code class="font-mono text-sm">BF</code>)</li>
<li><strong>Network Rejection</strong> — полная адаптивная форма: тип регистрации (LU / GPRS / EPS / 5GS), поля местоположения (MCC, MNC, LAC, RAC, TAC), технология доступа и единый выпадающий список из 53 кодов причин (EMM, GMM, 5GMM и LU)</li>
</ul>
<p class="text-sm mb-3">Отправка события использует <code class="font-mono text-sm">ENVELOPE(Event Download)</code> по TS 102 223 / TS 131 111.</p>
<h3 id="proactive-log" class="text-lg font-medium mb-2">6.3 Журнал проактивных команд</h3>
<p class="text-sm mb-2">Хронологический список извлечённых проактивных команд. Каждая строка показывает время, код типа, имя и декодированный квалификатор (для команд, у которых он есть). Для команд с данными ответа показывается строка <code class="font-mono text-sm">Ответ:</code> с байтами TERMINAL RESPONSE (без служебных TLV); ответы PROVIDE LOCAL INFORMATION декодируются через словарь данных PLI.</p>
<h3 id="status-polling" class="text-lg font-medium mb-2">6.4 Опрос STATUS</h3>
<p class="text-sm mb-3">Кнопка <strong>Отправить STATUS</strong> отправляет STATUS (F2) вручную. Переключатель <strong>Опрос</strong> включает фоновый опрос: после настраиваемого интервала бездействия (аргумент сервера <code class="font-mono text-sm">--poll-interval</code>, 1&ndash;255&nbsp;с, по умолчанию 30&nbsp;с, <code class="font-mono text-sm">0</code> отключает опрос) сервер отправляет STATUS и обрабатывает любую ожидающую проактивную команду. При извлечении карты опрос останавливается, а состояние карты сбрасывается.</p>
<h3 id="pli-dict" class="text-lg font-medium mb-2">6.5 &laquo;Конфигурация TR&raquo; &mdash; данные ответа PROVIDE LOCAL INFORMATION</h3>
<p class="text-sm mb-2">Редактируемые hex-значения для всех 22 квалификаторов PLI (TS 102 223 &sect;8.6 + TS 131 111). У десяти квалификаторов есть встроенные формы декодирования/кодирования:</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>00</strong> Location Info (MCC, MNC, LAC/TAC, Cell ID)</li>
<li><strong>01</strong> IMEI &middot; <strong>03</strong> Дата/время/TZ &middot; <strong>04</strong> Язык &middot; <strong>05</strong> Timing Advance</li>
<li><strong>06</strong> Access Technology &middot; <strong>08</strong> IMEISV &middot; <strong>09</strong> Search Mode</li>
<li><strong>0A</strong> Battery &middot; <strong>0E</strong> Multiple Access Technologies</li>
</ul>
<p class="text-sm mb-3">Значения хранятся на сервере до перезапуска. Когда карта выдаёт PLI, сервер вставляет значения словаря в TERMINAL RESPONSE.</p>
</section>
<section class="mb-10">
<h2 id="server" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">7. Установка сервера</h2>
<p class="mb-3">Для работы с картой (вкладка &laquo;Картридер&raquo;, &laquo;Симулятор телефона&raquo;, доставка OTA) нужен локальный <a href="https://github.com/anttro/otaman" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> — встроенный в OTAMan HTTP-сервер, оборачивающий pySim, работающий с ридером через PC/SC или serial и раздающий сам PWA (откройте <code class="font-mono text-sm">http://127.0.0.1:8080</code>).</p>
<h3 id="prerequisites" class="text-lg font-medium mb-2">7.1 Требования</h3>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Python 3.8+</strong> с <code class="font-mono text-sm">pip</code></li>
<li><strong>Git</strong></li>
<li><strong>Смарт-картридер</strong> (PC/SC или serial/FTDI). Предпочтителен PC/SC; на Linux требуются <code class="font-mono text-sm">pcsc-lite</code> + <code class="font-mono text-sm">ccid</code></li>
<li><strong>Только Windows</strong> — используйте <strong>Python 3.10&ndash;3.13</strong> (рекомендуется 3.13): <code class="font-mono text-sm">pyscard</code> (обёртка драйвера PC/SC) поставляет готовые wheels для этих версий. На Python 3.9 / 3.14 pip собирает <code class="font-mono text-sm">pyscard</code> из исходников, для чего требуются Microsoft C++ Build Tools (&laquo;Desktop development with C++&raquo;). Мост SMPP (<code class="font-mono text-sm">smpp.twisted3</code>) на Windows намеренно не устанавливается, поэтому для Python 3.10&ndash;3.13 C++ Build Tools не нужны.</li>
</ul>
<h3 id="quickstart-linux" class="text-lg font-medium mb-2">7.2 Быстрый старт — Linux / macOS</h3>
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">git clone https://github.com/anttro/otaman.git
cd otaman
chmod +x setup.sh start.sh
./setup.sh # создаёт .venv, устанавливает pysim + сервер (однократно)
./start.sh # запускает сервер (PWA + API, автоопределение ридера)</pre>
<h3 id="quickstart-windows" class="text-lg font-medium mb-2">7.3 Быстрый старт — Windows</h3>
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">git clone https://github.com/anttro/otaman.git
cd otaman
setup.bat # создаёт .venv, устанавливает pysim + сервер (однократно)
start.bat # запускает сервер (PWA + API)</pre>
<h3 id="helper-scripts" class="text-lg font-medium mb-2">7.4 Вспомогательные скрипты</h3>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Скрипт</th><th class="text-left py-1 px-2">Назначение</th></tr></thead>
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">setup.sh / setup.bat</td><td class="py-1 px-2">Создаёт <code class="font-mono text-sm">.venv/</code>, устанавливает pySim и сервер. Запускается один раз после клонирования.</td></tr>
<tr><td class="py-1 px-2 font-mono">start.sh / start.bat</td><td class="py-1 px-2">Запускает сервер из venv (при отсутствии — из глобальной установки).</td></tr>
</tbody>
</table>
<h3 id="reader-autodetect" class="text-lg font-medium mb-2">7.5 Автоопределение ридера</h3>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>PC/SC (Linux)</strong><code class="font-mono text-sm">start.sh</code> передаёт <code class="font-mono text-sm">-p 0</code>, если запущен демон <code class="font-mono text-sm">pcscd</code></li>
<li><strong>PC/SC (Windows)</strong><code class="font-mono text-sm">start.bat</code> всегда использует <code class="font-mono text-sm">-p 0</code> (PC/SC встроен в Windows)</li>
<li><strong>Резерв сервера</strong> — при запуске без аргументов ридера сервер сам опрашивает PC/SC-ридер при старте (3 попытки с интервалом 2&nbsp;с)</li>
<li><strong>Serial-ридеры</strong> — запустите сервер вручную с <code class="font-mono text-sm">-d /dev/ttyUSB0</code> (Linux)</li>
</ul>
<p class="text-sm mb-3">Если карта отсутствует, вкладка &laquo;Картридер&raquo; показывает &laquo;Карта не обнаружена. Вставьте карту и нажмите Подключить карту&raquo;.</p>
<h3 id="manual-install" class="text-lg font-medium mb-2">7.6 Ручная установка</h3>
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3"># Создать и активировать venv
python3 -m venv .venv
source .venv/bin/activate # Linux/macOS
# .venv\Scripts\activate # Windows
# Установить pysim
pip install git+https://github.com/osmocom/pysim.git
# Установить pysim-otaman-server (editable — раздаёт встроенный PWA)
pip install -e .
# Запустить сервер (PWA + API)
pysim-otaman-server --http-port 8080</pre>
<p class="text-sm mb-3">Подключите PC/SC-ридер с SIM-картой и откройте <code class="font-mono text-sm">http://127.0.0.1:8080</code> — PWA и API на одном origin, поэтому CORS не требуется.</p>
<p class="text-sm mb-3">Если PWA раздаётся с публичного HTTPS-хоста (например, <code class="font-mono text-sm">https://otaman.example.com</code>), для доступа к локальному серверу карт нужны два условия: (1) сервер отвечает на preflight заголовком <code class="font-mono text-sm">Access-Control-Allow-Private-Network: true</code> (pysim-otaman-server &ge; 1.6.1 делает это автоматически), и (2) браузеру должно быть разрешено обращаться к локальной сети &mdash; в Chrome/Edge/Vivaldi: Настройки сайта &rarr; Доступ к локальной сети &rarr; разрешить сайт (или подтвердить запрос). Без разрешения браузера запрос к <code class="font-mono text-sm">127.0.0.1</code> блокируется ещё до отправки preflight.</p>
<section class="mb-10">
<h2 id="compatibility" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">8. Совместимость версий</h2>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">PWA (OTAMan)</th><th class="text-left py-1 px-2">Сервер</th><th class="text-left py-1 px-2">Статус</th></tr></thead>
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">1.x.x</td><td class="py-1 px-2 font-mono">1.x.x</td><td class="py-1 px-2">&#9989; Совместимы</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">1.x.x</td><td class="py-1 px-2 font-mono">0.x.x</td><td class="py-1 px-2">&#10060; Устарел &mdash; обновите сервер</td></tr>
<tr><td class="py-1 px-2 font-mono">1.x.x</td><td class="py-1 px-2 font-mono">2.x.x+</td><td class="py-1 px-2">&#9888;&#65039; Сервер новее &mdash; обновите PWA</td></tr>
</tbody>
</table>
<p class="text-sm">PWA проверяет версию сервера при подключении через <code class="font-mono text-sm">GET /api/version</code> и предупреждает о несовместимости.</p>
</main>
</div>
<script>
(function () {
if (localStorage.getItem('theme') === 'dark' ||
(!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
var toc = document.getElementById('toc');
var main = document.querySelector('main');
if (!toc || !main) return;
var headings = Array.prototype.filter.call(main.querySelectorAll('h2, h3, h4'), function (h) {
return /^\d/.test(h.textContent.trim());
});
var stack = [document.createElement('ol')];
stack[0].className = 'space-y-1 list-none';
var lastLevel = 2;
headings.forEach(function (h) {
var level = parseInt(h.tagName.charAt(1), 10);
var li = document.createElement('li');
var a = document.createElement('a');
a.href = '#' + h.id;
a.textContent = h.textContent;
li.appendChild(a);
if (level > lastLevel) {
var ul = document.createElement('ul');
ul.className = 'pl-4 mt-1 space-y-1 list-none';
var parent = stack[stack.length - 1].lastElementChild;
parent.appendChild(ul);
stack.push(ul);
} else if (level < lastLevel) {
while (stack.length > 1 && level < lastLevel) { stack.pop(); lastLevel--; }
}
stack[stack.length - 1].appendChild(li);
lastLevel = level;
});
toc.appendChild(stack[0]);
})();
</script>
</body>
</html>
+562
View File
@@ -0,0 +1,562 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OTAMan — Help</title>
<link rel="stylesheet" href="style.css">
</head>
<body class="bg-neutral-50 dark:bg-slate-900 text-gray-800 dark:text-slate-200">
<div class="flex">
<aside class="sticky top-0 h-screen overflow-y-auto w-72 shrink-0 border-r border-gray-300 dark:border-slate-700 px-4 py-6">
<a href="index.html" class="block text-xs text-gray-400 hover:text-gray-600 dark:text-slate-500 dark:hover:text-slate-300 mb-4">&larr; Back to app</a>
<nav id="toc" class="text-sm"></nav>
</aside>
<main class="flex-1 min-w-0 px-6 py-8">
<h1 class="text-2xl font-bold text-heading mb-6">OTAMan <span class="text-xs text-gray-400 dark:text-slate-500 ml-2">Documentation</span></h1>
<section class="mb-10">
<h2 id="overview" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">1. Overview</h2>
<p class="mb-3">OTAMan is a Progressive Web App (PWA) for building APDU commands, assembling SCP80 secured packets, browsing the SIM Toolkit (STK) menu, and simulating a real network environment against a SIM/USIM/UICC card via a PC/SC reader.</p>
<p class="mb-3">The application is a single static <code class="font-mono text-sm">index.html</code> file. All computation runs in the browser; the card is accessed through a local HTTP server (<code class="font-mono text-sm">pysim-otaman-server</code>) that wraps the <a href="https://osmocom.org/projects/pysim" class="text-blue-600 dark:text-blue-400 hover:underline">pySim</a> library.</p>
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">Browser (OTAMan PWA) &rarr; HTTP :8080 &rarr; pysim-otaman-server &rarr; pySim &rarr; PC/SC &rarr; card reader &rarr; UICC/SIM</pre>
<p class="mb-3">Standards referenced across the application:</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li>ETSI TS 102 221 — UICC-Terminal interface (CLA 00, USIM files)</li>
<li>ETSI TS 102 222 — Administrative commands</li>
<li>ETSI TS 102 223 — Card Application Toolkit (CAT, proactive commands)</li>
<li>ETSI TS 102 225 — Secured packet structure for (U)SIM toolkit</li>
<li>ETSI TS 102 226 — Remote APDU structure for UICC based applications</li>
<li>ETSI TS 151 011 — SIM-ME interface (CLA A0)</li>
<li>3GPP TS 131 111 — USIM Application Toolkit (USAT)</li>
<li>3GPP TS 31.102 — USIM application characteristics</li>
<li>3GPP TS 23.038 — GSM 7-bit alphabet and DCS</li>
<li>3GPP TS 24.008 / 24.301 / 24.501 — NAS cause codes</li>
<li>GlobalPlatform Card Specification v2.3.1</li>
<li>GlobalPlatform GPC v2.2 Amendment B v1.1 — Remote Application Management over HTTP</li>
<li>ISO/IEC 7816-4 — commands for interchange</li>
<li>ISO/IEC 9797-1 — MAC algorithms</li>
</ul>
<h3 id="interface" class="text-lg font-medium mb-2">1.1 Interface</h3>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Header</strong> — the app version, an <strong>INSTALL PWA</strong> button (shown when the browser offers installation, enabling offline use), links to the project on GitHub and to this help, an <strong>EN/RU</strong> language toggle, and a dark/light <strong>theme</strong> toggle.</li>
<li>Language and theme choices are stored in <code class="font-mono text-sm">localStorage</code> and persist across reloads.</li>
<li>Top-level tabs: <strong>Remote APDU</strong> (<strong>SIM RFM</strong>, <strong>USIM RFM</strong>, <strong>Expanded Script</strong>, <strong>RAM/GP</strong>, <strong>HTTP OTA</strong>, <strong>C-APDU Parser</strong>, <strong>Response parser</strong>), <strong>SCP80</strong> (<strong>Secured Packet</strong>, <strong>Cards</strong>, <strong>RAM</strong>), <strong>Profiler</strong> (list tabs <strong>Profiles</strong>, <strong>Card snapshots</strong>, <strong>Custom files</strong>), <strong>Card reader</strong> (<strong>File manager</strong>, <strong>pySim command line</strong>, <strong>Raw APDU</strong>), and <strong>Phone simulator</strong>.</li>
<li>The <strong>help</strong> link opens this documentation at the section matching the current view (e.g. the Profiler tab opens &sect;5).</li>
</ul>
<section class="mb-10">
<h2 id="c-apdu" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">2. Remote APDU tab</h2>
<p class="mb-3">Builds command APDUs (C-APDUs). Seven sub-tabs cover different card generations, command sets and decoding tools: <strong>SIM RFM</strong>, <strong>USIM RFM</strong>, <strong>Expanded Script</strong>, <strong>RAM/GP</strong>, <strong>HTTP OTA</strong>, <strong>C-APDU Parser</strong>, and <strong>Response parser</strong>.</p>
<h3 id="sim-rfm" class="text-lg font-medium mb-2">2.1 SIM RFM</h3>
<p class="mb-2">CLA = <code class="font-mono text-sm">A0</code> (GSM 11.11 / TS 151 011, ISO 7816-4). Remote File Management for classic SIM cards.</p>
<p class="mb-2">Commands are assembled as a <strong>chain</strong>: press a command button (e.g. <code class="font-mono text-sm">+&nbsp;SELECT</code>) to append a row, fill that row&rsquo;s fields, and the chain preview (above the pack button) updates automatically. Add a <strong>GET RESPONSE</strong> row to fetch data following a SELECT. Press <strong>Pack into Secured packet</strong> to wrap the whole chain into an SCP80 packet.</p>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700">
<th class="text-left py-1 px-2">Command</th><th class="text-left py-1 px-2">INS</th><th class="text-left py-1 px-2">Description</th>
</tr></thead>
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">SELECT</td><td class="py-1 px-2 font-mono">A4</td><td class="py-1 px-2">Select EF/DF by FID, path, AID or chain</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">UPDATE RECORD</td><td class="py-1 px-2 font-mono">DC</td><td class="py-1 px-2">Update a record in a record-oriented EF</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">UPDATE BINARY</td><td class="py-1 px-2 font-mono">D6</td><td class="py-1 px-2">Update binary content at an offset</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">READ RECORD</td><td class="py-1 px-2 font-mono">B2</td><td class="py-1 px-2">Read a record</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">READ BINARY</td><td class="py-1 px-2 font-mono">B0</td><td class="py-1 px-2">Read binary content</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">ERASE BINARY</td><td class="py-1 px-2 font-mono">0E</td><td class="py-1 px-2">Erase binary at an offset</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">ACTIVATE FILE</td><td class="py-1 px-2 font-mono">44</td><td class="py-1 px-2">Activate a file</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">DEACTIVATE FILE</td><td class="py-1 px-2 font-mono">04</td><td class="py-1 px-2">Deactivate a file</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">VERIFY PIN</td><td class="py-1 px-2 font-mono">20</td><td class="py-1 px-2">Verify PIN1 or PIN2</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CHANGE PIN</td><td class="py-1 px-2 font-mono">24</td><td class="py-1 px-2">Change PIN1 or PIN2</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">DISABLE PIN</td><td class="py-1 px-2 font-mono">26</td><td class="py-1 px-2">Disable a PIN</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">ENABLE PIN</td><td class="py-1 px-2 font-mono">28</td><td class="py-1 px-2">Enable a PIN</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">UNBLOCK PIN</td><td class="py-1 px-2 font-mono">2C</td><td class="py-1 px-2">Unblock a PIN with PUK</td></tr>
<tr><td class="py-1 px-2 font-mono">GET RESPONSE</td><td class="py-1 px-2 font-mono">C0</td><td class="py-1 px-2">Fetch data indicated by a preceding <code class="font-mono text-sm">61XX</code>/<code class="font-mono text-sm">9FXX</code> status word</td></tr>
</tbody>
</table>
<h4 id="sim-select-methods" class="font-medium mb-1">SELECT methods</h4>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700">
<th class="text-left py-1 px-2">Method</th><th class="text-left py-1 px-2">P1</th><th class="text-left py-1 px-2">P2</th><th class="text-left py-1 px-2">Input</th>
</tr></thead>
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">By FID</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">2-byte FID (4 hex)</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">By full path from MF</td><td class="py-1 px-2 font-mono">08</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Full path hex from MF</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">By DF name / AID</td><td class="py-1 px-2 font-mono">04</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">AID (application ID)</td></tr>
<tr><td class="py-1 px-2">Chain</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Comma-separated FIDs; a <code class="font-mono text-sm">C0</code> (or <code class="font-mono text-sm">C0:NN</code>) token inserts a GET RESPONSE hop</td></tr>
</tbody>
</table>
<p class="text-sm mb-3">For record commands, the P2 mode is <strong>Absolute (04)</strong>, <strong>Next (02)</strong>, or <strong>Previous (03)</strong>. When a Case&nbsp;4 command is immediately followed by a GET RESPONSE row, the chain builder strips its trailing Le byte automatically (ETSI TS 102 226 &sect;5.1.1). A conversion panel is embedded in the right column (IMSI, MSISDN, ICCID, SPN, PLMN, nibble swap). See <a href="#conversion" class="text-blue-600 dark:text-blue-400 hover:underline">&sect;2.5</a>.</p>
<h3 id="usim-rfm" class="text-lg font-medium mb-2">2.2 USIM RFM</h3>
<p class="mb-2">CLA = <code class="font-mono text-sm">00</code> (ETSI TS 102 221). Same chain builder and command set as SIM. Differences:</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>SELECT</strong> requests FCP by default (P2=<code class="font-mono text-sm">04</code>) and appends Le=<code class="font-mono text-sm">00</code>; a <strong>Silent (P2=0C)</strong> checkbox selects without requesting FCP (no Le, no response data).</li>
<li><strong>By path</strong> offers <strong>from MF</strong> (P1=<code class="font-mono text-sm">08</code>) or <strong>from current DF</strong> (P1=<code class="font-mono text-sm">09</code>).</li>
<li>Each SELECT hop in a chain requests FCP unless marked silent.</li>
</ul>
<h3 id="ber-tlv" class="text-lg font-medium mb-2">2.3 Expanded Script</h3>
<p class="mb-2">Builds the Expanded Remote Application data format per ETSI TS 102 226 &sect;5.2.1.</p>
<h4 id="ber-format" class="font-medium mb-1">Format</h4>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Definite (AA)</strong>: <code class="font-mono text-sm">AA</code> + length + Command TLVs</li>
<li><strong>Indefinite (AE)</strong>: <code class="font-mono text-sm">AE</code> + <code class="font-mono text-sm">80</code> + Command TLVs + <code class="font-mono text-sm">00 00</code></li>
</ul>
<h4 id="ber-command-tlvs" class="font-medium mb-1">Command TLVs</h4>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700">
<th class="text-left py-1 px-2">Type</th><th class="text-left py-1 px-2">Tag</th><th class="text-left py-1 px-2">Description</th>
</tr></thead>
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">C-APDU</td><td class="py-1 px-2 font-mono">22</td><td class="py-1 px-2">Raw APDU hex</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Immediate Action</td><td class="py-1 px-2 font-mono">81</td><td class="py-1 px-2">Proactive command or action indicator</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Error Action</td><td class="py-1 px-2 font-mono">82</td><td class="py-1 px-2">Conditional error recovery with action indicator or proactive command</td></tr>
<tr><td class="py-1 px-2">Script Chaining</td><td class="py-1 px-2 font-mono">83</td><td class="py-1 px-2">Multi-packet script execution with First/Intermediary/Last flags</td></tr>
</tbody>
</table>
<p class="text-sm mb-3">The Immediate Action builder offers an action indicator (<code class="font-mono text-sm">81</code>/<code class="font-mono text-sm">82</code>), a structured proactive command builder (REFRESH, DISPLAY TEXT, PLAY TONE with auto-generated COMPREHENSION-TLV objects), or a freeform hex input.</p>
<h4 id="ber-error-action" class="font-medium mb-2 text-base">Error Action TLV (Tag 82)</h4>
<p class="text-sm mb-2">Error recovery per TS 102 226 §5.2.1.3 — one of four forms:</p>
<ul class="text-sm list-disc pl-5 mb-2">
<li><strong>Proactive command:</strong> COMPREHENSION-TLV set with DISPLAY TEXT or PLAY TONE (only these two are allowed in an Error Action, TS 102 226 Table 5.9)</li>
<li><strong>No action:</strong> <code class="font-mono text-sm">82 00</code></li>
<li><strong>Reference to EFRMA record:</strong> <code class="font-mono text-sm">82 01 &lt;ref&gt;</code> with record number <code class="font-mono text-sm">01</code><code class="font-mono text-sm">7F</code></li>
<li><strong>Custom hex:</strong> arbitrary TLV value</li>
</ul>
<h4 id="ber-script-chaining" class="font-medium mb-2 text-base">Script Chaining TLV (Tag 83)</h4>
<p class="text-sm mb-2">Multi-packet script execution with context preservation:</p>
<ul class="text-sm list-disc pl-5 mb-2">
<li><strong>Chaining Flags:</strong> <code class="font-mono text-sm">01</code> first script (delete chaining info on reset), <code class="font-mono text-sm">11</code> first script (keep chaining info across reset, RFM only), <code class="font-mono text-sm">02</code> subsequent script (more to follow), <code class="font-mono text-sm">03</code> subsequent script (last)</li>
<li><strong>Script ID:</strong> Correlation identifier across packets (1-4 bytes hex, auto-increment hints provided)</li>
<li><strong>Additional Data:</strong> Extended chaining information (optional hex)</li>
<li><strong>Context Preservation:</strong> UICC keeps security/transaction state open across chained scripts</li>
</ul>
<h4 id="expanded-response" class="font-medium mb-2 text-base">Response decoding (TS 102 226 §5.2.2)</h4>
<p class="text-sm mb-2">Incoming Proof-of-Receipt responses are decoded by the server — expanded Remote Application response data (TS 102 226 §5.2.2) or the compact format. The Secured Packet view shows the outcome after <strong>Send to Card</strong> (see <a href="#secured-packet" class="text-blue-600 dark:text-blue-400 hover:underline">&sect;3.1</a>): the PoR status (TAR, counter, raw PoR), with the last command&rsquo;s status word and response data filled into the <strong>Response parser</strong> pill under Remote APDU.</p>
<h3 id="ram-gp" class="text-lg font-medium mb-2">2.4 RAM/GP</h3>
<p class="mb-2">CLA = <code class="font-mono text-sm">80</code> (GlobalPlatform Card Specification v2.3.1). Remote Application Management commands for card content management. Built with the same chain builder as SIM/USIM: add rows, fill fields, and the chain preview updates automatically.</p>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700">
<th class="text-left py-1 px-2">Command</th><th class="text-left py-1 px-2">INS</th><th class="text-left py-1 px-2">P1</th><th class="text-left py-1 px-2">Description</th>
</tr></thead>
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INSTALL [for load]</td><td class="py-1 px-2 font-mono">E6</td><td class="py-1 px-2 font-mono">02</td><td class="py-1 px-2">Register a load file for loading</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INSTALL [for install]</td><td class="py-1 px-2 font-mono">E6</td><td class="py-1 px-2 font-mono">0C</td><td class="py-1 px-2">Install an application or SD</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INSTALL [make selectable]</td><td class="py-1 px-2 font-mono">E6</td><td class="py-1 px-2 font-mono">08</td><td class="py-1 px-2">Make an application selectable</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INSTALL [registry update]</td><td class="py-1 px-2 font-mono">E6</td><td class="py-1 px-2 font-mono">40</td><td class="py-1 px-2">Update registry entries</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INSTALL [extradition]</td><td class="py-1 px-2 font-mono">E6</td><td class="py-1 px-2 font-mono">10</td><td class="py-1 px-2">Extradition between SDs</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">LOAD</td><td class="py-1 px-2 font-mono">E8</td><td class="py-1 px-2 font-mono">80</td><td class="py-1 px-2">Load executable code block (P1=80 last block, block number in P2)</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">DELETE</td><td class="py-1 px-2 font-mono">E4</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Delete application or SD (P2=00 AID only / 80 AID + related objects)</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">GET STATUS</td><td class="py-1 px-2 font-mono">F2</td><td class="py-1 px-2 font-mono">80/40/20/10</td><td class="py-1 px-2">Get card status</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">GET DATA</td><td class="py-1 px-2 font-mono">CA</td><td class="py-1 px-2 font-mono">tag</td><td class="py-1 px-2">Read card data objects</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">STORE DATA</td><td class="py-1 px-2 font-mono">E2</td><td class="py-1 px-2 font-mono">00/40/80/C0/E0</td><td class="py-1 px-2">Store data (key, certificate, &hellip;)</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">SET STATUS</td><td class="py-1 px-2 font-mono">F0</td><td class="py-1 px-2 font-mono">80/40/60</td><td class="py-1 px-2">Lifecycle state management</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">EXTERNAL AUTHENTICATE</td><td class="py-1 px-2 font-mono">82</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">SCP host authentication</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INTERNAL AUTHENTICATE</td><td class="py-1 px-2 font-mono">88</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Card challenge-response</td></tr>
<tr><td class="py-1 px-2 font-mono">GET RESPONSE</td><td class="py-1 px-2 font-mono">C0</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Fetch data after a <code class="font-mono text-sm">61XX</code> status (Le configurable)</td></tr>
</tbody>
</table>
<h4 id="ram-privileges" class="font-medium mb-1">Privileges (INSTALL [for install])</h4>
<p class="text-sm mb-2">Three privilege bytes (GP spec Tables 11-7/8/9), encoded as a length-value field inside the INSTALL data:</p>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700">
<th class="text-left py-1 px-2">Bit</th><th class="text-left py-1 px-2">Byte 1</th><th class="text-left py-1 px-2">Byte 2</th><th class="text-left py-1 px-2">Byte 3</th>
</tr></thead>
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">b8</td><td class="py-1 px-2">Security Domain</td><td class="py-1 px-2">Trusted Path</td><td class="py-1 px-2">Receipt Generation</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">b7</td><td class="py-1 px-2">DAP Verification</td><td class="py-1 px-2">Authorized Management</td><td class="py-1 px-2"></td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">b6</td><td class="py-1 px-2">Delegated Management</td><td class="py-1 px-2">Token Verification</td><td class="py-1 px-2"></td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">b5</td><td class="py-1 px-2">Card Lock</td><td class="py-1 px-2">Global Delete</td><td class="py-1 px-2"></td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">b4</td><td class="py-1 px-2">Card Terminate</td><td class="py-1 px-2">Global Lock</td><td class="py-1 px-2"></td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">b3</td><td class="py-1 px-2">Card Reset</td><td class="py-1 px-2">Global Registry</td><td class="py-1 px-2"></td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">b2</td><td class="py-1 px-2">CVM Management</td><td class="py-1 px-2">Final Application</td><td class="py-1 px-2"></td></tr>
<tr><td class="py-1 px-2 font-mono">b1</td><td class="py-1 px-2">Mandated DAP Verification</td><td class="py-1 px-2"></td><td class="py-1 px-2"></td></tr>
</tbody>
</table>
<h4 id="ram-msl" class="font-medium mb-1">MSL (Minimum Security Level) &mdash; SPI1 byte per TS 102 225</h4>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Value</th><th class="text-left py-1 px-2">Meaning</th></tr></thead>
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">No check</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">11</td><td class="py-1 px-2">RC/CC/DS</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">12</td><td class="py-1 px-2">RC/DS/CC</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">15</td><td class="py-1 px-2">RC/DS/CC + MAC</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">16</td><td class="py-1 px-2">RC/DS/CC + MAC + Cipher</td></tr>
<tr><td class="py-1 px-2 font-mono">19</td><td class="py-1 px-2">RC/DS/CC + MAC + Cipher + DS</td></tr>
</tbody>
</table>
<p class="text-sm mb-3">Refer to GlobalPlatform v2.3.1 and ETSI TS 102 226 &sect;8.2.1.3.2 for the full GET STATUS P1/P2, GET DATA tag, DELETE P1, STORE DATA P1, and SET STATUS tables.</p>
<h3 id="conversion" class="text-lg font-medium mb-2">2.5 Conversion (SIM/USIM sidebars)</h3>
<ul class="list-disc list-inside text-sm space-y-1">
<li><strong>IMSI &rarr; EF.IMSI</strong> — 15-digit IMSI to 9-byte format (TS 31.102 &sect;4.2.3).</li>
<li><strong>MSISDN &rarr; BCD</strong> — strip <code class="font-mono text-sm">+</code>, pad odd length with <code class="font-mono text-sm">f</code>, swap nibbles.</li>
<li><strong>ICCID &rarr; hex</strong> — nibble-swap the ICCID string.</li>
<li><strong>Provider Name &rarr; SPN</strong> — GSM 7-bit packed, UCS2 non-BMP, or UCS2 BMP (TS 31.102 &sect;4.2.5, TS 23.038).</li>
<li><strong>PLMN &rarr; EF_PLMNsel / PLMNwAcT</strong> — 3-byte BCD + optional access technology selector.</li>
<li><strong>Nibble swap</strong> — swap nibble pairs of an even-length hex string.</li>
</ul>
<h3 id="c-apdu-parser" class="text-lg font-medium mb-2">2.6 C-APDU Parser</h3>
<p class="text-sm mb-3">Pastes raw APDU hex and renders a collapsible tree. It auto-detects the container: an <strong>Expanded Script</strong> (leading <code class="font-mono text-sm">AA</code> or <code class="font-mono text-sm">AE80</code>, decoded per ETSI TS 102 226 &sect;5.2.1) or a <strong>Compact C-APDU chain</strong> (a sequence of ISO 7816 C-APDUs). Each node shows its label, hex and a short description; parent nodes expand to reveal their sub-elements.</p>
<h3 id="http-ota" class="text-lg font-medium mb-2">2.7 HTTP OTA</h3>
<p class="text-sm mb-3">Builds the Remote Application Management over HTTP payloads defined in GlobalPlatform <strong>GPC v2.2 Amendment B v1.1</strong> (&sect;4.7). Two modes:</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Trigger (Push SMS)</strong> — administration session triggering parameters (<code class="font-mono text-sm">81 &gt; 83 &gt; 84/[85]/[86]/89</code>, Table 4-3). This is the message that asks the card's Security Domain to dial out and start an HTTP session.</li>
<li><strong>Store (SD admin params)</strong> — writes the same parameters as card (Security Domain) data via <strong>STORE DATA in TLV mode</strong> (<code class="font-mono text-sm">80 E2 90 00</code>, P1=90 = last block + BER-TLV per GP v2.2 Amendment B v1.1.3), wrapped in tag <code class="font-mono text-sm">85</code> (or <code class="font-mono text-sm">A5</code>) per Table 4-4.</li>
</ul>
<p class="text-sm mb-2">Sections mirror the spec tables:</p>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Section</th><th class="text-left py-1 px-2">Tag</th><th class="text-left py-1 px-2">Contents</th></tr></thead>
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Connection parameters</td><td class="py-1 px-2 font-mono">84</td><td class="py-1 px-2">Any COMPREHENSION-TLV needed to open the TCP connection (OPEN CHANNEL per TS 102 223): Device Identities <code class="font-mono text-sm">02</code>, Alpha <code class="font-mono text-sm">80</code>, Bearer <code class="font-mono text-sm">01</code>, vendor TLVs. Row editor + presets, editable hex.</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Security parameters</td><td class="py-1 px-2 font-mono">85</td><td class="py-1 px-2">Table 4-6: LV PSK Identity (text), LV Key version/KID. Identifies the PSK TLS key (RFC 4279).</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Retry policy</td><td class="py-1 px-2 font-mono">86</td><td class="py-1 px-2">Table 4-7: retry counter (2 bytes, e.g. <code class="font-mono text-sm">B000</code>), retry waiting delay as the TS 102 223 timer TLV (<code class="font-mono text-sm">25 03 HH MM SS</code>), optional vendor-specific report-failure TLV.</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">HTTP POST</td><td class="py-1 px-2 font-mono">89</td><td class="py-1 px-2">Tables 4-8/9/10: Host header (<code class="font-mono text-sm">8A</code>), X-Admin-From agent ID (<code class="font-mono text-sm">8B</code>), URI (<code class="font-mono text-sm">8C</code>) — text converted to octets.</td></tr>
</tbody>
</table>
<p class="text-sm mb-2"><strong>Command Scripting template</strong> (checkbox) wraps the whole <code class="font-mono text-sm">81</code> triggering command in the definite-length Expanded Remote Application data format (<code class="font-mono text-sm">AA</code>, ETSI TS 102 226 &sect;5.2.1) for TARs that process the expanded format (RAM-over-HTTP &sect;4.7).</p>
<p class="text-sm mb-2"><strong>Pack into Secured packet</strong> sends the built payload to the SCP80 tab for SPI/counter filling &mdash; insert the TAR the SD listens on (typically the OTASD TAR) there.</p>
<h3 id="response-parser" class="text-lg font-medium mb-2">2.8 Response parser</h3>
<p class="mb-3">Decodes a raw command response: pick the command that was sent, enter the SW (e.g. <code class="font-mono text-sm">9000</code>) and the response data hex, then press <strong>Decode</strong>. The fields are also auto-filled with the last command&rsquo;s status word and response data after a successful &ldquo;Send to Card&rdquo; (see <a href="#secured-packet" class="text-blue-600 dark:text-blue-400 hover:underline">&sect;3.1</a>).</p>
<ul class="list-disc list-inside text-sm space-y-1">
<li><strong>Command</strong> — SIM/USIM group (SELECT, STATUS, READ/UPDATE, PIN ops, CAT commands like TERMINAL PROFILE/ENVELOPE/FETCH/TERMINAL RESPONSE, MANAGE CHANNEL, &hellip;) or RAM/GP group (INSTALL, LOAD, DELETE, GET/STORE DATA, auth, SCP commands).</li>
<li><strong>SW decode</strong> — status words resolved against generic, UICC (TS 102 221), and GlobalPlatform maps, with context auto-detected.</li>
<li><strong>Privilege decode</strong> — GET DATA / INSTALL response payloads decode the privilege bytes into human-readable flags.</li>
<li><strong>Response data</strong> — raw hex rendered and interpreted per command (e.g. SELECT FCP templates).</li>
</ul>
<section class="mb-10">
<h2 id="scp80" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">3. SCP80 tab</h2>
<p class="mb-3">The <strong>SCP80</strong> top-level tab groups the SCP80-related views. It is switched by three pills: <strong>Secured Packet</strong>, <strong>Cards</strong>, and <strong>RAM</strong>. Assembles SCP80 secured packets per ETSI TS 102 225.</p>
<h3 id="secured-packet" class="text-lg font-medium mb-2">3.1 Secured Packet</h3>
<p class="mb-2">Builds SCP80 secured packets per ETSI TS 102 225.</p>
<h4 id="packet-structure" class="font-medium mb-1">Packet structure</h4>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Field</th><th class="text-left py-1 px-2">Size</th><th class="text-left py-1 px-2">Description</th></tr></thead>
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CPI</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Command Packet Identifier (02)</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CPL</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Command Packet Length</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CHI</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Command Header Identifier (01)</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CHL</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Command Header Length</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">SPI</td><td class="py-1 px-2">2</td><td class="py-1 px-2">Security Parameter Indicator</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">KIc</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Key Identifier for ciphering</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">KID</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Key Identifier for MAC</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">TAR</td><td class="py-1 px-2">3</td><td class="py-1 px-2">Toolkit Application Reference</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CNTR</td><td class="py-1 px-2">5</td><td class="py-1 px-2">Replay counter</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">PCNTR</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Padding counter</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">RC/CC/DS</td><td class="py-1 px-2">8</td><td class="py-1 px-2">Cryptographic Checksum / MAC</td></tr>
<tr><td class="py-1 px-2 font-mono">Secured Data</td><td class="py-1 px-2">var.</td><td class="py-1 px-2">Padded APDU (encrypted if required)</td></tr>
</tbody>
</table>
<h4 id="packet-crypto" class="font-medium mb-1">Crypto</h4>
<ul class="list-disc list-inside text-sm space-y-1">
<li><strong>3DES-CBC</strong> encryption (zero ICV), 8/16/24-byte keys &mdash; deprecated since Rel-18, still supported for backwards compatibility</li>
<li><strong>AES-CBC</strong> encryption (zero ICV, zero-padded to 16), 16/24/32-byte keys (TS 102 225 &sect;5.1.2, KIc <code class="font-mono text-sm">x2</code>)</li>
<li><strong>Retail MAC</strong> (ISO 9797-1 MAC algorithm 3) for the DES/3DES cryptographic checksum</li>
<li><strong>AES-CMAC</strong> (NIST SP 800-38B, truncated to 8 octets) for the AES cryptographic checksum (TS 102 225 &sect;5.1.3.1, KID <code class="font-mono text-sm">x2</code>)</li>
<li>AES requires a replay-protected counter: SPI1 bits b5&nbsp;b4 must be <code class="font-mono text-sm">10</code> (counter higher) or <code class="font-mono text-sm">11</code> (counter +1) per TS 102 225 &sect;5.1.2/&sect;5.1.3.1</li>
<li>Padding byte configurable (<code class="font-mono text-sm">00</code> default, or <code class="font-mono text-sm">FF</code>)</li>
</ul>
<p class="text-sm mb-3">A &ldquo;Verify vs pySim&rdquo; button cross-checks the assembled packet against pySim&rsquo;s reference <code class="font-mono text-sm">OtaDialectSms.encode_cmd</code>. A &ldquo;Send to Card&rdquo; button delivers it via SMS-PP-DOWNLOAD ENVELOPE (when connected to the server). The returned Proof of Receipt is decoded and shown as a PoR status line (status, TAR, counter, raw PoR); the last command&rsquo;s status word and response data are filled into the <strong>Response parser</strong> tab, and a successful PoR advances the replay counter and clears the packet.</p>
<h3 id="cards" class="text-lg font-medium mb-2">3.2 Cards</h3>
<p class="mb-2">Stores card presets locally in the browser (<code class="font-mono text-sm">localStorage</code>) so the Secured Packet view can auto-fill keys and parameters.</p>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Field</th><th class="text-left py-1 px-2">Description</th></tr></thead>
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Name</td><td class="py-1 px-2">Human-readable label</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">ICCID</td><td class="py-1 px-2">Optional card identifier</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">KIc / KID</td><td class="py-1 px-2">Key and algorithm indicators (e.g. 15 = index 1, 3DES-CBC2; x2 = AES)</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">SPI1 / SPI2</td><td class="py-1 px-2">Security Parameter Indicators</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">TAR</td><td class="py-1 px-2">Toolkit Application Reference</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Counter</td><td class="py-1 px-2">Replay counter (5 bytes)</td></tr>
<tr><td class="py-1 px-2">KIc key / KID key</td><td class="py-1 px-2">16/24/32 hex chars (8/16/24-byte 3DES) or 32/48/64 hex chars (16/24/32-byte AES) keys</td></tr>
</tbody>
</table>
<p class="text-sm mb-3">Presets can be shared with <strong>Export as JSON</strong> and <strong>Export to file</strong>, and restored with <strong>Import from file</strong>, <strong>Paste &amp; import</strong>, or <strong>Import JSON from clipboard</strong>. The selected card preset auto-fills the Secured Packet form.</p>
<h3 id="ram" class="text-lg font-medium mb-2">3.3 RAM</h3>
<p class="mb-2">Delivers Remote Application Management operations as SCP80 secured packets via SMS-PP-DOWNLOAD ENVELOPE. The card must support SCP03 (AES or 3DES). A saved card preset from the <strong>Cards</strong> sub-tab provides the SPI, keys, TAR, and counter.</p>
<h4 id="ram-operations" class="font-medium mb-1">Operations</h4>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Operation</th><th class="text-left py-1 px-2">Description</th></tr></thead>
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Explore Card (all GP data)</td><td class="py-1 px-2">Queries GET STATUS for ISD, Applications, ELFs, and ELF Modules, plus GET DATA FF21 for memory info. Results appear in an explorer view with per-item <strong>Delete</strong> buttons.</td></tr>
<tr><td class="py-1 px-2">Install Package (.cap file)</td><td class="py-1 px-2">Sends a <code class="font-mono text-sm">.cap</code> file to the card via the server: INSTALL[for load] &rarr; LOAD &times;N &rarr; INSTALL[for install (+make selectable)].</td></tr>
</tbody>
</table>
<h4 id="ram-explorer" class="font-medium mb-1">Explorer View</h4>
<p class="text-sm mb-2">After &ldquo;Explore Card&rdquo; runs, the explorer displays:</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>ISD</strong> &mdash; AID, lifecycle, privileges (no delete; the ISD cannot be removed)</li>
<li><strong>Applications</strong> &mdash; AID, lifecycle, privileges, associated ELF/SD. Each has a <strong>Delete</strong> button (GP <code class="font-mono text-sm">DELETE</code> by AID).</li>
<li><strong>Executable Load Files</strong> &mdash; AID, lifecycle, version, module AIDs. Each has <strong>Delete</strong> (ELF only) and <strong>Delete All</strong> (cascade: ELF + modules + installed Applications, P2=<code class="font-mono text-sm">0x80</code>) buttons.</li>
</ul>
<p class="text-sm mb-3">Delete confirms via a browser prompt before sending the GP <code class="font-mono text-sm">DELETE</code> command via SCP80. The explorer auto-refreshes after a successful deletion.</p>
<section class="mb-10">
<h2 id="card-reader" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">4. Card reader (pySim) tab</h2>
<p class="mb-3">Connects to a local <a href="https://github.com/anttro/otaman" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> for live card operations: enter the server URL (default <code class="font-mono text-sm">http://127.0.0.1:8080</code>) and press <strong>Connect</strong>. The status area shows the reader/card state, and <strong>Equip card</strong> (re)initializes the card after insertion. Sub-tabs: <strong>File manager</strong>, <strong>pySim command line</strong>, and <strong>Raw APDU</strong>. The <strong>Profiler</strong> and <strong>Phone simulator</strong> are separate top-level tabs.</p>
<h3 id="file-manager" class="text-lg font-medium mb-2">4.1 File manager</h3>
<p class="text-sm mb-2">The file system tree is displayed on the left; selecting a file opens its detail pane on the right. Entries are grouped with DFs above EFs and sorted by <strong>FID</strong> or symbolic <strong>Name</strong> (pills above the tree; the choice is remembered in <code class="font-mono text-sm">localStorage</code>). Selecting a file also shows its FID, file type, size / record layout and the decoded FCI above the content pane.</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Read</strong> — reads the selected file (auto-detects transparent vs record files)</li>
<li><strong>Edit</strong> — modify hex data, <strong>Save</strong> to write back (or <strong>Cancel</strong>)</li>
<li><strong>Raw / Decoded</strong> — toggle between hex dump and pySim-decoded JSON</li>
<li><strong>Probe all files</strong> — walks the whole tree (including custom files) and marks every entry present (normal) or absent (red ✗, no expand arrow); empty-but-present DFs show <code class="font-mono text-sm">(empty)</code>. Shows progress <em>N / total</em>, can be stopped, and finishes with a present/absent summary. Files are only verified when expanded or probed — browsing stays lazy.</li>
</ul>
<h3 id="pysim-cmdline" class="text-lg font-medium mb-2">4.2 pySim command line</h3>
<p class="text-sm mb-3">Execute any pySim-shell command with usage hints (300&nbsp;ms) and autocomplete.</p>
<h3 id="raw-apdu" class="text-lg font-medium mb-2">4.3 Raw APDU</h3>
<p class="text-sm mb-3">Send an arbitrary APDU and view the raw response.</p>
<h3 id="usage-scenarios" class="text-lg font-medium mb-2">4.4 Usage scenarios</h3>
<h4 id="scenario-a" class="font-medium mb-1">Scenario A &mdash; Working with files not in pySim&rsquo;s model (Custom files)</h4>
<ol class="list-decimal list-inside text-sm space-y-1 mb-3">
<li>Obtain the FID of the target file (vendor documentation or ATR/file-system analysis; such files are often not in public specs).</li>
<li>Open the <strong>Card reader</strong> tab &rarr; <strong>Custom files</strong> sub-tab.</li>
<li>Enter the full path (e.g. <code class="font-mono text-sm">3F00/7F20/6F46</code>) and an alias (e.g. <code class="font-mono text-sm">EF.SPN</code>).</li>
<li>Click <strong>Add</strong> — the file appears in the tree in italics (unverified).</li>
<li>Click the file to verify existence; on success (<code class="font-mono text-sm">9000</code>) it behaves like a normal file.</li>
<li>Read, edit and save hex data; toggle Raw/Decoded views.</li>
<li>Export the custom-file list as JSON to share with other machines.</li>
</ol>
<h4 id="scenario-b" class="font-medium mb-1">Scenario B &mdash; Simulating a real network environment for SIM testing</h4>
<p class="text-sm mb-1"><strong>B.1 Answer PROVIDE LOCAL INFORMATION (PLI)</strong></p>
<ol class="list-decimal list-inside text-sm space-y-1 mb-3">
<li>Open <strong>Phone simulator</strong> &rarr; <strong>PROVIDE LOCAL INFORMATION response data</strong>.</li>
<li>Use the decode/encode forms to set IMEI (<code class="font-mono text-sm">01</code>), Location Info (<code class="font-mono text-sm">00</code>), Access Technology (<code class="font-mono text-sm">06</code>), etc.</li>
<li>Click <strong>Save</strong> — values persist server-side.</li>
<li>Enable <strong>Polling</strong> (interval 30&nbsp;s) so the card issues PLI periodically.</li>
<li>The server injects the dictionary values into each TERMINAL RESPONSE.</li>
<li>Verify in the proactive log: the PLI entry shows the decoded response.</li>
</ol>
<p class="text-sm mb-1"><strong>B.2 Simulate network actions via ENVELOPE (event download)</strong></p>
<ol class="list-decimal list-inside text-sm space-y-1 mb-3">
<li>Check the <strong>subscribed events</strong> list (from SET UP EVENT LIST).</li>
<li>Click <strong>Send</strong> on an event (e.g. Location Status) and fill the form; an <code class="font-mono text-sm">ENVELOPE(Event Download)</code> is sent.</li>
<li>For <strong>Network Rejection</strong>, select registration type &rarr; location fields &rarr; access technology &rarr; rejection cause.</li>
<li>The card may respond with a proactive command, which the chain handler logs and answers automatically.</li>
</ol>
<p class="text-sm mb-1"><strong>B.3 Verify the simulated environment</strong></p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li>The proactive command log shows the full round-trip (command + TERMINAL RESPONSE bytes).</li>
<li>The STATUS button / auto-polling keep the CAT session alive (drain loop).</li>
</ul>
<section class="mb-10">
<h2 id="profiler" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">5. Profiler</h2>
<p class="text-sm mb-2">Verifies that a card matches a named <strong>profile</strong> — an ordered set of rules describing the expected file system and (optionally) file contents. Profiles are stored in <code class="font-mono text-sm">localStorage</code>.</p>
<h4 class="font-medium mb-1">Profile list</h4>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>New profile</strong> — creates an empty ruleset after prompting for a name.</li>
<li><strong>Profile from card</strong> — scans the equipped card and generates one rule per existing file (see below), then opens the editor.</li>
<li><strong>Profile from snapshot</strong> — picks a saved card snapshot and generates one rule per captured file using the same scan options (see below), without a card reader; the profile name is prefilled with the snapshot name.</li>
<li><strong>Import profile</strong> — loads a ruleset from a JSON file (the name is stored inside the JSON).</li>
<li>Each profile row shows its name and creation time, with <strong>Check card ▶</strong>, <strong>Check card snapshot</strong>, <strong>Edit</strong>, <strong>Export</strong> (download JSON), and <strong>Delete</strong> actions.</li>
</ul>
<h4 class="font-medium mb-1">Filesystem rules</h4>
<p class="text-sm mb-2">Rules run sequentially. The editor shows the symbolic pySim name (when known) next to each rule&rsquo;s path; use <strong>Add rule</strong> to append one and <strong>Save</strong> to keep the changes. A filesystem rule is defined by:</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Path</strong> — starts with <code class="font-mono text-sm">MF</code> (e.g. <code class="font-mono text-sm">MF/7F10/6F3A</code>) or an ADF AID (e.g. <code class="font-mono text-sm">A0000000871002/6F07</code>).</li>
<li><strong>FCP/FCI check</strong> — how much of the file control information to verify: <strong>Filetype only (FCP)</strong> (existence + file type), <strong>Filetype + size (FCP)</strong> (adds file size, or record length/count for record files), or <strong>Exact FCI</strong> (adds a byte-for-byte comparison of the raw SELECT response — the FCP template <code class="font-mono text-sm">'62'</code> — catching FID/AID, life-cycle status, security-attribute and proprietary-parameter changes).</li>
<li><strong>File attributes</strong> — file type, size, record length and record count, taken from the FCP template (any may be left unset).</li>
<li><strong>Check contents</strong> (optional) — <strong>Exact</strong> hex equality, or <strong>Mask</strong> where <code class="font-mono text-sm">?</code> is a per-nibble wildcard (a mask with no <code class="font-mono text-sm">?</code> is a prefix match, e.g. <code class="font-mono text-sm">0891</code> for the IMSI MCC/MNC). Record files store a per-record list.</li>
</ul>
<p class="text-sm mb-3"><strong>Check card</strong> runs every rule against the equipped card and shows a live progress line plus a pass/fail report. Each row states exactly what was verified next to the file path (e.g. <em>filetype and size, contents</em> or <em>exact FCI</em>); when some checks pass and others fail, each aspect is marked (<em>filetype ✓, size ✗, contents ✓</em>) with the mismatches detailed below. Mismatched raw data (FCI, contents, record data) is shown as read-only monospace fields — expected above actual, aligned in the same column — for easy comparison; FCI mismatches additionally show a decoded per-parameter comparison (file size, file descriptor/structure, life cycle, FID, SFI, proprietary parameters…). A decoded FCI preview is also shown beside the FCI hex field while editing a rule. If the FCI data is corrupt, whatever was decoded before the faulty part is shown together with an explicit decode-failure note. For record files with a contents mismatch, a <em>matching records: 1-5, 7-10</em> note lists the records that did match. In the report the mismatch fields and FCI comparison columns are labelled <em>expected (profile name)</em> and <em>actual (card ICCID)</em>, and the results header reads <em>Profile verification results for: &lt;profile&gt; &rarr; &lt;card ICCID&gt;</em>. An <strong>Only mismatches</strong> option in the results header hides all passing files and keeps only failures and errors.</p>
<h4 class="font-medium mb-1">&ldquo;Profile from card&rdquo; scan options</h4>
<p class="text-sm mb-2">The scan dialog asks for a profile name and offers a <strong>&ldquo;FCP/FCI check&rdquo;</strong> selector (the same three modes above, default <strong>Filetype + size</strong>) applied to every generated rule, plus an <strong>&ldquo;Ignore contents of files&rdquo;</strong> checklist (all checked by default except <code class="font-mono text-sm">EF.ARR</code>; the header checkbox checks or unchecks the whole list) of frequently-overwritten files whose contents are skipped: <code class="font-mono text-sm">EF.LOCI</code>, <code class="font-mono text-sm">EF.PSLOCI</code>, <code class="font-mono text-sm">EF.EPSLOCI</code>, <code class="font-mono text-sm">EF.5GS3GPPLOCI</code>, <code class="font-mono text-sm">EF.Keys</code>, <code class="font-mono text-sm">EF.KeysPS</code>, <code class="font-mono text-sm">EF.SMS</code>, <code class="font-mono text-sm">EF.Kc</code>, <code class="font-mono text-sm">EF.KcGPRS</code>, <code class="font-mono text-sm">EF.LOCIGPRS</code>, <code class="font-mono text-sm">EF.CBMID</code>, <code class="font-mono text-sm">EF.SMSS</code>, <code class="font-mono text-sm">EF.ACC</code>, <code class="font-mono text-sm">EF.EPSNSC</code>, <code class="font-mono text-sm">EF.START-HFN</code>, <code class="font-mono text-sm">EF.ARR</code>. Two further checked-by-default options <strong>&ldquo;Match first 4 bytes for&rdquo;</strong> <code class="font-mono text-sm">EF.IMSI</code> and <code class="font-mono text-sm">EF.ICCID</code> capture those files&rsquo; contents as a mask of only the first 4 bytes (uncheck for exact matching). A progress line shows <em>N / total files</em> with the current file path while scanning; during the scan the options are hidden and the buttons are locked. Rules are created only for files that actually exist on the card (a FCP template is returned); missing files are skipped. Custom files from the <strong>Custom files</strong> sub-tab are included under the same existence check. The same dialog and options are used by <strong>Profile from snapshot</strong>, which walks the selected snapshot&rsquo;s captured files instead of the card; rules for files whose contents were not captured during the scan get no content check (they are reported as unverifiable when the profile is later checked).</p>
<h4 id="card-snapshots" class="font-medium mb-1">Card snapshots</h4>
<p class="text-sm mb-2">The list view has two tabs &mdash; <strong>Profiles</strong> and <strong>Card snapshots</strong>. A card snapshot is an immutable capture of the card filesystem: for every existing file it stores the path, symbolic name, file type, size (or record length/count), the raw FCI from the SELECT response, and the contents whenever the file is readable (no ignore list, no masking). The ICCID is decoded from EF.ICCID and shown next to the snapshot name. The scan also measures each card command (SELECT, READ BINARY, READ RECORD) from command to response; the snapshot stores min/avg/max per command type and the total scan time, and the view shows these in the summary under the title plus the select/read times per file and the read time per record. Timings are informational only and are not used by checks or comparisons.</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>New snapshot</strong> &mdash; asks for a name and scans the card, then returns to the list.</li>
<li><strong>Import snapshot</strong> &mdash; loads a snapshot from a JSON file.</li>
<li>Each snapshot row has <strong>Open</strong>, <strong>Export</strong>, and <strong>Delete</strong>. <strong>Open</strong> shows all captured data read-only (raw FCI with the decoded FCI, contents); only the snapshot name is editable.</li>
<li><strong>Check card snapshot</strong> on a profile row runs the profile rules against a snapshot you pick from the list, without a card reader. The report is the same as a live check: the actual side is labelled with the snapshot name (<em>actual (snapshot name)</em>) and the header reads <em>Profile verification results for: &lt;profile&gt; &rarr; &lt;snapshot name&gt;</em>; files whose contents were not captured during the scan are reported as unverifiable errors.</li>
<li><strong>Compare snapshots</strong> compares two snapshots offline, exactly like a profile check: pick the <em>master</em> snapshot and the <em>snapshot to check</em>, optionally masking the first 4 bytes of EF.IMSI/EF.ICCID (on by default), and get the same pass/fail report; in that report the header reads <em>Snapshot comparison results: &lt;master&gt; &rarr; &lt;checked&gt;</em> and the mismatch fields and the FCI comparison columns are labeled with the master and checked snapshot names instead of expected/actual. Files present only in the checked snapshot are reported as extra files. Back to list returns to the Card snapshots tab.</li>
</ul>
</section>
<h4 id="custom-files" class="font-medium mb-1">Custom files</h4>
<p class="text-sm mb-3">Add files that pySim&rsquo;s model does not cover: enter the full path (e.g. <code class="font-mono text-sm">3F00/7F20/6F46</code>) and an alias (e.g. <code class="font-mono text-sm">EF.SPN</code>), then press <strong>Add</strong>; added files appear in the File manager tree. Each row has <strong>Edit</strong> (reloads the entry into the form — the button becomes <strong>Save</strong> and a <strong>Cancel</strong> button appears) and <strong>Delete</strong> (no confirmation) buttons. The list persists in <code class="font-mono text-sm">localStorage</code> and can be shared with <strong>Export as JSON</strong> / <strong>Export to file</strong> and restored with <strong>Import from file</strong> / <strong>Paste &amp; import</strong> / <strong>Import JSON from clipboard</strong>.</p>
<section class="mb-10">
<h2 id="proactive-uicc" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">6. Phone simulator</h2>
<p class="text-sm mb-3">Interacts with the Card Application Toolkit session. The view has two pills: <strong>Phone</strong> (STK menu, STATUS and polling, subscribed events, proactive command log) and <strong>TR Config</strong> (response data injected into TERMINAL RESPONSEs for proactive commands).</p>
<h3 id="stk-menu" class="text-lg font-medium mb-2">6.1 STK menu</h3>
<p class="text-sm mb-3">When the card has issued a SET UP MENU command, a &ldquo;STK menu&rdquo; block appears at the top of this view with an emerald <strong>STK: &lt;title&gt;</strong> button that opens the menu overlay (same as the card&rsquo;s STK menu browser). If the card has not set up a menu, the block shows &ldquo;No menu set by the card&rdquo; instead. The menu state is refreshed each time the view is opened. User-interactive proactive commands always get a TERMINAL RESPONSE: the overlay pauses for your choice, and if you neither answer nor press <strong>Timeout</strong>, the server answers with a timeout result after the <code class="font-mono text-sm">--menu-timeout</code> seconds (default 60, <code class="font-mono text-sm">0</code> disables). <strong>Back</strong> and <strong>Timeout</strong> keep the dialogue with the card going: when the card replies with a further proactive command (SELECT ITEM or DISPLAY TEXT) the panel shows it; the cached top menu appears only when the card has nothing more to execute.</p>
<h3 id="subscribed-events" class="text-lg font-medium mb-2">6.2 Subscribed events (SET UP EVENT LIST)</h3>
<p class="text-sm mb-2">The events the card monitors. Each event has a <strong>Send</strong> button that opens a form specific to the event type:</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>No-data events</strong> (User Activity, Idle Screen, Data Available, &hellip;) — one-click notification</li>
<li><strong>Location Status</strong> — dropdown: Normal / Limited / No service (tag <code class="font-mono text-sm">9B</code>)</li>
<li><strong>Access Technology Change</strong> — 13 RAT types (tag <code class="font-mono text-sm">BF</code>)</li>
<li><strong>Network Rejection</strong> — full adaptive form: registration type (LU / GPRS / EPS / 5GS), location fields (MCC, MNC, LAC, RAC, TAC), access technology, and a 53-cause unified rejection cause dropdown covering EMM, GMM, 5GMM and LU causes</li>
</ul>
<p class="text-sm mb-3">Sending an event uses <code class="font-mono text-sm">ENVELOPE(Event Download)</code> per TS 102 223 / TS 131 111.</p>
<h3 id="proactive-log" class="text-lg font-medium mb-2">6.3 Proactive command log</h3>
<p class="text-sm mb-2">Chronological list of fetched proactive commands. Each row shows the elapsed time, type code, name, and a decoded qualifier (for commands that have one). Commands with response data show a <code class="font-mono text-sm">Response:</code> line with the TERMINAL RESPONSE bytes (boilerplate TLVs stripped); PROVIDE LOCAL INFORMATION responses are decoded using the PLI data dictionary decoders.</p>
<h3 id="status-polling" class="text-lg font-medium mb-2">6.4 STATUS polling</h3>
<p class="text-sm mb-3">A <strong>Send STATUS</strong> button issues a manual STATUS (F2). A <strong>Polling</strong> toggle enables background polling: after a configurable idle interval (server CLI <code class="font-mono text-sm">--poll-interval</code>, 1&ndash;255&nbsp;s, default 30&nbsp;s, <code class="font-mono text-sm">0</code> disables polling) the server sends STATUS and handles any pending proactive command. Polling stops and card state resets if the card is removed.</p>
<h3 id="pli-dict" class="text-lg font-medium mb-2">6.5 TR Config &mdash; PROVIDE LOCAL INFORMATION response data</h3>
<p class="text-sm mb-2">Editable hex values for all 22 PLI qualifiers (TS 102 223 &sect;8.6 + TS 131 111). Ten qualifiers have inline decode/encode forms:</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>00</strong> Location Info (MCC, MNC, LAC/TAC, Cell ID)</li>
<li><strong>01</strong> IMEI &middot; <strong>03</strong> Date/Time/TZ &middot; <strong>04</strong> Language &middot; <strong>05</strong> Timing Advance</li>
<li><strong>06</strong> Access Technology &middot; <strong>08</strong> IMEISV &middot; <strong>09</strong> Search Mode</li>
<li><strong>0A</strong> Battery &middot; <strong>0E</strong> Multiple Access Technologies</li>
</ul>
<p class="text-sm mb-3">Values persist server-side until restart. When the card issues PLI, the server injects the dictionary values into the TERMINAL RESPONSE.</p>
</section>
<section class="mb-10">
<h2 id="server" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">7. Server installation</h2>
<p class="mb-3">Live card operations (Card reader tab, Phone simulator, OTA delivery) require the local <a href="https://github.com/anttro/otaman" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> — a small HTTP server bundled with OTAMan that wraps pySim, talks to the reader over PC/SC or serial, and also serves the PWA itself (open <code class="font-mono text-sm">http://127.0.0.1:8080</code>).</p>
<h3 id="prerequisites" class="text-lg font-medium mb-2">7.1 Prerequisites</h3>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Python 3.8+</strong> with <code class="font-mono text-sm">pip</code></li>
<li><strong>Git</strong></li>
<li><strong>Smart card reader</strong> (PC/SC or serial/FTDI). PC/SC is preferable; on Linux it requires <code class="font-mono text-sm">pcsc-lite</code> + <code class="font-mono text-sm">ccid</code></li>
<li><strong>Windows only</strong> — use <strong>Python 3.10&ndash;3.13</strong> (3.13 recommended): <code class="font-mono text-sm">pyscard</code> (the PC/SC driver wrapper) ships precompiled wheels for these versions. On Python 3.9 / 3.14 pip builds <code class="font-mono text-sm">pyscard</code> from source, which requires Microsoft C++ Build Tools (&ldquo;Desktop development with C++&rdquo;). The SMPP bridge (<code class="font-mono text-sm">smpp.twisted3</code>) is intentionally not installed on Windows, so no C++ Build Tools are needed for Python 3.10&ndash;3.13.</li>
</ul>
<h3 id="quickstart-linux" class="text-lg font-medium mb-2">7.2 Quick start — Linux / macOS</h3>
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">git clone https://github.com/anttro/otaman.git
cd otaman
chmod +x setup.sh start.sh
./setup.sh # creates .venv, installs pysim + server (run once)
./start.sh # starts the server (serves PWA + API, auto-detects reader)</pre>
<h3 id="quickstart-windows" class="text-lg font-medium mb-2">7.3 Quick start — Windows</h3>
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">git clone https://github.com/anttro/otaman.git
cd otaman
setup.bat # creates .venv, installs pysim + server (run once)
start.bat # starts the server (serves PWA + API)</pre>
<h3 id="helper-scripts" class="text-lg font-medium mb-2">7.4 Helper scripts</h3>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Script</th><th class="text-left py-1 px-2">Purpose</th></tr></thead>
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">setup.sh / setup.bat</td><td class="py-1 px-2">Creates <code class="font-mono text-sm">.venv/</code>, installs pySim and the server. Run once after cloning.</td></tr>
<tr><td class="py-1 px-2 font-mono">start.sh / start.bat</td><td class="py-1 px-2">Starts the server from the venv (falls back to a global install).</td></tr>
</tbody>
</table>
<h3 id="reader-autodetect" class="text-lg font-medium mb-2">7.5 Reader auto-detection</h3>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>PC/SC (Linux)</strong><code class="font-mono text-sm">start.sh</code> passes <code class="font-mono text-sm">-p 0</code> when the <code class="font-mono text-sm">pcscd</code> daemon is running</li>
<li><strong>PC/SC (Windows)</strong><code class="font-mono text-sm">start.bat</code> always uses <code class="font-mono text-sm">-p 0</code> (PC/SC is built into Windows)</li>
<li><strong>Server fallback</strong> — started without reader arguments, the server itself probes for a PC/SC reader at startup (3 attempts, 2&nbsp;s apart)</li>
<li><strong>Serial readers</strong> — start the server manually with <code class="font-mono text-sm">-d /dev/ttyUSB0</code> (Linux)</li>
</ul>
<p class="text-sm mb-3">If no card is present, the Card reader tab shows &ldquo;No card detected&rdquo;. Insert the card and click <strong>Equip card</strong> to initialize it.</p>
<h3 id="manual-install" class="text-lg font-medium mb-2">7.6 Manual installation</h3>
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3"># Create and activate a venv
python3 -m venv .venv
source .venv/bin/activate # Linux/macOS
# .venv\Scripts\activate # Windows
# Install pysim
pip install git+https://github.com/osmocom/pysim.git
# Install pysim-otaman-server (editable, so it serves the bundled PWA)
pip install -e .
# Start the server (serves PWA + API)
pysim-otaman-server --http-port 8080</pre>
<p class="text-sm mb-3">Connect a PC/SC reader with a SIM card, then open <code class="font-mono text-sm">http://127.0.0.1:8080</code> — the PWA and API share one origin, so no CORS is involved.</p>
<p class="text-sm mb-3">If the PWA is served from a public HTTPS host (e.g. <code class="font-mono text-sm">https://otaman.example.com</code>), two things are required to reach a local card server: (1) the server must answer the preflight with <code class="font-mono text-sm">Access-Control-Allow-Private-Network: true</code> (pysim-otaman-server &ge; 1.6.1 does this automatically), and (2) the browser must be allowed to access the local network &mdash; in Chrome/Edge/Vivaldi: Site settings &rarr; Local network access &rarr; allow the site (or accept the permission prompt). Without the browser permission, the request to <code class="font-mono text-sm">127.0.0.1</code> is blocked before any preflight is sent.</p>
<section class="mb-10">
<h2 id="compatibility" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">8. Version compatibility</h2>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">PWA (OTAMan)</th><th class="text-left py-1 px-2">Server</th><th class="text-left py-1 px-2">Status</th></tr></thead>
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">1.x.x</td><td class="py-1 px-2 font-mono">1.x.x</td><td class="py-1 px-2">&#9989; Compatible</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">1.x.x</td><td class="py-1 px-2 font-mono">0.x.x</td><td class="py-1 px-2">&#10060; Outdated &mdash; update server</td></tr>
<tr><td class="py-1 px-2 font-mono">1.x.x</td><td class="py-1 px-2 font-mono">2.x.x+</td><td class="py-1 px-2">&#9888;&#65039; Server newer &mdash; update PWA</td></tr>
</tbody>
</table>
<p class="text-sm">The PWA checks the server version on connect via <code class="font-mono text-sm">GET /api/version</code> and warns if versions are incompatible.</p>
</main>
</div>
<script>
(function () {
if (localStorage.getItem('theme') === 'dark' ||
(!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
var toc = document.getElementById('toc');
var main = document.querySelector('main');
if (!toc || !main) return;
var headings = Array.prototype.filter.call(main.querySelectorAll('h2, h3, h4'), function (h) {
return /^\d/.test(h.textContent.trim());
});
var stack = [document.createElement('ol')];
stack[0].className = 'space-y-1 list-none';
var lastLevel = 2;
headings.forEach(function (h) {
var level = parseInt(h.tagName.charAt(1), 10);
var li = document.createElement('li');
var a = document.createElement('a');
a.href = '#' + h.id;
a.textContent = h.textContent;
li.appendChild(a);
if (level > lastLevel) {
var ul = document.createElement('ul');
ul.className = 'pl-4 mt-1 space-y-1 list-none';
var parent = stack[stack.length - 1].lastElementChild;
parent.appendChild(ul);
stack.push(ul);
} else if (level < lastLevel) {
while (stack.length > 1 && level < lastLevel) { stack.pop(); lastLevel--; }
}
stack[stack.length - 1].appendChild(li);
lastLevel = level;
});
toc.appendChild(stack[0]);
})();
</script>
</body>
</html>

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

+9706
View File
File diff suppressed because it is too large Load Diff
View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

+7
View File
@@ -8,6 +8,7 @@
"name": "otaman",
"version": "1.0.0",
"dependencies": {
"aes-js": "^3.1.2",
"des.js": "^1.1.0"
},
"devDependencies": {
@@ -115,6 +116,12 @@
"node": ">= 8"
}
},
"node_modules/aes-js": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.1.2.tgz",
"integrity": "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==",
"license": "MIT"
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+4 -3
View File
@@ -4,13 +4,13 @@
"description": "Standalone offline HTML/JS tool for building APDU commands for SIM, USIM, and GlobalPlatform RAM, plus encoding conversions.",
"main": "index.js",
"scripts": {
"build": "npx tailwindcss -i src/style.css -o style.css --config tailwind.config.js",
"build:prod": "NODE_ENV=production npx tailwindcss -i src/style.css -o style.css --config tailwind.config.js --minify",
"build": "npx tailwindcss -i src/style.css -o style.css --config tailwind.config.js && cat src/contrast.css >> style.css",
"build:prod": "NODE_ENV=production npx tailwindcss -i src/style.css -o style.css --config tailwind.config.js --minify && cat src/contrast.css >> style.css",
"test": "node --test"
},
"repository": {
"type": "git",
"url": "https://gitea.atroshin.ru/catarrh/otaman.git"
"url": "https://github.com/anttro/otaman.git"
},
"keywords": [],
"type": "commonjs",
@@ -22,6 +22,7 @@
"tailwindcss": "^3.4.19"
},
"dependencies": {
"aes-js": "^3.1.2",
"des.js": "^1.1.0"
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

+112
View File
@@ -0,0 +1,112 @@
/* ===== Theme contrast adjustments ===== */
/* Light theme: darken softer (muted) text one step for higher contrast */
.text-gray-400 {
color: rgb(107 114 128 / var(--tw-text-opacity, 1));
}
.text-gray-500 {
color: rgb(75 85 99 / var(--tw-text-opacity, 1));
}
.text-gray-600 {
color: rgb(55 65 81 / var(--tw-text-opacity, 1));
}
/* Dark theme: lighten softer (muted) text one step for higher contrast */
.dark\:text-slate-500:is(.dark *) {
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
.dark\:text-slate-400:is(.dark *) {
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
/* Dark theme: red text is too dark on dark backgrounds — lighten it */
.dark :where(.text-red-500) {
color: rgb(248 113 113 / var(--tw-text-opacity, 1));
}
.dark :where(.text-red-600) {
color: rgb(248 113 113 / var(--tw-text-opacity, 1));
}
.dark :where(.text-red-700) {
color: rgb(239 68 68 / var(--tw-text-opacity, 1));
}
/* Light theme: darken light gray shades (backgrounds & borders) one step */
.bg-gray-50 {
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
}
.bg-gray-100 {
background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1));
}
.bg-gray-200 {
background-color: rgb(209 213 219 / var(--tw-bg-opacity, 1));
}
.border-gray-100 {
border-color: rgb(229 231 235 / var(--tw-border-opacity, 1));
}
.border-gray-200 {
border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
}
.border-gray-300 {
border-color: rgb(156 163 175 / var(--tw-border-opacity, 1));
}
.text-gray-300 {
color: rgb(156 163 175 / var(--tw-text-opacity, 1));
}
.hover\:bg-gray-100:hover {
background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1));
}
.hover\:bg-gray-200:hover {
background-color: rgb(209 213 219 / var(--tw-bg-opacity, 1));
}
.hover\:bg-gray-300:hover {
background-color: rgb(156 163 175 / var(--tw-bg-opacity, 1));
}
/* Dark theme: lighten gray fonts */
.dark\:text-gray-400:is(.dark *) {
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
.dark\:text-gray-600:is(.dark *) {
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
/* Dark theme: gray text without an explicit dark variant (ids, timestamps,
expand markers, ...) — match the lighten-on-dark level used above */
.dark .text-gray-300:not([class*="dark:text-"]) {
color: rgb(148 163 184 / var(--tw-text-opacity, 1));
}
.dark .text-gray-400:not([class*="dark:text-"]) {
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
.dark .text-gray-500:not([class*="dark:text-"]) {
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
.dark .text-gray-600:not([class*="dark:text-"]) {
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
.dark .text-gray-700:not([class*="dark:text-"]) {
color: rgb(226 232 240 / var(--tw-text-opacity, 1));
}
.dark .text-gray-800:not([class*="dark:text-"]) {
color: rgb(241 245 249 / var(--tw-text-opacity, 1));
}
/* Dark theme: brighten gray borders one step so they stay visible on dark
backgrounds */
.dark\:border-slate-600:is(.dark *) {
border-color: rgb(100 116 139 / var(--tw-border-opacity, 1));
}
.dark\:border-slate-700:is(.dark *) {
border-color: rgb(71 85 105 / var(--tw-border-opacity, 1));
}
.dark\:border-slate-700\/50:is(.dark *) {
border-color: rgb(71 85 105 / 0.5);
}
.dark\:border-slate-800:is(.dark *) {
border-color: rgb(51 65 85 / var(--tw-border-opacity, 1));
}
/* Dark theme: normal (non-muted) UI text — keep one step brighter than the
muted gray level so the two remain distinguishable */
.dark\:text-slate-300:is(.dark *) {
color: rgb(226 232 240 / var(--tw-text-opacity, 1));
}
+398 -6
View File
@@ -554,6 +554,44 @@ video {
display: none;
}
.container {
width: 100%;
}
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
}
.static {
position: static;
}
.fixed {
position: fixed;
}
@@ -566,6 +604,10 @@ video {
position: relative;
}
.sticky {
position: sticky;
}
.inset-0 {
inset: 0px;
}
@@ -582,6 +624,10 @@ video {
right: 0px;
}
.top-0 {
top: 0px;
}
.z-10 {
z-index: 10;
}
@@ -590,6 +636,11 @@ video {
z-index: 50;
}
.mx-4 {
margin-left: 1rem;
margin-right: 1rem;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
@@ -603,6 +654,10 @@ video {
margin-bottom: 0.375rem;
}
.mb-10 {
margin-bottom: 2.5rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
@@ -627,6 +682,10 @@ video {
margin-left: 0.5rem;
}
.ml-3 {
margin-left: 0.75rem;
}
.ml-4 {
margin-left: 1rem;
}
@@ -643,6 +702,10 @@ video {
margin-right: 0.25rem;
}
.mt-0\.5 {
margin-top: 0.125rem;
}
.mt-1 {
margin-top: 0.25rem;
}
@@ -655,10 +718,18 @@ video {
margin-top: 0.75rem;
}
.mt-4 {
margin-top: 1rem;
}
.block {
display: block;
}
.inline {
display: inline;
}
.flex {
display: flex;
}
@@ -679,14 +750,26 @@ video {
height: 4rem;
}
.h-screen {
height: 100vh;
}
.max-h-32 {
max-height: 8rem;
}
.max-h-\[60vh\] {
max-height: 60vh;
}
.max-h-\[80vh\] {
max-height: 80vh;
}
.max-h-\[85vh\] {
max-height: 85vh;
}
.min-h-20 {
min-height: 5rem;
}
@@ -719,6 +802,14 @@ video {
width: 6rem;
}
.w-4 {
width: 1rem;
}
.w-40 {
width: 10rem;
}
.w-48 {
width: 12rem;
}
@@ -731,6 +822,10 @@ video {
width: 1.5rem;
}
.w-72 {
width: 18rem;
}
.w-8 {
width: 2rem;
}
@@ -751,6 +846,10 @@ video {
max-width: 28rem;
}
.max-w-sm {
max-width: 24rem;
}
.max-w-xs {
max-width: 20rem;
}
@@ -759,10 +858,18 @@ video {
flex: 1 1 0%;
}
.flex-shrink-0 {
flex-shrink: 0;
}
.shrink-0 {
flex-shrink: 0;
}
.border-collapse {
border-collapse: collapse;
}
.cursor-pointer {
cursor: pointer;
}
@@ -777,6 +884,22 @@ video {
resize: none;
}
.list-inside {
list-style-position: inside;
}
.list-decimal {
list-style-type: decimal;
}
.list-disc {
list-style-type: disc;
}
.list-none {
list-style-type: none;
}
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -797,6 +920,14 @@ video {
align-items: center;
}
.items-baseline {
align-items: baseline;
}
.justify-end {
justify-content: flex-end;
}
.justify-center {
justify-content: center;
}
@@ -809,6 +940,10 @@ video {
gap: 0.25rem;
}
.gap-1\.5 {
gap: 0.375rem;
}
.gap-2 {
gap: 0.5rem;
}
@@ -821,6 +956,10 @@ video {
gap: 1rem;
}
.gap-6 {
gap: 1.5rem;
}
.gap-x-3 {
-moz-column-gap: 0.75rem;
column-gap: 0.75rem;
@@ -839,6 +978,12 @@ video {
row-gap: 0.5rem;
}
.space-y-0\.5 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(0.125rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(0.125rem * var(--tw-space-y-reverse));
}
.space-y-1 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse)));
@@ -851,6 +996,12 @@ video {
margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
}
.space-y-3 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
}
.overflow-auto {
overflow: auto;
}
@@ -863,6 +1014,12 @@ video {
overflow-y: auto;
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.whitespace-nowrap {
white-space: nowrap;
}
@@ -871,6 +1028,10 @@ video {
white-space: pre-wrap;
}
.break-all {
word-break: break-all;
}
.rounded {
border-radius: 0.25rem;
}
@@ -900,6 +1061,18 @@ video {
border-bottom-width: 1px;
}
.border-l {
border-left-width: 1px;
}
.border-l-2 {
border-left-width: 2px;
}
.border-r {
border-right-width: 1px;
}
.border-t {
border-top-width: 1px;
}
@@ -933,6 +1106,11 @@ video {
background-color: rgb(5 150 105 / var(--tw-bg-opacity, 1));
}
.bg-emerald-700 {
--tw-bg-opacity: 1;
background-color: rgb(4 120 87 / var(--tw-bg-opacity, 1));
}
.bg-gray-100 {
--tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
@@ -963,6 +1141,11 @@ video {
background-color: rgb(254 226 226 / var(--tw-bg-opacity, 1));
}
.bg-red-600 {
--tw-bg-opacity: 1;
background-color: rgb(220 38 38 / var(--tw-bg-opacity, 1));
}
.bg-slate-600 {
--tw-bg-opacity: 1;
background-color: rgb(71 85 105 / var(--tw-bg-opacity, 1));
@@ -989,11 +1172,20 @@ video {
padding: 1rem;
}
.p-6 {
padding: 1.5rem;
}
.px-1 {
padding-left: 0.25rem;
padding-right: 0.25rem;
}
.px-1\.5 {
padding-left: 0.375rem;
padding-right: 0.375rem;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
@@ -1049,9 +1241,30 @@ video {
padding-bottom: 0.625rem;
}
.py-4 {
padding-top: 1rem;
padding-bottom: 1rem;
.py-6 {
padding-top: 1.5rem;
padding-bottom: 1.5rem;
}
.py-8 {
padding-top: 2rem;
padding-bottom: 2rem;
}
.pb-1 {
padding-bottom: 0.25rem;
}
.pl-2 {
padding-left: 0.5rem;
}
.pl-4 {
padding-left: 1rem;
}
.pl-5 {
padding-left: 1.25rem;
}
.pt-1 {
@@ -1087,14 +1300,33 @@ video {
line-height: 2rem;
}
.text-\[13px\] {
font-size: 13px;
}
.text-base {
font-size: 1rem;
line-height: 1.5rem;
}
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
}
.text-xs {
font-size: 0.75rem;
line-height: 1rem;
font-size: 0.875rem;
line-height: 1.25rem;
}
.font-bold {
@@ -1109,6 +1341,10 @@ video {
font-weight: 400;
}
.font-semibold {
font-weight: 600;
}
.italic {
font-style: italic;
}
@@ -1117,11 +1353,25 @@ video {
line-height: 1.625;
}
.leading-snug {
line-height: 1.375;
}
.text-amber-600 {
--tw-text-opacity: 1;
color: rgb(217 119 6 / var(--tw-text-opacity, 1));
}
.text-blue-600 {
--tw-text-opacity: 1;
color: rgb(37 99 235 / var(--tw-text-opacity, 1));
}
.text-emerald-600 {
--tw-text-opacity: 1;
color: rgb(5 150 105 / var(--tw-text-opacity, 1));
}
.text-gray-300 {
--tw-text-opacity: 1;
color: rgb(209 213 219 / var(--tw-text-opacity, 1));
@@ -1162,6 +1412,11 @@ video {
color: rgb(30 41 59 / var(--tw-text-opacity, 1));
}
.text-indigo-600 {
--tw-text-opacity: 1;
color: rgb(79 70 229 / var(--tw-text-opacity, 1));
}
.text-purple-600 {
--tw-text-opacity: 1;
color: rgb(147 51 234 / var(--tw-text-opacity, 1));
@@ -1187,6 +1442,11 @@ video {
color: rgb(185 28 28 / var(--tw-text-opacity, 1));
}
.text-slate-300 {
--tw-text-opacity: 1;
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
@@ -1305,6 +1565,10 @@ video {
border-color: rgb(51 65 85 / var(--tw-border-opacity, 1));
}
.dark\:border-slate-700\/50:is(.dark *) {
border-color: rgb(51 65 85 / 0.5);
}
.dark\:bg-blue-500:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1));
@@ -1334,6 +1598,11 @@ video {
color: rgb(96 165 250 / var(--tw-text-opacity, 1));
}
.dark\:text-emerald-400:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(52 211 153 / var(--tw-text-opacity, 1));
}
.dark\:text-gray-600:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(75 85 99 / var(--tw-text-opacity, 1));
@@ -1344,6 +1613,11 @@ video {
color: rgb(74 222 128 / var(--tw-text-opacity, 1));
}
.dark\:text-indigo-400:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(129 140 248 / var(--tw-text-opacity, 1));
}
.dark\:text-purple-400:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(192 132 252 / var(--tw-text-opacity, 1));
@@ -1403,7 +1677,125 @@ video {
background-color: rgb(51 65 85 / var(--tw-bg-opacity, 1));
}
.dark\:hover\:text-gray-300:hover:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(209 213 219 / var(--tw-text-opacity, 1));
}
.dark\:hover\:text-slate-300:hover:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
}
/* ===== Theme contrast adjustments ===== */
/* Light theme: darken softer (muted) text one step for higher contrast */
.text-gray-400 {
color: rgb(107 114 128 / var(--tw-text-opacity, 1));
}
.text-gray-500 {
color: rgb(75 85 99 / var(--tw-text-opacity, 1));
}
.text-gray-600 {
color: rgb(55 65 81 / var(--tw-text-opacity, 1));
}
/* Dark theme: lighten softer (muted) text one step for higher contrast */
.dark\:text-slate-500:is(.dark *) {
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
.dark\:text-slate-400:is(.dark *) {
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
/* Dark theme: red text is too dark on dark backgrounds — lighten it */
.dark :where(.text-red-500) {
color: rgb(248 113 113 / var(--tw-text-opacity, 1));
}
.dark :where(.text-red-600) {
color: rgb(248 113 113 / var(--tw-text-opacity, 1));
}
.dark :where(.text-red-700) {
color: rgb(239 68 68 / var(--tw-text-opacity, 1));
}
/* Light theme: darken light gray shades (backgrounds & borders) one step */
.bg-gray-50 {
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
}
.bg-gray-100 {
background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1));
}
.bg-gray-200 {
background-color: rgb(209 213 219 / var(--tw-bg-opacity, 1));
}
.border-gray-100 {
border-color: rgb(229 231 235 / var(--tw-border-opacity, 1));
}
.border-gray-200 {
border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
}
.border-gray-300 {
border-color: rgb(156 163 175 / var(--tw-border-opacity, 1));
}
.text-gray-300 {
color: rgb(156 163 175 / var(--tw-text-opacity, 1));
}
.hover\:bg-gray-100:hover {
background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1));
}
.hover\:bg-gray-200:hover {
background-color: rgb(209 213 219 / var(--tw-bg-opacity, 1));
}
.hover\:bg-gray-300:hover {
background-color: rgb(156 163 175 / var(--tw-bg-opacity, 1));
}
/* Dark theme: lighten gray fonts */
.dark\:text-gray-400:is(.dark *) {
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
.dark\:text-gray-600:is(.dark *) {
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
/* Dark theme: gray text without an explicit dark variant (ids, timestamps,
expand markers, ...) match the lighten-on-dark level used above */
.dark .text-gray-300:not([class*="dark:text-"]) {
color: rgb(148 163 184 / var(--tw-text-opacity, 1));
}
.dark .text-gray-400:not([class*="dark:text-"]) {
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
.dark .text-gray-500:not([class*="dark:text-"]) {
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
.dark .text-gray-600:not([class*="dark:text-"]) {
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
.dark .text-gray-700:not([class*="dark:text-"]) {
color: rgb(226 232 240 / var(--tw-text-opacity, 1));
}
.dark .text-gray-800:not([class*="dark:text-"]) {
color: rgb(241 245 249 / var(--tw-text-opacity, 1));
}
/* Dark theme: brighten gray borders one step so they stay visible on dark
backgrounds */
.dark\:border-slate-600:is(.dark *) {
border-color: rgb(100 116 139 / var(--tw-border-opacity, 1));
}
.dark\:border-slate-700:is(.dark *) {
border-color: rgb(71 85 105 / var(--tw-border-opacity, 1));
}
.dark\:border-slate-700\/50:is(.dark *) {
border-color: rgb(71 85 105 / 0.5);
}
.dark\:border-slate-800:is(.dark *) {
border-color: rgb(51 65 85 / var(--tw-border-opacity, 1));
}
/* Dark theme: normal (non-muted) UI text keep one step brighter than the
muted gray level so the two remain distinguishable */
.dark\:text-slate-300:is(.dark *) {
color: rgb(226 232 240 / var(--tw-text-opacity, 1));
}
+22 -3
View File
@@ -1,12 +1,16 @@
const CACHE = 'otaman-v5';
const CACHE = 'otaman-v133';
const URLS = [
'index.html',
'help.html',
'help-ru.html',
'style.css',
'sim.png',
'favicon.svg',
'icon-192.png',
'icon-512.png',
'manifest.json',
'des-bundle.js',
'aes-bundle.js',
'sim.svg',
'sim_anim.svg',
'nosim.svg',
@@ -27,18 +31,33 @@ self.addEventListener('activate', e => {
);
});
const OFFLINE_RESPONSE = new Response('Offline: page not cached', {
status: 503,
statusText: 'Offline',
headers: { 'Content-Type': 'text/plain' },
});
self.addEventListener('fetch', e => {
if (!e.request.url.startsWith('http')) return;
const path = new URL(e.request.url).pathname;
if (path.startsWith('/api/')) return; // live data, never cache
if (e.request.method !== 'GET') return;
const isNavigate = e.request.mode === 'navigate' || e.request.url.endsWith('sw.js');
const isNavigate = e.request.mode === 'navigate';
const isSwScript = path.endsWith('/sw.js');
if (isNavigate) {
e.respondWith(
fetch(e.request).then(res => {
const clone = res.clone();
caches.open(CACHE).then(c => c.put(e.request, clone));
return res;
}).catch(() => caches.match(e.request))
}).catch(() =>
caches.match(e.request)
.then(r => r || caches.match('index.html'))
.then(r => r || OFFLINE_RESPONSE)
)
);
} else if (isSwScript) {
e.respondWith(fetch(e.request).catch(() => OFFLINE_RESPONSE));
} else {
e.respondWith(
caches.match(e.request).then(r => r || fetch(e.request).then(res => {
@@ -1,7 +1,7 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: 'class',
content: ['./index.html'],
content: ['./index.html', './help.html', './help-ru.html'],
theme: {
extend: {
textColor: {
+417
View File
@@ -0,0 +1,417 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
function extractFunc(src, name) {
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return src.slice(m.index, i + 1);
}
function findNode(tree, label) {
if (tree.label === label) return tree;
if (tree.children) {
for (const c of tree.children) {
const r = findNode(c, label);
if (r) return r;
}
}
return null;
}
function findNodes(tree, label) {
const out = [];
if (tree.label === label) out.push(tree);
if (tree.children) {
for (const c of tree.children) out.push(...findNodes(c, label));
}
return out;
}
const consts = ['BER_QUAL', 'BER_DEVICES', 'BER_TONES', 'GSM7_ALPHABET', 'GSM7_EXT_MAP'];
let prefix = '';
for (const c of consts) {
const m = html.match(new RegExp('const\\s+' + c + '\\s*=\\s*(?:\\[|\\{)[\\s\\S]*?(?:\\n[\\]\\}];|[\\]\\}];\\n)'));
if (!m) throw new Error(c + ' not found');
prefix += m[0].replace(/^const /, 'var ') + '\n';
}
const start = html.indexOf('// ===== C-APDU Parser =====');
const end = html.indexOf('// ===== Response Parser =====');
eval(prefix + html.slice(start, end).replace(/^const PARSE_INS = /m, 'var PARSE_INS = '));
test('Compact RAM INSTALL [for install] UICC', () => {
const tree = parseHexTree('80E60C00214F08A000000151000000C70100EA128010000000020101020200011603B0000100');
assert.strictEqual(tree.label, 'Compact C-APDU chain');
assert.strictEqual(tree.children.length, 1);
const apdu = tree.children[0];
assert.strictEqual(apdu.label, 'APDU');
assert.ok(findNode(apdu, 'INS').desc.includes('INSTALL'));
});
test('Compact RAM INSTALL [for install] SIM (CA) access domain 5A', () => {
const tree = parseHexTree('80E60C00204F08A000000151000000C70100CA11015A000000020101020200011603B00001');
const apdu = tree.children[0];
assert.ok(findNode(apdu, 'INS').desc.includes('INSTALL'));
});
test('Expanded AA DISPLAY TEXT UCS2 "HI" + 5s', () => {
const tree = parseHexTree('AA1681130103012101020281820D040048004904020105');
assert.strictEqual(tree.label, 'Expanded Script (AA)');
assert.strictEqual(tree.children.length, 1);
assert.strictEqual(tree.children[0].label, 'Immediate Action');
});
test('Expanded AA DISPLAY TEXT GSM7 "HI"', () => {
const tree = parseHexTree('AA0F810D0103012101020281820D02C824');
assert.strictEqual(tree.children.length, 1);
assert.strictEqual(tree.children[0].label, 'Immediate Action');
});
test('Expanded AA REFRESH + file list', () => {
const tree = parseHexTree('AA10810E01030101000202818212036F3B2F');
assert.strictEqual(tree.children.length, 1);
assert.strictEqual(tree.children[0].label, 'Immediate Action');
});
test('Expanded AE80 PLAY TONE + Error Action no action', () => {
const tree = parseHexTree('AE80810C0103200101020281820E010182000000');
assert.strictEqual(tree.label, 'Expanded Script (AE80)');
assert.strictEqual(tree.children.length, 2);
assert.strictEqual(tree.children[0].label, 'Immediate Action');
assert.strictEqual(tree.children[1].label, 'Error Action');
});
test('Immediate Action CR-set tags (real-world encoding)', () => {
const tree = parseHexTree('AA0E8101818109810301010482028182');
assert.strictEqual(tree.children.length, 2);
assert.strictEqual(tree.children[0].label, 'Immediate Action');
assert.strictEqual(tree.children[1].label, 'Immediate Action');
});
test('CR-set DISPLAY TEXT UCS2 "HI" + 5s', () => {
const tree = parseHexTree('AA1581138103012101820281828D040048004984020105');
assert.strictEqual(tree.children.length, 1);
});
test('CR-set PLAY TONE (tag 8E)', () => {
const tree = parseHexTree('AA0E810C8103012001820281828E0101');
assert.strictEqual(tree.children.length, 1);
});
test('CR-set REFRESH file list (tag 92)', () => {
const tree = parseHexTree('AA0F810D81030101048202818292023F00');
assert.strictEqual(tree.children.length, 1);
});
test('Expanded AA Error Action = proactive DISPLAY TEXT', () => {
const tree = parseHexTree('AA0F820D0103012101020281820D022852');
assert.strictEqual(tree.children.length, 1);
assert.strictEqual(tree.children[0].label, 'Error Action');
});
test('Expanded AA 22 C-APDU row (GET STATUS)', () => {
const tree = parseHexTree('AA09220780F22000024F00');
assert.strictEqual(tree.children.length, 1);
assert.strictEqual(tree.children[0].label, 'C-APDU');
});
test('Chained SIM SELECT + UPDATE RECORD', () => {
const tree = parseHexTree('A0A40000026F3BDC0102032B2F2D');
assert.strictEqual(tree.label, 'Compact C-APDU chain');
assert.strictEqual(tree.children.length, 2);
assert.strictEqual(tree.children[0].label, 'APDU');
});
test('INTERNAL AUTHENTICATE', () => {
const tree = parseHexTree('8088000008800F3495BA2355CD');
assert.strictEqual(tree.children.length, 1);
const apdu = tree.children[0];
assert.strictEqual(apdu.label, 'APDU');
assert.ok(findNode(apdu, 'INS').desc.includes('AUTHENTICATE'));
assert.ok(findNode(apdu, 'Data'));
});
test('baseCompTag clears CR bit for 0x80-0x9F', () => {
assert.strictEqual(baseCompTag('81'), '01');
assert.strictEqual(baseCompTag('82'), '02');
assert.strictEqual(baseCompTag('8D'), '0D');
assert.strictEqual(baseCompTag('8E'), '0E');
assert.strictEqual(baseCompTag('92'), '12');
assert.strictEqual(baseCompTag('84'), '04');
assert.strictEqual(baseCompTag('85'), '05');
assert.strictEqual(baseCompTag('01'), '01');
assert.strictEqual(baseCompTag('22'), '22');
});
test('parseTlvList handles BER-TLVs', () => {
const tlvs = parseTlvList('810301210182028182');
assert.strictEqual(tlvs.length, 2);
assert.strictEqual(tlvs[0].tag, '81');
assert.strictEqual(tlvs[0].length, 3);
assert.strictEqual(tlvs[0].value, '012101');
assert.strictEqual(tlvs[1].tag, '82');
});
test('parseBerLen handles short and long form lengths (ISO 7816-4 5.2)', () => {
assert.deepStrictEqual(parseBerLen('1200', 0), { len: 18, consumed: 2 });
assert.deepStrictEqual(parseBerLen('8112', 0), { len: 18, consumed: 4 });
assert.deepStrictEqual(parseBerLen('820100', 0), { len: 256, consumed: 6 });
assert.deepStrictEqual(parseBerLen('820182' + '0102030405060708', 0), { len: 386, consumed: 6 });
});
test('parseTlvList parses long-form lengths (81/82)', () => {
const short = parseTlvList('6212' + '8202412183026F078A010580020009880110');
assert.strictEqual(short.length, 1);
assert.strictEqual(short[0].tag, '62');
assert.strictEqual(short[0].length, 18);
// same content with a long-form outer length
const long81 = parseTlvList('628112' + '8202412183026F078A010580020009880110');
assert.strictEqual(long81.length, 1);
assert.strictEqual(long81[0].length, 18);
assert.strictEqual(long81[0].value, short[0].value);
// 2-byte length form
const long82 = parseTlvList('62820004' + '80020009');
assert.strictEqual(long82.length, 1);
assert.strictEqual(long82[0].length, 4);
assert.strictEqual(long82[0].value, '80020009');
// inner TLV with a long-form length (A5 81 05 85 03 00 00 00)
const inner = parseTlvList('A581058503000000');
assert.strictEqual(inner.length, 1);
assert.strictEqual(inner[0].tag, 'A5');
assert.strictEqual(inner[0].length, 5);
assert.strictEqual(inner[0].value, '8503000000');
});
test('gsm7Decode unpacks "HI" from C824', () => {
const bytes = new Uint8Array([0xC8, 0x24]);
assert.strictEqual(gsm7Decode(bytes), 'HI');
});
test('decodeTextData legacy UCS2 without DCS (regression: lead 00 is not a DCS)', () => {
assert.strictEqual(decodeTextData('00480049'), 'HI');
});
test('decodeTextData legacy Cyrillic UCS2 (uniform 04 high bytes)', () => {
assert.strictEqual(decodeTextData('041f04400438043204350442'), 'Привет');
});
test('decodeTextData DCS-prefixed UCS2 (08)', () => {
assert.strictEqual(decodeTextData('0800480049'), 'HI');
});
test('decodeTextData DCS-prefixed GSM7 packed (00)', () => {
assert.strictEqual(decodeTextData('00C824'), 'HI');
});
test('decodeTextData DCS-prefixed unpacked 8-bit (04)', () => {
assert.strictEqual(decodeTextData('04414243'), 'ABC');
});
test('decodeTextData explicit DCS 08 with malformed payload returns ?', () => {
assert.strictEqual(decodeTextData('0841'), '?');
});
test('decodePrivileges byte 2 b6 is Token Verification per GPC v2.3 Table 11-8', () => {
assert.ok(decodePrivileges('0020').includes('Token Verification'));
});
test('decodePrivileges', () => {
assert.ok(decodePrivileges('00').includes('None'));
assert.ok(decodePrivileges('80').includes('Security Domain'));
});
test('LV INSTALL [for install] decode with labeled fields and trailing Le', () => {
const tree = parseHexTree('80E60C0011000008A000000151000000010002C9000000');
const apdu = tree.children[0];
assert.ok(findNode(apdu, 'P1').desc.includes('for install + for make selectable'));
const elf = findNode(apdu, 'ELF AID');
assert.ok(elf);
assert.strictEqual(elf.desc, '(empty)');
assert.strictEqual(findNode(apdu, 'Application AID').desc, 'A000000151000000');
assert.strictEqual(findNode(apdu, 'Privileges').desc, 'None');
const params = findNode(apdu, 'Install parameters');
assert.ok(params.children.some(c => c.label.includes('C9')));
assert.strictEqual(findNode(apdu, 'Install token').desc, '(empty)');
assert.strictEqual(tree.children[1].label, 'Le');
});
test('Legacy TLV INSTALL falls back to raw Data node', () => {
const tree = parseHexTree('80E60C00214F08A000000151000000C70100EA13801100000002010102020002011603B0000100');
const apdu = tree.children[0];
const data = findNodes(apdu, 'Data');
assert.strictEqual(data.length, 1);
assert.ok(!data[0].children || !data[0].label.includes('ELF'));
assert.ok(findNode(apdu, 'P1').desc.includes('for install + for make selectable'));
});
test('Install parameters EF nesting exposes inner CA TLV', () => {
const tree = parseHexTree('80E60C0025000008A000000151000000010016C900EF12CA1000000000030101000003030002011600' + '00');
const apdu = tree.children[0];
const params = findNode(apdu, 'Install parameters');
assert.ok(params.children.some(c => c.label.includes('EF')));
const ef = params.children.find(c => c.label.includes('EF'));
assert.ok(ef.children.some(c => c.label.includes('CA')));
});
test('GET DATA case 2 renders P3 as Le', () => {
const tree = parseHexTree('80CA5F5000');
const apdu = tree.children[0];
assert.ok(findNode(apdu, 'Le'));
assert.ok(!findNode(apdu, 'Lc'));
});
test('GET DATA case 4 decodes Lc + tag list + Le', () => {
const tree = parseHexTree('80CA2F00025C0000');
const apdu = tree.children[0];
assert.ok(findNode(apdu, 'Lc'));
assert.strictEqual(findNode(apdu, 'Data').desc, 'Tag list: 5C00');
assert.ok(findNode(apdu, 'Le'));
});
test('SET STATUS raw AID data labeled', () => {
const tree = parseHexTree('80F0408008A000000151000000');
const apdu = tree.children[0];
assert.strictEqual(findNode(apdu, 'P1').desc, 'Application or SSD');
assert.strictEqual(findNode(apdu, 'P2').desc, 'LOCKED');
assert.ok(findNode(apdu, 'Data').desc.startsWith('AID (raw):'));
});
test('SET STATUS ISD card state label', () => {
const tree = parseHexTree('80F0807F00');
const apdu = tree.children[0];
assert.strictEqual(findNode(apdu, 'P1').desc, 'ISD');
assert.strictEqual(findNode(apdu, 'P2').desc, 'CARD_LOCKED');
assert.ok(!findNode(apdu, 'Data'));
});
test('Trailing single byte consumed as Le in compact chain', () => {
const tree = parseHexTree('A0A40000026F3BDC0102032B2F2D00');
const last = tree.children[tree.children.length - 1];
assert.strictEqual(last.label, 'Le');
assert.strictEqual(last.hex, '00');
});
test('ACTIVATE FILE case-1 (4 bytes)', () => {
const tree = parseHexTree('00440000');
const apdu = tree.children[0];
assert.strictEqual(apdu.hex, '00440000');
assert.strictEqual(apdu.desc, 'no data, no Le');
assert.ok(!findNode(apdu, 'P3'));
assert.ok(!findNode(apdu, 'Lc'));
});
test('ACTIVATE FILE legacy 5-byte empty-Lc form', () => {
const tree = parseHexTree('0044000000');
const apdu = tree.children[0];
assert.strictEqual(findNode(apdu, 'P3').desc, 'empty Lc (legacy form)');
});
test('ACTIVATE FILE with FID data', () => {
const tree = parseHexTree('00440000026F3B');
const apdu = tree.children[0];
assert.strictEqual(findNode(apdu, 'Data').desc, 'FID 6F3B');
});
test('READ RECORD next mode: P1 ignored note', () => {
const tree = parseHexTree('00B2000200');
const apdu = tree.children[0];
assert.strictEqual(findNode(apdu, 'P1').desc, 'ignored');
assert.ok(findNode(apdu, 'P2').desc.includes('next record'));
});
test('UPDATE RECORD absolute mode with record number', () => {
const tree = parseHexTree('00DC010404AABBCCDD');
const apdu = tree.children[0];
assert.strictEqual(findNode(apdu, 'P1').desc, 'record #1');
assert.strictEqual(findNode(apdu, 'P2').desc, 'absolute/current');
});
test('SELECT P1/P2 descriptors', () => {
const tree = parseHexTree('00A40804047FFF6FC500');
const apdu = tree.children[0];
assert.strictEqual(findNode(apdu, 'P1').desc, 'path from MF');
assert.ok(findNode(apdu, 'P2').desc.includes('FCP'));
});
test('Immediate Action EFRMA reference (01-7F)', () => {
const tree = parseHexTree('AA03810105');
const row = tree.children[0].children[0];
assert.strictEqual(row.label, 'Reference to EFRMA record');
assert.strictEqual(row.desc, 'Record 0x05');
});
test('CLA 84-87 labeled GlobalPlatform secure messaging', () => {
const tree = parseHexTree('8482030010' + '00112233445566778899AABBCCDDEEFF');
const apdu = tree.children[0];
assert.strictEqual(findNode(apdu, 'CLA').desc, 'GlobalPlatform (secure messaging)');
});
test('New PARSE_INS labels decode (FETCH le, RESIZE D4, RETRIEVE CB, AUTH 89)', () => {
const fetchTree = parseHexTree('8012000020');
assert.ok(findNode(fetchTree.children[0], 'INS').desc.includes('FETCH'));
assert.ok(findNode(fetchTree.children[0], 'Le'));
const resizeTree = parseHexTree('80D4000004' + '00000FA0');
assert.ok(findNode(resizeTree.children[0], 'INS').desc.includes('RESIZE FILE'));
assert.strictEqual(findNode(resizeTree.children[0], 'Data').desc, '00000FA0');
const retrTree = parseHexTree('80CB000000');
assert.ok(findNode(retrTree.children[0], 'INS').desc.includes('RETRIEVE DATA'));
const authTree = parseHexTree('0089000008' + '0011223344556677');
assert.ok(findNode(authTree.children[0], 'INS').desc.includes('odd INS'));
});
test('READ BINARY SFI mode and offset descriptors', () => {
const sfi = parseHexTree('00B0830602');
const p1sfi = findNode(sfi.children[0], 'P1').desc;
assert.ok(p1sfi.includes('SFI') && p1sfi.includes('3'), p1sfi);
assert.strictEqual(findNode(sfi.children[0], 'P2').desc, 'offset low');
const off = parseHexTree('00B0010202');
assert.strictEqual(findNode(off.children[0], 'P1').desc, 'offset high');
});
test('SELECT P2 response field per ETSI b5-b3', () => {
const fcp = parseHexTree('00A40804047FFF6FC500');
assert.ok(findNode(fcp.children[0], 'P2').desc.includes('FCP'));
const none = parseHexTree('00A4000C026F3B');
assert.ok(findNode(none.children[0], 'P2').desc.includes('no response data'));
});
test('live RFM captures decode (silent hop / implied CLA / SIM chain)', () => {
// capture line 1 head: silent relative hop 09/0C
const t1 = parseHexTree('00A4090C026F46');
const a1 = t1.children[0];
assert.ok(findNode(a1, 'P1').desc.includes('path from current DF'));
assert.ok(findNode(a1, 'P2').desc.includes('no response data'));
assert.strictEqual(findNode(a1, 'Data').desc, '6F46');
// capture line 2: silent hop + READ RECORD as implied-CLA continuation
const t2 = parseHexTree('00A4090C026FC5B2010414');
assert.strictEqual(t2.children.length, 2);
assert.strictEqual(t2.children[1].label, 'APDU (implied CLA)');
assert.ok(findNode(t2.children[1], 'INS').desc.includes('READ RECORD'));
// capture line 3: SIM chained FID selects + UPDATE RECORD (34 x FF)
const t3 = parseHexTree('A0A40000023F00A0A40000027F20A0A40000026FC5A0DC010422' + 'FF'.repeat(34));
assert.strictEqual(t3.children.length, 4);
assert.strictEqual(t3.children[3].label, 'APDU');
assert.ok(findNode(t3.children[3], 'INS').desc.includes('UPDATE RECORD'));
assert.ok(findNode(t3.children[3], 'Lc').desc.includes('34'));
});
+280
View File
@@ -0,0 +1,280 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
function extractFunc(src, name) {
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return src.slice(m.index, i + 1);
}
class StubEl {
constructor(value = '') {
this.value = value;
this.checked = false;
this.disabled = false;
this.children = [];
this.parentElement = null;
this.nextElementSibling = null;
}
querySelector(sel) {
return this._find(sel);
}
querySelectorAll(sel) {
const out = [];
this._findAll(sel, out);
return out;
}
_find(sel) {
for (const c of this.children) {
if (c._matches(sel)) return c;
const r = c._find(sel);
if (r) return r;
}
return null;
}
_findAll(sel, out) {
for (const c of this.children) {
if (c._matches(sel)) out.push(c);
c._findAll(sel, out);
}
}
_matches(sel) {
if (sel.startsWith('.')) return this.cls === sel.slice(1);
if (sel.startsWith('input[name="')) {
const m = sel.match(/^input\[name="([^"]+)"\]\[value="([^"]+)"\]$/);
if (m) return this.name === m[1] && this.value === m[2];
const m2 = sel.match(/^input\[name="([^"]+)"\]$/);
if (m2) return this.name === m2[1];
}
return false;
}
}
function el(cls, value = '') {
const e = new StubEl(value);
e.cls = cls;
return e;
}
function buildRow() {
const row = new StubEl();
row.cls = 'ber-row';
row.dataset = { berIdx: '0' };
const body = el('error-action-body');
const type = el('error-action-type', 'proactive');
type.nextElementSibling = body;
body.parentElement = row;
row.children.push(type, body);
return row;
}
function buildPcHtml(extraFields) {
// Build the .ber-pc tree expected by genBerPcValue
const pc = el('ber-pc');
const type = el('ber-pc-type', 'display');
const num = el('ber-pc-num', '1');
const qual = el('ber-pc-qual', '01');
const src = el('ber-pc-src', '81');
const dst = el('ber-pc-dst', '82');
const text = el('ber-pc-text', 'HI');
const enc = el('ber-pc-enc', 'ucs2');
const alpha = el('ber-pc-alpha', '');
const durEnable = el('ber-pc-dur-enable');
durEnable.checked = true;
const durUnit = el('ber-pc-dur-unit', '01');
const durVal = el('ber-pc-dur-val', '30');
pc.children.push(type, num, qual, src, dst, text, enc, alpha, durEnable, durUnit, durVal);
if (extraFields) extraFields(pc);
return pc;
}
const FNS = ['berLenStr', 'gsm7TextToSeptets', 'gsm7Encode', 'genBerPcValue', 'genErrorActionValue',
'genScriptChainingValue'];
let code = '';
for (const f of FNS) code += extractFunc(html, f) + '\n';
const berTagsMatch = html.match(/const BER_TAGS = \{[^}]*\};/);
const berQualMatch = html.match(/const BER_QUAL = \{[\s\S]*?\n\};/);
const berDevMatch = html.match(/const BER_DEVICES = \[[\s\S]*?\];/);
const berTonesMatch = html.match(/const BER_TONES = \[[\s\S]*?\];/);
const gsm7AlphaMatch = html.match(/const GSM7_ALPHABET = \[[\s\S]*?\n\];/);
const gsm7ExtMatch = html.match(/const GSM7_EXT_MAP = \{[\s\S]*?\n\};/);
for (const m of [berTagsMatch, berQualMatch, berDevMatch, berTonesMatch, gsm7AlphaMatch, gsm7ExtMatch]) {
if (!m) throw new Error('constant not found');
code = m[0] + '\n' + code;
}
code += '\nvar __berConsts = {BER_TAGS, BER_QUAL, BER_DEVICES, BER_TONES, GSM7_ALPHABET, GSM7_EXT_MAP};';
eval(code);
const {BER_TAGS, BER_QUAL, BER_DEVICES, BER_TONES, GSM7_ALPHABET, GSM7_EXT_MAP} = __berConsts;
function mkRow(type) {
const row = new StubEl();
row.cls = 'ber-row';
row.dataset = { berIdx: '0' };
const body = el('error-action-body');
const actionType = el('error-action-type', type);
actionType.nextElementSibling = body;
row.children.push(actionType, body);
return row;
}
test('BER_QUAL.refresh matches TS 102 223 §8.6 mapping', () => {
const r = BER_QUAL.refresh;
assert.strictEqual(r[0][0], '00');
assert.strictEqual(r[0][1], 'NAA Initialization and Full File Change Notification');
assert.strictEqual(r[4][0], '04');
assert.strictEqual(r[4][1], 'UICC Reset');
assert.strictEqual(r[5][1], 'NAA Application Reset (not for 2G SIM)');
assert.strictEqual(r[6][1], 'NAA Session Reset (not for 2G SIM)');
assert.strictEqual(r[7][1], 'Steering of Roaming');
assert.strictEqual(r.length, 11);
});
test('genBerPcValue DISPLAY TEXT GSM7 "HI"', () => {
const pc = buildPcHtml();
pc.children.find(c => c.cls === 'ber-pc-enc').value = 'gsm7';
pc.children.find(c => c.cls === 'ber-pc-dur-enable').checked = false;
const val = genBerPcValue({ querySelector: (s) => s === '.ber-pc' ? pc : null }, '81');
assert.strictEqual(val, '810E8103012101820281828D0300C824');
});
test('genBerPcValue DISPLAY TEXT UCS2 "HI" + 30s duration', () => {
const pc = buildPcHtml();
const val = genBerPcValue({ querySelector: (s) => s === '.ber-pc' ? pc : null }, '81');
// 81 13 8103 01 21 01 8202 81 82 8D 04 0048 0049 84 02 01 1E
assert.strictEqual(val, '81148103012101820281828D0508004800498402011E');
});
test('genBerPcValue duration disabled omits 84', () => {
const pc = buildPcHtml();
pc.children.find(c => c.cls === 'ber-pc-dur-enable').checked = false;
const val = genBerPcValue({ querySelector: (s) => s === '.ber-pc' ? pc : null }, '81');
assert.ok(!val.includes('84'));
});
test('genBerPcValue PLAY TONE uses type byte 20', () => {
const pc = buildPcHtml();
pc.children.find(c => c.cls === 'ber-pc-type').value = 'tone';
pc.children.find(c => c.cls === 'ber-pc-qual').value = '00';
pc.children.push(el('ber-pc-tone', '01'));
const val = genBerPcValue({ querySelector: (s) => s === '.ber-pc' ? pc : null }, '81');
assert.strictEqual(val, '810C8103012000820281828E0101');
});
test('genBerPcValue REFRESH uses CR-set file list tag 92', () => {
const pc = buildPcHtml();
pc.children.find(c => c.cls === 'ber-pc-type').value = 'refresh';
pc.children.find(c => c.cls === 'ber-pc-qual').value = '04';
pc.children.push(el('ber-pc-flist', '3F00'));
const val = genBerPcValue({ querySelector: (s) => s === '.ber-pc' ? pc : null }, '81');
assert.strictEqual(val, '810D81030101048202818292023F00');
});
test('genErrorActionValue noaction emits 82 00', () => {
const row = mkRow('noaction');
const val = genErrorActionValue(row, '82');
assert.strictEqual(val, '8200');
});
test('genErrorActionValue reference emits 82 01 ref', () => {
const row = mkRow('reference');
row.children[1].children.push(el('error-ref', '05'));
const val = genErrorActionValue(row, '82');
assert.strictEqual(val, '820105');
});
test('genErrorActionValue reference rejects out-of-range ref', () => {
const row = mkRow('reference');
row.children[1].children.push(el('error-ref', '80'));
assert.strictEqual(genErrorActionValue(row, '82'), '');
});
test('genErrorActionValue proactive wraps proactive command', () => {
const row = mkRow('proactive');
const pc = buildPcHtml();
row.children[1].children.push(pc);
const val = genErrorActionValue(row, '82');
// DISPLAY TEXT UCS2 "HI" + 30s inside Error Action
assert.strictEqual(val, '82148103012101820281828D0508004800498402011E');
});
test('genScriptChainingValue first emits 01', () => {
const row = new StubEl();
row.dataset = { berIdx: '0' };
const first = el('chaining-first', 'first');
first.name = 'chaining-0';
first.checked = true;
const interm = el('chaining-inter', 'intermediary');
interm.name = 'chaining-0';
const last = el('chaining-last', 'last');
last.name = 'chaining-0-last';
const keep = el('chaining-keep', 'keep');
keep.name = 'chaining-0-keep';
row.children.push(first, interm, last, keep, el('chaining-script-id'), el('chaining-additional'));
const val = genScriptChainingValue(row, '83');
assert.strictEqual(val, '830101');
});
test('genScriptChainingValue first with keep emits 11', () => {
const row = new StubEl();
row.dataset = { berIdx: '0' };
const first = el('chaining-first', 'first');
first.name = 'chaining-0';
first.checked = true;
const interm = el('chaining-inter', 'intermediary');
interm.name = 'chaining-0';
const last = el('chaining-last', 'last');
last.name = 'chaining-0-last';
const keep = el('chaining-keep', 'keep');
keep.name = 'chaining-0-keep';
keep.checked = true;
row.children.push(first, interm, last, keep, el('chaining-script-id'), el('chaining-additional'));
const val = genScriptChainingValue(row, '83');
assert.strictEqual(val, '830111');
});
test('genScriptChainingValue intermediary last emits 03', () => {
const row = new StubEl();
row.dataset = { berIdx: '0' };
const first = el('chaining-first', 'first');
first.name = 'chaining-0';
const interm = el('chaining-inter', 'intermediary');
interm.name = 'chaining-0';
interm.checked = true;
const last = el('chaining-last', 'last');
last.name = 'chaining-0-last';
last.checked = true;
const keep = el('chaining-keep', 'keep');
keep.name = 'chaining-0-keep';
row.children.push(first, interm, last, keep, el('chaining-script-id'), el('chaining-additional'));
const val = genScriptChainingValue(row, '83');
assert.strictEqual(val, '830103');
});
test('genScriptChainingValue no position emits empty', () => {
const row = new StubEl();
row.dataset = { berIdx: '0' };
const first = el('chaining-first', 'first');
first.name = 'chaining-0';
const interm = el('chaining-inter', 'intermediary');
interm.name = 'chaining-0';
const last = el('chaining-last', 'last');
last.name = 'chaining-0-last';
const keep = el('chaining-keep', 'keep');
keep.name = 'chaining-0-keep';
row.children.push(first, interm, last, keep, el('chaining-script-id'), el('chaining-additional'));
assert.strictEqual(genScriptChainingValue(row, '83'), '');
});
+170
View File
@@ -0,0 +1,170 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
function extractFunc(src, name) {
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return src.slice(m.index, i + 1);
}
let code = 'var _pysimCardStateKey = null;\nvar _pysimCardSession = null;\n'
+ 'var _pysimServerAvailable = null;\nvar _pysimCardEquipped = false;\n'
+ 'var _pysimProactiveSeq = null;\nvar _pysimStkSig = null;\n';
code += extractFunc(html, 'pysimCardStateUpdate') + '\n';
code += extractFunc(html, 'pysimAvailabilityState') + '\n';
code += extractFunc(html, 'pysimControlDisabled') + '\n';
code += extractFunc(html, 'pysimProactiveSeqChanged') + '\n';
code += extractFunc(html, 'pysimStkStatusChanged') + '\n';
code += '\nglobalThis.esc = s => s;\n';
code += 'globalThis.t = s => s;\n';
eval(code);
function setup() {
const el = { textContent: 'status line', innerHTML: '' };
const calls = { connected: [], resets: [], refreshStatus: [], proactive: 0 };
_pysimCardStateKey = null;
_pysimCardSession = null;
_pysimProactiveSeq = null;
globalThis.document = { getElementById: () => el, querySelectorAll: () => [] };
globalThis.pysimSetConnected = v => calls.connected.push(v);
globalThis.pysimResetCardData = refresh => calls.resets.push(refresh);
globalThis.pysimApplyAvailability = () => {};
globalThis.isViewVisible = () => true;
globalThis.pysimProactiveLogRender = () => { calls.proactive++; };
return { el, calls };
}
function status(extra) {
return Object.assign({ connected: false, card_present: false, equipping: false, auto_equip: false, card_session: 1 }, extra);
}
test('disconnect without card shows the no-card message', () => {
const { el, calls } = setup();
pysimCardStateUpdate(status({}));
assert.deepStrictEqual(calls.connected, [false]);
assert.ok(el.innerHTML.includes('No card detected'), el.innerHTML);
});
test('disconnect with card present shows the Equip hint when auto-equip is off', () => {
const { el } = setup();
pysimCardStateUpdate(status({ card_present: true }));
assert.ok(el.innerHTML.includes('Card inserted — press Equip'), el.innerHTML);
});
test('disconnect with auto-equip shows the initializing message', () => {
const { el } = setup();
pysimCardStateUpdate(status({ card_present: true, auto_equip: true }));
assert.ok(el.innerHTML.includes('initializing'), el.innerHTML);
});
test('unchanged state key does not touch the UI again', () => {
const { el, calls } = setup();
pysimCardStateUpdate(status({ card_session: 7 }));
el.innerHTML = 'unchanged';
calls.connected.length = 0;
pysimCardStateUpdate(status({ card_session: 7 }));
assert.deepStrictEqual(calls.connected, []);
assert.strictEqual(el.innerHTML, 'unchanged');
});
test('connected restores the UI and reloads card data', () => {
const { calls } = setup();
pysimCardStateUpdate(status({ connected: true, card_present: true, card_session: 2 }));
assert.deepStrictEqual(calls.connected, [true]);
assert.deepStrictEqual(calls.resets, [true]);
});
test('card session change triggers a data reset', () => {
const { calls } = setup();
pysimCardStateUpdate(status({ card_session: 3 }));
calls.resets.length = 0;
pysimCardStateUpdate(status({ card_session: 4 }));
assert.deepStrictEqual(calls.resets, [false]);
});
test('first observation does not trigger a reset on its own', () => {
const { calls } = setup();
pysimCardStateUpdate(status({ card_session: 9 }));
assert.deepStrictEqual(calls.resets, []);
});
test('payload without connected flag is ignored', () => {
const { calls } = setup();
pysimCardStateUpdate({ reader: 'x' });
pysimCardStateUpdate(null);
assert.deepStrictEqual(calls.connected, []);
});
test('availability state and control gating follow server/card state', () => {
_pysimServerAvailable = null;
assert.strictEqual(pysimAvailabilityState(), 'server-down');
assert.strictEqual(pysimControlDisabled('server', 'server-down'), true);
assert.strictEqual(pysimControlDisabled('card', 'server-down'), true);
_pysimServerAvailable = true;
_pysimCardEquipped = false;
assert.strictEqual(pysimAvailabilityState(), 'no-card');
assert.strictEqual(pysimControlDisabled('server', 'no-card'), false);
assert.strictEqual(pysimControlDisabled('card', 'no-card'), true);
_pysimCardEquipped = true;
assert.strictEqual(pysimAvailabilityState(), 'card');
assert.strictEqual(pysimControlDisabled('card', 'card'), false);
assert.strictEqual(pysimControlDisabled('server', 'card'), false);
});
test('no card with auto-equip enabled still shows the no-card message', () => {
const { el } = setup();
pysimCardStateUpdate(status({ connected: false, card_present: false, auto_equip: true }));
assert.ok(el.innerHTML.includes('No card detected'), el.innerHTML);
assert.ok(!el.innerHTML.includes('initializing'), el.innerHTML);
});
test('proactive log refreshes when the status sequence changes', () => {
const { calls } = setup();
pysimCardStateUpdate(status({ proactive_seq: 7 }));
pysimCardStateUpdate(status({ proactive_seq: 7 }));
assert.strictEqual(calls.proactive, 1);
pysimCardStateUpdate(status({ proactive_seq: 8 }));
assert.strictEqual(calls.proactive, 2);
});
test('proactive log is not refreshed while the phone view is hidden', () => {
const { calls } = setup();
globalThis.isViewVisible = () => false;
pysimCardStateUpdate(status({ proactive_seq: 3 }));
assert.strictEqual(calls.proactive, 0);
});
test('pysimProactiveSeqChanged tracks the last sequence', () => {
_pysimProactiveSeq = null;
assert.ok(pysimProactiveSeqChanged(4));
assert.ok(!pysimProactiveSeqChanged(4));
assert.ok(pysimProactiveSeqChanged(5));
assert.ok(!pysimProactiveSeqChanged(undefined));
assert.ok(!pysimProactiveSeqChanged(null));
});
test('pysimStkStatusChanged detects menu state transitions', () => {
_pysimStkSig = null;
assert.ok(pysimStkStatusChanged({ active: false, pending: false }));
assert.ok(!pysimStkStatusChanged({ active: false, pending: false }));
assert.ok(pysimStkStatusChanged({ active: true, pending: true, pending_type: 'select_item' }));
assert.ok(!pysimStkStatusChanged({ active: true, pending: true, pending_type: 'select_item' }));
assert.ok(pysimStkStatusChanged({ active: true, pending: false }));
assert.ok(!pysimStkStatusChanged(null));
});
+165
View File
@@ -0,0 +1,165 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
function extractFunc(src, name) {
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return src.slice(m.index, i + 1);
}
let code = 'var pysimCustomFiles = [];\nvar pysimCustomEditIndex = null;\n';
for (const fn of ['pysimCustomSave', 'pysimCustomSubmit', 'pysimCustomEdit', 'pysimCustomEditCancel', 'pysimCustomRemove', 'pysimCustomRender']) {
code += extractFunc(html, fn) + '\n';
}
code += 'globalThis.esc = s => s;\nglobalThis.t = s => s;\n';
eval(code);
function fakeEl(id) {
const classes = new Set();
return {
id,
value: '',
innerHTML: '',
textContent: '',
attrs: {},
focused: 0,
classList: {
add: (...cs) => cs.forEach(c => classes.add(c)),
remove: (...cs) => cs.forEach(c => classes.delete(c)),
contains: c => classes.has(c),
},
setAttribute(k, v) { this.attrs[k] = v; },
focus() { this.focused++; },
};
}
function setup(entries) {
const els = {
'pysim-cf-path': fakeEl('pysim-cf-path'),
'pysim-cf-name': fakeEl('pysim-cf-name'),
'pysim-cf-list': fakeEl('pysim-cf-list'),
'pysim-cf-add-btn': fakeEl('pysim-cf-add-btn'),
'pysim-cf-cancel-btn': fakeEl('pysim-cf-cancel-btn'),
};
els['pysim-cf-cancel-btn'].classList.add('hidden');
const store = {};
globalThis.localStorage = {
getItem: k => (k in store ? store[k] : null),
setItem: (k, v) => { store[k] = String(v); },
};
globalThis.document = { getElementById: id => els[id] || null };
globalThis.alertCalls = [];
globalThis.alert = m => { globalThis.alertCalls.push(m); };
pysimCustomFiles = (entries || []).map(e => Object.assign({}, e));
pysimCustomEditIndex = null;
return els;
}
const entry = (fid, name, parentFid) => ({ path: (parentFid || '3F00') + '/' + fid, name, fid, parentFid: parentFid || '3F00' });
test('rows render Edit and Delete buttons instead of the X glyph', () => {
const els = setup([entry('6F46', 'EF.SPN')]);
pysimCustomRender();
const out = els['pysim-cf-list'].innerHTML;
assert.match(out, /pysimCustomEdit\(0\)/);
assert.match(out, /pysimCustomRemove\(0\)/);
assert.match(out, />Edit</);
assert.match(out, />Delete</);
assert.ok(!out.includes('✕'), 'old X glyph must be gone');
});
test('edit fills the top form and switches it to Save mode', () => {
const els = setup([entry('6F46', 'EF.SPN')]);
pysimCustomEdit(0);
assert.strictEqual(pysimCustomEditIndex, 0);
assert.strictEqual(els['pysim-cf-path'].value, '3F00/6F46');
assert.strictEqual(els['pysim-cf-name'].value, 'EF.SPN');
assert.strictEqual(els['pysim-cf-add-btn'].textContent, 'Save');
assert.strictEqual(els['pysim-cf-add-btn'].attrs['data-l10n'], 'Save');
assert.ok(!els['pysim-cf-cancel-btn'].classList.contains('hidden'));
assert.strictEqual(els['pysim-cf-path'].focused, 1);
});
test('submit in edit mode updates the entry in place', () => {
const els = setup([entry('6F46', 'EF.SPN')]);
pysimCustomEdit(0);
els['pysim-cf-path'].value = '3F00/7F20/6F46';
els['pysim-cf-name'].value = 'EF.SPNX';
pysimCustomSubmit();
assert.strictEqual(pysimCustomFiles.length, 1);
assert.deepStrictEqual(pysimCustomFiles[0], { path: '3F00/7F20/6F46', name: 'EF.SPNX', fid: '6F46', parentFid: '7F20' });
assert.strictEqual(pysimCustomEditIndex, null);
assert.strictEqual(els['pysim-cf-add-btn'].textContent, 'Add');
assert.ok(els['pysim-cf-cancel-btn'].classList.contains('hidden'));
});
test('saving an edited entry with its own path is not a duplicate', () => {
const els = setup([entry('6F46', 'EF.SPN')]);
pysimCustomEdit(0);
pysimCustomSubmit();
assert.strictEqual(pysimCustomFiles.length, 1);
assert.deepStrictEqual(globalThis.alertCalls, []);
});
test('editing to another entry path is rejected as duplicate', () => {
const els = setup([entry('6F46', 'EF.SPN'), entry('6F44', 'EF.SPN2')]);
pysimCustomEdit(0);
els['pysim-cf-path'].value = '3F00/6F44';
pysimCustomSubmit();
assert.deepStrictEqual(globalThis.alertCalls, ['Path already exists']);
assert.deepStrictEqual(pysimCustomFiles.map(c => c.path), ['3F00/6F46', '3F00/6F44']);
assert.strictEqual(pysimCustomEditIndex, 0);
});
test('cancel restores the Add mode and clears the inputs', () => {
const els = setup([entry('6F46', 'EF.SPN')]);
pysimCustomEdit(0);
pysimCustomEditCancel();
assert.strictEqual(pysimCustomEditIndex, null);
assert.strictEqual(els['pysim-cf-path'].value, '');
assert.strictEqual(els['pysim-cf-name'].value, '');
assert.strictEqual(els['pysim-cf-add-btn'].textContent, 'Add');
assert.strictEqual(els['pysim-cf-add-btn'].attrs['data-l10n'], 'Add');
assert.ok(els['pysim-cf-cancel-btn'].classList.contains('hidden'));
});
test('deleting the entry being edited cancels the edit', () => {
const els = setup([entry('6F46', 'EF.SPN')]);
pysimCustomEdit(0);
pysimCustomRemove(0);
assert.deepStrictEqual(pysimCustomFiles, []);
assert.strictEqual(pysimCustomEditIndex, null);
assert.strictEqual(els['pysim-cf-add-btn'].textContent, 'Add');
});
test('deleting before the edited row shifts the edit index', () => {
setup([entry('6F46', 'EF.SPN'), entry('6F44', 'EF.SPN2'), entry('6F42', 'EF.SPN3')]);
pysimCustomEdit(2);
pysimCustomRemove(0);
assert.strictEqual(pysimCustomEditIndex, 1);
assert.strictEqual(pysimCustomFiles[1].name, 'EF.SPN3');
});
test('submit adds a new entry when not editing', () => {
const els = setup([]);
els['pysim-cf-path'].value = '3f00/6f46';
els['pysim-cf-name'].value = 'EF.SPN';
pysimCustomSubmit();
assert.deepStrictEqual(pysimCustomFiles, [{ path: '3F00/6F46', name: 'EF.SPN', fid: '6F46', parentFid: '3F00' }]);
assert.strictEqual(els['pysim-cf-path'].value, '');
assert.strictEqual(els['pysim-cf-name'].value, '');
});
+103
View File
@@ -0,0 +1,103 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
function extractFunc(src, name) {
const re = new RegExp('(?:async\\s+)?function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return src.slice(m.index, i + 1);
}
let code = '';
code += extractFunc(html, 'getParentSel') + '\n';
code += extractFunc(html, 'getParentPath') + '\n';
code += extractFunc(html, 'pysimFsLoadChildren') + '\n';
code += 'globalThis.pysimCustomInject = () => {};\n';
eval(code);
let calls = [];
let responses = [];
let renders = 0;
function setup() {
calls = [];
responses = [];
renders = 0;
globalThis.pysimFetch = async (p, body) => {
calls.push({ path: p, body: JSON.parse(JSON.stringify(body)) });
const r = responses.shift();
if (r instanceof Error) throw r;
return JSON.parse(JSON.stringify(r));
};
globalThis.pysimFsRenderTree = () => { renders++; };
const node = { name: 'DF.USIM', fid: '7fff', isDir: true, children: null, exists: null, parent: { name: 'MF', fid: '3f00' } };
return node;
}
test('tree error payload marks the directory as absent', async () => {
const node = setup();
responses = [
{ success: false, error: 'SW ... 6a82', exists: false },
{ success: false, error: 'SW ... 6a82', exists: false },
];
await pysimFsLoadChildren(node);
assert.strictEqual(node.exists, false);
assert.strictEqual(node.children, null);
assert.strictEqual(renders, 1);
assert.strictEqual(calls.length, 1);
assert.strictEqual(calls[0].body.parent_sel, 'MF');
assert.deepStrictEqual(calls[0].body.parent_path, ['MF']);
});
test('error payload without exists is not treated as an empty listing', async () => {
const node = setup();
responses = [
{ success: false, error: 'boom' },
{ success: false, error: 'boom' },
];
await pysimFsLoadChildren(node);
assert.strictEqual(node.exists, false);
assert.strictEqual(node.children, null);
assert.strictEqual(renders, 1);
assert.strictEqual(calls.length, 1);
});
test('a 200 exists:false response marks the directory absent without retrying', async () => {
const node = setup();
responses = [{ exists: false }];
await pysimFsLoadChildren(node);
assert.strictEqual(node.exists, false);
assert.strictEqual(node.children, null);
assert.strictEqual(calls.length, 1);
});
test('a node with loaded children is not fetched again', async () => {
const node = setup();
node.exists = true;
node.children = [{ name: 'EF.UPLMNWLAN', fid: '4f42', isDir: false, exists: true }];
await pysimFsLoadChildren(node);
assert.strictEqual(node.exists, true);
assert.strictEqual(node.children.length, 1);
assert.strictEqual(calls.length, 0);
});
test('empty successful listing keeps the directory present', async () => {
const node = setup();
responses = [{ exists: true, children: [] }];
await pysimFsLoadChildren(node);
assert.strictEqual(node.exists, true);
assert.deepStrictEqual(node.children, []);
});
+156
View File
@@ -0,0 +1,156 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
function extractFunc(src, name) {
const re = new RegExp('(?:async\\s+)?function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return src.slice(m.index, i + 1);
}
let code = 'var pysimFsTreeRoot = null;\nvar _pysimFsProbe = null;\nvar pysimFsSort = "fid";\n';
for (const fn of ['getParentSel', 'getParentPath', 'pysimFsSortChildren', 'pysimFsLoadChildren', 'pysimFsSelectBody', 'pysimFsProbeUi', 'pysimFsProbeAll']) {
code += extractFunc(html, fn) + '\n';
}
code += 'globalThis.esc = s => s;\nglobalThis.t = s => s;\nglobalThis.pysimCustomInject = () => {};\n';
eval(code);
function fakeEl() {
const classes = new Set();
return {
textContent: '',
attrs: {},
classList: {
add: (...cs) => cs.forEach(c => classes.add(c)),
remove: (...cs) => cs.forEach(c => classes.delete(c)),
contains: c => classes.has(c),
toggle: (c, on) => { if (on === undefined ? !classes.has(c) : on) classes.add(c); else classes.delete(c); },
},
setAttribute(k, v) { this.attrs[k] = v; },
};
}
let els = {};
let calls = [];
function setup(routes) {
els = { 'pysim-fs-probe-status': fakeEl(), 'pysim-fs-probe-btn': fakeEl() };
calls = [];
globalThis.document = { getElementById: id => els[id] || null };
globalThis.pysimFetch = async (p, body) => {
calls.push({ path: p, body: body || {} });
for (const r of routes) {
if (r.path !== p) continue;
if (r.name && (!body || body.name !== r.name)) continue;
return typeof r.reply === 'function' ? r.reply(body, calls) : JSON.parse(JSON.stringify(r.reply));
}
throw new Error('unexpected fetch ' + p + ' ' + JSON.stringify(body));
};
globalThis.pysimFsRenderTree = () => {};
}
function root(children) {
pysimFsTreeRoot = { name: 'MF', fid: '3f00', isDir: true, expanded: true, exists: true, children: null, parent: null };
pysimFsTreeRoot.children = children.map(c => Object.assign({ children: null, expanded: false, exists: true, parent: pysimFsTreeRoot }, c));
return pysimFsTreeRoot;
}
const df = (name, fid) => ({ name, fid, isDir: true });
const ef = (name, fid) => ({ name, fid, isDir: false });
const selectNames = () => calls.filter(c => c.path === '/api/select').map(c => c.body.name);
test('probes dirs and files, including custom entries, and reports counts', async () => {
root([df('DF.A', '5f01'), ef('EF.ROOT', '2f01'), Object.assign(ef('EF.CUSTOM', '6fcc'), { custom: true })]);
let sawStop = null;
setup([
{ path: '/api/tree', name: 'DF.A', reply: { exists: true, children: [{ name: 'EF.1', fid: '6f01', isDir: false }] } },
{ path: '/api/select', name: 'EF.1', reply: () => { sawStop = els['pysim-fs-probe-btn'].textContent; return { exists: true }; } },
{ path: '/api/select', name: 'EF.ROOT', reply: { error: 'SW 6a82', exists: false } },
{ path: '/api/select', name: 'EF.CUSTOM', reply: { exists: true } },
]);
await pysimFsProbeAll();
assert.strictEqual(sawStop, 'Stop');
assert.deepStrictEqual(selectNames(), ['EF.1', 'EF.ROOT', 'EF.CUSTOM']);
assert.strictEqual(pysimFsTreeRoot.children[0].children[0].exists, true);
assert.strictEqual(pysimFsTreeRoot.children[1].exists, false);
assert.strictEqual(pysimFsTreeRoot.children[2].exists, true);
const status = els['pysim-fs-probe-status'].textContent;
assert.match(status, /4\/4 files/);
assert.match(status, /3 present/);
assert.match(status, /1 absent/);
assert.strictEqual(els['pysim-fs-probe-btn'].textContent, 'Probe all files');
assert.strictEqual(els['pysim-fs-probe-btn'].attrs['data-l10n'], 'Probe all files');
});
test('an absent directory is marked and its subtree is never fetched', async () => {
root([df('DF.B', '5f02'), ef('EF.ROOT', '2f01')]);
setup([
{ path: '/api/tree', name: 'DF.B', reply: { success: false, error: 'SW 6a82', exists: false } },
{ path: '/api/select', name: 'EF.ROOT', reply: { error: 'SW 6a82', exists: false } },
]);
await pysimFsProbeAll();
assert.strictEqual(pysimFsTreeRoot.children[0].exists, false);
assert.deepStrictEqual(selectNames(), ['EF.ROOT']);
assert.strictEqual(calls.filter(c => c.path === '/api/tree' && c.body.name === 'DF.B').length, 1);
const status = els['pysim-fs-probe-status'].textContent;
assert.match(status, /2\/2 files/);
assert.match(status, /0 present/);
assert.match(status, /2 absent/);
});
test('stop halts the walk and still reports a summary', async () => {
root([ef('EF.X', '6f01'), ef('EF.Y', '6f02')]);
setup([
{ path: '/api/select', name: 'EF.X', reply: () => { _pysimFsProbe.stop = true; return { exists: true }; } },
]);
await pysimFsProbeAll();
assert.deepStrictEqual(selectNames(), ['EF.X']);
const status = els['pysim-fs-probe-status'].textContent;
assert.ok(status.startsWith('Stopped —'), status);
assert.strictEqual(els['pysim-fs-probe-btn'].textContent, 'Probe all files');
});
test('select bodies carry the parent path and set allow_probe only for custom files', async () => {
root([df('DF.A', '5f01'), Object.assign(ef('EF.CUSTOM', '6fcc'), { custom: true })]);
const body = pysimFsSelectBody(pysimFsTreeRoot.children[0]);
assert.deepStrictEqual(body.parent_path, ['MF']);
assert.strictEqual(body.parent_sel, 'MF');
assert.ok(!body.allow_probe);
const custom = pysimFsSelectBody(pysimFsTreeRoot.children[1]);
assert.deepStrictEqual(custom.parent_path, ['MF']);
assert.strictEqual(custom.allow_probe, true);
const nested = { name: 'EF.1', fid: '6f01', isDir: false, parent: pysimFsTreeRoot.children[0] };
assert.deepStrictEqual(pysimFsSelectBody(nested).parent_path, ['MF', '5f01']);
});
test('children of an absent directory are neither fetched nor selected', async () => {
const stale = Object.assign(df('DF.C', '5f03'), {
exists: false,
children: [{ name: 'EF.STALE', fid: '6f0e', isDir: false, exists: true, children: null }],
});
root([stale, ef('EF.ROOT', '2f01')]);
setup([
{ path: '/api/select', name: 'EF.ROOT', reply: { exists: true } },
]);
await pysimFsProbeAll();
assert.deepStrictEqual(selectNames(), ['EF.ROOT']);
assert.strictEqual(stale.exists, false);
assert.strictEqual(calls.filter(c => c.path === '/api/tree').length, 0);
const status = els['pysim-fs-probe-status'].textContent;
assert.match(status, /2\/2 files/);
assert.match(status, /1 present/);
assert.match(status, /1 absent/);
});
+67
View File
@@ -0,0 +1,67 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
function extractFunc(src, name) {
const re = new RegExp('(?:async\\s+)?function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return src.slice(m.index, i + 1);
}
let code = 'var pysimFsSort = "fid";\n';
code += extractFunc(html, 'pysimFsSortChildren') + '\n';
code += extractFunc(html, 'pysimFsRenderNode') + '\n';
code += 'globalThis.esc = s => s;\nglobalThis.t = s => s;\n';
eval(code);
const ef = (name, fid) => ({ name, fid, isDir: false, exists: true, children: null, expanded: false });
const df = (name, fid, extra) => Object.assign({ name, fid, isDir: true, exists: true, children: null, expanded: false }, extra || {});
test('absent directory renders a cross and no expand toggle', () => {
const node = df('DF.WLAN', '5f40', { exists: false, children: [] });
const out = pysimFsRenderNode(node, 0);
assert.ok(out.includes('✗'), out);
assert.ok(!out.includes('pysimFsToggleDir'), out);
assert.ok(!out.includes('▶'), out);
});
test('expanded empty directory shows the (empty) placeholder', () => {
const node = df('DF.EMPTY', '5f00', { children: [], expanded: true });
const out = pysimFsRenderNode(node, 0);
assert.ok(out.includes('(empty)'), out);
});
test('expanded directory with children renders no placeholder', () => {
const node = df('DF.GSM', '7f20', { children: [ef('EF.IMSI', '6f07')], expanded: true });
const out = pysimFsRenderNode(node, 0);
assert.ok(out.includes('EF.IMSI'), out);
assert.ok(!out.includes('(empty)'), out);
});
test('collapsed directory hides its children', () => {
const node = df('DF.GSM', '7f20', { children: [ef('EF.IMSI', '6f07')], expanded: false });
const out = pysimFsRenderNode(node, 0);
assert.ok(!out.includes('EF.IMSI'), out);
});
test('non-existing directory hides previously loaded children', () => {
const node = df('DF.WLAN', '5f40', { exists: false, expanded: true, children: [ef('EF.UPLMNWLAN', '4f42')] });
const out = pysimFsRenderNode(node, 0);
assert.ok(out.includes('✗'), out);
assert.ok(!out.includes('EF.UPLMNWLAN'), out);
assert.ok(!out.includes('(empty)'), out);
assert.ok(!out.includes('pysimFsToggleDir'), out);
});
+66
View File
@@ -0,0 +1,66 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
function extractFunc(src, name) {
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return src.slice(m.index, i + 1);
}
eval(extractFunc(html, 'pysimFsSortChildren'));
const f = (fid, name) => ({ fid, name, isDir: false });
const d = (fid, name) => ({ fid, name, isDir: true });
test('DFs sort before EFs in both modes', () => {
const children = [f('6F07', 'EF.IMSI'), d('7F20', 'DF.GSM'), f('2FE2', 'EF.ICCID'), d('7F10', 'DF.TELECOM')];
assert.deepStrictEqual(pysimFsSortChildren(children, 'fid').map(x => x.fid), ['7F10', '7F20', '2FE2', '6F07']);
assert.deepStrictEqual(pysimFsSortChildren(children, 'name').map(x => x.name), ['DF.GSM', 'DF.TELECOM', 'EF.ICCID', 'EF.IMSI']);
});
test('FID mode sorts EFs numerically by FID string', () => {
const children = [f('6F3A', 'EF.ADN'), f('2FE2', 'EF.ICCID'), f('6F07', 'EF.IMSI')];
assert.deepStrictEqual(pysimFsSortChildren(children, 'fid').map(x => x.fid), ['2FE2', '6F07', '6F3A']);
});
test('name mode sorts case-insensitively', () => {
const children = [f('6F3A', 'EF.ADN'), f('2FE2', 'ef.iccid'), f('6F07', 'EF.IMSI')];
assert.deepStrictEqual(pysimFsSortChildren(children, 'name').map(x => x.name), ['EF.ADN', 'ef.iccid', 'EF.IMSI']);
});
test('missing name falls back to the FID as the sort key', () => {
const children = [f('2FE2', null), f('6F07', 'EF.IMSI'), f('6F3A', '')];
// keys: '2FE2', 'EF.IMSI', '6F3A' -> '2FE2' < '6F3A' < 'EF.IMSI'
assert.deepStrictEqual(pysimFsSortChildren(children, 'name').map(x => x.fid), ['2FE2', '6F3A', '6F07']);
});
test('custom entries use their isDir flag for the DF priority', () => {
const children = [f('6F3A', 'EF.ADN'), d('7F20', 'DF.GSM')];
assert.strictEqual(pysimFsSortChildren(children, 'fid')[0].name, 'DF.GSM');
});
test('equal keys keep a deterministic tie-break by the other field', () => {
const children = [f('6F07', 'EF.SAME'), f('2FE2', 'EF.SAME')];
assert.deepStrictEqual(pysimFsSortChildren(children, 'name').map(x => x.fid), ['2FE2', '6F07']);
assert.deepStrictEqual(pysimFsSortChildren(children, 'fid').map(x => x.fid), ['2FE2', '6F07']);
});
test('does not mutate the input array', () => {
const children = [f('6F3A', 'B'), f('2FE2', 'A')];
pysimFsSortChildren(children, 'fid');
assert.deepStrictEqual(children.map(x => x.fid), ['6F3A', '2FE2']);
});
+90
View File
@@ -0,0 +1,90 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
const opens = (html.match(/<div\b/g) || []).length;
const closes = (html.match(/<\/div>/g) || []).length;
test('HTML <div> tags are balanced', () => {
assert.strictEqual(opens, closes, `Unbalanced divs: ${opens} opens vs ${closes} closes`);
});
test('top-level tabs match the rearranged views', () => {
const tabs = [...html.matchAll(/class="tab-btn[^"]*" data-tab="([^"]+)"/g)].map(m => m[1]);
assert.deepStrictEqual(tabs, ['c-apdu', 'scp80', 'profiler', 'pysim', 'phone']);
assert.match(html, /data-tab="c-apdu">Remote APDU</);
});
test('response parser is a Remote APDU pill', () => {
assert.match(html, /data-sub="response" onclick="cApduSwitchSubtab\('response'\)"/);
assert.ok(html.includes('id="c-apdu-sub-response"'));
});
test('profiler and phone simulator are top-level tab contents', () => {
assert.ok(html.includes('id="tab-profiler" class="tab-content hidden"'));
assert.ok(html.includes('id="tab-phone" class="tab-content hidden"'));
});
test('phone simulator has Phone / TR Config pills', () => {
assert.match(html, /data-phone-sub="phone" onclick="phoneSwitchSubtab\('phone'\)"/);
assert.match(html, /data-phone-sub="tr" onclick="phoneSwitchSubtab\('tr'\)"/);
assert.ok(html.includes('id="phone-sub-phone"'));
assert.ok(html.includes('id="phone-sub-tr"'));
});
test('scan name input starts scanning on Enter', () => {
assert.match(html, /id="profiler-scan-name"[^>]*onkeydown="profilerScanNameKeydown\(event\)"/);
});
test('snapshot view has a timing summary block', () => {
assert.ok(html.includes('id="snapshot-summary"'));
});
test('header state indicator and profiler custom-files tab', () => {
assert.ok(html.includes('id="state-indicator"'));
assert.ok(html.includes('id="profiler-list-custom"'));
assert.ok(html.includes('data-list-tab="custom"'));
assert.ok(!html.includes('data-pysim-sub="custom"'));
});
test('file manager has FID / Name sort pills', () => {
assert.match(html, /data-fs-sort="fid" onclick="pysimFsSetSort\('fid'\)"/);
assert.match(html, /data-fs-sort="name" onclick="pysimFsSetSort\('name'\)"/);
assert.ok(html.includes('pysim-fs-sort-pill'));
});
test('custom files form has add/save and cancel controls', () => {
assert.match(html, /id="pysim-cf-add-btn"[^>]*data-l10n="Add"/);
assert.match(html, /id="pysim-cf-cancel-btn"[^>]*class="hidden[^"]*"[^>]*data-l10n="Cancel"/);
assert.ok(html.includes("event.key==='Enter')pysimCustomSubmit()"));
assert.ok(!html.includes('pysimCustomAdd'));
});
test('file manager has a probe-all-files button and status line', () => {
assert.match(html, /id="pysim-fs-probe-btn"[^>]*data-needs="card"/);
assert.match(html, /id="pysim-fs-probe-btn"[^>]*data-l10n="Probe all files"/);
assert.ok(html.includes('onclick="pysimFsProbeAll()"'));
assert.ok(html.includes('id="pysim-fs-probe-status"'));
});
test('file manager shows FCI info and keeps the selection in state, not the DOM', () => {
const detail = html.indexOf('id="pysim-fs-detail"');
const info = html.indexOf('id="pysim-fs-info"');
const content = html.indexOf('id="pysim-fs-content"');
assert.ok(detail !== -1 && info > detail && info < content, 'pysim-fs-info must sit above the content');
assert.ok(html.includes('function pysimFsInfoHtml'));
assert.ok(html.includes("pysimFsInfoHtml(sel)"));
assert.ok(!html.includes('pysim-fs-filename'));
assert.ok(html.includes('let pysimFsSelected = null;'));
assert.ok(html.includes('pysimFsSelected = name;'));
assert.ok(!html.includes('pysimFsSelect()'));
});
test('profile list has a Profile from snapshot button', () => {
assert.match(html, /data-l10n="Profile from snapshot">Profile from snapshot</);
assert.ok(html.includes('onclick="profilerFromSnapshot()"'));
assert.ok(html.includes('function profilerScanFromSnapshot(si)'));
assert.ok(html.includes('function profilerBuildFileRuleFromSnapshot('));
});
+142
View File
@@ -0,0 +1,142 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
function extractFunc(src, name) {
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return src.slice(m.index, i + 1);
}
const FNS = ['_hotaHex', '_hotaAsciiHex', '_hotaBerLen', '_hotaTlv',
'hotaTlv', 'hotaBuildConn', 'hotaBuildSec', 'hotaBuildRetry',
'hotaBuildHttpPost', 'hotaBuildTrigger', 'hotaBuildStore', 'hotaBuild'];
let code = '';
for (const f of FNS) code += extractFunc(html, f) + '\n';
eval(code);
test('_hotaHex strips non-hex and uppercases', () => {
assert.strictEqual(_hotaHex(' 81 82 '), '8182');
assert.strictEqual(_hotaHex('b0-00'), 'B000');
assert.strictEqual(_hotaHex(''), '');
assert.strictEqual(_hotaHex(undefined), '');
});
test('_hotaAsciiHex encodes text as ASCII bytes', () => {
assert.strictEqual(_hotaAsciiHex('megafon.ru'), '6D656761666F6E2E7275');
assert.strictEqual(_hotaAsciiHex('v1.0'), '76312E30');
assert.strictEqual(_hotaAsciiHex('/sd'), '2F7364');
assert.strictEqual(_hotaAsciiHex(''), '');
});
test('_hotaBerLen encodes definite lengths', () => {
assert.strictEqual(_hotaBerLen(0), '00');
assert.strictEqual(_hotaBerLen(11), '0B');
assert.strictEqual(_hotaBerLen(127), '7F');
assert.strictEqual(_hotaBerLen(128), '8180');
assert.strictEqual(_hotaBerLen(255), '81FF');
assert.strictEqual(_hotaBerLen(256), '820100');
});
test('hotaTlv wraps tag + definite length + value', () => {
assert.strictEqual(hotaTlv('02', '8182'), '02028182');
assert.strictEqual(hotaTlv('05', ''), '0500');
assert.strictEqual(hotaTlv('8A', '6D656761666F6E2E7275'), '8A0A6D656761666F6E2E7275');
});
test('hotaBuildConn produces a full 84 TLV', () => {
assert.strictEqual(hotaBuildConn([{ tag: '02', value: '8182' }]), '840402028182');
assert.strictEqual(
hotaBuildConn([
{ tag: '01', value: '014001' },
{ tag: '02', value: '81 82' },
{ tag: '05', value: '' },
]),
'840B0103014001020281820500');
assert.strictEqual(hotaBuildConn([{ tag: '02', value: '8182' }, { tag: '', value: 'FF' }]), '840402028182');
assert.strictEqual(hotaBuildConn([]), '8400');
});
test('hotaBuildSec produces a full 85 TLV per Table 4-6', () => {
assert.strictEqual(
hotaBuildSec({ pskIdentity: 'Test123', kvn: '01', kid: '01' }),
'850B0754657374313233020101');
assert.strictEqual(
hotaBuildSec({ pskIdentity: '', kvn: '01', kid: '01' }),
'850400020101');
});
test('hotaBuildRetry uses TS 102 223 timer TLV (25 03) and wraps in 86', () => {
assert.strictEqual(
hotaBuildRetry({ counter: 'B000', delayH: 1, delayM: 2, delayS: 3, reportFailure: '' }),
'8607B0002503010203');
assert.strictEqual(
hotaBuildRetry({ counter: 'b0 00', delayH: '1', delayM: '2', delayS: '3', reportFailure: '0A080102030405060708' }),
'8611B00025030102030A080102030405060708');
assert.strictEqual(
hotaBuildRetry({ counter: '0000', delayH: 0, delayM: 0, delayS: 0, reportFailure: '0A0' }),
'860700002503000000');
});
test('hotaBuildHttpPost wraps 8A/8B/8C in a full 89 TLV', () => {
assert.strictEqual(
hotaBuildHttpPost({ host: '', agent: '', uri: '' }),
'89068A008B008C00');
assert.strictEqual(
hotaBuildHttpPost({ host: 'megafon.ru', agent: 'v1.0', uri: '/sd' }),
'89178A0A6D656761666F6E2E72758B0476312E308C032F7364');
});
test('hotaBuildTrigger wraps 81 > 83 > (84/85/86/89)', () => {
const conn = '840402028182';
const sec = '850400020101';
const retry = '8607B0002503010203';
const httpPost = '89068A008B008C00';
assert.strictEqual(
hotaBuildTrigger(conn, sec, retry, httpPost, false),
'811F831D8404020281828504000201018607B000250301020389068A008B008C00');
});
test('hotaBuildTrigger expanded wraps the 81 command in Command Scripting template AA', () => {
const conn = '840402028182';
const plain = hotaBuildTrigger(conn, '', '', '', false);
const expanded = hotaBuildTrigger(conn, '', '', '', true);
const byteLen = plain.length / 2;
assert.strictEqual(expanded, 'AA' + _hotaBerLen(byteLen) + plain);
assert.ok(expanded.startsWith('AA'));
});
test('hotaBuildStore emits STORE DATA TLV-mode APDU (80 E2 90 00)', () => {
assert.strictEqual(
hotaBuildStore('840402028182', '', '', '', '85'),
'80E29000088506840402028182');
assert.strictEqual(
hotaBuildStore('840402028182', '', '', '', 'A5'),
'80E2900008A506840402028182');
});
test('hotaBuild dispatches on mode', () => {
assert.strictEqual(
hotaBuild('trigger', '840402028182', '', '', '', '85', false),
hotaBuildTrigger('840402028182', '', '', '', false));
assert.strictEqual(
hotaBuild('store', '840402028182', '', '', '', '85', false),
hotaBuildStore('840402028182', '', '', '', '85'));
assert.strictEqual(
hotaBuild('store', '840402028182', '', '8607B0002503010203', '', '85', false),
hotaBuildStore('840402028182', '', '8607B0002503010203', '', '85'));
});
+136
View File
@@ -0,0 +1,136 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
function extractFunc(src, name) {
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return src.slice(m.index, i + 1);
}
let code = 'var _pysimServerAvailable = null;\nvar _pysimCardEquipped = false;\nvar _pysimEquipping = false;\n';
code += extractFunc(html, 'pysimAvailabilityState') + '\n';
code += extractFunc(html, 'pysimUpdateStateIndicator') + '\n';
code += 'globalThis.t = s => s;\n';
eval(code);
function fakeEl() {
const classes = new Set();
return {
classes,
attrs: {},
classList: {
add: (...cs) => cs.forEach(c => classes.add(c)),
remove: (...cs) => cs.forEach(c => classes.delete(c)),
contains: c => classes.has(c),
},
setAttribute(k, v) { this.attrs[k] = v; },
removeAttribute(k) { delete this.attrs[k]; },
};
}
function setup() {
const els = {
'state-indicator': fakeEl(),
'state-indicator-dot': fakeEl(),
'state-indicator-img': fakeEl(),
};
els['state-indicator-img'].src = '';
globalThis.document = { getElementById: id => els[id] || null };
_pysimServerAvailable = null;
_pysimCardEquipped = false;
_pysimEquipping = false;
return els;
}
test('unprobed server shows a gray dot and a Connecting title', () => {
const els = setup();
pysimUpdateStateIndicator();
const { 'state-indicator': wrap, 'state-indicator-dot': dot, 'state-indicator-img': img } = els;
assert.ok(dot.classes.has('text-gray-400'));
assert.ok(!dot.classes.has('hidden'));
assert.ok(img.classes.has('hidden'));
assert.strictEqual(wrap.attrs.title, 'Connecting...');
assert.strictEqual(dot.attrs.title, 'Connecting...');
});
test('unreachable server shows a red dot', () => {
const els = setup();
_pysimServerAvailable = false;
pysimUpdateStateIndicator();
const { 'state-indicator': wrap, 'state-indicator-dot': dot, 'state-indicator-img': img } = els;
assert.ok(dot.classes.has('text-red-500'));
assert.ok(!dot.classes.has('text-gray-400'));
assert.ok(img.classes.has('hidden'));
assert.strictEqual(wrap.attrs.title, 'No server connection');
assert.strictEqual(dot.attrs.title, 'No server connection');
});
test('server up without a card shows nosim.svg', () => {
const els = setup();
_pysimServerAvailable = true;
pysimUpdateStateIndicator();
const { 'state-indicator': wrap, 'state-indicator-dot': dot, 'state-indicator-img': img } = els;
assert.ok(dot.classes.has('hidden'));
assert.ok(!img.classes.has('hidden'));
assert.strictEqual(img.src, 'nosim.svg');
assert.strictEqual(wrap.attrs.title, 'Server connected, no card equipped');
assert.strictEqual(dot.attrs.title, undefined);
});
test('equipped card shows sim.svg', () => {
const els = setup();
_pysimServerAvailable = true;
_pysimCardEquipped = true;
pysimUpdateStateIndicator();
const { 'state-indicator': wrap, 'state-indicator-img': img } = els;
assert.strictEqual(img.src, 'sim.svg');
assert.strictEqual(wrap.attrs.title, 'Card equipped');
});
test('equipping shows the animated sim_anim.svg', () => {
const els = setup();
_pysimServerAvailable = true;
_pysimEquipping = true;
pysimUpdateStateIndicator();
const { 'state-indicator': wrap, 'state-indicator-img': img } = els;
assert.strictEqual(img.src, 'sim_anim.svg');
assert.strictEqual(wrap.attrs.title, 'Card inserted — initializing...');
});
test('dot color transitions do not accumulate', () => {
const els = setup();
_pysimServerAvailable = false;
pysimUpdateStateIndicator();
_pysimServerAvailable = null;
pysimUpdateStateIndicator();
const dot = els['state-indicator-dot'];
assert.ok(dot.classes.has('text-gray-400'));
assert.ok(!dot.classes.has('text-red-500'));
});
test('indicator markup carries the dot and image elements', () => {
assert.match(html, /id="state-indicator-dot"/);
assert.match(html, /id="state-indicator-img"[^>]*src="nosim\.svg"/);
});
test('indicator image stays within the 32px header row budget', () => {
const m = /id="state-indicator-img"[^>]*style="width:(\d+)px;height:(\d+)px"/.exec(html);
assert.ok(m, 'inline image size not found');
assert.strictEqual(m[1], m[2]);
const size = Number(m[1]);
assert.ok(size >= 24 && size <= 32, 'size ' + size + 'px would change the header height');
});
+83
View File
@@ -0,0 +1,83 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
function extractFunc(src, name) {
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return src.slice(m.index, i + 1);
}
const code = extractFunc(html, 'phoneSwitchSubtab') + '\n' +
'globalThis.setHelpAnchor = a => { globalThis._anchor = a; };\n' +
'globalThis.stkCheckMenu = () => { globalThis._stk = (globalThis._stk || 0) + 1; };\n' +
'globalThis.pysimEventsRender = () => { globalThis._events = (globalThis._events || 0) + 1; };\n' +
'globalThis.pysimProactiveLogRender = () => { globalThis._log = (globalThis._log || 0) + 1; };\n' +
'globalThis.pysimPollStatusInit = () => { globalThis._poll = (globalThis._poll || 0) + 1; };\n' +
'globalThis.pysimPliRender = () => { globalThis._pli = (globalThis._pli || 0) + 1; };\n';
eval(code);
function makeClassList() {
const set = new Set();
return {
toggle: (c, on) => { on ? set.add(c) : set.delete(c); },
has: c => set.has(c),
};
}
function setup() {
const buttons = [
{ dataset: { phoneSub: 'phone' }, classList: makeClassList() },
{ dataset: { phoneSub: 'tr' }, classList: makeClassList() },
];
const panels = {
'phone-sub-phone': { classList: makeClassList() },
'phone-sub-tr': { classList: makeClassList() },
};
globalThis.document = {
querySelectorAll: sel => (sel === '.phone-subtab' ? buttons : []),
getElementById: id => panels[id] || null,
};
globalThis._anchor = null;
globalThis._stk = globalThis._events = globalThis._log = globalThis._poll = globalThis._pli = 0;
return { buttons, panels };
}
test('TR Config pill shows the TR panel and renders PLI data', () => {
const { buttons, panels } = setup();
phoneSwitchSubtab('tr');
assert.ok(!panels['phone-sub-tr'].classList.has('hidden'));
assert.ok(panels['phone-sub-phone'].classList.has('hidden'));
assert.ok(buttons[1].classList.has('bg-blue-600'));
assert.ok(!buttons[0].classList.has('bg-blue-600'));
assert.strictEqual(globalThis._anchor, 'pli-dict');
assert.strictEqual(globalThis._pli, 1);
assert.strictEqual(globalThis._stk, 0);
});
test('Phone pill shows the phone panel and renders CAT views', () => {
const { buttons, panels } = setup();
phoneSwitchSubtab('phone');
assert.ok(!panels['phone-sub-phone'].classList.has('hidden'));
assert.ok(panels['phone-sub-tr'].classList.has('hidden'));
assert.ok(buttons[0].classList.has('bg-blue-600'));
assert.strictEqual(globalThis._anchor, 'stk-menu');
assert.strictEqual(globalThis._stk, 1);
assert.strictEqual(globalThis._events, 1);
assert.strictEqual(globalThis._log, 1);
assert.strictEqual(globalThis._poll, 1);
assert.strictEqual(globalThis._pli, 0);
});
File diff suppressed because it is too large Load Diff
+322
View File
@@ -0,0 +1,322 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
function extractFunc(src, name) {
const re = new RegExp('(?:async\\s+)?function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return src.slice(m.index, i + 1);
}
// Extract chain builder functions and dependencies
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml',
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute'];
let code = '';
for (const f of FNS) {
code += extractFunc(html, f) + '\n';
}
const m = html.match(/const _chains = \{\};/);
if (m) code += m[0].replace(/^const /, 'var ') + '\n';
const lc = html.match(/const RAM_LIFECYCLE = \{[\s\S]*?\n\};/);
if (lc) code += lc[0].replace(/^const /, 'var ') + '\n';
eval(code);
code += 'var _ramCardIdx = null;\nvar _ramOpLast = null;\nvar _ramExplorerData = null;\n';
eval(code);
const els = {};
const doc = { getElementById: (id) => { if (!els[id]) els[id] = {value:''}; return els[id]; } };
global.document = doc;
function reset() { for (const id of Object.keys(els)) delete els[id]; }
function genRamResult(fields) {
const row = { cmd: fields.cmd, fields: fields };
return chainRamBuildRowHex(0, row);
}
function genRamApdu() {
return genRamResult({
cmd: 'install-install',
aid: 'A000000151000000',
elfAid: '',
modAid: '',
priv: '00',
tkEnabled: true,
tkMode: 'ea',
tkPriority: '0',
tkTimers: '0',
tkTextlen: '0',
tkMenus: '2',
tkFirstpos: '1',
tkFirstid: '01',
tkLastpos: '2',
tkLastid: '02',
tkChannels: '0',
tkMsl: '16',
tkTar: 'B00001',
tkAd: '',
tkServices: '0',
});
}
test('UICC toolkit nested inside EA (m=2, services 0)', () => {
const apdu = genRamApdu();
assert.ok(apdu.includes('EA13801100000002010102020002011603B0000100'), apdu);
});
test('UICC m=1 emits single pair', () => {
const apdu = genRamResult({
cmd: 'install-install', aid: 'A000000151000000', priv: '00',
tkEnabled: true, tkMode: 'ea', tkPriority: '0', tkTimers: '0', tkTextlen: '0',
tkMenus: '1', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '0', tkLastid: '00',
tkChannels: '0', tkMsl: '16', tkTar: 'B00001', tkAd: '', tkServices: '0',
});
assert.ok(apdu.includes('EA11800F0000000101010002011603B0000100'), apdu);
});
test('UICC m=3 fills middle pair with 0000', () => {
const apdu = genRamResult({
cmd: 'install-install', aid: 'A000000151000000', priv: '00',
tkEnabled: true, tkMode: 'ea', tkPriority: '0', tkTimers: '0', tkTextlen: '0',
tkMenus: '3', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '3', tkLastid: '03',
tkChannels: '0', tkMsl: '16', tkTar: 'B00001', tkAd: '', tkServices: '0',
});
assert.ok(apdu.includes('EA158013000000030101000003030002011603B0000100'), apdu);
});
test('UICC services 7 appended as final byte', () => {
const apdu = genRamResult({
cmd: 'install-install', aid: 'A000000151000000', priv: '00',
tkEnabled: true, tkMode: 'ea', tkPriority: '0', tkTimers: '0', tkTextlen: '0',
tkMenus: '2', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '2', tkLastid: '02',
tkChannels: '0', tkMsl: '16', tkTar: 'B00001', tkAd: '', tkServices: '7',
});
assert.ok(apdu.includes('EA13801100000002010102020002011603B0000107'), apdu);
});
test('SIM (CA) access domain FIRST, no services byte', () => {
const apdu = genRamResult({
cmd: 'install-install', aid: 'A000000151000000', priv: '00',
tkEnabled: true, tkMode: 'ca', tkPriority: '0', tkTimers: '0', tkTextlen: '0',
tkMenus: '2', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '2', tkLastid: '02',
tkChannels: '0', tkMsl: '16', tkTar: 'B00001', tkAd: '5A', tkServices: '0',
});
assert.ok(apdu.includes('EF14CA12015A00000002010102020002011603B00001'), apdu);
});
test('SIM (CA) blank access domain emits length byte 00', () => {
const apdu = genRamResult({
cmd: 'install-install', aid: 'A000000151000000', priv: '00',
tkEnabled: true, tkMode: 'ca', tkPriority: '0', tkTimers: '0', tkTextlen: '0',
tkMenus: '2', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '2', tkLastid: '02',
tkChannels: '0', tkMsl: '16', tkTar: 'B00001', tkAd: '', tkServices: '0',
});
assert.ok(apdu.includes('EF13CA110000000002010102020002011603B00001'), apdu);
});
test('SIM (CA) m=3, no TAR, blank access domain', () => {
const apdu = genRamResult({
cmd: 'install-install', aid: 'A000000151000000', priv: '00',
tkEnabled: true, tkMode: 'ca', tkPriority: '0', tkTimers: '0', tkTextlen: '0',
tkMenus: '3', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '3', tkLastid: '03',
tkChannels: '0', tkMsl: '16', tkTar: '', tkAd: '', tkServices: '0',
});
assert.ok(apdu.includes('EF12CA1000000000030101000003030002011600'), apdu);
});
test('UICC m=60 uses long-form BER lengths (EA 81 87 / inner 81 84)', () => {
const apdu = genRamResult({
cmd: 'install-install', aid: 'A000000151000000', priv: '00',
tkEnabled: true, tkMode: 'ea', tkPriority: '0', tkTimers: '0', tkTextlen: '0',
tkMenus: '60', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '60', tkLastid: '3C',
tkChannels: '0', tkMsl: '16', tkTar: 'B00001', tkAd: '', tkServices: '0',
});
assert.ok(apdu.includes('EA8188808185'), apdu);
});
test('LOAD P1 fixed to 80 (last block)', () => {
const apdu = genRamResult({ cmd: 'load', data: 'AABBCC', block: '0' });
assert.ok(apdu.startsWith('80E88000'), apdu);
});
test('DELETE: P1=00, mode in P2', () => {
let apdu = genRamResult({ cmd: 'delete', aid: 'AA1902BC225501', delMode: '00' });
assert.ok(apdu.startsWith('80E40000'), apdu);
apdu = genRamResult({ cmd: 'delete', aid: 'AA1902BC225501', delMode: '80' });
assert.ok(apdu.startsWith('80E40080'), apdu);
});
test('STORE DATA ram-enc P1 values 00/40/80/C0/E0', () => {
for (const [enc, p1] of [['00','00'],['40','40'],['80','80'],['C0','C0'],['E0','E0']]) {
const apdu = genRamResult({ cmd: 'store-data', data: 'AABB', enc: enc, block: '0' });
assert.ok(apdu.startsWith('80E2' + p1 + '00'), enc + ' -> P1 ' + p1);
}
});
test('ramRenderExploreHtml localizes every label and button', () => {
const seen = [];
global.t = s => { seen.push(s); return 'XX' + s; };
const out = ramRenderExploreHtml(
{ appCount: 5, freeNV: 100, freeV: 50 },
[{ aid: 'A000000151000000', lifecycle: '07', privileges: '', sdAid: 'A000000151000000' }],
[{ aid: 'A1130001180001', lifecycle: '07', privileges: '80', implicitSel: '00', elfAid: 'ELF1' }],
[{ aid: 'ELF1', lifecycle: '01', version: '1.0', moduleAids: ['M1'], sdAid: null }]
);
delete global.t;
assert.ok(out.includes('XXDelete'), out);
assert.ok(out.includes('XXDelete All'), out);
assert.ok(out.includes('XXApplications:'), out);
assert.ok(out.includes('XXFree NV:'), out);
assert.ok(out.includes('XXFree Volatile:'), out);
assert.ok(out.includes('XXAID:'), out);
assert.ok(out.includes('XXLifecycle:'), out);
assert.ok(out.includes('XXPrivileges:'), out);
assert.ok(out.includes('XXSD AID:'), out);
assert.ok(out.includes('XXImplicit sel:'), out);
assert.ok(out.includes('XXVersion:'), out);
assert.ok(seen.includes('Application / Instance AID:'));
assert.ok(seen.includes('Load File AID / Package AID:'));
assert.ok(seen.includes('Executable Module AIDs / Applet Class AIDs:'));
assert.ok(!out.includes('data-l10n'), out);
});
test('ramFmtPrivileges uses the translated (none) placeholder', () => {
global.t = s => 'XX' + s;
assert.strictEqual(ramFmtPrivileges(''), 'XX(none)');
assert.strictEqual(ramFmtPrivileges('00'), 'XX(none)');
delete global.t;
});
function fakeClassList() {
const set = new Set();
return {
add: (...cs) => cs.forEach(c => set.add(c)),
remove: (...cs) => cs.forEach(c => set.delete(c)),
contains: c => set.has(c),
toggle: (c, on) => { if (on === undefined ? !set.has(c) : on) set.add(c); else set.delete(c); },
};
}
function fakeEl(id) {
return {
id,
value: '',
innerHTML: '',
textContent: '',
classList: fakeClassList(),
options: [],
appendChild(opt) { this.options.push(opt); },
};
}
function fakeRamDocument(ids) {
const els = {};
for (const id of ids) els[id] = fakeEl(id);
const sel = els['ram-card-sel'];
if (sel) {
Object.defineProperty(sel, 'innerHTML', {
get() { return this._html || ''; },
set(v) { this._html = v; this.value = ''; },
});
}
globalThis.document = {
getElementById: id => els[id] || null,
createElement: () => fakeEl('option'),
};
return els;
}
test('ramOpChanged clears the executed status only on a real op change', () => {
const els = fakeRamDocument(['ram-op', 'ram-install-params', 'ram-result', 'ram-explorer', 'ram-steps', 'ram-progress']);
_ramOpLast = null;
els['ram-op'].value = 'explore';
ramOpChanged();
assert.ok(!els['ram-result'].classList.contains('hidden'));
els['ram-result'].classList.remove('hidden');
els['ram-steps'].classList.remove('hidden');
ramOpChanged();
assert.ok(!els['ram-result'].classList.contains('hidden'), 'same op must keep the result');
els['ram-op'].value = 'install-cap';
ramOpChanged();
assert.ok(els['ram-result'].classList.contains('hidden'));
assert.ok(els['ram-steps'].classList.contains('hidden'));
assert.ok(els['ram-explorer'].classList.contains('hidden'));
assert.ok(els['ram-progress'].classList.contains('hidden'));
assert.ok(!els['ram-install-params'].classList.contains('hidden'));
});
test('ramRender keeps the selected card preset across rebuilds', () => {
const els = fakeRamDocument(['ram-card-sel', 'ram-op', 'ram-install-params', 'ram-result', 'ram-explorer', 'ram-steps', 'ram-progress']);
globalThis.cards = [{ name: 'A' }, { name: 'B' }, { name: 'C' }];
_ramCardIdx = null;
_ramOpLast = 'explore';
els['ram-op'].value = 'explore';
ramRender();
assert.strictEqual(els['ram-card-sel'].value, '');
els['ram-card-sel'].value = '1';
ramRender();
assert.strictEqual(els['ram-card-sel'].value, '1');
els['ram-card-sel'].value = '';
_ramCardIdx = 2;
ramRender();
assert.strictEqual(els['ram-card-sel'].value, '2');
globalThis.cards = [{ name: 'A' }];
_ramCardIdx = 2;
ramRender();
assert.strictEqual(els['ram-card-sel'].value, '');
delete globalThis.cards;
});
test('ramApplyCard remembers a valid picked preset', () => {
globalThis.cards = [{ name: 'A' }, { name: 'B' }];
let applied = null;
globalThis.cardsApply = i => { applied = i; };
_ramCardIdx = null;
ramApplyCard('1');
assert.strictEqual(_ramCardIdx, 1);
assert.strictEqual(applied, '1');
ramApplyCard('');
assert.strictEqual(_ramCardIdx, 1, 'invalid pick must not forget the preset');
delete globalThis.cards;
delete globalThis.cardsApply;
});
test('ramExecute commits the dropdown selection before running', async () => {
const els = fakeRamDocument(['ram-card-sel', 'ram-op', 'ram-install-params', 'ram-result', 'ram-explorer', 'ram-steps', 'ram-progress']);
globalThis.cards = [{ name: 'A' }];
globalThis.getRamSpParams = () => ({ kicKey: '11', kidKey: '22' });
let explored = false;
globalThis.ramExplore = async () => { explored = true; };
globalThis.alert = () => {};
_ramCardIdx = null;
els['ram-card-sel'].value = '0';
els['ram-op'].value = 'explore';
await ramExecute();
assert.strictEqual(_ramCardIdx, 0);
assert.ok(explored);
delete globalThis.cards;
delete globalThis.getRamSpParams;
delete globalThis.ramExplore;
delete globalThis.alert;
});
test('ramCardIdxAfterRemove keeps the remembered index aligned', () => {
assert.strictEqual(ramCardIdxAfterRemove(2, 0), 1);
assert.strictEqual(ramCardIdxAfterRemove(0, 0), null);
assert.strictEqual(ramCardIdxAfterRemove(0, 2), 0);
assert.strictEqual(ramCardIdxAfterRemove(null, 1), null);
});
+68
View File
@@ -0,0 +1,68 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
function extractBlock(startMarker, endMarker) {
const start = html.indexOf(startMarker);
const end = html.indexOf(endMarker, start);
if (start < 0 || end < 0) throw new Error('block not found');
return html.slice(start, end);
}
// Response parser maps + lookupSw live between SW_MAP and updateRespCmd.
// Rewrite top-level const -> var so the maps leak out of sloppy-mode eval.
eval(extractBlock('const SW_MAP = {', 'function updateRespCmd').replace(/^const /gm, 'var '));
test('lookupSw wildcard 91XX resolves proactive pending (any length)', () => {
const desc = lookupSw('91', '14', 'uicc');
assert.ok(desc.includes('proactive'), desc);
assert.ok(!desc.includes('no response data'));
});
test('lookupSw 63CX extracts retry counter', () => {
const desc = lookupSw('63', 'C3', 'uicc');
assert.ok(desc.includes('3'), desc);
});
test('SW_MAP.generic covers ISO 6A85/6A89/6A8A', () => {
assert.ok(SW_MAP.generic['6A85']);
assert.ok(SW_MAP.generic['6A89']);
assert.ok(SW_MAP.generic['6A8A']);
});
test('gp 6310 label does not reference removed P2=42 option', () => {
assert.ok(!SW_MAP.gp['6310'].includes('42'));
});
test('LIFECYCLE_MAP per GPC v2.3 life cycles', () => {
assert.strictEqual(LIFECYCLE_MAP['01'], 'OP_READY (card) / LOADED (ELF)');
assert.strictEqual(LIFECYCLE_MAP['03'], 'INSTALLED');
assert.strictEqual(LIFECYCLE_MAP['07'], 'SELECTABLE');
assert.strictEqual(LIFECYCLE_MAP['0F'], 'SECURED (card)');
assert.strictEqual(LIFECYCLE_MAP['1F'], 'PERSONALIZED (SD)');
assert.strictEqual(LIFECYCLE_MAP['7F'], 'CARD_LOCKED');
assert.strictEqual(LIFECYCLE_MAP['FF'], 'TERMINATED');
assert.strictEqual(LIFECYCLE_MAP['83'], 'LOCKED (SD)');
});
test('PRIVILEGE_NAMES matches GPC v2.3 Tables 11-7/11-8/11-9', () => {
assert.strictEqual(PRIVILEGE_NAMES[0][7], 'Mandated DAP Verification');
assert.strictEqual(PRIVILEGE_NAMES[1][2], 'Token Verification');
assert.strictEqual(PRIVILEGE_NAMES[1][7], 'Global Service');
assert.strictEqual(PRIVILEGE_NAMES[2][0], 'Receipt Generation');
assert.strictEqual(PRIVILEGE_NAMES[2][3], 'Contactless Self-Activation');
});
test('TS 51.011 SIM families resolve (94xx/98xx/92xx/9Exx/9Fxx)', () => {
assert.ok(lookupSw('94', '04', 'uicc').includes('not found'));
assert.ok(lookupSw('94', '00', 'uicc').includes('No EF selected'));
assert.ok(lookupSw('94', '08', 'uicc').includes('inconsistent with the command'));
assert.ok(lookupSw('98', '40', 'uicc').includes('blocked'));
assert.ok(lookupSw('92', '40', 'uicc').includes('Memory problem'));
assert.ok(lookupSw('98', '50', 'gp').includes('max value'));
assert.ok(lookupSw('9E', '15', 'uicc').includes('download'));
assert.ok(lookupSw('9F', '20', 'uicc').includes('GET RESPONSE'));
});
+228
View File
@@ -0,0 +1,228 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
function extractFunc(src, name) {
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return src.slice(m.index, i + 1);
}
// Extract chain builder functions and dependencies
const FNS = ['buildApdu', 'buildSelect', 'escHtml', 'chainInit', 'chainSimBuildRowHex'];
let code = '';
for (const f of FNS) {
code += extractFunc(html, f) + '\n';
}
// Also extract _chains
const m = html.match(/const _chains = \{\};/);
if (m) code += m[0].replace(/^const /, 'var ') + '\n';
eval(code);
function setup(mode, opts) {
const chainId = mode === 'sim' ? 'chain-sim' : 'chain-usim';
_chains[chainId] = { rows: [] };
const cmd = opts.cmd;
const fields = {};
if (cmd === 'select') {
fields.method = opts.selMethod || 'fid';
if (opts.fid) fields.fid = opts.fid;
if (opts.path) fields.path = opts.path;
if (opts.dfname) fields.dfname = opts.dfname;
if (opts.chain) fields.chain = opts.chain;
if (opts.silent) fields.silent = opts.silent;
if (opts.base) fields.base = opts.base;
} else if (cmd === 'read-record') {
fields.record = opts.record || '1';
fields.recMode = opts.recMode || '04';
fields.le = opts.le || '00';
} else if (cmd === 'read-binary') {
fields.offset = opts.offset || '0000';
fields.le = opts.le || '00';
} else if (cmd === 'update-record') {
fields.record = opts.record || '1';
fields.recMode = opts.recMode || '04';
fields.data = opts.data || '';
} else if (cmd === 'update-binary') {
fields.offset = opts.offset || '0000';
fields.data = opts.data || '';
} else if (cmd === 'erase-binary') {
fields.offset = opts.offset || '0000';
} else if (cmd === 'activate-file' || cmd === 'deactivate-file') {
fields.target = opts.actTarget || 'current';
fields.file = opts.actFile || '';
} else if (cmd === 'verify' || cmd === 'disable' || cmd === 'enable') {
fields.pin = opts.pin || '01';
fields.pinVal = opts.pinVal || '';
} else if (cmd === 'change' || cmd === 'unblock') {
fields.pin = opts.pin || '01';
fields.pinOld = opts.pinOld || '';
fields.pinNew = opts.pinNew || '';
}
_chains[chainId].rows.push({ cmd: cmd, fields: fields });
if (opts.doSelect) {
const selFields = {};
selFields.method = opts.selMethod || 'fid';
if (opts.fid) selFields.fid = opts.fid;
if (opts.path) selFields.path = opts.path;
if (opts.dfname) selFields.dfname = opts.dfname;
if (opts.chain) selFields.chain = opts.chain;
if (opts.silent) selFields.silent = opts.silent;
if (opts.base) selFields.base = opts.base;
_chains[chainId].rows.unshift({ cmd: 'select', fields: selFields });
}
const rowIdx = opts.doSelect ? 1 : 0;
return chainSimBuildRowHex(chainId, rowIdx, _chains[chainId].rows[rowIdx]);
}
function setupMulti(mode, rows) {
const chainId = mode === 'sim' ? 'chain-sim' : 'chain-usim';
_chains[chainId] = { rows: [] };
for (const r of rows) {
_chains[chainId].rows.push({ cmd: r.cmd, fields: r.fields || {} });
}
let hex = '';
for (let i = 0; i < _chains[chainId].rows.length; i++) {
hex += chainSimBuildRowHex(chainId, i, _chains[chainId].rows[i]);
}
return hex;
}
test('VERIFY PIN FF-pads to 8 bytes (Lc=08)', () => {
const apdu = setup('usim', { cmd: 'verify', pin: '01', pinVal: '1234' });
assert.strictEqual(apdu, '002000010831323334FFFFFFFF');
});
test('CHANGE PIN emits two 8-byte fields (Lc=10)', () => {
const apdu = setup('usim', { cmd: 'change', pin: '01', pinOld: '1234', pinNew: '5678' });
assert.strictEqual(apdu, '002400011031323334FFFFFFFF35363738FFFFFFFF');
});
test('DISABLE PIN single padded PIN (INS 26)', () => {
const apdu = setup('usim', { cmd: 'disable', pin: '02', pinVal: '9999' });
assert.strictEqual(apdu, '002600020839393939FFFFFFFF');
});
test('ENABLE PIN single padded PIN (INS 28)', () => {
const apdu = setup('usim', { cmd: 'enable', pin: '01', pinVal: '1111' });
assert.strictEqual(apdu, '002800010831313131FFFFFFFF');
});
test('UNBLOCK PIN two padded PINs (INS 2C)', () => {
const apdu = setup('usim', { cmd: 'unblock', pin: '02', pinOld: '12345678', pinNew: '4321' });
assert.strictEqual(apdu, '002C000210313233343536373834333231FFFFFFFF');
});
test('ACTIVATE FILE case-1 (no data, no Le)', () => {
const apdu = setup('usim', { cmd: 'activate-file' });
assert.strictEqual(apdu, '00440000');
});
test('DEACTIVATE FILE case-1', () => {
const apdu = setup('sim', { cmd: 'deactivate-file' });
assert.strictEqual(apdu, 'A0040000');
});
test('ACTIVATE FILE by FID (P1=00 + Lc/FID)', () => {
const apdu = setup('usim', { cmd: 'activate-file', actTarget: 'fid', actFile: '6F3B' });
assert.strictEqual(apdu, '00440000026F3B');
});
test('ACTIVATE FILE by path from MF (P1=08)', () => {
const apdu = setup('usim', { cmd: 'activate-file', actTarget: 'mfpath', actFile: '7FFF6FC5' });
assert.strictEqual(apdu, '00440800047FFF6FC5');
});
test('DEACTIVATE FILE by path from current DF (P1=09, SIM CLA)', () => {
const apdu = setup('sim', { cmd: 'deactivate-file', actTarget: 'dfpath', actFile: '6F3B' });
assert.strictEqual(apdu, 'A0040900026F3B');
});
test('READ RECORD next mode forces P1=00', () => {
const apdu = setup('usim', { cmd: 'read-record', recMode: '02', le: '20' });
assert.strictEqual(apdu, '00B2000220');
});
test('SELECT by FID USIM: P2=04 requests FCP, Le=00', () => {
const apdu = setup('usim', { cmd: 'select', selMethod: 'fid', fid: '6FC5' });
assert.strictEqual(apdu, '00A40004026FC500');
});
test('USIM chain: every hop requests FCP (04 + Le)', () => {
const apdu = setup('usim', { cmd: 'select', selMethod: 'chain', chain: '3F00,2FE2' });
assert.strictEqual(apdu, '00A40004023F000000A40004022FE200');
});
test('SIM chain with GET RESPONSE hop', () => {
const apdu = setup('sim', { cmd: 'select', selMethod: 'chain', chain: '3F00,2FE2,C0' });
assert.strictEqual(apdu, 'A0A40000023F00A0A40000022FE2A0C0000000');
});
test('GET RESPONSE hop with explicit Le (C0:NN)', () => {
const apdu = setup('usim', { cmd: 'select', selMethod: 'chain', chain: 'C0:0F' });
assert.strictEqual(apdu, '00C000000F');
});
test('silent path select + READ RECORD keeps full CLA (live PNN read)', () => {
const chainId = 'chain-usim';
_chains[chainId] = { rows: [
{ cmd: 'select', fields: { method: 'path', path: '6FC5', base: 'df', silent: true } },
{ cmd: 'read-record', fields: { record: '1', recMode: '04', le: '14' } },
]};
let apdu = '';
for (let i = 0; i < _chains[chainId].rows.length; i++) {
apdu += chainSimBuildRowHex(chainId, i, _chains[chainId].rows[i]);
}
assert.strictEqual(apdu, '00A4090C026FC500B2010414');
});
test('select + READ BINARY emits explicit CLA on second command', () => {
const chainId = 'chain-usim';
_chains[chainId] = { rows: [
{ cmd: 'select', fields: { method: 'path', path: '6FC5', base: 'df', silent: true } },
{ cmd: 'read-binary', fields: { offset: '0000', le: '0A' } },
]};
let apdu = '';
for (let i = 0; i < _chains[chainId].rows.length; i++) {
apdu += chainSimBuildRowHex(chainId, i, _chains[chainId].rows[i]);
}
assert.strictEqual(apdu, '00A4090C026FC500B000000A');
});
test('USIM silent path-from-current-DF hop (live RFM idiom 09/0C)', () => {
const apdu = setup('usim', { cmd: 'select', selMethod: 'path', path: '6F46', base: 'df', silent: true });
assert.strictEqual(apdu, '00A4090C026F46');
});
test('USIM silent FID select (P2=0C, no Le)', () => {
const apdu = setup('usim', { cmd: 'select', selMethod: 'fid', fid: '6FC5', silent: true });
assert.strictEqual(apdu, '00A4000C026FC5');
});
test('SELECT by path USIM: P2=04, Le=00', () => {
const apdu = setup('usim', { cmd: 'select', selMethod: 'path', path: '7FFF6FC5' });
assert.strictEqual(apdu, '00A40804047FFF6FC500');
});
test('SELECT by FID SIM (CLA A0): no Le anywhere', () => {
const apdu = setup('sim', { cmd: 'select', selMethod: 'fid', fid: '6FC5' });
assert.strictEqual(apdu, 'A0A40000026FC5');
});
+101 -5
View File
@@ -5,6 +5,8 @@ const path = require('node:path');
const des = require('des.js');
global.des = des;
const aesjs = require('aes-js');
global.aesjs = aesjs;
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
@@ -25,7 +27,8 @@ function extractFunc(src, name) {
}
const FNS = ['hexToBytes', 'bytesToHex', 'des3Keys', 'des3EncryptBlock', 'des3CbcEncrypt',
'xorBytes', 'zeroPad', 'cbcMac', 'genSp'];
'xorBytes', 'zeroPad', 'cbcMac', 'aesCbcEncrypt', 'aesShiftLeft1', 'aesCmacSubkeys',
'aesCmac', 'genSp'];
let code = '';
for (const f of FNS) code += extractFunc(html, f) + '\n';
@@ -90,13 +93,13 @@ test('ciphered + CC SPI 16/01 (counter_must_be_higher, plaintext PoR)', () => {
test('unciphered + CC SPI 02/09', () => {
assert.strictEqual(
makeRun({ 'sp-spi1': '02', 'sp-spi2-hex': '09' }),
'001D1502091515B0000000000000010085A8CA1A9828B0BB00A40000023F00');
'1502091515B0000000000000010085A8CA1A9828B0BB00A40000023F00');
});
test('unciphered packet uses CPL = octets from CHL to end (0x001d)', () => {
test('unciphered packet starts at CHL, no CPL prefix (pySim parity)', () => {
const out = makeRun({ 'sp-spi1': '02', 'sp-spi2-hex': '09' });
assert.strictEqual(out.slice(0, 4), '001D');
assert.strictEqual(out.length, 62);
assert.strictEqual(out.slice(0, 2), '15');
assert.strictEqual(out.length, 58);
});
test('sysmocom public reference vector (spi1 04 / spi2 19, cntr=0)', () => {
@@ -121,3 +124,96 @@ test('cbcMac known answer (synthetic key)', () => {
const input = hexToBytes('001d1502091515b0000000000000010000a40000023f00');
assert.strictEqual(bytesToHex(cbcMac(input, hexToBytes(K))), '85A8CA1A9828B0BB');
});
// Public synthetic AES keys from pySim tests/unittests/test_ota.py (no live keys).
const KIC_AES = '200102030405060708090a0b0c0d0e0f';
const KID_AES = '201102030405060708090a0b0c0d0e0f';
test('aesCmac known answer (NIST SP 800-38B, truncated to 8)', () => {
const key = hexToBytes('2b7e151628aed2a6abf7158809cf4f3c');
assert.strictEqual(bytesToHex(aesCmac(new Uint8Array(0), key)), 'BB1D6929E9593728');
assert.strictEqual(
bytesToHex(aesCmac(hexToBytes('6bc1bee22e409f96e93d7e117393172a'), key)),
'070A16B46B4D4144');
assert.strictEqual(
bytesToHex(aesCmac(hexToBytes('6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411'), key)),
'DFA66747DE9AE630');
});
test('AES ciphered + CC SPI 16/19 (counter higher)', () => {
assert.strictEqual(
makeRun({
'sp-apdu': '00A40004023F00',
'sp-spi1': '16',
'sp-spi2-hex': '19',
'sp-kic-hex': '22',
'sp-kid-hex': '22',
'sp-tar': 'B00011',
'sp-cntr': '0000000011',
'sp-kic-key': KIC_AES,
'sp-kid-key': KID_AES,
}),
'00281516192222B000115A47655527E96E832F1A5C698655715D4331454A0D83952C0ED35245706976B1');
});
test('AES unciphered + CC SPI 12/09 (counter higher)', () => {
assert.strictEqual(
makeRun({
'sp-apdu': '00A40004023F00',
'sp-spi1': '12',
'sp-spi2-hex': '09',
'sp-kic-hex': '22',
'sp-kid-hex': '22',
'sp-tar': 'B00011',
'sp-cntr': '0000000011',
'sp-kic-key': KIC_AES,
'sp-kid-key': KID_AES,
}),
'1512092222B0001100000000110029826122C7A0B79500A40004023F00');
});
test('AES ciphered + CC SPI 1E/19 (counter +1)', () => {
assert.strictEqual(
makeRun({
'sp-apdu': '00A40004023F00',
'sp-spi1': '1E',
'sp-spi2-hex': '19',
'sp-kic-hex': '22',
'sp-kid-hex': '22',
'sp-tar': 'B00011',
'sp-cntr': '0000000011',
'sp-kic-key': KIC_AES,
'sp-kid-key': KID_AES,
}),
'0028151E192222B0001118B202EE47A3203E7370861C383B4142E704157B36E5C0EB4BB33EB6036CBAF8');
});
test('AES rejects no_counter (SPI1 b5b4 = 00)', () => {
const err = makeRun({
'sp-apdu': '00A40004023F00',
'sp-spi1': '06',
'sp-spi2-hex': '19',
'sp-kic-hex': '22',
'sp-kid-hex': '22',
'sp-tar': 'B00011',
'sp-cntr': '0000000011',
'sp-kic-key': KIC_AES,
'sp-kid-key': KID_AES,
});
assert.ok(err.startsWith('Error: AES requires a replay-protected counter'));
});
test('AES rejects 8-byte key', () => {
const err = makeRun({
'sp-apdu': '00A40004023F00',
'sp-spi1': '16',
'sp-spi2-hex': '19',
'sp-kic-hex': '22',
'sp-kid-hex': '22',
'sp-tar': 'B00011',
'sp-cntr': '0000000011',
'sp-kic-key': '0011223344556677',
'sp-kid-key': KID_AES,
});
assert.strictEqual(err, 'Error: AES KIc key must be 16, 24, or 32 bytes');
});
+85
View File
@@ -0,0 +1,85 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
function extractFunc(src, name, asyncFn) {
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return (asyncFn ? 'async ' : '') + src.slice(m.index, i + 1);
}
let code = extractFunc(html, 'stkMenuRespond', true) + '\n';
code += 'globalThis.esc = s => s;\n';
eval(code);
function setup(response) {
const calls = { handled: null, rendered: 0 };
globalThis.pysimFetch = async () => response;
globalThis.stkMenuHandleResponse = d => { calls.handled = d; };
globalThis.stkMenuRenderItems = () => { calls.rendered++; };
const btns = { classList: { add: () => {} } };
const back = { style: {} };
globalThis.document = {
getElementById: id => (id === 'stk-menu-buttons' ? btns : id === 'stk-back-btn' ? back : { innerHTML: '' }),
};
return calls;
}
test('back with a fetched SELECT ITEM continues the card dialogue', async () => {
const data = { type: 'select_item', items: [{ id: 1, text: 'Info' }] };
const calls = setup(data);
await stkMenuRespond('back');
assert.strictEqual(calls.handled, data);
assert.strictEqual(calls.rendered, 0);
});
test('back with a fetched DISPLAY TEXT shows it', async () => {
const data = { type: 'display_text', text: 'hello' };
const calls = setup(data);
await stkMenuRespond('back');
assert.strictEqual(calls.handled, data);
assert.strictEqual(calls.rendered, 0);
});
test('timeout with a fetched SELECT ITEM continues the card dialogue', async () => {
const data = { type: 'select_item', items: [] };
const calls = setup(data);
await stkMenuRespond('timeout');
assert.strictEqual(calls.handled, data);
assert.strictEqual(calls.rendered, 0);
});
test('back answered with SW 9000 falls back to the cached top menu', async () => {
const calls = setup({ type: 'done', sw: '9000' });
await stkMenuRespond('back');
assert.strictEqual(calls.handled, null);
assert.strictEqual(calls.rendered, 1);
});
test('cancel falls back to the cached top menu', async () => {
const calls = setup({ sw: '9000' });
await stkMenuRespond('cancel');
assert.strictEqual(calls.handled, null);
assert.strictEqual(calls.rendered, 1);
});
test('ok navigates with the server response', async () => {
const data = { type: 'select_item', items: [{ id: 1, text: 'x' }] };
const calls = setup(data);
await stkMenuRespond('ok');
assert.strictEqual(calls.handled, data);
assert.strictEqual(calls.rendered, 0);
});
+121
View File
@@ -0,0 +1,121 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const swSource = fs.readFileSync(path.join(__dirname, '..', 'sw.js'), 'utf8');
class FakeResponse {
constructor(body, init) {
this.body = body;
this.status = init && init.status;
this.statusText = init && init.statusText;
}
clone() {
return new FakeResponse(this.body, { status: this.status, statusText: this.statusText });
}
}
function loadSW({ fetchImpl, cacheMatch }) {
const listeners = {};
const puts = [];
const sandbox = {
self: {
addEventListener: (type, fn) => { listeners[type] = fn; },
skipWaiting: () => {},
},
caches: {
open: async () => ({
addAll: async () => {},
put: async (req, res) => { puts.push([String(req && req.url || req), res]); },
}),
keys: async () => [],
delete: async () => true,
match: cacheMatch,
},
clients: { claim: () => {} },
fetch: fetchImpl,
Response: FakeResponse,
URL,
console,
};
vm.createContext(sandbox);
vm.runInContext(swSource, sandbox);
return { listeners, puts };
}
function navigateEvent(url) {
const event = {
request: { url, method: 'GET', mode: 'navigate' },
responded: null,
};
event.respondWith = p => { event.responded = p; };
event.passThrough = () => { event.responded = null; };
return event;
}
test('offline navigation falls back to the cached index.html', async () => {
const index = new FakeResponse('html');
const { listeners } = loadSW({
fetchImpl: async () => { throw new Error('offline'); },
cacheMatch: async req => (String(req && req.url || req) === 'index.html' ? index : undefined),
});
const event = navigateEvent('http://127.0.0.1:8080/');
listeners.fetch(event);
const res = await event.responded;
assert.strictEqual(res, index);
});
test('offline navigation with empty cache resolves to an offline Response', async () => {
const { listeners } = loadSW({
fetchImpl: async () => { throw new Error('offline'); },
cacheMatch: async () => undefined,
});
const event = navigateEvent('http://127.0.0.1:8080/');
listeners.fetch(event);
const res = await event.responded;
assert.ok(res instanceof FakeResponse);
assert.strictEqual(res.status, 503);
});
test('a successful navigation is cached and returned', async () => {
const page = new FakeResponse('html');
const { listeners, puts } = loadSW({
fetchImpl: async () => page,
cacheMatch: async () => undefined,
});
const event = navigateEvent('http://127.0.0.1:8080/help.html');
listeners.fetch(event);
const res = await event.responded;
assert.strictEqual(res, page);
await new Promise(r => setImmediate(r));
assert.strictEqual(puts.length, 1);
assert.strictEqual(puts[0][0], 'http://127.0.0.1:8080/help.html');
});
test('api requests bypass the service worker', () => {
const { listeners } = loadSW({
fetchImpl: async () => { throw new Error('unexpected'); },
cacheMatch: async () => undefined,
});
const event = navigateEvent('http://127.0.0.1:8080/api/status');
event.request.mode = 'cors';
listeners.fetch(event);
assert.strictEqual(event.responded, null);
});
test('uncached asset hits the network and gets cached', async () => {
const asset = new FakeResponse('js');
const { listeners, puts } = loadSW({
fetchImpl: async () => asset,
cacheMatch: async () => undefined,
});
const event = navigateEvent('http://127.0.0.1:8080/des-bundle.js');
event.request.mode = 'cors';
listeners.fetch(event);
const res = await event.responded;
assert.strictEqual(res, asset);
await new Promise(r => setImmediate(r));
assert.strictEqual(puts.length, 1);
});
-3918
View File
File diff suppressed because it is too large Load Diff
+18
View File
@@ -0,0 +1,18 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "pysim-otaman-server"
version = "2.1.2"
description = "HTTP REST server wrapping pysim for the OTAMan PWA"
requires-python = ">=3.8"
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
# Declaring it here would make pip resolve its full tree (including the SMPP
# bridge -> Twisted -> twisted-iocpsupport, which needs MSVC on Windows).
[project.scripts]
pysim-otaman-server = "pysim_otaman_server.__main__:main"
[tool.setuptools.packages.find]
include = ["pysim_otaman_server*"]
View File
+216
View File
@@ -0,0 +1,216 @@
import argparse
import logging
import os
import sys
import time
import traceback
from http.server import HTTPServer
from pySim.card_handler import CardHandler
from pySim.commands import SimCardCommands
from pySim.log import PySimLogger
from pySim.cards import UiccCardBase
from .shell import load_pysim_app
from . import fastinit
from .server import PysimHandler, StderrApduTracer, _LoggingApduTracer, VERSION, _send_terminal_profile, _DefaultProactiveHandler, _handle_proactive_chain, _send_status, _init_proactive_session, _timing_on, _tlog, _set_menu_timeout, start_card_monitor, set_auto_equip
_server_start = 0
def _log_stdout(msg):
elapsed = time.time() - _server_start
os.write(1, ('[%8.3f] %s\n' % (elapsed, msg)).encode())
def _default_web_dir():
# <repo>/frontend, whether run from source or an editable install.
return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'frontend')
def main():
global _server_start
_server_start = time.time()
mod = load_pysim_app()
parser = mod.option_parser
parser.description = 'pysim-otaman-server — HTTP API for pysim'
parser.add_argument('--http-host', default='127.0.0.1', help='Bind address (default: 127.0.0.1)')
parser.add_argument('--http-port', type=int, default=8080, help='Bind port (default: 8080)')
parser.add_argument('--web-dir', default=_default_web_dir(), metavar='PATH',
help='Directory with the OTAMan PWA static files to serve (default: <repo>/frontend)')
parser.add_argument('--log-requests', action='store_true', default=False, help='Log request/response payloads to stderr')
parser.add_argument('--sms-oa', default='12345', metavar='DIGITS',
help='TP-Originating-Address (SMSC number) for the SMS-DELIVER TPDU (default: 12345)')
parser.add_argument('--sms-sm-sc', default='12345678912', metavar='DIGITS',
help='SM-SC address for SMS-SUBMIT routing in PoR-in-submit mode (default: 12345678912)')
parser.add_argument('--terminal-profile', default='7FFFFFFFFF0000CF02', metavar='HEX',
help='TERMINAL PROFILE payload (default: 10-byte profile with SMS-PP download and event list)')
parser.add_argument('--poll-interval', type=int, default=30, metavar='SECS',
help='Idle interval before automatic STATUS polling (1-255 seconds, default: 30). Disable with --poll-interval 0')
parser.add_argument('--no-card-init', action='store_true', default=False,
help='Skip pysim card initialization (preserve CAT session — no file manager)')
parser.add_argument('--timing', action='store_true', default=False,
help='Log phase durations, card resets and APDU counters with elapsed timestamps')
parser.add_argument('--fast-init', action='store_true', help=argparse.SUPPRESS)
parser.add_argument('--full-pysim-init', action='store_true', default=False,
help="Use pysim's stock init_card/equip (multiple physical card resets) instead of the default reset-free fast init")
parser.add_argument('--menu-timeout', type=int, default=60, metavar='SECS',
help='Auto-send a timeout TERMINAL RESPONSE if a paused STK command is not answered (default: 60, 0 disables)')
parser.add_argument('--no-auto-equip', action='store_true', default=False,
help='Do not automatically initialize a card right after it is inserted (default: auto-equip on)')
opts = parser.parse_args()
opts.skip_card_init = opts.no_card_init
opts.fast_init = not opts.full_pysim_init
if opts.timing:
_timing_on()
if opts.menu_timeout is not None:
_set_menu_timeout(opts.menu_timeout)
set_auto_equip(not opts.no_auto_equip and not opts.skip_card_init)
sl = None
scc = None
card = None
rs = None
sim_menu = None
event_list = None
# Auto-detect PC/SC reader if none was explicitly specified.
# Handles late pcscd startup and USB enumeration delays.
if opts.pcsc_dev is None and opts.pcsc_regex is None:
try:
from smartcard.System import readers
for attempt in range(3):
r = readers()
if r:
sys.stderr.write('INIT: PC/SC reader detected: %s\n' % r[0])
opts.pcsc_dev = 0
break
if attempt < 2:
sys.stderr.write('INIT: no PC/SC readers found, retrying in 2s...\n')
time.sleep(2)
except Exception:
pass # smartcard module not available or pcscd unreachable
try:
kwargs = {}
if opts.apdu_trace:
kwargs['apdu_tracer'] = _LoggingApduTracer()
t_phase = time.time()
sl = mod.init_reader(opts, **kwargs)
_tlog('init_reader: %.0fms' % ((time.time() - t_phase) * 1000))
scc = SimCardCommands(sl)
scc.cat_cla = '80' # UICC CLA default; overridden for SIM after init_card
scc._tp.proactive_handler = _DefaultProactiveHandler()
t_phase = time.time()
if opts.fast_init:
try:
rs, card = fastinit.init_card_fast(sl, opts.skip_card_init, wait=True)
except Exception:
print("Warning: fast card initialization failed, falling back to pysim init:", file=sys.stderr)
traceback.print_exc()
rs, card = mod.init_card(sl, opts.skip_card_init)
else:
sl.wait_for_card(3)
rs, card = mod.init_card(sl, opts.skip_card_init)
_tlog('card_init: %.0fms' % ((time.time() - t_phase) * 1000))
scc.cat_cla = '80' if isinstance(card, UiccCardBase) else 'a0'
except Exception:
print("Warning: reader/card initialization failed:", file=sys.stderr)
traceback.print_exc()
ch = CardHandler(sl) if sl else None
t_phase = time.time()
try:
app = mod.PysimApp(verbose=opts.verbose, card=card, rs=rs, sl=sl, ch=ch)
except Exception:
print("Warning: PysimApp creation failed:", file=sys.stderr)
traceback.print_exc()
app = None
_tlog('pysim_app: %.0fms' % ((time.time() - t_phase) * 1000))
if app is not None and opts.fast_init:
fastinit.install(app)
if scc and card is not None and hasattr(scc, '_tp'):
scc._tp.apdu_tracer = _LoggingApduTracer()
try:
_init_proactive_session()
t_phase = time.time()
sys.stderr.write('INIT: sending TERMINAL PROFILE %s (CLA=%s)\n' % (opts.terminal_profile, scc.cat_cla))
sm, el = _send_terminal_profile(scc, opts.terminal_profile)
sys.stderr.write('INIT: TP done, menu=%s events=%s\n' % ('yes' if sm else 'no', 'yes' if el else 'no'))
sim_menu = sm or sim_menu
event_list = el or event_list
for _ in range(3):
st_data, st_sw = _send_status(scc)
sys.stderr.write('INIT: drain STATUS -> %s\n' % st_sw)
if not st_sw.startswith('91'):
break
_handle_proactive_chain(scc, st_sw)
_tlog('terminal_profile_drain: %.0fms' % ((time.time() - t_phase) * 1000))
except Exception:
traceback.print_exc(file=sys.stderr)
elif scc is not None:
sys.stderr.write('INIT: card not initialized — use Equip once the card is readable\n')
if app is not None and opts.apdu_trace:
# PysimApp.__init__ routes PySimLogger through app.poutput() (app.stdout)
# and drops the root level to INFO. Re-route pysim's own APDU trace logging
# directly to fd 1 so it survives the app.stdout/StringIO redirection in the
# HTTP handlers and the INFO level suppression.
PySimLogger.setup(print_callback=_log_stdout)
PySimLogger.set_level(logging.DEBUG)
# PysimApp.__init__ and every `equip` wipe the transport apdu_tracer
# (_onchange_apdu_trace sets it to None). Re-attach our tracer and make
# sure it stays attached across equip/re-equip.
tracer = _LoggingApduTracer()
def _reattach_tracer():
if app.card:
app.card._scc._tp.apdu_tracer = tracer
_reattach_tracer()
orig_onchange = app._onchange_apdu_trace
def _onchange_apdu_trace(param_name, old, new):
orig_onchange(param_name, old, new)
_reattach_tracer()
app._onchange_apdu_trace = _onchange_apdu_trace
server = HTTPServer((opts.http_host, opts.http_port), PysimHandler)
server.sl = sl
server.scc = scc
server.card = card
server.rs = rs
server.app = app
server.sms_oa = opts.sms_oa
server.sms_sc = opts.sms_sm_sc
server.log_requests = opts.log_requests
server.terminal_profile = opts.terminal_profile
server.web_dir = opts.web_dir
server.sim_menu = sim_menu
server.event_list = event_list
server.menu_active = False
server.stk_pending = None
server.card_present = card is not None
server.card_session = 1 if card is not None else 0
server.equipping = False
# Set server reference for polling timer and mark the card session state
import pysim_otaman_server.server
pysim_otaman_server.server._server_ref = server
pysim_otaman_server.server._CARD_CONNECTED = card is not None
if opts.poll_interval is not None:
pysim_otaman_server.server._set_poll_interval(opts.poll_interval)
# Auto-enable polling if card initialized successfully (unless interval is 0)
if server.scc and server.card and opts.poll_interval != 0:
pysim_otaman_server.server._poll_enable()
# Start presence monitoring only after the startup init: pyscard reports an
# already-present card as "added" on the first pass, and we must not
# auto-equip over a session we just initialized. If startup init failed,
# that event triggers auto-equip instead — the desired retry.
if sl is not None and getattr(sl, '_reader', None) is not None:
start_card_monitor(str(sl._reader))
print("" * 70)
print(" pysim-otaman-server v%s listening on http://%s:%s" % (VERSION, opts.http_host, opts.http_port))
print(" Open http://%s:%s in your browser for the OTAMan UI (served by this server)."
% (opts.http_host, opts.http_port))
print("" * 70)
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nShutting down...")
server.shutdown()
if __name__ == '__main__':
main()
+176
View File
@@ -0,0 +1,176 @@
"""Fast card initialization for pysim-otaman-server.
pySim's ``init_card()`` performs several physical card resets: one per profile
candidate tried by ``CardProfile.pick()`` plus one at the end of
``RuntimeState.__init__``, and ``PysimApp.equip()`` resets yet again. On common
readers each disconnect/connect costs around a second, so the stock path spends
most of its time re-establishing a clean state (MF selected) that can also be
restored in software.
This module mirrors ``pySim.app.init_card()`` with those resets removed: all
profile probes run back-to-back on the same connection and the runtime state
uses a software reset. It is the default init/equip path; ``--full-pysim-init``
restores pysim's stock behavior, and the explicit ``equip``/``reset`` commands
keep a real reconnect/physical reset.
"""
import operator
import sys
from pySim.cards import CardBase, SimCardBase, UiccCardBase, card_detect
from pySim.commands import SimCardCommands
from pySim.exceptions import ProtocolError, SwMatchError
from pySim.filesystem import CardApplication, CardModel
from pySim.profile import CardProfile
from pySim.runtime import RuntimeState
from pySim.ts_102_221 import CardProfileUICC
from pySim.utils import all_subclasses
import pySim.euicc
from .server import _tlog
class FastRuntimeState(RuntimeState):
"""RuntimeState whose reset() restores software state (selects MF) instead
of power-cycling the card. Use hard_reset() for an explicit reset."""
def reset(self, cmd_app=None):
try:
return self.soft_reset(cmd_app)
except (SwMatchError, ProtocolError) as e:
sys.stderr.write('FAST-RESET: soft reset failed (%s), falling back to physical reset\n' % e)
return self.hard_reset(cmd_app)
def soft_reset(self, cmd_app=None):
for lchan_nr in list(self.lchan.keys()):
self.lchan[lchan_nr].scc.scp = None
if lchan_nr == 0:
continue
del self.lchan[lchan_nr]
self.adm_verified = False
try:
atr = self.card._scc.get_atr()
except Exception:
atr = None
if cmd_app:
cmd_app.lchan = self.lchan[0]
self.lchan[0].select('MF', cmd_app)
self.lchan[0].selected_adf = None
self.identity['ATR'] = atr
return atr
def hard_reset(self, cmd_app=None):
return super().reset(cmd_app)
def pick_profile_no_reset(scc):
"""Like CardProfile.pick(), but without a physical reset between
candidates. Each probe selects its own discriminating file, so a reset only
costs a reconnect without changing the outcome."""
original_reset = scc.reset_card
scc.reset_card = lambda: None
try:
profiles = sorted(all_subclasses(CardProfile), key=operator.attrgetter('ORDER'))
for p in profiles:
if p.match_with_card(scc):
return p()
return None
finally:
scc.reset_card = original_reset
def init_card_fast(sl, skip_card_init=False, wait=True):
"""Replacement for pySim.app.init_card() that avoids redundant resets.
``wait`` performs the single disconnect/connect of this init (explicit
equip passes True; startup already connects via wait_for_card). If probing
leaves the card in a state the software reset cannot clear, retry once
after a physical reset."""
try:
return _init_card_once(sl, skip_card_init, wait)
except (SwMatchError, ProtocolError) as e:
sys.stderr.write('FAST-INIT: %s; retrying after physical reset\n' % e)
sl.reset_card()
return _init_card_once(sl, skip_card_init, wait=False)
def _init_card_once(sl, skip_card_init, wait):
scc = SimCardCommands(transport=sl)
if wait:
sl.wait_for_card(3)
if skip_card_init:
return None, CardBase(scc)
generic_card = False
card = card_detect(scc)
if card is None:
card = SimCardBase(scc)
generic_card = True
profile = pick_profile_no_reset(scc)
if profile is None:
return None, card
if generic_card and isinstance(profile, CardProfileUICC):
card._adm_chv_num = 0x0A
if isinstance(profile, CardProfileUICC):
for app_cls in all_subclasses(CardApplication):
if hasattr(app_cls, '_' + app_cls.__name__ + '__intermediate'):
continue
profile.add_application(app_cls())
if generic_card:
card = UiccCardBase(scc)
rs = FastRuntimeState(card, profile)
CardModel.apply_matching_models(scc, rs)
sl.set_sw_interpreter(rs)
isd_r = rs.mf.applications.get(pySim.euicc.AID_ISD_R.lower(), None)
if isd_r:
rs.lchan[0].select_file(isd_r)
try:
rs.identity['EID'] = pySim.euicc.CardApplicationISDR.get_eid(scc)
except SwMatchError:
pass
finally:
rs.soft_reset()
return rs, card
def do_equip_fast(app):
"""Explicit equip: one real reconnect (wait_for_card) then reset-free init.
PysimApp.equip() unregisters the old command sets itself after the new init
succeeds, so a failed init leaves the previous card state intact."""
rs, card = init_card_fast(app.sl, wait=True)
app.equip(card, rs)
def do_reset_fast(app):
"""Explicit reset: always a physical card reset."""
if app.rs is None:
app.card._scc.reset_card()
atr = app.card._scc.get_atr()
else:
atr = app.rs.hard_reset(app)
app.poutput('Card ATR: %s' % atr)
def install(app):
"""Route the pySim-shell equip/reset commands through the fast paths."""
def _do_equip(statement):
_tlog('do_equip_fast: start')
do_equip_fast(app)
_tlog('do_equip_fast: done')
def _do_reset(statement):
_tlog('do_reset_fast: start')
do_reset_fast(app)
_tlog('do_reset_fast: done')
app.do_equip = _do_equip
app.do_reset = _do_reset
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
import importlib.util
import os
import sys
def load_pysim_app():
import pySim
pysim_dir = os.path.dirname(pySim.__file__)
candidates = [
os.path.join(os.path.dirname(pysim_dir), 'pySim-shell.py'),
os.path.join(os.path.dirname(sys.executable), 'pySim-shell.py'),
]
for path in candidates:
if os.path.exists(path):
spec = importlib.util.spec_from_file_location("pySim_shell", path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
raise ImportError(
"pySim-shell.py not found. Make sure pysim is installed "
"(pip install pysim) and pySim-shell.py is on the PATH."
)
+18
View File
@@ -0,0 +1,18 @@
# pysim dependencies minus the SMPP bridge (smpp.twisted3). Used by setup.bat.
# pySim-shell does not use SMPP; omitting smpp.twisted3 avoids the
# twisted -> twisted-iocpsupport build chain on Windows.
pyscard
pyserial
pytlv
cmd2>=2.6.2,<4.0
jsonpath-ng
construct>=2.10.70
bidict
pyosmocom>=0.0.12
pyyaml>=5.4
termcolor
colorlog
pycryptodomex
packaging
smpp.pdu @ git+https://github.com/hologram-io/smpp.pdu
asn1tools
+86
View File
@@ -0,0 +1,86 @@
@echo off
REM pysim-otaman-server setup script for Windows
REM Creates a venv and installs pysim and its dependencies.
setlocal enabledelayedexpansion
set VENV_DIR=%~dp0.venv
echo === pysim-otaman-server setup ===
echo.
REM Check Python
python --version >nul 2>&1
if %errorlevel% neq 0 (
echo Error: Python is required but not found.
echo Install Python 3.8+ from https://python.org
pause
exit /b 1
)
echo Found Python
python --version
REM Check Git
git --version >nul 2>&1
if %errorlevel% neq 0 (
echo Error: Git is required but not found.
echo Install Git from https://git-scm.com
pause
exit /b 1
)
echo Found Git
git --version
echo.
REM Create venv
if not exist "%VENV_DIR%" (
echo Creating virtual environment...
python -m venv "%VENV_DIR%"
)
call "%VENV_DIR%\Scripts\activate.bat"
REM Upgrade pip
python -m pip install --upgrade pip -q
REM pyscard has precompiled wheels for Python 3.10-3.13.
REM On Python 3.9 / 3.14 pip builds it from source (requires Microsoft C++ Build Tools).
echo Note: pyscard ships precompiled wheels for Python 3.10-3.13.
echo Python 3.9 / 3.14 will build pyscard from source and require
echo Microsoft C++ Build Tools ("Desktop development with C++").
echo.
REM Install pysim dependencies first, then pysim itself (without the SMPP
REM bridge - not needed by pysim-shell). Installing pysim with --no-deps after
REM its dependencies avoids pip's resolver warning about the intentionally
REM omitted smpp.twisted3 (SMPP bridge) dependency.
echo === Installing pysim dependencies ===
pip install -r "%~dp0requirements-pysim.txt"
if %errorlevel% neq 0 (
echo Error: Failed to install pysim dependencies.
pause
exit /b 1
)
echo === Installing pysim ===
pip install --no-deps git+https://github.com/osmocom/pysim.git
if %errorlevel% neq 0 (
echo Error: Failed to install pysim.
pause
exit /b 1
)
echo.
REM Install pysim-otaman-server
echo === Installing pysim-otaman-server ===
pip install -e "%~dp0."
if %errorlevel% neq 0 (
echo Error: Failed to install pysim-otaman-server.
pause
exit /b 1
)
echo.
echo === Setup complete ===
echo Run start.bat to start the server.
pause
Executable
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
set -e
# pysim-otaman-server setup script
# Creates a venv and installs pysim and its dependencies.
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
VENV_DIR="$SCRIPT_DIR/.venv"
# Check prerequisites
command -v python3 >/dev/null 2>&1 || { echo "Error: Python 3 is required but not found. Install Python 3.8+ from https://python.org"; exit 1; }
command -v git >/dev/null 2>&1 || { echo "Error: Git is required but not found. Install Git from https://git-scm.com"; exit 1; }
PY_VER=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
echo "Found Python $PY_VER"
# Create venv
if [ ! -d "$VENV_DIR" ]; then
echo "Creating virtual environment..."
python3 -m venv "$VENV_DIR"
fi
source "$VENV_DIR/bin/activate"
PIP="$VENV_DIR/bin/pip"
# Upgrade pip
$PIP install --upgrade pip -q
# Install pysim from the Osmocom repository
echo ""
echo "=== Installing pysim ==="
$PIP install git+https://github.com/osmocom/pysim.git
# Install pysim-otaman-server
echo ""
echo "=== Installing pysim-otaman-server ==="
$PIP install -e "$SCRIPT_DIR"
# Check for PC/SC
echo ""
if command -v pcscd >/dev/null 2>&1; then
echo "=== PC/SC daemon found ==="
if ! pgrep -x pcscd >/dev/null; then
echo "Starting pcscd..."
sudo pcscd || echo "Warning: could not start pcscd. Start it manually: sudo systemctl start pcscd"
fi
else
echo "Note: PC/SC daemon not found. If you use a USB smart card reader,"
echo "install pcsc-lite and ccid:"
echo " Debian/Ubuntu: sudo apt install pcscd pcsc-tools"
echo " Arch Linux: sudo pacman -S pcsc-lite ccid"
echo " Fedora: sudo dnf install pcsc-lite pcsc-lite-ccid"
fi
echo ""
echo "=== Setup complete ==="
echo "Run ./start.sh to start the server."
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

+33
View File
@@ -0,0 +1,33 @@
@echo off
REM pysim-otaman-server start script for Windows
REM Starts the server, preferring the venv if it exists.
REM PC/SC is built into Windows, so defaults to reader 0.
set VENV_DIR=%~dp0.venv
if exist "%VENV_DIR%\Scripts\pysim-otaman-server.exe" (
echo Starting pysim-otaman-server from venv on http://127.0.0.1:8080
echo Press Ctrl+C to stop.
"%VENV_DIR%\Scripts\pysim-otaman-server.exe" --http-port 8080 -p 0
goto :eof
)
if exist "%~dp0pysim_otaman_server\__main__.py" (
echo Starting pysim-otaman-server from source on http://127.0.0.1:8080
echo Press Ctrl+C to stop.
python -m pysim_otaman_server --http-port 8080 -p 0
goto :eof
)
where pysim-otaman-server >nul 2>&1
if %errorlevel% equ 0 (
echo Starting pysim-otaman-server on http://127.0.0.1:8080
echo Press Ctrl+C to stop.
pysim-otaman-server --http-port 8080 -p 0
goto :eof
)
echo Error: pysim-otaman-server not installed.
echo Run setup.bat first or install manually:
echo pip install pysim-otaman-server
pause
Executable
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
set -e
# pysim-otaman-server start script
# Starts the server, preferring the venv if it exists.
# Auto-detects PC/SC reader if available.
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
VENV_DIR="$SCRIPT_DIR/.venv"
# Auto-detect reader
READER_ARGS=""
if command -v pcscd > /dev/null 2>&1; then
# Give pcscd a moment if it's not running yet (USB enumeration delay)
if ! pgrep -x pcscd > /dev/null 2>&1 && ! pgrep -x pcscd.bin > /dev/null 2>&1; then
sleep 1
fi
if pgrep -x pcscd > /dev/null 2>&1 || pgrep -x pcscd.bin > /dev/null 2>&1; then
READER_ARGS="-p 0"
fi
fi
SERVER=""
if [ -f "$VENV_DIR/bin/pysim-otaman-server" ]; then
SERVER="$VENV_DIR/bin/pysim-otaman-server"
elif command -v pysim-otaman-server &> /dev/null; then
SERVER="pysim-otaman-server"
elif [ -f "$SCRIPT_DIR/pysim_otaman_server/__main__.py" ]; then
echo "Starting pysim-otaman-server from source on http://127.0.0.1:8080"
cd "$SCRIPT_DIR" && python3 -m pysim_otaman_server --http-port 8080 $READER_ARGS
exit $?
else
echo "Error: pysim-otaman-server not installed."
echo "Run setup.sh first or install manually:"
echo " pip install pysim-otaman-server"
exit 1
fi
echo "Starting pysim-otaman-server on http://127.0.0.1:8080"
echo "Press Ctrl+C to stop."
$SERVER --http-port 8080 $READER_ARGS
-12
View File
@@ -1,12 +0,0 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
const opens = (html.match(/<div\b/g) || []).length;
const closes = (html.match(/<\/div>/g) || []).length;
test('HTML <div> tags are balanced', () => {
assert.strictEqual(opens, closes, `Unbalanced divs: ${opens} opens vs ${closes} closes`);
});
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""Tests for per-command APDU timing collection (card snapshot measurements)."""
import sys
import time
import types
import unittest
from pathlib import Path
from unittest import mock
PROJECTS = Path(__file__).resolve().parents[2]
PY_SIM = PROJECTS / 'pysim'
if str(PY_SIM) not in sys.path:
sys.path.insert(0, str(PY_SIM))
import pysim_otaman_server.server as S
class TestClassifyApdu(unittest.TestCase):
def test_select(self):
self.assertEqual(S._classify_apdu('00a40004023f0000'), 'select')
def test_read_binary(self):
self.assertEqual(S._classify_apdu('00b000000a'), 'read_binary')
def test_read_record(self):
self.assertEqual(S._classify_apdu('00b2010428'), 'read_record')
def test_other_not_classified(self):
self.assertIsNone(S._classify_apdu('80f2000c00'))
def test_short_input(self):
self.assertIsNone(S._classify_apdu(''))
self.assertIsNone(S._classify_apdu('00'))
class TestApduTimeCollection(unittest.TestCase):
def setUp(self):
self.saved = (S._APDU_TIME_COLLECT, list(S._APDU_TIMES), S._server_ref)
S._APDU_TIME_COLLECT = False
S._APDU_TIMES.clear()
S._server_ref = None
def tearDown(self):
S._APDU_TIME_COLLECT, times, S._server_ref = self.saved
S._APDU_TIMES[:] = times
def test_disabled_does_not_collect(self):
tracer = S.StderrApduTracer()
with mock.patch.object(S.os, 'write'):
tracer.trace_command('00a40004023f0000')
tracer.trace_response('00a40004023f0000', '9000', '')
self.assertEqual(S._APDU_TIMES, [])
def test_collects_only_classified_commands_with_ms(self):
S._collect_apdu_times()
tracer = S.StderrApduTracer()
with mock.patch.object(S.os, 'write'):
tracer._cmd_start = time.time() - 0.025
tracer.trace_response('00a40004023f0000', '9000', '')
tracer._cmd_start = time.time() - 0.010
tracer.trace_response('00b000000a', '9000', '')
tracer._cmd_start = time.time() - 0.005
tracer.trace_response('80f2000c00', '9000', '')
times = S._end_apdu_time_collection()
self.assertEqual([t['type'] for t in times], ['select', 'read_binary'])
self.assertGreaterEqual(times[0]['ms'], 20)
self.assertFalse(S._APDU_TIME_COLLECT)
self.assertEqual(S._APDU_TIMES, [])
def test_collect_reattaches_tracer_when_missing(self):
tp = types.SimpleNamespace(apdu_tracer=None)
scc = types.SimpleNamespace(_tp=tp)
S._server_ref = types.SimpleNamespace(scc=scc)
S._collect_apdu_times()
try:
self.assertIsInstance(tp.apdu_tracer, S._LoggingApduTracer)
finally:
S._end_apdu_time_collection()
def test_collect_keeps_existing_tracer(self):
tracer = S.StderrApduTracer()
tp = types.SimpleNamespace(apdu_tracer=tracer)
scc = types.SimpleNamespace(_tp=tp)
S._server_ref = types.SimpleNamespace(scc=scc)
S._collect_apdu_times()
try:
self.assertIs(tp.apdu_tracer, tracer)
finally:
S._end_apdu_time_collection()
if __name__ == '__main__':
unittest.main()
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env python3
"""Tests for the passive PC/SC card-presence observer and auto-equip state."""
import sys
import types
import unittest
from pathlib import Path
from unittest import mock
PROJECTS = Path(__file__).resolve().parents[2]
PY_SIM = PROJECTS / 'pysim'
if str(PY_SIM) not in sys.path:
sys.path.insert(0, str(PY_SIM))
import pysim_otaman_server.server as S
class FakeCard:
def __init__(self, reader):
self.reader = reader
class TestCardPresenceObserver(unittest.TestCase):
def setUp(self):
self.observer = S._CardPresenceObserver('Test Reader 00 00')
self.server = types.SimpleNamespace(card_present=True)
self.saved_ref = S._server_ref
S._server_ref = self.server
self.disconnects = []
self.patcher = mock.patch.object(
S, '_handle_card_disconnect',
side_effect=lambda: self.disconnects.append(True))
self.patcher.start()
self.trigger = mock.patch.object(S, '_auto_equip_trigger')
self.trigger_mock = self.trigger.start()
self.saved_auto = S._AUTO_EQUIP
S._AUTO_EQUIP = True
def tearDown(self):
S._AUTO_EQUIP = self.saved_auto
self.trigger.stop()
self.patcher.stop()
S._server_ref = self.saved_ref
def test_removal_of_our_reader_disconnects(self):
self.observer.update(None, ([], [FakeCard('Test Reader 00 00')]))
self.assertFalse(self.server.card_present)
self.assertEqual(len(self.disconnects), 1)
self.trigger_mock.assert_not_called()
def test_removal_of_other_reader_ignored(self):
self.observer.update(None, ([], [FakeCard('Other Reader 00 00')]))
self.assertTrue(self.server.card_present)
self.assertEqual(self.disconnects, [])
self.trigger_mock.assert_not_called()
def test_insertion_sets_card_present_and_triggers_auto_equip(self):
self.server.card_present = False
self.observer.update(None, ([FakeCard('Test Reader 00 00')], []))
self.assertTrue(self.server.card_present)
self.assertEqual(self.disconnects, [])
self.trigger_mock.assert_called_once()
def test_insertion_does_not_trigger_when_disabled(self):
S._AUTO_EQUIP = False
self.server.card_present = False
self.observer.update(None, ([FakeCard('Test Reader 00 00')], []))
self.assertTrue(self.server.card_present)
self.trigger_mock.assert_not_called()
def test_missing_reader_attribute_is_ignored(self):
self.observer.update(None, ([], [types.SimpleNamespace()]))
self.assertTrue(self.server.card_present)
self.assertEqual(self.disconnects, [])
self.trigger_mock.assert_not_called()
class TestAutoEquipTrigger(unittest.TestCase):
def tearDown(self):
S._AUTO_EQUIP = True
S._AUTO_EQUIP_BUSY = False
def test_disabled_does_not_spawn(self):
S._AUTO_EQUIP = False
with mock.patch.object(S.threading, 'Thread') as thread:
S._auto_equip_trigger()
thread.assert_not_called()
def test_busy_does_not_spawn_twice(self):
S._AUTO_EQUIP = True
S._AUTO_EQUIP_BUSY = True
with mock.patch.object(S.threading, 'Thread') as thread:
S._auto_equip_trigger()
thread.assert_not_called()
def test_spawns_worker_once(self):
S._AUTO_EQUIP = True
S._AUTO_EQUIP_BUSY = False
with mock.patch.object(S.threading, 'Thread') as thread:
S._auto_equip_trigger()
thread.assert_called_once_with(target=S._auto_equip_worker, name='auto-equip', daemon=True)
class TestCardSession(unittest.TestCase):
def test_disconnect_bumps_session_and_clears_equipping(self):
server = types.SimpleNamespace(
card_session=5, card=object(), scc=object(), stk_pending=object(),
menu_active=True, event_list=[1], sim_menu={}, equipping=True)
saved = S._server_ref
S._server_ref = server
try:
S._handle_card_disconnect()
finally:
S._server_ref = saved
self.assertEqual(server.card_session, 6)
self.assertFalse(server.equipping)
self.assertIsNone(server.card)
class TestApplyEquippedCard(unittest.TestCase):
def test_updates_state_and_bumps_session(self):
scc = types.SimpleNamespace(cat_cla=None)
card = types.SimpleNamespace(_scc=scc, name='Card')
app = types.SimpleNamespace(card=card)
server = types.SimpleNamespace(
app=app, card=None, scc=None, stk_pending=object(), menu_active=True,
event_list=[1], sim_menu={}, card_session=2, card_present=False,
equipping=False, terminal_profile='7F')
saved_ref, saved_conn = S._server_ref, S._CARD_CONNECTED
S._server_ref = server
S._CARD_CONNECTED = False
try:
with mock.patch.object(S, '_send_terminal_profile', return_value=('menu', ['ev'])):
with mock.patch.object(S, '_poll_enable'):
S._apply_equipped_card(server)
connected_after = S._CARD_CONNECTED
finally:
S._server_ref = saved_ref
S._CARD_CONNECTED = saved_conn
self.assertTrue(connected_after)
self.assertIs(server.card, card)
self.assertIs(server.scc, scc)
self.assertEqual(server.card_session, 3)
self.assertTrue(server.card_present)
self.assertEqual(server.sim_menu, 'menu')
self.assertEqual(server.event_list, ['ev'])
if __name__ == '__main__':
unittest.main()
+212
View File
@@ -0,0 +1,212 @@
#!/usr/bin/env python3
"""Tests for the reset-free fast initialization helpers."""
import sys
import types
import unittest
from pathlib import Path
from unittest import mock
PROJECTS = Path(__file__).resolve().parents[2]
PY_SIM = PROJECTS / 'pysim'
if str(PY_SIM) not in sys.path:
sys.path.insert(0, str(PY_SIM))
from pySim.exceptions import SwMatchError
from pySim.ts_102_221 import CardProfileUICC
import pysim_otaman_server.fastinit as fastinit
from pysim_otaman_server.fastinit import (
FastRuntimeState,
do_reset_fast,
pick_profile_no_reset,
)
class FakeScc:
def __init__(self):
self.sel_ctrl = '0004'
self.cla_byte = '00'
self.resets = 0
self.selected = []
def reset_card(self):
self.resets += 1
def select_file(self, fid):
self.selected.append(fid)
return ('', '9000')
def select_adf(self, aid):
raise SwMatchError('6a82', '9000')
class TestPickProfileNoReset(unittest.TestCase):
def test_uicc_selected_without_any_reset(self):
scc = FakeScc()
profile = pick_profile_no_reset(scc)
self.assertIsInstance(profile, CardProfileUICC)
self.assertEqual(scc.resets, 0)
self.assertIn('3f00', scc.selected)
def test_reset_card_restored_after_pick(self):
scc = FakeScc()
pick_profile_no_reset(scc)
scc.reset_card()
self.assertEqual(scc.resets, 1)
class FakeLchan:
def __init__(self):
self.scc = types.SimpleNamespace(scp=object())
self.selected_adf = 'SOMETHING'
self.selected = []
def select(self, path, cmd_app=None):
self.selected.append(path)
class TestFastRuntimeStateSoftReset(unittest.TestCase):
def make_rs(self):
rs = FastRuntimeState.__new__(FastRuntimeState)
rs.lchan = {0: FakeLchan(), 1: FakeLchan()}
rs.adm_verified = True
rs.card = types.SimpleNamespace(_scc=types.SimpleNamespace(get_atr=lambda: 'AABB'))
rs.identity = {}
return rs
def test_soft_reset_selects_mf_without_physical_reset(self):
rs = self.make_rs()
atr = rs.soft_reset()
self.assertEqual(atr, 'AABB')
self.assertEqual(rs.identity['ATR'], 'AABB')
self.assertEqual(rs.lchan[0].selected, ['MF'])
self.assertIsNone(rs.lchan[0].selected_adf)
self.assertFalse(rs.adm_verified)
self.assertNotIn(1, rs.lchan)
def test_reset_is_soft(self):
rs = self.make_rs()
rs.card = types.SimpleNamespace(_scc=types.SimpleNamespace(get_atr=lambda: 'EEFF'))
self.assertEqual(rs.reset(), 'EEFF')
self.assertEqual(rs.lchan[0].selected, ['MF'])
class FakeCardScc:
def __init__(self):
self.resets = 0
def reset_card(self):
self.resets += 1
return 'ATR'
def get_atr(self):
return 'AABB'
class TestDoResetFast(unittest.TestCase):
def test_explicit_reset_is_physical(self):
scc = FakeCardScc()
out = []
app = types.SimpleNamespace(rs=None, card=types.SimpleNamespace(_scc=scc), poutput=out.append)
do_reset_fast(app)
self.assertEqual(scc.resets, 1)
self.assertEqual(out, ['Card ATR: AABB'])
def test_explicit_reset_uses_hard_reset_with_runtime_state(self):
calls = []
rs = types.SimpleNamespace(hard_reset=lambda cmd_app=None: calls.append(cmd_app) or 'CCDD')
out = []
app = types.SimpleNamespace(rs=rs, card=None, poutput=out.append)
do_reset_fast(app)
self.assertEqual(calls, [app])
self.assertEqual(out, ['Card ATR: CCDD'])
if __name__ == '__main__':
unittest.main()
class FlakyLchan:
"""Lchan whose first MF select fails, as if probing left the card in a
context the software reset cannot clear."""
def __init__(self):
self.scc = types.SimpleNamespace(scp=object())
self.selected_adf = 'SOMETHING'
self.select_calls = 0
self.selected = []
def select(self, path, cmd_app=None):
self.select_calls += 1
if self.select_calls == 1:
raise SwMatchError('6d00', '9000')
self.selected.append(path)
class ResettableCard:
def __init__(self):
self.resets = 0
self._scc = types.SimpleNamespace(get_atr=lambda: 'AABB')
def reset(self):
self.resets += 1
return 'AABB'
class TestFastResetEscalation(unittest.TestCase):
def make_rs(self):
rs = FastRuntimeState.__new__(FastRuntimeState)
rs.lchan = {0: FlakyLchan(), 1: types.SimpleNamespace(scc=types.SimpleNamespace(scp=None))}
rs.adm_verified = True
rs.card = ResettableCard()
rs.identity = {}
return rs
def test_soft_reset_escalates_to_physical(self):
rs = self.make_rs()
atr = rs.reset()
self.assertEqual(atr, 'AABB')
self.assertEqual(rs.card.resets, 1)
self.assertEqual(rs.lchan[0].select_calls, 2)
self.assertEqual(rs.lchan[0].selected, ['MF'])
self.assertFalse(rs.adm_verified)
self.assertNotIn(1, rs.lchan)
class TestInitCardFastRetry(unittest.TestCase):
def test_retries_once_after_physical_reset(self):
calls = []
def once(sl, skip, wait):
calls.append(wait)
if len(calls) == 1:
raise SwMatchError('6d00', '9000')
return ('rs', 'card')
sl = types.SimpleNamespace(resets=0)
def reset_card():
sl.resets += 1
sl.reset_card = reset_card
with mock.patch.object(fastinit, '_init_card_once', side_effect=once):
rs, card = fastinit.init_card_fast(sl, wait=True)
self.assertEqual(calls, [True, False])
self.assertEqual(sl.resets, 1)
self.assertEqual((rs, card), ('rs', 'card'))
class TestDoEquipFastFailure(unittest.TestCase):
def test_failed_equip_keeps_previous_state(self):
calls = []
app = types.SimpleNamespace(
sl=object(),
rs=types.SimpleNamespace(profile=types.SimpleNamespace(shell_cmdsets=[object()])),
unregister_command_set=lambda cs: calls.append('unregister'),
equip=lambda card, rs: calls.append('equip'),
)
with mock.patch.object(fastinit, 'init_card_fast', side_effect=SwMatchError('6d00', '9000')):
with self.assertRaises(SwMatchError):
fastinit.do_equip_fast(app)
self.assertEqual(calls, [])
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
"""Tests for the paused-command (STK menu) timeout watchdog."""
import sys
import types
import unittest
from pathlib import Path
from unittest import mock
PROJECTS = Path(__file__).resolve().parents[2]
PY_SIM = PROJECTS / 'pysim'
if str(PY_SIM) not in sys.path:
sys.path.insert(0, str(PY_SIM))
import pysim_otaman_server.server as S
class TestMenuTimeout(unittest.TestCase):
def setUp(self):
self.saved = (S._MENU_TIMEOUT, S._MENU_TIMER)
def tearDown(self):
S._cancel_menu_timeout()
S._MENU_TIMEOUT, S._MENU_TIMER = self.saved
def test_clamped(self):
S._set_menu_timeout(30)
self.assertEqual(S._MENU_TIMEOUT, 30)
S._set_menu_timeout(0)
self.assertEqual(S._MENU_TIMEOUT, 0)
S._set_menu_timeout(-1)
self.assertEqual(S._MENU_TIMEOUT, 0)
S._set_menu_timeout(99999)
self.assertEqual(S._MENU_TIMEOUT, 3600)
def test_arm_starts_timer(self):
S._set_menu_timeout(30)
with mock.patch.object(S.threading, 'Timer') as timer:
S._arm_menu_timeout()
timer.assert_called_once_with(30, S._menu_timeout_fire)
def test_zero_disables_arming(self):
S._set_menu_timeout(0)
with mock.patch.object(S.threading, 'Timer') as timer:
S._arm_menu_timeout()
timer.assert_not_called()
def test_cancel(self):
timer = mock.Mock()
S._MENU_TIMER = timer
S._cancel_menu_timeout()
timer.cancel.assert_called_once()
self.assertIsNone(S._MENU_TIMER)
class TestMenuSendResponse(unittest.TestCase):
def test_timeout_tr_is_flat_with_general_result(self):
sent = []
def send_apdu(hexstr):
sent.append(hexstr)
return ('', '9000')
server = types.SimpleNamespace(
stk_pending={'type': 'display_text', 'cmd_num': 1, 'cmd_type': 0x21,
'dev_src': 0x81, 'dev_dst': 0x83},
menu_active=True,
scc=types.SimpleNamespace(cat_cla='80', _tp=types.SimpleNamespace(send_apdu=send_apdu)),
)
resp, code = S._menu_send_response(server, 'timeout', None)
self.assertEqual(code, 200)
self.assertEqual(resp['sw'], '9000')
self.assertEqual(resp['type'], 'done')
self.assertIsNone(server.stk_pending)
self.assertFalse(server.menu_active)
tr = sent[0]
self.assertTrue(tr.startswith('801400000d'), tr)
self.assertIn('8103012100', tr)
self.assertIn('82028381', tr)
self.assertIn('83021200', tr)
def test_no_pending_returns_400(self):
resp, code = S._menu_send_response(types.SimpleNamespace(stk_pending=None), 'ok')
self.assertEqual(code, 400)
self.assertIn('error', resp)
if __name__ == '__main__':
unittest.main()
class TestFinishPendingMenu(unittest.TestCase):
def make_server(self):
return types.SimpleNamespace(
stk_pending={'type': 'select_item', 'cmd_num': 1, 'cmd_type': 0x24,
'dev_src': 0x81, 'dev_dst': 0x83, 'items': []},
menu_active=True, scc=None)
def make_scc(self, sent, sw='9000'):
return types.SimpleNamespace(
cat_cla='80',
_tp=types.SimpleNamespace(send_apdu=lambda h: (sent.append(h) or ('', sw))))
def test_no_pending_is_noop(self):
sent = []
S._finish_pending_menu(types.SimpleNamespace(stk_pending=None), self.make_scc(sent))
self.assertEqual(sent, [])
def test_pending_finished_with_cancel_tr(self):
server = self.make_server()
sent = []
scc = self.make_scc(sent)
server.scc = scc
S._finish_pending_menu(server, scc)
self.assertEqual(len(sent), 1)
tr = sent[0]
self.assertTrue(tr.startswith('801400000d'), tr)
self.assertIn('83021000', tr) # general result 0x10 = cancel
self.assertIsNone(server.stk_pending)
self.assertFalse(server.menu_active)
def test_91xx_answer_drains_chain(self):
server = self.make_server()
scc = self.make_scc([], sw='9120')
server.scc = scc
with mock.patch.object(S, '_handle_proactive_chain') as chain:
S._finish_pending_menu(server, scc)
chain.assert_called_once_with(scc, '9120')
+647
View File
@@ -0,0 +1,647 @@
#!/usr/bin/env python3
"""Unit tests for the OTA helper functions in pysim_otaman_server.server.
Reference vectors are key-free: synthetic dummy keys plus the already-public
sysmocom sample-key vectors that ship in pySim's own tests/unittests/test_ota.py.
No live/sample card keys and no ICCIDs appear here.
"""
import sys
import unittest
from pathlib import Path
from unittest import mock
# pySim checkout is a sibling of this repo; put it on sys.path so the server
# module (which imports pySim at module level) can be exercised against it.
PROJECTS = Path(__file__).resolve().parents[2]
PY_SIM = PROJECTS / 'pysim'
if str(PY_SIM) not in sys.path:
sys.path.insert(0, str(PY_SIM))
from pysim_otaman_server.server import (
_build_sms_tpdu,
_build_tr,
_decode_cmd,
_decode_por,
_decode_tr,
_log_proactive,
_ota_reference,
_record_tr,
_spi_from_bytes,
_tr_data_only,
)
# Synthetic dummy key material (no real card keys).
K = '00112233445566778899AABBCCDDEEFF'
# Public sysmocom sample keys from pySim tests/unittests/test_ota.py.
KIC3 = 'C21DD66ACAC13CB3BC8B331B24AFB57B'
KID3 = '12110C78E678C25408233076AA033615'
# Public synthetic AES keys from pySim tests/unittests/test_ota.py.
KIC_AES = '200102030405060708090a0b0c0d0e0f'
KID_AES = '201102030405060708090a0b0c0d0e0f'
APDU = '00a40000023f00'
# (spi1, spi2) -> expected secured packet, generated with _ota_reference
# against pySim's OtaDialectSms.encode_cmd and cross-checked with the JS genSp().
REFERENCE_VECTORS = {
('06', '09'):
'00201506091515b00000c08f58c38860acb3a362fffe670ad13759a2a6b4c1a91116',
('16', '01'):
'00201516011515b00000e42573469e68a8462a57a505b0e2b1c09c1928c7a182311f',
('02', '09'):
'001d1502091515b0000000000000010085a8ca1a9828b0bb00a40000023f00',
}
# AES-128 reference vectors (public synthetic keys from pySim test_ota.py).
AES_APDU = '00a40004023f00'
AES_REFERENCE_VECTORS = {
('16', '19'):
'00281516192222b000115a47655527e96e832f1a5c698655715d4331454a0d83952c0ed35245706976b1',
('12', '09'):
'001d1512092222b0001100000000110029826122c7a0b79500a40004023f00',
('1e', '19'):
'0028151e192222b0001118b202ee47a3203e7370861c383b4142e704157b36e5c0eb4bb33eb6036cbaf8',
}
class TestSpiFromBytes(unittest.TestCase):
def test_06_09_ciphered_cc(self):
spi = _spi_from_bytes(0x06, 0x09)
self.assertEqual(spi, {
'counter': 'no_counter',
'ciphering': True,
'rc_cc_ds': 'cc',
'por_in_submit': False,
'por_shall_be_ciphered': False,
'por_rc_cc_ds': 'cc',
'por': 'por_required',
})
def test_16_01_counter_must_be_higher(self):
spi = _spi_from_bytes(0x16, 0x01)
self.assertEqual(spi['counter'], 'counter_must_be_higher')
self.assertTrue(spi['ciphering'])
self.assertEqual(spi['rc_cc_ds'], 'cc')
self.assertEqual(spi['por_rc_cc_ds'], 'no_rc_cc_ds')
def test_02_09_unciphered_cc(self):
spi = _spi_from_bytes(0x02, 0x09)
self.assertFalse(spi['ciphering'])
self.assertEqual(spi['rc_cc_ds'], 'cc')
self.assertEqual(spi['por_rc_cc_ds'], 'cc')
def test_04_19_ciphered_no_cc(self):
spi = _spi_from_bytes(0x04, 0x19)
self.assertTrue(spi['ciphering'])
self.assertEqual(spi['rc_cc_ds'], 'no_rc_cc_ds')
self.assertTrue(spi['por_shall_be_ciphered'])
self.assertEqual(spi['por_rc_cc_ds'], 'cc')
class TestBuildSmsTpdu(unittest.TestCase):
CHUNK = '00201506091515b00000c08f58c38860acb3a362fffe670ad13759a2a6b4c1a91116'
SCTS = bytes.fromhex('24051215173000')
def _build(self, *args, **kwargs):
with mock.patch('pysim_otaman_server.server._encode_scts', return_value=self.SCTS):
return _build_sms_tpdu(*args, **kwargs)
def test_single_message_with_cpi(self):
self.assertEqual(
self._build(self.CHUNK, include_cpi=True),
'4005812143f57ff6240512151730002502700000201506091515b00000'
'c08f58c38860acb3a362fffe670ad13759a2a6b4c1a91116')
def test_single_message_without_cpi(self):
self.assertEqual(
self._build(self.CHUNK, include_cpi=False),
'0405812143f57ff6240512151730002200201506091515b00000'
'c08f58c38860acb3a362fffe670ad13759a2a6b4c1a91116')
def test_first_chunk_has_cpi(self):
self.assertEqual(
self._build(self.CHUNK, chunk_total=3, chunk_num=1, include_cpi=True),
'4405812143f57ff6240512151730002a070003010301700000201506091515b00000'
'c08f58c38860acb3a362fffe670ad13759a2a6b4c1a91116')
def test_later_chunk_concat_only(self):
self.assertEqual(
self._build(self.CHUNK, chunk_total=3, chunk_num=2, include_cpi=True),
'4405812143f57ff6240512151730002805000301030200201506091515b00000'
'c08f58c38860acb3a362fffe670ad13759a2a6b4c1a91116')
class TestOtaReference(unittest.TestCase):
def test_ciphered_spi_06_09(self):
out, _ = _ota_reference('06', '09', '15', '15', 'b00000', '0000000001', APDU, K, K)
self.assertEqual(out, REFERENCE_VECTORS[('06', '09')])
def test_ciphered_spi_16_01(self):
out, _ = _ota_reference('16', '01', '15', '15', 'b00000', '0000000001', APDU, K, K)
self.assertEqual(out, REFERENCE_VECTORS[('16', '01')])
def test_unciphered_spi_02_09(self):
out, _ = _ota_reference('02', '09', '15', '15', 'b00000', '0000000001', APDU, K, K)
self.assertEqual(out, REFERENCE_VECTORS[('02', '09')])
def test_unciphered_cpl_is_0x001d(self):
# Regression: CPL counts octets from the CHL octet to the last octet
# of the secured data (29 here), it must NOT be len(out)-2 (27/0x001b).
out, _ = _ota_reference('02', '09', '15', '15', 'b00000', '0000000001', APDU, K, K)
self.assertEqual(out[:4], '001d')
self.assertEqual(len(out) // 2, 31)
def test_sysmocom_reference_vector(self):
# Public vector from pySim tests/unittests/test_ota.py (test_cmd_3des_ciphered).
out, _ = _ota_reference('04', '19', '35', '35', 'b00000', '0000000000', APDU, KIC3, KID3)
self.assertEqual(out, '00180d04193535b00000e3ec80a849b554421276af3883927c20')
def test_returns_spi_dict(self):
_, spi = _ota_reference('16', '01', '15', '15', 'b00000', '0000000001', APDU, K, K)
self.assertEqual(spi['counter'], 'counter_must_be_higher')
self.assertTrue(spi['ciphering'])
def test_aes128_ciphered_cc(self):
out, _ = _ota_reference('16', '19', '22', '22', 'b00011', '0000000011', AES_APDU, KIC_AES, KID_AES)
self.assertEqual(out, AES_REFERENCE_VECTORS[('16', '19')])
def test_aes128_unciphered_cc(self):
out, _ = _ota_reference('12', '09', '22', '22', 'b00011', '0000000011', AES_APDU, KIC_AES, KID_AES)
self.assertEqual(out, AES_REFERENCE_VECTORS[('12', '09')])
def test_aes128_counter_plus_one(self):
out, spi = _ota_reference('1e', '19', '22', '22', 'b00011', '0000000011', AES_APDU, KIC_AES, KID_AES)
self.assertEqual(out, AES_REFERENCE_VECTORS[('1e', '19')])
self.assertEqual(spi['counter'], 'counter_must_be_lower')
class TestDecodePor(unittest.TestCase):
def test_plaintext_no_cc_synthetic(self):
r = _decode_por('02', '01', '15', '15', '0000000001', K, K,
'027100000e0ab0000000000000010000016e00')
self.assertEqual(r['response_status'], 'por_ok')
self.assertEqual(r['tar'], 'B00000')
self.assertEqual(r['decoded']['last_status_word'], '6e00')
def test_sysmocom_signed(self):
r = _decode_por('06', '09', '35', '35', '0000000001', KIC3, KID3,
'027100001612b000110000000000000055f47118381175fb01612f')
self.assertEqual(r['response_status'], 'por_ok')
self.assertEqual(r['decoded']['last_status_word'], '612f')
def test_sysmocom_ciphered(self):
r = _decode_por('06', '19', '35', '35', '0000000001', KIC3, KID3,
'027100001c12b000119660ebdb81be189b5e4389e9e7ab2bc0954f963ad869ed7c')
self.assertEqual(r['response_status'], 'por_ok')
self.assertEqual(r['decoded']['last_status_word'], '612f')
def test_sysmocom_no_cc(self):
r = _decode_por('06', '01', '35', '35', '0000000001', KIC3, KID3,
'027100000e0ab000110000000000000001612f')
self.assertEqual(r['response_status'], 'por_ok')
self.assertEqual(r['decoded']['last_status_word'], '612f')
def test_complete_field_report(self):
"""All parsed PoR fields are surfaced verbatim (v1.9.4)."""
raw = '027100000e0ab000110000000000000001612f'
r = _decode_por('06', '01', '35', '35', '0000000001', KIC3, KID3, raw)
self.assertEqual(r['response_status'], 'por_ok')
self.assertEqual(r['tar'], 'B00011')
self.assertEqual(r['cntr'], '0000000000')
self.assertEqual(r['pcntr'], 0)
self.assertEqual(r['rpl'], 14)
self.assertEqual(r['rhl'], 10)
self.assertEqual(r['cc_rc'], '')
self.assertEqual(r['raw'], raw)
self.assertNotIn('cntr_low', str(r))
def test_cntr_low_fields(self):
r = _decode_por('02', '01', '15', '15', '0000000001', K, K,
'027100000b0ab0000000000000070002')
self.assertEqual(r['response_status'], 'cntr_low')
self.assertEqual(r['tar'], 'B00000')
self.assertEqual(r['cntr'], '0000000007')
self.assertEqual(r['rpl'], 11)
self.assertEqual(r['rhl'], 10)
self.assertIsNone(r.get('decoded'))
def test_sysmocom_bad_cc_returns_none(self):
r = _decode_por('06', '09', '35', '35', '0000000001', KIC3, KID3,
'027100001612b000110000000000000055f47118381175fb02612f')
self.assertIsNone(r)
def test_aes128_ciphered(self):
r = _decode_por('06', '19', '22', '22', '0000000001', KIC_AES, KID_AES,
'027100002412b00011ebc6b497e2cad7aedf36ace0e3a29b38853f0fe9ccde81913be5702b73abce1f')
self.assertEqual(r['response_status'], 'por_ok')
self.assertEqual(r['decoded']['last_status_word'], '6132')
def test_malformed_returns_none(self):
for bad in ['', '00', '00027100000e0a', '027100000e0ab00000', 'garbage', 'zz']:
self.assertIsNone(
_decode_por('02', '01', '15', '15', '0000000001', K, K, bad),
msg='expected None for %r' % bad)
class TestProactiveDecode(unittest.TestCase):
"""Server-side proactive command/TR decode helpers (v1.8.0 log feature)."""
def setUp(self):
import pysim_otaman_server.server as srv
srv._PROACTIVE_SESSION_START = 1234.0
srv._PLI_DATA[0x00] = '93055210011000'
def test_decode_cmd_poll_interval(self):
r = _decode_cmd(0x03, bytes.fromhex('d00d8103010300820283818402011e'), None)
self.assertEqual(r, [{'label': 'Interval', 'value': '30 s'}])
def test_decode_cmd_setup_event_list(self):
r = _decode_cmd(0x05, bytes.fromhex('d00c810301050082028381990101'), None)
self.assertEqual(r, [{'label': 'Events', 'value': 'Call connected'}])
def test_decode_cmd_send_short_message(self):
r = _decode_cmd(0x13, bytes.fromhex('d0158103011300820283818b0b916106152670f900a35f020101'), None)
self.assertEqual(r, [{'label': 'SMS TPDU', 'value': '916106152670f900a35f02'}])
def test_decode_cmd_pli_qualifier_name(self):
r = _decode_cmd(0x26, b'\xd0', 0x00)
self.assertTrue(r[0]['value'].startswith('Location Information (MCC, MNC, LAC/TAC, Cell ID)'))
def test_decode_cmd_empty_raw(self):
self.assertEqual(_decode_cmd(0x26, b'', None), [])
self.assertEqual(_decode_cmd(0x03, None, None), [])
def test_decode_tr_pli_location(self):
r = _decode_tr('26', '00', '93055210011000')
self.assertEqual(r, [
{'label': 'MCC', 'value': '250'},
{'label': 'MNC', 'value': '11'},
{'label': 'LAC/TAC', 'value': '1000'},
])
def test_decode_tr_pli_imei(self):
r = _decode_tr('26', '01', '94082143658709214305')
self.assertEqual(r[0], {'label': 'IMEI', 'value': '123456789012345'})
def test_decode_tr_pli_access_technology(self):
r = _decode_tr('26', '06', 'bf0103')
self.assertEqual(r, [{'label': 'Access Technology', 'value': 'UTRAN (3)'}])
def test_decode_tr_pli_search_mode(self):
r = _decode_tr('26', '09', 'ad0101')
self.assertEqual(r, [{'label': 'Search Mode', 'value': 'Manual'}])
def test_decode_tr_poll_interval(self):
r = _decode_tr('03', None, '8402011e')
self.assertEqual(r, [{'label': 'Interval', 'value': '30 s'}])
def test_decode_tr_empty(self):
self.assertEqual(_decode_tr('26', '00', ''), [])
def test_tr_data_only_strips_boilerplate(self):
tr = bytes.fromhex('81030326008202818393055210011000030100')
self.assertEqual(_tr_data_only(tr).hex(), '93055210011000')
def test_build_and_record_tr(self):
entry = {'type_hex': '26', 'qualifier': '00'}
tr = _build_tr(None, 3, 0x26, 0x83, 0x81, 0x00)
_record_tr(entry, tr, '9000')
self.assertEqual(entry['tr_hex'], '93055210011000')
self.assertEqual(entry['tr_sw'], '9000')
self.assertEqual([f['label'] for f in entry['tr_decoded']], ['MCC', 'MNC', 'LAC/TAC'])
def test_record_tr_without_sw(self):
entry = {'type_hex': '26', 'qualifier': '00'}
_record_tr(entry, bytes.fromhex('93055210011000'))
self.assertNotIn('tr_sw', entry)
self.assertEqual(entry['tr_hex'], '93055210011000')
def test_log_proactive_fields(self):
entry = _log_proactive(0x26, b'\xd0', 0x00, 7)
self.assertEqual(entry['cmd_num'], 7)
self.assertEqual(entry['type_hex'], '26')
self.assertEqual(entry['qualifier'], '00')
self.assertEqual(entry['raw'], 'd0')
self.assertEqual(entry['cmd_decoded'][0]['label'], 'Qualifier')
self.assertIn('id', entry)
def test_log_proactive_malformed_raw(self):
entry = _log_proactive(0x21, bytes([0xD0, 0xFF]), 0x00)
self.assertEqual(entry['cmd_decoded'], [])
def test_log_proactive_no_cmd_num(self):
entry = _log_proactive(0x05, bytes.fromhex('990101'), None)
self.assertNotIn('cmd_num', entry)
def test_record_tr_basic_result(self):
entry = {'type_hex': '03', 'qualifier': None}
_record_tr(entry, bytes.fromhex('8103010300820281838402011e030100'))
self.assertEqual(entry['tr_result'], '00')
self.assertEqual(entry['tr_result_name'], 'Command performed successfully')
self.assertEqual(entry['tr_hex'], '8402011e')
def test_record_tr_general_result(self):
entry = {'type_hex': '26', 'qualifier': '00'}
_record_tr(entry, bytes.fromhex('8103032600820281839305521001100083022001'))
self.assertEqual(entry['tr_result'], '2001')
self.assertEqual(entry['tr_result_name'], 'ME currently unable to process command')
self.assertEqual(entry['tr_hex'], '93055210011000')
def test_record_tr_unknown_result(self):
entry = {'type_hex': '26', 'qualifier': '00'}
_record_tr(entry, bytes.fromhex('810303260082028181030107'))
self.assertEqual(entry['tr_result'], '07')
self.assertNotIn('tr_result_name', entry)
def test_record_tr_no_result_tlv(self):
entry = {'type_hex': '26', 'qualifier': '00'}
_record_tr(entry, bytes.fromhex('810303260082028181'))
self.assertNotIn('tr_result', entry)
class TestExpandedRemoteResponse(unittest.TestCase):
"""Expanded Remote Response parsing (TS 102 226 §5.2.2)."""
def test_expanded_response_single_command(self):
entry = {'type_hex': '03', 'qualifier': None}
_record_tr(entry, bytes.fromhex('810301030082028183030100'))
self.assertEqual(entry['tr_result'], '00')
self.assertEqual(entry['tr_result_name'], 'Command performed successfully')
def test_expanded_response_parser_single_command(self):
# Simple test of the construct parsing structure
try:
from construct import Struct, Int8ub, Bytes, GreedyBytes, Optional, Array, this
from osmocom.utils import b2h
# Create sample expanded response data
secured_data = bytes.fromhex('01' '01' '9000' '11') # response_count, cmd#, SW, data
ExpandedRemoteResponse = Struct(
'response_count'/Int8ub,
'responses'/Array(this.response_count, Struct(
'command_number'/Int8ub,
'status_word'/Bytes(2),
'response_data'/GreedyBytes,
'error_details'/Optional(Struct(
'error_code'/Int8ub,
'error_info'/GreedyBytes
)),
'chaining_context'/Optional(Struct(
'script_id'/Bytes(4),
'is_first'/Int8ub,
'is_last'/Int8ub,
))
))
)
expanded = ExpandedRemoteResponse.parse(secured_data)
self.assertEqual(expanded.response_count, 1)
self.assertEqual(expanded.responses[0].command_number, 1)
self.assertEqual(b2h(expanded.responses[0].status_word).upper(), '9000')
self.assertEqual(b2h(expanded.responses[0].response_data).upper(), '11')
except Exception as e:
self.fail(f"ExpandedRemoteResponse parsing failed: {e}")
def test_expanded_response_with_error(self):
entry = {'type_hex': '26', 'qualifier': '01'}
# Result TLV: 03 01 6A (error_code 0x6A)
_record_tr(entry, bytes.fromhex('81030326008202818303016A'))
self.assertEqual(entry['tr_result'], '6a')
self.assertEqual(entry['tr_result_name'], 'Command performed with limited understanding')
def test_expanded_response_with_chaining(self):
entry = {'type_hex': '26', 'qualifier': '00'}
# Result: 03 01 00 + chaining context with script_id
_record_tr(entry, bytes.fromhex('81030326008202818303010093'))
self.assertEqual(entry['tr_result'], '00')
self.assertEqual(entry['tr_result_name'], 'Command performed successfully')
class TestSmsConcat(unittest.TestCase):
"""Tests for _parse_sms_concat — SMS UDH concatenation parsing."""
def test_no_udh_sms_submit(self):
"""SMS-SUBMIT without TP-UDHI: entire UD is payload."""
from pysim_otaman_server.server import _parse_sms_concat
# First octet 0x01: MTI=01 (SUBMIT), no UDH, no VP
# MR=00, DA_len=05, DA_type=90, DA=2143F5, PID=00, DCS=04, UDL=03, UD=AABBCC
tpdu = bytes.fromhex('0100' # first octet + MR
'05' # DA length
'90' # DA type
'2143F5' # DA data (3 bytes for 5 digits)
'0004' # PID + DCS
'03' # UDL
'AABBCC') # UD (payload)
ref, total, num, payload = _parse_sms_concat(tpdu)
self.assertIsNone(ref)
self.assertIsNone(total)
self.assertIsNone(num)
self.assertEqual(payload.hex(), 'aabbcc')
def test_8bit_concat_iei_0x00(self):
"""SMS-SUBMIT with IEI 0x00 (8-bit reference concatenation)."""
from pysim_otaman_server.server import _parse_sms_concat
# First octet 0x41: MTI=01 (SUBMIT), TP-UDHI=1, no VP
# MR=00, DA_len=05, DA_type=90, DA=2143F5, PID=00, DCS=04
# UDL=09, UDHL=05, UDH: 00 03 04 04 01 (concat IE), payload=AABBCC
tpdu = bytes.fromhex('4100' # first octet + MR
'05' # DA length
'90' # DA type
'2143F5' # DA data
'0004' # PID + DCS
'09' # UDL (1 UDHL + 5 UDH + 3 payload = 9)
'05' # UDHL = 5 bytes of UDH
'0003' # IEI=0x00, IEDL=3
'04' # ref
'04' # total (4 segments)
'01' # num (segment 1)
'AABBCC') # payload
ref, total, num, payload = _parse_sms_concat(tpdu)
self.assertEqual(ref, 0x04)
self.assertEqual(total, 4)
self.assertEqual(num, 1)
self.assertEqual(payload.hex(), 'aabbcc')
def test_16bit_concat_iei_0x08(self):
"""SMS-SUBMIT with IEI 0x08 (16-bit reference concatenation)."""
from pysim_otaman_server.server import _parse_sms_concat
# First octet 0x41: MTI=01, TP-UDHI=1
# UDH: 06 (UDHL) 08 04 01 02 03 04 (16-bit concat: ref=0x0102, total=3, num=4)
# payload=FF
tpdu = bytes.fromhex('4100'
'05'
'90'
'2143F5'
'0004'
'08' # UDL (1 UDHL + 6 UDH + 1 payload = 8)
'06' # UDHL
'0804' # IEI=0x08, IEDL=4
'0102' # ref (16-bit, big-endian)
'03' # total
'04' # num
'FF') # payload
ref, total, num, payload = _parse_sms_concat(tpdu)
self.assertEqual(ref, 0x0102)
self.assertEqual(total, 3)
self.assertEqual(num, 4)
self.assertEqual(payload.hex(), 'ff')
def test_udh_with_cpi(self):
"""UDH with concatenation IE + CPI IE (0x70)."""
from pysim_otaman_server.server import _parse_sms_concat
# First octet 0x41: MTI=01, TP-UDHI=1
# UDHL=07, UDH: 00 03 04 04 01 (concat) + 70 00 (CPI)
tpdu = bytes.fromhex('4100'
'05'
'90'
'2143F5'
'0004'
'0A' # UDL (1 UDHL + 7 UDH + 1 payload = 9? no: 1+5+2+1=9, but UDH=7 bytes)
'07' # UDHL = 7
'0003' # IEI=0x00, IEDL=3
'04' # ref
'04' # total
'01' # num
'7000' # CPI IE (IEI=0x70, IEDL=0)
'DD') # payload
ref, total, num, payload = _parse_sms_concat(tpdu)
self.assertEqual(ref, 0x04)
self.assertEqual(total, 4)
self.assertEqual(num, 1)
self.assertEqual(payload.hex(), 'dd')
def test_empty_payload(self):
"""Segment with empty payload after UDH."""
from pysim_otaman_server.server import _parse_sms_concat
# First octet 0x41: MTI=01, TP-UDHI=1
tpdu = bytes.fromhex('4100'
'05'
'90'
'2143F5'
'0004'
'06' # UDL (1 UDHL + 5 UDH + 0 payload = 6)
'05' # UDHL
'0003'
'01'
'02'
'01') # no payload after UDH
ref, total, num, payload = _parse_sms_concat(tpdu)
self.assertEqual(ref, 0x01)
self.assertEqual(total, 2)
self.assertEqual(num, 1)
self.assertEqual(len(payload), 0)
def test_short_tpdu(self):
"""Truncated TPDU returns gracefully."""
from pysim_otaman_server.server import _parse_sms_concat
ref, total, num, payload = _parse_sms_concat(b'\x01')
self.assertIsNone(ref)
self.assertIsNone(total)
self.assertIsNone(num)
def test_none_input(self):
"""None input returns empty payload."""
from pysim_otaman_server.server import _parse_sms_concat
ref, total, num, payload = _parse_sms_concat(None)
self.assertIsNone(ref)
self.assertIsNone(total)
self.assertIsNone(num)
self.assertEqual(len(payload), 0)
def test_short_tpdu(self):
"""Truncated TPDU returns gracefully."""
from pysim_otaman_server.server import _parse_sms_concat
ref, total, num, payload = _parse_sms_concat(b'\x44')
self.assertIsNone(ref)
self.assertIsNone(total)
self.assertIsNone(num)
def test_none_input(self):
"""None input returns empty payload."""
from pysim_otaman_server.server import _parse_sms_concat
ref, total, num, payload = _parse_sms_concat(None)
self.assertIsNone(ref)
self.assertIsNone(total)
self.assertIsNone(num)
self.assertEqual(len(payload), 0)
class TestSmsReassembly(unittest.TestCase):
"""Tests for SMS segment reassembly logic."""
def test_single_segment_no_concat(self):
"""Single segment without UDH → submit_tpdu_hex is set directly."""
from pysim_otaman_server.server import PoRSubmitHandler, _find_sms_tpdu, _parse_sms_concat
handler = PoRSubmitHandler()
# Build a simple D0 with tag 8B containing an SMS-SUBMIT without UDH
sms_tpdu = bytes.fromhex('040005902143F50004' # SMS-SUBMIT header
'03' # UDL
'AABBCC') # payload
# Wrap in D0 proactive command
d0 = bytes([0xD0, len(sms_tpdu) + 4, # approximate BER length
0x81, 0x03, 0x01, 0x13, 0x00, # Command Details
0x82, 0x02, 0x81, 0x83, # Device Identities
0x8B, len(sms_tpdu)]) # tag 8B
# Simulate _find_sms_tpdu extracting tag 8B
found = sms_tpdu.hex()
# Parse and check
ref, total, num, payload = _parse_sms_concat(sms_tpdu)
self.assertIsNone(ref)
handler.submit_tpdu_hex = found # single segment path
self.assertEqual(handler.submit_tpdu_hex, found)
def test_multi_segment_reassembly(self):
"""3 segments with IEI 0x00 in random order → assembled in correct order."""
from pysim_otaman_server.server import PoRSubmitHandler
handler = PoRSubmitHandler()
# Segment payloads (after UDH)
payloads = [b'\x01\x02', b'\x03\x04', b'\x05\x06']
ref = 0x42
total = 3
# Simulate receiving segments in random order: 2, 0, 1
for idx in [1, 0, 2]:
num = idx + 1
handler.sms_segments.append((ref, total, num, payloads[idx].hex()))
# Check if all segments collected
matching = [s for s in handler.sms_segments if s[0] == ref]
if len(matching) >= total:
sorted_segs = sorted(matching, key=lambda s: s[2])
assembled = b''.join(bytes.fromhex(s[3]) for s in sorted_segs)
handler.submit_tpdu_hex = assembled.hex()
self.assertEqual(handler.submit_tpdu_hex, '010203040506')
def test_independent_references(self):
"""Two different reference numbers are independent."""
from pysim_otaman_server.server import PoRSubmitHandler
handler = PoRSubmitHandler()
# Ref 0x01: 2 segments
handler.sms_segments.append((0x01, 2, 1, 'AA'))
handler.sms_segments.append((0x01, 2, 2, 'BB'))
matching = [s for s in handler.sms_segments if s[0] == 0x01]
if len(matching) >= 2:
sorted_segs = sorted(matching, key=lambda s: s[2])
assembled = b''.join(bytes.fromhex(s[3]) for s in sorted_segs)
handler.submit_tpdu_hex = assembled.hex()
self.assertEqual(handler.submit_tpdu_hex, 'aabb')
# Ref 0x02: 1 segment (independent)
handler.sms_segments.append((0x02, 1, 1, 'CC'))
matching2 = [s for s in handler.sms_segments if s[0] == 0x02]
if len(matching2) >= 1:
sorted_segs2 = sorted(matching2, key=lambda s: s[2])
assembled2 = b''.join(bytes.fromhex(s[3]) for s in sorted_segs2)
# Only update if ref 0x02 is complete
handler.submit_tpdu_hex = assembled2.hex()
# Last assembly was ref 0x02
self.assertEqual(handler.submit_tpdu_hex, 'cc')
if __name__ == '__main__':
unittest.main()
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""Tests for the background STATUS polling interval semantics."""
import sys
import unittest
from pathlib import Path
from unittest import mock
PROJECTS = Path(__file__).resolve().parents[2]
PY_SIM = PROJECTS / 'pysim'
if str(PY_SIM) not in sys.path:
sys.path.insert(0, str(PY_SIM))
import pysim_otaman_server.server as S
class TestPollInterval(unittest.TestCase):
def setUp(self):
self.saved = (S._POLL_ENABLED, S._POLL_INTERVAL, S._POLL_TIMER)
def tearDown(self):
S._poll_disable()
S._POLL_ENABLED, S._POLL_INTERVAL, S._POLL_TIMER = self.saved
def test_zero_interval_disables_polling(self):
S._set_poll_interval(0)
self.assertEqual(S._POLL_INTERVAL, 0)
with mock.patch.object(S.threading, 'Timer') as timer:
S._poll_enable()
timer.assert_not_called()
self.assertFalse(S._POLL_ENABLED)
self.assertIsNone(S._POLL_TIMER)
def test_negative_interval_clamped_to_zero(self):
S._set_poll_interval(-5)
self.assertEqual(S._POLL_INTERVAL, 0)
def test_positive_interval_starts_timer(self):
S._set_poll_interval(30)
with mock.patch.object(S.threading, 'Timer') as timer:
S._poll_enable()
self.assertTrue(S._POLL_ENABLED)
timer.assert_called_once_with(30, S._do_status_poll)
def test_reset_timer_skipped_when_disabled(self):
S._set_poll_interval(0)
S._POLL_ENABLED = True
with mock.patch.object(S.threading, 'Timer') as timer:
S._reset_poll_timer()
timer.assert_not_called()
self.assertIsNone(S._POLL_TIMER)
if __name__ == '__main__':
unittest.main()
+183
View File
@@ -0,0 +1,183 @@
#!/usr/bin/env python3
"""Unit tests for the parent-scoped select helpers in pysim_otaman_server.server.
The helpers must resolve every model-known file strictly within the requested
parent (no pySim global selectables, no probe_file model injection) and must
detach any model-unknown file that had to be probed for a custom file.
"""
import sys
import unittest
from pathlib import Path
from types import SimpleNamespace
PROJECTS = Path(__file__).resolve().parents[2]
PY_SIM = PROJECTS / 'pysim'
if str(PY_SIM) not in sys.path:
sys.path.insert(0, str(PY_SIM))
from pysim_otaman_server.server import (
_app_by_sel,
_fid4,
_file_by_sel,
_find_in_tree,
_select_path,
_select_with_parent,
)
class FakeFile:
def __init__(self, fid=None, name=None, parent=None, aid=None):
self.fid = fid
self.name = name
self.parent = parent
self.aid = aid
self.sfid = None
self.children = {}
def add_files(self, files):
for f in files:
f.parent = self
self.children[f.fid] = f
class FakeMF(FakeFile):
def __init__(self):
super().__init__(fid='3f00', name='MF')
self.applications = {}
class FakeLchan:
"""No .select() on purpose: any global-resolution call would fail loudly."""
def __init__(self, mf):
self.selected_file = mf
self.selects = []
self.probes = []
def select_file(self, f, app=None):
self.selected_file = f
self.selects.append(f)
def probe_file(self, fid, app=None):
self.probes.append(fid)
f = FakeFile(fid=fid, name='EF.' + fid.upper(), parent=self.selected_file)
self.selected_file.add_files([f])
self.selected_file = f
def build_model():
mf = FakeMF()
gsm = FakeFile('7f20', 'DF.GSM', mf)
mf.children['7f20'] = gsm
spn = FakeFile('6f46', 'EF.SPN', gsm)
gsm.children['6f46'] = spn
telecom = FakeFile('7f10', 'DF.TELECOM', mf)
mf.children['7f10'] = telecom
tel_ph = FakeFile('5f3a', 'DF.PHONEBOOK', telecom)
telecom.children['5f3a'] = tel_ph
usim = FakeFile(None, 'ADF.USIM', mf, aid='A0000000871002')
mf.applications['a0000000871002'] = usim
imsi = FakeFile('6f07', 'EF.IMSI', usim)
usim.children['6f07'] = imsi
usim_ph = FakeFile('5f3a', 'DF.PHONEBOOK', usim)
usim.children['5f3a'] = usim_ph
return mf, usim, usim_ph, telecom, tel_ph, gsm, spn
def setup():
mf, usim, usim_ph, telecom, tel_ph, gsm, spn = build_model()
app = SimpleNamespace(rs=SimpleNamespace(mf=mf))
lchan = FakeLchan(mf)
return app, lchan, mf, usim, usim_ph, gsm, spn
class FidHelpersTest(unittest.TestCase):
def test_fid4(self):
self.assertTrue(_fid4('6F07'))
self.assertFalse(_fid4('EF.IMSI'))
self.assertFalse(_fid4('6F0'))
def test_file_by_sel_matches_fid_and_name(self):
_, _, _, _, _, gsm, spn = setup()
self.assertIs(_file_by_sel(gsm, '6f46'), spn)
self.assertIs(_file_by_sel(gsm, 'EF.SPN'), spn)
self.assertIsNone(_file_by_sel(gsm, '6f07'))
def test_find_in_tree_reports_duplicates(self):
app, _, mf, _, _, _, _ = setup()
self.assertEqual(len(_find_in_tree(mf, '5f3a')), 2)
self.assertEqual(len(_find_in_tree(mf, 'EF.IMSI')), 1)
class ParentScopedSelectTest(unittest.TestCase):
def test_duplicate_fid_is_resolved_under_the_walked_parent(self):
app, lchan, mf, usim, usim_ph, _, _ = setup()
target, cleanup = _select_with_parent(lchan, '5f3a', None, app, parent_path=['MF', 'A0000000871002'])
self.assertIs(target, usim_ph)
self.assertIsNone(cleanup)
self.assertEqual(lchan.selects, [mf, usim, usim_ph])
self.assertEqual(lchan.probes, [])
def test_path_with_fids_selects_exactly(self):
app, lchan, mf, _, _, gsm, spn = setup()
target, cleanup = _select_path(lchan, 'MF/7F20/6F46', app)
self.assertIs(target, spn)
self.assertIsNone(cleanup)
self.assertEqual(lchan.selects, [mf, gsm, spn])
def test_path_with_aid_root_selects_application(self):
app, lchan, _, usim, _, _, _ = setup()
target, _ = _select_path(lchan, 'A0000000871002/6F07', app)
self.assertEqual(target.fid, '6f07')
self.assertEqual(lchan.selected_file.parent, usim)
def test_ambiguous_legacy_parent_selector_is_rejected(self):
app, lchan, _, _, _, _, _ = setup()
with self.assertRaisesRegex(RuntimeError, 'Ambiguous'):
_select_with_parent(lchan, '6f07', '5f3a', app)
def test_unknown_name_is_not_probed(self):
app, lchan, _, _, _, gsm, _ = setup()
with self.assertRaisesRegex(RuntimeError, 'File not found'):
_select_with_parent(lchan, 'NOSUCH', None, app, parent_path=['MF', '7F20'])
self.assertEqual(lchan.probes, [])
def test_unknown_fid_without_allow_probe_does_not_touch_the_card(self):
app, lchan, _, _, _, gsm, _ = setup()
with self.assertRaisesRegex(RuntimeError, 'File not found'):
_select_with_parent(lchan, '6f99', None, app, parent_path=['MF', '7F20'])
self.assertEqual(lchan.probes, [])
def test_allow_probe_detaches_the_temporary_file_and_restores_selection(self):
app, lchan, mf, _, _, gsm, _ = setup()
before = set(gsm.children)
target, cleanup = _select_with_parent(lchan, '6f99', None, app, parent_path=['MF', '7F20'], allow_probe=True)
self.assertEqual(lchan.probes, ['6f99'])
self.assertEqual(target.fid, '6f99')
self.assertIsNotNone(cleanup)
self.assertIn('6f99', gsm.children)
cleanup()
self.assertEqual(set(gsm.children), before)
self.assertIs(lchan.selected_file, mf)
def test_custom_path_segments_are_probed_and_detached(self):
app, lchan, mf, _, _, gsm, _ = setup()
before = set(gsm.children)
target, cleanup = _select_path(lchan, 'MF/7F20/A0B1/6F01', app)
self.assertEqual(lchan.probes, ['a0b1', '6f01'])
self.assertEqual(target.fid, '6f01')
cleanup()
self.assertEqual(set(gsm.children), before)
self.assertIs(lchan.selected_file, mf)
def test_model_known_selection_never_mutates_the_tree(self):
app, lchan, _, _, _, gsm, spn = setup()
before = {id(k): k for k in gsm.children}
_select_with_parent(lchan, '6f46', None, app, parent_path=['MF', '7F20'])
self.assertEqual({id(k): k for k in gsm.children}, before)
self.assertEqual(lchan.probes, [])
if __name__ == '__main__':
unittest.main()